@zergai/ztc 0.4.0-beta.1 → 0.5.0-beta.1

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 (718) hide show
  1. package/README.md +398 -45
  2. package/RUST_THIRD_PARTY_LICENSES.txt +1080 -822
  3. package/THIRD_PARTY_NOTICES.md +273 -12
  4. package/bin/zmux-darwin-arm64 +0 -0
  5. package/bin/ztc-audio-darwin-arm64 +0 -0
  6. package/bin/ztc-capture-darwin-arm64 +0 -0
  7. package/bin/ztc-computer-darwin-arm64 +0 -0
  8. package/bin/ztc-llm-darwin-arm64 +0 -0
  9. package/bin/ztc-tts-darwin-arm64 +0 -0
  10. package/bin/ztc-tui-darwin-arm64 +0 -0
  11. package/bin/ztc-zlicky-overlay-darwin-arm64 +0 -0
  12. package/bin/zterm-darwin-arm64 +0 -0
  13. package/dist/App.js +1206 -209
  14. package/dist/agent/agent.d.ts +74 -8
  15. package/dist/agent/agent.js +1073 -184
  16. package/dist/agent/backends/account_rotation.d.ts +4 -0
  17. package/dist/agent/backends/account_rotation.js +8 -2
  18. package/dist/agent/backends/anthropic.d.ts +13 -0
  19. package/dist/agent/backends/anthropic.js +169 -28
  20. package/dist/agent/backends/claude_cli.js +18 -8
  21. package/dist/agent/backends/codex_app_server.d.ts +1 -0
  22. package/dist/agent/backends/codex_app_server.js +55 -3
  23. package/dist/agent/backends/codex_auth.d.ts +1 -0
  24. package/dist/agent/backends/codex_auth.js +67 -13
  25. package/dist/agent/backends/codex_buffered_recovery.d.ts +27 -0
  26. package/dist/agent/backends/codex_buffered_recovery.js +293 -0
  27. package/dist/agent/backends/gemini.js +3 -1
  28. package/dist/agent/backends/http_error.d.ts +26 -0
  29. package/dist/agent/backends/http_error.js +221 -0
  30. package/dist/agent/backends/index.d.ts +9 -2
  31. package/dist/agent/backends/index.js +22 -2
  32. package/dist/agent/backends/openai.d.ts +0 -6
  33. package/dist/agent/backends/openai.js +5 -2
  34. package/dist/agent/backends/openai_compatible.d.ts +5 -4
  35. package/dist/agent/backends/openai_compatible.js +26 -19
  36. package/dist/agent/backends/openai_responses.d.ts +8 -3
  37. package/dist/agent/backends/openai_responses.js +77 -33
  38. package/dist/agent/backends/provider_ids.d.ts +1 -0
  39. package/dist/agent/backends/provider_ids.js +4 -0
  40. package/dist/agent/backends/request_metrics.js +12 -2
  41. package/dist/agent/backends/types.d.ts +109 -6
  42. package/dist/agent/backends/types.js +5 -1
  43. package/dist/agent/backends/zrouter_stream.d.ts +6 -1
  44. package/dist/agent/backends/zrouter_stream.js +160 -42
  45. package/dist/agent/background_registry.d.ts +2 -0
  46. package/dist/agent/background_registry.js +1 -0
  47. package/dist/agent/claim_observation.d.ts +6 -0
  48. package/dist/agent/claim_observation.js +54 -0
  49. package/dist/agent/commands/accounts.js +46 -177
  50. package/dist/agent/commands/attention.d.ts +2 -0
  51. package/dist/agent/commands/attention.js +67 -0
  52. package/dist/agent/commands/auth.js +18 -2
  53. package/dist/agent/commands/board.d.ts +2 -0
  54. package/dist/agent/commands/board.js +169 -0
  55. package/dist/agent/commands/clipboard.js +50 -1
  56. package/dist/agent/commands/compact.js +21 -13
  57. package/dist/agent/commands/config.js +4 -1
  58. package/dist/agent/commands/context.js +6 -1
  59. package/dist/agent/commands/desktop.d.ts +2 -0
  60. package/dist/agent/commands/desktop.js +29 -0
  61. package/dist/agent/commands/effort.js +14 -6
  62. package/dist/agent/commands/environment.d.ts +3 -0
  63. package/dist/agent/commands/environment.js +109 -0
  64. package/dist/agent/commands/evidence.d.ts +2 -0
  65. package/dist/agent/commands/evidence.js +9 -0
  66. package/dist/agent/commands/fork.js +4 -3
  67. package/dist/agent/commands/goal.d.ts +33 -1
  68. package/dist/agent/commands/goal.js +174 -14
  69. package/dist/agent/commands/goal_exit.d.ts +36 -0
  70. package/dist/agent/commands/goal_exit.js +120 -0
  71. package/dist/agent/commands/help.js +3 -1
  72. package/dist/agent/commands/history.js +3 -1
  73. package/dist/agent/commands/inbox.d.ts +2 -0
  74. package/dist/agent/commands/inbox.js +148 -0
  75. package/dist/agent/commands/index.js +16 -1
  76. package/dist/agent/commands/input_mode.js +8 -6
  77. package/dist/agent/commands/job.d.ts +2 -0
  78. package/dist/agent/commands/job.js +93 -0
  79. package/dist/agent/commands/keybindings.js +4 -1
  80. package/dist/agent/commands/label.d.ts +2 -0
  81. package/dist/agent/commands/label.js +77 -0
  82. package/dist/agent/commands/layout.d.ts +2 -0
  83. package/dist/agent/commands/layout.js +20 -0
  84. package/dist/agent/commands/mirror.js +16 -1
  85. package/dist/agent/commands/model.js +12 -2
  86. package/dist/agent/commands/permissions.d.ts +3 -1
  87. package/dist/agent/commands/permissions.js +79 -89
  88. package/dist/agent/commands/plan.js +5 -15
  89. package/dist/agent/commands/profile.js +21 -2
  90. package/dist/agent/commands/provider_selection.js +20 -1
  91. package/dist/agent/commands/rename.js +1 -1
  92. package/dist/agent/commands/resources.d.ts +2 -0
  93. package/dist/agent/commands/resources.js +67 -0
  94. package/dist/agent/commands/rewind.d.ts +3 -0
  95. package/dist/agent/commands/rewind.js +16 -0
  96. package/dist/agent/commands/runbooks.d.ts +2 -0
  97. package/dist/agent/commands/runbooks.js +73 -0
  98. package/dist/agent/commands/runbooks_lazy.d.ts +2 -0
  99. package/dist/agent/commands/runbooks_lazy.js +8 -0
  100. package/dist/agent/commands/sessions.d.ts +1 -1
  101. package/dist/agent/commands/sessions.js +33 -5
  102. package/dist/agent/commands/settings.js +185 -39
  103. package/dist/agent/commands/setup.d.ts +2 -0
  104. package/dist/agent/commands/setup.js +78 -0
  105. package/dist/agent/commands/share.d.ts +2 -0
  106. package/dist/agent/commands/share.js +64 -0
  107. package/dist/agent/commands/status.js +14 -2
  108. package/dist/agent/commands/types.d.ts +99 -6
  109. package/dist/agent/commands/update.js +8 -0
  110. package/dist/agent/commands/usage.d.ts +2 -0
  111. package/dist/agent/commands/usage.js +8 -0
  112. package/dist/agent/factory.d.ts +11 -0
  113. package/dist/agent/factory.js +113 -88
  114. package/dist/agent/loop_policy.d.ts +6 -3
  115. package/dist/agent/loop_policy.js +12 -3
  116. package/dist/agent/measurement_guidance.d.ts +2 -0
  117. package/dist/agent/measurement_guidance.js +5 -0
  118. package/dist/agent/request_encoder.d.ts +8 -1
  119. package/dist/agent/request_encoder.js +95 -23
  120. package/dist/agent/runtime/benchmark_comparison.d.ts +30 -0
  121. package/dist/agent/runtime/benchmark_comparison.js +94 -0
  122. package/dist/agent/runtime/benchmark_contract.d.ts +48 -0
  123. package/dist/agent/runtime/benchmark_contract.js +90 -0
  124. package/dist/agent/runtime/checked_commands.d.ts +32 -0
  125. package/dist/agent/runtime/checked_commands.js +119 -0
  126. package/dist/agent/runtime/command_execution_policy.d.ts +26 -0
  127. package/dist/agent/runtime/command_execution_policy.js +91 -0
  128. package/dist/agent/runtime/command_jobs.d.ts +36 -18
  129. package/dist/agent/runtime/command_jobs.js +151 -13
  130. package/dist/agent/runtime/command_panes.d.ts +59 -0
  131. package/dist/agent/runtime/command_panes.js +398 -0
  132. package/dist/agent/runtime/command_wait.d.ts +19 -0
  133. package/dist/agent/runtime/command_wait.js +167 -0
  134. package/dist/agent/runtime/cpu_benchmark.d.ts +30 -0
  135. package/dist/agent/runtime/cpu_benchmark.js +126 -0
  136. package/dist/agent/runtime/delegated_evidence.d.ts +26 -0
  137. package/dist/agent/runtime/delegated_evidence.js +80 -0
  138. package/dist/agent/runtime/evidence.d.ts +127 -0
  139. package/dist/agent/runtime/evidence.js +468 -0
  140. package/dist/agent/runtime/execution_context.d.ts +7 -0
  141. package/dist/agent/runtime/execution_context.js +39 -3
  142. package/dist/agent/runtime/json_verifier_recipe.d.ts +8 -0
  143. package/dist/agent/runtime/json_verifier_recipe.js +66 -0
  144. package/dist/agent/runtime/measurements.d.ts +110 -0
  145. package/dist/agent/runtime/measurements.js +317 -0
  146. package/dist/agent/runtime/policy.d.ts +4 -0
  147. package/dist/agent/runtime/policy.js +31 -20
  148. package/dist/agent/runtime/provider_native_evidence.d.ts +20 -0
  149. package/dist/agent/runtime/provider_native_evidence.js +86 -0
  150. package/dist/agent/runtime/recovery.d.ts +2 -0
  151. package/dist/agent/runtime/recovery.js +45 -2
  152. package/dist/agent/runtime/remote_broker_transport.d.ts +10 -0
  153. package/dist/agent/runtime/remote_broker_transport.js +84 -0
  154. package/dist/agent/runtime/remote_deletion_receipts.d.ts +3 -0
  155. package/dist/agent/runtime/remote_deletion_receipts.js +43 -0
  156. package/dist/agent/runtime/remote_journal_replay.d.ts +13 -0
  157. package/dist/agent/runtime/remote_journal_replay.js +233 -0
  158. package/dist/agent/runtime/remote_mutation_journal.d.ts +25 -0
  159. package/dist/agent/runtime/remote_mutation_journal.js +208 -0
  160. package/dist/agent/runtime/remote_resource_endpoint.d.ts +7 -0
  161. package/dist/agent/runtime/remote_resource_endpoint.js +6 -0
  162. package/dist/agent/runtime/remote_resource_errors.d.ts +5 -0
  163. package/dist/agent/runtime/remote_resource_errors.js +9 -0
  164. package/dist/agent/runtime/remote_resource_script.d.ts +2 -0
  165. package/dist/agent/runtime/remote_resource_script.js +1009 -0
  166. package/dist/agent/runtime/remote_resources.d.ts +29 -0
  167. package/dist/agent/runtime/remote_resources.js +160 -0
  168. package/dist/agent/runtime/source_verification.d.ts +12 -0
  169. package/dist/agent/runtime/source_verification.js +210 -0
  170. package/dist/agent/runtime/source_verification_contract.d.ts +75 -0
  171. package/dist/agent/runtime/source_verification_contract.js +203 -0
  172. package/dist/agent/runtime/tool_checkpoints.d.ts +11 -0
  173. package/dist/agent/runtime/tool_checkpoints.js +26 -0
  174. package/dist/agent/runtime/tool_effects.d.ts +5 -0
  175. package/dist/agent/runtime/tool_progress.d.ts +12 -0
  176. package/dist/agent/runtime/tool_progress.js +76 -0
  177. package/dist/agent/runtime/tool_runtime.d.ts +16 -1
  178. package/dist/agent/runtime/tool_runtime.js +204 -10
  179. package/dist/agent/runtime/tracing.d.ts +1 -1
  180. package/dist/agent/runtime/verifier_capture.d.ts +9 -0
  181. package/dist/agent/runtime/verifier_capture.js +100 -0
  182. package/dist/agent/runtime/verifier_contract.d.ts +24 -0
  183. package/dist/agent/runtime/verifier_contract.js +41 -0
  184. package/dist/agent/subagent.d.ts +20 -10
  185. package/dist/agent/subagent.js +266 -29
  186. package/dist/agent/tool_response_directory.d.ts +24 -0
  187. package/dist/agent/tool_response_directory.js +61 -0
  188. package/dist/agent/tool_response_source.d.ts +8 -0
  189. package/dist/agent/tool_response_source.js +49 -0
  190. package/dist/agent/tool_response_store.d.ts +75 -4
  191. package/dist/agent/tool_response_store.js +583 -64
  192. package/dist/agent/tools/ask_user.d.ts +3 -1
  193. package/dist/agent/tools/ask_user.js +30 -11
  194. package/dist/agent/tools/background.js +1 -0
  195. package/dist/agent/tools/benchmark_cpu.d.ts +2 -0
  196. package/dist/agent/tools/benchmark_cpu.js +102 -0
  197. package/dist/agent/tools/compare_benchmarks.d.ts +2 -0
  198. package/dist/agent/tools/compare_benchmarks.js +53 -0
  199. package/dist/agent/tools/deferred.js +1 -0
  200. package/dist/agent/tools/delegate.js +18 -2
  201. package/dist/agent/tools/helper_requirements.d.ts +7 -0
  202. package/dist/agent/tools/helper_requirements.js +19 -0
  203. package/dist/agent/tools/index.d.ts +1 -0
  204. package/dist/agent/tools/index.js +3 -0
  205. package/dist/agent/tools/measurement_report.d.ts +22 -0
  206. package/dist/agent/tools/measurement_report.js +190 -0
  207. package/dist/agent/tools/pane.d.ts +0 -7
  208. package/dist/agent/tools/pane.js +5 -4
  209. package/dist/agent/tools/project_context.d.ts +3 -0
  210. package/dist/agent/tools/project_context.js +45 -0
  211. package/dist/agent/tools/recall_tool_response.d.ts +3 -5
  212. package/dist/agent/tools/recall_tool_response.js +19 -29
  213. package/dist/agent/tools/registry.d.ts +1 -0
  214. package/dist/agent/tools/registry.js +23 -0
  215. package/dist/agent/tools/related_sessions.d.ts +2 -0
  216. package/dist/agent/tools/related_sessions.js +156 -0
  217. package/dist/agent/tools/routine.js +13 -3
  218. package/dist/agent/tools/session_evidence.d.ts +3 -0
  219. package/dist/agent/tools/session_evidence.js +72 -0
  220. package/dist/agent/tools/shell.d.ts +4 -3
  221. package/dist/agent/tools/shell.js +174 -47
  222. package/dist/agent/tools/tool_search.js +3 -2
  223. package/dist/agent/tools/types.d.ts +28 -0
  224. package/dist/agent/tools/verify_artifact.d.ts +2 -0
  225. package/dist/agent/tools/verify_artifact.js +60 -0
  226. package/dist/agent/tools/workflow.js +1 -0
  227. package/dist/app/activity.d.ts +6 -1
  228. package/dist/app/activity.js +81 -12
  229. package/dist/app/agent_runner.d.ts +6 -1
  230. package/dist/app/agent_runner.js +34 -19
  231. package/dist/app/command_context.js +106 -10
  232. package/dist/app/controller.d.ts +15 -4
  233. package/dist/app/controller.js +29 -3
  234. package/dist/app/escape_stop.d.ts +11 -0
  235. package/dist/app/escape_stop.js +15 -0
  236. package/dist/app/liveness.d.ts +6 -4
  237. package/dist/app/liveness.js +13 -3
  238. package/dist/app/model_catalog_cache.d.ts +5 -0
  239. package/dist/app/model_catalog_cache.js +138 -0
  240. package/dist/app/ordered_transcript.js +43 -4
  241. package/dist/app/prompt_turn.d.ts +14 -0
  242. package/dist/app/prompt_turn.js +53 -0
  243. package/dist/app/recap.d.ts +3 -0
  244. package/dist/app/recap.js +9 -3
  245. package/dist/app/restart.d.ts +3 -2
  246. package/dist/app/restart.js +22 -7
  247. package/dist/app/rewind.d.ts +3 -3
  248. package/dist/app/rewind.js +3 -3
  249. package/dist/app/selection_completion.d.ts +17 -2
  250. package/dist/app/selection_completion.js +84 -9
  251. package/dist/app/session_manager.d.ts +38 -6
  252. package/dist/app/session_manager.js +263 -60
  253. package/dist/app/session_messages.d.ts +3 -0
  254. package/dist/app/session_messages.js +60 -0
  255. package/dist/app/task_scope_router.d.ts +20 -0
  256. package/dist/app/task_scope_router.js +173 -0
  257. package/dist/app/transcript_window.d.ts +2 -0
  258. package/dist/app/transcript_window.js +4 -0
  259. package/dist/app/workspace_admission_details.d.ts +3 -0
  260. package/dist/app/workspace_admission_details.js +26 -0
  261. package/dist/app/workspace_layout_controller.d.ts +11 -0
  262. package/dist/app/workspace_layout_controller.js +88 -0
  263. package/dist/app/workspace_monitor.d.ts +1 -0
  264. package/dist/app/workspace_monitor.js +216 -0
  265. package/dist/app/workspace_presets.d.ts +29 -0
  266. package/dist/app/workspace_presets.js +192 -0
  267. package/dist/attention/queue.d.ts +37 -0
  268. package/dist/attention/queue.js +183 -0
  269. package/dist/bench/rf_shared.d.ts +2 -0
  270. package/dist/board/binding.d.ts +42 -0
  271. package/dist/board/binding.js +38 -0
  272. package/dist/board/client.d.ts +42 -0
  273. package/dist/board/client.js +60 -0
  274. package/dist/cli.js +1 -1
  275. package/dist/cli_args.js +19 -17
  276. package/dist/cli_main.js +110 -13
  277. package/dist/cli_ratatui.d.ts +183 -16
  278. package/dist/cli_ratatui.js +3989 -1255
  279. package/dist/components/ActivityLine.d.ts +3 -2
  280. package/dist/components/ActivityLine.js +2 -2
  281. package/dist/components/InputArea.d.ts +19 -1
  282. package/dist/components/InputArea.js +264 -190
  283. package/dist/components/MessageList.d.ts +2 -0
  284. package/dist/components/MessageList.js +3 -1
  285. package/dist/components/SingleMessage.d.ts +2 -0
  286. package/dist/components/SingleMessage.js +4 -2
  287. package/dist/components/StatusBar.d.ts +4 -2
  288. package/dist/components/StatusBar.js +3 -1
  289. package/dist/conductor_branch.js +3 -0
  290. package/dist/config/api_retry.d.ts +6 -1
  291. package/dist/config/api_retry.js +19 -1
  292. package/dist/config/defaults.d.ts +2 -1
  293. package/dist/config/defaults.js +9 -2
  294. package/dist/config/message_display.d.ts +1 -0
  295. package/dist/config/message_display.js +17 -2
  296. package/dist/config/permissions.d.ts +21 -0
  297. package/dist/config/permissions.js +89 -0
  298. package/dist/config/resolved.d.ts +2 -2
  299. package/dist/config/types.d.ts +32 -5
  300. package/dist/config.d.ts +38 -4
  301. package/dist/config.js +290 -42
  302. package/dist/context/project_intelligence.d.ts +44 -0
  303. package/dist/context/project_intelligence.js +246 -0
  304. package/dist/desktop/cli_launch.d.ts +8 -0
  305. package/dist/desktop/cli_launch.js +6 -0
  306. package/dist/desktop/cli_shim.d.ts +8 -0
  307. package/dist/desktop/cli_shim.js +120 -0
  308. package/dist/desktop/runtime.d.ts +16 -0
  309. package/dist/desktop/runtime.js +161 -0
  310. package/dist/desktop/worker_launch.d.ts +7 -0
  311. package/dist/desktop/worker_launch.js +7 -0
  312. package/dist/diagnostics/cargo_mutants_selection.d.ts +27 -0
  313. package/dist/diagnostics/cargo_mutants_selection.js +48 -0
  314. package/dist/diagnostics/native_terminal_benchmark.d.ts +58 -0
  315. package/dist/diagnostics/native_terminal_benchmark.js +1247 -0
  316. package/dist/diagnostics/session_evidence_package.d.ts +40 -0
  317. package/dist/diagnostics/session_evidence_package.js +234 -0
  318. package/dist/diagnostics/session_export.d.ts +93 -0
  319. package/dist/diagnostics/session_export.js +634 -0
  320. package/dist/emulation/tool_adapters.js +48 -18
  321. package/dist/emulation/ztc_addendum.d.ts +1 -0
  322. package/dist/emulation/ztc_addendum.js +13 -12
  323. package/dist/environment/service_runner.d.ts +1 -0
  324. package/dist/environment/service_runner.js +68 -0
  325. package/dist/environment/store.d.ts +120 -0
  326. package/dist/environment/store.js +387 -0
  327. package/dist/events.d.ts +40 -1
  328. package/dist/events.js +10 -17
  329. package/dist/goal_contract.d.ts +25 -0
  330. package/dist/goal_contract.js +44 -0
  331. package/dist/headless.d.ts +31 -0
  332. package/dist/headless.js +578 -70
  333. package/dist/headless_cli.js +8 -0
  334. package/dist/headless_cli_args.d.ts +1 -0
  335. package/dist/headless_cli_args.js +1 -0
  336. package/dist/intake/cli.d.ts +1 -0
  337. package/dist/intake/cli.js +33 -0
  338. package/dist/intake/format.d.ts +2 -0
  339. package/dist/intake/format.js +14 -0
  340. package/dist/intake/notifications.d.ts +16 -0
  341. package/dist/intake/notifications.js +132 -0
  342. package/dist/intake/store.d.ts +56 -0
  343. package/dist/intake/store.js +238 -0
  344. package/dist/library.d.ts +8 -0
  345. package/dist/library.js +19 -10
  346. package/dist/mcp/client.d.ts +1 -0
  347. package/dist/mcp/client.js +20 -0
  348. package/dist/modes/builtin/plan.yaml +2 -2
  349. package/dist/modes/handlers.js +1 -1
  350. package/dist/onboarding/first_run.d.ts +3 -0
  351. package/dist/onboarding/first_run.js +33 -25
  352. package/dist/onboarding/setup.d.ts +32 -0
  353. package/dist/onboarding/setup.js +133 -0
  354. package/dist/onboarding/setup_agent_health.d.ts +17 -0
  355. package/dist/onboarding/setup_agent_health.js +79 -0
  356. package/dist/onboarding/setup_environment.d.ts +66 -0
  357. package/dist/onboarding/setup_environment.js +219 -0
  358. package/dist/onboarding/setup_workspace.d.ts +17 -0
  359. package/dist/onboarding/setup_workspace.js +129 -0
  360. package/dist/optimize/adapters/index.d.ts +4 -1
  361. package/dist/optimize/adapters/index.js +7 -0
  362. package/dist/optimize/adapters/kernelbench.js +14 -5
  363. package/dist/optimize/adapters/popcorn.js +3 -2
  364. package/dist/optimize/adapters/profile_benchmark.js +13 -7
  365. package/dist/optimize/adapters/rfbench.js +25 -14
  366. package/dist/optimize/adapters/types.d.ts +3 -0
  367. package/dist/optimize/adapters/zergpod.js +5 -2
  368. package/dist/optimize/baseline.js +10 -21
  369. package/dist/optimize/branch.js +9 -12
  370. package/dist/optimize/combine.js +2 -3
  371. package/dist/optimize/executor.js +34 -48
  372. package/dist/optimize/measurements.d.ts +55 -0
  373. package/dist/optimize/measurements.js +173 -0
  374. package/dist/optimize/results.d.ts +1 -0
  375. package/dist/optimize/results.js +11 -33
  376. package/dist/optimize/tge.js +2 -1
  377. package/dist/panes/initial_input.d.ts +19 -0
  378. package/dist/panes/initial_input.js +206 -0
  379. package/dist/panes/labels.d.ts +16 -0
  380. package/dist/panes/labels.js +42 -0
  381. package/dist/panes/model_catalog.d.ts +3 -3
  382. package/dist/panes/model_catalog.js +28 -4
  383. package/dist/panes/pane_registry.js +6 -2
  384. package/dist/panes/types.d.ts +3 -0
  385. package/dist/panes/ztc_launch.js +19 -0
  386. package/dist/plugin/optimization.d.ts +2 -0
  387. package/dist/plugin/optimization.js +1 -0
  388. package/dist/plugin/runtime.d.ts +2 -0
  389. package/dist/plugin/simulation.d.ts +6 -1
  390. package/dist/plugin/simulation_contract.d.ts +836 -1
  391. package/dist/plugin/simulation_documents.d.ts +22 -0
  392. package/dist/plugin/simulation_documents.js +1 -0
  393. package/dist/plugin/simulation_interventions.d.ts +103 -0
  394. package/dist/plugin/simulation_interventions.js +1 -0
  395. package/dist/plugin/simulation_living.d.ts +1 -0
  396. package/dist/plugin/simulation_living.js +3 -0
  397. package/dist/plugin/simulation_meetings.d.ts +121 -0
  398. package/dist/plugin/simulation_meetings.js +1 -0
  399. package/dist/plugin/simulation_story_review.d.ts +111 -0
  400. package/dist/plugin/simulation_story_review.js +1 -0
  401. package/dist/plugin/simulation_storyteller.d.ts +91 -0
  402. package/dist/plugin/simulation_storyteller.js +1 -0
  403. package/dist/plugin/simulation_structured.d.ts +2 -0
  404. package/dist/plugin/simulation_structured.js +2 -0
  405. package/dist/plugin/simulation_toolkit.d.ts +21 -0
  406. package/dist/plugin/simulation_toolkit.js +24 -0
  407. package/dist/prompts/tool_delegate_task.j2 +1 -1
  408. package/dist/remote-broker/Dockerfile +10 -0
  409. package/dist/remote-broker/archive.py +1227 -0
  410. package/dist/remote-broker/cutover.py +761 -0
  411. package/dist/remote-broker/requirements.txt +5 -0
  412. package/dist/remote-broker/resources.json +6 -0
  413. package/dist/remote-broker/resources.write-staging.example.json +18 -0
  414. package/dist/remote-broker/retained.py +686 -0
  415. package/dist/remote-broker/retained_cleanup.py +550 -0
  416. package/dist/remote-broker/server.py +648 -0
  417. package/dist/remote-broker/worker.py +1007 -0
  418. package/dist/router_auth.d.ts +31 -0
  419. package/dist/router_auth.js +155 -9
  420. package/dist/runbooks/catalog.d.ts +3 -0
  421. package/dist/runbooks/catalog.js +26 -0
  422. package/dist/runbooks/library.d.ts +11 -0
  423. package/dist/runbooks/library.js +95 -0
  424. package/dist/runbooks/package.d.ts +43 -0
  425. package/dist/runbooks/package.js +102 -0
  426. package/dist/session.d.ts +65 -13
  427. package/dist/session.js +548 -43
  428. package/dist/session_handoff.d.ts +25 -0
  429. package/dist/session_handoff.js +322 -0
  430. package/dist/session_handoff_contract.d.ts +89 -0
  431. package/dist/session_handoff_contract.js +179 -0
  432. package/dist/session_media.d.ts +41 -0
  433. package/dist/session_media.js +357 -0
  434. package/dist/session_ownership.d.ts +16 -0
  435. package/dist/session_ownership.js +221 -0
  436. package/dist/session_public.d.ts +10 -2
  437. package/dist/session_public.js +6 -1
  438. package/dist/simulation/dynamic_history.d.ts +42 -0
  439. package/dist/simulation/dynamic_history.js +173 -0
  440. package/dist/simulation/engines/swarm/pane_manager.js +7 -6
  441. package/dist/simulation/inference.d.ts +3 -1
  442. package/dist/simulation/inference.js +19 -1
  443. package/dist/simulation/intervention_generation.d.ts +27 -0
  444. package/dist/simulation/intervention_generation.js +246 -0
  445. package/dist/simulation/intervention_material.d.ts +4 -0
  446. package/dist/simulation/intervention_material.js +81 -0
  447. package/dist/simulation/living/context.d.ts +3130 -0
  448. package/dist/simulation/living/context.js +944 -0
  449. package/dist/simulation/living/definer.d.ts +12 -0
  450. package/dist/simulation/living/definer.js +223 -0
  451. package/dist/simulation/living/definition.d.ts +257 -0
  452. package/dist/simulation/living/definition.js +341 -0
  453. package/dist/simulation/living/frame.d.ts +259 -0
  454. package/dist/simulation/living/frame.js +223 -0
  455. package/dist/simulation/living/identifiers.d.ts +2 -0
  456. package/dist/simulation/living/identifiers.js +12 -0
  457. package/dist/simulation/living/index.d.ts +15 -0
  458. package/dist/simulation/living/index.js +15 -0
  459. package/dist/simulation/living/inference.d.ts +219 -0
  460. package/dist/simulation/living/inference.js +576 -0
  461. package/dist/simulation/living/mechanics.d.ts +54 -0
  462. package/dist/simulation/living/mechanics.js +588 -0
  463. package/dist/simulation/living/model-input.d.ts +25 -0
  464. package/dist/simulation/living/model-input.js +215 -0
  465. package/dist/simulation/living/open-world.d.ts +257 -0
  466. package/dist/simulation/living/open-world.js +102 -0
  467. package/dist/simulation/living/projection.d.ts +7 -0
  468. package/dist/simulation/living/projection.js +31 -0
  469. package/dist/simulation/living/recall.d.ts +17 -0
  470. package/dist/simulation/living/recall.js +25 -0
  471. package/dist/simulation/living/reduce.d.ts +15 -0
  472. package/dist/simulation/living/reduce.js +1853 -0
  473. package/dist/simulation/living/replay.d.ts +6 -0
  474. package/dist/simulation/living/replay.js +22 -0
  475. package/dist/simulation/living/runtime.d.ts +140 -0
  476. package/dist/simulation/living/runtime.js +3020 -0
  477. package/dist/simulation/living/shared-kinds.d.ts +24 -0
  478. package/dist/simulation/living/shared-kinds.js +96 -0
  479. package/dist/simulation/living/types.d.ts +2785 -0
  480. package/dist/simulation/living/types.js +669 -0
  481. package/dist/simulation/llm.d.ts +3 -1
  482. package/dist/simulation/llm.js +90 -30
  483. package/dist/simulation/memory/embedding.d.ts +1 -1
  484. package/dist/simulation/memory/embedding.js +9 -3
  485. package/dist/simulation/memory/filesystem.d.ts +2 -0
  486. package/dist/simulation/memory/filesystem.js +5 -1
  487. package/dist/simulation/memory/filesystem_file_tree.d.ts +4 -1
  488. package/dist/simulation/memory/filesystem_file_tree.js +41 -10
  489. package/dist/simulation/memory/filesystem_invariants.js +1 -2
  490. package/dist/simulation/memory/filesystem_types.d.ts +1 -0
  491. package/dist/simulation/memory/memory.d.ts +2 -0
  492. package/dist/simulation/memory/memory.js +5 -1
  493. package/dist/simulation/orchestration/intent.js +0 -2
  494. package/dist/simulation/orchestration/lifecycle.js +0 -2
  495. package/dist/simulation/plugin_host.d.ts +25 -1
  496. package/dist/simulation/plugin_host.js +334 -17
  497. package/dist/simulation/session_documents.d.ts +20 -0
  498. package/dist/simulation/session_documents.js +62 -0
  499. package/dist/simulation/session_input_validation.d.ts +43 -0
  500. package/dist/simulation/session_input_validation.js +364 -0
  501. package/dist/simulation/session_inputs.d.ts +32 -0
  502. package/dist/simulation/session_inputs.js +154 -0
  503. package/dist/simulation/telemetry.d.ts +11 -14
  504. package/dist/simulation/telemetry.js +177 -54
  505. package/dist/simulation/toolkit/bounded_turns.d.ts +13 -0
  506. package/dist/simulation/toolkit/bounded_turns.js +80 -0
  507. package/dist/simulation/toolkit/dynamic_agents.d.ts +25 -0
  508. package/dist/simulation/toolkit/dynamic_agents.js +218 -0
  509. package/dist/simulation/toolkit/dynamic_digest.d.ts +1 -0
  510. package/dist/simulation/toolkit/dynamic_digest.js +8 -0
  511. package/dist/simulation/toolkit/dynamic_documents.d.ts +7 -0
  512. package/dist/simulation/toolkit/dynamic_documents.js +71 -0
  513. package/dist/simulation/toolkit/dynamic_driver.d.ts +8 -0
  514. package/dist/simulation/toolkit/dynamic_driver.js +269 -0
  515. package/dist/simulation/toolkit/dynamic_game.d.ts +53 -0
  516. package/dist/simulation/toolkit/dynamic_game.js +805 -0
  517. package/dist/simulation/toolkit/dynamic_knowledge.d.ts +15 -0
  518. package/dist/simulation/toolkit/dynamic_knowledge.js +91 -0
  519. package/dist/simulation/toolkit/dynamic_mechanics.d.ts +22 -0
  520. package/dist/simulation/toolkit/dynamic_mechanics.js +910 -0
  521. package/dist/simulation/toolkit/dynamic_meetings.d.ts +239 -0
  522. package/dist/simulation/toolkit/dynamic_meetings.js +350 -0
  523. package/dist/simulation/toolkit/dynamic_progress.d.ts +131 -0
  524. package/dist/simulation/toolkit/dynamic_progress.js +214 -0
  525. package/dist/simulation/toolkit/dynamic_projection.d.ts +9 -0
  526. package/dist/simulation/toolkit/dynamic_projection.js +219 -0
  527. package/dist/simulation/toolkit/dynamic_runtime.d.ts +412 -0
  528. package/dist/simulation/toolkit/dynamic_runtime.js +6536 -0
  529. package/dist/simulation/toolkit/dynamic_state.d.ts +111 -0
  530. package/dist/simulation/toolkit/dynamic_state.js +1967 -0
  531. package/dist/simulation/toolkit/dynamic_story_review.d.ts +508 -0
  532. package/dist/simulation/toolkit/dynamic_story_review.js +333 -0
  533. package/dist/simulation/toolkit/dynamic_storyteller.d.ts +77 -0
  534. package/dist/simulation/toolkit/dynamic_storyteller.js +114 -0
  535. package/dist/simulation/toolkit/dynamic_storyteller_runtime.d.ts +26 -0
  536. package/dist/simulation/toolkit/dynamic_storyteller_runtime.js +126 -0
  537. package/dist/simulation/toolkit/dynamic_world.d.ts +22 -0
  538. package/dist/simulation/toolkit/dynamic_world.js +377 -0
  539. package/dist/simulation/toolkit/structured_output.d.ts +5 -0
  540. package/dist/simulation/toolkit/structured_output.js +321 -0
  541. package/dist/simulation/types/world.d.ts +2 -0
  542. package/dist/simulation/world/artifact_store.js +1 -1
  543. package/dist/simulation/world/memory_manager.d.ts +5 -0
  544. package/dist/simulation/world/memory_manager.js +17 -2
  545. package/dist/simulation/world/state.d.ts +3 -2
  546. package/dist/simulation/world/state.js +15 -5
  547. package/dist/skills/bundled/measurement-validation/SKILL.md +144 -0
  548. package/dist/terminal_attach.d.ts +1 -0
  549. package/dist/terminal_attach.js +113 -0
  550. package/dist/tool_progress.d.ts +3 -0
  551. package/dist/tool_progress.js +27 -0
  552. package/dist/types.d.ts +145 -10
  553. package/dist/types.js +3 -0
  554. package/dist/ui/core/input.d.ts +3 -0
  555. package/dist/ui/core/input_segments.d.ts +18 -1
  556. package/dist/ui/core/input_segments.js +131 -47
  557. package/dist/ui/core/input_state.d.ts +12 -0
  558. package/dist/ui/core/layout_yoga.d.ts +1 -1
  559. package/dist/ui/core/turn_metadata.d.ts +5 -0
  560. package/dist/ui/core/turn_metadata.js +22 -0
  561. package/dist/ui/ink/render.d.ts +2 -1
  562. package/dist/ui/ink/render.js +6 -5
  563. package/dist/ui/ratatui/headless_pane_manager.d.ts +14 -1
  564. package/dist/ui/ratatui/headless_pane_manager.js +293 -104
  565. package/dist/ui/ratatui/index.d.ts +1 -0
  566. package/dist/ui/ratatui/input.d.ts +4 -0
  567. package/dist/ui/ratatui/input.js +15 -0
  568. package/dist/ui/ratatui/ipc_path.d.ts +2 -0
  569. package/dist/ui/ratatui/ipc_path.js +29 -1
  570. package/dist/ui/ratatui/mux_persistence.d.ts +3 -1
  571. package/dist/ui/ratatui/mux_persistence.js +139 -1
  572. package/dist/ui/ratatui/pane_relay.d.ts +1 -1
  573. package/dist/ui/ratatui/pane_relay.js +55 -27
  574. package/dist/ui/ratatui/process.d.ts +17 -1
  575. package/dist/ui/ratatui/process.js +80 -7
  576. package/dist/ui/ratatui/protocol.d.ts +236 -1
  577. package/dist/ui/ratatui/protocol.js +90 -0
  578. package/dist/ui/scrollback.d.ts +2 -1
  579. package/dist/ui/scrollback.js +9 -4
  580. package/dist/ui/stream_typewriter.d.ts +8 -3
  581. package/dist/ui/stream_typewriter.js +30 -1
  582. package/dist/ui/streaming_scrollback.d.ts +1 -0
  583. package/dist/ui/streaming_scrollback.js +1 -1
  584. package/dist/ui/transcript_render_cache.js +10 -0
  585. package/dist/ui/views/activity_line.d.ts +5 -3
  586. package/dist/ui/views/activity_line.js +65 -7
  587. package/dist/ui/views/app.d.ts +6 -3
  588. package/dist/ui/views/app.js +17 -6
  589. package/dist/ui/views/completion_menu.js +10 -5
  590. package/dist/ui/views/input_area.d.ts +7 -3
  591. package/dist/ui/views/input_area.js +15 -9
  592. package/dist/ui/views/message_list.d.ts +4 -1
  593. package/dist/ui/views/message_list.js +89 -19
  594. package/dist/ui/views/queue_panel.d.ts +3 -1
  595. package/dist/ui/views/queue_panel.js +9 -2
  596. package/dist/ui/views/status_bar.d.ts +4 -2
  597. package/dist/ui/views/status_bar.js +28 -13
  598. package/dist/ui/views/theme.d.ts +2 -0
  599. package/dist/ui/views/theme.js +2 -0
  600. package/dist/ui/widgets/color_panel.js +18 -0
  601. package/dist/ui/widgets/form.js +8 -0
  602. package/dist/ui/widgets/log_view.js +7 -0
  603. package/dist/ui/widgets/navigator_panel.js +8 -0
  604. package/dist/ui/widgets/prompt_viewer.js +2 -0
  605. package/dist/ui/widgets/questionnaire.d.ts +2 -0
  606. package/dist/ui/widgets/questionnaire.js +79 -16
  607. package/dist/ui/widgets/resume_panel.d.ts +2 -0
  608. package/dist/ui/widgets/resume_panel.js +26 -15
  609. package/dist/ui/widgets/rewind_panel.d.ts +2 -2
  610. package/dist/ui/widgets/rewind_panel.js +4 -2
  611. package/dist/ui/widgets/select.js +16 -27
  612. package/dist/ui/widgets/settings_panel.js +16 -0
  613. package/dist/ui/widgets/status_line_panel.d.ts +1 -0
  614. package/dist/ui/widgets/status_line_panel.js +30 -4
  615. package/dist/ui/widgets/text_input.d.ts +2 -0
  616. package/dist/ui/widgets/text_input.js +15 -1
  617. package/dist/ui/widgets/types.d.ts +2 -0
  618. package/dist/usage_projection.d.ts +24 -0
  619. package/dist/usage_projection.js +130 -0
  620. package/dist/usage_snapshot.d.ts +36 -0
  621. package/dist/usage_snapshot.js +131 -0
  622. package/dist/utils/assistant_message.d.ts +2 -0
  623. package/dist/utils/assistant_message.js +5 -0
  624. package/dist/utils/atomic_write.d.ts +6 -2
  625. package/dist/utils/atomic_write.js +29 -4
  626. package/dist/utils/checked_shell.d.ts +25 -0
  627. package/dist/utils/checked_shell.js +107 -0
  628. package/dist/utils/compaction.d.ts +4 -1
  629. package/dist/utils/compaction.js +31 -7
  630. package/dist/utils/context.d.ts +4 -4
  631. package/dist/utils/context.js +14 -12
  632. package/dist/utils/deepseek.d.ts +2 -2
  633. package/dist/utils/deepseek.js +13 -5
  634. package/dist/utils/ephemeral.js +4 -0
  635. package/dist/utils/ephemeral_tools.d.ts +18 -0
  636. package/dist/utils/ephemeral_tools.js +97 -0
  637. package/dist/utils/jev_client.d.ts +4 -0
  638. package/dist/utils/jev_client.js +15 -0
  639. package/dist/utils/model_cache.d.ts +15 -0
  640. package/dist/utils/model_cache.js +143 -0
  641. package/dist/utils/models.d.ts +4 -2
  642. package/dist/utils/models.js +12 -16
  643. package/dist/utils/reasoning.d.ts +8 -1
  644. package/dist/utils/reasoning.js +31 -11
  645. package/dist/utils/router_catalog.d.ts +49 -0
  646. package/dist/utils/router_catalog.js +271 -0
  647. package/dist/utils/sandbox.d.ts +9 -0
  648. package/dist/utils/sandbox.js +118 -15
  649. package/dist/utils/shell.d.ts +7 -0
  650. package/dist/utils/shell.js +98 -34
  651. package/dist/utils/subprocess_env.d.ts +3 -0
  652. package/dist/utils/subprocess_env.js +27 -1
  653. package/dist/web/accounts_mirror.d.ts +3 -1
  654. package/dist/web/accounts_mirror.js +13 -9
  655. package/dist/web/child_publication.d.ts +17 -0
  656. package/dist/web/child_publication.js +45 -0
  657. package/dist/web/mirror_authenticator.d.ts +31 -11
  658. package/dist/web/mirror_authenticator.js +67 -17
  659. package/dist/web/mirror_client.d.ts +31 -4
  660. package/dist/web/mirror_client.js +189 -23
  661. package/dist/web/mirror_stream.d.ts +84 -22
  662. package/dist/web/mirror_stream.js +676 -95
  663. package/dist/web/observer_query_redaction.d.ts +6 -0
  664. package/dist/web/observer_query_redaction.js +58 -0
  665. package/dist/web/pane_registry.d.ts +2 -0
  666. package/dist/web/pane_registry.js +86 -22
  667. package/dist/web/tool_image.d.ts +6 -0
  668. package/dist/web/tool_image.js +17 -0
  669. package/dist/web/view_plugin.d.ts +14 -0
  670. package/dist/web/view_plugin.js +23 -1
  671. package/dist/worker/executor.js +31 -12
  672. package/dist/worker/index.d.ts +1 -0
  673. package/dist/worker/toolsets.js +3 -1
  674. package/dist/worker/types.d.ts +12 -1
  675. package/dist/workflows/registry.js +1 -0
  676. package/dist/workflows/registry_handle.d.ts +2 -0
  677. package/dist/workflows/runtime.d.ts +2 -0
  678. package/dist/workflows/runtime.js +1 -0
  679. package/dist/workspace_agent.d.ts +7 -0
  680. package/dist/workspace_agent.js +36 -0
  681. package/dist/workspace_collision.d.ts +76 -0
  682. package/dist/workspace_collision.js +688 -0
  683. package/dist/workspace_layout.d.ts +18 -0
  684. package/dist/workspace_layout.js +121 -0
  685. package/dist/workspace_runtime.d.ts +44 -0
  686. package/dist/workspace_runtime.js +82 -0
  687. package/dist/workspace_session_context.d.ts +62 -0
  688. package/dist/workspace_session_context.js +160 -0
  689. package/dist/zlm-embed.mjs +32518 -9615
  690. package/dist/zlm.js +1 -0
  691. package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
  692. package/node_modules/@zerg/model-contracts/README.md +90 -0
  693. package/node_modules/@zerg/model-contracts/index.d.ts +20 -0
  694. package/node_modules/@zerg/model-contracts/index.js +223 -0
  695. package/node_modules/@zerg/model-contracts/package.json +8 -0
  696. package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
  697. package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
  698. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
  699. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  700. package/package.json +45 -24
  701. package/packages/model-contracts/LICENSE.md +375 -0
  702. package/packages/model-contracts/README.md +90 -0
  703. package/packages/model-contracts/index.d.ts +20 -0
  704. package/packages/model-contracts/index.js +223 -0
  705. package/packages/model-contracts/package.json +8 -0
  706. package/remote-broker/Dockerfile +10 -0
  707. package/remote-broker/README.md +308 -0
  708. package/remote-broker/archive.py +1227 -0
  709. package/remote-broker/cutover.py +761 -0
  710. package/remote-broker/requirements.txt +5 -0
  711. package/remote-broker/resources.example.json +6 -0
  712. package/remote-broker/resources.write-staging.example.json +18 -0
  713. package/remote-broker/retained.py +686 -0
  714. package/remote-broker/retained_cleanup.py +550 -0
  715. package/remote-broker/server.py +648 -0
  716. package/scripts/build-remote-broker.mjs +13 -0
  717. package/scripts/ensure-local-packages.mjs +6 -0
  718. package/scripts/node-pty-helpers.mjs +103 -0
