@opengeni/runtime 0.6.0 → 0.7.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/{chunk-BHA2SU3H.js → chunk-RYDCVTMW.js} +1959 -208
- package/dist/chunk-RYDCVTMW.js.map +1 -0
- package/dist/{index-otZkoLYq.d.ts → index-DIiiwQY2.d.ts} +693 -55
- package/dist/index.d.ts +293 -121
- package/dist/index.js +1819 -786
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +79 -3
- package/package.json +21 -20
- package/src/codex-tool-search.ts +185 -31
- package/src/context-compaction.ts +78 -243
- package/src/history-sanitizer.ts +172 -20
- package/src/index.ts +2573 -897
- package/src/metrics.ts +28 -2
- package/src/sandbox/capabilities.ts +12 -3
- package/src/sandbox/channel-a.ts +420 -72
- package/src/sandbox/display-stack.ts +18 -4
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +398 -112
- package/src/sandbox/op-correlation.ts +70 -0
- package/src/sandbox/providers/index.ts +9 -3
- package/src/sandbox/providers/modal.ts +99 -24
- package/src/sandbox/providers/selfhosted.ts +1 -4
- package/src/sandbox/recording.ts +146 -90
- package/src/sandbox/routing/backend-resolver.ts +144 -10
- package/src/sandbox/routing/routing-session.ts +129 -25
- package/src/sandbox/select.ts +60 -8
- package/src/sandbox/selfhosted/capabilities.ts +59 -25
- package/src/sandbox/selfhosted/control-rpc.ts +160 -28
- package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
- package/src/sandbox/selfhosted/op-observer.ts +54 -0
- package/src/sandbox/selfhosted/op-stream.ts +801 -0
- package/src/sandbox/selfhosted/op-testing.ts +468 -0
- package/src/sandbox/selfhosted/op-transport.ts +151 -0
- package/src/sandbox/selfhosted/retry-policy.ts +177 -0
- package/src/sandbox/selfhosted/session.ts +498 -55
- package/src/sandbox/selfhosted/testing.ts +60 -19
- package/src/sandbox/stream-port.ts +17 -5
- package/src/sandbox/stream-token.ts +8 -2
- package/src/sandbox/terminal-server.ts +18 -4
- package/src/sandbox-computer.ts +332 -70
- package/src/usage-telemetry.ts +73 -0
- package/dist/chunk-BHA2SU3H.js.map +0 -1
- package/src/image-history.ts +0 -149
package/src/sandbox/index.ts
CHANGED
|
@@ -20,14 +20,18 @@
|
|
|
20
20
|
|
|
21
21
|
import type { Settings } from "@opengeni/config";
|
|
22
22
|
import { collectSandboxEnvironment, parseExposedPorts } from "@opengeni/config";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DESKTOP_STREAM_PORT,
|
|
25
|
+
TERMINAL_STREAM_PORT,
|
|
26
|
+
type SandboxBackend,
|
|
27
|
+
} from "@opengeni/contracts";
|
|
24
28
|
import type {
|
|
25
29
|
SandboxClient,
|
|
26
30
|
SandboxSessionLike,
|
|
27
31
|
SandboxSessionState,
|
|
28
32
|
} from "@openai/agents/sandbox";
|
|
29
33
|
import { PROVIDER_REGISTRY } from "./providers";
|
|
30
|
-
import { SandboxConfigError } from "./errors";
|
|
34
|
+
import { SandboxConfigError, SandboxResumeStateUnavailableError } from "./errors";
|
|
31
35
|
import { isSelfhostedProviderNotFoundError } from "./selfhosted/session";
|
|
32
36
|
import type { RuntimeMetricsHooks } from "../metrics";
|
|
33
37
|
|
|
@@ -47,7 +51,11 @@ export {
|
|
|
47
51
|
assertDescriptorRegistryInvariants,
|
|
48
52
|
type CapabilityDescriptor,
|
|
49
53
|
} from "./capabilities";
|
|
50
|
-
export {
|
|
54
|
+
export {
|
|
55
|
+
SandboxConfigError,
|
|
56
|
+
SandboxProviderUnavailableError,
|
|
57
|
+
SandboxResumeStateUnavailableError,
|
|
58
|
+
} from "./errors";
|
|
51
59
|
export {
|
|
52
60
|
PROVIDER_REGISTRY,
|
|
53
61
|
assertProviderRegistryInvariants,
|
|
@@ -130,11 +138,12 @@ export {
|
|
|
130
138
|
} from "./stream-port";
|
|
131
139
|
|
|
132
140
|
// P4.3 recording loop — plain functions over a live session handle (no agent
|
|
133
|
-
// loop); finalize
|
|
141
|
+
// loop); finalize uploads box -> object storage without buffering in the worker.
|
|
134
142
|
export {
|
|
135
143
|
startRecording,
|
|
136
144
|
stopRecording,
|
|
137
|
-
|
|
145
|
+
inspectRecordingArtifact,
|
|
146
|
+
uploadRecordingArtifact,
|
|
138
147
|
deleteRecordingArtifacts,
|
|
139
148
|
recordingStorageKey,
|
|
140
149
|
contentTypeForCodec,
|
|
@@ -145,7 +154,7 @@ export {
|
|
|
145
154
|
type RecordingContentType,
|
|
146
155
|
type StartRecordingInput,
|
|
147
156
|
type RecordingProcess,
|
|
148
|
-
type
|
|
157
|
+
type RecordingArtifactMetadata,
|
|
149
158
|
} from "./recording";
|
|
150
159
|
|
|
151
160
|
// P4.4 Channel-A structured services — the provider-agnostic SandboxChannelAService
|
|
@@ -166,11 +175,14 @@ export {
|
|
|
166
175
|
parsePorcelainV2,
|
|
167
176
|
parseNumstatZ,
|
|
168
177
|
parseUnifiedPatch,
|
|
178
|
+
REPOSITORY_DISCOVERY_LIMIT,
|
|
169
179
|
type ChannelASession,
|
|
170
180
|
type ChannelAExecArgs,
|
|
171
181
|
type ChannelAExecResult,
|
|
172
182
|
type ChannelAEmitter,
|
|
173
183
|
type SandboxChannelAServiceOptions,
|
|
184
|
+
type RepositoryDiscoveryDegradedReason,
|
|
185
|
+
type RepositoryDiscoveryResult,
|
|
174
186
|
type NumstatEntry,
|
|
175
187
|
} from "./channel-a";
|
|
176
188
|
|
|
@@ -190,9 +202,43 @@ export {
|
|
|
190
202
|
timeoutControlResponse,
|
|
191
203
|
offlineAgentError,
|
|
192
204
|
timeoutAgentError,
|
|
205
|
+
drainingExhaustedError,
|
|
206
|
+
payloadTooLargeMessage,
|
|
207
|
+
drainingMessage,
|
|
208
|
+
execDeadlineHint,
|
|
209
|
+
NEVER_SENT_DETAIL_KEY,
|
|
193
210
|
type NatsRequestConnection,
|
|
194
211
|
type SelfhostedUnavailableReason,
|
|
195
212
|
} from "./selfhosted/control-rpc";
|
|
213
|
+
// The transport-agnostic per-op observation seam (out-of-band telemetry — metrics
|
|
214
|
+
// + machine.* events; the op-engine's op-stream client emits through this too).
|
|
215
|
+
export type { SelfhostedOpObserver, SelfhostedOpObservation } from "./selfhosted/op-observer";
|
|
216
|
+
// The four-field in-band fault renderer (the failure-visibility doctrine).
|
|
217
|
+
export {
|
|
218
|
+
selfhostedFaultClass,
|
|
219
|
+
SELFHOSTED_INFRASTRUCTURE_FAULT_CLASSES,
|
|
220
|
+
renderSelfhostedFault,
|
|
221
|
+
FAULT_FIELD_WHAT_HAPPENED,
|
|
222
|
+
FAULT_FIELD_WHICH_LAYER,
|
|
223
|
+
FAULT_FIELD_WHAT_PRESERVED,
|
|
224
|
+
FAULT_FIELD_WHAT_TO_TRY,
|
|
225
|
+
} from "./selfhosted/fault-rendering";
|
|
226
|
+
// The selfhosted control-op retry policy (the pure decision + the injected clock).
|
|
227
|
+
export {
|
|
228
|
+
decideSelfhostedRetry,
|
|
229
|
+
selfhostedRetryBackoffMs,
|
|
230
|
+
defaultSelfhostedRetryClock,
|
|
231
|
+
SELFHOSTED_IDEMPOTENT_READONLY_OPS,
|
|
232
|
+
SELFHOSTED_DRAINING_MAX_RETRIES,
|
|
233
|
+
SELFHOSTED_EXEC_DRAINING_MAX_RETRIES,
|
|
234
|
+
SELFHOSTED_TIMEOUT_MAX_RETRIES,
|
|
235
|
+
SELFHOSTED_NEVER_SENT_MAX_RETRIES,
|
|
236
|
+
SELFHOSTED_RETRY_BACKOFF_BASE_MS,
|
|
237
|
+
SELFHOSTED_RETRY_BACKOFF_FACTOR,
|
|
238
|
+
SELFHOSTED_RETRY_BACKOFF_CAP_MS,
|
|
239
|
+
type SelfhostedRetryClock,
|
|
240
|
+
type SelfhostedRetryDecision,
|
|
241
|
+
} from "./selfhosted/retry-policy";
|
|
196
242
|
export {
|
|
197
243
|
SelfhostedSession,
|
|
198
244
|
SelfhostedSandboxClient,
|
|
@@ -210,7 +256,32 @@ export {
|
|
|
210
256
|
type SelfhostedApplyDiff,
|
|
211
257
|
type SelfhostedEditor,
|
|
212
258
|
type SelfhostedImageOutput,
|
|
259
|
+
type SelfhostedOpStreamDeps,
|
|
213
260
|
} from "./selfhosted/session";
|
|
261
|
+
// The op-stream exec transport (op-stream protocol v1.1 — streaming exec to a
|
|
262
|
+
// Connected Machine runner). The worker injects `NatsOpStreamTransport` (over
|
|
263
|
+
// the same bus connection as the control rpc) plus an `OpStreamJournal`
|
|
264
|
+
// adapted onto Temporal; sessions without the injection keep the legacy exec.
|
|
265
|
+
export {
|
|
266
|
+
NatsOpStreamTransport,
|
|
267
|
+
OpStreamUnavailableError,
|
|
268
|
+
opAckSubject,
|
|
269
|
+
opFrameSubject,
|
|
270
|
+
type NatsOpStreamConnection,
|
|
271
|
+
type OpStreamSubscription,
|
|
272
|
+
type OpStreamTransport,
|
|
273
|
+
} from "./selfhosted/op-transport";
|
|
274
|
+
export {
|
|
275
|
+
OP_STREAM_ACK_INTERVAL_MS,
|
|
276
|
+
OP_STREAM_DEFAULT_WINDOW_BYTES,
|
|
277
|
+
OP_STREAM_RECONNECT_HOLD_MS,
|
|
278
|
+
OP_STREAM_SILENCE_TIMEOUT_MS,
|
|
279
|
+
type OpStreamExecOutcome,
|
|
280
|
+
type OpStreamJournal,
|
|
281
|
+
} from "./selfhosted/op-stream";
|
|
282
|
+
// The durable op-id correlation seam (B1): the runtime barrel BINDS it around
|
|
283
|
+
// the SDK shell tool; the sandbox leaf READS it when minting op ids.
|
|
284
|
+
export { nextDurableOpId, runWithToolCallCorrelation, sanitizeOpIdToken } from "./op-correlation";
|
|
214
285
|
export {
|
|
215
286
|
negotiateSelfhostedCapabilities,
|
|
216
287
|
selfhostedLiveness,
|
|
@@ -219,7 +290,11 @@ export {
|
|
|
219
290
|
type SelfhostedLivenessState,
|
|
220
291
|
type SelfhostedEnrollment,
|
|
221
292
|
} from "./selfhosted/capabilities";
|
|
222
|
-
export {
|
|
293
|
+
export {
|
|
294
|
+
MockAgentResponder,
|
|
295
|
+
type MockAgentResponderOptions,
|
|
296
|
+
type MockExecHandler,
|
|
297
|
+
} from "./selfhosted/testing";
|
|
223
298
|
|
|
224
299
|
// The hot-swap routing proxy (M7): ONE stable session-shaped object the SDK binds
|
|
225
300
|
// to, which re-reads the per-session active pointer per op and dispatches to the
|
|
@@ -237,8 +312,11 @@ export {
|
|
|
237
312
|
export {
|
|
238
313
|
makeActiveBackendResolver,
|
|
239
314
|
ActiveBackendUnresolvableError,
|
|
315
|
+
swapTargetEstablishability,
|
|
240
316
|
type ActiveBackendResolverDeps,
|
|
241
317
|
type RoutableSandbox,
|
|
318
|
+
type BackendUnresolvableCode,
|
|
319
|
+
type SwapTargetEstablishability,
|
|
242
320
|
} from "./routing/backend-resolver";
|
|
243
321
|
|
|
244
322
|
/**
|
|
@@ -252,8 +330,15 @@ export {
|
|
|
252
330
|
* ports on demand (modal/runloop/e2b pre-declare; blaxel resolves on demand).
|
|
253
331
|
* Existing modal/docker/local construction is behavior-preserved.
|
|
254
332
|
*/
|
|
255
|
-
export function createSandboxClient(
|
|
256
|
-
|
|
333
|
+
export function createSandboxClient(
|
|
334
|
+
settings: Settings,
|
|
335
|
+
environment = collectSandboxEnvironment(settings),
|
|
336
|
+
): unknown {
|
|
337
|
+
return createSandboxClientForBackend(
|
|
338
|
+
settings.sandboxBackend as SandboxBackend,
|
|
339
|
+
settings,
|
|
340
|
+
environment,
|
|
341
|
+
);
|
|
257
342
|
}
|
|
258
343
|
|
|
259
344
|
/**
|
|
@@ -287,10 +372,10 @@ export function createSandboxClientForBackend(
|
|
|
287
372
|
// on-demand → must pre-declare). blaxel is on-demand → skipped here.
|
|
288
373
|
const desktop = registration.descriptor.capabilities.DesktopStream;
|
|
289
374
|
if (
|
|
290
|
-
desktop.available
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
375
|
+
desktop.available &&
|
|
376
|
+
settings.sandboxDesktopEnabled &&
|
|
377
|
+
!registration.descriptor.portExposure.supportsOnDemandPorts &&
|
|
378
|
+
!exposedPorts.includes(DESKTOP_STREAM_PORT)
|
|
294
379
|
) {
|
|
295
380
|
exposedPorts.push(DESKTOP_STREAM_PORT);
|
|
296
381
|
}
|
|
@@ -299,10 +384,10 @@ export function createSandboxClientForBackend(
|
|
|
299
384
|
// at construction for resolveExposedPort(7681) to succeed later on a fresh box.
|
|
300
385
|
// Same condition as the 6080 merge (a desktop-capable image bakes ttyd too).
|
|
301
386
|
if (
|
|
302
|
-
desktop.available
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
387
|
+
desktop.available &&
|
|
388
|
+
settings.sandboxDesktopEnabled &&
|
|
389
|
+
!registration.descriptor.portExposure.supportsOnDemandPorts &&
|
|
390
|
+
!exposedPorts.includes(TERMINAL_STREAM_PORT)
|
|
306
391
|
) {
|
|
307
392
|
exposedPorts.push(TERMINAL_STREAM_PORT);
|
|
308
393
|
}
|
|
@@ -328,14 +413,48 @@ function withDockerNetwork(client: SandboxClient, network: string | undefined):
|
|
|
328
413
|
};
|
|
329
414
|
return {
|
|
330
415
|
backendId: client.backendId,
|
|
331
|
-
...(client.supportsDefaultOptions !== undefined
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
...(client.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
416
|
+
...(client.supportsDefaultOptions !== undefined
|
|
417
|
+
? { supportsDefaultOptions: client.supportsDefaultOptions }
|
|
418
|
+
: {}),
|
|
419
|
+
...(client.create
|
|
420
|
+
? {
|
|
421
|
+
create: async (...args: any[]) =>
|
|
422
|
+
await wrapSession(await (client.create as any)(...args)),
|
|
423
|
+
}
|
|
424
|
+
: {}),
|
|
425
|
+
...(client.resume
|
|
426
|
+
? {
|
|
427
|
+
resume: async (state: SandboxSessionState) =>
|
|
428
|
+
await wrapSession(await client.resume!(state)),
|
|
429
|
+
}
|
|
430
|
+
: {}),
|
|
431
|
+
...(client.delete
|
|
432
|
+
? { delete: async (state: SandboxSessionState) => await client.delete!(state) }
|
|
433
|
+
: {}),
|
|
434
|
+
...(client.serializeSessionState
|
|
435
|
+
? {
|
|
436
|
+
serializeSessionState: async (state: SandboxSessionState, options) =>
|
|
437
|
+
await client.serializeSessionState!(state, options),
|
|
438
|
+
}
|
|
439
|
+
: {}),
|
|
440
|
+
...(client.canPersistOwnedSessionState
|
|
441
|
+
? {
|
|
442
|
+
canPersistOwnedSessionState: async (state: SandboxSessionState) =>
|
|
443
|
+
await client.canPersistOwnedSessionState!(state),
|
|
444
|
+
}
|
|
445
|
+
: {}),
|
|
446
|
+
...(client.canReusePreservedOwnedSession
|
|
447
|
+
? {
|
|
448
|
+
canReusePreservedOwnedSession: async (state: SandboxSessionState) =>
|
|
449
|
+
await client.canReusePreservedOwnedSession!(state),
|
|
450
|
+
}
|
|
451
|
+
: {}),
|
|
452
|
+
...(client.deserializeSessionState
|
|
453
|
+
? {
|
|
454
|
+
deserializeSessionState: async (state: Record<string, unknown>) =>
|
|
455
|
+
await client.deserializeSessionState!(state),
|
|
456
|
+
}
|
|
457
|
+
: {}),
|
|
339
458
|
};
|
|
340
459
|
}
|
|
341
460
|
|
|
@@ -351,7 +470,9 @@ async function connectDockerNetwork(network: string, containerId: string): Promi
|
|
|
351
470
|
if (stderr.includes("already exists")) {
|
|
352
471
|
return;
|
|
353
472
|
}
|
|
354
|
-
throw new Error(
|
|
473
|
+
throw new Error(
|
|
474
|
+
`Failed to connect Docker sandbox container to network ${network}: ${stderr.trim()}`,
|
|
475
|
+
);
|
|
355
476
|
}
|
|
356
477
|
|
|
357
478
|
/**
|
|
@@ -364,14 +485,15 @@ export function sandboxStateEntryFromRunState(state: unknown): Record<string, un
|
|
|
364
485
|
if (!sandboxState) {
|
|
365
486
|
return null;
|
|
366
487
|
}
|
|
367
|
-
const entry =
|
|
368
|
-
|
|
488
|
+
const entry =
|
|
489
|
+
sandboxState.sessionsByAgent?.[sandboxState.currentAgentKey] ??
|
|
490
|
+
(sandboxState.currentAgentKey && sandboxState.sessionState
|
|
369
491
|
? {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
492
|
+
backendId: sandboxState.backendId,
|
|
493
|
+
currentAgentKey: sandboxState.currentAgentKey,
|
|
494
|
+
currentAgentName: sandboxState.currentAgentName,
|
|
495
|
+
sessionState: sandboxState.sessionState,
|
|
496
|
+
}
|
|
375
497
|
: null);
|
|
376
498
|
if (!entry || !entry.sessionState) {
|
|
377
499
|
return null;
|
|
@@ -384,7 +506,10 @@ export function sandboxStateEntryFromRunState(state: unknown): Record<string, un
|
|
|
384
506
|
* sandbox session state from a stored entry (as produced by
|
|
385
507
|
* sandboxStateEntryFromRunState) instead of from a RunState blob.
|
|
386
508
|
*/
|
|
387
|
-
export async function restoredSandboxSessionStateFromEntry(
|
|
509
|
+
export async function restoredSandboxSessionStateFromEntry(
|
|
510
|
+
entry: Record<string, unknown>,
|
|
511
|
+
client: unknown,
|
|
512
|
+
): Promise<SandboxSessionState | undefined> {
|
|
388
513
|
if (!client || !entry || typeof entry !== "object" || !("sessionState" in entry)) {
|
|
389
514
|
return undefined;
|
|
390
515
|
}
|
|
@@ -409,7 +534,9 @@ export async function restoredSandboxSessionStateFromEntry(entry: Record<string,
|
|
|
409
534
|
* and the SDK's deserializeSessionState must NOT receive it (it is an opaque
|
|
410
535
|
* runtime-level field, not provider state).
|
|
411
536
|
*/
|
|
412
|
-
export function readWorkspaceArchiveFromEnvelopeSessionState(
|
|
537
|
+
export function readWorkspaceArchiveFromEnvelopeSessionState(
|
|
538
|
+
sessionState: unknown,
|
|
539
|
+
): Uint8Array | undefined {
|
|
413
540
|
if (!sessionState || typeof sessionState !== "object") {
|
|
414
541
|
return undefined;
|
|
415
542
|
}
|
|
@@ -424,6 +551,24 @@ export function readWorkspaceArchiveFromEnvelopeSessionState(sessionState: unkno
|
|
|
424
551
|
}
|
|
425
552
|
}
|
|
426
553
|
|
|
554
|
+
function readWorkspaceArchivePairFromEnvelopeSessionState(sessionState: unknown): {
|
|
555
|
+
current?: Uint8Array;
|
|
556
|
+
previous?: Uint8Array;
|
|
557
|
+
} {
|
|
558
|
+
if (!sessionState || typeof sessionState !== "object") {
|
|
559
|
+
return {};
|
|
560
|
+
}
|
|
561
|
+
const state = sessionState as { workspaceArchivePrev?: unknown };
|
|
562
|
+
const current = readWorkspaceArchiveFromEnvelopeSessionState(sessionState);
|
|
563
|
+
const previous = readWorkspaceArchiveFromEnvelopeSessionState({
|
|
564
|
+
workspaceArchive: state.workspaceArchivePrev,
|
|
565
|
+
});
|
|
566
|
+
return {
|
|
567
|
+
...(current ? { current } : {}),
|
|
568
|
+
...(previous ? { previous } : {}),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
427
572
|
// The native snapshot-ref prefixes the @openai/agents-extensions modal client
|
|
428
573
|
// encodes (snapshots.mjs `NATIVE_SNAPSHOT_PREFIXES`). The ref is
|
|
429
574
|
// `<PREFIX>\n{"snapshot_id":"...",...}`. We re-implement the decode here because
|
|
@@ -473,7 +618,10 @@ export function decodeModalSnapshotId(archive: Uint8Array): string | undefined {
|
|
|
473
618
|
* A tar archive (no snapshot id) is a no-op. Returns the deleted snapshot id (or
|
|
474
619
|
* undefined when nothing was deleted) for observability.
|
|
475
620
|
*/
|
|
476
|
-
export async function deletePriorPersistedSnapshot(
|
|
621
|
+
export async function deletePriorPersistedSnapshot(
|
|
622
|
+
session: unknown,
|
|
623
|
+
priorArchiveBase64: string | null | undefined,
|
|
624
|
+
): Promise<string | undefined> {
|
|
477
625
|
if (!priorArchiveBase64) {
|
|
478
626
|
return undefined;
|
|
479
627
|
}
|
|
@@ -487,7 +635,8 @@ export async function deletePriorPersistedSnapshot(session: unknown, priorArchiv
|
|
|
487
635
|
if (!snapshotId) {
|
|
488
636
|
return undefined;
|
|
489
637
|
}
|
|
490
|
-
const modal = (session as { modal?: { images?: { delete?: (id: string) => Promise<unknown> } } })
|
|
638
|
+
const modal = (session as { modal?: { images?: { delete?: (id: string) => Promise<unknown> } } })
|
|
639
|
+
.modal;
|
|
491
640
|
const del = modal?.images?.delete;
|
|
492
641
|
if (typeof del !== "function") {
|
|
493
642
|
return undefined;
|
|
@@ -500,12 +649,17 @@ export async function deletePriorPersistedSnapshot(session: unknown, priorArchiv
|
|
|
500
649
|
}
|
|
501
650
|
}
|
|
502
651
|
|
|
503
|
-
export async function deserializeSandboxSessionStateEnvelope(
|
|
652
|
+
export async function deserializeSandboxSessionStateEnvelope(
|
|
653
|
+
client: SandboxClient,
|
|
654
|
+
envelope: unknown,
|
|
655
|
+
): Promise<SandboxSessionState | undefined> {
|
|
504
656
|
if (!envelope || typeof envelope !== "object") {
|
|
505
657
|
return undefined;
|
|
506
658
|
}
|
|
507
659
|
if (!client.deserializeSessionState) {
|
|
508
|
-
throw new Error(
|
|
660
|
+
throw new Error(
|
|
661
|
+
"Sandbox client must implement deserializeSessionState() to resume RunState sandbox state",
|
|
662
|
+
);
|
|
509
663
|
}
|
|
510
664
|
const state = envelope as {
|
|
511
665
|
providerState?: Record<string, unknown>;
|
|
@@ -520,8 +674,12 @@ export async function deserializeSandboxSessionStateEnvelope(client: SandboxClie
|
|
|
520
674
|
...(state.providerState ?? {}),
|
|
521
675
|
manifest: state.manifest,
|
|
522
676
|
...(state.snapshot !== undefined ? { snapshot: state.snapshot } : {}),
|
|
523
|
-
...(state.snapshotFingerprint !== undefined
|
|
524
|
-
|
|
677
|
+
...(state.snapshotFingerprint !== undefined
|
|
678
|
+
? { snapshotFingerprint: state.snapshotFingerprint }
|
|
679
|
+
: {}),
|
|
680
|
+
...(state.snapshotFingerprintVersion !== undefined
|
|
681
|
+
? { snapshotFingerprintVersion: state.snapshotFingerprintVersion }
|
|
682
|
+
: {}),
|
|
525
683
|
workspaceReady: state.workspaceReady,
|
|
526
684
|
...(state.exposedPorts ? { exposedPorts: structuredClone(state.exposedPorts) } : {}),
|
|
527
685
|
});
|
|
@@ -531,14 +689,18 @@ export async function deserializeSandboxSessionStateEnvelope(client: SandboxClie
|
|
|
531
689
|
// The ONE resume / recovery primitive (P1.2).
|
|
532
690
|
//
|
|
533
691
|
// establishSandboxSessionFromEnvelope is the single re-establish-from-envelope
|
|
534
|
-
// path the stateless model leans on
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
692
|
+
// path the stateless model leans on. Creation authority is explicit:
|
|
693
|
+
//
|
|
694
|
+
// - `create-or-restore` is reserved for the caller that won the durable
|
|
695
|
+
// cold->warming lease transition. It may create a cold box, or replace a
|
|
696
|
+
// resumable box that the provider proves is gone.
|
|
697
|
+
// - `resume-only` is for attached turns and API-direct operations. It may
|
|
698
|
+
// resume the leased box by id, but a provider NotFound propagates to the
|
|
699
|
+
// caller so the lease can be atomically marked cold. It NEVER creates.
|
|
700
|
+
//
|
|
701
|
+
// This ownership boundary is the double-spawn guard. A provider NotFound alone
|
|
702
|
+
// is not creation authority: many attached callers can observe the same dead id
|
|
703
|
+
// concurrently, while only one caller may own cold->warming.
|
|
542
704
|
// ============================================================================
|
|
543
705
|
|
|
544
706
|
/** A live, externally-owned sandbox session re-established from the group lease
|
|
@@ -551,6 +713,16 @@ export type EstablishedSandboxSession = {
|
|
|
551
713
|
sessionState: unknown;
|
|
552
714
|
instanceId: string;
|
|
553
715
|
backendId: string;
|
|
716
|
+
/** How this establish reached a live box: warm reattach by id ("resumed"),
|
|
717
|
+
* fresh create ("created"), or fresh create + archive hydration
|
|
718
|
+
* ("restored"). Callers use it to emit the durable sandbox.box.* lifecycle
|
|
719
|
+
* events — box transitions were previously unrecorded, which made both
|
|
720
|
+
* 2026-07-06 incidents near-unattributable. Optional so external/legacy
|
|
721
|
+
* constructors of this shape stay valid. */
|
|
722
|
+
origin?: "resumed" | "created" | "restored";
|
|
723
|
+
/** Set when a create-authorized reattach found the envelope's box GONE
|
|
724
|
+
* (provider NotFound) and fell through to cold-restore. */
|
|
725
|
+
lostInstanceId?: string;
|
|
554
726
|
};
|
|
555
727
|
|
|
556
728
|
export type SandboxCreatedCallback = (established: EstablishedSandboxSession) => Promise<void>;
|
|
@@ -590,14 +762,22 @@ export function isProviderSandboxNotFoundError(backendId: string, error: unknown
|
|
|
590
762
|
if (!error) {
|
|
591
763
|
return false;
|
|
592
764
|
}
|
|
593
|
-
const status =
|
|
594
|
-
|
|
765
|
+
const status =
|
|
766
|
+
(error as { status?: unknown; statusCode?: unknown }).status ??
|
|
767
|
+
(error as { statusCode?: unknown }).statusCode;
|
|
595
768
|
if (status === 404) {
|
|
596
769
|
return true;
|
|
597
770
|
}
|
|
598
|
-
const name =
|
|
599
|
-
|
|
600
|
-
const
|
|
771
|
+
const name =
|
|
772
|
+
typeof (error as { name?: unknown }).name === "string" ? (error as { name: string }).name : "";
|
|
773
|
+
const code =
|
|
774
|
+
typeof (error as { code?: unknown }).code === "string" ? (error as { code: string }).code : "";
|
|
775
|
+
const message =
|
|
776
|
+
error instanceof Error
|
|
777
|
+
? error.message
|
|
778
|
+
: typeof error === "string"
|
|
779
|
+
? error
|
|
780
|
+
: String((error as { message?: unknown })?.message ?? "");
|
|
601
781
|
const haystack = `${name} ${code} ${message}`.toLowerCase();
|
|
602
782
|
// Provider-agnostic "gone" markers (Modal: "sandbox … not found" / terminated;
|
|
603
783
|
// e2b/daytona/runloop: "not found" / "no longer running" / "terminated" /
|
|
@@ -619,7 +799,11 @@ export function isProviderSandboxNotFoundError(backendId: string, error: unknown
|
|
|
619
799
|
];
|
|
620
800
|
// A "running"/"already exists" resume-conflict is explicitly NOT NotFound — the
|
|
621
801
|
// box is alive; recreating would double-spawn.
|
|
622
|
-
if (
|
|
802
|
+
if (
|
|
803
|
+
haystack.includes("already running") ||
|
|
804
|
+
haystack.includes("still running") ||
|
|
805
|
+
haystack.includes("already exists")
|
|
806
|
+
) {
|
|
623
807
|
return false;
|
|
624
808
|
}
|
|
625
809
|
return goneMarkers.some((marker) => haystack.includes(marker));
|
|
@@ -627,53 +811,82 @@ export function isProviderSandboxNotFoundError(backendId: string, error: unknown
|
|
|
627
811
|
|
|
628
812
|
function readInstanceId(session: unknown): string {
|
|
629
813
|
const state = (session as { state?: Record<string, unknown> }).state ?? {};
|
|
630
|
-
const candidate =
|
|
814
|
+
const candidate =
|
|
815
|
+
state.sandboxId ??
|
|
816
|
+
state.instanceId ??
|
|
817
|
+
state.id ??
|
|
818
|
+
state.hostId ??
|
|
819
|
+
state.containerId ??
|
|
820
|
+
state.workspaceRootPath;
|
|
631
821
|
return typeof candidate === "string" && candidate.length > 0 ? candidate : "";
|
|
632
822
|
}
|
|
633
823
|
|
|
634
|
-
async function terminateCreatedSandbox(
|
|
824
|
+
async function terminateCreatedSandbox(
|
|
825
|
+
client: ResumeCapableClient,
|
|
826
|
+
session: unknown,
|
|
827
|
+
sessionState: unknown,
|
|
828
|
+
): Promise<void> {
|
|
635
829
|
const clientWithDelete = client as { delete?: (state: unknown) => Promise<unknown> };
|
|
636
830
|
if (typeof clientWithDelete.delete === "function" && sessionState !== undefined) {
|
|
637
|
-
try {
|
|
831
|
+
try {
|
|
832
|
+
await clientWithDelete.delete(sessionState);
|
|
833
|
+
} catch {
|
|
834
|
+
/* best-effort */
|
|
835
|
+
}
|
|
638
836
|
return;
|
|
639
837
|
}
|
|
640
|
-
const sess = session as {
|
|
641
|
-
|
|
838
|
+
const sess = session as {
|
|
839
|
+
close?: () => Promise<unknown>;
|
|
840
|
+
terminate?: () => Promise<unknown>;
|
|
841
|
+
kill?: () => Promise<unknown>;
|
|
842
|
+
};
|
|
843
|
+
try {
|
|
844
|
+
await (sess.terminate ?? sess.kill ?? sess.close)?.();
|
|
845
|
+
} catch {
|
|
846
|
+
/* best-effort */
|
|
847
|
+
}
|
|
642
848
|
}
|
|
643
849
|
|
|
644
850
|
/**
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* turn activity). A null envelope means a cold session that was never warmed →
|
|
649
|
-
* create() directly.
|
|
851
|
+
* Establish from one recovery envelope under an explicit creation policy. The
|
|
852
|
+
* envelope is the lease's box-identity descriptor (the same per-turn `_sandbox`
|
|
853
|
+
* envelope upserted by the turn activity).
|
|
650
854
|
*
|
|
651
855
|
* - `opts.backendOverride ?? envelope.backendId ?? settings.sandboxBackend`
|
|
652
856
|
* selects the backend; the client is built for THAT backend (resume-by-id is
|
|
653
857
|
* fenced to the original provider).
|
|
654
858
|
* - warm reattach: deserialize the envelope sessionState → client.resume(state)
|
|
655
|
-
* (no lock; R4-safe).
|
|
656
|
-
* -
|
|
859
|
+
* (no lock; R4-safe). `resume-only` propagates provider NotFound.
|
|
860
|
+
* - `create-or-restore`: the elected owner may replace a missing warm instance,
|
|
861
|
+
* or create directly from a cold/null envelope.
|
|
657
862
|
*/
|
|
658
863
|
export async function establishSandboxSessionFromEnvelope(
|
|
659
864
|
settings: Settings,
|
|
660
865
|
envelope: Record<string, unknown> | null,
|
|
661
866
|
opts: {
|
|
662
867
|
sessionId: string;
|
|
868
|
+
recovery: "create-or-restore" | "resume-only";
|
|
663
869
|
backendOverride?: SandboxBackend;
|
|
664
870
|
environment?: Record<string, string>;
|
|
665
871
|
onSandboxCreated?: SandboxCreatedCallback;
|
|
666
872
|
metrics?: RuntimeMetricsHooks;
|
|
667
873
|
},
|
|
668
874
|
): Promise<EstablishedSandboxSession> {
|
|
669
|
-
const envelopeBackend =
|
|
670
|
-
|
|
875
|
+
const envelopeBackend =
|
|
876
|
+
typeof envelope?.backendId === "string" ? (envelope.backendId as SandboxBackend) : undefined;
|
|
877
|
+
const backend =
|
|
878
|
+
opts.backendOverride ?? envelopeBackend ?? (settings.sandboxBackend as SandboxBackend);
|
|
671
879
|
const environment = opts.environment ?? collectSandboxEnvironment(settings);
|
|
672
|
-
const client = createSandboxClientForBackend(backend, settings, environment) as
|
|
880
|
+
const client = createSandboxClientForBackend(backend, settings, environment) as
|
|
881
|
+
| ResumeCapableClient
|
|
882
|
+
| undefined;
|
|
673
883
|
if (!client) {
|
|
674
|
-
throw new SandboxConfigError(
|
|
884
|
+
throw new SandboxConfigError(
|
|
885
|
+
backend,
|
|
886
|
+
`Cannot establish a sandbox session for backend "${backend}" (no client; sandboxBackend=none?)`,
|
|
887
|
+
);
|
|
675
888
|
}
|
|
676
|
-
if (!client.create) {
|
|
889
|
+
if (opts.recovery === "create-or-restore" && !client.create) {
|
|
677
890
|
throw new SandboxConfigError(backend, `Sandbox backend "${backend}" does not support create()`);
|
|
678
891
|
}
|
|
679
892
|
|
|
@@ -693,7 +906,10 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
693
906
|
// The serialized provider state the box was last persisted as. The envelope
|
|
694
907
|
// shape is the per-turn `_sandbox` entry; its `sessionState` is the provider
|
|
695
908
|
// payload deserializeSandboxSessionStateEnvelope re-hydrates.
|
|
696
|
-
const envelopeSessionState =
|
|
909
|
+
const envelopeSessionState =
|
|
910
|
+
envelope && typeof envelope === "object"
|
|
911
|
+
? (envelope as { sessionState?: unknown }).sessionState
|
|
912
|
+
: undefined;
|
|
697
913
|
|
|
698
914
|
// The persisted /workspace snapshot the reaper folded onto the lease envelope
|
|
699
915
|
// (sandbox-file-persistence). Present on a re-warm whose box was drain-persisted:
|
|
@@ -701,7 +917,8 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
701
917
|
// - COLD lease re-warm (confirmDrainCold preserved a MINIMAL archive-only
|
|
702
918
|
// envelope `{ sessionState: { workspaceArchive } }` — NO sandboxId, so the
|
|
703
919
|
// warm-reattach branch must NOT try resume()-by-id; it cold-creates+hydrates).
|
|
704
|
-
const
|
|
920
|
+
const workspaceArchives = readWorkspaceArchivePairFromEnvelopeSessionState(envelopeSessionState);
|
|
921
|
+
const workspaceArchive = workspaceArchives.current;
|
|
705
922
|
|
|
706
923
|
// create() a FRESH box, THEN replay the persisted /workspace snapshot via
|
|
707
924
|
// session.hydrateWorkspace(archive) when one rode the envelope. hydrateWorkspace
|
|
@@ -709,7 +926,10 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
709
926
|
// image (restoreSnapshotFilesystem); no archive -> a clean empty box. This is the
|
|
710
927
|
// SOLE archive-replay seam, shared by the NotFound warm-reattach path AND the
|
|
711
928
|
// cold-restore branch (b) below.
|
|
712
|
-
const coldRestore = async (
|
|
929
|
+
const coldRestore = async (
|
|
930
|
+
resumeFallbackState?: unknown,
|
|
931
|
+
skipWorkspaceHydrate = false,
|
|
932
|
+
): Promise<EstablishedSandboxSession> => {
|
|
713
933
|
const createStarted = Date.now();
|
|
714
934
|
let restored: Awaited<ReturnType<NonNullable<typeof client.create>>>;
|
|
715
935
|
try {
|
|
@@ -735,25 +955,53 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
735
955
|
throw createCallbackError;
|
|
736
956
|
}
|
|
737
957
|
}
|
|
738
|
-
|
|
739
|
-
|
|
958
|
+
// Whether an archive was ACTUALLY applied to /workspace — drives `origin`
|
|
959
|
+
// (and the worker's sandbox.box.created `hydrated` field). A clean-box
|
|
960
|
+
// fallback, a backend without hydrateWorkspace, or an all-archives-failed
|
|
961
|
+
// path leaves this false so we never report `hydrated: "archive"` for an
|
|
962
|
+
// empty workspace.
|
|
963
|
+
let hydrationApplied = false;
|
|
964
|
+
if (!skipWorkspaceHydrate && (workspaceArchive || workspaceArchives.previous)) {
|
|
965
|
+
const hydrate = (restored as { hydrateWorkspace?: (data: Uint8Array) => Promise<void> })
|
|
966
|
+
.hydrateWorkspace;
|
|
740
967
|
if (typeof hydrate === "function") {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
968
|
+
let hydrated = false;
|
|
969
|
+
for (const candidate of [
|
|
970
|
+
{ archive: workspaceArchive, label: "current" },
|
|
971
|
+
{ archive: workspaceArchives.previous, label: "previous" },
|
|
972
|
+
]) {
|
|
973
|
+
if (!candidate.archive) {
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
try {
|
|
977
|
+
// hydrateWorkspace may internally REPLACE the underlying box
|
|
978
|
+
// (restoreSnapshotFilesystem creates a replacement sandbox and terminates
|
|
979
|
+
// the placeholder), so the instanceId must be re-read AFTER.
|
|
980
|
+
await hydrate.call(restored, candidate.archive);
|
|
981
|
+
console.info(
|
|
982
|
+
`[sandbox] cold-restore hydrated workspace from ${candidate.label} archive`,
|
|
983
|
+
);
|
|
984
|
+
hydrated = true;
|
|
985
|
+
break;
|
|
986
|
+
} catch (hydrateError) {
|
|
987
|
+
console.warn(
|
|
988
|
+
`[sandbox] cold-restore failed to hydrate workspace from ${candidate.label} archive${
|
|
989
|
+
candidate.label === "current" && workspaceArchives.previous
|
|
990
|
+
? "; trying previous archive"
|
|
991
|
+
: ""
|
|
992
|
+
}`,
|
|
993
|
+
hydrateError,
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (!hydrated) {
|
|
998
|
+
// If both retained archives are unusable, drop the placeholder and
|
|
999
|
+
// fall through to the same clean-box behavior as an envelope with no
|
|
1000
|
+
// archive. Restore failure is fail-open; it must not strand a turn.
|
|
754
1001
|
await terminateCreatedSandbox(client, restored, restoredState);
|
|
755
|
-
|
|
1002
|
+
return await coldRestore(resumeFallbackState, true);
|
|
756
1003
|
}
|
|
1004
|
+
hydrationApplied = true;
|
|
757
1005
|
const hydratedState = (restored as { state?: unknown }).state;
|
|
758
1006
|
const hydratedInstanceId = readInstanceId(restored);
|
|
759
1007
|
if (hydratedInstanceId && hydratedInstanceId !== established.instanceId) {
|
|
@@ -782,6 +1030,7 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
782
1030
|
sessionState: restoredState ?? resumeFallbackState,
|
|
783
1031
|
instanceId: readInstanceId(restored),
|
|
784
1032
|
backendId: client.backendId,
|
|
1033
|
+
origin: hydrationApplied ? ("restored" as const) : ("created" as const),
|
|
785
1034
|
};
|
|
786
1035
|
};
|
|
787
1036
|
|
|
@@ -792,44 +1041,78 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
792
1041
|
// NotFound, so it would propagate instead of cold-restoring. Gate the resume
|
|
793
1042
|
// branch on a present sandbox identity so an archive-only envelope falls straight
|
|
794
1043
|
// through to the cold-restore+hydrate path (b).
|
|
795
|
-
const envelopeProviderState =
|
|
796
|
-
|
|
797
|
-
|
|
1044
|
+
const envelopeProviderState =
|
|
1045
|
+
envelopeSessionState && typeof envelopeSessionState === "object"
|
|
1046
|
+
? (envelopeSessionState as { providerState?: Record<string, unknown> }).providerState
|
|
1047
|
+
: undefined;
|
|
798
1048
|
const hasResumableInstance = Boolean(
|
|
799
|
-
envelopeProviderState
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1049
|
+
envelopeProviderState &&
|
|
1050
|
+
typeof envelopeProviderState === "object" &&
|
|
1051
|
+
(envelopeProviderState.sandboxId ||
|
|
1052
|
+
envelopeProviderState.instanceId ||
|
|
1053
|
+
envelopeProviderState.id ||
|
|
1054
|
+
envelopeProviderState.containerId ||
|
|
1055
|
+
envelopeProviderState.workspaceRootPath),
|
|
805
1056
|
);
|
|
806
1057
|
|
|
807
1058
|
// (a) WARM REATTACH BY ID — only when the envelope carries a resumable box id.
|
|
808
|
-
if (
|
|
1059
|
+
if (
|
|
1060
|
+
hasResumableInstance &&
|
|
1061
|
+
envelopeSessionState &&
|
|
1062
|
+
client.resume &&
|
|
1063
|
+
client.deserializeSessionState
|
|
1064
|
+
) {
|
|
809
1065
|
let resumedState: unknown;
|
|
810
1066
|
try {
|
|
811
|
-
resumedState = await deserializeSandboxSessionStateEnvelope(
|
|
1067
|
+
resumedState = await deserializeSandboxSessionStateEnvelope(
|
|
1068
|
+
client as unknown as SandboxClient,
|
|
1069
|
+
envelopeSessionState,
|
|
1070
|
+
);
|
|
812
1071
|
} catch (error) {
|
|
813
|
-
throw new SandboxConfigError(
|
|
1072
|
+
throw new SandboxConfigError(
|
|
1073
|
+
backend,
|
|
1074
|
+
`Failed to deserialize sandbox resume envelope for backend "${backend}": ${error instanceof Error ? error.message : String(error)}`,
|
|
1075
|
+
);
|
|
814
1076
|
}
|
|
815
1077
|
if (resumedState !== undefined) {
|
|
816
1078
|
try {
|
|
817
1079
|
const session = await client.resume(resumedState);
|
|
818
|
-
return {
|
|
1080
|
+
return {
|
|
1081
|
+
client,
|
|
1082
|
+
session,
|
|
1083
|
+
sessionState: resumedState,
|
|
1084
|
+
instanceId: readInstanceId(session),
|
|
1085
|
+
backendId: client.backendId,
|
|
1086
|
+
origin: "resumed",
|
|
1087
|
+
};
|
|
819
1088
|
} catch (error) {
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
|
|
1089
|
+
// Attached callers never own replacement. Propagate the provider
|
|
1090
|
+
// NotFound so their lease-aware caller can atomically mark this exact
|
|
1091
|
+
// warm epoch/instance cold and re-enter normal admission. Only the
|
|
1092
|
+
// cold->warming winner may replace the missing box.
|
|
1093
|
+
if (
|
|
1094
|
+
!isProviderSandboxNotFoundError(client.backendId, error) ||
|
|
1095
|
+
opts.recovery === "resume-only"
|
|
1096
|
+
) {
|
|
824
1097
|
throw error;
|
|
825
1098
|
}
|
|
826
1099
|
// COLD-RESTORE: the box is genuinely gone. Modal does NOT restore via
|
|
827
1100
|
// create({ snapshot }) — passing `snapshot` to ModalSandboxClient.create()
|
|
828
1101
|
// THROWS (assertCoreSnapshotUnsupported). Modal's real persistence is an
|
|
829
|
-
// OPAQUE ARCHIVE captured by session.persistWorkspace() at
|
|
830
|
-
// time and folded onto the lease envelope
|
|
831
|
-
// shared coldRestore() seam creates a
|
|
832
|
-
|
|
1102
|
+
// OPAQUE ARCHIVE captured by session.persistWorkspace() at drain/turn-
|
|
1103
|
+
// snapshot time and folded onto the lease envelope
|
|
1104
|
+
// (sandbox-file-persistence). The shared coldRestore() seam creates a
|
|
1105
|
+
// fresh box and replays that archive. `lostInstanceId` carries the gone
|
|
1106
|
+
// box's id so the caller can emit the durable sandbox.box.lost event.
|
|
1107
|
+
const lostInstanceId = [
|
|
1108
|
+
envelopeProviderState?.sandboxId,
|
|
1109
|
+
envelopeProviderState?.instanceId,
|
|
1110
|
+
envelopeProviderState?.id,
|
|
1111
|
+
envelopeProviderState?.containerId,
|
|
1112
|
+
envelopeProviderState?.workspaceRootPath,
|
|
1113
|
+
].find((value): value is string => typeof value === "string" && value.length > 0);
|
|
1114
|
+
const restoredSession = await coldRestore(resumedState);
|
|
1115
|
+
return lostInstanceId ? { ...restoredSession, lostInstanceId } : restoredSession;
|
|
833
1116
|
}
|
|
834
1117
|
}
|
|
835
1118
|
}
|
|
@@ -839,6 +1122,9 @@ export async function establishSandboxSessionFromEnvelope(
|
|
|
839
1122
|
// envelope confirmDrainCold preserves across draining->cold), replay it so
|
|
840
1123
|
// /workspace survives the box churn (sandbox-file-persistence). No archive -> a
|
|
841
1124
|
// clean empty box (a never-warmed session).
|
|
1125
|
+
if (opts.recovery === "resume-only") {
|
|
1126
|
+
throw new SandboxResumeStateUnavailableError(backend);
|
|
1127
|
+
}
|
|
842
1128
|
return await coldRestore();
|
|
843
1129
|
}
|
|
844
1130
|
|