@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
@@ -16,6 +16,7 @@ import {
16
16
  type RuntimeMetricsHooks,
17
17
  type SelfhostedOpObservation,
18
18
  type SelfhostedOpObserver,
19
+ type ToolPreparationPhaseMeasurement,
19
20
  } from "@opengeni/runtime";
20
21
 
21
22
  export type TurnOutcome = "completed" | "failed" | "cancelled" | "recovering";
@@ -119,6 +120,10 @@ export function runtimeMetricsHooksForObservability(
119
120
  ): RuntimeMetricsHooks {
120
121
  return {
121
122
  onModelCall: ({ provider, outcome, durationSeconds }) => {
123
+ completedOperationSpan(observability, "worker.model.call", durationSeconds, {
124
+ provider,
125
+ outcome,
126
+ });
122
127
  observability.incrementCounter({
123
128
  name: "opengeni_model_calls_total",
124
129
  help: "Total model calls by provider and outcome.",
@@ -172,6 +177,20 @@ export function runtimeMetricsHooksForObservability(
172
177
  labels: { outcome, port: String(port) },
173
178
  });
174
179
  },
180
+ onWorkspaceCapture: ({ backend, outcome, durationSeconds }) => {
181
+ if (!Number.isFinite(durationSeconds) || durationSeconds < 0) return;
182
+ const safeBackend = SandboxBackend.safeParse(backend).success ? backend : "unknown";
183
+ completedOperationSpan(observability, "worker.workspace_capture", durationSeconds, {
184
+ backend: safeBackend,
185
+ outcome,
186
+ });
187
+ observability.observeHistogram({
188
+ name: "opengeni_workspace_capture_duration_seconds",
189
+ help: "Physical warm workspace capture and publication duration, including late settlement after caller timeout.",
190
+ labels: { backend: safeBackend, outcome },
191
+ value: durationSeconds,
192
+ });
193
+ },
175
194
  onWorkspaceArchiveObject: ({ outcome, backend }) => {
176
195
  observability.incrementCounter({
177
196
  name: "opengeni_workspace_archive_object_total",
@@ -180,6 +199,7 @@ export function runtimeMetricsHooksForObservability(
180
199
  });
181
200
  },
182
201
  onMcpToolCall: ({ outcome, durationSeconds }) => {
202
+ completedOperationSpan(observability, "worker.mcp.tool_call", durationSeconds, { outcome });
183
203
  observability.incrementCounter({
184
204
  name: "opengeni_mcp_tool_calls_total",
185
205
  help: "Total physical MCP tool calls by bounded structural outcome.",
@@ -1386,7 +1406,7 @@ export type TurnStartupPhase =
1386
1406
  | "history_generated_image_materialization"
1387
1407
  | "history_position_load"
1388
1408
  | "owned_sandbox_setup"
1389
- | "provider_dispatch"
1409
+ | "runtime_stream_initialization"
1390
1410
  | "model_request_preparation"
1391
1411
  | "model_sdk_serialization"
1392
1412
  | "model_prepare_sandbox_agent_preparation"
@@ -1492,7 +1512,8 @@ export function turnStartupCountBucket(count: number | null): TurnStartupCountBu
1492
1512
  }
1493
1513
 
1494
1514
  /**
1495
- * Measure the critical path from a durable turn start to provider dispatch.
1515
+ * Measure startup operations, including nested and parallel work. These are
1516
+ * not additive critical-path intervals; use milestones for elapsed latency.
1496
1517
  * Every label is a closed or configuration-derived enum; high-cardinality turn,
1497
1518
  * session, credential, connection, file, and model identifiers are forbidden.
1498
1519
  */
@@ -1506,8 +1527,19 @@ export function recordTurnStartupPhase(
1506
1527
  durationSeconds: number;
1507
1528
  count?: number | null;
1508
1529
  cache?: TurnStartupCache;
1530
+ /** Trace-only lookup for the completed claim while its execution root is still open. */
1531
+ executionCorrelationId?: string;
1509
1532
  },
1510
1533
  ): void {
1534
+ completedOperationSpan(observability, `worker.prepare.${input.phase}`, input.durationSeconds, {
1535
+ provider: input.provider,
1536
+ backend: input.backend,
1537
+ outcome: input.outcome,
1538
+ ...(input.phase === "claim_and_policy" &&
1539
+ /^turn_[0-9a-f]{32}$/.test(input.executionCorrelationId ?? "")
1540
+ ? { correlationId: input.executionCorrelationId }
1541
+ : {}),
1542
+ });
1511
1543
  observability.observeHistogram({
1512
1544
  name: "opengeni_turn_startup_phase_duration_seconds",
1513
1545
  help: "Turn startup phase duration before the model response stream begins.",
@@ -1524,6 +1556,77 @@ export function recordTurnStartupPhase(
1524
1556
  });
1525
1557
  }
1526
1558
 
1559
+ /** Background MCP preparation must not inflate startup phase distributions. */
1560
+ export function recordToolPreparationPhase(
1561
+ observability: Observability,
1562
+ input: ToolPreparationPhaseMeasurement & { provider: string; backend: string },
1563
+ ): void {
1564
+ if (input.execution === "blocking") {
1565
+ recordTurnStartupPhase(observability, {
1566
+ ...input,
1567
+ phase: `tool_${input.phase}`,
1568
+ });
1569
+ return;
1570
+ }
1571
+ completedOperationSpan(
1572
+ observability,
1573
+ `worker.tool_prepare.${input.phase}`,
1574
+ input.durationSeconds,
1575
+ {
1576
+ provider: input.provider,
1577
+ backend: input.backend,
1578
+ outcome: input.outcome,
1579
+ },
1580
+ );
1581
+ observability.observeHistogram({
1582
+ name: "opengeni_tool_background_preparation_duration_seconds",
1583
+ help: "Nonblocking MCP preparation operations; may overlap startup and later execution.",
1584
+ buckets: TURN_STARTUP_PHASE_BUCKETS,
1585
+ labels: {
1586
+ phase: input.phase,
1587
+ provider: input.provider,
1588
+ backend: input.backend,
1589
+ outcome: input.outcome,
1590
+ },
1591
+ value: Math.max(0, input.durationSeconds),
1592
+ });
1593
+ }
1594
+
1595
+ /** Completed measurements become siblings under the scoped physical attempt.
1596
+ * They never establish ambient ancestry for work which has already finished. */
1597
+ function completedOperationSpan(
1598
+ observability: Observability,
1599
+ name: string,
1600
+ durationSeconds: number,
1601
+ attributes: {
1602
+ outcome: string;
1603
+ provider?: string;
1604
+ backend?: string;
1605
+ correlationId?: string | undefined;
1606
+ },
1607
+ ): void {
1608
+ if (!Number.isFinite(durationSeconds) || durationSeconds < 0) return;
1609
+ try {
1610
+ const failed = [
1611
+ "failed",
1612
+ "error",
1613
+ "provider_declared_error",
1614
+ "auth_needed",
1615
+ "outcome_uncertain",
1616
+ "timeout",
1617
+ "thrown_transport_error",
1618
+ "thrown_protocol_error",
1619
+ ].includes(attributes.outcome);
1620
+ observability
1621
+ .startSpan(name, attributes, { startTimeMs: Date.now() - durationSeconds * 1_000 })
1622
+ .end({
1623
+ ...(failed ? { error: true } : {}),
1624
+ });
1625
+ } catch {
1626
+ // Observers cannot change the completed model/tool/phase outcome.
1627
+ }
1628
+ }
1629
+
1527
1630
  /**
1528
1631
  * Explain why a turn did or did not keep provider sandbox work off the
1529
1632
  * pre-model critical path. The reason is a closed enum: never attach session,
@@ -329,7 +329,6 @@ function exactPersonalGitHubDelegation(
329
329
  delegation.providerDomain !== "github.com" ||
330
330
  delegation.kind !== "oauth2" ||
331
331
  !delegation.originWorkspaceId ||
332
- !delegation.userDelegation ||
333
332
  !delegation.personalGitHubRepositorySelection
334
333
  ) {
335
334
  throw new Error("personal GitHub Git delegation is unavailable");
@@ -5,7 +5,6 @@ import type {
5
5
  GitCredentialsRequest,
6
6
  } from "@opengeni/contracts";
7
7
  import { prReviewRegistrationIdFromCredentialBinding } from "@opengeni/core";
8
- import { createNativeRemoteMcpCredentialsPort } from "@opengeni/core/remote-mcp-credentials";
9
8
  import { decryptVariableSetValue, resolvePrReviewGitCredential, type Database } from "@opengeni/db";
10
9
  import {
11
10
  createGitHubAppInstallationTokenWithExpiry,
@@ -19,7 +18,6 @@ export function createStandaloneConnectionCredentialsPort(
19
18
  db: Database,
20
19
  ): ConnectionCredentialsPort {
21
20
  return {
22
- ...createNativeRemoteMcpCredentialsPort(settings, db),
23
21
  gitCredentials: async (request) => await resolveStandaloneGitCredentials(settings, db, request),
24
22
  };
25
23
  }
@@ -0,0 +1,50 @@
1
+ import {
2
+ databaseFailureCode,
3
+ nestedPostgresSqlState,
4
+ safeDatabaseErrorFacts,
5
+ SandboxWorkspaceMutationFencedError,
6
+ type SandboxRetainedProcess,
7
+ } from "@opengeni/db";
8
+ import type { Observability } from "@opengeni/observability";
9
+
10
+ /** Protected cause locations and schema facts; public logs receive only the diagnostic key. */
11
+ export function warnRetainedProcessProofFailure(
12
+ observability: Pick<Observability, "recordFailureDiagnostic" | "warn">,
13
+ error: unknown,
14
+ process: Pick<
15
+ SandboxRetainedProcess,
16
+ "id" | "sessionId" | "ownerAttemptId" | "ownerTurnId" | "reconcileAttempts"
17
+ >,
18
+ ): void {
19
+ let correlationId: string | undefined;
20
+ try {
21
+ const sqlState = nestedPostgresSqlState(error);
22
+ const constraint = safeDatabaseErrorFacts(error).constraint;
23
+ correlationId = observability.recordFailureDiagnostic({
24
+ code:
25
+ error instanceof SandboxWorkspaceMutationFencedError
26
+ ? "retained_process_fenced"
27
+ : sqlState
28
+ ? databaseFailureCode(sqlState)
29
+ : "retained_process_proof_failed",
30
+ stage: "sandbox_retained_processes.proof",
31
+ error,
32
+ processId: process.id,
33
+ sessionId: process.sessionId,
34
+ ...(process.ownerAttemptId ? { attemptId: process.ownerAttemptId } : {}),
35
+ ...(process.ownerTurnId ? { turnId: process.ownerTurnId } : {}),
36
+ attempts: process.reconcileAttempts,
37
+ sqlState,
38
+ ...(constraint ? { constraint } : {}),
39
+ });
40
+ } catch {
41
+ // A diagnostic/export failure must not change reconciliation or release its fence.
42
+ }
43
+ try {
44
+ observability.warn("sandbox reaper: retained-process proof checkpoint failed", {
45
+ ...(correlationId ? { correlationId } : {}),
46
+ });
47
+ } catch {
48
+ // Keep the original failure behavior even when the public sink fails.
49
+ }
50
+ }
@@ -27,6 +27,7 @@ import {
27
27
  type Settings,
28
28
  } from "@opengeni/config";
29
29
  import { randomUUID } from "node:crypto";
30
+ import { retainsHolderForWarmCapture } from "./warm-capture-holder";
30
31
  import {
31
32
  acquireLease,
32
33
  adoptLegacyModalCheckpointArtifact,
@@ -86,7 +87,9 @@ import {
86
87
  WorkspaceArchiveStorageError,
87
88
  type ObjectStorage,
88
89
  } from "@opengeni/storage";
89
- import type { Observability } from "@opengeni/observability";
90
+ import { sandboxLeaseTelemetryKey, type Observability } from "@opengeni/observability";
91
+ import { safeSnapshotError } from "./sandbox-snapshot-diagnostics";
92
+ export { safeSnapshotError } from "./sandbox-snapshot-diagnostics";
90
93
  import { parseWorkspaceArchiveObjectRef } from "@opengeni/contracts";
91
94
  import {
92
95
  persistWorkspaceArchiveCandidate,
@@ -125,9 +128,7 @@ export type SandboxResumeServices = {
125
128
  db: Database;
126
129
  settings: Settings;
127
130
  objectStorage?: ObjectStorage | null;
128
- /** Exact settings before a verified rig provider image overlaid the logical
129
- * pack/deployment image. Fresh-create NotFound fallback must preserve an
130
- * ID-only logical base instead of selecting the provider default. */
131
+
131
132
  logicalFallbackSettings?: Settings;
132
133
  sandboxMetrics?: RuntimeMetricsHooks;
133
134
  /** Structured logger for lifecycle decisions (holder release reasons). Falls
@@ -369,54 +370,6 @@ class SnapshotTimeoutError extends Error {
369
370
  }
370
371
  }
371
372
 
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
373
  export async function waitForWarmSnapshot(
421
374
  snapshot: Promise<unknown>,
422
375
  timeoutMs: number,
@@ -787,6 +740,19 @@ async function persistWarmWorkspaceSnapshot(
787
740
  registerOwnedContinuation: (continuation: Promise<void>) => void,
788
741
  ): Promise<boolean> {
789
742
  const { db, settings } = services;
743
+ const warnSnapshotFailure = (message: string, error: unknown): void => {
744
+ const fields = {
745
+ ...safeSnapshotError(error),
746
+ sandboxLeaseKey: sandboxLeaseTelemetryKey(ids.workspaceId, ids.sandboxGroupId),
747
+ leaseEpoch,
748
+ };
749
+ try {
750
+ if (services.observability) services.observability.warn(message, fields);
751
+ else console.warn(message, fields);
752
+ } catch {
753
+ // Diagnostics cannot change provider settlement or capture-gate cleanup.
754
+ }
755
+ };
790
756
  const intervalMs = settings.sandboxSnapshotIntervalMs;
791
757
  if (intervalMs <= 0 && !force) {
792
758
  return false;
@@ -896,6 +862,9 @@ async function persistWarmWorkspaceSnapshot(
896
862
  : {}),
897
863
  });
898
864
  if (claimed.status !== "claimed") {
865
+ // A scheduled interval not being due is normal, including after a failed
866
+ // attempt. Do not turn every ten-second heartbeat into an error log.
867
+ if (claimed.status === "throttled") return false;
899
868
  console.error("mid-session workspace snapshot skipped (capture claim)", {
900
869
  sandboxGroupId: ids.sandboxGroupId,
901
870
  status: claimed.status,
@@ -951,6 +920,8 @@ async function persistWarmWorkspaceSnapshot(
951
920
  // turn signal resolves first. Its finally block is the only normal release
952
921
  // of the exact admission gate; a late callback cannot release a successor.
953
922
  const captureAndPublish = (async (): Promise<boolean> => {
923
+ const captureStarted = performance.now();
924
+ let captureOutcome: "completed" | "failed" = "failed";
954
925
  let archive: VerifiedWorkspaceArchivePayload | undefined;
955
926
  let candidate: { id: string } | null = null;
956
927
  let publicationAttempted = false;
@@ -1001,6 +972,7 @@ async function persistWarmWorkspaceSnapshot(
1001
972
  if (!wrote && candidate) {
1002
973
  await abandonCandidate(candidate.id, "snapshot_publication_fenced");
1003
974
  }
975
+ if (wrote) captureOutcome = "completed";
1004
976
  return wrote;
1005
977
  } catch (error) {
1006
978
  if (candidate && !publicationAttempted)
@@ -1018,20 +990,24 @@ async function persistWarmWorkspaceSnapshot(
1018
990
  expectedEpoch: leaseEpoch,
1019
991
  expectedInstanceId: instanceId,
1020
992
  }).catch((error) => {
1021
- console.error(
1022
- "mid-session workspace capture gate release failed",
1023
- safeSnapshotError(error),
1024
- );
993
+ captureOutcome = "failed";
994
+ warnSnapshotFailure("mid-session workspace capture gate release failed", error);
1025
995
  });
996
+ try {
997
+ services.sandboxMetrics?.onWorkspaceCapture?.({
998
+ backend: lease.backend,
999
+ outcome: captureOutcome,
1000
+ durationSeconds: Math.max(0, performance.now() - captureStarted) / 1_000,
1001
+ });
1002
+ } catch {
1003
+ // A completed capture and released gate cannot depend on telemetry.
1004
+ }
1026
1005
  }
1027
1006
  })();
1028
1007
  const settled = captureAndPublish.then(
1029
1008
  (persisted) => ({ kind: "settled" as const, persisted }),
1030
1009
  (error) => {
1031
- console.error(
1032
- "mid-session workspace snapshot failed (turn unaffected)",
1033
- safeSnapshotError(error),
1034
- );
1010
+ warnSnapshotFailure("mid-session workspace snapshot failed (turn unaffected)", error);
1035
1011
  return { kind: "settled" as const, persisted: false };
1036
1012
  },
1037
1013
  );
@@ -1063,12 +1039,10 @@ async function persistWarmWorkspaceSnapshot(
1063
1039
  // exact claim cleanup, so it cannot leak an unhandled promise or artifact.
1064
1040
  return outcome.kind === "settled" ? outcome.persisted : false;
1065
1041
  } catch (error) {
1066
- // Protection, not a dependency: a failed snapshot must never fail (or slow
1067
- // down retrying) the turn. The next heartbeat/turn-end tick retries.
1068
- console.error(
1069
- "mid-session workspace snapshot failed (turn unaffected)",
1070
- safeSnapshotError(error),
1071
- );
1042
+ // A failed snapshot does not fail the turn. Physical captures can still
1043
+ // fence commands until their exact gate settles; the caller timeout is not
1044
+ // permission to release that gate. The next eligible tick can retry.
1045
+ warnSnapshotFailure("mid-session workspace snapshot failed (turn unaffected)", error);
1072
1046
  return false;
1073
1047
  }
1074
1048
  }
@@ -1276,7 +1250,16 @@ export async function resumeBoxForTurn(
1276
1250
  // provider operation may still be mutating /workspace. Both loops therefore
1277
1251
  // converge on "holder alive -> never release"; finalization performs the
1278
1252
  // proof-bearing release.
1279
- const releaseDeadHolder = (reason: string): void => {
1253
+ const releaseDeadHolder = async (reason: string): Promise<void> => {
1254
+ // Logical closure deliberately fences heartbeat authority. It is not proof
1255
+ // that the already-admitted capture has physically settled: the finalizer
1256
+ // owns that wait and the original deadline bounds dead-worker recovery.
1257
+ // Match the reaper's retention rule without touching holder/lease TTLs.
1258
+ if (reason === "holder_gone" && kind === "turn" && holderLeaseHeartbeat) {
1259
+ const expectedEpoch = holderLeaseHeartbeat.expectedEpoch;
1260
+ const lease = await readLease(db, ids.workspaceId, ids.sandboxGroupId);
1261
+ if (retainsHolderForWarmCapture(lease, expectedEpoch)) return;
1262
+ }
1280
1263
  const message = "sandbox resume holder released: holder no longer live";
1281
1264
  const fields = {
1282
1265
  workspaceId: ids.workspaceId,
@@ -1301,13 +1284,13 @@ export async function resumeBoxForTurn(
1301
1284
  expectedEpoch: heartbeat.expectedEpoch,
1302
1285
  leaseTtlMs: heartbeat.leaseTtlMs,
1303
1286
  })
1304
- .then((status) => {
1287
+ .then(async (status) => {
1305
1288
  if (heartbeat !== holderLeaseHeartbeat) return;
1306
1289
  if (!status.holderAlive) {
1307
1290
  // Reaped, released, or its exact attempt is no longer the active
1308
1291
  // writer. Stop this otherwise-unbounded provider operation and
1309
1292
  // idempotently drop any remaining holder state.
1310
- releaseDeadHolder(status.fence);
1293
+ await releaseDeadHolder(status.fence);
1311
1294
  return;
1312
1295
  }
1313
1296
  if (status.fence === "epoch") {
@@ -1316,7 +1299,7 @@ export async function resumeBoxForTurn(
1316
1299
  // instance replaced, re-established by a later turn). The holder
1317
1300
  // row is stale authority for an instance that no longer exists;
1318
1301
  // drop it promptly instead of pinning a successor's lease.
1319
- releaseDeadHolder(status.fence);
1302
+ await releaseDeadHolder(status.fence);
1320
1303
  return;
1321
1304
  }
1322
1305
  // The box is still serving this live holder: keep the provider TTL
@@ -1338,13 +1321,13 @@ export async function resumeBoxForTurn(
1338
1321
  kind,
1339
1322
  holderId,
1340
1323
  })
1341
- .then((touched) => {
1324
+ .then(async (touched) => {
1342
1325
  if (heartbeat !== holderLeaseHeartbeat) return;
1343
1326
  // A canonical turn holder is rejected once its exact attempt is no
1344
1327
  // longer the active writer (touch is attempt-fenced only; it never
1345
1328
  // consults lease authority, so false here always means holder death).
1346
1329
  if (!touched) {
1347
- releaseDeadHolder("holder_gone");
1330
+ await releaseDeadHolder("holder_gone");
1348
1331
  return;
1349
1332
  }
1350
1333
  void maybeRenewProviderExpiration().catch(() => undefined);
@@ -1422,7 +1405,8 @@ export async function resumeBoxForTurn(
1422
1405
  const continuityRecovery = acquired.lease.recovery.continuity;
1423
1406
  if (
1424
1407
  (acquired.lease.recovery.archive.status === "available" &&
1425
- acquired.lease.archiveComplete) ||
1408
+ (acquired.lease.archiveComplete ||
1409
+ acquired.lease.historicalRecoveryAuthorized === true)) ||
1426
1410
  (acquired.lease.recovery.archive.status === "none" &&
1427
1411
  workspaceArchiveFieldsFromEnvelope(archiveSource) !== null)
1428
1412
  ) {
@@ -1875,6 +1859,7 @@ export async function resumeBoxForTurn(
1875
1859
  sandboxGroupId: ids.sandboxGroupId,
1876
1860
  expectedEpoch: leaseEpoch,
1877
1861
  expectedInstanceId: live.instanceId,
1862
+ expectedBackend: ids.backend,
1878
1863
  });
1879
1864
  if (marked.status === "marked") {
1880
1865
  await services.onSandboxLost?.({
@@ -17,12 +17,13 @@
17
17
  import { sandboxLifecycleTransitionWaitMs, type Settings } from "@opengeni/config";
18
18
  import {
19
19
  createProviderCommandRetainer,
20
- retainedProviderCommandPersistence,
20
+ supervisedCommandProtocolReady,
21
21
  } from "@opengeni/db/retained-provider-commands";
22
22
  import {
23
23
  adoptConnectedMachineSessionBackgroundCommand,
24
24
  adoptManagedSessionBackgroundCommand,
25
25
  advanceWorkspaceGenerationForRetainedProcess,
26
+ retainedProviderCommandPersistence,
26
27
  advanceWorkspaceGeneration,
27
28
  getRetainedProcess,
28
29
  retainWorkspaceMutationProcess,
@@ -72,6 +73,7 @@ import {
72
73
  type RoutingMutationSettlementResult,
73
74
  type RoutingSandboxFirstOperationObserver,
74
75
  type RoutingSandboxOperationObserver,
76
+ type RoutingSandboxCaptureWaitObserver,
75
77
  type RoutingSandboxWaitObservation,
76
78
  type RoutingRetainedProcess,
77
79
  type RoutingRetainedProcessTerminalProof,
@@ -112,6 +114,7 @@ export type RoutingWiringServices = {
112
114
  onOp?: SelfhostedOpObserver;
113
115
  /** Every physical routed provider call, across cloud and selfhosted homes. */
114
116
  onSandboxOperation?: RoutingSandboxOperationObserver;
117
+ onSandboxCaptureWait?: RoutingSandboxCaptureWaitObserver;
115
118
  /** The op-stream durable-resume journal (the Temporal adaptation from
116
119
  * op-journal.ts): attach generation + settled-frontier persistence. Absent ⇒
117
120
  * the runtime defaults (generation "1", no persistence) — tests / non-turn
@@ -124,11 +127,12 @@ export type RoutingWiringServices = {
124
127
  instanceId: string;
125
128
  leaseEpoch: number;
126
129
  }) => Promise<void>;
127
- /** Called when a route repair replaces the worker's original home handle. */
130
+ /** Prepare and publish the worker's home handle. Rejection leaves the route
131
+ * unpublished; replacement preparation must settle before the first tool op. */
128
132
  onHomeSandboxRebound?: (input: {
129
133
  established: EstablishedSandboxSession;
130
134
  leaseEpoch: number;
131
- }) => void;
135
+ }) => void | Promise<void>;
132
136
  /** Cancel bounded capture waits with the owning turn activity. */
133
137
  waitSignal?: AbortSignal;
134
138
  };
@@ -342,7 +346,7 @@ async function resolveCurrentHomeBackend(
342
346
  // A route epoch can advance without a provider replacement. Reuse the exact
343
347
  // established handle only when its identity still equals the durable one.
344
348
  if (lease.instanceId === established.instanceId) {
345
- services.onHomeSandboxRebound?.({
349
+ await services.onHomeSandboxRebound?.({
346
350
  established,
347
351
  leaseEpoch: lease.leaseEpoch,
348
352
  });
@@ -393,6 +397,7 @@ async function resolveCurrentHomeBackend(
393
397
  sandboxGroupId: ids.sandboxGroupId,
394
398
  expectedEpoch: lease.leaseEpoch,
395
399
  expectedInstanceId: lease.instanceId,
400
+ expectedBackend: lease.backend,
396
401
  diagnostic: "provider_not_found_during_home_route_rebind",
397
402
  });
398
403
  if (marked.status === "marked") {
@@ -416,7 +421,7 @@ async function resolveCurrentHomeBackend(
416
421
  if (rebound.instanceId !== lease.instanceId || reboundBackend !== resumeBackend) {
417
422
  throw homeRouteRecoveryError(lease, lease.leaseEpoch);
418
423
  }
419
- services.onHomeSandboxRebound?.({
424
+ await services.onHomeSandboxRebound?.({
420
425
  established: rebound,
421
426
  leaseEpoch: lease.leaseEpoch,
422
427
  });
@@ -747,7 +752,7 @@ function settleRetainedProcessForTurn(
747
752
  if (
748
753
  !durable ||
749
754
  durable.providerSessionId !== process.providerSessionId ||
750
- durable.providerBackend !== backend.kind ||
755
+ durable.providerBackend !== (sandboxBackendForSdkBackendId(backend.kind) ?? backend.kind) ||
751
756
  durable.providerInstanceId !== backend.providerInstanceId ||
752
757
  durable.leaseEpoch !== backend.leaseEpoch ||
753
758
  durable.routeKind !== (backend.sandboxId === null ? "home" : "active") ||
@@ -795,7 +800,7 @@ function adoptRetainedProcessAsBackgroundCommandForTurn(
795
800
  if (
796
801
  !durable ||
797
802
  durable.providerSessionId !== process.providerSessionId ||
798
- durable.providerBackend !== backend.kind ||
803
+ durable.providerBackend !== (sandboxBackendForSdkBackendId(backend.kind) ?? backend.kind) ||
799
804
  durable.providerInstanceId !== backend.providerInstanceId ||
800
805
  durable.leaseEpoch !== backend.leaseEpoch ||
801
806
  durable.routeKind !== (backend.sandboxId === null ? "home" : "active") ||
@@ -1026,16 +1031,25 @@ export function wrapTurnBoxWithRouting(
1026
1031
  },
1027
1032
  resolveActiveBackend: resolver,
1028
1033
  ...(services.onSandboxOperation ? { onOperation: services.onSandboxOperation } : {}),
1034
+ ...(services.onSandboxCaptureWait ? { onCaptureWait: services.onSandboxCaptureWait } : {}),
1029
1035
  providerCommandHandle: admittedCommandHandle,
1036
+ providerSupervisionReady: async () =>
1037
+ settings.modalCommandSupervisionEnabled && (await supervisedCommandProtocolReady(db)),
1030
1038
  ...(ids.workspaceMutationFence
1031
1039
  ? {
1032
1040
  providerCommandPersistence: (process: RoutingRetainedProcess) =>
1033
- retainedProviderCommandPersistence(db, {
1034
- accountId: ids.workspaceMutationFence!.accountId,
1035
- workspaceId: ids.workspaceId,
1036
- sessionId: ids.sessionId,
1037
- processId: process.id,
1038
- }),
1041
+ retainedProviderCommandPersistence(
1042
+ db,
1043
+ {
1044
+ accountId: ids.workspaceMutationFence!.accountId,
1045
+ workspaceId: ids.workspaceId,
1046
+ sessionId: ids.sessionId,
1047
+ processId: process.id,
1048
+ },
1049
+ services.bus
1050
+ ? (events) => services.bus!.publish(ids.workspaceId, ids.sessionId, events)
1051
+ : undefined,
1052
+ ),
1039
1053
  }
1040
1054
  : {}),
1041
1055
  ...(beforeMutation ? { beforeMutation } : {}),
@@ -1069,6 +1083,7 @@ export function wrapTurnBoxWithRouting(
1069
1083
  sandboxGroupId: home.sandboxGroupId,
1070
1084
  expectedEpoch,
1071
1085
  expectedInstanceId,
1086
+ expectedBackend: home.backend,
1072
1087
  diagnostic: "provider_not_found_during_routed_operation",
1073
1088
  });
1074
1089
  if (marked.status === "marked") {
@@ -1276,17 +1291,26 @@ export function wrapLazyTurnBoxWithRouting(
1276
1291
  return routedResolver(pointer);
1277
1292
  },
1278
1293
  ...(services.onSandboxOperation ? { onOperation: services.onSandboxOperation } : {}),
1294
+ ...(services.onSandboxCaptureWait ? { onCaptureWait: services.onSandboxCaptureWait } : {}),
1279
1295
  ...(args.onFirstOperation ? { onFirstOperation: args.onFirstOperation } : {}),
1280
1296
  providerCommandHandle: admittedCommandHandle,
1297
+ providerSupervisionReady: async () =>
1298
+ settings.modalCommandSupervisionEnabled && (await supervisedCommandProtocolReady(db)),
1281
1299
  ...(ids.workspaceMutationFence
1282
1300
  ? {
1283
1301
  providerCommandPersistence: (process: RoutingRetainedProcess) =>
1284
- retainedProviderCommandPersistence(db, {
1285
- accountId: ids.workspaceMutationFence!.accountId,
1286
- workspaceId: ids.workspaceId,
1287
- sessionId: ids.sessionId,
1288
- processId: process.id,
1289
- }),
1302
+ retainedProviderCommandPersistence(
1303
+ db,
1304
+ {
1305
+ accountId: ids.workspaceMutationFence!.accountId,
1306
+ workspaceId: ids.workspaceId,
1307
+ sessionId: ids.sessionId,
1308
+ processId: process.id,
1309
+ },
1310
+ services.bus
1311
+ ? (events) => services.bus!.publish(ids.workspaceId, ids.sessionId, events)
1312
+ : undefined,
1313
+ ),
1290
1314
  }
1291
1315
  : {}),
1292
1316
  ...(beforeMutation ? { beforeMutation } : {}),
@@ -1321,6 +1345,7 @@ export function wrapLazyTurnBoxWithRouting(
1321
1345
  sandboxGroupId: home.sandboxGroupId,
1322
1346
  expectedEpoch: backend.leaseEpoch,
1323
1347
  expectedInstanceId: backend.providerInstanceId,
1348
+ expectedBackend: home.backend,
1324
1349
  diagnostic: "provider_not_found_during_routed_operation",
1325
1350
  });
1326
1351
  if (marked.status === "marked") {