@@ -1,38 +1,54 @@
1
1
  // Standard library
2
- import { randomUUID } from 'node:crypto';
3
- import { existsSync, readFileSync } from 'fs';
4
- import { extname } from 'path';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { isDeepStrictEqual } from 'node:util';
4
+ import { existsSync, readFileSync, realpathSync } from 'fs';
5
+ import { extname, resolve } from 'path';
6
+ import { formatTaskHandoffContext, parseTaskHandoffProjection } from '../session_handoff_contract.js';
5
7
  import { createBackendForProvider } from './backends/index.js';
6
8
  import { mergeTokenUsage } from './backends/token_usage.js';
9
+ import { createUsageSnapshot, mergeUsageSnapshots, parseUsageSnapshot, recordUsageReceipt } from '../usage_snapshot.js';
7
10
  import { isTransportCodeError, isTransportMessageError } from './backends/transport_errors.js';
11
+ import { retryAfterDelayMs } from './backends/http_error.js';
8
12
  import { inferDefaultProvider, getDefaultApiKeyForProvider, getDefaultModelForProvider } from '../config/defaults.js';
9
- import { resolveApiRetrySettings } from '../config/api_retry.js';
13
+ import { apiRetryDelayMs, resolveApiRetrySettings } from '../config/api_retry.js';
10
14
  import { resolveExecutionSettings } from '../config/execution.js';
