Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ All notable changes to Agent Relay will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [Unreleased - Minor]

### Added

- `agent-relay fleet spawn --sandbox` (unpinned or `--sandbox-provider agent37`) prepares the sandbox through Cloud's durable `async-v1` preparation: it prints each preparation phase and polls by the exact sandbox identity instead of holding one long request open, so a slow Relayfile initial sync no longer ends as an unknown outcome. Older Cloud deployments keep working through the synchronous response.
- `@agent-relay/cloud` `ensureCloudFleetSandbox` accepts `preparationMode: 'async-v1'` and an `onPreparationProgress` callback; `CloudFleetSandboxProvisionError` exposes `sandboxAbsent` and a typed `preparationFailure` (`code`, `phase`, `causeStage`).

### Fixed

- When Cloud ends a sandbox preparation (for example `relayfile_mount_failed` / `initial_sync_deadline`), `fleet spawn --sandbox` names the cause and confirms no sandbox was left running, without a redundant cleanup call or unknown-outcome guidance.
- A lost Cloud response during sandbox preparation is reconciled through read-only status for the same sandbox identity; the CLI never resubmits the spawn or a preparation step blindly.
- `fleet spawn --sandbox` prints the generated sandbox identity before provisioning; re-running an interrupted spawn with `--sandbox-id <id>` resumes that sandbox instead of creating a second one.
- Against a Cloud that does not support async preparation, a typed failure such as `relayfile_mount_failed` or a capacity rejection is reported at once with its cause instead of after a status-polling wait.

## [13.1.5] - 2026-10-08

