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
66 changes: 66 additions & 0 deletions src/orchestrator.start-mount.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,72 @@ describe("startMount initial-sync idle budget", () => {
);
});

it("classifies a rejected initial-sync launch transport", async () => {
// The launch exec is the first long-lived call in startMount. Daytona's
// proxy read timeout is ~120s, so a rejection here is exactly what a
// wedged mount looks like from the outside — and while this call was
// unwrapped, the raw transport error matched none of the message prefixes
// callers classify on, so it fell through to their "unknown" bucket and
// the real cause was discarded. See AgentWorkforce/sandbox#45.
const cause = new Error("read ETIMEDOUT after 120000ms");
let calls = 0;
const orchestrator = new SandboxOrchestrator<{ id: string }>({
provision: async () => ({ id: "sbx" }),
uploadBundle: async () => {},
runScript: async () => {
calls += 1;
if (calls === 2) throw cause;
return { output: "ok", exitCode: 0 };
},
teardown: async () => {},
});

await assert.rejects(
orchestrator.startMount({ id: "sbx" }, MOUNT),
(error: unknown) => {
assert.ok(error instanceof Error);
assert.match(
error.message,
/Failed to launch relayfile initial sync: read ETIMEDOUT after 120000ms/u,
);
assert.equal(error.cause, cause);
return true;
},
);
assert.equal(calls, 2);
});

it("classifies a rejected daemon-start transport", async () => {
// The other formerly-unwrapped exec. Reached only after the initial sync
// reports a clean exit, so it needs the full happy path in front of it.
const cause = new Error("read ETIMEDOUT after 120000ms");
const orchestrator = new SandboxOrchestrator<{ id: string }>({
provision: async () => ({ id: "sbx" }),
uploadBundle: async () => {},
runScript: async (_handle, options) => {
if (options.command.includes("nohup relayfile-mount")) throw cause;
if (options.command.includes("relayfile-initial-sync-exit:")) {
return { output: "relayfile-initial-sync-exit:0", exitCode: 0 };
}
return { output: "ok", exitCode: 0 };
},
teardown: async () => {},
});

await assert.rejects(
orchestrator.startMount({ id: "sbx" }, MOUNT),
(error: unknown) => {
assert.ok(error instanceof Error);
assert.match(
error.message,
/Failed to start relayfile mount: read ETIMEDOUT after 120000ms/u,
);
assert.equal(error.cause, cause);
return true;
},
);
});

it("requests a complete readiness traversal with bounded foreground concurrency", async () => {
const { orchestrator, commands } = recordingRuntime();
await orchestrator.startMount({ id: "sbx" }, MOUNT);
Expand Down
60 changes: 39 additions & 21 deletions src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,21 +252,30 @@ export class SandboxOrchestrator<Handle> {
// mount lease, and the exit sentinel is written only after the one-shot
// supervisor has exited and released that lease.
const initialSyncRun = { runId: relayfileInitialSyncRunId() };
const launch = await this.runtime.runScript(handle, {
command: withRelayfileInitialSyncEnvironment(
buildRelayfileMountInitialSyncBackgroundShell(
{
...config,
idleTimeoutSeconds: initialSyncIdleTimeoutSeconds,
},
initialSyncRun,
let launch: SandboxCommandResult;
try {
launch = await this.runtime.runScript(handle, {
command: withRelayfileInitialSyncEnvironment(
buildRelayfileMountInitialSyncBackgroundShell(
{
...config,
idleTimeoutSeconds: initialSyncIdleTimeoutSeconds,
},
initialSyncRun,
),
initialSyncIdleTimeoutSeconds,
initialSyncReadConcurrency,
initialSyncMaxFilesPerCycle,
),
initialSyncIdleTimeoutSeconds,
initialSyncReadConcurrency,
initialSyncMaxFilesPerCycle,
),
cwd,
});
cwd,
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to launch relayfile initial sync: ${detail}`,
{ cause: error },
);
}
if (launch.exitCode !== 0) {
throw new Error(`Failed to launch relayfile initial sync: ${launch.output}`);
}
Expand Down Expand Up @@ -338,13 +347,22 @@ export class SandboxOrchestrator<Handle> {
await sleepMs(pollIntervalMs);
}

const start = await this.runtime.runScript(handle, {
command: withRelayfileBootstrapIdleTimeout(
buildRelayfileMountStartShell(config),
initialSyncIdleTimeoutSeconds,
),
cwd,
});
let start: SandboxCommandResult;
try {
start = await this.runtime.runScript(handle, {
command: withRelayfileBootstrapIdleTimeout(
buildRelayfileMountStartShell(config),
initialSyncIdleTimeoutSeconds,
),
cwd,
});
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to start relayfile mount: ${detail}`,
{ cause: error },
);
}
if (start.exitCode !== 0) {
throw new Error(`Failed to start relayfile mount: ${start.output}`);
}
Expand Down
Loading