@opengeni/worker-bundle 1.0.1 → 2.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.
Files changed (122) hide show
  1. package/dist/activities/agent-turn/codex.d.ts +1 -1
  2. package/dist/activities/agent-turn/compaction-prep.d.ts +1 -5
  3. package/dist/activities/agent-turn/errors.d.ts +31 -5
  4. package/dist/activities/agent-turn/finalization-monitor.d.ts +17 -0
  5. package/dist/activities/agent-turn/finalization.d.ts +1 -1
  6. package/dist/activities/agent-turn/governance-model.d.ts +0 -2
  7. package/dist/activities/agent-turn/quiescence.d.ts +1 -0
  8. package/dist/activities/agent-turn/recovery-warning.d.ts +8 -0
  9. package/dist/activities/agent-turn/run-credentials.d.ts +0 -1
  10. package/dist/activities/agent-turn/sandbox-establish.d.ts +1 -1
  11. package/dist/activities/agent-turn/sandbox-route.d.ts +0 -20
  12. package/dist/activities/agent-turn/sandbox-runtime.d.ts +1 -1
  13. package/dist/activities/agent-turn/skill-remove.d.ts +14 -0
  14. package/dist/activities/agent-turn/skill-search.d.ts +1 -1
  15. package/dist/activities/agent-turn/stream-attempt.d.ts +1 -0
  16. package/dist/activities/agent-turn/tool-environment.d.ts +0 -4
  17. package/dist/activities/agent-turn/turn-context.d.ts +2 -0
  18. package/dist/activities/agent-turn/turn-sandbox-access.d.ts +3 -0
  19. package/dist/activities/codex-capacity.d.ts +6 -1
  20. package/dist/activities/codex-rotation.d.ts +1 -1
  21. package/dist/activities/context-compaction.d.ts +7 -1
  22. package/dist/activities/mcp-account-routes.d.ts +22 -0
  23. package/dist/activities/mcp-credentials.d.ts +12 -13
  24. package/dist/activities/mcp-operation-authority.d.ts +0 -5
  25. package/dist/activities/rig-verification.d.ts +2 -3
  26. package/dist/activities/sandbox-images.d.ts +1 -1
  27. package/dist/activities/sandbox-lease.d.ts +4 -3
  28. package/dist/activities/session-state.d.ts +57 -2
  29. package/dist/activities/types.d.ts +12 -3
  30. package/dist/{activities-control-HMFGSKC5.js → activities-control-BOLIY3H4.js} +366 -187
  31. package/dist/activities-control-BOLIY3H4.js.map +1 -0
  32. package/dist/activities-control.d.ts +110 -2
  33. package/dist/{activities-turn-AP67JADS.js → activities-turn-2B3WMHBB.js} +767 -471
  34. package/dist/activities-turn-2B3WMHBB.js.map +1 -0
  35. package/dist/activities.d.ts +110 -2
  36. package/dist/artifact-materializer-entry.js +5 -2
  37. package/dist/artifact-materializer-entry.js.map +1 -1
  38. package/dist/artifact-outbox-entry.js +2 -2
  39. package/dist/artifact-outbox-entry.js.map +1 -1
  40. package/dist/{chunk-A4XQWVZK.js → chunk-I7HKKMUJ.js} +77 -1
  41. package/dist/chunk-I7HKKMUJ.js.map +1 -0
  42. package/dist/{chunk-CYITU5UH.js → chunk-TVEE2UUC.js} +275 -146
  43. package/dist/chunk-TVEE2UUC.js.map +1 -0
  44. package/dist/editable-artifact-materializer-service.d.ts +2 -2
  45. package/dist/editable-artifact-outbox-service.d.ts +2 -2
  46. package/dist/index.d.ts +55 -1
  47. package/dist/index.js +4 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/observability-metrics.d.ts +11 -3
  50. package/dist/retained-process-diagnostics.d.ts +4 -0
  51. package/dist/sandbox-resume.d.ts +2 -12
  52. package/dist/sandbox-routing.d.ts +5 -3
  53. package/dist/sandbox-snapshot-diagnostics.d.ts +20 -0
  54. package/dist/warm-capture-holder.d.ts +3 -0
  55. package/dist/workflow-bundle.js +51 -11
  56. package/package.json +15 -15
  57. package/src/activities/agent-turn/agent-build.ts +42 -5
  58. package/src/activities/agent-turn/claim.ts +12 -0
  59. package/src/activities/agent-turn/codex-capacity.ts +26 -4
  60. package/src/activities/agent-turn/codex.ts +6 -2
  61. package/src/activities/agent-turn/compaction-prep.ts +53 -72
  62. package/src/activities/agent-turn/errors.ts +98 -10
  63. package/src/activities/agent-turn/failure-settlement.ts +49 -28
  64. package/src/activities/agent-turn/finalization-monitor.ts +121 -0
  65. package/src/activities/agent-turn/finalization.ts +79 -21
  66. package/src/activities/agent-turn/governance-model.ts +15 -19
  67. package/src/activities/agent-turn/history.ts +24 -10
  68. package/src/activities/agent-turn/quiescence.ts +3 -0
  69. package/src/activities/agent-turn/recovery-warning.ts +18 -0
  70. package/src/activities/agent-turn/run-credentials.ts +3 -7
  71. package/src/activities/agent-turn/run.ts +68 -52
  72. package/src/activities/agent-turn/sandbox-establish.ts +10 -2
  73. package/src/activities/agent-turn/sandbox-route.ts +0 -20
  74. package/src/activities/agent-turn/sandbox-runtime.ts +40 -7
  75. package/src/activities/agent-turn/skill-remove.ts +46 -0
  76. package/src/activities/agent-turn/skill-save.ts +1 -1
  77. package/src/activities/agent-turn/skill-search.ts +1 -1
  78. package/src/activities/agent-turn/skill-selection.ts +2 -0
  79. package/src/activities/agent-turn/skill-tools.ts +10 -5
  80. package/src/activities/agent-turn/stream-attempt.ts +38 -15
  81. package/src/activities/agent-turn/tool-environment.ts +44 -29
  82. package/src/activities/agent-turn/turn-context.ts +3 -0
  83. package/src/activities/agent-turn/turn-sandbox-access.ts +7 -1
  84. package/src/activities/capabilities.ts +17 -2
  85. package/src/activities/codex-capacity.ts +38 -9
  86. package/src/activities/codex-rotation.ts +18 -5
  87. package/src/activities/context-compaction.ts +26 -4
  88. package/src/activities/goals.ts +1 -0
  89. package/src/activities/google-drive-publication.ts +1 -3
  90. package/src/activities/mcp-account-routes.ts +139 -0
  91. package/src/activities/mcp-credentials.ts +83 -194
  92. package/src/activities/mcp-operation-authority.ts +16 -41
  93. package/src/activities/parent-wake.ts +8 -0
  94. package/src/activities/rig-verification.ts +38 -37
  95. package/src/activities/run-input.ts +25 -16
  96. package/src/activities/sandbox-images.ts +0 -1
  97. package/src/activities/sandbox-lease.ts +177 -44
  98. package/src/activities/scheduled-tasks.ts +118 -93
  99. package/src/activities/session-state.ts +83 -3
  100. package/src/activities/types.ts +18 -2
  101. package/src/activities/workflow-wake.ts +2 -17
  102. package/src/editable-artifact-materializer-service.ts +7 -3
  103. package/src/editable-artifact-outbox-service.ts +4 -4
  104. package/src/github-rest-mcp.ts +0 -1
  105. package/src/index.ts +2 -1
  106. package/src/observability-metrics.ts +105 -2
  107. package/src/personal-github-git-credentials.ts +0 -1
  108. package/src/pr-review-credentials.ts +0 -2
  109. package/src/retained-process-diagnostics.ts +50 -0
  110. package/src/sandbox-resume.ts +58 -73
  111. package/src/sandbox-routing.ts +44 -19
  112. package/src/sandbox-snapshot-diagnostics.ts +112 -0
  113. package/src/warm-capture-holder.ts +15 -0
  114. package/src/workflows/session.ts +64 -2
  115. package/dist/activities/agent-turn/knowledge-sources.d.ts +0 -20
  116. package/dist/activities/packs.d.ts +0 -55
  117. package/dist/activities-control-HMFGSKC5.js.map +0 -1
  118. package/dist/activities-turn-AP67JADS.js.map +0 -1
  119. package/dist/chunk-A4XQWVZK.js.map +0 -1
  120. package/dist/chunk-CYITU5UH.js.map +0 -1
  121. package/src/activities/agent-turn/knowledge-sources.ts +0 -86
  122. package/src/activities/packs.ts +0 -166
