@namzu/sdk 52.0.0 → 52.2.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 (157) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/dist/compaction/verifier.d.ts.map +1 -1
  4. package/dist/compaction/verifier.js +57 -17
  5. package/dist/compaction/verifier.js.map +1 -1
  6. package/dist/connector/tools/adapter.d.ts +2 -2
  7. package/dist/contracts/a2a.d.ts +94 -94
  8. package/dist/manager/resident/learning.d.ts +6 -6
  9. package/dist/manager/resident/store.d.ts +4 -4
  10. package/dist/pals/activity/subscription-schema.d.ts +48 -48
  11. package/dist/pals/communication/ingress-schema.d.ts +192 -192
  12. package/dist/pals/communication/schema.d.ts +18 -18
  13. package/dist/peers/protocol.d.ts +36 -36
  14. package/dist/provider/capabilities.d.ts +9 -0
  15. package/dist/provider/capabilities.d.ts.map +1 -1
  16. package/dist/provider/capabilities.js +15 -0
  17. package/dist/provider/capabilities.js.map +1 -1
  18. package/dist/provider/errors.d.ts.map +1 -1
  19. package/dist/provider/errors.js +13 -0
  20. package/dist/provider/errors.js.map +1 -1
  21. package/dist/provider/fallback.d.ts.map +1 -1
  22. package/dist/provider/fallback.js +10 -0
  23. package/dist/provider/fallback.js.map +1 -1
  24. package/dist/provider/idle-timeout.d.ts.map +1 -1
  25. package/dist/provider/idle-timeout.js +5 -0
  26. package/dist/provider/idle-timeout.js.map +1 -1
  27. package/dist/provider/image-input-refusal.d.ts +9 -0
  28. package/dist/provider/image-input-refusal.d.ts.map +1 -0
  29. package/dist/provider/image-input-refusal.js +54 -0
  30. package/dist/provider/image-input-refusal.js.map +1 -0
  31. package/dist/provider/index.d.ts +2 -1
  32. package/dist/provider/index.d.ts.map +1 -1
  33. package/dist/provider/index.js +2 -1
  34. package/dist/provider/index.js.map +1 -1
  35. package/dist/provider/retry.d.ts +7 -2
  36. package/dist/provider/retry.d.ts.map +1 -1
  37. package/dist/provider/retry.js +38 -4
  38. package/dist/provider/retry.js.map +1 -1
  39. package/dist/public-runtime.d.ts +2 -1
  40. package/dist/public-runtime.d.ts.map +1 -1
  41. package/dist/public-runtime.js +1 -1
  42. package/dist/public-runtime.js.map +1 -1
  43. package/dist/public-tools.d.ts +1 -0
  44. package/dist/public-tools.d.ts.map +1 -1
  45. package/dist/public-tools.js +1 -0
  46. package/dist/public-tools.js.map +1 -1
  47. package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
  48. package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
  49. package/dist/runtime/query/deferred-sandbox.js +75 -0
  50. package/dist/runtime/query/deferred-sandbox.js.map +1 -0
  51. package/dist/runtime/query/executor.d.ts +11 -0
  52. package/dist/runtime/query/executor.d.ts.map +1 -1
  53. package/dist/runtime/query/executor.js +25 -3
  54. package/dist/runtime/query/executor.js.map +1 -1
  55. package/dist/runtime/query/index.d.ts +2 -0
  56. package/dist/runtime/query/index.d.ts.map +1 -1
  57. package/dist/runtime/query/index.js +94 -25
  58. package/dist/runtime/query/index.js.map +1 -1
  59. package/dist/runtime/query/iteration/index.d.ts +26 -0
  60. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  61. package/dist/runtime/query/iteration/index.js +164 -21
  62. package/dist/runtime/query/iteration/index.js.map +1 -1
  63. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +10 -7
  64. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  65. package/dist/runtime/query/iteration/phases/compaction-breaker.js +10 -7
  66. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  67. package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
  68. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  69. package/dist/runtime/query/iteration/phases/compaction.js +119 -21
  70. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  71. package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
  72. package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
  73. package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
  74. package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
  75. package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
  76. package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
  77. package/dist/runtime/query/iteration/request-repair.js +593 -0
  78. package/dist/runtime/query/iteration/request-repair.js.map +1 -0
  79. package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
  80. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/stream-turn.js +2 -2
  82. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  83. package/dist/runtime/query/request-rich-content.d.ts +5 -1
  84. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  85. package/dist/runtime/query/request-rich-content.js +67 -25
  86. package/dist/runtime/query/request-rich-content.js.map +1 -1
  87. package/dist/runtime/query/tooling.d.ts +1 -0
  88. package/dist/runtime/query/tooling.d.ts.map +1 -1
  89. package/dist/runtime/query/tooling.js +1 -0
  90. package/dist/runtime/query/tooling.js.map +1 -1
  91. package/dist/tools/builtins/browser.d.ts +4 -4
  92. package/dist/tools/builtins/code-analysis.d.ts +49 -0
  93. package/dist/tools/builtins/code-analysis.d.ts.map +1 -0
  94. package/dist/tools/builtins/code-analysis.js +409 -0
  95. package/dist/tools/builtins/code-analysis.js.map +1 -0
  96. package/dist/tools/builtins/computer-use.d.ts +25 -25
  97. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  98. package/dist/tools/builtins/computer-use.js +12 -5
  99. package/dist/tools/builtins/computer-use.js.map +1 -1
  100. package/dist/tools/builtins/index.d.ts +1 -0
  101. package/dist/tools/builtins/index.d.ts.map +1 -1
  102. package/dist/tools/builtins/index.js +1 -0
  103. package/dist/tools/builtins/index.js.map +1 -1
  104. package/dist/tools/builtins/lsp.d.ts +1 -1
  105. package/dist/tools/builtins/skill.d.ts +2 -2
  106. package/dist/types/code-analysis/index.d.ts +299 -0
  107. package/dist/types/code-analysis/index.d.ts.map +1 -0
  108. package/dist/types/code-analysis/index.js +30 -0
  109. package/dist/types/code-analysis/index.js.map +1 -0
  110. package/dist/types/plugin/index.d.ts +6 -6
  111. package/dist/types/provider/errors.d.ts.map +1 -1
  112. package/dist/types/provider/errors.js +30 -5
  113. package/dist/types/provider/errors.js.map +1 -1
  114. package/dist/types/provider/interface.d.ts +12 -0
  115. package/dist/types/provider/interface.d.ts.map +1 -1
  116. package/dist/types/provider/stream.d.ts +1 -1
  117. package/dist/types/session/config.d.ts +14 -0
  118. package/dist/types/session/config.d.ts.map +1 -1
  119. package/dist/types/session/events.d.ts +20 -5
  120. package/dist/types/session/events.d.ts.map +1 -1
  121. package/dist/types/session/events.js.map +1 -1
  122. package/dist/types/session/records.d.ts +127 -127
  123. package/dist/types/tool/index.d.ts +9 -0
  124. package/dist/types/tool/index.d.ts.map +1 -1
  125. package/dist/types/tool/index.js.map +1 -1
  126. package/package.json +2 -2
  127. package/src/compaction/verifier.ts +57 -20
  128. package/src/provider/capabilities.ts +17 -0
  129. package/src/provider/errors.ts +13 -0
  130. package/src/provider/fallback.ts +10 -0
  131. package/src/provider/idle-timeout.ts +5 -0
  132. package/src/provider/image-input-refusal.ts +54 -0
  133. package/src/provider/index.ts +2 -0
  134. package/src/provider/retry.ts +42 -4
  135. package/src/public-runtime.ts +28 -0
  136. package/src/public-tools.ts +5 -0
  137. package/src/runtime/query/deferred-sandbox.ts +110 -0
  138. package/src/runtime/query/executor.ts +27 -3
  139. package/src/runtime/query/index.ts +127 -49
  140. package/src/runtime/query/iteration/index.ts +188 -23
  141. package/src/runtime/query/iteration/phases/compaction-breaker.ts +10 -8
  142. package/src/runtime/query/iteration/phases/compaction.ts +151 -35
  143. package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
  144. package/src/runtime/query/iteration/request-repair.ts +743 -0
  145. package/src/runtime/query/iteration/stream-turn.ts +11 -1
  146. package/src/runtime/query/request-rich-content.ts +77 -27
  147. package/src/runtime/query/tooling.ts +2 -0
  148. package/src/tools/builtins/code-analysis.ts +486 -0
  149. package/src/tools/builtins/computer-use.ts +13 -4
  150. package/src/tools/builtins/index.ts +5 -0
  151. package/src/types/code-analysis/index.ts +325 -0
  152. package/src/types/provider/errors.ts +33 -5
  153. package/src/types/provider/interface.ts +13 -0
  154. package/src/types/provider/stream.ts +1 -1
  155. package/src/types/session/config.ts +14 -0
  156. package/src/types/session/events.ts +25 -5
  157. package/src/types/tool/index.ts +9 -0