Expand Down
11 changes: 8 additions & 3 deletions packages/cli/src/cli/commands/fleet-sandbox-regression.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,10 @@ describe('fleet sandbox command regressions', () => {

expect(resolveRepository).toHaveBeenCalledWith(outsideGit, undefined, undefined, 'strict');
expect(materialize).not.toHaveBeenCalled();
expect(ensure).toHaveBeenCalledWith(expect.objectContaining({ mountRelayfile: true }));
expect(ensure).toHaveBeenCalledWith(
expect.objectContaining({ mountRelayfile: true }),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
expect((ensure.mock.calls[0]?.[0] as Record<string, unknown>).relayfilePaths).toBeUndefined();
expect(placement).toHaveBeenCalledWith(
expect.objectContaining({ input: expect.objectContaining({ worker_cwd: '/workspace' }) })
Expand Down Expand Up @@ -257,7 +260,8 @@ describe('fleet sandbox command regressions', () => {
sandboxId: REPLAY_SANDBOX_ID,
name: REPLAY_SANDBOX_NAME,
mountRelayfile: true,
})
}),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
const ensureInput = ensure.mock.calls[0]?.[0] as Record<string, unknown>;
expect(ensureInput.repos).toBeUndefined();
Expand Down Expand Up @@ -324,7 +328,8 @@ describe('fleet sandbox command regressions', () => {
expect.objectContaining({
repos: [selection.repository],
repoRevisions: { [selection.repository]: REVISION },
})
}),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
});
});
136 changes: 127 additions & 9 deletions packages/cli/src/cli/commands/fleet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,7 @@ describe('fleet command support', () => {
expect(ensureInput).toEqual({
workspaceId: 'rw_abc',
requiredCapability: 'spawn:codex',
preparationMode: 'async-v1',
maxAgents: 1,
mountRelayfile: true,
relayfilePaths: ['/live-review/run-123/**'],
Expand Down Expand Up @@ -1264,6 +1265,7 @@ describe('fleet command support', () => {
workerCwd: '/srv/agent-workforce/cloud/packages/web',
};
const events: string[] = [];
const warnings: string[] = [];
const materializeCloudRelayfileRepository = vi.fn(async () => {
events.push('materialize');
return {
Expand Down Expand Up @@ -1329,7 +1331,7 @@ describe('fleet command support', () => {
deleteCloudFleetSandbox: vi.fn(async () => undefined),
createFleetWorkspaceClient: vi.fn() as never,
log: () => undefined,
warn: () => undefined,
warn: (message) => warnings.push(message),
error: () => undefined,
});

Expand Down Expand Up @@ -1365,9 +1367,24 @@ describe('fleet command support', () => {
'/github/repos/AgentWorkforce/cloud/.relayfile/**',
'/.skills/**',
],
})
}),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
const ensureInput = ensureCloudFleetSandbox.mock.calls[0]?.[0] as Record<string, unknown>;
const ensureOptions = ensureCloudFleetSandbox.mock.calls[0]?.[1];
ensureOptions?.onPreparationProgress?.({
sandboxId: REPLAY_SANDBOX_ID,
state: 'pending',
phase: 'relayfile_mount_bootstrap',
generation: 2,
});
// The generated identity is printed before Cloud work so an interrupted
// run can be resumed with --sandbox-id instead of creating a second sandbox.
expect(warnings).toEqual([
`Cloud sandbox identity: ${String(ensureInput.sandboxId)}. If this command is interrupted, re-run it with --sandbox-id ${String(ensureInput.sandboxId)} to resume the same sandbox instead of creating another.`,
'Cloud sandbox preparation: relayfile_mount_bootstrap (pending, generation 2).',
]);
expect(ensureInput.sandboxId).toMatch(/^sbx_[0-9a-f-]{36}$/);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This assertion doesn't actually pin the sbx_<UUID> contract the command enforces elsewhere. /^sbx_[0-9a-f-]{36}$/ accepts any 36-char lowercase hex/hyphen string, so it can't catch a regression where the generated ID stops being an RFC 4122 UUID. Use the same strict pattern as CLOUD_SANDBOX_ID_PATTERN in fleet.ts:99 so the test asserts the documented identity contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/cli/src/cli/commands/fleet.test.ts, line 1387:

<comment>This assertion doesn't actually pin the sbx_<UUID> contract the command enforces elsewhere. `/^sbx_[0-9a-f-]{36}$/` accepts any 36-char lowercase hex/hyphen string, so it can't catch a regression where the generated ID stops being an RFC 4122 UUID. Use the same strict pattern as CLOUD_SANDBOX_ID_PATTERN in fleet.ts:99 so the test asserts the documented identity contract.</comment>

<file context>
@@ -1378,9 +1378,13 @@ describe('fleet command support', () => {
+      `Cloud sandbox identity: ${String(ensureInput.sandboxId)}. If this command is interrupted, re-run it with --sandbox-id ${String(ensureInput.sandboxId)} to resume the same sandbox instead of creating another.`,
       'Cloud sandbox preparation: relayfile_mount_bootstrap (pending, generation 2).',
     ]);
+    expect(ensureInput.sandboxId).toMatch(/^sbx_[0-9a-f-]{36}$/);
     expect(ensureInput.repos).toBeUndefined();
     expect(ensureInput.repoRevisions).toBeUndefined();
</file context>
Suggested change
expect(ensureInput.sandboxId).toMatch(/^sbx_[0-9a-f-]{36}$/);
expect(ensureInput.sandboxId).toMatch(
/^sbx_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, no code change in this round: /^sbx_[0-9a-f-]{36}$/ only checks shape. The generator is sbx_${randomUUID()} (fleet.ts:772, RFC 4122 v4) and --sandbox-id input is already validated with the strict CLOUD_SANDBOX_ID_PATTERN (fleet.ts:99, test rejects an invalid replay sandbox ID before provisioning), so no current behaviour escapes; but the assertion should use the same strict pattern to pin the contract. Per the CI batching rule (reviewer asked for no push now), that tightening is queued for the next push on this PR rather than pushed alone.

expect(ensureInput.repos).toBeUndefined();
expect(ensureInput.repoRevisions).toBeUndefined();
expect(placement.spawn).toHaveBeenCalledWith(
Expand Down Expand Up @@ -1482,7 +1499,8 @@ describe('fleet command support', () => {
expect.objectContaining({
mountRelayfile: true,
relayfilePaths: ['/github/repos/AgentWorkforce/agent-assistant/contents/**', '/memory/**'],
})
}),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
const ensureInput = ensureCloudFleetSandbox.mock.calls[0]?.[0] as Record<string, unknown>;
expect(ensureInput.repos).toBeUndefined();
Expand Down Expand Up @@ -1602,7 +1620,8 @@ describe('fleet command support', () => {
expect.objectContaining({
mountRelayfile: true,
relayfilePaths: ['/github/repos/AgentWorkforce/cloud/contents/**', '/memory/**'],
})
}),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
const spawnInput = placement.spawn.mock.calls[0]?.[0]?.input as { task?: string };
expect(placement.spawn).toHaveBeenCalledWith(
Expand Down Expand Up @@ -1970,7 +1989,8 @@ describe('fleet command support', () => {
{ from: 'user' }
);
expect(ensureCloudFleetSandbox).toHaveBeenCalledWith(
expect.objectContaining({ relayfilePaths: paths16 })
expect.objectContaining({ relayfilePaths: paths16 }),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);

const tooMany = new Command();
Expand Down Expand Up @@ -2195,7 +2215,12 @@ describe('fleet command support', () => {
expect(findProjectRoot).toHaveBeenCalledWith(path.resolve(process.cwd(), 'packages/web'));
expect(selectionOptions[0]).toMatchObject({ projectRoot: "/local/cloud/packages/web team's" });
const ensureInput = ensureCloudFleetSandbox.mock.calls[0]?.[0] as Record<string, unknown>;
// The exact checkout revision is part of the durable async request, so a
// resumed preparation clones the same commit it was accepted for.
expect(ensureInput).toMatchObject({
preparationMode: 'async-v1',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ensureInput.preparationMode will never be 'async-v1' in this test: it passes --name cloud-worker without --sandbox-id, so sandboxId is undefined and useAsyncPreparation (fleet.ts:794) is false, which is why fleet.ts:800 only adds preparationMode conditionally. Drop the preparationMode assertion (and the stale comment above it); forceProvision and providerId are the only newly asserted fields the command actually sends.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/cli/src/cli/commands/fleet.test.ts, line 2217:

<comment>`ensureInput.preparationMode` will never be `'async-v1'` in this test: it passes `--name cloud-worker` without `--sandbox-id`, so `sandboxId` is `undefined` and `useAsyncPreparation` (fleet.ts:794) is false, which is why fleet.ts:800 only adds `preparationMode` conditionally. Drop the `preparationMode` assertion (and the stale comment above it); `forceProvision` and `providerId` are the only newly asserted fields the command actually sends.</comment>

<file context>
@@ -2211,7 +2211,12 @@ describe('fleet command support', () => {
+    // The exact checkout revision is part of the durable async request, so a
+    // resumed preparation clones the same commit it was accepted for.
     expect(ensureInput).toMatchObject({
+      preparationMode: 'async-v1',
+      forceProvision: true,
+      providerId: 'agent37',
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered with evidence, no change. --name is the worker name, not --sandbox-name. With no --sandbox-name, packages/cli/src/cli/commands/fleet.ts:772 generates sbx_<uuid> (sandboxIdOption ?? (sandboxName === undefined ? \sbx_${randomUUID()}` : undefined)), so useAsyncPreparationis true for this unpinned/agent37 spawn and the async call shape is real. Mutation check at b43487424: forcinguseAsyncPreparationto false fails this test (12 failures in fleet.test.ts, including--checkout infers the Git root and forwards only the public revision attestationandfleet spawn --sandbox reuses an Agent37 target without a Relayfile mount`).

forceProvision: true,
providerId: 'agent37',
repos: ['AgentWorkforce/cloud'],
repoRevisions: { 'AgentWorkforce/cloud': revision },
});
Expand Down Expand Up @@ -2412,7 +2437,12 @@ describe('fleet command support', () => {

expect(resolveWorkspaceByKey).toHaveBeenCalledWith('rk_live_pinned');
expect(ensureCloudFleetSandbox).toHaveBeenCalledWith(
expect.objectContaining({ workspaceId: 'cloud-pinned', workloadProfile: 'long-running-agent' })
expect.objectContaining({
workspaceId: 'cloud-pinned',
workloadProfile: 'long-running-agent',
preparationMode: 'async-v1',
}),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
expect(createWorkspaceRelay).toHaveBeenCalledWith(
expect.objectContaining({
Expand Down Expand Up @@ -2672,6 +2702,10 @@ describe('fleet command support', () => {
{ from: 'user' }
);

expect(ensureCloudFleetSandbox).toHaveBeenCalledWith(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This call can never match: the test passes --name reused-worker without --sandbox-id, so sandboxId is undefined, useAsyncPreparation is false, and the command invokes ensureCloudFleetSandbox(ensureInput) with one argument, no preparationMode, and no onPreparationProgress. Remove this toHaveBeenCalledWith block (or add --sandbox-id and rename the sandbox to the required fleet-sandbox-<uuid> form if the intent is to cover the async path).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/cli/src/cli/commands/fleet.test.ts, line 2701:

<comment>This call can never match: the test passes `--name reused-worker` without `--sandbox-id`, so `sandboxId` is `undefined`, `useAsyncPreparation` is false, and the command invokes `ensureCloudFleetSandbox(ensureInput)` with one argument, no `preparationMode`, and no `onPreparationProgress`. Remove this `toHaveBeenCalledWith` block (or add `--sandbox-id` and rename the sandbox to the required `fleet-sandbox-<uuid>` form if the intent is to cover the async path).</comment>

<file context>
@@ -2693,6 +2698,10 @@ describe('fleet command support', () => {
       { from: 'user' }
     );
 
+    expect(ensureCloudFleetSandbox).toHaveBeenCalledWith(
+      expect.objectContaining({ preparationMode: 'async-v1', providerId: 'agent37', mountRelayfile: false }),
+      expect.objectContaining({ onPreparationProgress: expect.any(Function) })
</file context>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered with evidence, no change. --name is the worker name, not --sandbox-name. With no --sandbox-name, packages/cli/src/cli/commands/fleet.ts:772 generates sbx_<uuid> (sandboxIdOption ?? (sandboxName === undefined ? \sbx_${randomUUID()}` : undefined)), so useAsyncPreparationis true for this unpinned/agent37 spawn and the async call shape is real. Mutation check at b43487424: forcinguseAsyncPreparationto false fails this test (12 failures in fleet.test.ts, including--checkout infers the Git root and forwards only the public revision attestationandfleet spawn --sandbox reuses an Agent37 target without a Relayfile mount`).

expect.objectContaining({ preparationMode: 'async-v1', providerId: 'agent37', mountRelayfile: false }),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
expect(createWorkspaceRelay).toHaveBeenCalledWith({
projectRoot: process.cwd(),
token: undefined,
Expand Down Expand Up @@ -3273,7 +3307,9 @@ describe('fleet command support', () => {
expect(ensureCloudFleetSandbox).toHaveBeenCalledWith(
expect.objectContaining({
workloadProfile: 'long-running-agent',
})
preparationMode: 'async-v1',
}),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
expect(deleteCloudFleetSandbox).not.toHaveBeenCalled();
});
Expand Down Expand Up @@ -3482,7 +3518,8 @@ describe('fleet command support', () => {
{ from: 'user' }
);
expect(ensureCloudFleetSandbox).toHaveBeenCalledWith(
expect.objectContaining({ workspaceId: 'rw_captured' })
expect.objectContaining({ workspaceId: 'rw_captured', preparationMode: 'async-v1' }),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
});

Expand Down Expand Up @@ -3548,7 +3585,8 @@ describe('fleet command support', () => {
);

expect(ensureCloudFleetSandbox).toHaveBeenCalledWith(
expect.objectContaining({ workspaceId: cloudWorkspaceId })
expect.objectContaining({ workspaceId: cloudWorkspaceId, preparationMode: 'async-v1' }),
expect.objectContaining({ onPreparationProgress: expect.any(Function) })
);
expect(createWorkspaceRelay).toHaveBeenCalledWith({
projectRoot: process.cwd(),
Expand Down Expand Up @@ -3715,6 +3753,10 @@ describe('fleet command support', () => {
/^sbx_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
);
expect(ensureInput?.name).toBe(`fleet-sandbox-${ensureInput?.sandboxId?.slice('sbx_'.length)}`);
expect(ensureInput?.preparationMode).toBe('async-v1');
expect(ensureCloudFleetSandbox.mock.calls[0]?.[1]).toEqual({
onPreparationProgress: expect.any(Function),
});
});

it('rejects a replay sandbox name that does not match its identity', async () => {
Expand Down Expand Up @@ -3890,6 +3932,7 @@ describe('fleet command support', () => {

expect(deleteCloudFleetSandbox).not.toHaveBeenCalled();
expect(warnings.join('\n')).toContain(`check Cloud Fleet for node '${REPLAY_SANDBOX_NAME}'`);
expect(warnings.join('\n')).not.toContain('no sandbox was left running');
expect(warnings.join('\n')).toContain(`--sandbox-id '${REPLAY_SANDBOX_ID}'`);
});

Expand Down Expand Up @@ -3964,6 +4007,81 @@ describe('fleet command support', () => {
expect(warnings.join('\n')).not.toContain('check Cloud Fleet');
});

it('names a terminal relayfile_mount_failed cause and reports no sandbox left running without cleanup', async () => {
const warnings: string[] = [];
const deleteCloudFleetSandbox = vi.fn(async () => undefined);
const terminalMessage =
'Cloud fleet sandbox preparation failed during relayfile_mount (relayfile_mount_failed, initial_sync_deadline): Relayfile mount failed during relayfile_mount. Retry provisioning the sandbox.';
const program = new Command();
program.exitOverride();
registerFleetCommands(program, {
resolveSandboxRepository: () => undefined,
sdk: {
createAgentRelay: vi.fn() as never,
createWorkspaceRelay: vi.fn(() => ({
workspace: { info: vi.fn(async () => ({ id: 'rw_abc' })) },
})) as never,
createWorkspace: vi.fn() as never,
log: vi.fn(),
error: vi.fn(),
exit: (() => {
throw new Error('__exit__');
}) as never,
},
ensureCloudFleetSandbox: vi.fn(async () => {
throw new CloudFleetSandboxProvisionError(terminalMessage, {
cloudWorkspaceId: '50587328-441d-4acb-b8f3-dbe1b3c5de99',
sandboxId: REPLAY_SANDBOX_ID,
nodeName: REPLAY_SANDBOX_NAME,
providerId: 'agent37',
sandboxAbsent: true,
preparationFailure: {
code: 'relayfile_mount_failed',
phase: 'relayfile_mount',
causeStage: 'initial_sync_deadline',
},
});
}),
deleteCloudFleetSandbox,
createFleetWorkspaceClient: vi.fn() as never,
log: () => undefined,
warn: (...args: unknown[]) => warnings.push(args.join(' ')),
error: () => undefined,
});

await expect(
program.parseAsync(
[
'fleet',
'spawn',
'codex',
'--sandbox',
'--sandbox-id',
REPLAY_SANDBOX_ID,
'--sandbox-name',
REPLAY_SANDBOX_NAME,
'--workspace-id',
'rw_abc',
'--name',
'sandbox-worker',
'--task',
'Work',
'--workspace-key',
'rk_live_test',
'--token',
'at_live_lead',
],
{ from: 'user' }
)
).rejects.toThrow('__exit__');

expect(deleteCloudFleetSandbox).not.toHaveBeenCalled();
expect(warnings).toEqual([
`${terminalMessage} Cloud confirmed sandbox '${REPLAY_SANDBOX_ID}' is not running; no sandbox was left running.`,
]);
expect(warnings.join('\n')).not.toContain('outcome is unknown');
});

it('preserves the caller Daytona ID after a matched malformed provisioned response', async () => {
const warnings: string[] = [];
const deleteCloudFleetSandbox = vi.fn(async () => undefined);
Expand Down
39 changes: 35 additions & 4 deletions packages/cli/src/cli/commands/fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
resolveWorkspaceByKey,
type CloudFleetSandboxProviderId,
type CloudRelayfileRepositoryMaterialization,
type EnsureCloudFleetSandboxInput,
type EnsureCloudFleetSandboxResult,
} from '@agent-relay/cloud';
import { HarnessDriverClient } from '@agent-relay/harness-driver';
Expand Down Expand Up @@ -457,7 +458,10 @@ export function registerFleetCommands(
'--sandbox-name <name>',
'Explicit sandbox node name (custom unless --sandbox-id requires matching fleet-sandbox-<UUID>)'
)
.option('--sandbox-id <id>', 'Reuse a caller-declared sbx_<UUID> identity for an exact replay')
.option(
'--sandbox-id <id>',
'Reuse a caller-declared sbx_<UUID> identity for an exact replay; re-run an interrupted --sandbox spawn with the identity it printed to resume it'
)
.option('--workspace-id <id>', 'Explicit Relay workspace identity required for sandbox provisioning')
.option('--sandbox-provider <provider>', 'Sandbox provider: daytona, e2b, or agent37')
.option(
Expand Down Expand Up @@ -790,10 +794,20 @@ export function registerFleetCommands(
sandboxProvider === undefined || sandboxProvider === 'agent37'
? 'long-running-agent'
: 'standard-long-running-agent';
const useAsyncPreparation =
sandboxId !== undefined && (sandboxProvider === undefined || sandboxProvider === 'agent37');
if (useAsyncPreparation && sandboxIdOption === undefined) {
// The generated identity is not persisted. Print it before Cloud work
// starts so an interrupted run can be resumed instead of duplicated.
deps.warn(
`Cloud sandbox identity: ${sandboxId}. If this command is interrupted, re-run it with --sandbox-id ${sandboxId} to resume the same sandbox instead of creating another.`
);
}
try {
sandbox = await deps.ensureCloudFleetSandbox({
const ensureInput: EnsureCloudFleetSandboxInput = {
workspaceId: relayWorkspaceId,
requiredCapability: `spawn:${cli}`,
...(useAsyncPreparation ? { preparationMode: 'async-v1' as const } : {}),
maxAgents: 1,
mountRelayfile: mountSandboxRelayfile,
...(sandboxMountPaths === undefined ? {} : { relayfilePaths: sandboxMountPaths }),
Expand All @@ -807,10 +821,27 @@ export function registerFleetCommands(
...(checkoutRepository && sandboxRepository
? { repoRevisions: { [sandboxRepository.repository]: sandboxRepository.revision } }
: {}),
});
};
sandbox = useAsyncPreparation
? await deps.ensureCloudFleetSandbox(ensureInput, {
onPreparationProgress: (progress) => {
deps.warn(
`Cloud sandbox preparation: ${progress.phase} (${progress.state}, generation ${progress.generation}).`
);
},
})
: await deps.ensureCloudFleetSandbox(ensureInput);
assertSandboxRepositoryRevision(sandbox, checkoutRepository ? sandboxRepository : undefined);
} catch (error) {
if (
if (error instanceof CloudFleetSandboxProvisionError && error.sandboxAbsent) {
// Cloud's terminal record for this exact identity already proves the
// provider sandbox is gone; a delete here would only race its reaper.
deps.warn(
`${error.message} Cloud confirmed sandbox '${
error.sandboxId ?? sandboxId ?? 'the requested sandbox'
}' is not running; no sandbox was left running.`
);
} else if (
shouldCleanupSandbox &&
error instanceof CloudFleetSandboxProvisionError &&
error.confirmedProvisioned &&
Expand Down
Loading
Loading