@namzu/sdk 62.6.1 → 62.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +5 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
  6. package/dist/agents/examples/SupervisorAgent.js +1 -0
  7. package/dist/agents/examples/SupervisorAgent.js.map +1 -1
  8. package/dist/agents/explore.d.ts.map +1 -1
  9. package/dist/agents/explore.js +2 -1
  10. package/dist/agents/explore.js.map +1 -1
  11. package/dist/compaction/index.d.ts +1 -1
  12. package/dist/compaction/index.d.ts.map +1 -1
  13. package/dist/compaction/index.js +1 -1
  14. package/dist/compaction/index.js.map +1 -1
  15. package/dist/compaction/model-summary.js +2 -2
  16. package/dist/compaction/model-summary.js.map +1 -1
  17. package/dist/compaction/summary.d.ts +16 -1
  18. package/dist/compaction/summary.d.ts.map +1 -1
  19. package/dist/compaction/summary.js +25 -2
  20. package/dist/compaction/summary.js.map +1 -1
  21. package/dist/config/runtime.d.ts +8 -0
  22. package/dist/config/runtime.d.ts.map +1 -1
  23. package/dist/config/runtime.js +15 -0
  24. package/dist/config/runtime.js.map +1 -1
  25. package/dist/execution/workflow/runtime.d.ts.map +1 -1
  26. package/dist/execution/workflow/runtime.js +3 -0
  27. package/dist/execution/workflow/runtime.js.map +1 -1
  28. package/dist/execution/workflow/types.d.ts +8 -0
  29. package/dist/execution/workflow/types.d.ts.map +1 -1
  30. package/dist/manager/agent/child-session.d.ts +12 -0
  31. package/dist/manager/agent/child-session.d.ts.map +1 -1
  32. package/dist/manager/agent/child-session.js +29 -0
  33. package/dist/manager/agent/child-session.js.map +1 -1
  34. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  35. package/dist/manager/agent/lifecycle.js +8 -0
  36. package/dist/manager/agent/lifecycle.js.map +1 -1
  37. package/dist/manager/session/turn-recorder.d.ts +13 -2
  38. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  39. package/dist/manager/session/turn-recorder.js +107 -29
  40. package/dist/manager/session/turn-recorder.js.map +1 -1
  41. package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
  42. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  43. package/dist/prompt/coding-agent-doctrine.js +5 -3
  44. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  45. package/dist/provider/fallback.d.ts.map +1 -1
  46. package/dist/provider/fallback.js +5 -1
  47. package/dist/provider/fallback.js.map +1 -1
  48. package/dist/provider/retry.d.ts +25 -0
  49. package/dist/provider/retry.d.ts.map +1 -1
  50. package/dist/provider/retry.js +28 -0
  51. package/dist/provider/retry.js.map +1 -1
  52. package/dist/public-runtime.d.ts +3 -3
  53. package/dist/public-runtime.d.ts.map +1 -1
  54. package/dist/public-runtime.js +3 -3
  55. package/dist/public-runtime.js.map +1 -1
  56. package/dist/public-tools.d.ts +4 -2
  57. package/dist/public-tools.d.ts.map +1 -1
  58. package/dist/public-tools.js +5 -2
  59. package/dist/public-tools.js.map +1 -1
  60. package/dist/runtime/query/auto-review/classifier.d.ts +7 -1
  61. package/dist/runtime/query/auto-review/classifier.d.ts.map +1 -1
  62. package/dist/runtime/query/auto-review/classifier.js +8 -4
  63. package/dist/runtime/query/auto-review/classifier.js.map +1 -1
  64. package/dist/runtime/query/auto-review/prompt.d.ts.map +1 -1
  65. package/dist/runtime/query/auto-review/prompt.js +2 -0
  66. package/dist/runtime/query/auto-review/prompt.js.map +1 -1
  67. package/dist/runtime/query/auto-review/script-evidence.d.ts +93 -0
  68. package/dist/runtime/query/auto-review/script-evidence.d.ts.map +1 -0
  69. package/dist/runtime/query/auto-review/script-evidence.js +566 -0
  70. package/dist/runtime/query/auto-review/script-evidence.js.map +1 -0
  71. package/dist/runtime/query/checkpoint.d.ts +13 -0
  72. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  73. package/dist/runtime/query/checkpoint.js +37 -0
  74. package/dist/runtime/query/checkpoint.js.map +1 -1
  75. package/dist/runtime/query/context.d.ts.map +1 -1
  76. package/dist/runtime/query/context.js +28 -2
  77. package/dist/runtime/query/context.js.map +1 -1
  78. package/dist/runtime/query/executor.d.ts +38 -1
  79. package/dist/runtime/query/executor.d.ts.map +1 -1
  80. package/dist/runtime/query/executor.js +154 -13
  81. package/dist/runtime/query/executor.js.map +1 -1
  82. package/dist/runtime/query/finalize-turn.d.ts +7 -1
  83. package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
  84. package/dist/runtime/query/finalize-turn.js +2 -0
  85. package/dist/runtime/query/finalize-turn.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +8 -0
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +7 -0
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts +32 -1
  91. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/index.js +158 -10
  93. package/dist/runtime/query/iteration/index.js.map +1 -1
  94. package/dist/runtime/query/iteration/phases/compaction.d.ts +12 -0
  95. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  96. package/dist/runtime/query/iteration/phases/compaction.js +48 -1
  97. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  98. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  99. package/dist/runtime/query/iteration/phases/context.js +5 -0
  100. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  101. package/dist/runtime/query/iteration/stream-turn.js +3 -3
  102. package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
  103. package/dist/runtime/query/prelude-lease.d.ts.map +1 -1
  104. package/dist/runtime/query/prelude-lease.js +22 -0
  105. package/dist/runtime/query/prelude-lease.js.map +1 -1
  106. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  107. package/dist/runtime/query/prepare-turn.js +30 -12
  108. package/dist/runtime/query/prepare-turn.js.map +1 -1
  109. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  110. package/dist/runtime/query/review-policy.js +29 -0
  111. package/dist/runtime/query/review-policy.js.map +1 -1
  112. package/dist/runtime/query/session-storage.d.ts.map +1 -1
  113. package/dist/runtime/query/session-storage.js +44 -3
  114. package/dist/runtime/query/session-storage.js.map +1 -1
  115. package/dist/runtime/query/tooling.d.ts +2 -0
  116. package/dist/runtime/query/tooling.d.ts.map +1 -1
  117. package/dist/runtime/query/tooling.js +1 -0
  118. package/dist/runtime/query/tooling.js.map +1 -1
  119. package/dist/runtime/query/turn/completion-audit.d.ts +27 -1
  120. package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -1
  121. package/dist/runtime/query/turn/completion-audit.js +71 -6
  122. package/dist/runtime/query/turn/completion-audit.js.map +1 -1
  123. package/dist/runtime/query/turn/outcome-ledger.d.ts +113 -0
  124. package/dist/runtime/query/turn/outcome-ledger.d.ts.map +1 -0
  125. package/dist/runtime/query/turn/outcome-ledger.js +578 -0
  126. package/dist/runtime/query/turn/outcome-ledger.js.map +1 -0
  127. package/dist/sandbox/file-walk.d.ts +7 -1
  128. package/dist/sandbox/file-walk.d.ts.map +1 -1
  129. package/dist/sandbox/file-walk.js +30 -4
  130. package/dist/sandbox/file-walk.js.map +1 -1
  131. package/dist/sandbox/glob-segment.d.ts +8 -1
  132. package/dist/sandbox/glob-segment.d.ts.map +1 -1
  133. package/dist/sandbox/glob-segment.js +28 -9
  134. package/dist/sandbox/glob-segment.js.map +1 -1
  135. package/dist/store/checkpoint/contract.d.ts +12 -0
  136. package/dist/store/checkpoint/contract.d.ts.map +1 -1
  137. package/dist/store/checkpoint/contract.js.map +1 -1
  138. package/dist/store/checkpoint/disk.d.ts.map +1 -1
  139. package/dist/store/checkpoint/disk.js +34 -2
  140. package/dist/store/checkpoint/disk.js.map +1 -1
  141. package/dist/store/checkpoint/prune.d.ts.map +1 -1
  142. package/dist/store/checkpoint/prune.js +6 -0
  143. package/dist/store/checkpoint/prune.js.map +1 -1
  144. package/dist/store/evidence/disk.js +40 -3
  145. package/dist/store/evidence/disk.js.map +1 -1
  146. package/dist/store/session-log/incremental.d.ts +23 -0
  147. package/dist/store/session-log/incremental.d.ts.map +1 -0
  148. package/dist/store/session-log/incremental.js +117 -0
  149. package/dist/store/session-log/incremental.js.map +1 -0
  150. package/dist/store/session-log/index.d.ts +1 -0
  151. package/dist/store/session-log/index.d.ts.map +1 -1
  152. package/dist/store/session-log/index.js +1 -0
  153. package/dist/store/session-log/index.js.map +1 -1
  154. package/dist/store/session-log/turn-outcome.d.ts +37 -0
  155. package/dist/store/session-log/turn-outcome.d.ts.map +1 -0
  156. package/dist/store/session-log/turn-outcome.js +77 -0
  157. package/dist/store/session-log/turn-outcome.js.map +1 -0
  158. package/dist/tools/admit-facts.d.ts.map +1 -1
  159. package/dist/tools/admit-facts.js +1 -0
  160. package/dist/tools/admit-facts.js.map +1 -1
  161. package/dist/tools/builtins/bash.d.ts.map +1 -1
  162. package/dist/tools/builtins/bash.js +11 -2
  163. package/dist/tools/builtins/bash.js.map +1 -1
  164. package/dist/tools/builtins/bounded-file-read.d.ts +19 -1
  165. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -1
  166. package/dist/tools/builtins/bounded-file-read.js +56 -3
  167. package/dist/tools/builtins/bounded-file-read.js.map +1 -1
  168. package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
  169. package/dist/tools/builtins/edit-apply.js +24 -3
  170. package/dist/tools/builtins/edit-apply.js.map +1 -1
  171. package/dist/tools/builtins/edit.d.ts +31 -17
  172. package/dist/tools/builtins/edit.d.ts.map +1 -1
  173. package/dist/tools/builtins/edit.js +92 -27
  174. package/dist/tools/builtins/edit.js.map +1 -1
  175. package/dist/tools/builtins/glob.js +1 -1
  176. package/dist/tools/builtins/glob.js.map +1 -1
  177. package/dist/tools/builtins/grep.d.ts +8 -0
  178. package/dist/tools/builtins/grep.d.ts.map +1 -1
  179. package/dist/tools/builtins/grep.js +148 -21
  180. package/dist/tools/builtins/grep.js.map +1 -1
  181. package/dist/tools/builtins/notebook-guard.d.ts +19 -0
  182. package/dist/tools/builtins/notebook-guard.d.ts.map +1 -0
  183. package/dist/tools/builtins/notebook-guard.js +75 -0
  184. package/dist/tools/builtins/notebook-guard.js.map +1 -0
  185. package/dist/tools/builtins/read-render.d.ts.map +1 -1
  186. package/dist/tools/builtins/read-render.js +8 -2
  187. package/dist/tools/builtins/read-render.js.map +1 -1
  188. package/dist/tools/builtins/search-support.d.ts +8 -0
  189. package/dist/tools/builtins/search-support.d.ts.map +1 -1
  190. package/dist/tools/builtins/search-support.js +66 -0
  191. package/dist/tools/builtins/search-support.js.map +1 -1
  192. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  193. package/dist/tools/builtins/write-file.js +14 -1
  194. package/dist/tools/builtins/write-file.js.map +1 -1
  195. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  196. package/dist/tools/coordinator/agent.js +25 -6
  197. package/dist/tools/coordinator/agent.js.map +1 -1
  198. package/dist/tools/coordinator/index.d.ts.map +1 -1
  199. package/dist/tools/coordinator/index.js +6 -1
  200. package/dist/tools/coordinator/index.js.map +1 -1
  201. package/dist/tools/coordinator/outcome-record.d.ts +22 -0
  202. package/dist/tools/coordinator/outcome-record.d.ts.map +1 -0
  203. package/dist/tools/coordinator/outcome-record.js +91 -0
  204. package/dist/tools/coordinator/outcome-record.js.map +1 -0
  205. package/dist/tools/defineTool.d.ts +2 -0
  206. package/dist/tools/defineTool.d.ts.map +1 -1
  207. package/dist/tools/defineTool.js +1 -0
  208. package/dist/tools/defineTool.js.map +1 -1
  209. package/dist/tools/file-read-tracker.d.ts +23 -2
  210. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  211. package/dist/tools/file-read-tracker.js +37 -2
  212. package/dist/tools/file-read-tracker.js.map +1 -1
  213. package/dist/tools/untrusted-envelope.d.ts +6 -0
  214. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  215. package/dist/tools/untrusted-envelope.js +22 -1
  216. package/dist/tools/untrusted-envelope.js.map +1 -1
  217. package/dist/toolsets/manager.d.ts.map +1 -1
  218. package/dist/toolsets/manager.js +3 -4
  219. package/dist/toolsets/manager.js.map +1 -1
  220. package/dist/toolsets/validation-issues.d.ts +27 -0
  221. package/dist/toolsets/validation-issues.d.ts.map +1 -0
  222. package/dist/toolsets/validation-issues.js +83 -0
  223. package/dist/toolsets/validation-issues.js.map +1 -0
  224. package/dist/turn/memory-recall.d.ts +13 -0
  225. package/dist/turn/memory-recall.d.ts.map +1 -1
  226. package/dist/turn/memory-recall.js +18 -0
  227. package/dist/turn/memory-recall.js.map +1 -1
  228. package/dist/types/agent/base.d.ts +19 -0
  229. package/dist/types/agent/base.d.ts.map +1 -1
  230. package/dist/types/agent/query.d.ts +2 -0
  231. package/dist/types/agent/query.d.ts.map +1 -1
  232. package/dist/types/common/index.d.ts +17 -0
  233. package/dist/types/common/index.d.ts.map +1 -1
  234. package/dist/types/common/index.js +8 -1
  235. package/dist/types/common/index.js.map +1 -1
  236. package/dist/types/hitl/index.d.ts +22 -0
  237. package/dist/types/hitl/index.d.ts.map +1 -1
  238. package/dist/types/hitl/index.js.map +1 -1
  239. package/dist/types/session/config.d.ts +20 -0
  240. package/dist/types/session/config.d.ts.map +1 -1
  241. package/dist/types/session/events.d.ts +3 -3
  242. package/dist/types/session/events.d.ts.map +1 -1
  243. package/dist/types/session/index.d.ts +1 -0
  244. package/dist/types/session/index.d.ts.map +1 -1
  245. package/dist/types/session/index.js +1 -0
  246. package/dist/types/session/index.js.map +1 -1
  247. package/dist/types/session/outcome.d.ts +95 -0
  248. package/dist/types/session/outcome.d.ts.map +1 -0
  249. package/dist/types/session/outcome.js +45 -0
  250. package/dist/types/session/outcome.js.map +1 -0
  251. package/dist/types/session/prepare-step.d.ts +2 -0
  252. package/dist/types/session/prepare-step.d.ts.map +1 -1
  253. package/dist/types/session/records.d.ts +167 -2
  254. package/dist/types/session/records.d.ts.map +1 -1
  255. package/dist/types/session/records.js +42 -0
  256. package/dist/types/session/records.js.map +1 -1
  257. package/dist/types/session/turn.d.ts +18 -0
  258. package/dist/types/session/turn.d.ts.map +1 -1
  259. package/dist/types/session/turn.js.map +1 -1
  260. package/dist/types/tool/index.d.ts +30 -0
  261. package/dist/types/tool/index.d.ts.map +1 -1
  262. package/dist/types/tool/index.js.map +1 -1
  263. package/dist/types/tool/presentation.d.ts +6 -0
  264. package/dist/types/tool/presentation.d.ts.map +1 -1
  265. package/package.json +1 -1
  266. package/src/agents/QueryAgent.ts +5 -0
  267. package/src/agents/examples/SupervisorAgent.ts +1 -0
  268. package/src/agents/explore.ts +2 -1
  269. package/src/compaction/index.ts +1 -0
  270. package/src/compaction/model-summary.ts +2 -2
  271. package/src/compaction/summary.ts +30 -2
  272. package/src/config/runtime.ts +15 -0
  273. package/src/execution/workflow/runtime.ts +4 -0
  274. package/src/execution/workflow/types.ts +9 -0
  275. package/src/manager/agent/child-session.ts +36 -0
  276. package/src/manager/agent/lifecycle.ts +9 -0
  277. package/src/manager/session/turn-recorder.ts +115 -30
  278. package/src/pricing/rates.source.json +1 -1
  279. package/src/prompt/coding-agent-doctrine.ts +5 -3
  280. package/src/provider/fallback.ts +6 -1
  281. package/src/provider/retry.ts +51 -0
  282. package/src/public-runtime.ts +10 -1
  283. package/src/public-tools.ts +6 -1
  284. package/src/runtime/query/auto-review/classifier.ts +21 -5
  285. package/src/runtime/query/auto-review/prompt.ts +2 -0
  286. package/src/runtime/query/auto-review/script-evidence.ts +618 -0
  287. package/src/runtime/query/checkpoint.ts +53 -1
  288. package/src/runtime/query/context.ts +33 -6
  289. package/src/runtime/query/executor.ts +183 -21
  290. package/src/runtime/query/finalize-turn.ts +8 -1
  291. package/src/runtime/query/index.ts +19 -0
  292. package/src/runtime/query/iteration/index.ts +167 -14
  293. package/src/runtime/query/iteration/phases/compaction.ts +53 -3
  294. package/src/runtime/query/iteration/phases/context.ts +6 -0
  295. package/src/runtime/query/iteration/stream-turn.ts +3 -3
  296. package/src/runtime/query/prelude-lease.ts +19 -0
  297. package/src/runtime/query/prepare-turn.ts +29 -12
  298. package/src/runtime/query/review-policy.ts +33 -0
  299. package/src/runtime/query/session-storage.ts +42 -3
  300. package/src/runtime/query/tooling.ts +3 -0
  301. package/src/runtime/query/turn/completion-audit.ts +76 -5
  302. package/src/runtime/query/turn/outcome-ledger.ts +633 -0
  303. package/src/sandbox/file-walk.ts +41 -4
  304. package/src/sandbox/glob-segment.ts +48 -24
  305. package/src/store/checkpoint/contract.ts +15 -0
  306. package/src/store/checkpoint/disk.ts +34 -2
  307. package/src/store/checkpoint/prune.ts +13 -0
  308. package/src/store/evidence/disk.ts +57 -2
  309. package/src/store/session-log/incremental.ts +147 -0
  310. package/src/store/session-log/index.ts +7 -0
  311. package/src/store/session-log/turn-outcome.ts +84 -0
  312. package/src/tools/admit-facts.ts +1 -0
  313. package/src/tools/builtins/bash.ts +12 -2
  314. package/src/tools/builtins/bounded-file-read.ts +59 -2
  315. package/src/tools/builtins/edit-apply.ts +26 -4
  316. package/src/tools/builtins/edit.ts +106 -41
  317. package/src/tools/builtins/glob.ts +1 -1
  318. package/src/tools/builtins/grep.ts +173 -27
  319. package/src/tools/builtins/notebook-guard.ts +76 -0
  320. package/src/tools/builtins/read-render.ts +12 -2
  321. package/src/tools/builtins/search-support.ts +62 -0
  322. package/src/tools/builtins/write-file.ts +12 -1
  323. package/src/tools/coordinator/agent.ts +31 -7
  324. package/src/tools/coordinator/index.ts +6 -1
  325. package/src/tools/coordinator/outcome-record.ts +93 -0
  326. package/src/tools/defineTool.ts +3 -0
  327. package/src/tools/file-read-tracker.ts +51 -2
  328. package/src/tools/untrusted-envelope.ts +23 -1
  329. package/src/toolsets/manager.ts +6 -10
  330. package/src/toolsets/validation-issues.ts +97 -0
  331. package/src/turn/memory-recall.ts +28 -0
  332. package/src/types/agent/base.ts +20 -0
  333. package/src/types/agent/query.ts +2 -0
  334. package/src/types/common/index.ts +25 -1
  335. package/src/types/hitl/index.ts +27 -0
  336. package/src/types/session/config.ts +20 -0
  337. package/src/types/session/events.ts +3 -3
  338. package/src/types/session/index.ts +1 -0
  339. package/src/types/session/outcome.ts +139 -0
  340. package/src/types/session/prepare-step.ts +2 -0
  341. package/src/types/session/records.ts +67 -1
  342. package/src/types/session/turn.ts +19 -0
  343. package/src/types/tool/index.ts +30 -0
  344. package/src/types/tool/presentation.ts +6 -0