@@ -336,6 +336,7 @@ export const DEFAULT_TOOL_RETRY_BACKOFF: BackoffPolicy = {
336
336
 
337
337
  export interface ToolExecutorConfig {
338
338
  fileReadTracker?: FileReadTracker
339
+ codeAnalysis?: import('../../types/code-analysis/index.js').CodeAnalysisProvider
339
340
  tools: ToolManager
340
341
  /** Exact runtime output-tool owner allowed to retain post-screen, post-hook JSON before preview capping. */
341
342
  durableStructuredOutputTool?: ToolDefinition
@@ -653,6 +654,7 @@ export class ToolExecutor {
653
654
  /** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
654
655
  private stepAllowedTools?: readonly string[]
655
656
  private readonly fileReadTracker: FileReadTracker
657
+ private sandboxGate?: () => Promise<void>
656
658
  /** A ledger is rebuilt from history at most once; a second pass would re-append its chains. */
657
659
  private fileObservationsSeeded = false
658
660
 
@@ -686,6 +688,25 @@ export class ToolExecutor {
686
688
 
687
689
  setSandbox(sandbox: Sandbox): void {
688
690
  this.config = { ...this.config, sandbox }
691
+ this.sandboxGate = undefined
692
+ }
693
+
694
+ /**
695
+ * Hold tool admission until a sandbox that is still being acquired exists.
696
+ * The gate rejects when none will, and the turn settles that rejection. It
697
+ * is cleared by {@link setSandbox}, so a turn pays the check once.
698
+ */
699
+ setSandboxGate(gate: () => Promise<void>): void {
700
+ this.sandboxGate = gate
701
+ }
702
+
703
+ private async awaitSandbox(): Promise<void> {
704
+ await this.sandboxGate?.()
705
+ }
706
+
707
+ /** True once the turn has a sandbox, or is certain to get one before a tool runs. */
708
+ private get sandboxed(): boolean {
709
+ return this.config.sandbox !== undefined || this.sandboxGate !== undefined
689
710
  }
690
711
 
691
712
  /** Classify only the exact successful result executed by this executor. */
@@ -771,7 +792,7 @@ export class ToolExecutor {
771
792
  messages,
772
793
  this.fileReadTracker,
773
794
  this.config.workingDirectory,
774
- this.config.sandbox !== undefined,
795
+ this.sandboxed,
775
796
  )
776
797
  }
777
798
 
@@ -953,7 +974,7 @@ export class ToolExecutor {
953
974
  const tool = this.config.tools.get(toolName)
954
975
  return (
955
976
  tool?.commandDialect?.({
956
- sandboxed: this.config.sandbox !== undefined,
977
+ sandboxed: this.sandboxed,
957
978
  }) ?? 'sh'
958
979
  )
959
980
  }
@@ -979,6 +1000,7 @@ export class ToolExecutor {
979
1000
  hostModifiedCallIds?: ReadonlySet<string>,
980
1001
  ): Promise<PreparedToolBatch> {
981
1002
  assertUniqueToolCallIds(response.message.toolCalls ?? [])
1003
+ if ((response.message.toolCalls ?? []).length > 0) await this.awaitSandbox()
982
1004
  const calls = new Map<string, PreparedDirectCall>()
983
1005
  const escalations = new Map<string, ToolCallEscalation>()
984
1006
  for (const toolCall of response.message.toolCalls ?? []) {
@@ -1012,7 +1034,7 @@ export class ToolExecutor {
1012
1034
  const tool = this.config.tools.get(call.toolName)
1013
1035
  if (!tool || call.input === null || typeof call.input !== 'object') return undefined
1014
1036
  const input = call.input as Record<string, unknown>
1015
- const sandboxed = this.config.sandbox !== undefined
1037
+ const sandboxed = this.sandboxed
1016
1038
  let outsidePaths: string[] | undefined
1017
1039
  if (
1018
1040
  this.config.outsideRootAccess === 'review' &&
@@ -1199,6 +1221,7 @@ export class ToolExecutor {
1199
1221
  return { messages: [], results: [], observations: [] }
1200
1222
  }
1201
1223
  assertUniqueToolCallIds(toolCalls)
1224
+ if (toolCalls.length > 0) await this.awaitSandbox()
1202
1225
 
1203
1226
  // Sampled here, once, and held for every call below. See the note on
1204
1227
  // `permissionMode` in the config type.
@@ -1883,6 +1906,7 @@ export class ToolExecutor {
1883
1906
  this.dispatchNested(name, input, context, recordObservation, undefined, options),
1884
1907
  sandbox: this.config.sandbox,
1885
1908
  fileReadTracker: this.fileReadTracker,
1909
+ ...(this.config.codeAnalysis ? { codeAnalysis: this.config.codeAnalysis } : {}),
1886
1910
  // Bound to this turn, once. Binding here rather than passing the
1887
1911
  // owner from the tool is what makes the scoping structural: there
1888
1912
  // is no argument a tool could pass to reach another turn's jobs.
@@ -104,6 +104,13 @@ import { AwaitedJobs } from '../jobs/awaited-jobs.js'
104
104
  import type { BackgroundJobRegistry } from '../jobs/registry.js'
105
105
  import { catchUpFromCursor, settlePreStartCancellation } from './cancelled-before-start.js'
106
106
  import { CheckpointManager, findPendingCheckpoint } from './checkpoint.js'
107
+ import {
108
+ type DeferredSandbox,
109
+ type SandboxAcquireMode,
110
+ type SandboxAcquisitionStop,
111
+ SandboxAcquisitionStopped,
112
+ createDeferredSandbox,
113
+ } from './deferred-sandbox.js'
107
114
  import type { EventTranslator } from './events.js'
108
115
  import { finalizeTurn } from './finalize-turn.js'
109
116
  import { GuardCoordinator } from './guard.js'
@@ -146,6 +153,8 @@ import { ToolingBootstrap } from './tooling.js'
146
153
  export interface QueryParams {
147
154
  /** Share observations across turns of one conversation and filesystem; otherwise run-local. */
148
155
  fileReadTracker?: import('../../types/tool/index.js').FileReadTracker
156
+ /** The `code_analysis` tool's index; see `ToolContext.codeAnalysis`. Absent: the tool is not offered. */
157
+ codeAnalysis?: import('../../types/code-analysis/index.js').CodeAnalysisProvider
149
158
  /** One ledger shared with the task scheduler and descendant child sessions, keyed by (rootSessionId, rootTurnId). */
150
159
  budget?: SessionTokenBudget
151
160
  /**
@@ -1421,6 +1430,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1421
1430
  ...(params.skillRegistry ? { skills: params.skillRegistry } : {}),
1422
1431
  ...(params.web ? { web: params.web } : {}),
1423
1432
  ...(params.fileReadTracker ? { fileReadTracker: params.fileReadTracker } : {}),
1433
+ ...(params.codeAnalysis ? { codeAnalysis: params.codeAnalysis } : {}),
1424
1434
  ...(params.toolTimeoutMs !== undefined ? { toolTimeoutMs: params.toolTimeoutMs } : {}),
1425
1435
  ...(params.toolRetryBackoff !== undefined
1426
1436
  ? { toolRetryBackoff: params.toolRetryBackoff }
@@ -1825,6 +1835,10 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1825
1835
  })
1826
1836
 
1827
1837
  let sandbox: Sandbox | undefined
1838
+ /** Set only for `sandbox.acquire: 'concurrent' | 'on-first-use'`. */
1839
+ let deferredSandbox: DeferredSandbox | undefined
1840
+ /** How a stopped acquisition settles the turn; set where the provider is known. */
1841
+ let applyAcquisitionStop: (acquisition: SandboxAcquisitionStop) => void = () => {}
1828
1842
  // Decided during checkpoint restore, executed after the sandbox
1829
1843
  // exists — the approved tools may well need it.
1830
1844
  let pendingResume: PendingResumePlan | null = null
@@ -2300,6 +2314,20 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2300
2314
  yield* eventTranslator.drainPending()
2301
2315
  }
2302
2316
 
2317
+ // Checked whether or not a provider is present: a host that mistypes the
2318
+ // option should hear about it before it wires a provider in, not after.
2319
+ const acquireMode: SandboxAcquireMode = turnConfig.sandbox?.acquire ?? 'eager'
2320
+ if (
2321
+ acquireMode !== 'eager' &&
2322
+ acquireMode !== 'concurrent' &&
2323
+ acquireMode !== 'on-first-use'
2324
+ ) {
2325
+ throw new NamzuError({
2326
+ code: 'invalid_config',
2327
+ message: `sandbox.acquire must be 'eager', 'concurrent' or 'on-first-use'; received ${String(acquireMode)}`,
2328
+ details: { acquire: String(acquireMode) },
2329
+ })
2330
+ }
2303
2331
  // --- Sandbox lifecycle: create before iteration loop ---
2304
2332
  if (params.sandboxProvider) {
2305
2333
  const rootAtCwd = turnConfig.sandbox?.workspace === 'working-directory'
@@ -2331,40 +2359,60 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2331
2359
  },
2332
2360
  })
2333
2361
  }
2334
- const acquisition = await acquireSandbox({
2335
- provider: params.sandboxProvider,
2336
- config: {
2337
- ...(rootAtCwd ? { workingDirectory: ctx.cwd } : {}),
2338
- ...(rootAtCwd && params.additionalDirectories?.length
2339
- ? { additionalDirectories: params.additionalDirectories }
2340
- : {}),
2341
- timeoutMs: turnConfig.sandbox?.timeoutMs,
2342
- memoryLimitMb: turnConfig.sandbox?.memoryLimitMb,
2343
- maxProcesses: turnConfig.sandbox?.maxProcesses,
2344
- },
2345
- signal: ctx.abortController.signal,
2346
- timeoutMs: guard.remainingUntilTimeoutMs(),
2347
- teardownTimeoutMs: sandboxTeardownTimeoutMs,
2348
- onLateTeardown: (result) => {
2349
- if (result.kind === 'destroyed') {
2350
- ctx.log.info('Late sandbox allocation was destroyed after the turn stopped', {
2362
+ const sandboxProvider = params.sandboxProvider
2363
+ const startAcquisition = (signal: AbortSignal) =>
2364
+ acquireSandbox({
2365
+ provider: sandboxProvider,
2366
+ config: {
2367
+ ...(rootAtCwd ? { workingDirectory: ctx.cwd } : {}),
2368
+ ...(rootAtCwd && params.additionalDirectories?.length
2369
+ ? { additionalDirectories: params.additionalDirectories }
2370
+ : {}),
2371
+ timeoutMs: turnConfig.sandbox?.timeoutMs,
2372
+ memoryLimitMb: turnConfig.sandbox?.memoryLimitMb,
2373
+ maxProcesses: turnConfig.sandbox?.maxProcesses,
2374
+ },
2375
+ signal,
2376
+ timeoutMs: guard.remainingUntilTimeoutMs(),
2377
+ teardownTimeoutMs: sandboxTeardownTimeoutMs,
2378
+ onLateTeardown: (result) => {
2379
+ if (result.kind === 'destroyed') {
2380
+ ctx.log.info('Late sandbox allocation was destroyed after the turn stopped', {
2381
+ [NAMZU.TURN_ID]: ctx.turnId,
2382
+ })
2383
+ return
2384
+ }
2385
+ ctx.log.error('Late sandbox allocation did not stop cleanly', {
2351
2386
  [NAMZU.TURN_ID]: ctx.turnId,
2387
+ ...errorAttributes(result.error),
2352
2388
  })
2353
- return
2354
- }
2355
- ctx.log.error('Late sandbox allocation did not stop cleanly', {
2356
- [NAMZU.TURN_ID]: ctx.turnId,
2357
- ...errorAttributes(result.error),
2358
- })
2359
- },
2360
- })
2361
- if (acquisition.kind !== 'created') {
2389
+ },
2390
+ })
2391
+ const publishSandbox = async (acquired: Sandbox): Promise<void> => {
2392
+ sandbox = acquired
2393
+ toolExecutor.setSandbox(acquired)
2394
+
2395
+ await eventTranslator.emitEvent({
2396
+ type: 'sandbox_created',
2397
+ turnId: ctx.turnId,
2398
+ sandboxId: acquired.id,
2399
+ environment: acquired.environment,
2400
+ })
2401
+ }
2402
+ const logSandboxCreated = (acquired: Sandbox): void => {
2403
+ ctx.log.info('Sandbox created for run', {
2404
+ 'namzu.sandbox.id': acquired.id,
2405
+ 'namzu.execution.environment': acquired.environment,
2406
+ 'namzu.runtime.root_dir': acquired.rootDir,
2407
+ })
2408
+ }
2409
+ applyAcquisitionStop = (acquisition) => {
2362
2410
  if (acquisition.createPending) {
2363
2411
  ctx.log.warn(
2364
2412
  'Sandbox creation was unsettled when the turn stopped; any returned handle will be released, but a remote allocation hidden behind a lost response requires provider-side reconciliation or a fleet reaper',
2365
2413
  {
2366
2414
  [NAMZU.TURN_ID]: ctx.turnId,
2367
- 'namzu.sandbox.provider_id': params.sandboxProvider.id,
2415
+ 'namzu.sandbox.provider_id': sandboxProvider.id,
2368
2416
  },
2369
2417
  )
2370
2418
  }
@@ -2374,32 +2422,40 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2374
2422
  ctx.recorder.setStopReason('timeout')
2375
2423
  ctx.log.warn('Sandbox creation exhausted the turn timeout', {
2376
2424
  [NAMZU.TURN_ID]: ctx.turnId,
2377
- 'namzu.sandbox.provider_id': params.sandboxProvider.id,
2425
+ 'namzu.sandbox.provider_id': sandboxProvider.id,
2378
2426
  ...errorAttributes(acquisition.error),
2379
2427
  })
2380
2428
  }
2381
- yield* resultAssembler.completeTurn(rootSpan)
2382
- // The turn HAS settled, so the outer `finally` must not read
2383
- // this as an abandonment — it would persist a second time.
2384
- settled = true
2385
- return await resultAssembler.finalize()
2386
2429
  }
2387
- sandbox = acquisition.sandbox
2388
- toolExecutor.setSandbox(sandbox)
2389
-
2390
- await eventTranslator.emitEvent({
2391
- type: 'sandbox_created',
2392
- turnId: ctx.turnId,
2393
- sandboxId: sandbox.id,
2394
- environment: sandbox.environment,
2395
- })
2396
- yield* eventTranslator.drainPending()
2397
-
2398
- ctx.log.info('Sandbox created for run', {
2399
- 'namzu.sandbox.id': sandbox.id,
2400
- 'namzu.execution.environment': sandbox.environment,
2401
- 'namzu.runtime.root_dir': sandbox.rootDir,
2402
- })
2430
+ if (acquireMode === 'eager') {
2431
+ const acquisition = await startAcquisition(ctx.abortController.signal)
2432
+ if (acquisition.kind !== 'created') {
2433
+ applyAcquisitionStop(acquisition)
2434
+ yield* resultAssembler.completeTurn(rootSpan)
2435
+ // The turn HAS settled, so the outer `finally` must not read
2436
+ // this as an abandonment — it would persist a second time.
2437
+ settled = true
2438
+ return await resultAssembler.finalize()
2439
+ }
2440
+ await publishSandbox(acquisition.sandbox)
2441
+ yield* eventTranslator.drainPending()
2442
+ logSandboxCreated(acquisition.sandbox)
2443
+ } else {
2444
+ // The handle is published, and `sandbox_created` emitted, when
2445
+ // the provider answers. Until then the executor holds tool
2446
+ // admission on the gate, so nothing runs without it.
2447
+ const deferred = createDeferredSandbox({
2448
+ turnSignal: ctx.abortController.signal,
2449
+ acquire: startAcquisition,
2450
+ publish: async (acquired) => {
2451
+ await publishSandbox(acquired)
2452
+ logSandboxCreated(acquired)
2453
+ },
2454
+ })
2455
+ deferredSandbox = deferred
2456
+ toolExecutor.setSandboxGate(() => deferred.ensure())
2457
+ if (acquireMode === 'concurrent') deferred.start()
2458
+ }
2403
2459
  }
2404
2460
 
2405
2461
  // Before the first model call: the cheapest place to refuse, since
@@ -2525,6 +2581,14 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2525
2581
 
2526
2582
  yield* iterationOrchestrator.runLoop()
2527
2583
 
2584
+ // A turn that never needed the sandbox still settles it: a failure or
2585
+ // a timeout ends the turn the way an eager acquisition would have, and
2586
+ // a handle that arrives after that is released by the lifecycle.
2587
+ if (deferredSandbox) {
2588
+ await deferredSandbox.settle()
2589
+ yield* eventTranslator.drainPending()
2590
+ }
2591
+
2528
2592
  yield* finalizeTurn({
2529
2593
  ctx,
2530
2594
  params,
@@ -2536,6 +2600,16 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2536
2600
  rootSpan,
2537
2601
  })
2538
2602
  } catch (err) {
2603
+ // A deferred acquisition stopped (cancelled, or out of time) and the
2604
+ // turn found out at its first tool call or at its end. Settled the
2605
+ // way the eager path settles it, with nothing else to report.
2606
+ if (err instanceof SandboxAcquisitionStopped) {
2607
+ applyAcquisitionStop(err.acquisition)
2608
+ yield* eventTranslator.drainPending()
2609
+ yield* resultAssembler.completeTurn(rootSpan)
2610
+ settled = true
2611
+ return await resultAssembler.finalize()
2612
+ }
2539
2613
  // Another turn is active in this session: nothing of this turn was
2540
2614
  // written, so there is nothing to settle. The caller gets the
2541
2615
  // refusal itself.
@@ -2546,6 +2620,10 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2546
2620
  yield* eventTranslator.drainPending()
2547
2621
  yield* resultAssembler.handleError(err, rootSpan)
2548
2622
  } finally {
2623
+ // A pending acquisition is withdrawn and awaited first, so a handle
2624
+ // that arrives now is either published (and released below) or
2625
+ // handed to the late-teardown path. Nothing is left allocated.
2626
+ await deferredSandbox?.cancel()
2549
2627
  yield* releaseTurnResources({
2550
2628
  ctx,
2551
2629
  eventTranslator,