Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ea5328c
feat(fleet): poll async sandbox preparation
khaliqgant Oct 2, 2026
aae3911
fix(fleet): support legacy synchronous ensure responses
khaliqgant Oct 2, 2026
86d6e11
fix(broker): retry pressured control frames
Oct 7, 2026
55f413b
fix(broker): retain pressured registration cleanup
Oct 7, 2026
1bb1e18
fix(broker): harden pressure retry ownership
Oct 7, 2026
bd5ebb0
fix(broker): retain pressured results across reconnects
Oct 7, 2026
ede3771
Merge remote-tracking branch 'origin/main' into fix/1915-agent-regist…
Oct 7, 2026
1d56a11
fix(broker): bound pressured result replay
Oct 7, 2026
bd1ccb8
test(broker): harden pressure ownership regressions
Oct 7, 2026
8f5b34b
style(broker): format pressure regression
Oct 7, 2026
8450644
test(broker): allow CI slack for pressure retry watchdog
Oct 8, 2026
78db279
fix(cli): honor persisted route over ambient base URL
Oct 8, 2026
e689fe4
docs: record relay 1919 release investigation
Oct 8, 2026
faeb9da
Merge origin/main into feat/agent37-async-preparation-client
khaliqgant Oct 8, 2026
33a7d50
test(cloud): bind async-prepared Relaycast target to the answering Cloud
khaliqgant Oct 8, 2026
8a3776a
fix(cloud): let Cloud route unpinned async sandbox preparation
khaliqgant Oct 8, 2026
0b2a276
fix(cli): preserve broker route fallback provenance
Oct 8, 2026
51eb330
fix(fleet): report a terminal async preparation as proven absent with…
khaliqgant Oct 8, 2026
c116f62
test(fleet): pin async preparation for --checkout and --no-sandbox-re…
khaliqgant Oct 8, 2026
07378e4
fix(cloud): surface auth and cancellation from the async compatibilit…
khaliqgant Oct 8, 2026
b434874
fix(cloud): reconcile a truncated status or advance response instead …
khaliqgant Oct 8, 2026
54c4412
test(broker): make pressure exhaustion deterministic
Oct 8, 2026
b858fea
test(cli): prove persisted route for fleet release
Oct 8, 2026
5e51d9a
fix(fleet): parse an older Cloud's typed 5xx and print the resumable …
khaliqgant Oct 8, 2026
c458dd1
docs: remove lane investigation report
Oct 8, 2026
2aa0afb
docs: restore patch release heading
Oct 8, 2026
ac6dacc
Merge commit '54c441210ca3cdd07ac66530819bec300902f077' into integrat…
khaliqgant Oct 8, 2026
c072f3b
Merge commit '2aa0afb1adff8b8454a738abbc764489ba0b1654' into integrat…
khaliqgant Oct 8, 2026
3d58e47
Merge commit '5e51d9a95f7bf0361ee83669518cfc13af224c6b' into integrat…
khaliqgant Oct 8, 2026
d895cc5
docs(changelog): move #1917 entries out of the released 13.1.5 section
khaliqgant Oct 8, 2026
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
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,23 @@ 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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recreate the aggregator as a real merge commit

This commit has only one parent (20ae3e3), and none of the advertised heads (54c4412, 2aa0afb, or 5e51d9a) is its ancestor. Merging this commit therefore only copies their trees; it does not make the original PR heads reachable from main, so #1917/#1923/#1881 will not be recorded as merged as intended. Rebuild the aggregator with actual --no-ff merges so those commits appear in its ancestry.

Useful? React with 👍 / 👎.

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.

Not applicable to this PR's history; no change. #1928 was never squashed or force-pushed. The head chain is three two-parent merge commits: ac6dacc97 (parents 20ae3e398 + #1917 head 54c441210), c072f3b77 (ac6dacc97 + #1923 head 2aa0afb1a), 3d58e479d (c072f3b77 + #1881 head 5e51d9a95), then d895cc508 (CHANGELOG only). git merge-base --is-ancestor is true for all three PR heads against the PR head, so a merge-commit merge records #1917/#1923/#1881 as merged. The single-parent commit described was not on this branch.


### Added

- `agent-relay fleet spawn --sandbox` (unpinned or `--sandbox-provider agent37`) now uses Cloud's durable `async-v1` preparation and prints each preparation phase, 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 `fleet spawn --sandbox` preparation no longer causes the spawn or a preparation step to be resubmitted.
- `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.
- Workspace-scoped commands such as `agent-relay fleet release` continue using the workspace's Relaycast server when `--base-url` is omitted; explicit conflicting `--base-url` values are still rejected.
- Fleet spawns now recover from transient Relaycast `d1_pressure`; persistent registration pressure returns a named `d1_pressure` error within 25 seconds, and any success arriving after retry-count or wall-clock exhaustion is explicitly deregistered.
- Acknowledged `agent.deregister`, cumulative `delivery.ack`, and non-task action results now recover from transient Relaycast `d1_pressure`; exhausted non-task action results are replayed after reconnect.
- `agent-relay fleet spawn` reports registration `d1_pressure` as retryable only when the registration is proven to have failed before any change was made.

## [13.1.5] - 2026-10-08

Expand Down
2,115 changes: 2,004 additions & 111 deletions crates/broker/src/node_control.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ describe('fleet CLI lifecycle routing', () => {
relayHome = fs.mkdtempSync(path.join(os.tmpdir(), 'relay-fleet-lifecycle-home-'));
vi.stubEnv('AGENT_RELAY_PROJECT', projectRoot);
vi.stubEnv('AGENT_RELAY_HOME', relayHome);
vi.stubEnv('RELAY_WORKSPACE_KEY', 'rk_live_workspace');
vi.stubEnv('RELAY_API_KEY', '');
vi.stubEnv('RELAY_BASE_URL', 'https://cast.agentrelay.com');
const dataDir = path.join(projectRoot, '.agentworkforce', 'relay');
writeProjectWorkspaceKey(dataDir, 'rk_live_workspace', { workspaceId: TARGET.workspaceId });

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.

P2: This accepts malformed UUID shapes, including non-v4 versions, so the generated-ID check does not enforce the Cloud sandbox identity contract. Use the strict pattern already asserted in this file at line 3752.

(Based on your team's feedback about strict Cloud sandbox ID assertions.)

View Feedback

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 accepts malformed UUID shapes, including non-v4 versions, so the generated-ID check does not enforce the Cloud sandbox identity contract. Use the strict pattern already asserted in this file at line 3752.

(Based on your team's feedback about strict Cloud sandbox ID assertions.) </comment>

<file context>
@@ -1365,9 +1367,24 @@ 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 nit, deliberately not changed here: this file must stay byte-identical to #1881's head so the combine is provably content-neutral. The generator is sbx_${randomUUID()} (fleet.ts:772, RFC 4122 v4) and --sandbox-id input is already checked with the strict CLOUD_SANDBOX_ID_PATTERN. Tightening the assertion is queued as a follow-up (same nit acknowledged on #1881).

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',
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(
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