@opengeni/worker-bundle 1.4.0 → 1.4.2

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 (36) hide show
  1. package/dist/activities/agent-turn/sandbox-provision.d.ts +22 -0
  2. package/dist/activities/agent-turn/turn-context.d.ts +6 -0
  3. package/dist/activities/agent-turn/web-search.d.ts +5 -1
  4. package/dist/activities/codemode-dispatcher.d.ts +8 -0
  5. package/dist/activities/programmatic-approvals.d.ts +0 -17
  6. package/dist/activities/sandbox-lease.d.ts +4 -0
  7. package/dist/{activities-control-D5KVEPHI.js → activities-control-AKJZHDTY.js} +53 -13
  8. package/dist/activities-control-AKJZHDTY.js.map +1 -0
  9. package/dist/{activities-turn-4Z3M7Z3F.js → activities-turn-AOXNE6BM.js} +6038 -5890
  10. package/dist/activities-turn-AOXNE6BM.js.map +1 -0
  11. package/dist/{chunk-FF4HPY7L.js → chunk-DCQ7XTXW.js} +108 -3
  12. package/dist/chunk-DCQ7XTXW.js.map +1 -0
  13. package/dist/{chunk-BMTWULF7.js → chunk-WVZQJBGD.js} +55 -17
  14. package/dist/chunk-WVZQJBGD.js.map +1 -0
  15. package/dist/index.js +2 -2
  16. package/dist/observability-metrics.d.ts +52 -14
  17. package/dist/sandbox-resume.d.ts +3 -0
  18. package/package.json +21 -21
  19. package/src/activities/agent-turn/errors.ts +7 -7
  20. package/src/activities/agent-turn/finalization.ts +16 -0
  21. package/src/activities/agent-turn/run.ts +12 -0
  22. package/src/activities/agent-turn/sandbox-establish.ts +87 -80
  23. package/src/activities/agent-turn/sandbox-provision.ts +51 -0
  24. package/src/activities/agent-turn/stream-attempt.ts +37 -50
  25. package/src/activities/agent-turn/turn-context.ts +8 -0
  26. package/src/activities/agent-turn/web-search.ts +135 -6
  27. package/src/activities/codemode-dispatcher.ts +57 -8
  28. package/src/activities/programmatic-approvals.ts +0 -26
  29. package/src/activities/sandbox-lease.ts +63 -10
  30. package/src/observability-metrics.ts +130 -18
  31. package/src/sandbox-resume.ts +24 -5
  32. package/src/sandbox-routing.ts +12 -0
  33. package/dist/activities-control-D5KVEPHI.js.map +0 -1
  34. package/dist/activities-turn-4Z3M7Z3F.js.map +0 -1
  35. package/dist/chunk-BMTWULF7.js.map +0 -1
  36. package/dist/chunk-FF4HPY7L.js.map +0 -1
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  startContextCompactionPendingMonitor,
20
20
  startSessionRecoveryMonitor,
21
21
  startTurnCapacityMonitor
22
- } from "./chunk-FF4HPY7L.js";
22
+ } from "./chunk-DCQ7XTXW.js";
23
23
  import "./chunk-PZ5AY32C.js";
24
24
 
25
25
  // src/index.ts
@@ -839,7 +839,7 @@ function temporalWorkflowExecutionNotFound(error) {
839
839
  return isGrpcServiceError(error) && error.code === 5;
840
840
  }
841
841
  async function createDefaultWorkerActivities(role, dependencies = {}) {
842
- return role === "control" ? (await import("./activities-control-D5KVEPHI.js")).createControlActivities(dependencies) : (await import("./activities-turn-4Z3M7Z3F.js")).createTurnActivities(dependencies);
842
+ return role === "control" ? (await import("./activities-control-AKJZHDTY.js")).createControlActivities(dependencies) : (await import("./activities-turn-AOXNE6BM.js")).createTurnActivities(dependencies);
843
843
  }