11
15
  import { createRecoveryGuard } from './runtime/recovery.js';
12
16
  import { listCommandJobs, interruptCommandJobs } from './runtime/command_jobs.js';
13
- import { describeExecutionContext } from './runtime/execution_context.js';
17
+ import { awaitManagedCommandProgress, formatManagedCommandObservations } from './runtime/command_wait.js';
18
+ import { describeExecutionContext, DELETION_EVIDENCE_GUIDANCE } from './runtime/execution_context.js';
14
19
  import { scheduleToolBatch, commandStillRunning } from './runtime/tool_scheduler.js';
15
20
  import { AllowAllPolicy } from './runtime/policy.js';
16
21
  import { NoopTracer } from './runtime/tracing.js';
22
+ import { createDiagnosticSnapshot, mergeDiagnosticSnapshots, parseDiagnosticSnapshot, recordDiagnosticEvent as appendDiagnosticEvent } from '../diagnostics/session_export.js';
23
+ import { getVersion } from '../utils/version.js';
17
24
  import { defaultTools as baseDefaultTools } from './tools/registry.js';
18
25
  import { toolSearchTool } from './tools/tool_search.js';
19
26
  import { recallToolResponseTool } from './tools/recall_tool_response.js';
20
27
  import { adaptationRunTool } from './tools/adaptation.js';
21
28
  import { getRequestToolDefinitions } from './tools/deferred.js';
22
29
  import { getLatestTodos } from './tools/todo.js';
23
- import { ToolResponseStore, describeOffloadPlaceholderFormat } from './tool_response_store.js';
24
- import { RequestEncoder } from './request_encoder.js';
30
+ import { ToolResponseStore, ToolResponseIntegrityError, describeOffloadPlaceholderFormat } from './tool_response_store.js';
31
+ import { formatEvidenceContext } from './runtime/evidence.js';
32
+ import { captureNativeReport, NativeObservationLimitError } from './runtime/provider_native_evidence.js';
33
+ import { projectCurrentVerification } from './runtime/source_verification.js';
34
+ import { sessionEvidenceTool } from './tools/session_evidence.js';
35
+ import { MEASUREMENT_EVIDENCE_GUIDANCE } from './measurement_guidance.js';
36
+ import { RequestEncoder, STABLE_RESULT_LIMIT, nativeHistoryContextId } from './request_encoder.js';
25
37
  import { parseImageToolResult } from './tool_result_parsing.js';
26
38
  import { createRepeatGuard, pinnedTaskSystem } from './loop_policy.js';
27
39
  import { extractSourcePath } from './tool_compression.js';
28
40
  import { executeToolWithRuntime } from './runtime/tool_runtime.js';
41
+ import { createToolProgressAccumulator } from './runtime/tool_progress.js';
42
+ import { ToolCapability } from './runtime/capabilities.js';
29
43
  import { buildUserSubprocessEnv } from '../utils/subprocess_env.js';
30
44
  import { resolvePromptPrefixId } from '../utils/compaction.js';
31
45
  import { INCOMPLETE_MODEL_RESPONSE_CODE, incompleteModelResponseMessage, isIncompleteModelResponse, } from './completion_state.js';
46
+ import { remoteDeletionReport, NO_VERIFIED_DELETION_REPORT } from './runtime/remote_deletion_receipts.js';
32
47
  const defaultTools = [
33
48
  ...baseDefaultTools,
34
49
  toolSearchTool,
35
50
  recallToolResponseTool,
51
+ sessionEvidenceTool,
36
52
  adaptationRunTool,
37
53
  ];
