diff --git a/package-lock.json b/package-lock.json index 1f73e64..24d1e26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agent-relay/sandbox", - "version": "0.1.8", + "version": "0.1.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-relay/sandbox", - "version": "0.1.8", + "version": "0.1.9", "license": "Apache-2.0", "devDependencies": { "@aws-sdk/client-bedrock-agentcore": "^3.1115.0", diff --git a/package.json b/package.json index a7d7798..089c683 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agent-relay/sandbox", - "version": "0.1.8", + "version": "0.1.9", "description": "Provider-agnostic sandbox runtimes and orchestration for agent workloads.", "license": "Apache-2.0", "type": "module", diff --git a/src/orchestrator.flush-mount.test.ts b/src/orchestrator.flush-mount.test.ts new file mode 100644 index 0000000..b665c43 --- /dev/null +++ b/src/orchestrator.flush-mount.test.ts @@ -0,0 +1,70 @@ +import { strict as assert } from "node:assert"; +import { describe, it } from "node:test"; + +import { + SandboxOrchestrator, + type SandboxRunScriptOptions, +} from "./orchestrator.js"; + +function recordingOrchestrator(): { + orchestrator: SandboxOrchestrator; + calls: SandboxRunScriptOptions[]; +} { + const calls: SandboxRunScriptOptions[] = []; + return { + calls, + orchestrator: new SandboxOrchestrator({ + runScript: async (_handle, options) => { + calls.push(options); + return { output: "", exitCode: 0 }; + }, + }), + }; +} + +describe("flushMount exact-root timeout budget", () => { + it("keeps the pathless mount at one default allowance", async () => { + const { orchestrator, calls } = recordingOrchestrator(); + + await orchestrator.flushMount("sandbox-1", { + baseUrl: "https://relayfile.example", + workspaceId: "rw_abc12345", + localDir: "/home/daytona/workspace", + stateDir: "/home/daytona/.relayfile-mount-state", + token: "relay_pa_test", + }); + + assert.equal(calls.length, 1); + assert.equal(calls[0]?.timeoutMs, 120_000); + }); + + it("budgets the default allowance for every sequential exact root", async () => { + const { orchestrator, calls } = recordingOrchestrator(); + + const config = { + baseUrl: "https://relayfile.example", + workspaceId: "rw_abc12345", + localDir: "/home/daytona/workspace", + stateDir: "/home/daytona/.relayfile-mount-state", + token: "relay_pa_test", + paths: [ + "/slack/channels/C123/**", + "/github/repos/acme/cloud/**", + "/linear/issues/TEAM/**", + ], + }; + await orchestrator.flushMount("sandbox-1", config); + + assert.equal(calls.length, 1); + assert.equal(calls[0]?.timeoutMs, 360_000); + + await orchestrator.flushMount("sandbox-1", config, { timeoutMs: 9_000 }); + + assert.equal(calls.length, 2); + assert.equal( + calls[1]?.timeoutMs, + 9_000, + "an explicit whole-operation timeout must remain authoritative", + ); + }); +}); diff --git a/src/orchestrator.ts b/src/orchestrator.ts index a3089a1..be9191a 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -112,6 +112,10 @@ export type StartMountOptions = { export type FlushMountOptions = { cwd?: string; + /** + * Optional whole-operation runtime timeout. When omitted, the default + * allowance scales by the number of sequential exact-layout commands. + */ timeoutMs?: number; }; @@ -299,10 +303,13 @@ export class SandboxOrchestrator { config: RelayfileMountShellOptions, options: FlushMountOptions = {}, ): Promise { + const exactLayout = resolveRelayfileMountExactLayout(config); + const defaultTimeoutMs = 120_000 + * Math.max(1, exactLayout.mountLocalDirs.length); const result = await this.runtime.runScript(handle, { command: buildRelayfileMountFlushShell(config), cwd: options.cwd, - timeoutMs: options.timeoutMs ?? 120_000, + timeoutMs: options.timeoutMs ?? defaultTimeoutMs, }); if (result.exitCode !== 0) { throw new Error(`Failed to flush relayfile mount: ${result.output}`);