@opengeni/worker-bundle 0.28.3 → 0.28.4-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/activities/agent-turn/mcp-operation-read-tool.d.ts +16 -0
  2. package/dist/activities/mcp-operation-authority.d.ts +16 -0
  3. package/dist/activities/mcp-operation-observer.d.ts +14 -0
  4. package/dist/activities/mcp-operation-reader.d.ts +45 -0
  5. package/dist/activities/mcp-operation-store.d.ts +6 -0
  6. package/dist/activities/sandbox-lease.d.ts +4 -4
  7. package/dist/{activities-control-UXGPMDL3.js → activities-control-J5QNRWVH.js} +56 -70
  8. package/dist/activities-control-J5QNRWVH.js.map +1 -0
  9. package/dist/{activities-turn-NOYDE7PX.js → activities-turn-ZJBY3W5D.js} +15692 -799
  10. package/dist/activities-turn-ZJBY3W5D.js.map +1 -0
  11. package/dist/artifact-materializer-entry.js +2 -0
  12. package/dist/artifact-materializer-entry.js.map +1 -1
  13. package/dist/artifact-outbox-entry.js +2 -0
  14. package/dist/artifact-outbox-entry.js.map +1 -1
  15. package/dist/{chunk-BARTLVBK.js → chunk-A4XQWVZK.js} +25 -25
  16. package/dist/chunk-A4XQWVZK.js.map +1 -0
  17. package/dist/chunk-PZ5AY32C.js +10 -0
  18. package/dist/chunk-PZ5AY32C.js.map +1 -0
  19. package/dist/{chunk-NNXLW6V7.js → chunk-ZNIKZQMN.js} +144 -111
  20. package/dist/chunk-ZNIKZQMN.js.map +1 -0
  21. package/dist/host-export.js +1 -0
  22. package/dist/index.js +3 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/observability-metrics.d.ts +12 -4
  25. package/dist/sandbox-archive-storage.d.ts +15 -3
  26. package/dist/sandbox-resume.d.ts +1 -1
  27. package/package.json +18 -17
  28. package/src/activities/agent-turn/claim.ts +3 -3
  29. package/src/activities/agent-turn/finalization.ts +10 -5
  30. package/src/activities/agent-turn/mcp-operation-read-tool.ts +61 -0
  31. package/src/activities/agent-turn/stream-attempt.ts +1 -1
  32. package/src/activities/agent-turn/tool-environment.ts +65 -0
  33. package/src/activities/mcp-credentials.ts +94 -40
  34. package/src/activities/mcp-operation-authority.ts +76 -0
  35. package/src/activities/mcp-operation-observer.ts +86 -0
  36. package/src/activities/mcp-operation-reader.ts +108 -0
  37. package/src/activities/mcp-operation-store.ts +98 -0
  38. package/src/activities/sandbox-lease.ts +64 -71
  39. package/src/observability-metrics.ts +28 -24
  40. package/src/sandbox-archive-storage.ts +82 -18
  41. package/src/sandbox-resume.ts +79 -63
  42. package/dist/activities-control-UXGPMDL3.js.map +0 -1
  43. package/dist/activities-turn-NOYDE7PX.js.map +0 -1
  44. package/dist/chunk-BARTLVBK.js.map +0 -1
  45. package/dist/chunk-NNXLW6V7.js.map +0 -1