@@ -13,14 +13,17 @@
13
13
  // harness and executes admitted drains concurrently. There is no per-session
14
14
  // timer, viewer activity, owner task queue, or provider-specific lifecycle path
15
15
  // in the normal drain state machine.
16
+ import { warnDrainSnapshotFailure } from "../sandbox-snapshot-diagnostics";
17
+ import { warnRetainedProcessProofFailure } from "../retained-process-diagnostics";
16
18
 
17
19
  import { createHash, randomUUID } from "node:crypto";
18
- import { retainedProviderCommandPersistence } from "@opengeni/db/retained-provider-commands";
20
+ import { requestRetainedProcessDeadlineCancellation } from "@opengeni/db/retained-provider-commands";
19
21
  import type { ProviderCommandPersistence, ProviderCommandSession } from "@opengeni/runtime";
20
22
  import { Context } from "@temporalio/activity";
21
23
  import { OpLostReason, OpState, type OpStatus } from "@opengeni/agent-proto";
22
24
  import {
23
25
  accrueWarmSeconds,
26
+ retainedProviderCommandPersistence,
24
27
  adoptLegacyModalCheckpointArtifact,
25
28
  confirmDrainCold,
26
29
  appendSessionEventToSandboxGroup,
@@ -49,6 +52,7 @@ import {
49
52
  recordRetainedProcessReconciliationProof,
50
53
  replaceWorkspaceArchiveCaptureAfterProof,
51
54
  readLease,
55
+ readWorkspaceArchiveCapturePreflight,
52
56
  reapExpiredSessionListSnapshots,
53
57
  reapStaleLeaseHoldersGlobal,
54
58
  requestDueSandboxRotationsGlobal,
@@ -251,6 +255,7 @@ export type TerminateBoxFn = (
251
255
  capturePolicy?: ProviderWorkspaceCapturePolicy | null,
252
256
  diskBackedArchives?: boolean,
253
257
  releaseFailedCapture?: () => Promise<void>,
258
+ workspaceId?: string,
254
259
  ) => Promise<boolean | ProviderTerminationOutcome>;
255
260
 
256
261
  export type SweepModalOrphansFn = (
@@ -488,6 +493,7 @@ export function createSandboxLeaseActivities(
488
493
  capturePolicy,
489
494
  diskBackedArchives,
490
495
  releaseFailedCapture,
496
+ workspaceId,
491
497
  ) =>
492
498
  await terminateProviderBox(
493
499
  settings,
@@ -501,6 +507,7 @@ export function createSandboxLeaseActivities(
501
507
  capturePolicy,
502
508
  diskBackedArchives,
503
509
  releaseFailedCapture,
510
+ workspaceId,
504
511
  ));
505
512
  const sweepModalOrphans: SweepModalOrphansFn =
506
513
  options.sweepModalOrphans ?? sweepModalOrphansForConfiguredBackend;
@@ -1160,18 +1167,14 @@ export async function reconcileConnectedMachineBackgroundCommands(
1160
1167
  retryClock: defaultSelfhostedRetryClock,
1161
1168
  journal: { attachGeneration: () => String(Date.now()), persistSettled: () => {} },
1162
1169
  });
1163
- const replay = await client.readExisting(
1170
+ await replayConnectedCommandOutput(
1171
+ client,
1164
1172
  claim.opId,
1165
- proof ? 5_000 : 250,
1173
+ Boolean(proof),
1166
1174
  async (frames) => {
1167
1175
  await captureConnectedCommandOutput(db, claim, bus)(claim.commandId, frames);
1168
1176
  },
1169
1177
  );
1170
- if (proof && replay.status !== "completed" && !replay.terminal) {
1171
- throw new Error(
1172
- "Connected command terminal output replay has not reached its exit frontier",
1173
- );
1174
- }
1175
1178
  }
1176
1179
  if (!proof) {
1177
1180
  await deferConnectedCommandClaim(
@@ -1250,6 +1253,35 @@ export async function reconcileConnectedMachineBackgroundCommands(
1250
1253
  } while (Date.now() < deadline);
1251
1254
  }
1252
1255
 
1256
+ export async function replayConnectedCommandOutput(
1257
+ client: Pick<OpStreamExecClient, "readExisting">,
1258
+ opId: string,
1259
+ terminalKnown: boolean,
1260
+ capture: Parameters<OpStreamExecClient["readExisting"]>[2],
1261
+ ): Promise<void> {
1262
+ // A completed operation can need several bounded reads to replay its retained
1263
+ // frames. Keep the reader's integrity checkpoint until the exit is verified;
1264
+ // replacing it after each partial read would restart forever at frame one.
1265
+ let capturedThrough = 0n;
1266
+ while (true) {
1267
+ const before = capturedThrough;
1268
+ const replay = await client.readExisting(opId, terminalKnown ? 5_000 : 250, async (frames) => {
1269
+ await capture(frames);
1270
+ for (const frame of frames) {
1271
+ const sequence = BigInt(frame.sequence);
1272
+ if (sequence > capturedThrough) capturedThrough = sequence;
1273
+ }
1274
+ });
1275
+ if (!terminalKnown || replay.status === "completed" || replay.terminal) return;
1276
+ // This is a finite terminal drain, not a poller for a running command.
1277
+ // Failed persistence, transport/integrity errors, and no-progress reads
1278
+ // return control to normal reconciliation without licensing settlement.
1279
+ if (capturedThrough === before) {
1280
+ throw new Error("Connected command terminal output replay has not reached its exit frontier");
1281
+ }
1282
+ }
1283
+ }
1284
+
1253
1285
  export async function probeConnectedMachineBackgroundCommand(
1254
1286
  claim: ConnectedMachineBackgroundCommandClaim,
1255
1287
  controlRpc: ControlRpc,
@@ -1381,6 +1413,47 @@ async function reconcileTerminalRetainedProcesses(
1381
1413
  let process = claim.process;
1382
1414
  const expected = retainedProcessSettlementIdentity(process);
1383
1415
  let proof = retainedProcessReconciliationProof(process);
1416
+ const processScope = {
1417
+ accountId: process.accountId,
1418
+ workspaceId: process.workspaceId,
1419
+ sessionId: process.sessionId,
1420
+ processId: process.id,
1421
+ };
1422
+ const storedCommandPersistence = retainedProviderCommandPersistence(
1423
+ db,
1424
+ processScope,
1425
+ bus ? (events) => bus.publish(process.workspaceId, process.sessionId, events) : undefined,
1426
+ );
1427
+ const commandPersistence = {
1428
+ ...storedCommandPersistence,
1429
+ requestCancellation: async (reason: "provider_deadline" | "explicit_stop") => {
1430
+ if (reason === "provider_deadline") {
1431
+ await requestRetainedProcessDeadlineCancellation(db, processScope);
1432
+ if (!(await storedCommandPersistence.cancellationRequested()))
1433
+ throw new Error(
1434
+ "Supervised deadline cancellation no longer owns its original rotating lease",
1435
+ );
1436
+ } else await storedCommandPersistence.requestCancellation(reason);
1437
+ },
1438
+ };
1439
+ if (proof && process.providerBackend === "modal") {
1440
+ try {
1441
+ const command = await commandPersistence.load();
1442
+ if (
1443
+ command?.kind === "modal-router-v1" &&
1444
+ command.supervision &&
1445
+ (!(await commandPersistence.loadSupervisionReceipt()) ||
1446
+ ![command.streams.stdout, command.streams.stderr].every(
1447
+ (stream) => stream.eof && stream.exitCode === 0,
1448
+ ))
1449
+ )
1450
+ proof = null;
1451
+ } catch {
1452
+ // An earlier terminal observation cannot make missing control/output
1453
+ // proof unreachable. Reprobe the original binding; DB remains the gate.
1454
+ proof = null;
1455
+ }
1456
+ }
1384
1457
  if (!proof) {
1385
1458
  let observation: RetainedProcessProbeResult | null = null;
1386
1459
 
@@ -1490,6 +1563,26 @@ async function reconcileTerminalRetainedProcesses(
1490
1563
  }
1491
1564
  } else {
1492
1565
  try {
1566
+ const command = await commandPersistence.load();
1567
+ const supervised = command?.kind === "modal-router-v1" && Boolean(command.supervision);
1568
+ const supervisionMetric = (outcome: string) =>
1569
+ observability.incrementCounter({
1570
+ name: "opengeni_command_supervision_total",
1571
+ help: "Native retained command supervision reconciliation outcomes.",
1572
+ labels: { outcome },
1573
+ });
1574
+ if (
1575
+ supervised &&
1576
+ (lease!.rotationReason === "provider_deadline" ||
1577
+ claim.ownerState === "background_stopping")
1578
+ ) {
1579
+ await commandPersistence.requestCancellation(
1580
+ lease!.rotationReason === "provider_deadline"
1581
+ ? "provider_deadline"
1582
+ : "explicit_stop",
1583
+ );
1584
+ supervisionMetric("cancellation_intent");
1585
+ }
1493
1586
  observation = await probe(
1494
1587
  settings,
1495
1588
  lease!,
@@ -1516,13 +1609,21 @@ async function reconcileTerminalRetainedProcesses(
1516
1609
  .publish(process.workspaceId, process.sessionId, events)
1517
1610
  .catch(() => undefined);
1518
1611
  },
1519
- retainedProviderCommandPersistence(db, {
1520
- accountId: process.accountId,
1521
- workspaceId: process.workspaceId,
1522
- sessionId: process.sessionId,
1523
- processId: process.id,
1524
- }),
1612
+ commandPersistence,
1525
1613
  );
1614
+ if (supervised) {
1615
+ supervisionMetric(
1616
+ (await commandPersistence.loadSupervisionReceipt())
1617
+ ? "proof_retained"
1618
+ : "proof_missing",
1619
+ );
1620
+ if (observation.status === "deferred")
1621
+ supervisionMetric(
1622
+ observation.reason === "provider_running"
1623
+ ? "checkpoint_blocked"
1624
+ : "provider_failure",
1625
+ );
1626
+ }
1526
1627
  } catch (error) {
1527
1628
  observability.warn("sandbox reaper: retained-process provider probe failed", {
1528
1629
  processId: process.id,
@@ -1581,10 +1682,7 @@ async function reconcileTerminalRetainedProcesses(
1581
1682
  recordRetainedProcessReconciliation(observability, `proof_${proof.outcome}`);
1582
1683
  } catch (error) {
1583
1684
  recordRetainedProcessReconciliation(observability, "proof_checkpoint_failed");
1584
- observability.warn("sandbox reaper: retained-process proof checkpoint failed", {
1585
- processId: process.id,
1586
- error: error instanceof Error ? error.message : String(error),
1587
- });
1685
+ warnRetainedProcessProofFailure(observability, error, process);
1588
1686
  continue;
1589
1687
  }
1590
1688
  }
@@ -1788,7 +1886,8 @@ export async function probeRetainedProcessAtProvider(
1788
1886
  lease.leaseEpoch !== process.leaseEpoch ||
1789
1887
  lease.backend !== process.providerBackend ||
1790
1888
  lease.instanceId !== process.providerInstanceId ||
1791
- lease.resumeBackendId !== process.providerBackend ||
1889
+ !lease.resumeBackendId ||
1890
+ sandboxBackendForSdkBackendId(lease.resumeBackendId) !== process.providerBackend ||
1792
1891
  process.routeTargetId !== null
1793
1892
  ) {
1794
1893
  return { status: "deferred", reason: "identity_mismatch" };
@@ -1803,7 +1902,11 @@ export async function probeRetainedProcessAtProvider(
1803
1902
  return classifyRetainedProcessPollResult(pending.result, process.providerSessionId);
1804
1903
  }
1805
1904
  const envelopeBackend = (lease.resumeState as { backendId?: unknown }).backendId;
1806
- if (envelopeBackend !== undefined && envelopeBackend !== process.providerBackend) {
1905
+ if (
1906
+ envelopeBackend !== undefined &&
1907
+ (typeof envelopeBackend !== "string" ||
1908
+ sandboxBackendForSdkBackendId(envelopeBackend) !== process.providerBackend)
1909
+ ) {
1807
1910
  return { status: "deferred", reason: "identity_mismatch" };
1808
1911
  }
1809
1912
  if (sandboxProviderInstanceIdFromEnvelope(lease.resumeState) !== process.providerInstanceId) {
@@ -1837,7 +1940,11 @@ export async function probeRetainedProcessAtProvider(
1837
1940
  } catch {
1838
1941
  return { status: "deferred", reason: "provider_error" };
1839
1942
  }
1840
- if (!client || client.backendId !== process.providerBackend || !client.resume) {
1943
+ if (
1944
+ !client ||
1945
+ sandboxBackendForSdkBackendId(client.backendId) !== process.providerBackend ||
1946
+ !client.resume
1947
+ ) {
1841
1948
  return { status: "deferred", reason: "backend_unsupported" };
1842
1949
  }
1843
1950
  const envelopeSessionState =
@@ -1928,6 +2035,11 @@ export async function probeRetainedProcessAtProvider(
1928
2035
  return;
1929
2036
  }
1930
2037
  const page = session.getProviderCommandOutput?.(value);
2038
+ if (page?.command.kind === "modal-router-v1" && typeof value === "string") {
2039
+ if (!session.captureCommandOutput || !(await session.captureCommandOutput(value)))
2040
+ throw new Error("Byte-offset output requires atomic capture before settlement");
2041
+ return;
2042
+ }
1931
2043
  if (page) {
1932
2044
  for (const chunk of page.chunks)
1933
2045
  if (chunk.text)
@@ -1940,10 +2052,17 @@ export async function probeRetainedProcessAtProvider(
1940
2052
 
1941
2053
  let result: unknown;
1942
2054
  try {
2055
+ const supervisedCancelled =
2056
+ mode === "cancel" || lease.rotationReason === "provider_deadline"
2057
+ ? ((await session.cancelSupervisedCommand?.(
2058
+ process.providerSessionId,
2059
+ lease.rotationReason === "provider_deadline" ? "provider_deadline" : "explicit_stop",
2060
+ )) ?? false)
2061
+ : false;
1943
2062
  result = await withRetainedProcessProbeTimeout(
1944
2063
  session.writeStdin({
1945
2064
  sessionId: process.providerSessionId,
1946
- chars: mode === "cancel" ? "\u0003" : "",
2065
+ chars: mode === "cancel" && !supervisedCancelled ? "\u0003" : "",
1947
2066
  yieldTimeMs: 1_000,
1948
2067
  maxOutputTokens: 2_000,
1949
2068
  }),
@@ -1972,7 +2091,10 @@ export async function probeRetainedProcessAtProvider(
1972
2091
  if (
1973
2092
  observation.status === "deferred" &&
1974
2093
  observation.reason === "provider_running" &&
1975
- lease.rotationRequestedAt !== null
2094
+ lease.rotationRequestedAt !== null &&
2095
+ !(await providerPersistence
2096
+ ?.load()
2097
+ .then((command) => command?.kind === "modal-router-v1" && command.supervision))
1976
2098
  ) {
1977
2099
  // A background PTY cannot outlive the finite provider box. Interrupt only
1978
2100
  // this exact durable provider session after rotation admission is fenced;
@@ -2799,6 +2921,24 @@ async function terminateDrainableBox(
2799
2921
  // lease draining for a later sweep (NEVER terminate a box whose files we
2800
2922
  // could not capture). A persist CAS miss means the box was re-armed and left
2801
2923
  // running, so the cold commit is skipped.
2924
+ // A published capture retry skips persistence. Revalidate legacy containment
2925
+ // even on that path: stale enrollment is not native-supervisor exit proof.
2926
+ // Typed provider absence is independently fenced by the canonical loss commit.
2927
+ if (
2928
+ !providerMissingBeforeCapture &&
2929
+ backend === "modal" &&
2930
+ lease.instanceId &&
2931
+ !(await readWorkspaceArchiveCapturePreflight(db, {
2932
+ accountId,
2933
+ workspaceId: row.workspaceId,
2934
+ sandboxGroupId: row.sandboxGroupId,
2935
+ expectedEpoch: row.leaseEpoch,
2936
+ expectedInstanceId: lease.instanceId,
2937
+ liveness: "draining",
2938
+ }))
2939
+ ) {
2940
+ return false;
2941
+ }
2802
2942
  const termination: ProviderTerminationOutcome | boolean = providerMissingBeforeCapture
2803
2943
  ? { terminated: true, providerMissingBeforeCapture: true }
2804
2944
  : await terminateBox(
@@ -2821,6 +2961,7 @@ async function terminateDrainableBox(
2821
2961
  expectedInstanceId: lease.instanceId,
2822
2962
  });
2823
2963
  },
2964
+ row.workspaceId,
2824
2965
  );
2825
2966
  const terminated = typeof termination === "boolean" ? termination : termination.terminated;
2826
2967
  if (!terminated) {
@@ -2955,18 +3096,22 @@ export async function terminateProviderBox(
2955
3096
  claimedCapturePolicy?: ProviderWorkspaceCapturePolicy | null,
2956
3097
  diskBackedArchives = false,
2957
3098
  releaseFailedCapture?: () => Promise<void>,
3099
+ workspaceId?: string,
2958
3100
  ): Promise<ProviderTerminationOutcome> {
2959
3101
  const durableBackendId = (lease.resumeBackendId ?? lease.backend) as string;
2960
3102
  const backend = sandboxBackendForSdkBackendId(durableBackendId) ?? durableBackendId;
2961
3103
  const logIdentity = providerDrainLogIdentity(lease, backend);
3104
+ const warnCaptureFailure = (message: string, error: unknown): void =>
3105
+ warnDrainSnapshotFailure(observability, message, error, {
3106
+ sandboxGroupId: lease.sandboxGroupId,
3107
+ leaseEpoch: lease.leaseEpoch,
3108
+ ...(workspaceId ? { workspaceId } : {}),
3109
+ });
2962
3110
  const releaseAfterCaptureFailure = async (): Promise<void> => {
2963
3111
  try {
2964
3112
  await releaseFailedCapture?.();
2965
3113
  } catch (error) {
2966
- observability.warn("sandbox reaper: failed capture claim release failed", {
2967
- ...logIdentity,
2968
- error: error instanceof Error ? error.message : String(error),
2969
- });
3114
+ warnCaptureFailure("sandbox reaper: failed capture claim release failed", error);
2970
3115
  }
2971
3116
  };
2972
3117
  // 'none' / no backend -> nothing to terminate.
@@ -3208,29 +3353,20 @@ export async function terminateProviderBox(
3208
3353
  },
3209
3354
  );
3210
3355
  } catch (error) {
3211
- observability.warn("sandbox reaper: late workspace capture publication failed", {
3212
- ...logIdentity,
3213
- error: error instanceof Error ? error.message : String(error),
3214
- });
3356
+ warnCaptureFailure("sandbox reaper: late workspace capture publication failed", error);
3215
3357
  } finally {
3216
3358
  await disposeWorkspaceArchive(archive);
3217
3359
  }
3218
3360
  },
3219
3361
  observeLatePublicationFailure: (error) => {
3220
- observability.warn("sandbox reaper: late capture publication cleanup failed", {
3221
- ...logIdentity,
3222
- error: error instanceof Error ? error.message : String(error),
3223
- });
3362
+ warnCaptureFailure("sandbox reaper: late capture publication cleanup failed", error);
3224
3363
  },
3225
3364
  observeLateFailure: (error) => {
3226
3365
  // Capture has now rejected and this timed-out path can never reach
3227
3366
  // provider teardown. Release only our unpublished claim so a waiting
3228
3367
  // turn can re-arm the intact live box; stale callbacks are DB-fenced.
3229
3368
  void releaseAfterCaptureFailure();
3230
- observability.warn("sandbox reaper: timed-out provider capture later failed", {
3231
- ...logIdentity,
3232
- error: error instanceof Error ? error.message : String(error),
3233
- });
3369
+ warnCaptureFailure("sandbox reaper: timed-out provider capture later failed", error);
3234
3370
  },
3235
3371
  });
3236
3372
  }
@@ -3241,12 +3377,9 @@ export async function terminateProviderBox(
3241
3377
  });
3242
3378
  return { terminated: true, providerMissingBeforeCapture: true };
3243
3379
  }
3244
- observability.warn(
3380
+ warnCaptureFailure(
3245
3381
  "sandbox reaper: persistWorkspace failed — leaving box draining (files NOT lost)",
3246
- {
3247
- ...logIdentity,
3248
- error: error instanceof Error ? error.message : String(error),
3249
- },
3382
+ error,
3250
3383
  );
3251
3384
  // An unresolved timeout is not proof of capture failure: retain the fence
3252
3385
  // until its provider promise settles. A settled failure cannot enter the