From 3f85a90bef76fa79627e2c8c82f1b0a094ce3d13 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 24 Aug 2026 00:30:39 +0200 Subject: [PATCH 1/2] fix(mount): scale flush timeout per exact root --- package-lock.json | 4 +- package.json | 2 +- src/orchestrator.flush-mount.test.ts | 64 ++++++++++++++++++++++++++++ src/orchestrator.ts | 10 ++++- 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 src/orchestrator.flush-mount.test.ts 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..1767bcf --- /dev/null +++ b/src/orchestrator.flush-mount.test.ts @@ -0,0 +1,64 @@ +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 requested allowance for every sequential exact root", 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", + paths: [ + "/slack/channels/C123/**", + "/github/repos/acme/cloud/**", + "/linear/issues/TEAM/**", + ], + }, + { timeoutMs: 120_000 }, + ); + + assert.equal(calls.length, 1); + assert.equal(calls[0]?.timeoutMs, 360_000); + }); +}); diff --git a/src/orchestrator.ts b/src/orchestrator.ts index a3089a1..59fd8c2 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -112,6 +112,11 @@ export type StartMountOptions = { export type FlushMountOptions = { cwd?: string; + /** + * Runtime allowance for each exact mount root. Multi-root flushes run each + * root sequentially, so the outer runtime timeout is scaled by the number + * of generated exact-layout commands to avoid cutting later roots off. + */ timeoutMs?: number; }; @@ -299,10 +304,13 @@ export class SandboxOrchestrator { config: RelayfileMountShellOptions, options: FlushMountOptions = {}, ): Promise { + const exactLayout = resolveRelayfileMountExactLayout(config); + const timeoutMs = (options.timeoutMs ?? 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, }); if (result.exitCode !== 0) { throw new Error(`Failed to flush relayfile mount: ${result.output}`); From 7579526be7f5c1b13c3f6394638ccff3b1ed72eb Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 24 Aug 2026 00:33:06 +0200 Subject: [PATCH 2/2] fix(mount): preserve explicit flush deadlines --- src/orchestrator.flush-mount.test.ts | 40 ++++++++++++++++------------ src/orchestrator.ts | 9 +++---- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/orchestrator.flush-mount.test.ts b/src/orchestrator.flush-mount.test.ts index 1767bcf..b665c43 100644 --- a/src/orchestrator.flush-mount.test.ts +++ b/src/orchestrator.flush-mount.test.ts @@ -38,27 +38,33 @@ describe("flushMount exact-root timeout budget", () => { assert.equal(calls[0]?.timeoutMs, 120_000); }); - it("budgets the requested allowance for every sequential exact root", async () => { + it("budgets the default allowance for every sequential exact root", 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", - paths: [ - "/slack/channels/C123/**", - "/github/repos/acme/cloud/**", - "/linear/issues/TEAM/**", - ], - }, - { timeoutMs: 120_000 }, - ); + 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 59fd8c2..be9191a 100644 --- a/src/orchestrator.ts +++ b/src/orchestrator.ts @@ -113,9 +113,8 @@ export type StartMountOptions = { export type FlushMountOptions = { cwd?: string; /** - * Runtime allowance for each exact mount root. Multi-root flushes run each - * root sequentially, so the outer runtime timeout is scaled by the number - * of generated exact-layout commands to avoid cutting later roots off. + * Optional whole-operation runtime timeout. When omitted, the default + * allowance scales by the number of sequential exact-layout commands. */ timeoutMs?: number; }; @@ -305,12 +304,12 @@ export class SandboxOrchestrator { options: FlushMountOptions = {}, ): Promise { const exactLayout = resolveRelayfileMountExactLayout(config); - const timeoutMs = (options.timeoutMs ?? 120_000) + const defaultTimeoutMs = 120_000 * Math.max(1, exactLayout.mountLocalDirs.length); const result = await this.runtime.runScript(handle, { command: buildRelayfileMountFlushShell(config), cwd: options.cwd, - timeoutMs, + timeoutMs: options.timeoutMs ?? defaultTimeoutMs, }); if (result.exitCode !== 0) { throw new Error(`Failed to flush relayfile mount: ${result.output}`);