@opengeni/worker-bundle 1.0.0-canary.1 → 1.0.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.
- package/dist/activities/agent-run-admission.d.ts +3 -1
- package/dist/activities/agent-turn/admission.d.ts +24 -3
- package/dist/activities/agent-turn/agent-build.d.ts +4 -0
- package/dist/activities/agent-turn/claude-usage-observer.d.ts +58 -0
- package/dist/activities/agent-turn/code-search.d.ts +60 -0
- package/dist/activities/agent-turn/codex-capacity.d.ts +13 -0
- package/dist/activities/agent-turn/codex-plan-entitlement.d.ts +105 -0
- package/dist/activities/agent-turn/codex.d.ts +3 -2
- package/dist/activities/agent-turn/compaction-prep.d.ts +21 -5
- package/dist/activities/agent-turn/credential-leases.d.ts +19 -33
- package/dist/activities/agent-turn/credential-restriction.d.ts +3 -0
- package/dist/activities/agent-turn/errors.d.ts +82 -11
- package/dist/activities/agent-turn/failure-settlement.d.ts +6 -3
- package/dist/activities/agent-turn/final-reply.d.ts +18 -0
- package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
- package/dist/activities/agent-turn/finalization.d.ts +4 -2
- package/dist/activities/agent-turn/github-repository-bindings.d.ts +2 -0
- package/dist/activities/agent-turn/governance-model.d.ts +9 -2
- package/dist/activities/agent-turn/history-sink.d.ts +2 -0
- package/dist/activities/agent-turn/history.d.ts +2 -0
- package/dist/activities/agent-turn/input-wait-reply.d.ts +41 -0
- package/dist/activities/agent-turn/media-artifacts.d.ts +5 -3
- package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
- package/dist/activities/agent-turn/model-usage.d.ts +6 -0
- package/dist/activities/agent-turn/model-wire-dispatch.d.ts +15 -0
- package/dist/activities/agent-turn/optional-repositories.d.ts +59 -0
- package/dist/activities/agent-turn/provider-dispatch-barrier.d.ts +12 -1
- package/dist/activities/agent-turn/quiescence.d.ts +7 -7
- package/dist/activities/agent-turn/recovery-warning.d.ts +9 -0
- package/dist/activities/agent-turn/refresh-credentials.d.ts +6 -0
- package/dist/activities/agent-turn/run-credentials.d.ts +8 -3
- package/dist/activities/agent-turn/sandbox-establish.d.ts +2 -1
- package/dist/activities/agent-turn/sandbox-provision.d.ts +3 -1
- package/dist/activities/agent-turn/sandbox-route.d.ts +3 -20
- package/dist/activities/agent-turn/sandbox-runtime.d.ts +2 -2
- package/dist/activities/agent-turn/session-title.d.ts +34 -2
- package/dist/activities/agent-turn/skill-checkout.d.ts +25 -6
- package/dist/activities/agent-turn/skill-read.d.ts +70 -2
- package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
- package/dist/activities/agent-turn/skill-search.d.ts +3 -1
- package/dist/activities/agent-turn/skill-tools.d.ts +15 -2
- package/dist/activities/agent-turn/skill-transfer.d.ts +18 -6
- package/dist/activities/agent-turn/stream-attempt.d.ts +7 -3
- package/dist/activities/agent-turn/subscription-lease.d.ts +38 -0
- package/dist/activities/agent-turn/tool-environment.d.ts +56 -6
- package/dist/activities/agent-turn/tool-policy.d.ts +43 -0
- package/dist/activities/agent-turn/turn-context.d.ts +29 -0
- package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
- package/dist/activities/agent-turn/xai-capacity.d.ts +2 -1
- package/dist/activities/automations.d.ts +3 -1
- package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
- package/dist/activities/codemode-dispatcher.d.ts +9 -2
- package/dist/activities/codex-capacity.d.ts +18 -2
- package/dist/activities/codex-rotation.d.ts +7 -1
- package/dist/activities/context-compaction.d.ts +16 -2
- package/dist/activities/gmail-files.d.ts +32 -0
- package/dist/activities/goals.d.ts +8 -0
- package/dist/activities/knowledge-indexing.d.ts +19 -0
- package/dist/activities/mcp-account-routes.d.ts +3 -0
- package/dist/activities/mcp-credentials.d.ts +12 -13
- package/dist/activities/mcp-operation-authority.d.ts +0 -5
- package/dist/activities/model-call-fact-reconciler.d.ts +34 -0
- package/dist/activities/open-suffix-resume.d.ts +13 -0
- package/dist/activities/parent-wake.d.ts +6 -12
- package/dist/activities/retained-screenshots.d.ts +7 -2
- package/dist/activities/rig-verification.d.ts +2 -3
- package/dist/activities/run-credential-renewal.d.ts +1 -0
- package/dist/activities/run-credentials.d.ts +15 -3
- package/dist/activities/sandbox-images.d.ts +1 -1
- package/dist/activities/sandbox-lease.d.ts +47 -9
- package/dist/activities/scheduled-tasks.d.ts +4 -35
- package/dist/activities/session-state.d.ts +70 -3
- package/dist/activities/types.d.ts +55 -6
- package/dist/activities/workspace-capture.d.ts +2 -0
- package/dist/activities/workspace-credential-provider.d.ts +46 -0
- package/dist/{activities-control-HMFGSKC5.js → activities-control-BNPTRTOE.js} +1896 -418
- package/dist/activities-control-BNPTRTOE.js.map +1 -0
- package/dist/activities-control.d.ts +182 -4
- package/dist/{activities-turn-AP67JADS.js → activities-turn-5LGZOSVU.js} +4476 -1327
- package/dist/activities-turn-5LGZOSVU.js.map +1 -0
- package/dist/activities.d.ts +182 -4
- package/dist/artifact-materializer-entry.js +5 -2
- package/dist/artifact-materializer-entry.js.map +1 -1
- package/dist/artifact-outbox-entry.js +5 -2
- package/dist/artifact-outbox-entry.js.map +1 -1
- package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
- package/dist/{chunk-A4XQWVZK.js → chunk-3ZTRWDMN.js} +307 -4
- package/dist/chunk-3ZTRWDMN.js.map +1 -0
- package/dist/{chunk-RAMNYSAW.js → chunk-4ER3YXOT.js} +22 -4
- package/dist/chunk-4ER3YXOT.js.map +1 -0
- package/dist/{chunk-CYITU5UH.js → chunk-LJX7Z2KP.js} +1445 -343
- package/dist/chunk-LJX7Z2KP.js.map +1 -0
- package/dist/editable-artifact-materializer-service.d.ts +2 -2
- package/dist/editable-artifact-outbox-service.d.ts +2 -2
- package/dist/host-export-pump.d.ts +3 -1
- package/dist/host-export.d.ts +1 -1
- package/dist/host-export.js +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/index.d.ts +95 -3
- package/dist/index.js +137 -43
- package/dist/index.js.map +1 -1
- package/dist/observability-metrics.d.ts +118 -8
- package/dist/retained-process-diagnostics.d.ts +4 -0
- package/dist/retained-process-retry.d.ts +7 -0
- package/dist/sandbox-resume.d.ts +55 -13
- package/dist/sandbox-routing.d.ts +5 -3
- package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
- package/dist/session-event-fanout.d.ts +6 -0
- package/dist/warm-capture-holder.d.ts +3 -0
- package/dist/worker-service-lifecycle.d.ts +12 -0
- package/dist/workflow-bundle.js +273 -41
- package/dist/workflows/activities.d.ts +1 -1
- package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
- package/dist/workflows/scheduled-tasks.d.ts +4 -0
- package/dist/workflows/session.d.ts +36 -0
- package/dist/workflows.d.ts +1 -0
- package/package.json +21 -19
- package/src/activities/agent-run-admission.ts +17 -1
- package/src/activities/agent-turn/admission.ts +66 -3
- package/src/activities/agent-turn/agent-build.ts +84 -10
- package/src/activities/agent-turn/claim.ts +186 -43
- package/src/activities/agent-turn/claude-usage-observer.ts +221 -0
- package/src/activities/agent-turn/code-search.ts +275 -0
- package/src/activities/agent-turn/codex-capacity.ts +172 -8
- package/src/activities/agent-turn/codex-plan-entitlement.ts +254 -0
- package/src/activities/agent-turn/codex.ts +13 -5
- package/src/activities/agent-turn/compaction-prep.ts +170 -102
- package/src/activities/agent-turn/credential-leases.ts +122 -216
- package/src/activities/agent-turn/credential-restriction.ts +14 -0
- package/src/activities/agent-turn/errors.ts +647 -48
- package/src/activities/agent-turn/failure-settlement.ts +682 -130
- package/src/activities/agent-turn/final-reply.ts +46 -0
- package/src/activities/agent-turn/finalization-monitor.ts +120 -0
- package/src/activities/agent-turn/finalization.ts +146 -43
- package/src/activities/agent-turn/github-repository-bindings.ts +3 -1
- package/src/activities/agent-turn/governance-model.ts +69 -28
- package/src/activities/agent-turn/history-sink.ts +16 -2
- package/src/activities/agent-turn/history.ts +38 -11
- package/src/activities/agent-turn/input-wait-reply.ts +103 -0
- package/src/activities/agent-turn/knowledge-source-tools.ts +2 -0
- package/src/activities/agent-turn/media-artifacts.ts +2 -1
- package/src/activities/agent-turn/model-call-admission.ts +94 -0
- package/src/activities/agent-turn/model-usage.ts +64 -3
- package/src/activities/agent-turn/model-wire-dispatch.ts +27 -0
- package/src/activities/agent-turn/optional-repositories.ts +224 -0
- package/src/activities/agent-turn/provider-dispatch-barrier.ts +22 -0
- package/src/activities/agent-turn/quiescence.ts +10 -7
- package/src/activities/agent-turn/recovery-warning.ts +16 -0
- package/src/activities/agent-turn/refresh-credentials.ts +44 -0
- package/src/activities/agent-turn/run-credentials.ts +116 -55
- package/src/activities/agent-turn/run.ts +320 -93
- package/src/activities/agent-turn/sandbox-establish.ts +84 -8
- package/src/activities/agent-turn/sandbox-provision.ts +3 -1
- package/src/activities/agent-turn/sandbox-route.ts +4 -21
- package/src/activities/agent-turn/sandbox-runtime.ts +343 -18
- package/src/activities/agent-turn/session-title.ts +81 -5
- package/src/activities/agent-turn/skill-checkout.ts +111 -24
- package/src/activities/agent-turn/skill-install.ts +1 -1
- package/src/activities/agent-turn/skill-read.ts +374 -28
- package/src/activities/agent-turn/skill-remove.ts +46 -0
- package/src/activities/agent-turn/skill-save.ts +1 -1
- package/src/activities/agent-turn/skill-search.ts +10 -1
- package/src/activities/agent-turn/skill-selection.ts +4 -0
- package/src/activities/agent-turn/skill-tools.ts +112 -18
- package/src/activities/agent-turn/skill-transfer.ts +68 -23
- package/src/activities/agent-turn/stream-attempt.ts +313 -77
- package/src/activities/agent-turn/subscription-lease.ts +115 -0
- package/src/activities/agent-turn/tool-environment.ts +316 -167
- package/src/activities/agent-turn/tool-policy.ts +80 -1
- package/src/activities/agent-turn/turn-context.ts +35 -0
- package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
- package/src/activities/agent-turn/xai-capacity.ts +94 -51
- package/src/activities/automations.ts +76 -4
- package/src/activities/browser-deadline-checkpoint.ts +279 -0
- package/src/activities/capabilities.ts +108 -11
- package/src/activities/codemode-dispatcher.ts +36 -8
- package/src/activities/codex-capacity.ts +60 -25
- package/src/activities/codex-rotation.ts +39 -17
- package/src/activities/context-compaction.ts +75 -18
- package/src/activities/gmail-files.ts +88 -0
- package/src/activities/goals.ts +116 -121
- package/src/activities/google-drive-publication.ts +1 -3
- package/src/activities/image-generation-operation.ts +1 -1
- package/src/activities/knowledge-indexing.ts +269 -15
- package/src/activities/knowledge-source-sync.ts +10 -0
- package/src/activities/mcp-account-routes.ts +17 -0
- package/src/activities/mcp-credentials.ts +83 -194
- package/src/activities/mcp-operation-authority.ts +16 -41
- package/src/activities/model-call-fact-reconciler.ts +112 -0
- package/src/activities/open-suffix-resume.ts +15 -2
- package/src/activities/parent-wake.ts +46 -21
- package/src/activities/retained-screenshots.ts +82 -11
- package/src/activities/rig-verification.ts +38 -37
- package/src/activities/run-credential-renewal.ts +13 -1
- package/src/activities/run-credentials.ts +59 -17
- package/src/activities/run-input.ts +25 -16
- package/src/activities/sandbox-images.ts +0 -1
- package/src/activities/sandbox-lease.ts +798 -82
- package/src/activities/scheduled-tasks.ts +550 -178
- package/src/activities/session-state.ts +245 -6
- package/src/activities/site-auth-maintenance.ts +3 -0
- package/src/activities/types.ts +72 -5
- package/src/activities/workflow-wake.ts +2 -17
- package/src/activities/workspace-capture.ts +13 -2
- package/src/activities/workspace-credential-provider.ts +283 -0
- package/src/activities-control.ts +4 -0
- package/src/activities-turn.ts +5 -0
- package/src/activities.ts +13 -0
- package/src/activity-services.ts +16 -4
- package/src/browser-deadline-checkpoint-contract.ts +5 -0
- package/src/editable-artifact-hint-broker.ts +3 -0
- package/src/editable-artifact-materializer-service.ts +7 -3
- package/src/editable-artifact-outbox-service.ts +4 -4
- package/src/github-rest-mcp.ts +1 -2
- package/src/host-export-pump.ts +26 -4
- package/src/host-export.ts +3 -0
- package/src/http.ts +4 -2
- package/src/index.ts +99 -37
- package/src/observability-metrics.ts +510 -5
- package/src/personal-github-git-credentials.ts +2 -1
- package/src/pr-review-credentials.ts +1 -3
- package/src/retained-process-diagnostics.ts +50 -0
- package/src/retained-process-retry.ts +36 -0
- package/src/sandbox-resume.ts +388 -101
- package/src/sandbox-routing.ts +150 -25
- package/src/sandbox-snapshot-diagnostics.ts +112 -0
- package/src/session-event-fanout.ts +23 -0
- package/src/warm-capture-holder.ts +15 -0
- package/src/worker-service-lifecycle.ts +53 -0
- package/src/workflows/activities.ts +16 -1
- package/src/workflows/browser-deadline-checkpoint.ts +50 -0
- package/src/workflows/file-upload-reaper.ts +2 -0
- package/src/workflows/scheduled-tasks.ts +26 -13
- package/src/workflows/session.ts +242 -22
- package/src/workflows.ts +4 -0
- package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
- package/dist/activities/packs.d.ts +0 -55
- package/dist/activities-control-HMFGSKC5.js.map +0 -1
- package/dist/activities-turn-AP67JADS.js.map +0 -1
- package/dist/chunk-A4XQWVZK.js.map +0 -1
- package/dist/chunk-CYITU5UH.js.map +0 -1
- package/dist/chunk-RAMNYSAW.js.map +0 -1
- package/src/activities/agent-turn/knowledge-sources.ts +0 -86
- package/src/activities/packs.ts +0 -166
package/src/sandbox-resume.ts
CHANGED
|
@@ -20,21 +20,27 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Liveness between turns is the lease refcount; there is no keepalive loop.
|
|
22
22
|
|
|
23
|
+
import type { EventBus } from "@opengeni/events";
|
|
24
|
+
import { publishDurableSessionEvents } from "./session-event-fanout";
|
|
23
25
|
import {
|
|
24
26
|
effectiveSandboxLifecycle,
|
|
25
27
|
sandboxArchiveCaptureTimeoutMs,
|
|
26
28
|
sandboxLifecycleTransitionWaitMs,
|
|
29
|
+
sandboxWarmRateMicrosPerSecond,
|
|
27
30
|
type Settings,
|
|
28
31
|
} from "@opengeni/config";
|
|
29
32
|
import { randomUUID } from "node:crypto";
|
|
33
|
+
import { retainsHolderForWarmCapture } from "./warm-capture-holder";
|
|
30
34
|
import {
|
|
31
35
|
acquireLease,
|
|
32
36
|
adoptLegacyModalCheckpointArtifact,
|
|
33
37
|
beginSandboxRematerialization,
|
|
38
|
+
sessionHoldsFreshWorkspaceRecovery,
|
|
34
39
|
claimWorkspaceArchiveCapture,
|
|
35
40
|
commitWarmingToWarm,
|
|
36
41
|
failSandboxRematerialization,
|
|
37
42
|
failWarmingToCold,
|
|
43
|
+
beginModalProviderCreate,
|
|
38
44
|
getSandboxSessionEnvelope,
|
|
39
45
|
heartbeatLeaseHolderStatus,
|
|
40
46
|
markSandboxRestoreVerifying,
|
|
@@ -79,6 +85,7 @@ import {
|
|
|
79
85
|
withoutSandboxProviderIdentity,
|
|
80
86
|
type EstablishedSandboxSession,
|
|
81
87
|
type RuntimeMetricsHooks,
|
|
88
|
+
type SandboxReadinessReplacementOutcome,
|
|
82
89
|
type WorkspaceArchiveDescriptor,
|
|
83
90
|
} from "@opengeni/runtime";
|
|
84
91
|
import {
|
|
@@ -86,7 +93,9 @@ import {
|
|
|
86
93
|
WorkspaceArchiveStorageError,
|
|
87
94
|
type ObjectStorage,
|
|
88
95
|
} from "@opengeni/storage";
|
|
89
|
-
import type
|
|
96
|
+
import { sandboxLeaseTelemetryKey, type Observability } from "@opengeni/observability";
|
|
97
|
+
import { safeSnapshotError } from "./sandbox-snapshot-diagnostics";
|
|
98
|
+
export { safeSnapshotError } from "./sandbox-snapshot-diagnostics";
|
|
90
99
|
import { parseWorkspaceArchiveObjectRef } from "@opengeni/contracts";
|
|
91
100
|
import {
|
|
92
101
|
persistWorkspaceArchiveCandidate,
|
|
@@ -125,9 +134,7 @@ export type SandboxResumeServices = {
|
|
|
125
134
|
db: Database;
|
|
126
135
|
settings: Settings;
|
|
127
136
|
objectStorage?: ObjectStorage | null;
|
|
128
|
-
|
|
129
|
-
* pack/deployment image. Fresh-create NotFound fallback must preserve an
|
|
130
|
-
* ID-only logical base instead of selecting the provider default. */
|
|
137
|
+
|
|
131
138
|
logicalFallbackSettings?: Settings;
|
|
132
139
|
sandboxMetrics?: RuntimeMetricsHooks;
|
|
133
140
|
/** Structured logger for lifecycle decisions (holder release reasons). Falls
|
|
@@ -148,6 +155,26 @@ export type SandboxResumeServices = {
|
|
|
148
155
|
/** Test seam for the bounded command-readiness proof performed before an
|
|
149
156
|
* attached provider box is handed to the agent. */
|
|
150
157
|
verifyAttachedSandboxReadiness?: (established: EstablishedSandboxSession) => Promise<void>;
|
|
158
|
+
/** Test seam for the bounded command-readiness proof the elected spawner runs
|
|
159
|
+
* on its freshly created box before publishing the lease warm. Production
|
|
160
|
+
* uses {@link waitForSandboxExecReadiness} with the shared 60 s budget. */
|
|
161
|
+
verifySpawnedSandboxReadiness?: (
|
|
162
|
+
established: EstablishedSandboxSession,
|
|
163
|
+
identity: { sandboxGroupId: string },
|
|
164
|
+
) => Promise<void>;
|
|
165
|
+
/** Test seam for the jittered pause before the single fresh-box readiness
|
|
166
|
+
* replacement. Production uses {@link freshSandboxReadinessReplacementDelayMs}.
|
|
167
|
+
* It may be async so a test can observe the rolled-back lease in between. */
|
|
168
|
+
freshSandboxReadinessReplacementDelayMs?: () => number | Promise<number>;
|
|
169
|
+
/**
|
|
170
|
+
* The turn attempt's fresh-box readiness replacement budget. The lazy
|
|
171
|
+
* provisioner may call resumeBoxForTurn again after a typed lease
|
|
172
|
+
* supersession, so the turn creates the budget once and shares it with every
|
|
173
|
+
* call. Absent, the call gets its own single replacement.
|
|
174
|
+
*/
|
|
175
|
+
freshSandboxReadinessReplacementBudget?: FreshSandboxReadinessReplacementBudget;
|
|
176
|
+
/** Live fanout for background commands a provider loss settled. */
|
|
177
|
+
bus?: EventBus | null;
|
|
151
178
|
/** Called only by the observer that wins the exact warm->cold loss CAS. */
|
|
152
179
|
onSandboxLost?: (input: {
|
|
153
180
|
sandboxGroupId: string;
|
|
@@ -187,6 +214,9 @@ export type ResumeBoxIds = {
|
|
|
187
214
|
* never conflicts).
|
|
188
215
|
*/
|
|
189
216
|
image?: string;
|
|
217
|
+
/** Deployment/workspace image pins apply only to new creates; an existing
|
|
218
|
+
* group keeps its image. Omission preserves explicit image matching (B3). */
|
|
219
|
+
imagePolicy?: "require_match" | "new_creates_only";
|
|
190
220
|
/**
|
|
191
221
|
* RIG IS SHARED STATE (M3): the frozen rig version this run rides. Threaded to
|
|
192
222
|
* acquireLease, which stamps it on the cold-create and conflicts on a live box
|
|
@@ -316,6 +346,80 @@ export class SandboxSiblingWarmingTimeoutError extends SandboxWarmingTimeoutErro
|
|
|
316
346
|
}
|
|
317
347
|
}
|
|
318
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Out-of-band proof attached to one exact {@link SandboxExecReadinessTimeoutError}
|
|
351
|
+
* thrown by the elected spawner: the box that missed its readiness budget was
|
|
352
|
+
* freshly created by this caller (not an attached, resumed, or provider-continuity
|
|
353
|
+
* box), it was never published warm, the provider terminated it, and the holder
|
|
354
|
+
* was released. Only such an error may be replaced by re-entering admission.
|
|
355
|
+
*/
|
|
356
|
+
const disposedUnreadyFreshSandboxErrors = new WeakSet<object>();
|
|
357
|
+
|
|
358
|
+
/** True only for a spawner readiness timeout whose unready fresh box is proven gone. */
|
|
359
|
+
export function isReplaceableFreshSandboxReadinessTimeout(error: unknown): boolean {
|
|
360
|
+
return (
|
|
361
|
+
error instanceof SandboxExecReadinessTimeoutError &&
|
|
362
|
+
disposedUnreadyFreshSandboxErrors.has(error)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* A burst of simultaneous turn starts can leave many freshly created provider
|
|
368
|
+
* boxes command-unready at once. The single replacement re-enters admission
|
|
369
|
+
* after a jittered pause so the replacement creates do not re-synchronize into
|
|
370
|
+
* the same burst. Bounded to [2 s, 10 s).
|
|
371
|
+
*/
|
|
372
|
+
export const FRESH_SANDBOX_READINESS_REPLACEMENT_BASE_DELAY_MS = 2_000;
|
|
373
|
+
export const FRESH_SANDBOX_READINESS_REPLACEMENT_JITTER_MS = 8_000;
|
|
374
|
+
|
|
375
|
+
export function freshSandboxReadinessReplacementDelayMs(
|
|
376
|
+
random: () => number = Math.random,
|
|
377
|
+
): number {
|
|
378
|
+
const sample = random();
|
|
379
|
+
const unit = Number.isFinite(sample) ? Math.min(Math.max(sample, 0), 1 - Number.EPSILON) : 0;
|
|
380
|
+
return (
|
|
381
|
+
FRESH_SANDBOX_READINESS_REPLACEMENT_BASE_DELAY_MS +
|
|
382
|
+
Math.floor(unit * FRESH_SANDBOX_READINESS_REPLACEMENT_JITTER_MS)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Replacements allowed per turn attempt, across every resumeBoxForTurn call. */
|
|
387
|
+
export const FRESH_SANDBOX_READINESS_REPLACEMENTS_PER_TURN_ATTEMPT = 1;
|
|
388
|
+
|
|
389
|
+
export type FreshSandboxReadinessReplacementBudget = { remaining: number };
|
|
390
|
+
|
|
391
|
+
export function createFreshSandboxReadinessReplacementBudget(): FreshSandboxReadinessReplacementBudget {
|
|
392
|
+
return { remaining: FRESH_SANDBOX_READINESS_REPLACEMENTS_PER_TURN_ATTEMPT };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function recordFreshSandboxReadinessReplacement(
|
|
396
|
+
metrics: RuntimeMetricsHooks | undefined,
|
|
397
|
+
backend: string,
|
|
398
|
+
outcome: SandboxReadinessReplacementOutcome,
|
|
399
|
+
): void {
|
|
400
|
+
try {
|
|
401
|
+
metrics?.onSandboxReadinessReplacement?.({ backend, outcome });
|
|
402
|
+
} catch {
|
|
403
|
+
// Metrics emission must never affect sandbox recovery or error propagation.
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function sleepUnlessCancelled(ms: number, signal: AbortSignal | undefined): Promise<boolean> {
|
|
408
|
+
if (signal?.aborted) return false;
|
|
409
|
+
if (ms <= 0) return true;
|
|
410
|
+
return await new Promise<boolean>((resolve) => {
|
|
411
|
+
const onAbort = () => {
|
|
412
|
+
clearTimeout(timer);
|
|
413
|
+
resolve(false);
|
|
414
|
+
};
|
|
415
|
+
const timer = setTimeout(() => {
|
|
416
|
+
signal?.removeEventListener("abort", onAbort);
|
|
417
|
+
resolve(true);
|
|
418
|
+
}, ms);
|
|
419
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
319
423
|
/** The exact attached caller that won warm->cold after proving the provider
|
|
320
424
|
* instance gone. It is a lease supersession (the same logical turn recovers),
|
|
321
425
|
* plus the lost id needed for one durable observability event. */
|
|
@@ -348,9 +452,10 @@ export async function waitForSandboxExecReadiness(
|
|
|
348
452
|
established: EstablishedSandboxSession,
|
|
349
453
|
timeoutMs = MODAL_EXEC_READINESS_TIMEOUT_MS,
|
|
350
454
|
identity: { sandboxGroupId?: string | null } = {},
|
|
455
|
+
signal?: AbortSignal,
|
|
351
456
|
): Promise<void> {
|
|
352
457
|
try {
|
|
353
|
-
await verifySandboxExecReadiness(established, timeoutMs);
|
|
458
|
+
await verifySandboxExecReadiness(established, timeoutMs, signal);
|
|
354
459
|
} catch (error) {
|
|
355
460
|
if (error instanceof SandboxExecReadinessError && error.code === "exec_probe_timeout") {
|
|
356
461
|
throw new SandboxExecReadinessTimeoutError(established.backendId, timeoutMs, {
|
|
@@ -369,54 +474,6 @@ class SnapshotTimeoutError extends Error {
|
|
|
369
474
|
}
|
|
370
475
|
}
|
|
371
476
|
|
|
372
|
-
export function safeSnapshotError(error: unknown): {
|
|
373
|
-
errorClass: "SnapshotOperationError";
|
|
374
|
-
errorCode: "snapshot_operation_failed";
|
|
375
|
-
status?: number;
|
|
376
|
-
origin: "sandbox-resume";
|
|
377
|
-
causeName?: string;
|
|
378
|
-
integrityCode?: string;
|
|
379
|
-
} {
|
|
380
|
-
const fields: {
|
|
381
|
-
errorClass: "SnapshotOperationError";
|
|
382
|
-
errorCode: "snapshot_operation_failed";
|
|
383
|
-
status?: number;
|
|
384
|
-
origin: "sandbox-resume";
|
|
385
|
-
causeName?: string;
|
|
386
|
-
integrityCode?: string;
|
|
387
|
-
} = {
|
|
388
|
-
errorClass: "SnapshotOperationError",
|
|
389
|
-
errorCode: "snapshot_operation_failed",
|
|
390
|
-
origin: "sandbox-resume",
|
|
391
|
-
};
|
|
392
|
-
try {
|
|
393
|
-
if (error && typeof error === "object") {
|
|
394
|
-
const candidate = error as {
|
|
395
|
-
name?: unknown;
|
|
396
|
-
code?: unknown;
|
|
397
|
-
status?: unknown;
|
|
398
|
-
statusCode?: unknown;
|
|
399
|
-
};
|
|
400
|
-
if (
|
|
401
|
-
typeof candidate.name === "string" &&
|
|
402
|
-
/^[A-Z][A-Za-z0-9]{2,62}Error$/u.test(candidate.name)
|
|
403
|
-
) {
|
|
404
|
-
fields.causeName = candidate.name;
|
|
405
|
-
}
|
|
406
|
-
if (typeof candidate.code === "string" && /^[a-z0-9_]{1,64}$/u.test(candidate.code)) {
|
|
407
|
-
fields.integrityCode = candidate.code;
|
|
408
|
-
}
|
|
409
|
-
const rawStatus = candidate.status ?? candidate.statusCode;
|
|
410
|
-
const status = Number(rawStatus);
|
|
411
|
-
if (Number.isInteger(status) && status >= 100 && status <= 599) fields.status = status;
|
|
412
|
-
}
|
|
413
|
-
} catch {
|
|
414
|
-
// Public diagnostics are best-effort and must never replace the exact
|
|
415
|
-
// internal snapshot failure.
|
|
416
|
-
}
|
|
417
|
-
return fields;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
477
|
export async function waitForWarmSnapshot(
|
|
421
478
|
snapshot: Promise<unknown>,
|
|
422
479
|
timeoutMs: number,
|
|
@@ -582,8 +639,9 @@ async function materializeSpawnEnvelopeArchive(
|
|
|
582
639
|
}
|
|
583
640
|
if (!objectStorage) {
|
|
584
641
|
throw new WorkspaceArchiveIntegrityError(
|
|
585
|
-
"
|
|
642
|
+
"archive_storage_unavailable",
|
|
586
643
|
"workspace archive object storage is not configured",
|
|
644
|
+
{ retryable: true },
|
|
587
645
|
);
|
|
588
646
|
}
|
|
589
647
|
const descriptor = parseWorkspaceArchiveDescriptor(sessionState.workspaceArchiveMeta);
|
|
@@ -787,6 +845,19 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
787
845
|
registerOwnedContinuation: (continuation: Promise<void>) => void,
|
|
788
846
|
): Promise<boolean> {
|
|
789
847
|
const { db, settings } = services;
|
|
848
|
+
const warnSnapshotFailure = (message: string, error: unknown): void => {
|
|
849
|
+
const fields = {
|
|
850
|
+
...safeSnapshotError(error),
|
|
851
|
+
sandboxLeaseKey: sandboxLeaseTelemetryKey(ids.workspaceId, ids.sandboxGroupId),
|
|
852
|
+
leaseEpoch,
|
|
853
|
+
};
|
|
854
|
+
try {
|
|
855
|
+
if (services.observability) services.observability.warn(message, fields);
|
|
856
|
+
else console.warn(message, fields);
|
|
857
|
+
} catch {
|
|
858
|
+
// Diagnostics cannot change provider settlement or capture-gate cleanup.
|
|
859
|
+
}
|
|
860
|
+
};
|
|
790
861
|
const intervalMs = settings.sandboxSnapshotIntervalMs;
|
|
791
862
|
if (intervalMs <= 0 && !force) {
|
|
792
863
|
return false;
|
|
@@ -896,6 +967,9 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
896
967
|
: {}),
|
|
897
968
|
});
|
|
898
969
|
if (claimed.status !== "claimed") {
|
|
970
|
+
// A scheduled interval not being due is normal, including after a failed
|
|
971
|
+
// attempt. Do not turn every ten-second heartbeat into an error log.
|
|
972
|
+
if (claimed.status === "throttled") return false;
|
|
899
973
|
console.error("mid-session workspace snapshot skipped (capture claim)", {
|
|
900
974
|
sandboxGroupId: ids.sandboxGroupId,
|
|
901
975
|
status: claimed.status,
|
|
@@ -951,6 +1025,8 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
951
1025
|
// turn signal resolves first. Its finally block is the only normal release
|
|
952
1026
|
// of the exact admission gate; a late callback cannot release a successor.
|
|
953
1027
|
const captureAndPublish = (async (): Promise<boolean> => {
|
|
1028
|
+
const captureStarted = performance.now();
|
|
1029
|
+
let captureOutcome: "completed" | "failed" = "failed";
|
|
954
1030
|
let archive: VerifiedWorkspaceArchivePayload | undefined;
|
|
955
1031
|
let candidate: { id: string } | null = null;
|
|
956
1032
|
let publicationAttempted = false;
|
|
@@ -1001,6 +1077,7 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
1001
1077
|
if (!wrote && candidate) {
|
|
1002
1078
|
await abandonCandidate(candidate.id, "snapshot_publication_fenced");
|
|
1003
1079
|
}
|
|
1080
|
+
if (wrote) captureOutcome = "completed";
|
|
1004
1081
|
return wrote;
|
|
1005
1082
|
} catch (error) {
|
|
1006
1083
|
if (candidate && !publicationAttempted)
|
|
@@ -1018,20 +1095,24 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
1018
1095
|
expectedEpoch: leaseEpoch,
|
|
1019
1096
|
expectedInstanceId: instanceId,
|
|
1020
1097
|
}).catch((error) => {
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
safeSnapshotError(error),
|
|
1024
|
-
);
|
|
1098
|
+
captureOutcome = "failed";
|
|
1099
|
+
warnSnapshotFailure("mid-session workspace capture gate release failed", error);
|
|
1025
1100
|
});
|
|
1101
|
+
try {
|
|
1102
|
+
services.sandboxMetrics?.onWorkspaceCapture?.({
|
|
1103
|
+
backend: lease.backend,
|
|
1104
|
+
outcome: captureOutcome,
|
|
1105
|
+
durationSeconds: Math.max(0, performance.now() - captureStarted) / 1_000,
|
|
1106
|
+
});
|
|
1107
|
+
} catch {
|
|
1108
|
+
// A completed capture and released gate cannot depend on telemetry.
|
|
1109
|
+
}
|
|
1026
1110
|
}
|
|
1027
1111
|
})();
|
|
1028
1112
|
const settled = captureAndPublish.then(
|
|
1029
1113
|
(persisted) => ({ kind: "settled" as const, persisted }),
|
|
1030
1114
|
(error) => {
|
|
1031
|
-
|
|
1032
|
-
"mid-session workspace snapshot failed (turn unaffected)",
|
|
1033
|
-
safeSnapshotError(error),
|
|
1034
|
-
);
|
|
1115
|
+
warnSnapshotFailure("mid-session workspace snapshot failed (turn unaffected)", error);
|
|
1035
1116
|
return { kind: "settled" as const, persisted: false };
|
|
1036
1117
|
},
|
|
1037
1118
|
);
|
|
@@ -1063,12 +1144,10 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
1063
1144
|
// exact claim cleanup, so it cannot leak an unhandled promise or artifact.
|
|
1064
1145
|
return outcome.kind === "settled" ? outcome.persisted : false;
|
|
1065
1146
|
} catch (error) {
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
safeSnapshotError(error),
|
|
1071
|
-
);
|
|
1147
|
+
// A failed snapshot does not fail the turn. Physical captures can still
|
|
1148
|
+
// fence commands until their exact gate settles; the caller timeout is not
|
|
1149
|
+
// permission to release that gate. The next eligible tick can retry.
|
|
1150
|
+
warnSnapshotFailure("mid-session workspace snapshot failed (turn unaffected)", error);
|
|
1072
1151
|
return false;
|
|
1073
1152
|
}
|
|
1074
1153
|
}
|
|
@@ -1082,12 +1161,103 @@ async function persistWarmWorkspaceSnapshot(
|
|
|
1082
1161
|
* holderId is the globally unique durable turn-attempt id. It must not be a
|
|
1083
1162
|
* Temporal activity id, because activity ids are only workflow-local and
|
|
1084
1163
|
* collide when sibling sessions share one sandbox group.
|
|
1164
|
+
*
|
|
1165
|
+
* A freshly created box that misses its bounded command-readiness budget is
|
|
1166
|
+
* replaced at most once per turn attempt (the budget is shared by every call
|
|
1167
|
+
* the turn's provisioner makes). The first establish has already terminated that exact
|
|
1168
|
+
* unpublished box, rolled its exact warming epoch back to cold (advancing the
|
|
1169
|
+
* epoch), and released its holder, so the replacement is an ordinary new
|
|
1170
|
+
* admission: it re-runs the cold->warming CAS, persists its own provider
|
|
1171
|
+
* instance before readiness, and may instead attach to a sibling that won the
|
|
1172
|
+
* CAS first. Nothing model- or tool-visible ran on the discarded box, so no
|
|
1173
|
+
* side effect is replayed. A second readiness timeout fails the turn exactly
|
|
1174
|
+
* as before; attached/resumed boxes and unconfirmed terminations never
|
|
1175
|
+
* replace.
|
|
1085
1176
|
*/
|
|
1086
1177
|
export async function resumeBoxForTurn(
|
|
1087
1178
|
services: SandboxResumeServices,
|
|
1088
1179
|
ids: ResumeBoxIds,
|
|
1089
1180
|
kind: "turn",
|
|
1090
1181
|
holderId: TurnSandboxLeaseHolderId,
|
|
1182
|
+
): Promise<ResumedTurnSandbox> {
|
|
1183
|
+
try {
|
|
1184
|
+
return await resumeBoxForTurnOnce(services, ids, kind, holderId);
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
if (!isReplaceableFreshSandboxReadinessTimeout(error)) throw error;
|
|
1187
|
+
const readiness = error as SandboxExecReadinessTimeoutError;
|
|
1188
|
+
const warn = (message: string, fields: Parameters<Observability["warn"]>[1]) => {
|
|
1189
|
+
if (services.observability) services.observability.warn(message, fields);
|
|
1190
|
+
else console.warn(message, fields);
|
|
1191
|
+
};
|
|
1192
|
+
const identity = {
|
|
1193
|
+
workspaceId: ids.workspaceId,
|
|
1194
|
+
sessionId: ids.sessionId,
|
|
1195
|
+
sandboxGroupId: ids.sandboxGroupId,
|
|
1196
|
+
backend: readiness.backend,
|
|
1197
|
+
instanceId: readiness.instanceId,
|
|
1198
|
+
readinessTimeoutMs: readiness.timeoutMs,
|
|
1199
|
+
};
|
|
1200
|
+
const budget =
|
|
1201
|
+
services.freshSandboxReadinessReplacementBudget ??
|
|
1202
|
+
createFreshSandboxReadinessReplacementBudget();
|
|
1203
|
+
if (budget.remaining <= 0) {
|
|
1204
|
+
recordFreshSandboxReadinessReplacement(
|
|
1205
|
+
services.sandboxMetrics,
|
|
1206
|
+
readiness.backend,
|
|
1207
|
+
"budget_spent",
|
|
1208
|
+
);
|
|
1209
|
+
warn(
|
|
1210
|
+
"sandbox command-readiness timed out on a fresh box; this turn attempt already used its replacement",
|
|
1211
|
+
identity,
|
|
1212
|
+
);
|
|
1213
|
+
throw error;
|
|
1214
|
+
}
|
|
1215
|
+
budget.remaining -= 1;
|
|
1216
|
+
const delayMs = await (
|
|
1217
|
+
services.freshSandboxReadinessReplacementDelayMs ?? freshSandboxReadinessReplacementDelayMs
|
|
1218
|
+
)();
|
|
1219
|
+
warn("sandbox command-readiness timed out on a fresh box; replacing it once after jitter", {
|
|
1220
|
+
...identity,
|
|
1221
|
+
replacementDelayMs: delayMs,
|
|
1222
|
+
});
|
|
1223
|
+
if (!(await sleepUnlessCancelled(delayMs, services.cancellationSignal))) {
|
|
1224
|
+
// Cancellation owns the turn boundary; surface the original typed
|
|
1225
|
+
// readiness failure rather than starting a replacement.
|
|
1226
|
+
recordFreshSandboxReadinessReplacement(
|
|
1227
|
+
services.sandboxMetrics,
|
|
1228
|
+
readiness.backend,
|
|
1229
|
+
"cancelled",
|
|
1230
|
+
);
|
|
1231
|
+
throw error;
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
const replaced = await resumeBoxForTurnOnce(services, ids, kind, holderId);
|
|
1235
|
+
recordFreshSandboxReadinessReplacement(
|
|
1236
|
+
services.sandboxMetrics,
|
|
1237
|
+
readiness.backend,
|
|
1238
|
+
"replaced",
|
|
1239
|
+
);
|
|
1240
|
+
return replaced;
|
|
1241
|
+
} catch (replacementError) {
|
|
1242
|
+
recordFreshSandboxReadinessReplacement(
|
|
1243
|
+
services.sandboxMetrics,
|
|
1244
|
+
readiness.backend,
|
|
1245
|
+
services.cancellationSignal?.aborted
|
|
1246
|
+
? "cancelled"
|
|
1247
|
+
: replacementError instanceof SandboxExecReadinessTimeoutError
|
|
1248
|
+
? "failed_again"
|
|
1249
|
+
: "replacement_failed",
|
|
1250
|
+
);
|
|
1251
|
+
throw replacementError;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
async function resumeBoxForTurnOnce(
|
|
1257
|
+
services: SandboxResumeServices,
|
|
1258
|
+
ids: ResumeBoxIds,
|
|
1259
|
+
kind: "turn",
|
|
1260
|
+
holderId: TurnSandboxLeaseHolderId,
|
|
1091
1261
|
): Promise<ResumedTurnSandbox> {
|
|
1092
1262
|
const { db, settings } = services;
|
|
1093
1263
|
const os = ids.os ?? "linux";
|
|
@@ -1179,13 +1349,18 @@ export async function resumeBoxForTurn(
|
|
|
1179
1349
|
holderId,
|
|
1180
1350
|
subjectId: ids.sessionId,
|
|
1181
1351
|
backend: ids.backend,
|
|
1352
|
+
warmBilling: {
|
|
1353
|
+
mode: settings.sandboxWarmBillingMode,
|
|
1354
|
+
rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(settings, ids.backend),
|
|
1355
|
+
},
|
|
1182
1356
|
os,
|
|
1183
1357
|
// IMAGE IS SHARED STATE (B3): thread the resolved image so the lease stamps it +
|
|
1184
|
-
// conflicts on a live box already running a different image. A
|
|
1358
|
+
// conflicts on a live box already running a different required image. A
|
|
1185
1359
|
// SandboxImageConflictError propagates while another holder is active; a
|
|
1186
1360
|
// solo change requests a capture-and-drain rotation and this attempt retries
|
|
1187
1361
|
// after the cold successor can safely stamp the new image.
|
|
1188
1362
|
...(ids.image ? { image: ids.image } : {}),
|
|
1363
|
+
...(ids.imagePolicy ? { imagePolicy: ids.imagePolicy } : {}),
|
|
1189
1364
|
// RIG IS SHARED STATE (M3): thread the frozen rig version so the lease stamps it
|
|
1190
1365
|
// + conflicts on a live box under a different rig. A SandboxRigConflictError
|
|
1191
1366
|
// propagates while another holder is active; a solo change uses the same
|
|
@@ -1276,7 +1451,16 @@ export async function resumeBoxForTurn(
|
|
|
1276
1451
|
// provider operation may still be mutating /workspace. Both loops therefore
|
|
1277
1452
|
// converge on "holder alive -> never release"; finalization performs the
|
|
1278
1453
|
// proof-bearing release.
|
|
1279
|
-
const releaseDeadHolder = (reason: string): void => {
|
|
1454
|
+
const releaseDeadHolder = async (reason: string): Promise<void> => {
|
|
1455
|
+
// Logical closure deliberately fences heartbeat authority. It is not proof
|
|
1456
|
+
// that the already-admitted capture has physically settled: the finalizer
|
|
1457
|
+
// owns that wait and the original deadline bounds dead-worker recovery.
|
|
1458
|
+
// Match the reaper's retention rule without touching holder/lease TTLs.
|
|
1459
|
+
if (reason === "holder_gone" && kind === "turn" && holderLeaseHeartbeat) {
|
|
1460
|
+
const expectedEpoch = holderLeaseHeartbeat.expectedEpoch;
|
|
1461
|
+
const lease = await readLease(db, ids.workspaceId, ids.sandboxGroupId);
|
|
1462
|
+
if (retainsHolderForWarmCapture(lease, expectedEpoch)) return;
|
|
1463
|
+
}
|
|
1280
1464
|
const message = "sandbox resume holder released: holder no longer live";
|
|
1281
1465
|
const fields = {
|
|
1282
1466
|
workspaceId: ids.workspaceId,
|
|
@@ -1300,14 +1484,15 @@ export async function resumeBoxForTurn(
|
|
|
1300
1484
|
holderId,
|
|
1301
1485
|
expectedEpoch: heartbeat.expectedEpoch,
|
|
1302
1486
|
leaseTtlMs: heartbeat.leaseTtlMs,
|
|
1487
|
+
billingMode: services.settings.sandboxWarmBillingMode,
|
|
1303
1488
|
})
|
|
1304
|
-
.then((status) => {
|
|
1489
|
+
.then(async (status) => {
|
|
1305
1490
|
if (heartbeat !== holderLeaseHeartbeat) return;
|
|
1306
1491
|
if (!status.holderAlive) {
|
|
1307
1492
|
// Reaped, released, or its exact attempt is no longer the active
|
|
1308
1493
|
// writer. Stop this otherwise-unbounded provider operation and
|
|
1309
1494
|
// idempotently drop any remaining holder state.
|
|
1310
|
-
releaseDeadHolder(status.fence);
|
|
1495
|
+
await releaseDeadHolder(status.fence);
|
|
1311
1496
|
return;
|
|
1312
1497
|
}
|
|
1313
1498
|
if (status.fence === "epoch") {
|
|
@@ -1316,7 +1501,7 @@ export async function resumeBoxForTurn(
|
|
|
1316
1501
|
// instance replaced, re-established by a later turn). The holder
|
|
1317
1502
|
// row is stale authority for an instance that no longer exists;
|
|
1318
1503
|
// drop it promptly instead of pinning a successor's lease.
|
|
1319
|
-
releaseDeadHolder(status.fence);
|
|
1504
|
+
await releaseDeadHolder(status.fence);
|
|
1320
1505
|
return;
|
|
1321
1506
|
}
|
|
1322
1507
|
// The box is still serving this live holder: keep the provider TTL
|
|
@@ -1338,13 +1523,13 @@ export async function resumeBoxForTurn(
|
|
|
1338
1523
|
kind,
|
|
1339
1524
|
holderId,
|
|
1340
1525
|
})
|
|
1341
|
-
.then((touched) => {
|
|
1526
|
+
.then(async (touched) => {
|
|
1342
1527
|
if (heartbeat !== holderLeaseHeartbeat) return;
|
|
1343
1528
|
// A canonical turn holder is rejected once its exact attempt is no
|
|
1344
1529
|
// longer the active writer (touch is attempt-fenced only; it never
|
|
1345
1530
|
// consults lease authority, so false here always means holder death).
|
|
1346
1531
|
if (!touched) {
|
|
1347
|
-
releaseDeadHolder("holder_gone");
|
|
1532
|
+
await releaseDeadHolder("holder_gone");
|
|
1348
1533
|
return;
|
|
1349
1534
|
}
|
|
1350
1535
|
void maybeRenewProviderExpiration().catch(() => undefined);
|
|
@@ -1391,6 +1576,8 @@ export async function resumeBoxForTurn(
|
|
|
1391
1576
|
if (acquired.role === "spawner") {
|
|
1392
1577
|
const expectedEpoch = acquired.lease.leaseEpoch;
|
|
1393
1578
|
let createdEstablished: EstablishedSandboxSession | null = null;
|
|
1579
|
+
let providerCreateOperationId: string | undefined;
|
|
1580
|
+
let providerCreateBindingKey: string | undefined;
|
|
1394
1581
|
let rematerialization: {
|
|
1395
1582
|
id: string;
|
|
1396
1583
|
selectedRevision: string;
|
|
@@ -1402,29 +1589,53 @@ export async function resumeBoxForTurn(
|
|
|
1402
1589
|
> | null;
|
|
1403
1590
|
} | null = null;
|
|
1404
1591
|
try {
|
|
1405
|
-
|
|
1592
|
+
// An audited decision to continue a definitively lost workspace on a new
|
|
1593
|
+
// EMPTY box: hydrate nothing (not even a per-session legacy archive) and
|
|
1594
|
+
// resume no prior provider identity. Every group member was already told
|
|
1595
|
+
// the workspace is empty; the DB accepts only this exact publication.
|
|
1596
|
+
const freshWorkspaceRecoveryId = acquired.lease.freshWorkspaceRecoveryId ?? null;
|
|
1597
|
+
const envelope = freshWorkspaceRecoveryId
|
|
1598
|
+
? null
|
|
1599
|
+
: await getSandboxSessionEnvelope(db, ids.workspaceId, ids.sessionId);
|
|
1406
1600
|
// The lease is authoritative. A legacy per-session fallback archive may
|
|
1407
1601
|
// only be used after beginSandboxRematerialization imports its archive
|
|
1408
1602
|
// fields under the warming-row lock and records one selected revision.
|
|
1409
1603
|
// Select the fallback only when the lease has no archive truth at all. A
|
|
1410
1604
|
// lease-carried unverified/invalid archive must fail closed rather than
|
|
1411
1605
|
// silently substituting another revision.
|
|
1606
|
+
// Backstop: a session already told its workspace is empty never gets a
|
|
1607
|
+
// legacy per-session archive back, even if the lease marker was lost.
|
|
1608
|
+
const legacyFallbackRefused =
|
|
1609
|
+
!freshWorkspaceRecoveryId &&
|
|
1610
|
+
acquired.lease.recovery.archive.status === "none" &&
|
|
1611
|
+
workspaceArchiveFieldsFromEnvelope(envelope) !== null &&
|
|
1612
|
+
(await sessionHoldsFreshWorkspaceRecovery(db, ids.workspaceId, ids.sessionId));
|
|
1412
1613
|
const fallbackArchiveEnvelope =
|
|
1614
|
+
!freshWorkspaceRecoveryId &&
|
|
1615
|
+
!legacyFallbackRefused &&
|
|
1413
1616
|
acquired.lease.recovery.archive.status === "none" &&
|
|
1414
1617
|
workspaceArchiveFieldsFromEnvelope(envelope) !== null
|
|
1415
1618
|
? withoutSandboxProviderIdentity(envelope)
|
|
1416
1619
|
: null;
|
|
1417
|
-
let spawnEnvelope =
|
|
1620
|
+
let spawnEnvelope = freshWorkspaceRecoveryId
|
|
1621
|
+
? null
|
|
1622
|
+
: legacyFallbackRefused
|
|
1623
|
+
? (acquired.lease.resumeState ?? null)
|
|
1624
|
+
: (fallbackArchiveEnvelope ?? acquired.lease.resumeState ?? envelope);
|
|
1418
1625
|
const archiveSource =
|
|
1419
1626
|
acquired.lease.recovery.archive.status === "none"
|
|
1420
1627
|
? fallbackArchiveEnvelope
|
|
1421
1628
|
: acquired.lease.resumeState;
|
|
1422
1629
|
const continuityRecovery = acquired.lease.recovery.continuity;
|
|
1630
|
+
// A fresh-workspace spawn is deliberately archive-free; commitWarmingToWarm
|
|
1631
|
+
// verifies the exact decision before publication.
|
|
1423
1632
|
if (
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1633
|
+
!freshWorkspaceRecoveryId &&
|
|
1634
|
+
((acquired.lease.recovery.archive.status === "available" &&
|
|
1635
|
+
(acquired.lease.archiveComplete ||
|
|
1636
|
+
acquired.lease.historicalRecoveryAuthorized === true)) ||
|
|
1637
|
+
(acquired.lease.recovery.archive.status === "none" &&
|
|
1638
|
+
workspaceArchiveFieldsFromEnvelope(archiveSource) !== null))
|
|
1428
1639
|
) {
|
|
1429
1640
|
const rematerializationId = crypto.randomUUID();
|
|
1430
1641
|
const legacyNativeArchive = legacyNativeArchiveFromEnvelope(archiveSource);
|
|
@@ -1467,7 +1678,11 @@ export async function resumeBoxForTurn(
|
|
|
1467
1678
|
legacyCheckpoint: begun.checkpointArtifact === null ? legacyNativeArchive : null,
|
|
1468
1679
|
legacyProviderBinding: null,
|
|
1469
1680
|
};
|
|
1470
|
-
} else if (
|
|
1681
|
+
} else if (
|
|
1682
|
+
!freshWorkspaceRecoveryId &&
|
|
1683
|
+
acquired.lease.recovery.archive.status !== "none" &&
|
|
1684
|
+
!continuityRecovery
|
|
1685
|
+
) {
|
|
1471
1686
|
throw new SandboxLeaseRecoveryBlockedError(
|
|
1472
1687
|
ids.sandboxGroupId,
|
|
1473
1688
|
expectedEpoch,
|
|
@@ -1526,6 +1741,34 @@ export async function resumeBoxForTurn(
|
|
|
1526
1741
|
...(services.logicalFallbackSettings
|
|
1527
1742
|
? { logicalFallbackSettings: services.logicalFallbackSettings }
|
|
1528
1743
|
: {}),
|
|
1744
|
+
onBeforeSandboxCreate: async (createSettings, intent, providerContext) => {
|
|
1745
|
+
if (ids.backend !== "modal") return;
|
|
1746
|
+
if (!intent || !providerContext)
|
|
1747
|
+
throw new Error("Modal create requires the provider dispatch boundary");
|
|
1748
|
+
throwIfReleasedOrCancelled();
|
|
1749
|
+
const binding = await resolveModalCheckpointProviderBindingForSession(
|
|
1750
|
+
createSettings,
|
|
1751
|
+
providerContext,
|
|
1752
|
+
);
|
|
1753
|
+
const operationId = intent.operationId;
|
|
1754
|
+
await beginModalProviderCreate(db, {
|
|
1755
|
+
accountId: ids.accountId,
|
|
1756
|
+
workspaceId: ids.workspaceId,
|
|
1757
|
+
sandboxGroupId: ids.sandboxGroupId,
|
|
1758
|
+
expectedEpoch,
|
|
1759
|
+
operationId,
|
|
1760
|
+
providerBindingKey: binding.key,
|
|
1761
|
+
rematerializationId: rematerialization?.id ?? null,
|
|
1762
|
+
selectedRevision: rematerialization?.selectedRevision ?? null,
|
|
1763
|
+
imageId: intent.imageId,
|
|
1764
|
+
imageRef: createSettings.modalImageRef ?? null,
|
|
1765
|
+
appId: intent.appId,
|
|
1766
|
+
providerName: intent.name,
|
|
1767
|
+
requestSha256: intent.requestSha256,
|
|
1768
|
+
});
|
|
1769
|
+
providerCreateOperationId = operationId;
|
|
1770
|
+
providerCreateBindingKey = binding.key;
|
|
1771
|
+
},
|
|
1529
1772
|
onSandboxCreated: async (created) => {
|
|
1530
1773
|
createdEstablished = created;
|
|
1531
1774
|
providerRenewalTarget = {
|
|
@@ -1533,7 +1776,16 @@ export async function resumeBoxForTurn(
|
|
|
1533
1776
|
instanceId: created.instanceId,
|
|
1534
1777
|
};
|
|
1535
1778
|
providerRenewedAtMs = Date.now();
|
|
1536
|
-
|
|
1779
|
+
if (
|
|
1780
|
+
providerCreateBindingKey &&
|
|
1781
|
+
!(await modalSessionMatchesCheckpointProviderBinding(
|
|
1782
|
+
settings,
|
|
1783
|
+
created.session,
|
|
1784
|
+
providerCreateBindingKey,
|
|
1785
|
+
))
|
|
1786
|
+
) {
|
|
1787
|
+
throw new Error("Modal creation receipt crossed the fenced provider namespace");
|
|
1788
|
+
}
|
|
1537
1789
|
if (
|
|
1538
1790
|
rematerialization &&
|
|
1539
1791
|
(rematerialization.providerBindingKey || rematerialization.legacyCheckpoint)
|
|
@@ -1584,6 +1836,7 @@ export async function resumeBoxForTurn(
|
|
|
1584
1836
|
sandboxGroupId: ids.sandboxGroupId,
|
|
1585
1837
|
expectedEpoch,
|
|
1586
1838
|
rematerializationId: rematerialization?.id ?? null,
|
|
1839
|
+
...(providerCreateOperationId ? { providerCreateOperationId } : {}),
|
|
1587
1840
|
...(created.providerContinuity
|
|
1588
1841
|
? { continuityRecovery: created.providerContinuity }
|
|
1589
1842
|
: {}),
|
|
@@ -1666,11 +1919,22 @@ export async function resumeBoxForTurn(
|
|
|
1666
1919
|
// A sandbox handle is not sufficient evidence that an asynchronous
|
|
1667
1920
|
// provider's command router is live. Do not publish a warm lease until
|
|
1668
1921
|
// one bounded no-op exec works.
|
|
1669
|
-
// On timeout the catch below terminates the box and rolls warming -> cold
|
|
1670
|
-
//
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1922
|
+
// On timeout the catch below terminates the box and rolls warming -> cold;
|
|
1923
|
+
// resumeBoxForTurn may then replace a proven-terminated fresh box once.
|
|
1924
|
+
if (services.verifySpawnedSandboxReadiness) {
|
|
1925
|
+
await services.verifySpawnedSandboxReadiness(established, {
|
|
1926
|
+
sandboxGroupId: ids.sandboxGroupId,
|
|
1927
|
+
});
|
|
1928
|
+
} else {
|
|
1929
|
+
await waitForSandboxExecReadiness(
|
|
1930
|
+
established,
|
|
1931
|
+
MODAL_EXEC_READINESS_TIMEOUT_MS,
|
|
1932
|
+
{
|
|
1933
|
+
sandboxGroupId: ids.sandboxGroupId,
|
|
1934
|
+
},
|
|
1935
|
+
cancellationSignal,
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1674
1938
|
await maybeRenewProviderExpiration(true);
|
|
1675
1939
|
throwIfReleasedOrCancelled();
|
|
1676
1940
|
// Fold the LIVE box into a re-resumable envelope and persist it as the
|
|
@@ -1716,16 +1980,18 @@ export async function resumeBoxForTurn(
|
|
|
1716
1980
|
dataPlaneUrl: null,
|
|
1717
1981
|
resumeBackendId: established.backendId,
|
|
1718
1982
|
resumeState: resumeEnvelope,
|
|
1719
|
-
...(
|
|
1720
|
-
? {
|
|
1721
|
-
:
|
|
1722
|
-
? {
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1983
|
+
...(freshWorkspaceRecoveryId
|
|
1984
|
+
? { freshWorkspace: { operationId: freshWorkspaceRecoveryId } }
|
|
1985
|
+
: established.providerContinuity
|
|
1986
|
+
? { continuityRecovery: established.providerContinuity }
|
|
1987
|
+
: rematerialization
|
|
1988
|
+
? {
|
|
1989
|
+
rematerialization: {
|
|
1990
|
+
id: rematerialization.id,
|
|
1991
|
+
verifiedRevision: rematerialization.selectedRevision,
|
|
1992
|
+
},
|
|
1993
|
+
}
|
|
1994
|
+
: {}),
|
|
1729
1995
|
leaseTtlMs,
|
|
1730
1996
|
});
|
|
1731
1997
|
if (!committed.committed || !committed.lease) {
|
|
@@ -1806,6 +2072,16 @@ export async function resumeBoxForTurn(
|
|
|
1806
2072
|
}
|
|
1807
2073
|
await release();
|
|
1808
2074
|
recordSandboxWarmingTimeout(services.sandboxMetrics, error);
|
|
2075
|
+
if (
|
|
2076
|
+
terminated &&
|
|
2077
|
+
createdEstablished &&
|
|
2078
|
+
error instanceof SandboxExecReadinessTimeoutError &&
|
|
2079
|
+
error.instanceId === createdEstablished.instanceId &&
|
|
2080
|
+
(createdEstablished.origin === "created" || createdEstablished.origin === "restored") &&
|
|
2081
|
+
!createdEstablished.providerContinuity
|
|
2082
|
+
) {
|
|
2083
|
+
disposedUnreadyFreshSandboxErrors.add(error);
|
|
2084
|
+
}
|
|
1809
2085
|
throw sandboxProvisionStageError(rematerialization ? "archive_recovery" : "create", error);
|
|
1810
2086
|
}
|
|
1811
2087
|
}
|
|
@@ -1855,9 +2131,14 @@ export async function resumeBoxForTurn(
|
|
|
1855
2131
|
if (services.verifyAttachedSandboxReadiness) {
|
|
1856
2132
|
await services.verifyAttachedSandboxReadiness(established);
|
|
1857
2133
|
} else {
|
|
1858
|
-
await waitForSandboxExecReadiness(
|
|
1859
|
-
|
|
1860
|
-
|
|
2134
|
+
await waitForSandboxExecReadiness(
|
|
2135
|
+
established,
|
|
2136
|
+
MODAL_EXEC_READINESS_TIMEOUT_MS,
|
|
2137
|
+
{
|
|
2138
|
+
sandboxGroupId: ids.sandboxGroupId,
|
|
2139
|
+
},
|
|
2140
|
+
cancellationSignal,
|
|
2141
|
+
);
|
|
1861
2142
|
}
|
|
1862
2143
|
providerRenewalTarget = {
|
|
1863
2144
|
backend: ids.backend,
|
|
@@ -1875,8 +2156,14 @@ export async function resumeBoxForTurn(
|
|
|
1875
2156
|
sandboxGroupId: ids.sandboxGroupId,
|
|
1876
2157
|
expectedEpoch: leaseEpoch,
|
|
1877
2158
|
expectedInstanceId: live.instanceId,
|
|
2159
|
+
expectedBackend: ids.backend,
|
|
1878
2160
|
});
|
|
1879
2161
|
if (marked.status === "marked") {
|
|
2162
|
+
await publishDurableSessionEvents(
|
|
2163
|
+
services.bus,
|
|
2164
|
+
ids.workspaceId,
|
|
2165
|
+
marked.backgroundCommandEvents,
|
|
2166
|
+
);
|
|
1880
2167
|
await services.onSandboxLost?.({
|
|
1881
2168
|
sandboxGroupId: ids.sandboxGroupId,
|
|
1882
2169
|
instanceId: live.instanceId,
|