@@ -0,0 +1,86 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import type { AttemptToolEnvironment } from "@opengeni/codemode";
3
+ import { digestCanonicalJson } from "@opengeni/tool-gateway";
4
+ import {
5
+ runMcpOperationObservationWithAuthority,
6
+ type ResolveConnectionCredentialInput,
7
+ type ResolveConnectionCredentialResult,
8
+ } from "@opengeni/runtime";
9
+ import type { McpOperationReaderDependencies } from "./mcp-operation-reader";
10
+
11
+ /** Current-attempt adapter: no credential renewal authority survives this
12
+ * closure. The stored digest constrains fresh authority, never establishes it. */
13
+ export function createMcpOperationObserverResolver(input: {
14
+ settings: Pick<Settings, "mcpServers">;
15
+ workspaceId: string;
16
+ credentialSubjectId?: string;
17
+ assertAttempt: () => Promise<void>;
18
+ resolveCredential: (
19
+ request: ResolveConnectionCredentialInput,
20
+ ) => Promise<ResolveConnectionCredentialResult>;
21
+ getEnvironment: () => Promise<AttemptToolEnvironment | null>;
22
+ }): McpOperationReaderDependencies["resolveObserver"] {
23
+ return async (operation) => {
24
+ await input.assertAttempt();
25
+ const configured = input.settings.mcpServers.find(
26
+ (candidate) => candidate.id === operation.serverId,
27
+ );
28
+ const recovery = configured?.operationRecovery?.[operation.originalTool];
29
+ if (!configured?.connectionRef || !recovery) return { status: "unsupported" };
30
+ const destinationUrl = new URL(configured.url).toString();
31
+ if (
32
+ recovery.observerTool !== operation.observerTool ||
33
+ digestCanonicalJson(destinationUrl) !== operation.destinationDigest
34
+ ) {
35
+ return { status: "binding_changed" };
36
+ }
37
+ const connectionRef = structuredClone(configured.connectionRef);
38
+ const environment = await input.getEnvironment();
39
+ const observerEntry = environment?.catalog.entries.find(
40
+ (entry) =>
41
+ entry.identity.serverId === operation.serverId &&
42
+ entry.identity.toolName === operation.observerTool,
43
+ );
44
+ if (!environment || !observerEntry) return { status: "unsupported" };
45
+
46
+ const authorize = async () => {
47
+ await input.assertAttempt();
48
+ const resolved = await input.resolveCredential({
49
+ workspaceId: input.workspaceId,
50
+ serverId: operation.serverId,
51
+ toolName: operation.observerTool,
52
+ connectionRef,
53
+ destinationUrl,
54
+ forceRefresh: false,
55
+ ...(input.credentialSubjectId ? { subjectId: input.credentialSubjectId } : {}),
56
+ });
57
+ // This is credential-resolution authorization. The actual broker keeps
58
+ // ownership of the physical provider-request authorization/audit fact.
59
+ return (
60
+ resolved.status === "ok" && resolved.operationAuthorityDigest === operation.authorityDigest
61
+ );
62
+ };
63
+ if (!(await authorize())) return { status: "auth_needed" };
64
+ return {
65
+ status: "ready",
66
+ authorize,
67
+ callObserver: async (tool, args) => {
68
+ if (tool !== operation.observerTool) throw new Error("MCP observer binding changed");
69
+ return await runMcpOperationObservationWithAuthority(
70
+ {
71
+ serverId: operation.serverId,
72
+ observerTool: operation.observerTool,
73
+ destinationDigest: operation.destinationDigest,
74
+ authorityDigest: operation.authorityDigest,
75
+ },
76
+ async () =>
77
+ await environment.callModel({
78
+ modelName: observerEntry.modelName,
79
+ arguments: args,
80
+ subjectId: "worker:first-party-mcp",
81
+ }),
82
+ );
83
+ },
84
+ };
85
+ };
86
+ }
@@ -0,0 +1,108 @@
1
+ import type { AttemptToolResult } from "@opengeni/contracts";
2
+ import { observeMcpOperation, type McpObservationReceipt } from "@opengeni/runtime";
3
+ import type { McpOperationReadSelector } from "./agent-turn/mcp-operation-read-tool";
4
+
5
+ type TerminalReceipt = Extract<McpObservationReceipt, { status: "completed" }>;
6
+
7
+ /** Internal, credential-free projection loaded through canonical current
8
+ * attempt/session authorization. Never return this object directly to tools. */
9
+ export type McpOperationReadRecord = {
10
+ operationId: string;
11
+ sourceTurnId: string;
12
+ sourceCallId: string | null;
13
+ serverId: string;
14
+ originalTool: string;
15
+ observerTool: string;
16
+ argumentDigest: string;
17
+ destinationDigest: string;
18
+ authorityDigest: string;
19
+ originalOutcome: "captured" | "completed" | "outcome_unknown";
20
+ originalResult: AttemptToolResult | null;
21
+ receipt: TerminalReceipt | null;
22
+ };
23
+
24
+ type ObserverResolution =
25
+ | { status: "unsupported" | "auth_needed" | "binding_changed" }
26
+ | {
27
+ status: "ready";
28
+ authorize: (phase: "before_request" | "before_delivery") => Promise<boolean>;
29
+ callObserver: (tool: string, args: Record<string, unknown>) => Promise<unknown>;
30
+ };
31
+
32
+ export type McpOperationReaderDependencies = {
33
+ /** Includes live attempt and original-principal/session disclosure checks. */
34
+ load: (selector: McpOperationReadSelector) => Promise<McpOperationReadRecord | null>;
35
+ claim: (operationId: string) => Promise<string | null>;
36
+ release: (operationId: string, claimId: string) => Promise<void>;
37
+ /** Claim-fenced transaction storing one immutable receipt. Must recheck
38
+ * canonical current authority and never rewrite the original outcome.
39
+ * Delivery uses the ordinary operation_read tool-result lifecycle. */
40
+ settle: (
41
+ operationId: string,
42
+ claimId: string,
43
+ receipt: TerminalReceipt,
44
+ ) => Promise<McpOperationReadRecord>;
45
+ /** Binds the current trusted config/connection to the persisted original
46
+ * digests. Model arguments may not supply or replace any of that authority. */
47
+ resolveObserver: (operation: McpOperationReadRecord) => Promise<ObserverResolution>;
48
+ };
49
+
50
+ /** One explicit read; no timer, inference wake, or mutation replay. */
51
+ export async function readMcpOperation(
52
+ selector: McpOperationReadSelector,
53
+ dependencies: McpOperationReaderDependencies,
54
+ ): Promise<Record<string, unknown>> {
55
+ const operation = await dependencies.load(selector);
56
+ if (!operation) throw new Error("MCP operation is unavailable");
57
+ if (operation.receipt || operation.originalOutcome === "completed") return project(operation);
58
+
59
+ const observer = await dependencies.resolveObserver(operation);
60
+ if (observer.status !== "ready") return project(operation, { status: observer.status });
61
+ const claimId = await dependencies.claim(operation.operationId);
62
+ if (!claimId) {
63
+ // A competing reader may have committed after our initial load. Re-read
64
+ // under current authority instead of returning an old cached projection.
65
+ const fresh = await dependencies.load({ operationId: operation.operationId });
66
+ if (!fresh) throw new Error("MCP operation is unavailable");
67
+ return project(
68
+ fresh,
69
+ fresh.receipt || fresh.originalOutcome === "completed"
70
+ ? undefined
71
+ : { status: "observation_in_progress" },
72
+ );
73
+ }
74
+ let settled = false;
75
+ try {
76
+ const receipt = await observeMcpOperation({
77
+ binding: {
78
+ operationId: operation.operationId,
79
+ serverId: operation.serverId,
80
+ originalTool: operation.originalTool,
81
+ observerTool: operation.observerTool,
82
+ argumentDigest: operation.argumentDigest,
83
+ },
84
+ authorize: observer.authorize,
85
+ callObserver: observer.callObserver,
86
+ });
87
+ if (receipt.status !== "completed") return project(operation, receipt);
88
+ const recorded = await dependencies.settle(operation.operationId, claimId, receipt);
89
+ settled = true;
90
+ return project(recorded);
91
+ } finally {
92
+ if (!settled) await dependencies.release(operation.operationId, claimId);
93
+ }
94
+ }
95
+
96
+ function project(
97
+ operation: McpOperationReadRecord,
98
+ observation?: Record<string, unknown>,
99
+ ): Record<string, unknown> {
100
+ return {
101
+ operationId: operation.operationId,
102
+ original: { turnId: operation.sourceTurnId, sourceCallId: operation.sourceCallId },
103
+ invocationOutcome:
104
+ operation.originalOutcome === "captured" ? "outcome_unknown" : operation.originalOutcome,
105
+ ...(operation.originalOutcome === "completed" ? { result: operation.originalResult } : {}),
106
+ observation: observation ?? operation.receipt ?? { status: "unknown" },
107
+ };
108
+ }
@@ -0,0 +1,98 @@
1
+ import { AttemptToolResult } from "@opengeni/contracts";
2
+ import type { Database } from "@opengeni/db";
3
+ import {
4
+ readMcpOperation as readStoredMcpOperation,
5
+ claimMcpOperationObservation,
6
+ releaseMcpOperationObservation,
7
+ settleMcpOperationObservation,
8
+ type McpOperationAttempt,
9
+ type McpOperationRecord,
10
+ } from "@opengeni/db/mcp-operations";
11
+ import type {
12
+ McpOperationReaderDependencies,
13
+ McpOperationReadRecord,
14
+ } from "./mcp-operation-reader";
15
+
16
+ /** All DB calls remain bound to the actual accepted worker attempt. Locators
17
+ * supplied to operation_read cannot replace this scope or its principal. */
18
+ export function createMcpOperationReadStore(
19
+ db: Database,
20
+ attempt: McpOperationAttempt,
21
+ ): Pick<McpOperationReaderDependencies, "load" | "claim" | "release" | "settle"> {
22
+ const current = { ...attempt };
23
+ const load: McpOperationReaderDependencies["load"] = async (selector) => {
24
+ const found = await readStoredMcpOperation(db, current, selector);
25
+ if (found.status === "ambiguous") {
26
+ throw new Error("MCP source call matches multiple operations; select an exact operationId");
27
+ }
28
+ return found.status === "found" ? projectRecord(found.operation) : null;
29
+ };
30
+ return {
31
+ load,
32
+ claim: async (operationId) => {
33
+ const result = await claimMcpOperationObservation(db, current, operationId);
34
+ return result.status === "claimed" ? result.claimId : null;
35
+ },
36
+ release: async (operationId, claimId) => {
37
+ await releaseMcpOperationObservation(db, current, { operationId, claimId });
38
+ },
39
+ settle: async (operationId, claimId, receipt) => {
40
+ const outcome = await settleMcpOperationObservation(db, current, {
41
+ operationId,
42
+ claimId,
43
+ receiptRevision: receipt.receiptRevision,
44
+ result: receipt.result,
45
+ });
46
+ if (outcome.status === "conflict") {
47
+ throw new Error(
48
+ "MCP provider returned conflicting terminal receipts; the first receipt is preserved",
49
+ );
50
+ }
51
+ if (
52
+ outcome.status !== "settled" &&
53
+ outcome.status !== "existing" &&
54
+ outcome.status !== "original_completed"
55
+ ) {
56
+ throw new Error(
57
+ "MCP observation claim is no longer current; read the existing operation again",
58
+ );
59
+ }
60
+ const recorded = await load({ operationId });
61
+ if (!recorded) throw new Error("MCP operation is unavailable");
62
+ return recorded;
63
+ },
64
+ };
65
+ }
66
+
67
+ function projectRecord(record: McpOperationRecord): McpOperationReadRecord {
68
+ if (record.observationResult !== null && record.receiptRevision === null) {
69
+ throw new Error("Stored MCP operation receipt is incomplete");
70
+ }
71
+ return {
72
+ operationId: record.operationId,
73
+ sourceTurnId: record.sourceTurnId,
74
+ sourceCallId: record.sourceCallId,
75
+ serverId: record.serverId,
76
+ originalTool: record.originalTool,
77
+ observerTool: record.observerTool,
78
+ argumentDigest: record.argumentDigest,
79
+ destinationDigest: record.destinationDigest,
80
+ authorityDigest: record.authorityDigest,
81
+ // Capture precedes the physical request; after a crash it is not proof
82
+ // that the mutation was or was not sent. It never authorizes a replay.
83
+ originalOutcome: record.originalOutcome,
84
+ originalResult:
85
+ record.originalResult === null ? null : AttemptToolResult.parse(record.originalResult),
86
+ receipt:
87
+ record.observationResult === null
88
+ ? null
89
+ : {
90
+ version: 1,
91
+ operationRef: record.operationId,
92
+ fingerprint: { version: 1, algorithm: "sha256", value: record.argumentDigest },
93
+ status: "completed",
94
+ receiptRevision: record.receiptRevision!,
95
+ result: AttemptToolResult.parse(record.observationResult),
96
+ },
97
+ };
98
+ }
@@ -82,7 +82,10 @@ import {
82
82
  // box is typed as provider loss. Stale capture reconciliation separately uses
83
83
  // establishSandboxSessionFromEnvelope in strict resume-only mode so it can
84
84
  // reuse the runtime's provider-ready proof without ever cold-restoring.
85
- captureVerifiedWorkspaceArchive,
85
+ captureWorkspaceArchiveForStorage,
86
+ disposeWorkspaceArchive,
87
+ type VerifiedWorkspaceArchivePayload,
88
+ type VerifiedHostWorkspaceArchive,
86
89
  cancelSelfhostedOp,
87
90
  assertConsistentSandboxProviderIdentity,
88
91
  createSandboxClientForBackend,
@@ -160,9 +163,7 @@ import {
160
163
  } from "../opensandbox-kubernetes-inventory";
161
164
  import type { ObjectStorage } from "@opengeni/storage";
162
165
  import {
163
- collectWorkspaceArchiveObjectKeys,
164
- deleteUnpublishedWorkspaceArchiveObject,
165
- deleteWorkspaceArchiveObjectKeys,
166
+ persistWorkspaceArchiveCandidate,
166
167
  putVersion1TarArchiveOrInline,
167
168
  } from "../sandbox-archive-storage";
168
169
 
@@ -210,7 +211,7 @@ type DrainSandboxClient = {
210
211
  * aborts the terminate before client.delete()).
211
212
  */
212
213
  export type PersistArchiveFn = (
213
- archiveBase64: string | null,
214
+ archiveBase64: string | VerifiedHostWorkspaceArchive | null,
214
215
  archiveMetadata?: WorkspaceArchiveDescriptor,
215
216
  providerSession?: unknown,
216
217
  providerBinding?: Awaited<
@@ -247,6 +248,7 @@ export type TerminateBoxFn = (
247
248
  providerCaptureRequestId?: string,
248
249
  captureDisposition?: DrainCaptureDisposition,
249
250
  capturePolicy?: ProviderWorkspaceCapturePolicy | null,
251
+ diskBackedArchives?: boolean,
250
252
  ) => Promise<boolean | ProviderTerminationOutcome>;
251
253
 
252
254
  export type SweepModalOrphansFn = (
@@ -479,6 +481,7 @@ export function createSandboxLeaseActivities(
479
481
  providerCaptureRequestId,
480
482
  captureDisposition,
481
483
  capturePolicy,
484
+ diskBackedArchives,
482
485
  ) =>
483
486
  await terminateProviderBox(
484
487
  settings,
@@ -490,6 +493,7 @@ export function createSandboxLeaseActivities(
490
493
  providerCaptureRequestId,
491
494
  captureDisposition,
492
495
  capturePolicy,
496
+ diskBackedArchives,
493
497
  ));
494
498
  const sweepModalOrphans: SweepModalOrphansFn =
495
499
  options.sweepModalOrphans ?? sweepModalOrphansForConfiguredBackend;
@@ -2675,7 +2679,7 @@ async function terminateDrainableBox(
2675
2679
  ? (lease.recovery.archive.current?.revision ?? null)
2676
2680
  : null;
2677
2681
  const persistArchive: PersistArchiveFn = async (
2678
- archiveBase64: string | null,
2682
+ archiveBase64: string | VerifiedHostWorkspaceArchive | null,
2679
2683
  archiveMetadata?: WorkspaceArchiveDescriptor,
2680
2684
  _providerSession?: unknown,
2681
2685
  providerBinding?: Awaited<
@@ -2691,6 +2695,7 @@ async function terminateDrainableBox(
2691
2695
  const archiveMetrics = runtimeMetricsHooksForObservability(observability);
2692
2696
  let checkpointArtifactId: string | null = null;
2693
2697
  if (
2698
+ typeof archiveBase64 === "string" &&
2694
2699
  archiveBase64 &&
2695
2700
  archiveMetadata?.version === 2 &&
2696
2701
  (archiveMetadata.provider === "modal_snapshot_filesystem" ||
@@ -2735,59 +2740,33 @@ async function terminateDrainableBox(
2735
2740
  if (!archiveMetadata) {
2736
2741
  throw new Error("Sandbox snapshot publication requires a verified archive descriptor");
2737
2742
  }
2738
- const priorKeys = collectWorkspaceArchiveObjectKeys(
2739
- (lease.resumeState as Record<string, unknown> | null | undefined) ?? null,
2740
- );
2741
- let workspaceArchiveRef:
2742
- | Awaited<ReturnType<typeof putVersion1TarArchiveOrInline>>["workspaceArchiveRef"]
2743
- | undefined;
2744
- try {
2745
- const published = await putVersion1TarArchiveOrInline({
2746
- backend: lease.backend,
2747
- objectStorage,
2748
- accountId,
2749
- workspaceId: row.workspaceId,
2750
- sandboxGroupId: row.sandboxGroupId,
2751
- archive: {
2752
- bytes: Buffer.from(archiveBase64, "base64"),
2753
- descriptor: archiveMetadata,
2754
- base64: archiveBase64,
2755
- },
2756
- metrics: archiveMetrics,
2757
- });
2758
- workspaceArchiveRef = published.workspaceArchiveRef;
2759
- result = await persistDrainSnapshot(db, {
2760
- ...baseInput,
2761
- workspaceArchiveMeta: archiveMetadata,
2762
- ...published,
2763
- ...(checkpointArtifactId ? { checkpointArtifactId } : {}),
2764
- });
2765
- if (published.workspaceArchiveRef && objectStorage) {
2766
- if (!result.wrote) {
2767
- await deleteUnpublishedWorkspaceArchiveObject(
2768
- objectStorage,
2769
- published.workspaceArchiveRef,
2770
- archiveMetrics,
2771
- );
2772
- } else {
2773
- const afterLease = await readLease(db, row.workspaceId, row.sandboxGroupId);
2774
- const afterKeys = collectWorkspaceArchiveObjectKeys(
2775
- (afterLease?.resumeState as Record<string, unknown> | null | undefined) ?? null,
2776
- );
2777
- await deleteWorkspaceArchiveObjectKeys(
2778
- objectStorage,
2779
- [...priorKeys].filter((key) => !afterKeys.has(key)),
2780
- ).catch(() => undefined);
2781
- }
2782
- }
2783
- } catch (error) {
2784
- await deleteUnpublishedWorkspaceArchiveObject(
2785
- objectStorage,
2786
- workspaceArchiveRef,
2787
- archiveMetrics,
2788
- );
2789
- throw error;
2790
- }
2743
+ const published = await putVersion1TarArchiveOrInline({
2744
+ backend: lease.backend,
2745
+ objectStorage,
2746
+ accountId,
2747
+ workspaceId: row.workspaceId,
2748
+ sandboxGroupId: row.sandboxGroupId,
2749
+ archive:
2750
+ typeof archiveBase64 === "object"
2751
+ ? archiveBase64
2752
+ : {
2753
+ bytes: Buffer.from(archiveBase64, "base64"),
2754
+ descriptor: archiveMetadata,
2755
+ base64: archiveBase64,
2756
+ },
2757
+ metrics: archiveMetrics,
2758
+ });
2759
+ result = await persistWorkspaceArchiveCandidate({
2760
+ objectStorage,
2761
+ ...(published.workspaceArchiveRef ? { ref: published.workspaceArchiveRef } : {}),
2762
+ persist: () =>
2763
+ persistDrainSnapshot(db, {
2764
+ ...baseInput,
2765
+ workspaceArchiveMeta: archiveMetadata,
2766
+ ...published,
2767
+ ...(checkpointArtifactId ? { checkpointArtifactId } : {}),
2768
+ }),
2769
+ });
2791
2770
  }
2792
2771
  if (!result.wrote && checkpointArtifactId) {
2793
2772
  await markSandboxCheckpointArtifactDeletePending(db, {
@@ -2823,6 +2802,7 @@ async function terminateDrainableBox(
2823
2802
  captureClaim?.providerRequestId ?? attempt.operationId,
2824
2803
  captureDisposition,
2825
2804
  capturePolicy,
2805
+ Boolean(objectStorage),
2826
2806
  );
2827
2807
  const terminated = typeof termination === "boolean" ? termination : termination.terminated;
2828
2808
  if (!terminated) {
@@ -2955,6 +2935,7 @@ export async function terminateProviderBox(
2955
2935
  providerCaptureRequestId?: string,
2956
2936
  captureDisposition: DrainCaptureDisposition = "capture_required",
2957
2937
  claimedCapturePolicy?: ProviderWorkspaceCapturePolicy | null,
2938
+ diskBackedArchives = false,
2958
2939
  ): Promise<ProviderTerminationOutcome> {
2959
2940
  const durableBackendId = (lease.resumeBackendId ?? lease.backend) as string;
2960
2941
  const backend = sandboxBackendForSdkBackendId(durableBackendId) ?? durableBackendId;
@@ -3147,7 +3128,7 @@ export async function terminateProviderBox(
3147
3128
  // snapshot must re-throw BEFORE any terminate so files are never lost — the next
3148
3129
  // sweep retries, the provider idle-timeout is the backstop. A NotFound here (the
3149
3130
  // box raced gone between resume and persist) is success: nothing to persist.
3150
- let verifiedArchive: Awaited<ReturnType<typeof captureVerifiedWorkspaceArchive>> | undefined;
3131
+ let verifiedArchive: VerifiedWorkspaceArchivePayload | undefined;
3151
3132
  const sessionWorkspacePersistence = (
3152
3133
  session as { state?: { workspacePersistence?: unknown } } | undefined
3153
3134
  )?.state?.workspacePersistence;
@@ -3162,10 +3143,15 @@ export async function terminateProviderBox(
3162
3143
  : null;
3163
3144
  try {
3164
3145
  if (captureDisposition !== "archive_published" && session?.persistWorkspace) {
3165
- const capture = captureVerifiedWorkspaceArchive(session, Date.now(), {
3166
- requestId: providerCaptureRequestId ?? randomUUID(),
3167
- strategy: liveCapturePolicy.strategy,
3168
- });
3146
+ const capture = captureWorkspaceArchiveForStorage(
3147
+ session,
3148
+ Date.now(),
3149
+ {
3150
+ requestId: providerCaptureRequestId ?? randomUUID(),
3151
+ strategy: liveCapturePolicy.strategy,
3152
+ },
3153
+ diskBackedArchives,
3154
+ );
3169
3155
  verifiedArchive = await awaitProviderCaptureWithLatePublication({
3170
3156
  capture,
3171
3157
  timeoutMs: settings.sandboxSnapshotTimeoutMs,
@@ -3179,7 +3165,7 @@ export async function terminateProviderBox(
3179
3165
  publishLate: async (archive) => {
3180
3166
  try {
3181
3167
  const result = await persistArchive(
3182
- archive.base64,
3168
+ archive.kind === "host_spool" ? archive : archive.base64,
3183
3169
  archive.descriptor,
3184
3170
  session,
3185
3171
  checkpointBinding,
@@ -3197,6 +3183,8 @@ export async function terminateProviderBox(
3197
3183
  ...logIdentity,
3198
3184
  error: error instanceof Error ? error.message : String(error),
3199
3185
  });
3186
+ } finally {
3187
+ await disposeWorkspaceArchive(archive);
3200
3188
  }
3201
3189
  },
3202
3190
  observeLateFailure: (error) => {
@@ -3237,12 +3225,17 @@ export async function terminateProviderBox(
3237
3225
  ...logIdentity,
3238
3226
  });
3239
3227
  } else if (verifiedArchive) {
3240
- const { wrote } = await persistArchive(
3241
- verifiedArchive.base64,
3242
- verifiedArchive.descriptor,
3243
- session,
3244
- checkpointBinding,
3245
- );
3228
+ let wrote: boolean;
3229
+ try {
3230
+ ({ wrote } = await persistArchive(
3231
+ verifiedArchive.kind === "host_spool" ? verifiedArchive : verifiedArchive.base64,
3232
+ verifiedArchive.descriptor,
3233
+ session,
3234
+ checkpointBinding,
3235
+ ));
3236
+ } finally {
3237
+ await disposeWorkspaceArchive(verifiedArchive);
3238
+ }
3246
3239
  if (!wrote) {
3247
3240
  observability.info(
3248
3241
  "sandbox reaper: lease re-armed during persist — leaving box RUNNING (no terminate)",
@@ -413,7 +413,7 @@ export function recordWorkerDeathRecoveryMetrics(
413
413
  }
414
414
 
415
415
  export class TurnLifecycleMetrics {
416
- private readonly turns = new Map<string, { startedAt: number; lastProgressAt: number }>();
416
+ private readonly attempts = new Map<string, { startedAt: number; lastProgressAt: number }>();
417
417
  private timer: ReturnType<typeof setInterval> | null = null;
418
418
 
419
419
  constructor(
@@ -424,42 +424,46 @@ export class TurnLifecycleMetrics {
424
424
  } = {},
425
425
  ) {}
426
426
 
427
- start(turnId: string): void {
427
+ start(input: { attemptId: string }): void {
428
428
  const now = this.now();
429
- this.turns.set(turnId, { startedAt: now, lastProgressAt: now });
429
+ this.attempts.set(input.attemptId, { startedAt: now, lastProgressAt: now });
430
430
  this.ensureTimer();
431
431
  this.refreshGauges();
432
432
  }
433
433
 
434
- progress(turnId: string): void {
435
- const turn = this.turns.get(turnId);
436
- if (!turn) return;
437
- turn.lastProgressAt = this.now();
434
+ progress(input: { attemptId: string }): void {
435
+ const attempt = this.attempts.get(input.attemptId);
436
+ if (!attempt) return;
437
+ attempt.lastProgressAt = this.now();
438
438
  }
439
439
 
440
- finish(turnId: string, outcome: TurnOutcome | null, durationSeconds?: number): void {
441
- const startedAt = this.turns.get(turnId)?.startedAt;
440
+ finish(input: {
441
+ attemptId: string;
442
+ outcome: TurnOutcome | null;
443
+ durationSeconds?: number;
444
+ }): void {
445
+ const startedAt = this.attempts.get(input.attemptId)?.startedAt;
442
446
  if (startedAt !== undefined) {
443
- this.turns.delete(turnId);
447
+ this.attempts.delete(input.attemptId);
444
448
  }
445
- if (outcome) {
449
+ if (input.outcome) {
446
450
  const observedDuration =
447
- durationSeconds ??
451
+ input.durationSeconds ??
448
452
  (startedAt === undefined ? 0 : Math.max(0, (this.now() - startedAt) / 1000));
449
453
  this.observability.incrementCounter({
450
454
  name: "opengeni_turns_total",
451
455
  help: "Total agent turns by terminal outcome.",
452
- labels: { outcome },
456
+ labels: { outcome: input.outcome },
453
457
  });
454
458
  this.observability.observeHistogram({
455
459
  name: "opengeni_turn_duration_seconds",
456
460
  help: "Agent turn duration in seconds by terminal outcome.",
457
- labels: { outcome },
461
+ labels: { outcome: input.outcome },
458
462
  value: observedDuration,
459
463
  });
460
464
  }
461
465
  this.refreshGauges();
462
- if (this.turns.size === 0) {
466
+ if (this.attempts.size === 0) {
463
467
  this.stopTimer();
464
468
  }
465
469
  }
@@ -467,42 +471,42 @@ export class TurnLifecycleMetrics {
467
471
  refreshGauges(): void {
468
472
  this.observability.setGauge({
469
473
  name: "opengeni_turns_inflight",
470
- help: "Current number of in-flight agent turns in this worker process.",
471
- value: this.turns.size,
474
+ help: "Current number of in-flight physical agent-turn attempts in this worker process.",
475
+ value: this.attempts.size,
472
476
  });
473
477
  this.observability.setGauge({
474
478
  name: "opengeni_turn_oldest_inflight_age_seconds",
475
- help: "Age in seconds of the oldest in-flight agent turn in this worker process.",
479
+ help: "Age in seconds of the oldest in-flight physical agent-turn attempt in this worker process.",
476
480
  value: this.oldestInflightAgeSeconds(),
477
481
  });
478
482
  this.observability.setGauge({
479
483
  name: "opengeni_turn_oldest_no_progress_age_seconds",
480
- help: "Seconds since durable progress for the least recently progressing in-flight turn.",
484
+ help: "Seconds since durable progress for the least recently progressing in-flight physical agent-turn attempt.",
481
485
  value: this.oldestNoProgressAgeSeconds(),
482
486
  });
483
487
  }
484
488
 
485
489
  stop(): void {
486
- this.turns.clear();
490
+ this.attempts.clear();
487
491
  this.refreshGauges();
488
492
  this.stopTimer();
489
493
  }
490
494
 
491
495
  private oldestInflightAgeSeconds(): number {
492
- if (this.turns.size === 0) {
496
+ if (this.attempts.size === 0) {
493
497
  return 0;
494
498
  }
495
499
  let oldest = Number.POSITIVE_INFINITY;
496
- for (const { startedAt } of this.turns.values()) {
500
+ for (const { startedAt } of this.attempts.values()) {
497
501
  oldest = Math.min(oldest, startedAt);
498
502
  }
499
503
  return Math.max(0, (this.now() - oldest) / 1000);
500
504
  }
501
505
 
502
506
  private oldestNoProgressAgeSeconds(): number {
503
- if (this.turns.size === 0) return 0;
507
+ if (this.attempts.size === 0) return 0;
504
508
  let leastRecentProgress = Number.POSITIVE_INFINITY;
505
- for (const { lastProgressAt } of this.turns.values()) {
509
+ for (const { lastProgressAt } of this.attempts.values()) {
506
510
  leastRecentProgress = Math.min(leastRecentProgress, lastProgressAt);
507
511
  }
508
512
  return Math.max(0, (this.now() - leastRecentProgress) / 1000);