@@ -3,6 +3,7 @@ import { type TurnRecorder, readFoldedHistory } from '../../manager/session/turn
3
3
  import type { CheckpointScope, SessionCheckpointStore } from '../../store/checkpoint/index.js'
4
4
  import type { SessionLog } from '../../store/session-log/index.js'
5
5
  import { restoreSpilledRequestInputs } from '../../store/session-log/spilled-input.js'
6
+ import { readOutcomeProgress } from '../../store/session-log/turn-outcome.js'
6
7
  import type { SerializedSpanContext } from '../../telemetry/attributes.js'
7
8
  import { NamzuError } from '../../types/errors/index.js'
8
9
  import type {
@@ -84,7 +85,14 @@ type CheckpointState = Pick<
84
85
  | 'review'
85
86
  | 'latestUserMessageId'
86
87
  | 'workingState'
87
- >
88
+ > & {
89
+ /**
90
+ * What the turn's outcome record had gathered. Not a field of the document:
91
+ * it is written as its own `outcome_progress_written` record beside the
92
+ * checkpoint, so a release that validates the document strictly reads it.
93
+ */
94
+ readonly outcome?: Readonly<Record<string, unknown>>
95
+ }
88
96
 
89
97
  /**
90
98
  * A checkpoint read back for a resume: the document, the context it
@@ -276,6 +284,11 @@ export class CheckpointManager {
276
284
  private restoredNativeAttempts = 0
277
285
  private toolStructuredAttemptsSource?: () => number
278
286
  private restoredToolAttempts = 0
287
+ private outcomeSource?: () => Readonly<Record<string, unknown>> | undefined
288
+ private restoredOutcomeState: { resumed: boolean; snapshot?: Readonly<Record<string, unknown>> } =
289
+ {
290
+ resumed: false,
291
+ }
279
292
  private lastCreatedId?: CheckpointId
280
293
  private traceSource?: () => SerializedSpanContext | undefined
281
294
  private parkTtlMs?: number
@@ -329,6 +342,23 @@ export class CheckpointManager {
329
342
  return this.restoredToolAttempts
330
343
  }
331
344
 
345
+ /** Wire the turn's outcome record in, so every checkpoint carries what it had gathered. */
346
+ setOutcomeSource(source: () => Readonly<Record<string, unknown>> | undefined): void {
347
+ this.outcomeSource = source
348
+ }
349
+
350
+ /**
351
+ * What a resumed turn's checkpoint kept of the outcome record. `resumed` is
352
+ * false for a turn that began here; true with no `snapshot` for one resumed
353
+ * from a checkpoint that predates the record.
354
+ */
355
+ get restoredOutcome(): {
356
+ readonly resumed: boolean
357
+ readonly snapshot?: Readonly<Record<string, unknown>>
358
+ } {
359
+ return this.restoredOutcomeState
360
+ }
361
+
332
362
  setStructuredReviewAttemptsSource(source: () => number): void {
333
363
  this.structuredReviewAttemptsSource = source
334
364
  }
@@ -417,6 +447,7 @@ export class CheckpointManager {
417
447
  },
418
448
  ...(latestUserMessageId ? { latestUserMessageId } : {}),
419
449
  ...withDefined('workingState', this.workingStateSource?.()),
450
+ ...withDefined('outcome', this.outcomeSource?.() ?? this.restoredOutcomeState.snapshot),
420
451
  }