38
54
  function getTool(name, tools) {
@@ -160,6 +176,8 @@ export class Agent {
160
176
  backend;
161
177
  policy;
162
178
  tracer;
179
+ diagnostics = createDiagnosticSnapshot();
180
+ usageSnapshot = createUsageSnapshot();
163
181
  streamChunkSize = 32;
164
182
  /**
165
183
  * True when the last callAPI() consumed the backend's generateStream and
@@ -173,13 +191,134 @@ export class Agent {
173
191
  /** Auto-continues spent since the last tool run; reset per turn and per tool. */
174
192
  _consecutiveAutoContinues = 0;
175
193
  _subprocessEnv;
194
+ commandPolicy;
176
195
  toolEffectObserver;
196
+ onLocalFileChange;
197
+ toolCheckpoint;
198
+ toolCheckpointTail = Promise.resolve();
199
+ toolCheckpointFailure = null;
200
+ failedToolCheckpoint = null;
201
+ nativeEvidencePending = Promise.resolve();
202
+ nativeEvidenceFailure = null;
203
+ nativeEvidenceCaptureDepth = 0;
204
+ // Invocation identity is independent of command text: a fresh call ID is a
205
+ // new operation, while a recorded ID must never reach dispatch a second time.
206
+ recordedToolCalls = new Map();
207
+ remoteResourceSession;
177
208
  #conversationBinding;
178
209
  // Per-instance offload store + tool-discovery set. Owning these at the Agent
179
210
  // level (rather than as module singletons) means subagents get isolated
180
211
  // copies — their tool_search discoveries and tool-result writes never leak
181
212
  // into the parent's request schemas or tool_responses/ directory.
182
- toolResponseStore = new ToolResponseStore();
213
+ toolResponseStore;
214
+ evidenceUnsubscribe;
215
+ getEvidenceSnapshot() {
216
+ return this.toolResponseStore.evidence.getSnapshot();
217
+ }
218
+ getDiagnosticSessionId() { return this.config.sessionId; }
219
+ getUsageSessionId() { return this.config.sessionId; }
220
+ getUsageSnapshot() { return structuredClone(this.usageSnapshot); }
221
+ restoreUsageSnapshot(snapshot) {
222
+ const admitted = parseUsageSnapshot(snapshot);
223
+ if (admitted === null)
224
+ throw new TypeError('Invalid usage snapshot');
225
+ this.usageSnapshot = mergeUsageSnapshots(admitted, this.usageSnapshot);
226
+ }
227
+ retainUsageReceipt(receipt) {
228
+ this.usageSnapshot = recordUsageReceipt(this.usageSnapshot, receipt);
229
+ this.emit({ type: 'request_usage', receipt: structuredClone(receipt) });
230
+ }
231
+ getDiagnosticSnapshot() { return structuredClone(this.diagnostics); }
232
+ restoreDiagnosticSnapshot(snapshot) {
233
+ const admitted = parseDiagnosticSnapshot(snapshot);
234
+ if (admitted === null)
235
+ throw new TypeError('Invalid diagnostic snapshot');
236
+ // Retain both the restored history and this newly constructed runtime epoch.
237
+ this.diagnostics = mergeDiagnosticSnapshots(admitted, this.diagnostics);
238
+ }
239
+ recordDiagnosticEvent(kind, data) {
240
+ this.diagnostics = appendDiagnosticEvent(this.diagnostics, { id: `diagnostic_${randomUUID()}`,
241
+ kind, origin: kind === 'provider_response' ? 'provider' : 'host', storedRole: null,
242
+ timestamp: new Date().toISOString(), data });
243
+ }
244
+ /** Persist only factual API metadata, never prompts, credentials, or raw response bodies. */
245
+ captureDiagnosticTrace(event) {
246
+ const data = event.data;
247
+ if (!data)
248
+ return;
249
+ if (event.type === 'api_request') {
250
+ const tools = Array.isArray(data.tools) ? data.tools.flatMap(tool => typeof tool === 'object' && tool !== null && 'name' in tool && typeof tool.name === 'string' ? [tool.name] : []) : [];
251
+ this.recordDiagnosticEvent('runtime', { phase: 'provider_request',
252
+ turnId: typeof data.turnId === 'string' ? data.turnId : null,
253
+ model: typeof data.model === 'string' ? data.model : null,
254
+ maxTokens: typeof data.maxTokens === 'number' ? data.maxTokens : null, tools });
255
+ }
256
+ else if (event.type === 'api_response') {
257
+ const usage = {};
258
+ if (typeof data.usage === 'object' && data.usage !== null) {
259
+ for (const [key, value] of Object.entries(data.usage)) {
260
+ if (['inputTokens', 'outputTokens', 'totalTokens', 'cachedInputTokens', 'cacheWriteInputTokens', 'reasoningTokens', 'contextWindow'].includes(key)
261
+ && typeof value === 'number' && Number.isFinite(value))
262
+ usage[key] = value;
263
+ if (key === 'estimated' && typeof value === 'boolean')
264
+ usage[key] = value;
265
+ }
266
+ }
267
+ this.recordDiagnosticEvent('provider_response', {
268
+ turnId: typeof data.turnId === 'string' ? data.turnId : null,
269
+ model: typeof data.model === 'string' ? data.model : null,
270
+ provider: this.config.runtimeFacts.provider ?? null,
271
+ providerStopReason: typeof data.stopReason === 'string' ? data.stopReason : null,
272
+ stopReasonSource: 'backend_response',
273
+ usage: data.usage === undefined ? null : usage,
274
+ });
275
+ }
276
+ }
277
+ toolTraceData(call) {
278
+ const source = this.getEvidenceSnapshot().records.find(record => record.kind === 'observation' && record.source.toolCallId === call.id);
279
+ return { tool: call.name, toolCallId: call.id, args: structuredClone(call.args), status: call.status,
280
+ startedAt: call.startedAt?.toISOString() ?? null, completedAt: call.completedAt?.toISOString() ?? null,
281
+ ...(source?.kind === 'observation' ? { resultReference: { observationId: source.id,
282
+ sha256: source.resultSha256, bytes: source.resultBytes, partial: source.partial,
283
+ availability: source.availability }, job: source.source.job ?? null } : {}) };
284
+ }
285
+ /** Fresh applicability is a projection; original process/source observations remain immutable. */
286
+ async refreshVerificationCoverage() {
287
+ const latest = new Map();
288
+ for (const record of this.getEvidenceSnapshot().records) {
289
+ if (record.source.verification) {
290
+ latest.delete(record.source.verification.receipt_id);
291
+ latest.set(record.source.verification.receipt_id, record.source.verification);
292
+ }
293
+ }
294
+ if (!latest.size)
295
+ return;
296
+ const receipts = [...latest.values()].reverse();
297
+ const selected = [...receipts.filter(receipt => receipt.process_outcome === 'running'),
298
+ ...receipts.filter(receipt => receipt.process_outcome !== 'running').slice(0, 8)];
299
+ const coverage = [];
300
+ for (const receipt of selected) {
301
+ let admitted = !this.remoteResourceSession && receipt.source.origin === 'local';
302
+ if (admitted) {
303
+ try {
304
+ admitted = realpathSync(this._cwd) === receipt.source.cwd;
305
+ }
306
+ catch {
307
+ admitted = false;
308
+ }
309
+ }
310
+ if (admitted)
311
+ admitted = receipt.source.files.every(file => this.policy.evaluateTool({ name: 'read_file',
312
+ args: { path: resolve(receipt.source.cwd, file.path) }, capabilities: [ToolCapability.FILE_READ] }).allowed);
313
+ coverage.push(admitted ? await projectCurrentVerification(receipt) : {
314
+ receipt_id: receipt.receipt_id, checked_at: new Date().toISOString(), source_id: null, applicability: 'unknown',
315
+ reason: 'The current host, working directory or file policy does not admit a fresh check of this source.',
316
+ });
317
+ }
318
+ const retained = new Set(this.getEvidenceSnapshot().records.flatMap(record => record.source.verification
319
+ ? [record.source.verification.receipt_id] : []));
320
+ this.toolResponseStore.evidence.setVerificationCoverage(coverage.filter(row => retained.has(row.receipt_id)));
321
+ }
183
322
  discoveredTools = new Set();
184
323
  // Host-injected agent-domain tools that ship every request regardless of the
185
324
  // deferred-tool registry. Unlike discoveredTools (session-scoped, cleared on
@@ -192,6 +331,7 @@ export class Agent {
192
331
  _hasCustomSystemPrompt;
193
332
  _authenticationManagedByBackend;
194
333
  constructor(config = {}) {
334
+ this.toolResponseStore = config.toolResponseStore ?? new ToolResponseStore();
195
335
  const ambientDiscovery = config.ambientDiscovery ?? true;
196
336
  const defaultProvider = ambientDiscovery ? inferDefaultProvider() : 'anthropic';
197
337
  const defaultModel = getDefaultModelForProvider(defaultProvider);
@@ -204,6 +344,7 @@ export class Agent {
204
344
  apiKey,
205
345
  apiEndpoint: config.apiEndpoint,
206
346
  });
347
+ const tools = config.tools ?? (ambientDiscovery ? defaultTools : []);
207
348
  this.config = {
208
349
  model: config.model || defaultModel,
209
350
  apiKey,
@@ -218,8 +359,8 @@ export class Agent {
218
359
  runtimeFacts: config.runtimeFacts ?? {},
219
360
  apiRetry: resolveApiRetrySettings({ apiRetry: config.apiRetry }),
220
361
  llmRequestTimeoutMs: resolveLlmRequestTimeoutMs(config.llmRequestTimeoutMs, config.apiRetry?.requestTimeoutMs),
221
- tools: config.tools || (ambientDiscovery ? defaultTools : []),
222
- systemPrompt: config.systemPrompt || this.getDefaultSystemPrompt(),
362
+ tools,
363
+ systemPrompt: config.systemPrompt || this.getDefaultSystemPrompt(tools),
223
364
  backend,
224
365
  ambientDiscovery,
225
366
  policy: config.policy || new AllowAllPolicy(),
@@ -233,7 +374,11 @@ export class Agent {
233
374
  this._subprocessEnv = config.subprocessEnv === undefined
234
375
  ? undefined
235
376
  : buildUserSubprocessEnv(config.subprocessEnv);
377
+ this.commandPolicy = config.commandPolicy === undefined ? undefined : structuredClone(config.commandPolicy);
236
378
  this.toolEffectObserver = config.toolEffectObserver;
379
+ this.onLocalFileChange = config.onLocalFileChange;
380
+ this.toolCheckpoint = config.toolCheckpoint;
381
+ this.remoteResourceSession = config.remoteResourceSession;
237
382
  this.#conversationBinding = config.conversationBinding;
238
383
  // Host-injected agent-domain tools always ship their schema (they're the
239
384
  // agent's primary surface, not a corpus to search). Kept separate from
@@ -242,8 +387,16 @@ export class Agent {
242
387
  this.alwaysLoadedToolNames.add(name);
243
388
  }
244
389
  this.backend = this.config.backend;
390
+ if (this.backend.getModelCapabilities?.(this.config.model)?.tools === false) {
391
+ this.config.tools = [];
392
+ this.config.autoContinue = false;
393
+ }
245
394
  this.policy = this.config.policy;
246
- this.tracer = this.config.tracer;
395
+ const suppliedTracer = this.config.tracer;
396
+ this.tracer = { event: event => {
397
+ this.captureDiagnosticTrace(event);
398
+ suppliedTracer.event(event);
399
+ } };
247
400
  this._cwd = this.config.cwd;
248
401
  this._autoContinue = this.config.autoContinue;
249
402
  this.hooks = config.hooks;
@@ -255,9 +408,25 @@ export class Agent {
255
408
  this._codexEffort = config.codexEffort;
256
409
  this._codexFastMode = config.codexFastMode ?? false;
257
410
  this._hasCustomSystemPrompt = typeof config.systemPrompt === 'string' && config.systemPrompt.trim().length > 0;
411
+ this.evidenceUnsubscribe = this.toolResponseStore.evidence.onChange(() => {
412
+ if (this.nativeEvidenceCaptureDepth > 0)
413
+ return;
414
+ this.emit({ type: 'evidence', sessionId: this.config.sessionId, snapshot: this.getEvidenceSnapshot() });
415
+ });
416
+ const version = getVersion();
417
+ this.recordDiagnosticEvent('runtime', { phase: 'agent_constructed',
418
+ version: version === '0.0.0' ? null : version,
419
+ provider: this.config.runtimeFacts.provider ?? null, model: this.config.model,
420
+ mode: this.config.runtimeFacts.mode ?? null, mux: this.config.runtimeFacts.mux ?? null,
421
+ configuredTools: this.getConfiguredToolNames(),
422
+ runtimeCredentialsAvailable: this.config.runtimeCredentialsAvailable,
423
+ backendAuthenticationConfigured: this._authenticationManagedByBackend,
424
+ llmRequestTimeoutMs: this.config.llmRequestTimeoutMs,
425
+ execution: { ...this.config.execution },
426
+ });
258
427
  }
259
428
  setAutoContinue(enabled) {
260
- this._autoContinue = enabled;
429
+ this._autoContinue = enabled && this.backend.getModelCapabilities?.(this.config.model)?.tools !== false;
261
430
  }
262
431
  /** Override the agent's system prompt (e.g., for simulation persona injection). */
263
432
  setSystemPrompt(prompt) {
@@ -271,7 +440,7 @@ export class Agent {
271
440
  refreshSystemPrompt() {
272
441
  if (this._hasCustomSystemPrompt)
273
442
  return;
274
- this.config.systemPrompt = this.getDefaultSystemPrompt();
443
+ this.config.systemPrompt = this.getDefaultSystemPrompt(this.config.tools);
275
444
  }
276
445
  getAutoContinue() {
277
446
  return this._autoContinue;
@@ -319,12 +488,12 @@ export class Agent {
319
488
  getConfiguredToolNames() {
320
489
  return this.config.tools.map((tool) => tool.definition.name);
321
490
  }
322
- getDefaultSystemPrompt() {
491
+ getDefaultSystemPrompt(tools) {
323
492
  const offloadEnabled = this.toolResponseStore.getConfig().offloadEnabled;
324
493
  const offloadSection = offloadEnabled
325
494
  ? `\nTool results older than a handful of turns or larger than ~2k tokens appear as offload placeholders (e.g. \`${describeOffloadPlaceholderFormat()}\`). Full content is stored on disk per-session; call \`recall_tool_response\` with the \`tool_use_id\` to retrieve it.
326
495
 
327
- Recall is one-shot per session: each \`tool_use_id\` should be recalled at most once. After you recall it, the original tool_result above will show the full content again as long as the inline budget permits. If you call recall again on an id that was already recalled, you will receive a no-op acknowledgement — read the original tool_result block instead. If a placeholder is followed by \`First lines:\` / \`Last lines:\` previews, you can often answer from those without recalling.\n`
496
+ Recall returns bounded source bytes with \`cursor\`, \`nextCursor\`, and \`complete\`; repeat at \`nextCursor\` until the needed source is available. An acknowledgement is used only for a small result whose exact full bytes are already visible. Large original results may remain historical previews, so read the returned recall content. If a placeholder is followed by \`First lines:\` / \`Last lines:\` previews, you can often answer from those without recalling.\n`
328
497
  : '';
329
498
  return `You are ZTC (Zerg Terminal Client), an AI assistant that helps users interact with the Zerg continual AI system and manage local development tasks.
330
499
 
@@ -348,9 +517,9 @@ ${offloadSection}
348
517
  INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow this sequence:
349
518
  1. State your hypothesis: what do you think is happening and why? Which files or functions are involved?
350
519
  2. Plan your reads: identify the specific locations you need to check. Prefer one larger read (100–200 lines) over many small 20-line windows — fewer tool calls, more context per call.
351
- 3. Read, then extract: after reading a file, state the key findings in your response text before moving on. Your own text persists in context even when tool results age out — treat your written analysis as your working memory.
520
+ 3. Read, then extract: distinguish captured observations from your interpretation. Use session_evidence to discover source IDs and record hypotheses/conclusions; corrections require supporting evidence and an explicit supersedes link. Assistant prose never replaces a tool outcome or grants user authorization. Recall the original source when a summary conflicts with it; missing evidence remains unknown.
352
521
  4. Verify before acting: trace the full chain (caller → function → side effects → re-render) in one pass before proposing a fix. Jumping between scattered reads wastes rounds.
353
- 5. Never re-read a file section you already read in this session unless the file was modified since. If you need data from a prior read, check your earlier analysis first.`;
522
+ 5. Never re-read a file section you already read in this session unless the file was modified since. If you need data from a prior read, check your earlier analysis first.\n\n${DELETION_EVIDENCE_GUIDANCE}${tools.some(tool => tool.definition.name === 'measurement_report') ? `\n\n${MEASUREMENT_EVIDENCE_GUIDANCE}` : ''}`;
354
523
  }
355
524
  // Event handling
356
525
  on(handler) {
@@ -362,6 +531,28 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
362
531
  handler(event);
363
532
  }
364
533
  }
534
+ toolOutputObserver(toolCall, signal) {
535
+ const progress = createToolProgressAccumulator(toolCall.startedAt.getTime(), snapshot => {
536
+ if (signal?.aborted || toolCall.status !== 'running')
537
+ return;
538
+ const event = { type: 'tool_progress', toolCallId: toolCall.id, tool: toolCall.name, progress: snapshot };
539
+ // A failed display subscriber must neither fail the admitted tool nor
540
+ // prevent another subscriber from observing its informational output.
541
+ for (const handler of this.eventHandlers) {
542
+ try {
543
+ handler(event);
544
+ }
545
+ catch { /* informational observer only */ }
546
+ }
547
+ });
548
+ return {
549
+ onOutput: (stream, chunk) => {
550
+ if (!signal?.aborted && toolCall.status === 'running')
551
+ progress.onOutput(stream, chunk);
552
+ },
553
+ close: progress.close,
554
+ };
555
+ }
365
556
  nextToolEventId() {
366
557
  this.toolEventSequence += 1;
367
558
  return `tool_${this.toolEventSequence}`;
@@ -381,6 +572,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
381
572
  // confusing reversal). We subtract this from the emitted context so the bar
382
573
  // tracks durable context and only moves backward on a real reset/compaction.
383
574
  _currentTransientOverageTokens = 0;
575
+ _nativeContextId;
384
576
  formatMessages(messages) {
385
577
  // Pre-pass: collect every tool_use_id appearing in tool_result messages,
386
578
  // and remember the LAST batch (treated as current turn). Running the
@@ -409,17 +601,26 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
409
601
  this._currentRequestToolIds = currentTurnToolIds;
410
602
  if (allToolIds.length > 0) {
411
603
  const activeModel = this.config.model || 'default';
412
- const budget = this.toolResponseStore.effectiveBudget(activeModel);
604
+ const budget = this.toolResponseStore.effectiveBudget(activeModel, this.backend.getModelCapabilities?.(activeModel) ? this.backend.getModelCapabilities(activeModel)?.context_window ?? null : undefined);
413
605
  this._selectorVerdict = this.toolResponseStore.selectInlineSet(allToolIds, currentTurnToolIds, budget);
414
606
  }
415
607
  else {
416
608
  this._selectorVerdict = null;
609
+ this.toolResponseStore.selectInlineSet([], new Set(), 0);
417
610
  }
418
611
  // The role filter, per-message content encoding, and same-role merge live in
419
612
  // RequestEncoder (provider-neutral, shared shape). The selector verdict +
420
613
  // current-turn ids computed above are injected; the transient-overage count
421
614
  // comes back as an explicit return instead of a hidden field mutation.
422
- const { messages: encoded, transientOverageTokens } = new RequestEncoder(this._encodeDeps()).encode(messages);
615
+ // Preserve opaque provider context through saved history without placing it
616
+ // in the display-only reasoningContent field. Copies are wire-only.
617
+ const wireMessages = messages.map(message => {
618
+ const providerReasoning = message.metadata?.providerReasoning;
619
+ return message.role === 'assistant' && typeof providerReasoning === 'string'
620
+ ? { ...message, metadata: { ...message.metadata, reasoningContent: providerReasoning } }
621
+ : message;
622
+ });
623
+ const { messages: encoded, transientOverageTokens } = new RequestEncoder(this._encodeDeps()).encode(wireMessages);
423
624
  this._currentTransientOverageTokens = transientOverageTokens;
424
625
  return encoded;
425
626
  }
@@ -428,6 +629,9 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
428
629
  // buildContentBlocks pipeline entry point below.
429
630
  _encodeDeps() {
430
631
  return {
632
+ preserveNativeHistory: this.backend.preservesNativeHistory,
633
+ model: this.config.model,
634
+ nativeContextId: this._nativeContextId,
431
635
  selectorVerdict: this._selectorVerdict,
432
636
  currentTurnToolIds: this._currentRequestToolIds,
433
637
  renderForContext: (id) => this.toolResponseStore.renderForContext(id),
@@ -635,7 +839,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
635
839
  }
636
840
  const missingBeforeStrip = activeToolCalls.filter(tc => !resultIds.has(tc.id));
637
841
  const hasStoredResults = missingBeforeStrip.some(tc => Boolean(tc.error) || (tc.result !== undefined && tc.result !== null));
638
- const stripIds = new Set(hasRoleToolEvidence && !hasStoredResults
842
+ const stripIds = new Set(hasRoleToolEvidence && !hasStoredResults && msg.metadata?.toolCheckpoint !== true
639
843
  ? missingBeforeStrip.map(tc => tc.id)
640
844
  : []);
641
845
  const keptToolCalls = activeToolCalls.filter(tc => !stripIds.has(tc.id));
@@ -671,7 +875,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
671
875
  content = tc.result;
672
876
  }
673
877
  else {
674
- content = 'Error: Tool execution was interrupted. Please retry.';
878
+ content = 'Tool outcome unknown: the original result was not durably observed. Inspect this exact call and its existing receipts/current state before deciding whether to retry. Do not automatically repeat the original command; local interruption does not prove that an external operation stopped.';
675
879
  }
676
880
  if (content.length > SYNTH_RESULT_CAP) {
677
881
  content = content.slice(0, SYNTH_RESULT_CAP) + `\n[truncated — ${content.length} chars total]`;
@@ -695,6 +899,14 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
695
899
  return repaired;
696
900
  }
697
901
  buildRequest(messages, availableTools = this.config.tools) {
902
+ this.captureUserInstructions(messages);
903
+ const evidence = formatEvidenceContext(this.getEvidenceSnapshot());
904
+ const evidenceTurn = [...messages].reverse().find(message => message.role === 'user');
905
+ // Stamp each source turn once. Render the host index next to that turn
906
+ // without changing its text, the system prefix or prior request records.
907
+ if (evidence && evidenceTurn && typeof evidenceTurn.metadata?.evidenceContext !== 'string') {
908
+ evidenceTurn.metadata = { ...evidenceTurn.metadata, evidenceContext: evidence };
909
+ }
698
910
  // Repair any orphaned tool_use blocks before building request
699
911
  if (this.config.pinTaskHeader) {
700
912
  const facts = describeExecutionContext({ ...this.getExecutionContext(), tools: availableTools.map(tool => tool.definition.name) });
@@ -705,22 +917,70 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
705
917
  latestUser.metadata = { ...latestUser.metadata, executionContext: facts };
706
918
  }
707
919
  }
920
+ const currentCoverage = this.getEvidenceSnapshot().verificationCoverage;
921
+ if (currentCoverage?.length && evidenceTurn) {
922
+ const rows = [];
923
+ let remaining = 12 * 1024 - 512;
924
+ for (const projection of currentCoverage) {
925
+ const row = JSON.stringify(projection);
926
+ const size = Buffer.byteLength(row) + 1;
927
+ if (size > remaining)
928
+ continue;
929
+ rows.push(row);
930
+ remaining -= size;
931
+ }
932
+ evidenceTurn.metadata = { ...evidenceTurn.metadata,
933
+ verificationContext: '[Current source applicability — host observations]\n' + rows.join('\n')
934
+ + `\n${currentCoverage.length - rows.length} additional applicability checks omitted. Checks are scoped and as-of checked_at. Historical process outcomes and declared purposes do not qualify edited source; matching endpoints do not prove immutable inputs. Qualification remains unverified.`,
935
+ };
936
+ }
708
937
  const repairedMessages = this.repairConversationHistory(messages);
709
938
  // Facts are attached to the user/tool-result turn that observed them. Keep
710
939
  // earlier snapshots immutable so provider threads can reuse their prefix.
711
- const contextMessages = this.config.pinTaskHeader ? repairedMessages.flatMap(message => {
712
- const facts = message.metadata?.executionContext;
713
- return typeof facts === 'string' ? [message, {
714
- id: `${message.id}_execution`, role: 'user', content: facts, timestamp: message.timestamp,
715
- metadata: { synthetic: true },
716
- }] : [message];
717
- }) : repairedMessages;
940
+ const contextMessages = repairedMessages.flatMap(message => {
941
+ const facts = this.config.pinTaskHeader ? message.metadata?.executionContext : undefined;
942
+ const observations = message.metadata?.evidenceContext;
943
+ const applicability = message.metadata?.verificationContext;
944
+ return [message, ...(typeof facts === 'string' ? [{
945
+ id: `${message.id}_execution`, role: 'user', content: facts, timestamp: message.timestamp,
946
+ metadata: { synthetic: true, hostContext: true },
947
+ }] : []), ...(typeof observations === 'string' ? [{
948
+ id: `${message.id}_evidence`, role: 'user', content: observations, timestamp: message.timestamp,
949
+ metadata: { synthetic: true, hostContext: true },
950
+ }] : []), ...(typeof applicability === 'string' ? [{
951
+ id: `${message.id}_verification`, role: 'user', content: applicability, timestamp: message.timestamp,
952
+ metadata: { synthetic: true, hostContext: true },
953
+ }] : [])];
954
+ });
955
+ if (this.config.pinTaskHeader && this.taskHandoff) {
956
+ const snapshot = this.taskHandoff.snapshot;
957
+ // Saved user/tool text retains conversation priority, alongside the other
958
+ // host observations, without changing a hosted thread's base instructions.
959
+ let currentUserIndex = contextMessages.length - 1;
960
+ for (; currentUserIndex >= 0; currentUserIndex--) {
961
+ const message = contextMessages[currentUserIndex];
962
+ if (message.role === 'user' && !message.metadata?.synthetic && !message.metadata?.hostContext
963
+ && !message.metadata?.toolResult && !message.metadata?.compactionSummary)
964
+ break;
965
+ }
966
+ // Historical context belongs before the current instruction. Never split
967
+ // an assistant/tool-result pair or replace the request tail with history.
968
+ contextMessages.splice(currentUserIndex < 0 ? 0 : currentUserIndex, 0, {
969
+ id: `handoff_${snapshot.source.sessionId}_${snapshot.revision}`,
970
+ role: 'user', content: formatTaskHandoffContext(snapshot),
971
+ timestamp: new Date(snapshot.source.capturedAt),
972
+ metadata: { synthetic: true, hostContext: true, preserveUserBoundary: true }
973
+ });
974
+ }
975
+ const system = this.systemWithTaskHeader(messages);
976
+ const tools = getRequestToolDefinitions(availableTools, this.discoveredTools, this.config.deferToolSchemas !== false, this.alwaysLoadedToolNames);
977
+ this._nativeContextId = this.backend.preservesNativeHistory ? nativeHistoryContextId(system, tools) : undefined;
718
978
  return {
719
979
  model: this.config.model,
720
980
  maxTokens: this.config.maxTokens,
721
- system: this.systemWithTaskHeader(messages),
981
+ system,
722
982
  messages: this.formatMessages(contextMessages),
723
- tools: getRequestToolDefinitions(availableTools, this.discoveredTools, this.config.deferToolSchemas !== false, this.alwaysLoadedToolNames),
983
+ tools,
724
984
  // A container pin identifies a cached prompt prefix, so the transport has to
725
985
  // know when this conversation is no longer sending the prefix its container
726
986
  // was warmed for. That is compaction, and only this layer can see it.
@@ -779,12 +1039,16 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
779
1039
  * visible text, so an empty or whitespace-only response emits no stream
780
1040
  * events (EPO-622), matching emitStreamText.
781
1041
  */
782
- async streamAndAssemble(request, signal, onAnswerOutput) {
1042
+ async streamAndAssemble(request, signal, onAnswerOutput, onUsage) {
783
1043
  let text = '';
784
1044
  let reasoning = '';
785
1045
  const toolUses = [];
1046
+ const nativeContent = [];
786
1047
  let usage;
1048
+ let reportedUsage;
1049
+ let costUsd;
787
1050
  let stopReason = 'end_turn';
1051
+ let stopDetails;
788
1052
  let streamOpen = false;
789
1053
  // Text that arrived but is still whitespace only. Providers open a turn
790
1054
  // this way — DeepSeek streams "\n\n" before its tool calls — and each delta
@@ -802,14 +1066,22 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
802
1066
  switch (ev.type) {
803
1067
  case 'heartbeat':
804
1068
  break;
1069
+ case 'content_block':
1070
+ nativeContent.push(ev.block);
1071
+ break;
805
1072
  case 'reasoning_delta':
806
1073
  if (ev.text) {
807
1074
  reasoning += ev.text;
808
- this.emit({ type: 'reasoning_delta', content: ev.text });
1075
+ if (!this.remoteResourceSession)
1076
+ this.emit({ type: 'reasoning_delta', content: ev.text });
809
1077
  }
810
1078
  break;
811
1079
  case 'text_delta':
812
1080
  if (ev.text) {
1081
+ if (this.remoteResourceSession) {
1082
+ text += ev.text;
1083
+ break;
1084
+ }
813
1085
  if (streamOpen) {
814
1086
  text += ev.text;
815
1087
  onAnswerOutput();
@@ -849,11 +1121,31 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
849
1121
  }
850
1122
  case 'tool_use_end':
851
1123
  break;
852
- case 'usage':
1124
+ case 'usage': {
853
1125
  usage = ev.usage;
1126
+ // Usage snapshots are cumulative for this request. Report increments
1127
+ // as they arrive so cancellation cannot discard already billed work.
1128
+ onUsage?.({
1129
+ ...usage,
1130
+ inputTokens: Math.max(0, usage.inputTokens - (reportedUsage?.inputTokens ?? 0)),
1131
+ outputTokens: Math.max(0, usage.outputTokens - (reportedUsage?.outputTokens ?? 0)),
1132
+ totalTokens: Math.max(0, usage.totalTokens - (reportedUsage?.totalTokens ?? 0)),
1133
+ });
1134
+ reportedUsage = {
1135
+ ...usage,
1136
+ inputTokens: Math.max(usage.inputTokens, reportedUsage?.inputTokens ?? 0),
1137
+ outputTokens: Math.max(usage.outputTokens, reportedUsage?.outputTokens ?? 0),
1138
+ totalTokens: Math.max(usage.totalTokens, reportedUsage?.totalTokens ?? 0),
1139
+ };
1140
+ break;
1141
+ }
1142
+ case 'cost':
1143
+ if (Number.isFinite(ev.costUsd) && ev.costUsd >= 0)
1144
+ costUsd = ev.costUsd;
854
1145
  break;
855
1146
  case 'done':
856
1147
  stopReason = ev.stopReason;
1148
+ stopDetails = ev.stopDetails;
857
1149
  break;
858
1150
  }
859
1151
  }
@@ -879,7 +1171,9 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
879
1171
  }
880
1172
  content.push({ type: 'tool_use', id: tu.id, name: tu.name, input });
881
1173
  }
882
- return { content, stopReason, reasoning: reasoning || undefined, usage };
1174
+ return { content: nativeContent.length > 0 ? nativeContent : content, stopReason,
1175
+ ...(stopDetails ? { stopDetails } : {}), reasoning: reasoning || undefined, usage,
1176
+ ...(costUsd === undefined ? {} : { costUsd }) };
883
1177
  }
884
1178
  emitReasoningText(text) {
885
1179
  if (!text || !text.trim()) {
@@ -894,7 +1188,28 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
894
1188
  * Server). The action has already run, so a command emits tool_start
895
1189
  * immediately followed by tool_end.
896
1190
  */
897
- emitObservation(observation) {
1191
+ async emitObservation(observation, nativeTurnId, canPublish) {
1192
+ // Provider observations have no host-managed operation receipt.
1193
+ if (this.remoteResourceSession)
1194
+ return;
1195
+ if (observation.kind !== 'reasoning') {
1196
+ const captured = captureNativeReport(observation, { provider: this.config.runtimeFacts.provider ?? 'unknown-provider', turnId: nativeTurnId });
1197
+ if (!captured)
1198
+ return;
1199
+ this.nativeEvidenceCaptureDepth++;
1200
+ try {
1201
+ this.toolResponseStore.put(captured.handle, `provider_${observation.kind}`, captured.content, captured.partial, { source: captured.source, isError: captured.isError });
1202
+ await this.toolResponseStore.flush();
1203
+ }
1204
+ finally {
1205
+ this.nativeEvidenceCaptureDepth--;
1206
+ }
1207
+ this.emit({ type: 'evidence', sessionId: this.config.sessionId, snapshot: this.getEvidenceSnapshot() });
1208
+ }
1209
+ // Retain admitted original source facts even when persistence finishes
1210
+ // after cancellation; visible progress belongs to its producing request.
1211
+ if (!canPublish())
1212
+ return;
898
1213
  switch (observation.kind) {
899
1214
  case 'reasoning':
900
1215
  this.emitReasoningText(observation.text);
@@ -944,6 +1259,17 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
944
1259
  * genuinely multi-task long-lived agents is deferred (plan A10).
945
1260
  */
946
1261
  _taskHeader = null;
1262
+ taskHandoff = null;
1263
+ /** Only a host-selected checkpoint may supply historical task context. */
1264
+ setTaskHandoff(projection) {
1265
+ const admitted = projection === null ? null : parseTaskHandoffProjection(projection);
1266
+ if (projection !== null && !admitted)
1267
+ throw new TypeError('Invalid task handoff projection');
1268
+ if (admitted && this.config.sessionId && admitted.snapshot.source.sessionId !== this.config.sessionId) {
1269
+ throw new TypeError('Task handoff belongs to a different selected session');
1270
+ }
1271
+ this.taskHandoff = admitted;
1272
+ }
947
1273
  /**
948
1274
  * Drop the explicitly pinned task for the next conversation. Wired into the /clear-only path (clearTransientState), never into
949
1275
  * /compact — the pin must survive compaction (that is the whole point of R7).
@@ -971,6 +1297,10 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
971
1297
  /** Append the pinned task header (R7) to the static system prompt. */
972
1298
  getExecutionContext() {
973
1299
  return {
1300
+ permissions: this.config.runtimeFacts.permissions,
1301
+ ...(this.remoteResourceSession ? { remoteResourceGuard: {
1302
+ ownerId: this.remoteResourceSession.ownerId, resources: this.remoteResourceSession.resources.map(resource => resource.id),
1303
+ } } : {}),
974
1304
  mode: this.config.runtimeFacts.mode ?? 'normal', cwd: this._cwd,
975
1305
  tools: this.getConfiguredToolNames(), mux: this.config.runtimeFacts.mux ?? false,
976
1306
  configScope: this.config.runtimeFacts.configScope ?? 'host configuration',
@@ -985,26 +1315,114 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
985
1315
  return this.config.systemPrompt;
986
1316
  return pinnedTaskSystem(this.config.systemPrompt, this._taskHeader);
987
1317
  }
988
- /**
989
- * Execute one terminal turn. A host binding makes the remote conversation the
990
- * authoritative history and mirrors the accepted user/assistant pair around
991
- * the same core loop used by unhosted ZTC.
992
- */
993
- async run(messages, signal, takeSteering) {
1318
+ async persistToolCheckpoint(phase, toolCallId, messages, handler = this.toolCheckpoint) {
1319
+ if (this.toolCheckpointFailure)
1320
+ throw this.toolCheckpointFailure;
1321
+ if (!handler) {
1322
+ if (phase === 'terminal')
1323
+ await this.toolResponseStore.flush();
1324
+ return;
1325
+ }
1326
+ // Capture at enqueue time: a later sibling completion or a host retaining
1327
+ // this payload cannot rewrite the arguments admitted for this invocation.
1328
+ const checkpoint = { sessionId: this.config.sessionId || 'ztc-session', phase,
1329
+ toolCallId, messages: structuredClone(messages) };
1330
+ const pending = this.toolCheckpointTail.then(async () => {
1331
+ if (this.toolCheckpointFailure)
1332
+ throw this.toolCheckpointFailure;
1333
+ try {
1334
+ if (phase === 'terminal')
1335
+ await this.toolResponseStore.flush();
1336
+ await handler(structuredClone(checkpoint));
1337
+ }
1338
+ catch (error) {
1339
+ this.failedToolCheckpoint = { checkpoint, handler };
1340
+ this.toolCheckpointFailure = this.checkpointPersistenceError(checkpoint, error);
1341
+ throw this.toolCheckpointFailure;
1342
+ }
1343
+ });
1344
+ this.toolCheckpointTail = pending.catch(() => { });
1345
+ await pending;
1346
+ }
1347
+ checkpointPersistenceError(checkpoint, error) {
1348
+ return new AgentError(`Unable to persist ${checkpoint.phase === 'terminal' ? 'terminal outcome' : 'call intent'} checkpoint for ${checkpoint.toolCallId}: ${error instanceof Error ? error.message : String(error)}. Stop this turn and inspect the original checkpoint and existing receipts before deciding whether to repeat any work.`, 'TOOL_CHECKPOINT_FAILED');
1349
+ }
1350
+ /** Retry persistence only, before a new public invocation can admit effects. */
1351
+ async recoverToolCheckpoint() {
1352
+ await this.toolCheckpointTail;
1353
+ if (!this.toolCheckpointFailure)
1354
+ return;
1355
+ const failed = this.failedToolCheckpoint;
1356
+ if (!failed)
1357
+ throw this.toolCheckpointFailure;
1358
+ const pending = this.toolCheckpointTail.then(async () => {
1359
+ // Another entrant can have repaired this exact record while we queued.
1360
+ if (this.failedToolCheckpoint !== failed)
1361
+ return;
1362
+ try {
1363
+ if (failed.checkpoint.phase === 'terminal')
1364
+ await this.toolResponseStore.flush();
1365
+ await failed.handler(structuredClone(failed.checkpoint));
1366
+ this.failedToolCheckpoint = null;
1367
+ this.toolCheckpointFailure = null;
1368
+ }
1369
+ catch (error) {
1370
+ this.toolCheckpointFailure = this.checkpointPersistenceError(failed.checkpoint, error);
1371
+ throw this.toolCheckpointFailure;
1372
+ }
1373
+ });
1374
+ this.toolCheckpointTail = pending.catch(() => { });
1375
+ await pending;
1376
+ }
1377
+ /** Execute a host-bound or standalone turn through the same core loop. */
1378
+ async run(messages, signal, takeSteering, toolCheckpoint = this.toolCheckpoint, steeringAdmission, onUsage) {
1379
+ const diagnosticTurn = { id: `turn_${randomUUID()}`, iterations: 0 };
1380
+ let result;
1381
+ let failure;
994
1382
  const onInterrupt = () => { if (this.config.execution.interruptBackgroundJobs)
995
1383
  void interruptCommandJobs(this.config.sessionId); };
996
1384
  signal?.addEventListener('abort', onInterrupt, { once: true });
997
1385
  try {
998
- return await this.runBound(messages, signal, takeSteering);
1386
+ await this.recoverToolCheckpoint();
1387
+ await this.nativeEvidencePending;
1388
+ if (this.nativeEvidenceFailure)
1389
+ throw this.nativeEvidenceFailure;
1390
+ result = await this.runBound(messages, signal, takeSteering, toolCheckpoint, steeringAdmission, diagnosticTurn, onUsage);
1391
+ return result;
1392
+ }
1393
+ catch (error) {
1394
+ failure = error;
1395
+ throw error;
999
1396
  }
1000
1397
  finally {
1001
1398
  signal?.removeEventListener('abort', onInterrupt);
1399
+ try {
1400
+ await this.nativeEvidencePending;
1401
+ if (this.nativeEvidenceFailure)
1402
+ throw this.nativeEvidenceFailure;
1403
+ await this.toolResponseStore.flush();
1404
+ }
1405
+ catch (error) {
1406
+ failure = error;
1407
+ throw error;
1408
+ }
1409
+ finally {
1410
+ const errorCode = failure instanceof AgentError ? failure.code ?? null : null;
1411
+ const reason = failure !== undefined
1412
+ ? signal?.aborted || errorCode === 'CANCELLED' ? 'cancelled' : 'error'
1413
+ : result?.stopReason ?? 'error';
1414
+ const data = { reason, errorCode, iterations: diagnosticTurn.iterations };
1415
+ // This outcome belongs to the invocation, even after a UI cancels its
1416
+ // event subscription or a hosted driver fails before the model loop.
1417
+ this.recordDiagnosticEvent('turn_end', { ...data, turnId: diagnosticTurn.id });
1418
+ this.tracer.event({ type: 'turn_end', timestamp: new Date(), data });
1419
+ }
1002
1420
  }
1003
1421
  }
1004
- async runBound(messages, signal, takeSteering) {
1422
+ async runBound(messages, signal, takeSteering, toolCheckpoint = this.toolCheckpoint, steeringAdmission, diagnosticTurn, onUsage) {
1005
1423
  const binding = this.#conversationBinding;
1006
1424
  if (!binding)
1007
- return this.runCore(messages, signal, takeSteering);
1425
+ return this.runCore(messages, signal, takeSteering, toolCheckpoint, steeringAdmission, diagnosticTurn, onUsage);
1008
1426
  const userMessage = [...messages].reverse().find((message) => message.role === 'user'
1009
1427
  && !message.metadata?.synthetic
1010
1428
  && !message.metadata?.toolResult);
@@ -1036,14 +1454,15 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1036
1454
  // The host driver publishes only the initial user and final assistant
1037
1455
  // for this turn. Leave busy input pending for a new hosted turn so the
1038
1456
  // authoritative conversation cannot silently omit it.
1039
- const result = await this.runCore(workingMessages, signal);
1457
+ const result = await this.runCore(workingMessages, signal, undefined, toolCheckpoint, undefined, diagnosticTurn, onUsage);
1040
1458
  const assistantMessage = {
1041
1459
  id: `host_assistant_${randomUUID()}`,
1042
1460
  role: 'assistant',
1043
1461
  content: result.content,
1462
+ ...(result.nativeContent ? { nativeContent: result.nativeContent, nativeModel: result.nativeModel, nativeContextId: result.nativeContextId } : {}),
1044
1463
  timestamp: new Date(),
1045
1464
  ...(result.toolCalls.length > 0 ? { toolCalls: result.toolCalls } : {}),
1046
- ...(result.reasoning ? { metadata: { reasoningContent: result.reasoning } } : {}),
1465
+ metadata: { ...(this.config.runtimeFacts.mode === 'plan' ? { planMode: true } : {}), ...(result.reasoning ? { reasoningContent: result.reasoning } : {}) },
1047
1466
  };
1048
1467
  const receipt = await binding.driver.publish({
1049
1468
  turn,
@@ -1082,10 +1501,18 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1082
1501
  }
1083
1502
  }
1084
1503
  // Main execution loop shared by hosted and standalone turns.
1085
- async runCore(messages, signal, takeSteering) {
1504
+ async runCore(messages, signal, takeSteering, toolCheckpoint = this.toolCheckpoint, steeringAdmission, diagnosticTurn, onUsage) {
1086
1505
  if (!this.hasApiKey()) {
1087
1506
  throw new AgentError('No API key configured. Use /config key <key> to set one.', 'NO_API_KEY');
1088
1507
  }
1508
+ for (const message of messages) {
1509
+ if (message.role !== 'assistant')
1510
+ continue;
1511
+ for (const call of message.toolCalls ?? []) {
1512
+ if (!this.recordedToolCalls.has(call.id))
1513
+ this.recordedToolCalls.set(call.id, structuredClone(call));
1514
+ }
1515
+ }
1089
1516
  const allToolCalls = [];
1090
1517
  const turnMessages = [];
1091
1518
  let iterations = 0;
@@ -1098,6 +1525,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1098
1525
  // "as-is, including any reasoning_content"; without this the trace survives
1099
1526
  // only inside one run() and every later user turn sends a stripped history.
1100
1527
  let finalReasoning;
1528
+ let finalNativeContent;
1101
1529
  let lastToolTurnText = '';
1102
1530
  let totalUsage = null;
1103
1531
  let runError = null;
@@ -1118,11 +1546,14 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1118
1546
  throw new AgentError('Cancelled', 'CANCELLED');
1119
1547
  }
1120
1548
  iterations++;
1549
+ if (diagnosticTurn)
1550
+ diagnosticTurn.iterations = iterations;
1121
1551
  this.emit({ type: 'thinking_start' });
1122
1552
  try {
1123
1553
  // Call the API
1124
1554
  const availability = await Promise.all(this.config.tools.map(tool => tool.isAvailable?.({ cwd: this._cwd }) ?? true));
1125
1555
  const availableTools = this.config.tools.filter((_tool, index) => availability[index]);
1556
+ await this.refreshVerificationCoverage();
1126
1557
  const request = this.buildRequest(messages, availableTools);
1127
1558
  request.signal = signal;
1128
1559
  request.onMetrics = metrics => {
@@ -1131,9 +1562,55 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1131
1562
  };
1132
1563
  // Providers that run their own tool loop (Codex App Server) report each
1133
1564
  // command/reasoning/file action here so it shows in the mirror/TUI.
1134
- request.onObservation = (observation) => this.emitObservation(observation);
1135
- this.tracer.event({ type: 'api_request', timestamp: new Date(), data: { model: request.model, maxTokens: request.maxTokens, system: request.system, messages: request.messages, tools: request.tools } });
1136
- const response = await this.callAPI(request, signal);
1565
+ const nativeTurnId = `native_request_${randomUUID()}`;
1566
+ const captureNativeObservation = (observation, attemptCanPublish) => {
1567
+ const capture = this.emitObservation(observation, nativeTurnId, attemptCanPublish);
1568
+ this.nativeEvidencePending = Promise.all([this.nativeEvidencePending, capture]).then(() => { }).catch(error => {
1569
+ this.nativeEvidenceFailure ??= new AgentError(`Unable to retain native provider report: ${error instanceof Error ? error.message : String(error)}. Inspect existing reports before repeating work.`, 'NATIVE_EVIDENCE_FAILED');
1570
+ });
1571
+ return capture;
1572
+ };
1573
+ this.tracer.event({ type: 'api_request', timestamp: new Date(), data: { turnId: diagnosticTurn?.id ?? null,
1574
+ model: request.model, maxTokens: request.maxTokens, system: request.system, messages: request.messages, tools: request.tools } });
1575
+ // One identity covers this logical call, including its internal transport retries.
1576
+ // Selected account pointers are not evidence of which account served the request.
1577
+ const identity = { id: `usage_${randomUUID()}`, sessionId: this.config.sessionId,
1578
+ projectHash: createHash('sha256').update(this._cwd).digest('hex').slice(0, 12),
1579
+ provider: this.config.runtimeFacts.provider ?? null, model: request.model, accountId: null };
1580
+ const retainReturnedAccounting = (response) => {
1581
+ const rawContent = response.content;
1582
+ const accountingResponse = { ...response, content: Array.isArray(rawContent) ? rawContent
1583
+ : typeof rawContent === 'string' && rawContent ? [{ type: 'text', text: rawContent }] : [] };
1584
+ const usage = response.usage ?? this.estimateUsage(request, accountingResponse);
1585
+ this.retainUsageReceipt({ ...identity, observedAt: new Date().toISOString(),
1586
+ source: usage.estimated === true ? 'estimated' : 'reported', usage,
1587
+ costUsd: typeof response.costUsd === 'number' && Number.isFinite(response.costUsd) && response.costUsd >= 0
1588
+ ? response.costUsd : null });
1589
+ return usage;
1590
+ };
1591
+ const requestUsage = onUsage?.forRequest?.() ?? onUsage;
1592
+ let response;
1593
+ let returnedResponse;
1594
+ try {
1595
+ response = await this.callAPI(request, captureNativeObservation, signal, { messages, turnMessages, toolCalls: allToolCalls, handler: toolCheckpoint }, observed => { returnedResponse = observed; }, requestUsage);
1596
+ }
1597
+ catch (error) {
1598
+ // A success metrics observer may cancel synchronously just before the
1599
+ // completed response promise wins the race. Its returned facts remain known.
1600
+ if (returnedResponse)
1601
+ retainReturnedAccounting(returnedResponse);
1602
+ else
1603
+ this.retainUsageReceipt({ ...identity, observedAt: new Date().toISOString(),
1604
+ source: 'unavailable', usage: null, costUsd: null });
1605
+ throw error;
1606
+ }
1607
+ // Retain returned accounting before a later cancellation/checkpoint guard can discard the answer.
1608
+ const usage = retainReturnedAccounting(response);
1609
+ await this.nativeEvidencePending;
1610
+ if (this.nativeEvidenceFailure)
1611
+ throw this.nativeEvidenceFailure;
1612
+ if (this.toolCheckpointFailure)
1613
+ throw this.toolCheckpointFailure;
1137
1614
  if (signal?.aborted) {
1138
1615
  throw new AgentError('Cancelled', 'CANCELLED');
1139
1616
  }
@@ -1144,16 +1621,19 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1144
1621
  ? [{ type: 'text', text: raw }]
1145
1622
  : [];
1146
1623
  }
1147
- response.content = normalizeRepeatedResponseContent(response.content);
1148
- this.tracer.event({ type: 'api_response', timestamp: new Date(), data: { stopReason: response.stopReason, content: response.content, usage: response.usage } });
1624
+ if (!this.backend.preservesNativeHistory)
1625
+ response.content = normalizeRepeatedResponseContent(response.content);
1626
+ this.tracer.event({ type: 'api_response', timestamp: new Date(), data: { turnId: diagnosticTurn?.id ?? null,
1627
+ model: request.model, stopReason: response.stopReason, stopReasonSource: 'backend_response', content: response.content, usage: response.usage } });
1149
1628
  // Forward ZRouter prompt echo if present (superuser-authorized by the server)
1150
1629
  if (response.promptEcho) {
1151
1630
  this.tracer.event({ type: 'prompt_echo', timestamp: new Date(), data: response.promptEcho });
1152
1631
  }
1153
1632
  // Skip when the response was streamed live (reasoning_delta already emitted).
1154
- if (!this._lastResponseStreamed)
1633
+ if (!this.remoteResourceSession && !this._lastResponseStreamed)
1155
1634
  this.emitReasoningText(response.reasoning);
1156
- const usage = response.usage ?? this.estimateUsage(request, response);
1635
+ if (!response.usage)
1636
+ requestUsage?.(usage);
1157
1637
  totalUsage = mergeTokenUsage(totalUsage, usage);
1158
1638
  // Durable context = real input_tokens minus the current-turn tool-result
1159
1639
  // bytes that get stubbed next turn. Without this the bar spikes on a big
@@ -1173,12 +1653,59 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1173
1653
  if (isIncompleteModelResponse(response.stopReason)) {
1174
1654
  throw new AgentError(incompleteModelResponseMessage(response.stopReason, request.maxTokens, response.usage?.outputTokens), INCOMPLETE_MODEL_RESPONSE_CODE);
1175
1655
  }
1656
+ if (response.stopReason === 'refusal') {
1657
+ throw new AgentError('The model declined this request.', 'MODEL_REFUSAL');
1658
+ }
1176
1659
  // Check for tool use
1177
- const toolUseBlocks = response.content.filter(this.isToolUseBlock);
1660
+ const requestedToolBlocks = response.content.filter(this.isToolUseBlock);
1661
+ const requestedIds = new Set();
1662
+ for (const block of requestedToolBlocks) {
1663
+ if (requestedIds.has(block.id)) {
1664
+ throw new AgentError(`Provider response contains duplicate tool invocation ID ${block.id}; no call in this batch was executed.`, 'DUPLICATE_TOOL_CALL_ID');
1665
+ }
1666
+ requestedIds.add(block.id);
1667
+ }
1668
+ // Provider IDs identify calls within this response, not operations for
1669
+ // the lifetime of the Agent. Opaque IDs outside the store's bounded
1670
+ // filename alphabet and reused IDs need a fresh canonical identity for
1671
+ // transcript pairing, receipts and immutable stored result bytes.
1672
+ // Explicit host executeHarnessToolCall retries retain their own replay
1673
+ // contract; provider response reuse must never replay those outcomes.
1674
+ const occupiedIds = new Set([...this.recordedToolCalls.keys(), ...requestedIds]);
1675
+ const toolUseBlocks = [];
1676
+ for (const block of requestedToolBlocks) {
1677
+ if (/^[A-Za-z0-9_-]{1,256}$/.test(block.id)
1678
+ && !this.recordedToolCalls.has(block.id) && !await this.hasStoredToolIdentity(block.id)) {
1679
+ toolUseBlocks.push(block);
1680
+ continue;
1681
+ }
1682
+ let id;
1683
+ let identityAttempts = 0;
1684
+ do {
1685
+ if (++identityAttempts > 16) {
1686
+ throw new AgentError('Unable to allocate a fresh tool invocation identity. Inspect retained source availability before retrying.', 'TOOL_CALL_IDENTITY_UNAVAILABLE');
1687
+ }
1688
+ id = `tool_${randomUUID()}`;
1689
+ } while (occupiedIds.has(id) || await this.hasStoredToolIdentity(id));
1690
+ occupiedIds.add(id);
1691
+ toolUseBlocks.push({ ...block, id });
1692
+ }
1693
+ if (this.backend.preservesNativeHistory && toolUseBlocks.length > 0) {
1694
+ // Native replay keeps opaque signed blocks unchanged. Its tool-use
1695
+ // IDs still need to pair with the canonical identities admitted above.
1696
+ const canonicalIds = new Map(requestedToolBlocks.map((block, index) => [block.id, toolUseBlocks[index].id]));
1697
+ response.content = response.content.map(block => this.isToolUseBlock(block)
1698
+ ? { ...block, id: canonicalIds.get(block.id) } : block);
1699
+ }
1178
1700
  if (toolUseBlocks.length === 0) {
1701
+ if (this.backend.preservesNativeHistory)
1702
+ finalNativeContent = response.content;
1179
1703
  // No tools, extract text
1180
1704
  const textBlocks = response.content.filter(this.isTextBlock);
1181
- const iterationText = textBlocks.map(b => b.text).join('\n');
1705
+ const iterationText = this.remoteResourceSession
1706
+ ? allToolCalls.filter(call => call.name === 'remote_resource' && call.status === 'complete')
1707
+ .flatMap(call => remoteDeletionReport(call.result ?? '') ?? []).join('\n') || NO_VERIFIED_DELETION_REPORT
1708
+ : textBlocks.map(b => b.text).join('\n');
1182
1709
  // Last non-empty text wins. An empty no-tool iteration (e.g. the
1183
1710
  // follow-up turn after an auto-continue prompt) must NOT clobber text
1184
1711
  // an earlier iteration already produced and streamed — otherwise run()
@@ -1187,16 +1714,49 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1187
1714
  // (ZTC-28). Mirrors the tool-branch lastToolTurnText guard below.
1188
1715
  if (iterationText.trim()) {
1189
1716
  finalContent = iterationText;
1717
+ finalReasoning = this.remoteResourceSession ? undefined : response.reasoning;
1718
+ }
1719
+ else if (!this.remoteResourceSession && response.reasoning?.trim()) {
1720
+ // Reasoning-only output is observable activity too. Keep it without
1721
+ // inventing text or losing earlier text on an empty follow-up.
1190
1722
  finalReasoning = response.reasoning;
1191
1723
  }
1192
1724
  // Skip the buffered pacing when the text was already streamed live.
1193
- if (!this._lastResponseStreamed)
1725
+ if (this.remoteResourceSession ? lastToolTurnText !== iterationText : !this._lastResponseStreamed)
1194
1726
  await this.emitStreamText(iterationText, signal);
1727
+ // Deliver busy input before automatic continuation or ending this turn.
1728
+ // At the iteration cap leave the queue intact for the renderer's next turn.
1729
+ if (iterations < this.config.maxIterations) {
1730
+ const steering = takeSteering?.() ?? [];
1731
+ if (steering.length > 0) {
1732
+ if (iterationText.trim()) {
1733
+ const answer = {
1734
+ id: `assistant_${Date.now()}_${iterations}`, role: 'assistant', content: iterationText, timestamp: new Date(),
1735
+ turn: requestUsage?.getTurn?.(),
1736
+ metadata: { ...(this.config.runtimeFacts.mode === 'plan' ? { planMode: true } : {}), ...(response.reasoning ? { reasoningContent: response.reasoning } : {}) },
1737
+ };
1738
+ messages.push(answer);
1739
+ turnMessages.push(answer);
1740
+ }
1741
+ messages.push(...steering);
1742
+ turnMessages.push(...steering);
1743
+ this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
1744
+ if (signal?.aborted)
1745
+ throw new AgentError('Cancelled', 'CANCELLED');
1746
+ await steeringAdmission?.({ messages: [...turnMessages], sourceMessageIds: steering.map(message => message.id),
1747
+ deliveryState: 'unknown', signal });
1748
+ if (signal?.aborted)
1749
+ throw new AgentError('Cancelled', 'CANCELLED');
1750
+ finalContent = '';
1751
+ finalReasoning = undefined;
1752
+ continue;
1753
+ }
1754
+ }
1195
1755
  // Check if we should auto-continue. Bounded: every continuation is a
1196
1756
  // full-context request, so a model that keeps narrating an intention
1197
1757
  // instead of calling a tool must not be able to spend maxIterations
1198
1758
  // round trips on it. The budget is reset by any executed tool.
1199
- if (this.shouldContinue(iterationText)
1759
+ if (!this.remoteResourceSession && this.shouldContinue(iterationText)
1200
1760
  && this._consecutiveAutoContinues < this.config.maxConsecutiveAutoContinues) {
1201
1761
  this._consecutiveAutoContinues++;
1202
1762
  const acTodos = getLatestTodos();
@@ -1214,8 +1774,9 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1214
1774
  id: `assistant_${Date.now()}`,
1215
1775
  role: 'assistant',
1216
1776
  content: iterationText,
1777
+ ...(this.backend.preservesNativeHistory ? { nativeContent: structuredClone(response.content), nativeModel: this.config.model, nativeContextId: this._nativeContextId } : {}),
1217
1778
  timestamp: new Date(),
1218
- ...(response.reasoning ? { metadata: { reasoningContent: response.reasoning } } : {}),
1779
+ metadata: { ...(this.config.runtimeFacts.mode === 'plan' ? { planMode: true } : {}), ...(response.reasoning ? { reasoningContent: response.reasoning } : {}) },
1219
1780
  });
1220
1781
  // Add auto-continue prompt as user message
1221
1782
  messages.push({
@@ -1229,42 +1790,92 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1229
1790
  stopReason = 'complete';
1230
1791
  break;
1231
1792
  }
1232
- // Per-batch dedup for `recall_tool_response`: two recalls of the same
1233
- // inner tool_use_id in one assistant turn share a single execute. The
1234
- // outer toolBlock.id pairing is preserved for each block so the
1235
- // tool_use ↔ tool_result API invariant holds.
1793
+ // Identical no-range recall proposals may share an execution. Explicit
1794
+ // ranges need their own bytes and validation, and even normalized source
1795
+ // IDs must preserve each invocation's original arguments and pairing.
1236
1796
  const sharedRecallExec = new Map();
1237
- const recordToolSuccess = (toolBlock, toolCall, content, effectiveArgs) => {
1797
+ const toolUseMessageId = `assistant_${randomUUID()}`;
1798
+ const batchCalls = toolUseBlocks.map(block => ({
1799
+ id: block.id, name: block.name, args: structuredClone(block.input), status: 'pending',
1800
+ }));
1801
+ for (const call of batchCalls)
1802
+ this.recordedToolCalls.set(call.id, call);
1803
+ const toolUseMessage = {
1804
+ id: toolUseMessageId, role: 'assistant',
1805
+ content: this.remoteResourceSession ? '[Using tools...]'
1806
+ : response.content.filter(this.isTextBlock).map(block => block.text).join('\n') || '[Using tools...]',
1807
+ ...(this.backend.preservesNativeHistory ? { nativeContent: structuredClone(response.content), nativeModel: this.config.model, nativeContextId: this._nativeContextId } : {}),
1808
+ timestamp: new Date(), toolCalls: batchCalls,
1809
+ metadata: { toolCheckpoint: true, ...(this.config.runtimeFacts.mode === 'plan' ? { planMode: true } : {}), ...(response.reasoning ? this.remoteResourceSession
1810
+ ? { providerReasoning: response.reasoning } : { reasoningContent: response.reasoning } : {}) },
1811
+ };
1812
+ messages.push(toolUseMessage);
1813
+ turnMessages.push(toolUseMessage);
1814
+ allToolCalls.push(...batchCalls);
1815
+ const toolResultMessage = {
1816
+ id: `tool_${randomUUID()}`, role: 'user', content: '[]', timestamp: new Date(),
1817
+ metadata: { toolResult: true, toolCheckpoint: true },
1818
+ };
1819
+ const completedResults = new Map();
1820
+ const retainToolResult = async (toolCall, rawContent, guardedContent, managedCommand, contentTruncated = false, verificationReceipt, verifierDecision, benchmarkComparisonDecision) => {
1821
+ const partial = Boolean(signal?.aborted || contentTruncated || managedCommand?.result.has_more || managedCommand?.result.output_truncated);
1822
+ const { rendered } = this.toolResponseStore.put(toolCall.id, toolCall.name, rawContent, partial, {
1823
+ args: toolCall.args,
1824
+ sourcePath: extractSourcePath(toolCall.name, toolCall.args), dedupable: toolCall.status === 'complete',
1825
+ isError: toolCall.status === 'error',
1826
+ source: { ...this.evidenceSource(toolCall.id, toolCall.name, toolCall.args, managedCommand, verificationReceipt, partial || toolCall.status === 'error' ? undefined : verifierDecision, partial || toolCall.status === 'error' ? undefined : benchmarkComparisonDecision), messageId: toolUseMessageId },
1827
+ });
1828
+ const annotation = guardedContent.slice(rawContent.length);
1829
+ completedResults.set(toolCall.id, { tool_use_id: toolCall.id, content: rendered,
1830
+ is_error: toolCall.status === 'error', ...(annotation ? { context_annotation: annotation } : {}) });
1831
+ toolResultMessage.content = JSON.stringify(batchCalls.flatMap(call => completedResults.has(call.id) ? [completedResults.get(call.id)] : []));
1832
+ if (!turnMessages.some(message => message.id === toolResultMessage.id)) {
1833
+ messages.push(toolResultMessage);
1834
+ turnMessages.push(toolResultMessage);
1835
+ }
1836
+ await this.refreshVerificationCoverage();
1837
+ await this.persistToolCheckpoint('terminal', toolCall.id, turnMessages, toolCheckpoint);
1838
+ };
1839
+ const recordToolSuccess = async (toolBlock, toolCall, content, effectiveArgs, managedCommand, contentTruncated = false, verificationReceipt, verifierDecision, benchmarkComparisonDecision) => {
1238
1840
  toolCall.completedAt = new Date();
1239
1841
  toolCall.args = effectiveArgs;
1240
1842
  toolCall.status = 'complete';
1241
1843
  toolCall.result = content;
1844
+ const guarded = applyRepeatGuard(toolBlock.name, toolBlock.input, content, managedCommand);
1845
+ await retainToolResult(toolCall, content, guarded, managedCommand, contentTruncated, verificationReceipt, verifierDecision, benchmarkComparisonDecision);
1242
1846
  this.emit({ type: 'tool_end', tool: toolBlock.name, result: content, toolCallId: toolBlock.id });
1243
1847
  this.tracer.event({
1244
1848
  type: 'tool_end',
1245
1849
  timestamp: new Date(),
1246
- data: { tool: toolBlock.name },
1850
+ data: this.toolTraceData(toolCall),
1247
1851
  });
1248
- const guarded = applyRepeatGuard(toolBlock.name, toolBlock.input, content);
1249
- return { toolCall, toolResult: { tool_use_id: toolBlock.id, content: guarded, is_error: false } };
1852
+ return { toolCall, managedCommand, contentTruncated, verificationReceipt, verifierDecision, benchmarkComparisonDecision, rawContent: content, toolResult: { tool_use_id: toolBlock.id, content: guarded, is_error: false } };
1250
1853
  };
1251
- const recordToolError = (toolBlock, toolCall, error, effectiveArgs) => {
1854
+ const recordToolError = async (toolBlock, toolCall, error, effectiveArgs, managedCommand, contentTruncated = false, verificationReceipt, verifierDecision, benchmarkComparisonDecision) => {
1252
1855
  toolCall.completedAt = new Date();
1253
1856
  if (effectiveArgs !== undefined)
1254
1857
  toolCall.args = effectiveArgs;
1255
1858
  toolCall.status = 'error';
1256
1859
  toolCall.error = error;
1860
+ const guarded = applyRepeatGuard(toolBlock.name, toolBlock.input, `Error: ${error}`);
1861
+ await retainToolResult(toolCall, `Error: ${error}`, guarded, managedCommand, contentTruncated, verificationReceipt, verifierDecision, benchmarkComparisonDecision);
1257
1862
  this.emit({ type: 'tool_error', tool: toolBlock.name, error, toolCallId: toolBlock.id });
1258
1863
  this.tracer.event({
1259
1864
  type: 'tool_error',
1260
1865
  timestamp: new Date(),
1261
- data: { tool: toolBlock.name, error },
1866
+ data: { ...this.toolTraceData(toolCall), error },
1262
1867
  });
1263
1868
  return {
1264
1869
  toolCall,
1870
+ managedCommand,
1871
+ contentTruncated,
1872
+ verificationReceipt,
1873
+ verifierDecision,
1874
+ benchmarkComparisonDecision,
1875
+ rawContent: `Error: ${error}`,
1265
1876
  toolResult: {
1266
1877
  tool_use_id: toolBlock.id,
1267
- content: applyRepeatGuard(toolBlock.name, toolBlock.input, `Error: ${error}`),
1878
+ content: guarded,
1268
1879
  is_error: true,
1269
1880
  },
1270
1881
  };
@@ -1283,17 +1894,19 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1283
1894
  // current agent is rebuilt before any subsequent iteration.
1284
1895
  const exclusiveTransitionId = toolUseBlocks.find((toolBlock) => toolBlock.name === 'plan_mode')?.id ?? null;
1285
1896
  const combined = await scheduleToolBatch(toolUseBlocks, this.config.tools, this.config.execution, async (toolBlock, skipReason) => {
1286
- const toolCall = {
1287
- id: toolBlock.id,
1288
- name: toolBlock.name,
1289
- args: toolBlock.input,
1290
- status: 'running',
1291
- startedAt: new Date()
1897
+ if (this.toolCheckpointFailure)
1898
+ throw this.toolCheckpointFailure;
1899
+ const toolCall = batchCalls.find(call => call.id === toolBlock.id);
1900
+ toolCall.status = 'running';
1901
+ toolCall.startedAt = new Date();
1902
+ const beforeExecute = async (args) => {
1903
+ toolCall.args = structuredClone(args);
1904
+ await this.persistToolCheckpoint('before_execute', toolBlock.id, turnMessages, toolCheckpoint);
1292
1905
  };
1293
1906
  this.tracer.event({
1294
1907
  type: 'tool_start',
1295
1908
  timestamp: new Date(),
1296
- data: { tool: toolBlock.name }
1909
+ data: { ...this.toolTraceData(toolCall), argsSource: 'provider_proposal' }
1297
1910
  });
1298
1911
  this.emit({ type: 'tool_start', tool: toolBlock.name, args: toolBlock.input, toolCallId: toolBlock.id });
1299
1912
  if (skipReason)
@@ -1306,15 +1919,19 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1306
1919
  if (tool?.finalResponse && toolUseBlocks.length !== 1) {
1307
1920
  return recordToolError(toolBlock, toolCall, 'finish_task must be the only tool call; observe other results before completing the task.');
1308
1921
  }
1309
- // Dispatch-layer short-circuit for recalls on already-pinned ids.
1310
- // The selector marked the original tool_result inline this request;
1311
- // the model doesn't need the content rehydrated again. Returning a
1312
- // tiny ack (paired with the OUTER toolBlock.id) preserves the API
1313
- // protocol invariant.
1922
+ // Selector intent does not prove visibility: the encoder may cap or
1923
+ // compress the result. Acknowledge only exact encoded source bytes
1924
+ // that also remain within the historical stability limit. Larger
1925
+ // current results become previews after this recall is appended.
1926
+ // A hook dispatcher still owns each invocation's admission and outcome.
1314
1927
  const innerId = recallInnerIdOf(toolBlock);
1315
- if (innerId !== null &&
1928
+ if (!this.hooks && innerId !== null &&
1929
+ toolBlock.input.cursor === undefined && toolBlock.input.max_bytes === undefined &&
1316
1930
  this.toolResponseStore.willInlineNextRender(innerId) &&
1317
- this.toolResponseStore.hasCached(innerId)) {
1931
+ this.toolResponseStore.hasCached(innerId) &&
1932
+ request.messages.some(message => typeof message.content !== 'string' && message.content.some(block => block.type === 'tool_result' && block.tool_use_id === innerId &&
1933
+ typeof block.content === 'string' && block.content.length <= STABLE_RESULT_LIMIT &&
1934
+ createHash('sha256').update(block.content).digest('hex') === this.toolResponseStore.getMeta(innerId)?.result_sha256))) {
1318
1935
  const ack = JSON.stringify({
1319
1936
  ok: true,
1320
1937
  tool_use_id: innerId,
@@ -1323,124 +1940,133 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1323
1940
  });
1324
1941
  return recordToolSuccess(toolBlock, toolCall, ack, toolBlock.input);
1325
1942
  }
1943
+ const outputObserver = this.toolOutputObserver(toolCall, signal);
1326
1944
  try {
1327
- // Share the execute promise across duplicate recalls in this batch.
1945
+ // Hooks own per-invocation transformations, admission and outcomes.
1946
+ // Only a host without a hook dispatcher may share default recalls.
1328
1947
  let result;
1329
- if (innerId !== null) {
1330
- const existing = sharedRecallExec.get(innerId);
1948
+ if (!this.hooks && innerId !== null && toolBlock.input.cursor === undefined && toolBlock.input.max_bytes === undefined) {
1949
+ const recallShareKey = JSON.stringify(toolBlock.input);
1950
+ const existing = sharedRecallExec.get(recallShareKey);
1331
1951
  if (existing !== undefined) {
1332
1952
  result = await existing;
1333
1953
  }
1334
1954
  else {
1335
1955
  const fresh = executeToolWithRuntime(toolBlock.name, toolBlock.input, {
1336
1956
  tools: this.config.tools,
1957
+ remoteResourceSession: this.remoteResourceSession,
1337
1958
  context: {
1338
1959
  cwd: this._cwd,
1339
1960
  toolCallId: toolBlock.id,
1961
+ onUsage: requestUsage,
1340
1962
  sessionId: this.config.sessionId,
1963
+ managedCommandWaitSliceMs: 0,
1341
1964
  signal,
1342
1965
  toolResponseStore: this.toolResponseStore,
1343
1966
  discoveredTools: this.discoveredTools,
1344
1967
  availableTools: this.config.tools,
1345
1968
  subprocessEnv: this.copySubprocessEnv(),
1969
+ commandPolicy: this.commandPolicy,
1970
+ onOutput: outputObserver.onOutput,
1346
1971
  },
1347
1972
  policy: this.policy,
1348
1973
  hooks: this.hooks,
1349
1974
  approvalRequester: this.approvalRequester,
1350
1975
  onToolEffect: this.toolEffectObserver,
1976
+ onLocalFileChange: this.onLocalFileChange,
1977
+ beforeExecute,
1351
1978
  approvalRequiredCapabilities: this.approvalRequiredCapabilities,
1352
1979
  approvalContext: this.approvalContext,
1353
1980
  });
1354
- sharedRecallExec.set(innerId, fresh);
1981
+ sharedRecallExec.set(recallShareKey, fresh);
1355
1982
  result = await fresh;
1356
1983
  }
1357
1984
  }
1358
1985
  else {
1359
1986
  result = await executeToolWithRuntime(toolBlock.name, toolBlock.input, {
1360
1987
  tools: this.config.tools,
1988
+ remoteResourceSession: this.remoteResourceSession,
1361
1989
  context: {
1362
1990
  cwd: this._cwd,
1363
1991
  toolCallId: toolBlock.id,
1992
+ onUsage: requestUsage,
1364
1993
  sessionId: this.config.sessionId,
1994
+ managedCommandWaitSliceMs: 0,
1365
1995
  signal,
1366
1996
  toolResponseStore: this.toolResponseStore,
1367
1997
  discoveredTools: this.discoveredTools,
1368
1998
  availableTools: this.config.tools,
1369
1999
  subprocessEnv: this.copySubprocessEnv(),
2000
+ commandPolicy: this.commandPolicy,
2001
+ onOutput: outputObserver.onOutput,
1370
2002
  },
1371
2003
  policy: this.policy,
1372
2004
  hooks: this.hooks,
1373
2005
  approvalRequester: this.approvalRequester,
1374
2006
  onToolEffect: this.toolEffectObserver,
2007
+ onLocalFileChange: this.onLocalFileChange,
2008
+ beforeExecute,
1375
2009
  approvalRequiredCapabilities: this.approvalRequiredCapabilities,
1376
2010
  approvalContext: this.approvalContext,
1377
2011
  });
1378
2012
  }
2013
+ outputObserver.close();
1379
2014
  if (result.isError) {
1380
- return recordToolError(toolBlock, toolCall, result.content, result.effectiveArgs);
2015
+ return recordToolError(toolBlock, toolCall, result.content, result.effectiveArgs, result.managedCommand, result.contentTruncated, result.verificationReceipt, result.verifierDecision, result.benchmarkComparisonDecision);
1381
2016
  }
1382
- return recordToolSuccess(toolBlock, toolCall, result.content, result.effectiveArgs);
2017
+ return recordToolSuccess(toolBlock, toolCall, result.content, result.effectiveArgs, result.managedCommand, result.contentTruncated, result.verificationReceipt, result.verifierDecision, result.benchmarkComparisonDecision);
1383
2018
  }
1384
2019
  catch (err) {
2020
+ outputObserver.close();
2021
+ if (this.toolCheckpointFailure)
2022
+ throw this.toolCheckpointFailure;
1385
2023
  const error = err.message;
1386
2024
  return recordToolError(toolBlock, toolCall, error);
1387
2025
  }
1388
- }, result => result.toolCall.status === 'error' || commandStillRunning(result.toolResult.content), signal, exclusiveTransitionId);
1389
- allToolCalls.push(...combined.map(r => r.toolCall));
1390
- const partial = Boolean(signal?.aborted);
1391
- const toolResults = combined.map(r => {
1392
- const { rendered } = this.toolResponseStore.put(r.toolResult.tool_use_id, r.toolCall.name, r.toolResult.content, partial, {
1393
- sourcePath: extractSourcePath(r.toolCall.name, r.toolCall.args),
1394
- dedupable: r.toolCall.status === 'complete',
1395
- isError: r.toolResult.is_error,
1396
- });
1397
- return { tool_use_id: r.toolResult.tool_use_id, content: rendered, is_error: r.toolResult.is_error };
1398
- });
2026
+ finally {
2027
+ outputObserver.close();
2028
+ }
2029
+ }, result => result.toolCall.status === 'error' || result.managedCommand?.result.status === 'running'
2030
+ || commandStillRunning(result.toolResult.content), signal, exclusiveTransitionId);
1399
2031
  // Real work happened, so the turn has demonstrably not stalled on
1400
2032
  // narration: give it a fresh continuation budget.
1401
2033
  this._consecutiveAutoContinues = 0;
1402
2034
  // Add assistant message with tool use
1403
- const textContent = response.content
1404
- .filter(this.isTextBlock)
1405
- .map(b => b.text)
1406
- .join('\n');
1407
- const toolUseMessage = {
1408
- id: `assistant_${Date.now()}`,
1409
- role: 'assistant',
1410
- content: textContent || '[Using tools...]',
1411
- timestamp: new Date(),
1412
- toolCalls: allToolCalls.slice(-toolUseBlocks.length),
1413
- ...(response.reasoning ? { metadata: { reasoningContent: response.reasoning } } : {}),
1414
- };
1415
- messages.push(toolUseMessage);
1416
- turnMessages.push(toolUseMessage);
1417
- const toolResultMessage = {
1418
- id: `tool_${Date.now()}`,
1419
- role: 'user',
1420
- content: JSON.stringify(toolResults),
1421
- timestamp: new Date(),
1422
- metadata: { toolResult: true },
1423
- };
1424
- messages.push(toolResultMessage);
1425
- turnMessages.push(toolResultMessage);
2035
+ const textContent = this.remoteResourceSession
2036
+ ? combined.filter(result => result.toolCall.name === 'remote_resource' && result.toolCall.status === 'complete')
2037
+ .flatMap(result => remoteDeletionReport(result.toolResult.content) ?? []).join('\n')
2038
+ : response.content.filter(this.isTextBlock).map(b => b.text).join('\n');
2039
+ toolUseMessage.content = textContent || '[Using tools...]';
2040
+ toolUseMessage.turn = requestUsage?.getTurn?.();
1426
2041
  this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
1427
2042
  if (signal?.aborted)
1428
2043
  throw new AgentError('Cancelled', 'CANCELLED');
1429
2044
  // Stream any text content that came with tool calls (important for user feedback!)
1430
2045
  if (textContent && textContent.trim() && !signal?.aborted) {
1431
- if (!this._lastResponseStreamed)
2046
+ if (this.remoteResourceSession || !this._lastResponseStreamed)
1432
2047
  await this.emitStreamText(textContent, signal);
1433
2048
  // Last non-empty text wins — empty iterations preserve the prior value
1434
2049
  // so maxIterations exhaustion still has something to return.
1435
2050
  lastToolTurnText = textContent;
1436
2051
  }
2052
+ // Recorded IDs were reconciled above; the scheduler receives only new
2053
+ // calls. Add one host notice after retaining every refused result pair.
2054
+ const batchLimit = resolveExecutionSettings(this.config.execution).maxCallsPerResponse;
2055
+ if (toolUseBlocks.length > batchLimit) {
2056
+ const recoveryMessage = { id: `batch_recovery_${randomUUID()}`, role: 'user',
2057
+ content: `[Tool batch recovery] The ${toolUseBlocks.length} new-call batch exceeds the limit of ${batchLimit} total calls (reads + effects) per response. None of this rejected batch executed. Submit smaller batches of at most ${batchLimit} calls in bounded rounds, and inspect prerequisite results before dependent effects. Retry only operations explicitly marked not_executed, using fresh invocation IDs for those proven unexecuted retries. Reuse retained completed results; inspect and reconcile running or unknown outcomes before considering another operation.`,
2058
+ metadata: { synthetic: true }, timestamp: new Date() };
2059
+ messages.push(recoveryMessage);
2060
+ turnMessages.push(recoveryMessage);
2061
+ this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
2062
+ }
1437
2063
  const recoveryAction = recovery(combined.map(result => ({ name: result.toolCall.name,
1438
- content: result.toolResult.content, isError: result.toolResult.is_error })));
2064
+ content: result.toolResult.content, isError: result.toolResult.is_error, managedCommand: result.managedCommand })));
1439
2065
  if (recoveryAction === 'stop')
1440
2066
  throw new AgentError('Automatic recovery budget exhausted after repeated tool failures. Inspect the retained results and current capabilities before retrying.', 'RECOVERY_EXHAUSTED');
1441
2067
  if (recoveryAction === 'recover') {
1442
2068
  const recoveryMessage = { id: `recovery_${randomUUID()}`, role: 'user',
1443
- content: '[Recovery] Repeated failures share the same cause. Stop that sequence. Inspect current capabilities and active jobs, then make one changed recovery attempt. Use file/stdin tools for scripts and raw Ctrl+C for stuck shell input, then read the pane in the same response or immediately afterward. A silent running-job poll allows one further inspection before stopping; do not repeat empty polls. Continue authorized work; use ask_user only for a concrete missing prerequisite. Do not repeat an acknowledgement as proof of success.',
2069
+ content: '[Recovery] Repeated failures share the same cause. Stop that sequence. Inspect current capabilities and active jobs, then make one changed recovery attempt. Use file/stdin tools for scripts and raw Ctrl+C for stuck shell input, then read the pane in the same response or immediately afterward. Healthy managed job waiting preserves this failure budget; silence is not a reason to relaunch or abandon the owned job. Continue authorized work; use ask_user only for a concrete missing prerequisite. Do not repeat an acknowledgement as proof of success.',
1444
2070
  metadata: { synthetic: true }, timestamp: new Date() };
1445
2071
  messages.push(recoveryMessage);
1446
2072
  turnMessages.push(recoveryMessage);
@@ -1469,7 +2095,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1469
2095
  const hasSuccessfulInteractiveTool = toolUseBlocks.some(tb => {
1470
2096
  if (!Agent.TURN_ENDING_TOOLS.has(tb.name))
1471
2097
  return false;
1472
- const result = toolResults.find(r => r.tool_use_id === tb.id);
2098
+ const result = completedResults.get(tb.id);
1473
2099
  return result ? !result.content.startsWith('Error:') : false;
1474
2100
  });
1475
2101
  if (hasSuccessfulInteractiveTool) {
@@ -1480,10 +2106,53 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1480
2106
  // and before the next inference. Keep terminal tools and exhausted turns
1481
2107
  // on the normal next-turn path because no model request remains here.
1482
2108
  if (iterations < this.config.maxIterations) {
1483
- const steering = takeSteering?.() ?? [];
2109
+ const observations = combined.flatMap(result => result.managedCommand && !result.toolResult.is_error
2110
+ ? [{ ...result.managedCommand, toolCallId: result.toolCall.id }] : []);
2111
+ const consumedSteering = [];
2112
+ let waited;
2113
+ try {
2114
+ waited = observations.length === combined.length
2115
+ ? await awaitManagedCommandProgress(observations, { signal, deadlineAt: Date.now() + 60_000,
2116
+ takeSteering: takeSteering ? () => {
2117
+ const next = takeSteering();
2118
+ consumedSteering.push(...next);
2119
+ return next;
2120
+ } : undefined, emit: event => this.emit(event) })
2121
+ : { waited: false, observations: [], steering: [] };
2122
+ }
2123
+ catch (error) {
2124
+ // A failed receipt refresh must not discard Enter messages already drained.
2125
+ if (consumedSteering.length) {
2126
+ messages.push(...consumedSteering);
2127
+ turnMessages.push(...consumedSteering);
2128
+ this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
2129
+ }
2130
+ throw error;
2131
+ }
2132
+ if (waited.waited) {
2133
+ const observation = { id: `command_observation_${randomUUID()}`, role: 'user',
2134
+ content: formatManagedCommandObservations(waited.observations), timestamp: new Date(),
2135
+ metadata: { synthetic: true, hostContext: true, managedCommandObservation: true } };
2136
+ for (const [index, item] of waited.observations.entries()) {
2137
+ this.toolResponseStore.put(`${observation.id}_${index}`, 'read_command', JSON.stringify(item.result), item.result.has_more || Boolean(item.result.output_truncated), {
2138
+ source: { ...this.evidenceSource(item.toolCallId, 'read_command', {}, item), originToolCallId: item.toolCallId },
2139
+ isError: item.result.status !== 'running' && item.result.status !== 'completed',
2140
+ });
2141
+ }
2142
+ messages.push(observation);
2143
+ turnMessages.push(observation);
2144
+ this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
2145
+ }
2146
+ const steering = [...waited.steering, ...(takeSteering?.() ?? [])];
1484
2147
  if (steering.length > 0) {
1485
2148
  messages.push(...steering);
1486
2149
  turnMessages.push(...steering);
2150
+ this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
2151
+ if (signal?.aborted)
2152
+ throw new AgentError('Cancelled', 'CANCELLED');
2153
+ await steeringAdmission?.({ messages: [...turnMessages], sourceMessageIds: steering.map(message => message.id), signal });
2154
+ if (signal?.aborted)
2155
+ throw new AgentError('Cancelled', 'CANCELLED');
1487
2156
  }
1488
2157
  }
1489
2158
  }
@@ -1534,11 +2203,12 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1534
2203
  }
1535
2204
  }
1536
2205
  return {
1537
- content: finalContent || lastToolTurnText,
2206
+ content: finalContent || lastToolTurnText || (this.remoteResourceSession ? NO_VERIFIED_DELETION_REPORT : ''),
1538
2207
  toolCalls: allToolCalls,
1539
2208
  usage: totalUsage || undefined,
1540
2209
  stopReason,
1541
2210
  ...(finalReasoning ? { reasoning: finalReasoning } : {}),
2211
+ ...(finalNativeContent ? { nativeContent: finalNativeContent, nativeModel: this.config.model, nativeContextId: this._nativeContextId } : {}),
1542
2212
  turnMessages,
1543
2213
  };
1544
2214
  }
@@ -1551,6 +2221,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1551
2221
  * implements it, so this is a no-op for every other provider.
1552
2222
  */
1553
2223
  dispose() {
2224
+ this.evidenceUnsubscribe();
1554
2225
  this.backend.close?.();
1555
2226
  }
1556
2227
  /**
@@ -1560,7 +2231,50 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1560
2231
  * approval requester, tool-response store, and mirror-visible tool events.
1561
2232
  */
1562
2233
  async executeHarnessToolCall(name, args, options = {}) {
2234
+ await this.recoverToolCheckpoint();
2235
+ return this.executeHarnessToolCallWithCheckpoint(name, args, options);
2236
+ }
2237
+ recordedToolCall(id, name, args) {
2238
+ const recorded = this.recordedToolCalls.get(id);
2239
+ if (recorded && (recorded.name !== name || !isDeepStrictEqual(recorded.args, args))) {
2240
+ throw new AgentError(`Tool invocation identity conflict for ${id}: the recorded tool or arguments differ. Inspect the original call; it was not executed again.`, 'TOOL_CALL_IDENTITY_CONFLICT');
2241
+ }
2242
+ return recorded;
2243
+ }
2244
+ async hasStoredToolIdentity(id) {
2245
+ if (this.toolResponseStore.getMeta(id))
2246
+ return true;
2247
+ try {
2248
+ // Compaction can remove the call row while its immutable source remains.
2249
+ // Lazy lookup also reserves unavailable or unindexed retained sources.
2250
+ return await this.toolResponseStore.get(id) !== null;
2251
+ }
2252
+ catch (error) {
2253
+ if (error instanceof ToolResponseIntegrityError)
2254
+ return true;
2255
+ throw error;
2256
+ }
2257
+ }
2258
+ async executeHarnessToolCallWithCheckpoint(name, args, options, context) {
2259
+ if (this.toolCheckpointFailure)
2260
+ throw this.toolCheckpointFailure;
1563
2261
  options.onActivity?.();
2262
+ const toolCallId = options.callId ?? `tool_${randomUUID()}`;
2263
+ const recorded = this.recordedToolCall(toolCallId, name, args);
2264
+ if (recorded) {
2265
+ if (recorded.error !== undefined)
2266
+ return { success: false, output: `Error: ${recorded.error}` };
2267
+ if (recorded.result !== undefined) {
2268
+ const image = parseImageToolResult(recorded.result);
2269
+ return { success: true, output: image
2270
+ ? { text: image.description ?? '', images: [{ mediaType: image.mediaType, data: image.data }] }
2271
+ : recorded.result };
2272
+ }
2273
+ return { success: false, output: `Tool outcome unknown for recorded invocation ${toolCallId}. Inspect the original call and reconcile its existing receipts/current state; it was not executed again.` };
2274
+ }
2275
+ if (await this.hasStoredToolIdentity(toolCallId)) {
2276
+ return { success: false, output: `Tool outcome unknown for retained invocation ${toolCallId}: its original argument record is unavailable. Inspect its retained source and reconcile existing receipts/current state; it was not executed again.` };
2277
+ }
1564
2278
  const tool = this.config.tools.find(candidate => candidate.definition.name === name);
1565
2279
  if (!tool) {
1566
2280
  return {
@@ -1568,59 +2282,156 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1568
2282
  output: `Error: unknown tool '${name}' is not registered for this agent.`,
1569
2283
  };
1570
2284
  }
1571
- const toolCallId = options.callId ?? this.nextToolEventId();
2285
+ // Direct calls have an isolated canonical segment. Provider callbacks hold
2286
+ // the arrays and handler belonging to their original turn, even if their
2287
+ // underlying IO settles after cancellation and a newer turn has started.
2288
+ const owner = context ?? { messages: [], turnMessages: [], toolCalls: [], handler: this.toolCheckpoint };
2289
+ const toolCall = { id: toolCallId, name, args: structuredClone(args),
2290
+ status: 'running', startedAt: new Date() };
2291
+ this.recordedToolCalls.set(toolCallId, toolCall);
2292
+ const toolUseMessage = {
2293
+ id: `assistant_${randomUUID()}`, role: 'assistant', content: '[Using tools...]',
2294
+ timestamp: new Date(), toolCalls: [toolCall], metadata: { toolCheckpoint: true },
2295
+ };
2296
+ const toolResultMessage = {
2297
+ id: `tool_${randomUUID()}`, role: 'user', content: '[]', timestamp: new Date(),
2298
+ metadata: { toolResult: true, toolCheckpoint: true },
2299
+ };
2300
+ owner.messages.push(toolUseMessage);
2301
+ owner.turnMessages.push(toolUseMessage);
2302
+ owner.toolCalls.push(toolCall);
1572
2303
  this.emit({ type: 'tool_start', tool: name, args, toolCallId });
1573
- this.tracer.event({ type: 'tool_start', timestamp: new Date(), data: { tool: name } });
2304
+ this.tracer.event({ type: 'tool_start', timestamp: new Date(), data: this.toolTraceData(toolCall) });
2305
+ const outputObserver = this.toolOutputObserver(toolCall, options.signal);
2306
+ let result;
1574
2307
  try {
1575
- const result = await executeToolWithRuntime(name, args, {
2308
+ result = await executeToolWithRuntime(name, args, {
1576
2309
  tools: this.config.tools,
2310
+ remoteResourceSession: this.remoteResourceSession,
1577
2311
  context: {
1578
2312
  cwd: this._cwd,
1579
2313
  toolCallId,
1580
2314
  sessionId: this.config.sessionId,
1581
2315
  signal: options.signal,
2316
+ onUsage: options.onUsage,
1582
2317
  toolResponseStore: this.toolResponseStore,
1583
2318
  discoveredTools: this.discoveredTools,
1584
2319
  availableTools: this.config.tools,
1585
2320
  subprocessEnv: this.copySubprocessEnv(),
2321
+ commandPolicy: this.commandPolicy,
2322
+ onOutput: outputObserver.onOutput,
1586
2323
  },
1587
2324
  policy: this.policy,
1588
2325
  hooks: this.hooks,
1589
2326
  approvalRequester: this.approvalRequester,
1590
2327
  onToolEffect: this.toolEffectObserver,
2328
+ onLocalFileChange: this.onLocalFileChange,
2329
+ beforeExecute: async (effectiveArgs) => {
2330
+ toolCall.args = structuredClone(effectiveArgs);
2331
+ await this.persistToolCheckpoint('before_execute', toolCallId, owner.turnMessages, owner.handler);
2332
+ },
1591
2333
  approvalRequiredCapabilities: this.approvalRequiredCapabilities,
1592
2334
  approvalContext: this.approvalContext,
1593
2335
  });
1594
- options.onActivity?.();
1595
- if (result.isError) {
1596
- this.emit({ type: 'tool_error', tool: name, error: result.content, toolCallId });
1597
- this.tracer.event({ type: 'tool_error', timestamp: new Date(), data: { tool: name, error: result.content } });
1598
- return { success: false, output: `Error: ${result.content}` };
1599
- }
1600
- this.emit({ type: 'tool_end', tool: name, result: result.content, toolCallId });
1601
- this.tracer.event({ type: 'tool_end', timestamp: new Date(), data: { tool: name } });
1602
- const image = parseImageToolResult(result.content);
1603
- if (image) {
1604
- return {
1605
- success: true,
1606
- output: {
1607
- text: image.description ?? '',
1608
- images: [{ mediaType: image.mediaType, data: image.data }],
1609
- },
1610
- };
1611
- }
1612
- return { success: true, output: result.content };
1613
2336
  }
1614
2337
  catch (error) {
1615
- const message = error instanceof Error ? error.message : String(error);
1616
- this.emit({ type: 'tool_error', tool: name, error: message, toolCallId });
1617
- this.tracer.event({ type: 'tool_error', timestamp: new Date(), data: { tool: name, error: message } });
1618
- return { success: false, output: `Error: ${message}` };
2338
+ if (this.toolCheckpointFailure)
2339
+ throw this.toolCheckpointFailure;
2340
+ result = { content: error instanceof Error ? error.message : String(error),
2341
+ isError: true, effectiveArgs: toolCall.args };
2342
+ }
2343
+ finally {
2344
+ outputObserver.close();
2345
+ }
2346
+ toolCall.args = structuredClone(result.effectiveArgs);
2347
+ toolCall.completedAt = new Date();
2348
+ toolCall.status = result.isError ? 'error' : 'complete';
2349
+ if (result.isError)
2350
+ toolCall.error = result.content;
2351
+ else
2352
+ toolCall.result = result.content;
2353
+ const partial = Boolean(options.signal?.aborted || result.contentTruncated || result.managedCommand?.result.has_more || result.managedCommand?.result.output_truncated);
2354
+ const { rendered } = this.toolResponseStore.put(toolCallId, name, result.content, partial, {
2355
+ args: result.effectiveArgs,
2356
+ sourcePath: extractSourcePath(name, result.effectiveArgs), isError: result.isError,
2357
+ source: { ...this.evidenceSource(toolCallId, name, result.effectiveArgs, result.managedCommand, result.verificationReceipt, partial || result.isError ? undefined : result.verifierDecision, partial || result.isError ? undefined : result.benchmarkComparisonDecision),
2358
+ messageId: toolUseMessage.id },
2359
+ });
2360
+ toolResultMessage.content = JSON.stringify([{ tool_use_id: toolCallId, content: rendered, is_error: result.isError }]);
2361
+ owner.messages.push(toolResultMessage);
2362
+ owner.turnMessages.push(toolResultMessage);
2363
+ await this.refreshVerificationCoverage();
2364
+ await this.persistToolCheckpoint('terminal', toolCallId, owner.turnMessages, owner.handler);
2365
+ if (context)
2366
+ this.emit({ type: 'history_checkpoint', messages: [...owner.turnMessages] });
2367
+ options.onActivity?.();
2368
+ if (result.isError) {
2369
+ this.emit({ type: 'tool_error', tool: name, error: result.content, toolCallId });
2370
+ this.tracer.event({ type: 'tool_error', timestamp: new Date(), data: { ...this.toolTraceData(toolCall), error: result.content } });
2371
+ return { success: false, output: `Error: ${result.content}` };
2372
+ }
2373
+ this.emit({ type: 'tool_end', tool: name, result: result.content, toolCallId });
2374
+ this.tracer.event({ type: 'tool_end', timestamp: new Date(), data: this.toolTraceData(toolCall) });
2375
+ const image = parseImageToolResult(result.content);
2376
+ if (image) {
2377
+ return {
2378
+ success: true,
2379
+ output: {
2380
+ text: image.description ?? '',
2381
+ images: [{ mediaType: image.mediaType, data: image.data }],
2382
+ },
2383
+ };
2384
+ }
2385
+ return { success: true, output: result.content };
2386
+ }
2387
+ captureUserInstructions(messages) {
2388
+ const known = new Set(this.getEvidenceSnapshot().records.map(record => record.id));
2389
+ for (const message of messages) {
2390
+ if (message.role !== 'user' || message.metadata?.synthetic || message.metadata?.toolResult
2391
+ || message.metadata?.compactionSummary || message.metadata?.managedCommandObservation)
2392
+ continue;
2393
+ const id = `instruction_${message.id}`;
2394
+ if (known.has(id))
2395
+ continue;
2396
+ // Legacy tool-result arrays may lack the metadata marker. They remain
2397
+ // quoted tool data, never a source of user permission.
2398
+ try {
2399
+ const parsed = JSON.parse(message.content);
2400
+ if (Array.isArray(parsed) && parsed.some(item => item && typeof item === 'object' && 'tool_use_id' in item))
2401
+ continue;
2402
+ }
2403
+ catch { /* ordinary user text */ }
2404
+ this.toolResponseStore.evidence.recordUserInstruction({ id, text: message.content,
2405
+ textSha256: createHash('sha256').update(message.content).digest('hex'), originalTextBytes: Buffer.byteLength(message.content),
2406
+ recordedAt: message.timestamp instanceof Date ? message.timestamp.toISOString() : new Date().toISOString(),
2407
+ source: { origin: 'user', messageId: message.id } });
2408
+ known.add(id);
2409
+ }
2410
+ }
2411
+ evidenceSource(toolCallId, toolName, args, managed, verificationReceipt, verifierDecision, benchmarkComparisonDecision) {
2412
+ const path = extractSourcePath(toolName, args);
2413
+ const verification = verificationReceipt ?? managed?.result.verification;
2414
+ const source = { origin: managed || verification || benchmarkComparisonDecision ? 'host_receipt' : 'host_tool', toolCallId, toolName,
2415
+ cwd: managed?.result.cwd ?? this._cwd, ...(path ? { path } : {}) };
2416
+ if (verification)
2417
+ source.verification = structuredClone(verification);
2418
+ if (verifierDecision)
2419
+ source.verifier = structuredClone(verifierDecision);
2420
+ if (benchmarkComparisonDecision)
2421
+ source.benchmarkComparison = structuredClone(benchmarkComparisonDecision);
2422
+ if (managed) {
2423
+ const result = managed.result;
2424
+ source.job = { sessionId: managed.sessionId, jobId: result.job_id, status: result.status,
2425
+ exitCode: result.exit_code ?? null, startedAt: result.started_at, endedAt: result.ended_at,
2426
+ cursor: result.cursor, hasMore: result.has_more, outputTruncated: Boolean(result.output_truncated),
2427
+ ...(result.checked === undefined ? {} : { checked: structuredClone(result.checked) }) };
2428
+ source.artifact = { path: result.log_path };
1619
2429
  }
2430
+ return source;
1620
2431
  }
1621
2432
  // API call with retry logic for transient errors
1622
- async callAPI(request, signal) {
1623
- const { maxAttempts, baseDelayMs: baseDelay } = this.config.apiRetry;
2433
+ async callAPI(request, onProviderObservation, signal, checkpointContext, onReturnedResponse, onUsage) {
2434
+ const { maxAttempts, recoveryWindowMs } = this.config.apiRetry;
1624
2435
  const timeoutMs = this.config.llmRequestTimeoutMs;
1625
2436
  if (this.backend.handlesTransportRetries?.(request) === true) {
1626
2437
  request = { ...request, transportTurnId: request.transportTurnId ?? randomUUID() };
@@ -1638,11 +2449,23 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1638
2449
  // model like deepseek can regenerate the turn instead of killing the run.
1639
2450
  const useStream = process.env.ZTC_NO_STREAM !== '1' && !this.backend.streamsSyntheticallyFor?.(request);
1640
2451
  let answerOutputEmitted = false;
1641
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2452
+ let effectObserved = false;
2453
+ let recoveryStartedAt;
2454
+ let lastReason = 'Server temporarily unavailable';
2455
+ const canRegenerate = () => this.backend.allowsRequestRetryAfterFailure?.(request) !== false;
2456
+ const returned = (response) => { onReturnedResponse?.(response); return response; };
2457
+ for (let attempt = 1;; attempt++) {
1642
2458
  if (signal?.aborted) {
1643
2459
  throw new AgentError('Cancelled', 'CANCELLED');
1644
2460
  }
2461
+ if (recoveryStartedAt !== undefined && recoveryWindowMs !== null
2462
+ && Date.now() - recoveryStartedAt >= recoveryWindowMs) {
2463
+ throw new AgentError(lastReason, 'API_ERROR');
2464
+ }
2465
+ if (attempt > 1)
2466
+ this.emit({ type: 'thinking_start' });
1645
2467
  const attemptController = new AbortController();
2468
+ let providerAttemptActive = true;
1646
2469
  let timedOut = false;
1647
2470
  let removeAbortListener;
1648
2471
  // Inactivity guard, not a total-duration cap: harness-owning backends
@@ -1659,30 +2482,47 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1659
2482
  // this to tell a user cancel (skip failover) from a hung account (rotate).
1660
2483
  userSignal: signal,
1661
2484
  timeoutMs,
1662
- onActivity: () => touchActivity(),
2485
+ onActivity: () => {
2486
+ if (providerAttemptActive && !signal?.aborted && !attemptController.signal.aborted)
2487
+ touchActivity();
2488
+ },
1663
2489
  // A backend reconnect is a sign of life too: the Router is still holding
1664
2490
  // the turn, so the inactivity guard must not fire during the backoff.
1665
2491
  onTransportRetry: (info) => {
2492
+ if (!providerAttemptActive || signal?.aborted || attemptController.signal.aborted)
2493
+ return;
1666
2494
  touchActivity();
1667
2495
  this.emit({
1668
2496
  type: 'api_retry',
1669
2497
  attempt: info.attempt,
1670
2498
  maxAttempts: info.maxAttempts,
1671
2499
  delay: info.delayMs,
1672
- reason: 'Connection to the LLM endpoint dropped',
2500
+ reason: info.rateLimited ? 'Rate limit exceeded' : 'Connection to the LLM endpoint dropped',
2501
+ ...(info.rateLimited ? { rateLimited: true } : {}),
1673
2502
  });
1674
2503
  },
1675
2504
  onObservation: (observation) => {
1676
- touchActivity();
1677
- request.onObservation?.(observation);
2505
+ if (providerAttemptActive && !signal?.aborted && !attemptController.signal.aborted) {
2506
+ touchActivity();
2507
+ if (observation.kind !== 'reasoning')
2508
+ effectObserved = true;
2509
+ }
2510
+ // Original reports remain evidence after retirement; liveness and
2511
+ // visible progress must stay with the request that produced them.
2512
+ return onProviderObservation(observation, () => providerAttemptActive && !signal?.aborted && !attemptController.signal.aborted);
1678
2513
  },
1679
2514
  onToolCall: async (name, args) => {
2515
+ if (!providerAttemptActive || signal?.aborted || attemptController.signal.aborted) {
2516
+ throw new AgentError('Provider request retired or cancelled; no new tool call was admitted.', 'CANCELLED');
2517
+ }
2518
+ effectObserved = true;
1680
2519
  // A tool stops when the caller cancels or this provider attempt times
1681
2520
  // out, whichever comes first.
1682
- const result = await this.executeHarnessToolCall(name, args, {
2521
+ const result = await this.executeHarnessToolCallWithCheckpoint(name, args, {
1683
2522
  signal: signal ? AbortSignal.any([signal, attemptController.signal]) : attemptController.signal,
1684
2523
  onActivity: touchActivity,
1685
- });
2524
+ onUsage,
2525
+ }, checkpointContext);
1686
2526
  return result.output;
1687
2527
  },
1688
2528
  };
@@ -1717,34 +2557,52 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1717
2557
  this._lastResponseStreamed = true;
1718
2558
  try {
1719
2559
  return await Promise.race([
1720
- this.streamAndAssemble(guardedRequest, signal, () => { answerOutputEmitted = true; }),
2560
+ this.streamAndAssemble(guardedRequest, signal, () => { answerOutputEmitted = true; }, onUsage).then(returned),
1721
2561
  guardPromise,
1722
2562
  ]);
1723
2563
  }
1724
2564
  catch (streamErr) {
2565
+ if (this.toolCheckpointFailure)
2566
+ throw this.toolCheckpointFailure;
1725
2567
  // If streaming failed before emitting anything (and not because of
1726
2568
  // cancel/timeout), fall back to the buffered generate() for this
1727
2569
  // attempt — never regress a turn just because generateStream can't
1728
2570
  // serve this request.
1729
2571
  if (this.backend.allowsBufferedFallbackAfterStreamFailure?.(guardedRequest) === false
1730
- || answerOutputEmitted || timedOut || signal?.aborted
2572
+ || !canRegenerate() || effectObserved || answerOutputEmitted || timedOut || signal?.aborted
1731
2573
  || (streamErr instanceof AgentError && streamErr.code === 'CANCELLED')) {
1732
2574
  throw streamErr;
1733
2575
  }
1734
2576
  this._lastResponseStreamed = false;
1735
2577
  return await Promise.race([
1736
- this.backend.generate(guardedRequest),
2578
+ this.backend.generate(guardedRequest).then(response => {
2579
+ if (response.usage)
2580
+ onUsage?.(response.usage);
2581
+ return returned(response);
2582
+ }),
1737
2583
  guardPromise,
1738
2584
  ]);
1739
2585
  }
1740
2586
  }
1741
2587
  this._lastResponseStreamed = false;
1742
2588
  return await Promise.race([
1743
- this.backend.generate(guardedRequest),
2589
+ this.backend.generate(guardedRequest).then(response => {
2590
+ if (response.usage)
2591
+ onUsage?.(response.usage);
2592
+ return returned(response);
2593
+ }),
1744
2594
  guardPromise,
1745
2595
  ]);
1746
2596
  }
1747
2597
  catch (err) {
2598
+ onUsage?.({ inputTokens: 0, outputTokens: 0, totalTokens: 0, estimated: true, unreported: true });
2599
+ providerAttemptActive = false;
2600
+ if (this.toolCheckpointFailure)
2601
+ throw this.toolCheckpointFailure;
2602
+ if (err instanceof NativeObservationLimitError) {
2603
+ this.nativeEvidenceFailure ??= new AgentError(err.message, 'NATIVE_OBSERVATION_LIMIT');
2604
+ throw this.nativeEvidenceFailure;
2605
+ }
1748
2606
  if (err instanceof AgentError && err.code === 'CANCELLED') {
1749
2607
  throw err;
1750
2608
  }
@@ -1759,7 +2617,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1759
2617
  // — re-streaming would duplicate that text in the UI. Fail the turn
1760
2618
  // instead. (Connect-time failures, and failures during the reasoning
1761
2619
  // phase only, still retry.)
1762
- if (useStream && answerOutputEmitted) {
2620
+ if (effectObserved || !canRegenerate() || useStream && answerOutputEmitted) {
1763
2621
  throw new AgentError(this.parseAPIError(error).reason, 'API_ERROR');
1764
2622
  }
1765
2623
  const { retryable, reason, kind } = this.parseAPIError(error);
@@ -1770,20 +2628,37 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1770
2628
  // attempt resumes the same Router-buffered turn instead of appending a
1771
2629
  // duplicate. Only a failure AFTER answer text is unrecoverable, and the
1772
2630
  // answerOutputEmitted guard above already handled that case.
1773
- if (!retryable || attempt === maxAttempts) {
2631
+ if (!retryable || maxAttempts === 1 || attempt >= maxAttempts && recoveryWindowMs === 0) {
1774
2632
  if (kind === 'context_overflow') {
1775
2633
  throw new ContextOverflowError(reason, request, error.message);
1776
2634
  }
1777
2635
  throw new AgentError(reason, 'API_ERROR');
1778
2636
  }
1779
- // Calculate delay with exponential backoff
1780
- const delay = baseDelay * Math.pow(2, attempt - 1);
2637
+ lastReason = reason;
2638
+ const recovering = attempt >= maxAttempts;
2639
+ if (recovering)
2640
+ recoveryStartedAt ??= Date.now();
2641
+ const elapsedMs = recoveryStartedAt === undefined ? 0 : Math.max(0, Date.now() - recoveryStartedAt);
2642
+ if (recovering && recoveryWindowMs !== null && elapsedMs >= recoveryWindowMs) {
2643
+ throw new AgentError(reason, 'API_ERROR');
2644
+ }
2645
+ // Keep the exact pending inference alive. Never resubmit the user turn
2646
+ // or revisit already checkpointed tool results to recover an endpoint.
2647
+ let delay = Math.max(apiRetryDelayMs(attempt, this.config.apiRetry), retryAfterDelayMs(error));
2648
+ if (recovering) {
2649
+ delay = Math.max(1, delay);
2650
+ if (recoveryWindowMs !== null)
2651
+ delay = Math.min(delay, recoveryWindowMs - elapsedMs);
2652
+ }
1781
2653
  this.emit({
1782
2654
  type: 'api_retry',
1783
2655
  attempt,
1784
2656
  maxAttempts,
1785
2657
  delay,
1786
- reason
2658
+ reason,
2659
+ phase: recovering ? 'recovery' : 'retry',
2660
+ elapsedMs,
2661
+ ...(kind === 'rate_limit' ? { rateLimited: true } : {}),
1787
2662
  });
1788
2663
  removeAbortListener?.();
1789
2664
  removeAbortListener = undefined;
@@ -1791,15 +2666,16 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1791
2666
  await waitForRetryDelay(delay, signal);
1792
2667
  }
1793
2668
  finally {
2669
+ providerAttemptActive = false;
1794
2670
  removeAbortListener?.();
1795
2671
  }
1796
2672
  }
1797
- // Should never reach here, but TypeScript needs it
1798
- throw new AgentError('Unexpected retry loop exit', 'API_ERROR');
1799
2673
  }
1800
2674
  // Parse API error to determine if retryable and extract user-friendly message
1801
2675
  parseAPIError(error) {
1802
- const message = error.message;
2676
+ // A provider may embed an oversized/malformed diagnostic in an Error.
2677
+ // Keep all parsing bounded even for transports with their own error type.
2678
+ const message = error.message.slice(0, 64 * 1024);
1803
2679
  // Check for status codes in error message
1804
2680
  const statusMatch = message.match(/API error \((\d+)\)/);
1805
2681
  const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 0;
@@ -1811,9 +2687,12 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1811
2687
  let errorMessage = '';
1812
2688
  let admissionMessage = '';
1813
2689
  try {
1814
- const jsonMatch = message.match(/:\s*(\{[\s\S]*\})\s*$/);
1815
- if (jsonMatch) {
1816
- const parsed = JSON.parse(jsonMatch[1]);
2690
+ // Provider envelopes start with an object after the diagnostic prefix.
2691
+ // Index searches avoid a greedy regex restarting at every forged colon.
2692
+ const objectStart = message.indexOf('{');
2693
+ const objectEnd = message.lastIndexOf('}');
2694
+ if (objectStart >= 0 && objectEnd >= objectStart) {
2695
+ const parsed = JSON.parse(message.slice(objectStart, objectEnd + 1));
1817
2696
  const errorPayload = parsed.error;
1818
2697
  if (typeof errorPayload === 'string') {
1819
2698
  errorMessage = errorPayload;
@@ -1848,6 +2727,11 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1848
2727
  catch {
1849
2728
  // Not valid JSON, use raw message
1850
2729
  }
2730
+ if (statusCode === 410 && message.startsWith('ZRouter API error (')
2731
+ && ['stream_producer_lost', 'stream_producer_failed', 'stream_resume_expired'].includes(errorCode)) {
2732
+ return { retryable: false, reason: 'The Router stream was interrupted and cannot be resumed. '
2733
+ + 'Send a message to continue from the existing conversation.' };
2734
+ }
1851
2735
  // Detected before the generic 400 bucket so the host can dump the request.
1852
2736
  if (errorCode === 'context_length_exceeded' || isContextOverflowMessage(`${errorMessage} ${message}`)) {
1853
2737
  return { retryable: false, reason: 'Context window exceeded', kind: 'context_overflow' };
@@ -1857,10 +2741,15 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1857
2741
  return { retryable: true, reason: 'API is overloaded' };
1858
2742
  }
1859
2743
  if (statusCode === 429 || errorType === 'rate_limit_error' || errorType === 'RESOURCE_EXHAUSTED') {
1860
- return { retryable: true, reason: 'Rate limit exceeded' };
2744
+ return { retryable: true, reason: 'Rate limit exceeded', kind: 'rate_limit' };
1861
2745
  }
1862
- if (statusCode === 503 || statusCode === 502 || statusCode === 500 || errorType === 'UNAVAILABLE' || errorType === 'INTERNAL') {
1863
- return { retryable: true, reason: 'Server temporarily unavailable' };
2746
+ if (statusCode === 504 || statusCode === 503 || statusCode === 502 || statusCode === 500 || errorType === 'UNAVAILABLE' || errorType === 'INTERNAL') {
2747
+ // Only Router admission guidance is public. Provider diagnostics stay
2748
+ // private; a bounded response-header ID supplies support correlation.
2749
+ const requestId = /(?:fly-request-id|x-request-id)=([A-Za-z0-9._:-]{1,128})(?:[ \]]|$)/.exec(message)?.[1];
2750
+ const detail = [admissionMessage.trim().replace(/\s+/g, ' '), requestId && `request_id: ${requestId}`].filter(Boolean).join('; ');
2751
+ const cause = detail ? `: ${detail.length > 160 ? `${detail.slice(0, 159)}…` : detail}` : '';
2752
+ return { retryable: true, reason: `Upstream server error (${statusCode || 'no status'})${cause}` };
1864
2753
  }
1865
2754
  // Non-retryable errors with clean messages
1866
2755
  if (statusCode === 401 || errorType === 'authentication_error' || errorType === 'UNAUTHENTICATED') {
@@ -1876,7 +2765,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1876
2765
  // unsubscribed user hits on the default route, so say what to do. Only the
1877
2766
  // Router's H3 envelope may supply its public wording; any other 402 body
1878
2767
  // stays private behind the canonical message.
1879
- if (statusCode === 402) {
2768
+ if (statusCode === 402 || errorCode === 'billing_required') {
1880
2769
  const guidance = 'Subscribe in Zerg Router (Dashboard → Billing), or choose another model with /model.';
1881
2770
  return {
1882
2771
  retryable: false,