844
844
  function resolveOpenGeniWorkflowDefinition(moduleUrl = import.meta.url) {
845
845
  const modulePath = fileURLToPath(moduleUrl).replaceAll("\\", "/");
@@ -176,7 +176,7 @@ export declare function startContextCompactionPendingMonitor(input: {
176
176
  };
177
177
  export declare function recordSandboxLeaseGauges(observability: Observability, counts: Partial<Record<SandboxLeaseLiveness, number>>): void;
178
178
  export declare function recordOpenSandboxKubernetesInventoryGauges(observability: Observability, inventory: OpenSandboxKubernetesInventory): void;
179
- export declare const SANDBOX_INVENTORY_PROJECTION_DOMAINS: readonly ["leases", "checkpoint_artifacts", "recovery_observations", "rotation_backlog", "retained_processes", "expired_drains", "opensandbox_kubernetes"];
179
+ export declare const SANDBOX_INVENTORY_PROJECTION_DOMAINS: readonly ["leases", "checkpoint_artifacts", "recovery_observations", "rotation_backlog", "retained_processes", "expired_drains", "opensandbox_kubernetes", "modal_provider", "interaction_idle"];
180
180
  export type SandboxInventoryProjectionDomain = (typeof SANDBOX_INVENTORY_PROJECTION_DOMAINS)[number];
181
181
  export declare function recordSandboxInventoryProjectionSuccess(observability: Observability, domain: SandboxInventoryProjectionDomain, timestampSeconds?: number): void;
182
182
  export declare function recordSandboxInventoryProjectionFailure(observability: Observability, domain: SandboxInventoryProjectionDomain): void;
@@ -218,6 +218,22 @@ export declare function recordManagedAuthNewSignupsSwitchGauge(observability: Ob
218
218
  * grants happen only while this gauge and the runtime switch gauge are both 1.
219
219
  */
220
220
  export declare function recordVerifiedSignupTrialDeploymentFlagGauge(observability: Observability, enabled: boolean): void;
221
+ /**
222
+ * Provider-side Modal inventory reconciled against live leases by the orphan
223
+ * sweep: `running` = every running box in the app, `unleased` = running boxes
224
+ * no live lease protects (orphans, counted before termination), and
225
+ * `lease_missing_instance` = live (warming/warm/draining) leases whose exact
226
+ * provider instance is no longer running (zombies).
227
+ */
228
+ export declare function recordModalSandboxInventoryGauges(observability: Observability, inventory: {
229
+ running: number;
230
+ unleased: number;
231
+ liveLeaseInstancesMissing: number;
232
+ }): void;
233
+ /** Warm leases held only by Browser/Computer interaction holders, by time since
234
+ * the newest interaction activity. Such boxes never drain on their own until the
235
+ * interaction session ends or the provider deadline. */
236
+ export declare function recordInteractionOnlyLeaseGauges(observability: Observability, counts: Record<string, number>): void;
221
237
  export declare function recordSandboxOrphansTerminated(observability: Observability, count: number): void;
222
238
  export type SandboxCheckpointArtifactMetricState = "candidate" | "current" | "previous" | "delete_pending" | "deleting" | "delete_failed" | "deleted";
223
239
  export declare function recordSandboxCheckpointArtifactGauges(observability: Observability, counts: Record<SandboxCheckpointArtifactMetricState, number>): void;
@@ -284,7 +300,7 @@ export declare function recordModelRequestPhase(observability: Observability, in
284
300
  outcome?: "completed" | "failed" | "timed_out";
285
301
  durationSeconds: number;
286
302
  }): void;
287
- export type TurnStartupPhase = "services_initialization" | "claim_catalog_read" | "claim_atomic" | "claim_session_read" | "claim_capability_settings" | "learning_policy_freeze" | "attachment_authority_projection" | "claim_and_policy" | "turn_start_settlement" | "credential_selection" | "runtime_preparation" | "session_mcp_settings" | "sandbox_establish" | "file_resolution" | "tool_context_preparation" | "tool_preparation" | "tool_server_construction" | "tool_required_connect" | "tool_optional_connect" | "tool_attempt_catalog_build" | "tool_attempt_catalog_persist" | "tool_workspace_gateway_catalog_build" | "post_tool_preparation" | "agent_construction" | "post_agent_preparation" | "programmatic_operation_recovery" | "file_materialization" | "history_preparation" | "history_system_update_load" | "history_current_attachment_resolution" | "history_durable_history_load" | "history_sandbox_envelope_load" | "history_canonical_projection" | "history_provider_projection" | "history_attachment_ref_projection" | "history_screenshot_materialization" | "history_model_attachment_projection" | "history_runtime_input_assembly" | "history_artifact_candidate_scan" | "history_generated_image_materialization" | "history_position_load" | "owned_sandbox_setup" | "runtime_stream_initialization" | "model_request_preparation" | "model_sdk_serialization" | "model_prepare_sandbox_agent_preparation" | "model_prepare_sandbox_agent_manifest_inventory" | "model_prepare_sandbox_session_manifest_inventory" | "model_prepare_sandbox_manifest_apply" | "model_prepare_sandbox_entry_materialization" | "model_prepare_sandbox_running_check" | "model_prepare_sandbox_start" | "model_prepare_sandbox_client_create" | "model_prepare_sandbox_client_resume" | "model_prepare_sandbox_client_delete" | "model_prepare_sandbox_client_state_serialize" | "model_prepare_sandbox_client_reuse_check" | "model_prepare_sandbox_workspace_mutation_admission" | "model_prepare_sandbox_workspace_mutation_provider" | "model_prepare_sandbox_workspace_mutation_settlement" | "model_prepare_sandbox_first_routed_resolution_other" | "model_prepare_sandbox_first_routed_mutation_admission" | "model_prepare_sandbox_first_routed_provider_operation" | "model_prepare_sandbox_first_routed_mutation_settlement" | "model_prepare_sandbox_first_routed_other" | "model_prepare_sandbox_snapshot_wait" | "model_prepare_runner_before_first_sandbox_operation" | "model_prepare_sdk_after_first_sandbox_operation" | "model_prepare_runner_before_mcp_tools" | "model_prepare_mcp_tools_snapshot" | "model_prepare_mcp_tools_before_input_filter" | "model_prepare_mcp_tools_before_repository_skill_discovery" | "model_prepare_repository_skill_discovery" | "model_prepare_repository_skill_discovery_before_input_filter" | "model_prepare_input_filter_base" | "model_prepare_input_filter_genesis" | "model_prepare_input_filter_host" | "model_prepare_input_filter_tool_output" | "model_prepare_input_filter_modality" | "model_prepare_input_filter_context" | "model_prepare_responses_input_conversion" | "model_prepare_responses_request_build" | "model_credential_resolution" | "model_wire_normalization" | "model_request_audit" | "stream_bootstrap";
303
+ export type TurnStartupPhase = "services_initialization" | "claim_catalog_read" | "claim_atomic" | "claim_session_read" | "claim_capability_settings" | "learning_policy_freeze" | "attachment_authority_projection" | "claim_and_policy" | "turn_start_settlement" | "credential_selection" | "runtime_preparation" | "session_mcp_settings" | "sandbox_establish" | "file_resolution" | "tool_context_preparation" | "tool_preparation" | "tool_server_construction" | "tool_required_connect" | "tool_optional_connect" | "tool_attempt_catalog_build" | "tool_attempt_catalog_persist" | "tool_workspace_gateway_catalog_build" | "post_tool_preparation" | "agent_construction" | "post_agent_preparation" | "file_materialization" | "history_preparation" | "history_system_update_load" | "history_current_attachment_resolution" | "history_durable_history_load" | "history_sandbox_envelope_load" | "history_canonical_projection" | "history_provider_projection" | "history_attachment_ref_projection" | "history_screenshot_materialization" | "history_model_attachment_projection" | "history_runtime_input_assembly" | "history_artifact_candidate_scan" | "history_generated_image_materialization" | "history_position_load" | "owned_sandbox_setup" | "runtime_stream_initialization" | "model_request_preparation" | "model_sdk_serialization" | "model_prepare_sandbox_agent_preparation" | "model_prepare_sandbox_agent_manifest_inventory" | "model_prepare_sandbox_session_manifest_inventory" | "model_prepare_sandbox_manifest_apply" | "model_prepare_sandbox_entry_materialization" | "model_prepare_sandbox_running_check" | "model_prepare_sandbox_start" | "model_prepare_sandbox_client_create" | "model_prepare_sandbox_client_resume" | "model_prepare_sandbox_client_delete" | "model_prepare_sandbox_client_state_serialize" | "model_prepare_sandbox_client_reuse_check" | "model_prepare_sandbox_workspace_mutation_admission" | "model_prepare_sandbox_workspace_mutation_provider" | "model_prepare_sandbox_workspace_mutation_settlement" | "model_prepare_sandbox_first_routed_resolution_other" | "model_prepare_sandbox_first_routed_mutation_admission" | "model_prepare_sandbox_first_routed_provider_operation" | "model_prepare_sandbox_first_routed_mutation_settlement" | "model_prepare_sandbox_first_routed_other" | "model_prepare_sandbox_snapshot_wait" | "model_prepare_runner_before_first_sandbox_operation" | "model_prepare_sdk_after_first_sandbox_operation" | "model_prepare_runner_before_mcp_tools" | "model_prepare_mcp_tools_snapshot" | "model_prepare_mcp_tools_before_input_filter" | "model_prepare_mcp_tools_before_repository_skill_discovery" | "model_prepare_repository_skill_discovery" | "model_prepare_repository_skill_discovery_before_input_filter" | "model_prepare_input_filter_base" | "model_prepare_input_filter_genesis" | "model_prepare_input_filter_host" | "model_prepare_input_filter_tool_output" | "model_prepare_input_filter_modality" | "model_prepare_input_filter_context" | "model_prepare_responses_input_conversion" | "model_prepare_responses_request_build" | "model_credential_resolution" | "model_wire_normalization" | "model_request_audit" | "stream_bootstrap";
288
304
  export type TurnStartupOutcome = "completed" | "failed";
289
305
  export type TurnStartupMilestone = "queue" | "provider_dispatch" | "first_byte";
290
306
  export type TurnStartupCache = "hit" | "miss" | "disabled" | "none";
@@ -375,22 +391,35 @@ export declare function recordTurnStartupMilestone(observability: Observability,
375
391
  durationSeconds: number;
376
392
  }): void;
377
393
  export type StreamDeltaClass = "message" | "reasoning";
394
+ /** First streamed content of a provider response. `any` is the first reasoning
395
+ * or answer delta; `text` is the first answer-text delta. They overlap — never
396
+ * sum across `content`. */
397
+ export type ProviderFirstContent = "any" | "text";
378
398
  /**
379
- * Per-turn stream-timing tracker fed every normalized runtime event in push order.
380
- * It emits two model-responsiveness SLIs from the worker's seat on the stream:
399
+ * Per-stream timing tracker fed every normalized runtime event in push order,
400
+ * plus two producer-side hooks. It separates OUR latency from the PROVIDER's:
381
401
  *
382
- * - `opengeni_stream_ttft_seconds{provider}` — time from a model (re)start to its
383
- * first streamed content delta. The anchor starts at construction (≈ runStream
384
- * start, so the first observation is "how long until text appears") and re-arms
385
- * on every non-content event (a tool call, a completed message, a usage frame),
386
- * so a post-tool response measures the model's restart latency, NOT our own
387
- * tool-execution time.
402
+ * - `opengeni_stream_ttft_seconds{provider}` — user-perceived (re)start latency:
403
+ * time from a stream start / structural boundary (tool call, tool result,
404
+ * completed message, usage frame) to the first streamed content delta. It
405
+ * mixes our between-call work with provider time; keep it for the absolute
406
+ * dashboard view, not for provider alerting.
407
+ * - `opengeni_model_request_pre_dispatch_seconds{provider}` — OUR per-request
408
+ * latency: SDK model entry (the first admission check) to the literal
409
+ * provider dispatch. Covers admission, the durable history checkpoint,
410
+ * credit revalidation, the durable request audit and request build.
411
+ * - `opengeni_model_provider_ttft_seconds{provider,content}` — the PROVIDER's
412
+ * latency: literal dispatch to the first streamed `any` (reasoning or text)
413
+ * delta and to the first answer `text` delta. Includes network, provider
414
+ * queueing, prompt prefill and reasoning before the first streamed token.
388
415
  * - `opengeni_stream_inter_delta_gap_seconds{provider,class}` — gap between
389
- * consecutive content deltas of the SAME class. The run resets on any
390
- * non-content event so a gap never spans a tool call or a model boundary — it
391
- * measures only the choppiness of a live token stream.
416
+ * consecutive content deltas of the SAME class, reset at every structural
417
+ * event so a gap never spans a tool call or a model boundary.
392
418
  *
393
- * Purely observational and clock-injectable; it never touches the events it sees.
419
+ * Dispatch timing deliberately survives structural events: the consumer may
420
+ * still be persisting the previous response's tool results when the producer
421
+ * dispatches the next request, but the next request's first delta always
422
+ * follows its own dispatch. Purely observational and clock-injectable.
394
423
  */
395
424
  export declare class StreamTimingMetrics {
396
425
  private readonly observability;
@@ -399,11 +428,20 @@ export declare class StreamTimingMetrics {
399
428
  private ttftAnchor;
400
429
  private ttftArmed;
401
430
  private readonly lastDeltaAt;
431
+ private modelEntryAt;
432
+ private dispatchedAt;
433
+ private readonly providerFirstRecorded;
402
434
  constructor(observability: Observability, options: {
403
435
  provider: string;
404
436
  now?: () => number;
405
437
  });
438
+ /** Producer side: the SDK entered a model request (first admission check).
439
+ * Admission can be re-entered for the same request; keep the earliest. */
440
+ onModelRequestEntry(): void;
441
+ /** Producer side: request bytes are about to leave this process. */
442
+ onProviderDispatch(): void;
406
443
  onEvent(type: SessionEventType): void;
444
+ private observeProviderFirst;
407
445
  }
408
446
  /** Flush shape of the streaming batcher: how many events coalesced into one flush
409
447
  * (the coalescing win) and how long that flush took (append + publish). */
@@ -54,6 +54,9 @@ export type SandboxResumeServices = {
54
54
  * replacement. Production uses {@link freshSandboxReadinessReplacementDelayMs}.
55
55
  * It may be async so a test can observe the rolled-back lease in between. */
56
56
  freshSandboxReadinessReplacementDelayMs?: () => number | Promise<number>;
57
+ /** Test seam: runs immediately after the elected spawner published its box
58
+ * warm, before the final cancellation check. */
59
+ onSpawnedSandboxPublished?: () => void | Promise<void>;
57
60
  /**
58
61
  * The turn attempt's fresh-box readiness replacement budget. The lazy
59
62
  * provisioner may call resumeBoxForTurn again after a typed lease
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/worker-bundle",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "OpenGeni worker entry and reusable embedded lifecycle, shipped with a release-coherent pre-bundled Temporal workflow artifact.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -54,26 +54,26 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@llamaindex/liteparse": "2.14.2",
57
- "@opengeni/agent-proto": "1.4.0",
58
- "@opengeni/capabilities": "1.4.0",
59
- "@opengeni/codemode": "1.4.0",
60
- "@opengeni/codex": "1.4.0",
61
- "@opengeni/config": "1.4.0",
62
- "@opengeni/contracts": "1.4.0",
63
- "@opengeni/core": "1.4.0",
64
- "@opengeni/db": "1.4.0",
65
- "@opengeni/documents": "1.4.0",
66
- "@opengeni/events": "1.4.0",
67
- "@opengeni/github": "1.4.0",
68
- "@opengeni/interaction": "1.4.0",
69
- "@opengeni/jev": "1.4.0",
70
- "@opengeni/network": "1.4.0",
71
- "@opengeni/observability": "1.4.0",
72
- "@opengeni/runtime": "1.4.0",
73
- "@opengeni/sdk": "1.4.0",
74
- "@opengeni/storage": "1.4.0",
75
- "@opengeni/tool-gateway": "1.4.0",
76
- "@opengeni/xai-subscription": "1.4.0",
57
+ "@opengeni/agent-proto": "1.4.2",
58
+ "@opengeni/capabilities": "1.4.2",
59
+ "@opengeni/codemode": "1.4.2",
60
+ "@opengeni/codex": "1.4.2",
61
+ "@opengeni/config": "1.4.2",
62
+ "@opengeni/contracts": "1.4.2",
63
+ "@opengeni/core": "1.4.2",
64
+ "@opengeni/db": "1.4.2",
65
+ "@opengeni/documents": "1.4.2",
66
+ "@opengeni/events": "1.4.2",
67
+ "@opengeni/github": "1.4.2",
68
+ "@opengeni/interaction": "1.4.2",
69
+ "@opengeni/jev": "1.4.2",
70
+ "@opengeni/network": "1.4.2",
71
+ "@opengeni/observability": "1.4.2",
72
+ "@opengeni/runtime": "1.4.2",
73
+ "@opengeni/sdk": "1.4.2",
74
+ "@opengeni/storage": "1.4.2",
75
+ "@opengeni/tool-gateway": "1.4.2",
76
+ "@opengeni/xai-subscription": "1.4.2",
77
77
  "@temporalio/activity": "^1.17.0",
78
78
  "@temporalio/client": "^1.17.0",
79
79
  "@temporalio/worker": "^1.17.0",
@@ -500,15 +500,20 @@ function retryableDatabaseFailureCode(
500
500
  const connectionOutage = requireDatabaseProvenance
501
501
  ? isRunningTurnDatabaseConnectionSqlState(sqlState)
502
502
  : isDatabaseConnectionSqlState(sqlState);
503
+ // PostgreSQL aborts a deadlock or serialization victim's whole
504
+ // transaction: unlike a lost connection, that write certainly did not
505
+ // commit, so it is at least as safe for exact-attempt recovery.
506
+ const rolledBack = isRetryablePersistenceSqlState(sqlState);
503
507
  const code = typed
504
508
  ? retryablePersistenceFailureCode(sqlState)
505
- : connectionOutage || isRetryablePersistenceSqlState(sqlState)
509
+ : connectionOutage || rolledBack
506
510
  ? databaseFailureCode(sqlState)
507
511
  : null;
508
512
  if (
509
513
  !code ||
510
514
  (requireDatabaseProvenance &&
511
515
  !connectionOutage &&
516
+ !rolledBack &&
512
517
  !(sqlState === null && hasOwnTransport(node)))
513
518
  )
514
519
  return null;
@@ -603,12 +608,7 @@ export function postClaimDatabaseRecoveryFailure(input: {
603
608
  };
604
609
  }): ApplicationFailure | null {
605
610
  const code = retryableDatabaseFailureCode(input.error, input.requireDatabaseProvenance);
606
- if (
607
- !code ||
608
- input.executionGeneration < 1 ||
609
- (input.requireDatabaseProvenance && code !== "db_failure")
610
- )
611
- return null;
611
+ if (!code || input.executionGeneration < 1) return null;
612
612
  if (
613
613
  (input.sandboxSetupOutcomeUnknown && input.sandboxSetupRecoveryExhausted) ||
614
614
  ((input.sandboxSetupOutcomeUnknown || input.sandboxSetupRecoveryExhausted) &&
@@ -43,6 +43,7 @@ import {
43
43
  type ResumedTurnSandbox,
44
44
  } from "../../sandbox-resume";
45
45
  import { createTurnCredentialLeases } from "./credential-leases";
46
+ import { drainPhysicalSandboxResumes } from "./sandbox-provision";
46
47
  import { safeErrorDiagnostic, safeErrorForTelemetry } from "./errors";
47
48
  import {
48
49
  assertPhysicalToolQuiescenceForCancellation,
@@ -778,6 +779,21 @@ async function finalizeTurnAttemptSteps(
778
779
  }
779
780
  monitor.enter("workspace_snapshot");
780
781
  await drainInFlightWarmSnapshot();
782
+ // Join any provider establish still running behind a cancelled wrapper
783
+ // (finalization already aborted provisioning when no box resolved). Its
784
+ // own exact cleanup (roll an unpublished box back to cold, or keep a
785
+ // published box and drop its holder) must commit before this activity can
786
+ // let a shutting-down worker exit. A late success is routed through
787
+ // releaseLateSandbox before the release targets are collected below.
788
+ monitor.enter("sandbox_provisioning");
789
+ if (sandboxState.inFlightSandboxResumes.size > 0) {
790
+ const drained = await drainPhysicalSandboxResumes(sandboxState.inFlightSandboxResumes);
791
+ if (drained === "timed_out") {
792
+ console.error(
793
+ "in-flight sandbox establish did not settle before finalization; the lease reaper owns its warming row",
794
+ );
795
+ }
796
+ }
781
797
  monitor.enter("sandbox_release");
782
798
  const sandboxReleaseTargets = new Set(sandboxState.lateSandboxesAwaitingWriterDrain);
783
799
  sandboxState.lateSandboxesAwaitingWriterDrain.clear();
@@ -385,6 +385,14 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
385
385
  getModelRunSettings: () => eventing.modelRunSettings,
386
386
  getExecutionGeneration: () => attempt.executionGeneration,
387
387
  });
388
+ // Diagnostic only: a timing observer can never fence or fail a request.
389
+ const observeProviderDispatch = () => {
390
+ try {
391
+ eventing.providerDispatchObserver?.();
392
+ } catch {
393
+ // Metrics emission must never affect a model call.
394
+ }
395
+ };
388
396
  const checkpointBeforeProviderDispatch = async () => {
389
397
  await awaitModelCallAdmission();
390
398
  await checkpointHistoryBeforeProviderDispatch(historySink, {
@@ -682,6 +690,7 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
682
690
  }, // latest wins; flushed once in finally
683
691
  beforeProviderDispatch: () => {
684
692
  leases.codex.assertUsable();
693
+ observeProviderDispatch();
685
694
  },
686
695
  onRequestPreparationDiagnostic: (phase) => {
687
696
  if (
@@ -959,6 +968,9 @@ export function createRunAgentTurnActivity(services: () => Promise<ActivityServi
959
968
  },
960
969
  },
961
970
  ]);
971
+ // SuperGrok places bytes on the wire immediately after this
972
+ // awaited audit returns.
973
+ if (event.phase === "started") observeProviderDispatch();
962
974
  attempt.providerRecoveryCount = providerRecoveryCountAfterModelRequestPhase(
963
975
  attempt.providerRecoveryCount,
964
976
  event.phase,
@@ -66,6 +66,7 @@ import {
66
66
  classifySandboxLogicalProvisionFailure,
67
67
  isLazySandboxProvisionRetryable,
68
68
  createTurnSandboxProvisioner,
69
+ trackPhysicalSandboxResume,
69
70
  } from "./sandbox-provision";
70
71
  import {
71
72
  resolveActiveSandboxBackend,
@@ -645,40 +646,43 @@ export async function establishTurnSandbox(deps: EstablishTurnSandboxDeps): Prom
645
646
  const lazyHolderId = sandboxState.sandboxHolderId;
646
647
  const lazyGroupId = sandboxState.sandboxGroupId;
647
648
  sandboxState.resumeManagedGroupBox = () =>
648
- resumeBoxForTurn(
649
- {
650
- db,
651
- settings: runSettings,
652
- logicalFallbackSettings: logicalSandboxSettings,
653
- cancellationSignal: sandboxResumeSignal,
654
- sandboxMetrics: runtimeMetricsHooksForObservability(observability),
655
- observability,
656
- freshSandboxReadinessReplacementBudget,
657
- onSandboxLost: publishSandboxLost,
658
- objectStorage,
659
- bus,
660
- },
661
- {
662
- accountId: input.accountId,
663
- workspaceId: input.workspaceId,
664
- sandboxGroupId: lazyGroupId,
665
- sessionId: input.sessionId,
666
- backend: groupBoxBackend,
667
- os: groupBoxOs,
668
- environment: sandboxEnvironment,
669
- ...(groupBoxImage
670
- ? {
671
- image: groupBoxImage,
672
- imagePolicy: groupBoxImagePolicy,
673
- }
674
- : {}),
675
- // The lazy acquire must enforce the same frozen rig authority
676
- // as the eager acquire; otherwise a warm box for another rig
677
- // can bypass the shared-state conflict/rotation fence.
678
- ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
679
- },
680
- "turn",
681
- lazyHolderId,
649
+ trackPhysicalSandboxResume(
650
+ sandboxState.inFlightSandboxResumes,
651
+ resumeBoxForTurn(
652
+ {
653
+ db,
654
+ settings: runSettings,
655
+ logicalFallbackSettings: logicalSandboxSettings,
656
+ cancellationSignal: sandboxResumeSignal,
657
+ sandboxMetrics: runtimeMetricsHooksForObservability(observability),
658
+ observability,
659
+ freshSandboxReadinessReplacementBudget,
660
+ onSandboxLost: publishSandboxLost,
661
+ objectStorage,
662
+ bus,
663
+ },
664
+ {
665
+ accountId: input.accountId,
666
+ workspaceId: input.workspaceId,
667
+ sandboxGroupId: lazyGroupId,
668
+ sessionId: input.sessionId,
669
+ backend: groupBoxBackend,
670
+ os: groupBoxOs,
671
+ environment: sandboxEnvironment,
672
+ ...(groupBoxImage
673
+ ? {
674
+ image: groupBoxImage,
675
+ imagePolicy: groupBoxImagePolicy,
676
+ }
677
+ : {}),
678
+ // The lazy acquire must enforce the same frozen rig authority
679
+ // as the eager acquire; otherwise a warm box for another rig
680
+ // can bypass the shared-state conflict/rotation fence.
681
+ ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
682
+ },
683
+ "turn",
684
+ lazyHolderId,
685
+ ),
682
686
  );
683
687
  if (
684
688
  shouldPrefetchManagedSandbox({
@@ -725,52 +729,55 @@ export async function establishTurnSandbox(deps: EstablishTurnSandboxDeps): Prom
725
729
  );
726
730
  try {
727
731
  sandboxState.resolvedSandbox = await waitForTurnOperation(
728
- resumeBoxForTurn(
729
- {
730
- db,
731
- settings: runSettings,
732
- logicalFallbackSettings: logicalSandboxSettings,
733
- cancellationSignal: sandboxResumeSignal,
734
- sandboxMetrics: runtimeMetricsHooksForObservability(observability),
735
- observability,
736
- freshSandboxReadinessReplacementBudget,
737
- onSandboxLost: publishSandboxLost,
738
- objectStorage,
739
- bus,
740
- },
741
- {
742
- accountId: input.accountId,
743
- workspaceId: input.workspaceId,
744
- sandboxGroupId: session.sandboxGroupId,
745
- sessionId: input.sessionId,
746
- // groupBoxBackend, not turn.sandboxBackend: a machine-home turn that
747
- // is not machine-primary resumes a real cloud group box (the
748
- // deployment default), never a "selfhosted" box (which would throw
749
- // for lack of a bound agentId).
750
- backend: groupBoxBackend,
751
- os: groupBoxOs,
752
- environment: sandboxEnvironment,
753
- // Deployment/workspace pins select NEW creates only. The lease
754
- // retains a live group's image through between-turn repins, even
755
- // with other holders; only rotation/reaping elects a new image.
756
- // Select the image for the actual group-box backend; a configured Modal image must
757
- // never override a Docker run. The selfhosted branch
758
- // (establishSelfhostedTurnSession) NEVER passes
759
- // an image — B3 lives only on this managed-box branch.
760
- ...(groupBoxImage
761
- ? {
762
- image: groupBoxImage,
763
- imagePolicy: groupBoxImagePolicy,
764
- }
765
- : {}),
766
- // RIG IS SHARED STATE (M3): stamp the frozen rig version so the lease
767
- // conflicts on a live shared box set up under a different rig (solo
768
- // durable rotation / N-holders SandboxRigConflictError). Omitted for a
769
- // rig-less turn -> never stamped or enforced (shares exactly as today).
770
- ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
771
- },
772
- "turn",
773
- managedOwnership!.holderId,
732
+ trackPhysicalSandboxResume(
733
+ sandboxState.inFlightSandboxResumes,
734
+ resumeBoxForTurn(
735
+ {
736
+ db,
737
+ settings: runSettings,
738
+ logicalFallbackSettings: logicalSandboxSettings,
739
+ cancellationSignal: sandboxResumeSignal,
740
+ sandboxMetrics: runtimeMetricsHooksForObservability(observability),
741
+ observability,
742
+ freshSandboxReadinessReplacementBudget,
743
+ onSandboxLost: publishSandboxLost,
744
+ objectStorage,
745
+ bus,
746
+ },
747
+ {
748
+ accountId: input.accountId,
749
+ workspaceId: input.workspaceId,
750
+ sandboxGroupId: session.sandboxGroupId,
751
+ sessionId: input.sessionId,
752
+ // groupBoxBackend, not turn.sandboxBackend: a machine-home turn that
753
+ // is not machine-primary resumes a real cloud group box (the
754
+ // deployment default), never a "selfhosted" box (which would throw
755
+ // for lack of a bound agentId).
756
+ backend: groupBoxBackend,
757
+ os: groupBoxOs,
758
+ environment: sandboxEnvironment,
759
+ // Deployment/workspace pins select NEW creates only. The lease
760
+ // retains a live group's image through between-turn repins, even
761
+ // with other holders; only rotation/reaping elects a new image.
762
+ // Select the image for the actual group-box backend; a configured Modal image must
763
+ // never override a Docker run. The selfhosted branch
764
+ // (establishSelfhostedTurnSession) NEVER passes
765
+ // an image — B3 lives only on this managed-box branch.
766
+ ...(groupBoxImage
767
+ ? {
768
+ image: groupBoxImage,
769
+ imagePolicy: groupBoxImagePolicy,
770
+ }
771
+ : {}),
772
+ // RIG IS SHARED STATE (M3): stamp the frozen rig version so the lease
773
+ // conflicts on a live shared box set up under a different rig (solo
774
+ // durable rotation / N-holders SandboxRigConflictError). Omitted for a
775
+ // rig-less turn -> never stamped or enforced (shares exactly as today).
776
+ ...(rigVersion ? { rigVersionId: rigVersion.id } : {}),
777
+ },
778
+ "turn",
779
+ managedOwnership!.holderId,
780
+ ),
774
781
  ),
775
782
  sandboxResumeSignal,
776
783
  releaseLateSandbox,
@@ -153,6 +153,57 @@ export async function waitForTurnOperation<T>(
153
153
  }
154
154
  }
155
155
 
156
+ /**
157
+ * Upper bound for joining a cancelled attempt's in-flight provider establish.
158
+ * Worker shutdown cancels activities 5s after SIGTERM and force-stops 100s
159
+ * after it; this leaves room for the remaining finalization steps.
160
+ */
161
+ export const PHYSICAL_SANDBOX_RESUME_DRAIN_TIMEOUT_MS = 60_000;
162
+
163
+ /** Register the PHYSICAL lease acquire/provider establish promise, not the
164
+ * cancellable wrapper awaiting it. */
165
+ export function trackPhysicalSandboxResume<T>(
166
+ inFlight: Set<Promise<unknown>>,
167
+ operation: Promise<T>,
168
+ ): Promise<T> {
169
+ inFlight.add(operation);
170
+ const settle = (): void => {
171
+ inFlight.delete(operation);
172
+ };
173
+ operation.then(settle, settle);
174
+ return operation;
175
+ }
176
+
177
+ /**
178
+ * Join every lease acquire/provider establish this attempt started, even after
179
+ * the turn-facing wrapper rejected on cancellation. An establish has no
180
+ * portable abort: after cancellation it still finishes its own exact cleanup
181
+ * (terminate an unpublished box and roll warming back to cold, or keep a
182
+ * published box and drop its holder). If the activity returned first, a
183
+ * shutting-down worker exited mid-cleanup and stranded a warming lease naming a
184
+ * terminated box; the replacement attempt then waited out the warming TTL and
185
+ * inherited a lost-workspace verdict. This wait is deliberately independent of
186
+ * the finalizer cancellation signal (cancellation is what started it) and is
187
+ * bounded so an uncooperative provider call cannot pin the activity.
188
+ */
189
+ export async function drainPhysicalSandboxResumes(
190
+ inFlight: Set<Promise<unknown>>,
191
+ timeoutMs: number = PHYSICAL_SANDBOX_RESUME_DRAIN_TIMEOUT_MS,
192
+ ): Promise<"none" | "settled" | "timed_out"> {
193
+ if (inFlight.size === 0) return "none";
194
+ let timer: ReturnType<typeof setTimeout> | undefined;
195
+ try {
196
+ return await Promise.race([
197
+ Promise.allSettled([...inFlight]).then(() => "settled" as const),
198
+ new Promise<"timed_out">((resolve) => {
199
+ timer = setTimeout(() => resolve("timed_out"), Math.max(1, timeoutMs));
200
+ }),
201
+ ]);
202
+ } finally {
203
+ if (timer) clearTimeout(timer);
204
+ }
205
+ }
206
+
156
207
  export function sleep(ms: number): Promise<void> {
157
208
  return new Promise((resolve) => setTimeout(resolve, ms));
158
209
  }