421
452
  }
422
453
 
@@ -449,6 +480,19 @@ export class CheckpointManager {
449
480
  turnCreatedAt: this.turnCreatedAt ?? new Date(recorder.getTurn().startedAt).toISOString(),
450
481
  createdAt: new Date().toISOString(),
451
482
  }
483
+ // What the outcome record had gathered goes in its own record, ahead of the
484
+ // one that commits the checkpoint, and is read back by the checkpoint's id.
485
+ // Failing to write it is not failing to checkpoint: a resume without it
486
+ // reports no outcome, never a false `unchanged`.
487
+ if (state.outcome !== undefined) {
488
+ await recorder.appendIgnorableRecord({
489
+ type: 'outcome_progress_written',
490
+ turnId: this.scope.turnId,
491
+ ignorable: true,
492
+ checkpointId: document.checkpointId,
493
+ progress: state.outcome,
494
+ })
495
+ }
452
496
  const receipt = await this.store.write(this.scope, document)
453
497
  await recorder.appendRecord({
454
498
  type: 'checkpoint_written',
@@ -654,6 +698,14 @@ export class CheckpointManager {
654
698
  this.restoredNativeAttempts = document.review.nativeStructuredAttempts
655
699
  this.restoredToolAttempts = document.review.toolStructuredAttempts ?? 0
656
700
  this.restoredReviewAttempts = document.review.structuredAttempts
701
+ await this.recorder.flush()
702
+ const progress = await readOutcomeProgress(this.recorder.log, checkpointId).catch(
703
+ () => undefined,
704
+ )
705
+ this.restoredOutcomeState = {
706
+ resumed: true,
707
+ ...(progress !== undefined ? { snapshot: progress } : {}),
708
+ }
657
709
  this.restoredUserMessage = restored.latestUserMessage
658
710
  this.restoredUserMessageId = restored.latestUserMessage
659
711
  ? document.latestUserMessageId
@@ -92,6 +92,34 @@ export interface TurnContext {
92
92
  trackingConfig: ActivityTrackingConfig
93
93
  }
94
94
 
95
+ /**
96
+ * Forward `signal`'s abort to `controller` without keeping the controller, or
97
+ * anything the turn built around it, alive for the life of `signal`.
98
+ *
99
+ * The session's signal outlives every turn, and a listener on it is strongly
100
+ * held for as long as the signal is. A listener that closed over the turn's
101
+ * controller (and the configuration it was built from) kept each finished
102
+ * turn's whole object graph in memory, one more per turn. This one holds the
103
+ * controller through a `WeakRef`, lives in its own scope so the closure
104
+ * captures nothing else, and is removed once the controller is collected.
105
+ */
106
+ function relayAbort(signal: AbortSignal, controller: AbortController): void {
107
+ const controllerRef = new WeakRef(controller)
108
+ const signalRef = new WeakRef(signal)
109
+ const relay = (): void => {
110
+ controllerRef.deref()?.abort(signalRef.deref()?.reason)
111
+ }
112
+ signal.addEventListener('abort', relay, { once: true })
113
+ relayCleanup.register(controller, { signalRef, relay })
114
+ }
115
+
116
+ const relayCleanup = new FinalizationRegistry<{
117
+ signalRef: WeakRef<AbortSignal>
118
+ relay: () => void
119
+ }>(({ signalRef, relay }) => {
120
+ signalRef.deref()?.removeEventListener('abort', relay)
121
+ })
122
+
95
123
  export class TurnContextFactory {
96
124
  /**
97
125
  * The turn's one correlated logger, built once and handed to every
@@ -136,11 +164,7 @@ export class TurnContextFactory {
136
164
  if (config.signal.aborted) {
137
165
  abortController.abort(config.signal.reason)
138
166
  } else {
139
- config.signal.addEventListener(
140
- 'abort',
141
- () => abortController.abort(config.signal?.reason),
142
- { once: true },
143
- )
167
+ relayAbort(config.signal, abortController)
144
168
  }
145
169
  }
146
170
 
@@ -178,7 +202,10 @@ export class TurnContextFactory {
178
202
  config.enableActivityTracking,
179
203
  )
180
204
  const activityStore = new ActivityStore(config.turnId, trackingConfig)
181
- const planManager = new PlanManager({ sessionId: config.sessionId, turnId: config.turnId })
205
+ const planManager = new PlanManager({
206
+ sessionId: config.sessionId,
207
+ turnId: config.turnId,
208
+ })
182
209
  const toolResultsDir =
183
210
  config.storage.sessionDir === undefined
184
211
  ? undefined
@@ -29,7 +29,11 @@ import { resolveRetainedOutput } from '../../tools/retained-output.js'
29
29
  import type { ToolManager } from '../../toolsets/manager.js'
30
30
  import type { ToolSourceRef } from '../../toolsets/types.js'
31
31
  import type { ToolResultGuardrailSpec } from '../../types/guardrail/index.js'
32
- import type { ToolCallDeclined, ToolCallEscalation } from '../../types/hitl/index.js'
32
+ import type {
33
+ LaunchedScript,
34
+ ToolCallDeclined,
35
+ ToolCallEscalation,
36
+ } from '../../types/hitl/index.js'
33
37
  import type { SessionId, ToolUseId, TurnId } from '../../types/ids/index.js'
34
38
  import type { InvocationState } from '../../types/invocation/index.js'
35
39
  import {
@@ -72,6 +76,12 @@ import { compressShellOutput } from '../../utils/shell-compress.js'
72
76
  import { parseStructuredResultJson } from '../../utils/structured-result-json.js'
73
77
  import { ensureScratchpadDirectory } from '../../utils/temp.js'
74
78
  import { type BackgroundJobRegistry, type JobProcess, bindOwner } from '../jobs/registry.js'
79
+ import {
80
+ type ScriptEvidence,
81
+ analyzeLaunches,
82
+ changedSinceRead,
83
+ readLaunchedScripts,
84
+ } from './auto-review/script-evidence.js'
75
85
  import { type BatchDetach, stillRunningResult } from './detached-calls.js'
76
86
  import {
77
87
  type ToolAdmissionHost,
@@ -378,6 +388,8 @@ export interface ToolExecutorConfig {
378
388
  outsideRootAccess?: 'refuse' | 'review'
379
389
  /** See `QueryParams.sandboxEscape`. Default `'refuse'`. */
380
390
  sandboxEscape?: 'refuse' | 'review'
391
+ /** See `QueryParams.outsideRootRefusal`. */
392
+ outsideRootRefusal?: string
381
393
  /**
382
394
  * Read LIVE, not frozen at turn start.
383
395
  *
@@ -577,6 +589,12 @@ export interface ToolCallOutcome {
577
589
  /** Rich form for the model, when the tool supplied one. */
578
590
  content?: ToolResultContent
579
591
  isError?: boolean
592
+ /**
593
+ * What the call was about and what came of it, as typed fields; the same value
594
+ * `tool_completed` carries. Absent when a post-tool hook replaced the output,
595
+ * so a reader never takes an exit status from a result the hook rewrote.
596
+ */
597
+ facts?: ToolFacts
580
598
  /** Provider argument admission failure, never inferred from receipt text. */
581
599
  inputFailure?: CompletedToolRecord['inputFailure']
582
600
  /** A pre-tool hook deliberately skipped execution; its receipt is not an output candidate. */
@@ -687,7 +705,7 @@ export type PriorToolResults = ReadonlyMap<string, PriorToolResult>
687
705
  * for administrator rights is named as that; everything else is a plain block.
688
706
  * Read from the call's own argument, never from the refusal's wording.
689
707
  */
690
- function refusalFacts(toolName: string, input: unknown): ToolFacts {
708
+ function refusalFacts(toolName: string, input: unknown, reason?: string): ToolFacts {
691
709
  const command =
692
710
  input && typeof input === 'object' ? (input as { command?: unknown }).command : undefined
693
711
  const elevated = typeof command === 'string' && /(^|[\s;&|(])(sudo|doas)\b/.test(command)
@@ -695,9 +713,31 @@ function refusalFacts(toolName: string, input: unknown): ToolFacts {
695
713
  kind: 'failure',
696
714
  reason: elevated ? 'needs_administrator' : 'blocked',
697
715
  subject: toolName,
716
+ ...(refusalNote(reason) ? { note: refusalNote(reason) } : {}),
698
717
  }
699
718
  }
700
719
 
720
+ const REFUSAL_NOTE_MAX = 200
721
+
722
+ /**
723
+ * The refusal's own reason as one line for the person's view: the first
724
+ * non-empty line, the leading "Blocked:" label dropped, first letter up, bounded. The policy
725
+ * wrote it; nothing here reads the model's text.
726
+ */
727
+ function refusalNote(reason: string | undefined): string | undefined {
728
+ const line = reason
729
+ ?.split(/\r?\n/)
730
+ .map((part) => part.trim())
731
+ .find(Boolean)
732
+ if (!line) return undefined
733
+ const plain = line.replace(/^Blocked(?: by [^:]{1,60})?:\s*/i, '').trim()
734
+ if (!plain) return undefined
735
+ const sentence = `${plain.charAt(0).toUpperCase()}${plain.slice(1)}`
736
+ return sentence.length > REFUSAL_NOTE_MAX
737
+ ? `${sentence.slice(0, REFUSAL_NOTE_MAX - 1)}…`
738
+ : sentence
739
+ }
740
+
701
741
  export function deniedToolOutput(toolName: string, reason: string): string {
702
742
  return `Error: Tool "${toolName}" was not executed. ${reason}`
703
743
  }
@@ -732,6 +772,9 @@ export class ToolExecutor {
732
772
  private readonly toolCallBudget?: ToolCallBudget
733
773
  private readonly preparedBatches = new WeakSet<PreparedToolBatch>()
734
774
  private readonly preparedBashDirectories = new WeakMap<PreparedSafetyCall, string>()
775
+ /** Script files a classifier read for a call, by call id, to be checked again when it runs. */
776
+ private readonly reviewedScripts = new Map<string, ScriptEvidence['watch']>()
777
+ private readonly startedAt = Date.now()
735
778
  /** Ephemeral proof of actual execution; restored messages do not acquire it. */
736
779
  private readonly recordedObservations = new Map<
737
780
  string,
@@ -982,6 +1025,47 @@ export class ToolExecutor {
982
1025
  )
983
1026
  }
984
1027
 
1028
+ /**
1029
+ * The script files a shell line launches that the agent wrote or that
1030
+ * appeared during the session, read now for the auto-mode classifier.
1031
+ * Remembers what was read for `callId`, so the file can be checked again
1032
+ * just before the call runs.
1033
+ */
1034
+ async launchedScripts(
1035
+ callId: string,
1036
+ line: string,
1037
+ dialect: ShellDialect,
1038
+ ): Promise<readonly LaunchedScript[]> {
1039
+ if (this.sandboxed) {
1040
+ // A sandbox's files are not on this host's disk. Say so rather than
1041
+ // letting a launch inside one look like a launch of nothing.
1042
+ const analysis = analyzeLaunches(line, dialect)
1043
+ const named = analysis.launches.map((l) => l.path)
1044
+ return named.length === 0 && analysis.unresolved.length === 0
1045
+ ? []
1046
+ : [
1047
+ {
1048
+ path: named[0] ?? '(launch form not resolved)',
1049
+ origin: 'unknown',
1050
+ state: 'unreadable',
1051
+ note: 'the command runs in a sandbox whose files are not read for this review, so what the script does is not known',
1052
+ },
1053
+ ]
1054
+ }
1055
+ const evidence = await readLaunchedScripts(line, dialect, {
1056
+ cwd: effectiveBashWorkingDirectory(this.config.sessionId, this.config.workingDirectory, {
1057
+ sandboxed: false,
1058
+ }),
1059
+ startedAt: this.fileReadTracker.startedAt ?? this.startedAt,
1060
+ ...(this.fileReadTracker.authoredFingerprint
1061
+ ? { authoredFingerprint: (key: string) => this.fileReadTracker.authoredFingerprint?.(key) }
1062
+ : {}),
1063
+ })
1064
+ if (evidence.watch.length > 0) this.reviewedScripts.set(callId, evidence.watch)
1065
+ else this.reviewedScripts.delete(callId)
1066
+ return evidence.scripts
1067
+ }
1068
+
985
1069
  /** The whole of the evidence as one text, from the same ledger. No filesystem I/O. */
986
1070
  describeFileEvidence(messages: readonly Message[]): string | undefined {
987
1071
  return describeVisibleFileEvidence(
@@ -1207,11 +1291,12 @@ export class ToolExecutor {
1207
1291
  toolCall,
1208
1292
  !hostModifiedCallIds?.has(toolCall.id),
1209
1293
  )
1210
- const call = await this.refusedBeforeReview(prepared, earlierPaths)
1294
+ const beforeReview = await this.refusedBeforeReview(prepared, earlierPaths)
1211
1295
  const path = inputPathOf(prepared)
1212
1296
  if (path !== undefined) earlierPaths.push(path)
1297
+ this.admitScratchpad(beforeReview)
1298
+ const call = await this.refusedOutsideRoot(beforeReview)
1213
1299
  calls.set(toolCall.id, call)
1214
- this.admitScratchpad(call)
1215
1300
  const escalation = await this.escalationOrUnfinished(call)
1216
1301
  if (escalation) escalations.set(toolCall.id, escalation)
1217
1302
  if (await this.writesScratchpad(call, escalation)) scratchpadWrites.add(toolCall.id)
@@ -1220,6 +1305,49 @@ export class ToolExecutor {
1220
1305
  return this.publishPreparedBatch(calls, escalations, scratchpadWrites)
1221
1306
  }
1222
1307
 
1308
+ /**
1309
+ * A file tool's path outside the roots, refused with the host's own words
1310
+ * (`QueryParams.outsideRootRefusal`) instead of the tool's generic one.
1311
+ *
1312
+ * It takes the place of the call like any refusal found while preparing it,
1313
+ * so it holds in every permission mode and reaches no reviewer. Only a turn
1314
+ * that refuses (`outsideRootAccess: 'refuse'`) and has no sandbox is looked
1315
+ * at, and only a tool that declares `pathArgument`. The exemptions are the
1316
+ * review's: the scratch folder is a root, and so is this session's retained
1317
+ * output for `read`.
1318
+ */
1319
+ private async refusedOutsideRoot(call: PreparedDirectCall): Promise<PreparedDirectCall> {
1320
+ const note = this.config.outsideRootRefusal
1321
+ if (call.kind !== 'ready' || note === undefined) return call
1322
+ if (this.config.outsideRootAccess !== 'refuse' || this.sandboxed) return call
1323
+ const tool = this.config.tools.get(call.toolName)
1324
+ if (tool?.pathArgument === undefined) return call
1325
+ if (call.input === null || typeof call.input !== 'object') return call
1326
+ const value = (call.input as Record<string, unknown>)[tool.pathArgument]
1327
+ if (typeof value !== 'string') return call
1328
+ try {
1329
+ const retained =
1330
+ tool.name === 'read' && (await resolveRetainedOutput(this.outputDirectory(), value))
1331
+ if (retained) return call
1332
+ const outside = await pathOutsideRoots(this.rootsNow(), value)
1333
+ if (outside === undefined) return call
1334
+ return {
1335
+ kind: 'synthetic',
1336
+ toolCall: call.toolCall,
1337
+ toolName: call.toolName,
1338
+ input: call.input,
1339
+ message: formatFailedToolOutput(
1340
+ undefined,
1341
+ `${outside} is outside the working directory. ${note}`,
1342
+ ),
1343
+ isError: true,
1344
+ }
1345
+ } catch {
1346
+ this.config.abortSignal.throwIfAborted()
1347
+ return call
1348
+ }
1349
+ }
1350
+
1223
1351
  /**
1224
1352
  * A ready call the tool already knows cannot run, answered now instead of
1225
1353
  * after a reviewer has been asked about it (`ToolDefinition.refuseBeforeReview`).
@@ -1461,12 +1589,13 @@ export class ToolExecutor {
1461
1589
  const earlierPaths: string[] = []
1462
1590
  for (const toolCall of response.message.toolCalls ?? []) {
1463
1591
  if (changedCallIds.has(toolCall.id)) {
1464
- const call = await this.refusedBeforeReview(
1592
+ const beforeReview = await this.refusedBeforeReview(
1465
1593
  await prepareDirectCall(this.admissionHost(), toolCall, false),
1466
1594
  earlierPaths,
1467
1595
  )
1596
+ this.admitScratchpad(beforeReview)
1597
+ const call = await this.refusedOutsideRoot(beforeReview)
1468
1598
  calls.set(toolCall.id, call)
1469
- this.admitScratchpad(call)
1470
1599
  const escalation = await this.escalationOrUnfinished(call)
1471
1600
  if (escalation) escalations.set(toolCall.id, escalation)
1472
1601
  else escalations.delete(toolCall.id)
@@ -1730,13 +1859,25 @@ export class ToolExecutor {
1730
1859
  let outcome: ToolCallOutcome
1731
1860
  started.add(toolCall.id)
1732
1861
  try {
1733
- outcome = await this.executeSingle(
1734
- toolCall,
1735
- ctx,
1736
- recordObservation,
1737
- () => progress.close(),
1738
- preparedCall,
1739
- )
1862
+ const reviewedScripts = this.reviewedScripts.get(toolCall.id)
1863
+ this.reviewedScripts.delete(toolCall.id)
1864
+ const moved =
1865
+ reviewedScripts === undefined ? undefined : await changedSinceRead(reviewedScripts)
1866
+ outcome =
1867
+ moved !== undefined
1868
+ ? {
1869
+ toolCallId: toolCall.id,
1870
+ toolName: toolCall.function.name,
1871
+ output: `Error: ${moved} changed after the safety check read it, so the command was not run. Run it again and it will be checked against the new text.`,
1872
+ isError: true,
1873
+ }
1874
+ : await this.executeSingle(
1875
+ toolCall,
1876
+ ctx,
1877
+ recordObservation,
1878
+ () => progress.close(),
1879
+ preparedCall,
1880
+ )
1740
1881
  } catch (error) {
1741
1882
  // A call already answered with the placeholder has no batch
1742
1883
  // left to fail; its failure is its outcome.
@@ -1932,6 +2073,7 @@ export class ToolExecutor {
1932
2073
  } catch (error) {
1933
2074
  failure ??= { error }
1934
2075
  } finally {
2076
+ this.reviewedScripts.delete(unit.toolCall.id)
1935
2077
  settledUnit()
1936
2078
  inFlight.delete(unit)
1937
2079
  pump()
@@ -2269,7 +2411,7 @@ export class ToolExecutor {
2269
2411
  toolName: name,
2270
2412
  toolUseId: nestedId,
2271
2413
  output: rawOutput,
2272
- maxChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
2414
+ maxChars: this.toolOutputCap(this.config.tools.get(name), false),
2273
2415
  retainedPreviewChars: this.config.retainedToolPreviewChars,
2274
2416
  spillDir: this.outputDirectory(),
2275
2417
  recoveryRoutes: this.recoveryRoutes(),
@@ -2734,11 +2876,7 @@ export class ToolExecutor {
2734
2876
  ]
2735
2877
  }
2736
2878
  }
2737
- // A tool that bounds its own output is exempt from the DEFAULT per-result
2738
- // cap only; a host that set the cap explicitly still gets it.
2739
- const maxToolOutputChars =
2740
- this.config.maxToolOutputChars ??
2741
- (executedDefinition?.selfLimitingOutput === true ? 0 : DEFAULT_MAX_TOOL_OUTPUT_CHARS)
2879
+ const maxToolOutputChars = this.toolOutputCap(executedDefinition, true)
2742
2880
  const sourceOutput = output
2743
2881
  // This owner-bound channel follows the selected receipt after both result
2744
2882
  // screening and post-tool replacement. ToolResult.data and raw input can
@@ -2967,6 +3105,7 @@ export class ToolExecutor {
2967
3105
  ? { handoff: result.handoff }
2968
3106
  : {}),
2969
3107
  ...(revealedTools !== undefined ? { revealedTools } : {}),
3108
+ ...(result.facts !== undefined && !postOverride ? { facts: result.facts } : {}),
2970
3109
  }
2971
3110
  }
2972
3111
 
@@ -3465,6 +3604,8 @@ export class ToolExecutor {
3465
3604
  preparedCall?: PreparedDirectCall,
3466
3605
  declined?: ToolCallDeclined,
3467
3606
  ): Promise<ToolCallOutcome> {
3607
+ // A call that does not run has no use for what a review read of its scripts.
3608
+ this.reviewedScripts.delete(toolCall.id)
3468
3609
  const toolName = preparedCall?.toolName ?? toolCall.function.name
3469
3610
  let input: unknown = preparedCall?.input ?? {}
3470
3611
  if (!preparedCall) {
@@ -3514,7 +3655,7 @@ export class ToolExecutor {
3514
3655
  ...(declined ? { presentation: this.declinedPresentation(toolName, input, declined) } : {}),
3515
3656
  // A refusal by policy says what kind it was; the person's own No is
3516
3657
  // already carried by the presentation above.
3517
- ...(declined ? {} : { facts: refusalFacts(toolName, input) }),
3658
+ ...(declined ? {} : { facts: refusalFacts(toolName, input, reason) }),
3518
3659
  isError: true,
3519
3660
  })
3520
3661
 
@@ -3623,6 +3764,7 @@ export class ToolExecutor {
3623
3764
  toolName,
3624
3765
  output: outcome.output,
3625
3766
  isError: outcome.kind === 'error',
3767
+ ...(outcome.facts ? { facts: outcome.facts } : {}),
3626
3768
  ...(outcome.inputFailure ? { inputFailure: outcome.inputFailure } : {}),
3627
3769
  ...(outcome.skipped ? { skipped: true as const } : {}),
3628
3770
  }
@@ -3638,6 +3780,26 @@ export class ToolExecutor {
3638
3780
  })
3639
3781
  }
3640
3782
 
3783
+ /**
3784
+ * The per-result character cap for one tool: the turn's cap, tightened by
3785
+ * the tool's own `maxOutputChars`. A tool that bounds its own output is
3786
+ * exempt from the DEFAULT cap only (`exemptSelfLimiting`); a host that set
3787
+ * the cap explicitly still gets it. 0 means no cap.
3788
+ */
3789
+ private toolOutputCap(
3790
+ definition: ToolDefinition | undefined,
3791
+ exemptSelfLimiting: boolean,
3792
+ ): number {
3793
+ const turn =
3794
+ this.config.maxToolOutputChars ??
3795
+ (exemptSelfLimiting && definition?.selfLimitingOutput === true
3796
+ ? 0
3797
+ : DEFAULT_MAX_TOOL_OUTPUT_CHARS)
3798
+ const own = definition?.maxOutputChars
3799
+ if (own === undefined || !Number.isFinite(own) || own <= 0) return turn
3800
+ return turn > 0 ? Math.min(turn, Math.floor(own)) : Math.floor(own)
3801
+ }
3802
+
3641
3803
  /**
3642
3804
  * The host preview and model text may differ; each gets its own text
3643
3805
  * budget. Rich bytes are measured separately and withheld whole when
@@ -3674,7 +3836,7 @@ export class ToolExecutor {
3674
3836
  toolName,
3675
3837
  toolUseId,
3676
3838
  output: text,
3677
- maxChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
3839
+ maxChars: this.toolOutputCap(this.config.tools.get(toolName), false),
3678
3840
  ...(notice ? { notice } : {}),
3679
3841
  // A tool may return different host and model text. They must
3680
3842
  // never compete for the same exclusive spill filename.
@@ -2,7 +2,7 @@ import type { Span } from '@opentelemetry/api'
2
2
  import { consolidationEntry, isConsolidated } from '../../compaction/consolidation.js'
3
3
  import type { WorkingStateManager } from '../../compaction/manager.js'
4
4
  import { NAMZU } from '../../constants/telemetry/index.js'
5
- import type { SessionEvent, StepResult } from '../../types/session/index.js'
5
+ import type { SessionEvent, StepResult, TurnOutcome } from '../../types/session/index.js'
6
6
  import { toErrorMessage } from '../../utils/error.js'
7
7
  import type { TurnContext } from './context.js'
8
8
  import type { EventTranslator } from './events.js'
@@ -53,6 +53,12 @@ export interface TurnFinalization {
53
53
  * immediately before the record is written.
54
54
  */
55
55
  readonly takeSteps: () => readonly StepResult[]
56
+ /**
57
+ * The turn's outcome record, read here for the same reason as the steps: the
58
+ * workspace is compared once, as late as it can be and before the record is
59
+ * written. Never throws; `undefined` means no record was kept.
60
+ */
61
+ readonly takeOutcome?: () => Promise<TurnOutcome | undefined>
56
62
  /**
57
63
  * Whether the loop already fired `turn_end` (and `subagent_stop`) when the
58
64
  * model finished, so a hook could keep the turn going. Fired there, it is
@@ -108,6 +114,7 @@ export async function* finalizeTurn(
108
114
  // Hand the step record to the turn before it settles, so the
109
115
  // returned `Turn` carries it.
110
116
  ctx.recorder.setSteps(takeSteps())
117
+ if (finalization.takeOutcome) ctx.recorder.setOutcome(await finalization.takeOutcome())
111
118
 
112
119
  // Gates the FINAL result, not the stream — `text_delta` already
113
120
  // reached the host as the model produced it. A rewrite is
@@ -619,6 +619,14 @@ export interface QueryParams {
619
619
  * is never, because the path is not mounted there to be reached.
620
620
  */
621
621
  outsideRootAccess?: 'refuse' | 'review'
622
+ /**
623
+ * The sentence a host wants the model to read when `outsideRootAccess` is
624
+ * `'refuse'` and a file tool names a path outside the roots, for example
625
+ * which setting made the wall and how to move it. Without it the tool's own
626
+ * generic refusal stands. The refusal is made before any review, so no
627
+ * permission mode (bypass included) lifts it.
628
+ */
629
+ outsideRootRefusal?: string
622
630
  /**
623
631
  * Whether a call may ask to run outside the turn's sandbox (the shipped
624
632
  * `bash` tool's `dangerously_disable_sandbox`).
@@ -1599,6 +1607,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
1599
1607
  : {}),
1600
1608
  ...(scratchpadDirectory ? { scratchpadDirectory } : {}),
1601
1609
  ...(params.outsideRootAccess ? { outsideRootAccess: params.outsideRootAccess } : {}),
1610
+ ...(params.outsideRootRefusal ? { outsideRootRefusal: params.outsideRootRefusal } : {}),
1602
1611
  ...(params.sandboxEscape ? { sandboxEscape: params.sandboxEscape } : {}),
1603
1612
  permissionMode: () => ctx.permissionMode.current,
1604
1613
  env: turnConfig.env ?? {},
@@ -2823,6 +2832,9 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2823
2832
  }
2824
2833
  }
2825
2834
 
2835
+ // The parked batch runs here, outside the loop's own steps, so
2836
+ // the outcome record is told about it here.
2837
+ await iterationOrchestrator.outcomeBefore(pendingResume.response)
2826
2838
  const resumedBatch = await applyPendingResume(
2827
2839
  pendingResume,
2828
2840
  ctx.recorder,
@@ -2831,6 +2843,11 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2831
2843
  pendingResumeAssistantId,
2832
2844
  )
2833
2845
  iterationOrchestrator.setResumedToolResults(resumedBatch.results)
2846
+ await iterationOrchestrator.outcomeObserve(
2847
+ ctx.recorder.currentIteration,
2848
+ pendingResume.response,
2849
+ resumedBatch.results,
2850
+ )
2834
2851
  yield* eventTranslator.drainPending()
2835
2852
  }
2836
2853
 
@@ -2911,6 +2928,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2911
2928
  params,
2912
2929
  eventTranslator,
2913
2930
  takeSteps: () => iterationOrchestrator.getSteps(),
2931
+ takeOutcome: () => iterationOrchestrator.takeOutcome(),
2914
2932
  turnEndHooksFired: () => iterationOrchestrator.turnEndHooksFired,
2915
2933
  workingStateManager,
2916
2934
  resultAssembler,
@@ -2933,6 +2951,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
2933
2951
  if (isTurnInProgressError(err) && !ctx.recorder.isActive) throw err
2934
2952
  // A failed turn still spent its steps; report them.
2935
2953
  ctx.recorder.setSteps(iterationOrchestrator.getSteps())
2954
+ ctx.recorder.setOutcome(await iterationOrchestrator.takeOutcome())
2936
2955
  await executeUserInterruptHooks(err)
2937
2956
  yield* eventTranslator.drainPending()
2938
2957
  await executeTurnFailureHooks(err)