@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
@@ -15,6 +15,9 @@ import { VCS_NAMES, createIgnoreStack } from './ignore-matcher.js'
15
15
  /** Entries a walk examines before it stops with what it has: a safety valve, not a budget. */
16
16
  export const DEFAULT_MAX_VISITED_ENTRIES = 500_000
17
17
 
18
+ /** Longest the walk holds the event loop between yields. */
19
+ const WALK_SLICE_MS = 8
20
+
18
21
  const GLOBSTAR = Symbol('globstar')
19
22
  type Segment = string | { source: string; flags: string } | typeof GLOBSTAR
20
23
 
@@ -185,6 +188,7 @@ async function* executeWalk(
185
188
  plan: WalkPlan,
186
189
  signal?: AbortSignal,
187
190
  onSummary?: (summary: SandboxWalkSummary) => void,
191
+ withoutSizes = false,
188
192
  ): AsyncGenerator<SandboxFileEntry> {
189
193
  const fs = await import('node:fs/promises')
190
194
  const path = await import('node:path')
@@ -196,6 +200,7 @@ async function* executeWalk(
196
200
  /** Whether leaving this directory also drops its ignore layer. */
197
201
  layered: boolean
198
202
  }[] = []
203
+ let sliceStart = performance.now()
199
204
  let visited = 0
200
205
  let emitted = 0
201
206
  let ignoredDirectories = 0
@@ -327,8 +332,22 @@ async function* executeWalk(
327
332
  while (open.length > 0) {
328
333
  check()
329
334
  const frame = open[open.length - 1] as (typeof open)[number]
330
- const entry = await wait(frame.dir.read())
331
- check()
335
+ let entry: import('node:fs').Dirent | null
336
+ if (withoutSizes) {
337
+ // A local search consumer reads a directory's buffered entries
338
+ // synchronously: one promise per entry costs more than the listing.
339
+ // The loop yields on a time slice, and a blocked read cannot be
340
+ // raced, which only the default path promises.
341
+ if (performance.now() - sliceStart > WALK_SLICE_MS) {
342
+ await new Promise<void>((done) => setImmediate(done))
343
+ sliceStart = performance.now()
344
+ }
345
+ check()
346
+ entry = frame.dir.readSync()
347
+ } else {
348
+ entry = await wait(frame.dir.read())
349
+ check()
350
+ }
332
351
  if (!entry) {
333
352
  open.pop()
334
353
  if (frame.layered) ignore?.leave()
@@ -351,6 +370,13 @@ async function* executeWalk(
351
370
  }
352
371
  if (!entry.isFile() || !matchers.some((matcher) => matcher.test(relative))) continue
353
372
  if (ignore?.isIgnored(absolute, false)) continue
373
+ if (withoutSizes) {
374
+ // The caller opens the file anyway and reads its size from the handle.
375
+ emitted += 1
376
+ yield { path: absolute, size: 0 }
377
+ if (emitted >= plan.maxEntries) return
378
+ continue
379
+ }
354
380
  let info: import('node:fs').Stats
355
381
  try {
356
382
  info = await wait(fs.lstat(absolute))
@@ -384,10 +410,21 @@ function globalIgnoreFiles(): string[] {
384
410
  /** Internal host path; the caller first resolves its authorized filesystem root. */
385
411
  export async function* walkFilesLocally(
386
412
  rootPath: string,
387
- options: SandboxWalkFilesOptions,
413
+ options: SandboxWalkFilesOptions & {
414
+ /**
415
+ * Skip the per-file size read: entries carry `size: 0`. For a caller that
416
+ * opens every file itself and takes the size from the handle.
417
+ */
418
+ readonly withoutSizes?: boolean
419
+ },
388
420
  ): AsyncGenerator<SandboxFileEntry> {
389
421
  options.signal?.throwIfAborted()
390
- yield* executeWalk(planWalk(rootPath, options), options.signal, options.onSummary)
422
+ yield* executeWalk(
423
+ planWalk(rootPath, options),
424
+ options.signal,
425
+ options.onSummary,
426
+ options.withoutSizes === true,
427
+ )
391
428
  }
392
429
 
393
430
  /** The existing sandbox execution seam; no separate worker protocol is needed. */
@@ -7,22 +7,24 @@
7
7
  */
8
8
 
9
9
  // Written for the `u` flag, which every compiled expression carries.
10
- const POSIX_CLASSES: Record<string, string> = {
11
- alnum: '\\p{L}\\p{Nl}\\p{Nd}',
12
- alpha: '\\p{L}\\p{Nl}',
13
- ascii: '\\x00-\\x7f',
14
- blank: '\\p{Zs}\\t',
15
- cntrl: '\\p{Cc}',
16
- digit: '\\p{Nd}',
17
- graph: '\\x21-\\x7e',
18
- lower: '\\p{Ll}',
19
- print: '\\x20-\\x7e',
20
- punct: '\\p{P}\\p{S}',
21
- space: '\\s',
22
- upper: '\\p{Lu}',
23
- word: '\\p{L}\\p{Nl}\\p{Nd}_',
24
- xdigit: '0-9a-fA-F',
25
- }
10
+ const POSIX_CLASSES: ReadonlyMap<string, string> = new Map(
11
+ Object.entries({
12
+ alnum: '\\p{L}\\p{Nl}\\p{Nd}',
13
+ alpha: '\\p{L}\\p{Nl}',
14
+ ascii: '\\x00-\\x7f',
15
+ blank: '\\p{Zs}\\t',
16
+ cntrl: '\\p{Cc}',
17
+ digit: '\\p{Nd}',
18
+ graph: '\\x21-\\x7e',
19
+ lower: '\\p{Ll}',
20
+ print: '\\x20-\\x7e',
21
+ punct: '\\p{P}\\p{S}',
22
+ space: '\\s',
23
+ upper: '\\p{Lu}',
24
+ word: '\\p{L}\\p{Nl}\\p{Nd}_',
25
+ xdigit: '0-9a-fA-F',
26
+ }),
27
+ )
26
28
 
27
29
  export interface CompiledSegment {
28
30
  /** Set when the segment has no magic: the exact name it matches. */
@@ -31,6 +33,23 @@ export interface CompiledSegment {
31
33
  source: string
32
34
  }
33
35
 
36
+ /**
37
+ * The longest segment, in UTF-16 code units, that `compileSegment` accepts. A
38
+ * file name is at most 255 bytes on the file systems Namzu runs on, so a
39
+ * pattern for one name beyond four times that is never legitimate, while
40
+ * compiling a segment costs time in proportion to the square of its length.
41
+ */
42
+ export const MAX_GLOB_SEGMENT_LENGTH = 1024
43
+
44
+ /** How much of an over-long pattern an error message shows, in code points. */
45
+ const PREVIEW_LENGTH = 60
46
+
47
+ /** The first `PREVIEW_LENGTH` code points of `text`, never cutting a surrogate pair. */
48
+ const preview = (text: string) =>
49
+ Array.from(text.slice(0, PREVIEW_LENGTH * 2))
50
+ .slice(0, PREVIEW_LENGTH)
51
+ .join('')
52
+
34
53
  /**
35
54
  * Raised for a segment that repeats a group (`*( )` or `+( )`) which could match
36
55
  * the same text more than one way. Such a group backtracks exponentially on a
@@ -38,14 +57,17 @@ export interface CompiledSegment {
38
57
  */
39
58
  export class GlobTooComplexError extends Error {
40
59
  readonly segment: string
41
- constructor(segment: string, reason: 'ambiguous' | 'stretch' = 'ambiguous') {
60
+ constructor(segment: string, reason: 'ambiguous' | 'stretch' | 'length' = 'ambiguous') {
42
61
  super(
43
- reason === 'stretch'
44
- ? `The pattern '${segment}' has more than three wildcards that can stretch; split it across folders or use fewer *.`
45
- : `The pattern '${segment}' repeats a group that can match the same text more than one way; use ** or {a,b} instead.`,
62
+ reason === 'length'
63
+ ? `The pattern '${preview(segment)}...' is too long (${segment.length} characters; at most ${MAX_GLOB_SEGMENT_LENGTH} between slashes); shorten it or split it across folders.`
64
+ : reason === 'stretch'
65
+ ? `The pattern '${segment}' has more than three wildcards that can stretch; split it across folders or use fewer *.`
66
+ : `The pattern '${segment}' repeats a group that can match the same text more than one way; use ** or {a,b} instead.`,
46
67
  )
47
68
  this.name = 'GlobTooComplexError'
48
- this.segment = segment
69
+ // An over-long pattern is not kept whole: the error may be logged.
70
+ this.segment = reason === 'length' ? preview(segment) : segment
49
71
  }
50
72
  }
51
73
 
@@ -85,7 +107,7 @@ function classEnd(text: string, start: number): number {
85
107
  if (ch === '\\') index += 1
86
108
  else if (ch === '[' && text[index + 1] === ':') {
87
109
  const end = text.indexOf(':]', index + 2)
88
- if (end > 0 && POSIX_CLASSES[text.slice(index + 2, end)]) index = end + 1
110
+ if (end > 0 && POSIX_CLASSES.has(text.slice(index + 2, end))) index = end + 1
89
111
  } else if (ch === ']') return index
90
112
  }
91
113
  return -1
@@ -134,8 +156,9 @@ function classSource(body: string): string {
134
156
  if (ch === '[' && body[index + 1] === ':') {
135
157
  const end = body.indexOf(':]', index + 2)
136
158
  const name = end > 0 ? body.slice(index + 2, end) : ''
137
- if (POSIX_CLASSES[name]) {
138
- out += POSIX_CLASSES[name]
159
+ const members = POSIX_CLASSES.get(name)
160
+ if (members !== undefined) {
161
+ out += members
139
162
  index = end + 1
140
163
  continue
141
164
  }
@@ -268,6 +291,7 @@ function isOneGroup(text: string): boolean {
268
291
  * segment that itself begins with a literal `.` can.
269
292
  */
270
293
  export function compileSegment(segment: string, dot: boolean): CompiledSegment {
294
+ if (segment.length > MAX_GLOB_SEGMENT_LENGTH) throw new GlobTooComplexError(segment, 'length')
271
295
  const guard = dot ? '' : '(?!\\.)'
272
296
  let parsed: Parsed
273
297
  try {
@@ -62,6 +62,21 @@ export interface CheckpointLogView {
62
62
  * `decision_resolved` or `decision_expired` yet. `prune` never deletes one.
63
63
  */
64
64
  openDecisionCheckpoints(scope: CheckpointScope): Promise<Iterable<CheckpointId>>
65
+ /**
66
+ * Both answers `prune` needs in one read of the log: for each of
67
+ * `checkpointIds`, what `writtenDocSha256` would return (absent from
68
+ * `written` when there is no record), and what `openDecisionCheckpoints`
69
+ * would return. Optional: a view without it is asked the two questions
70
+ * separately, one walk of the log each, which costs a walk per stored
71
+ * checkpoint on every prune.
72
+ */
73
+ pruneFacts?(
74
+ scope: CheckpointScope,
75
+ checkpointIds: readonly CheckpointId[],
76
+ ): Promise<{
77
+ readonly written: ReadonlyMap<CheckpointId, string>
78
+ readonly open: Iterable<CheckpointId>
79
+ }>
65
80
  }
66
81
 
67
82
  /** Why a restore refused a checkpoint. */
@@ -1,4 +1,4 @@
1
- import { link, mkdir, open, readFile, readdir, unlink } from 'node:fs/promises'
1
+ import { link, mkdir, open, readFile, readdir, stat, unlink } from 'node:fs/promises'
2
2
  import type { FileHandle } from 'node:fs/promises'
3
3
  import { dirname } from 'node:path'
4
4
  import type { SessionLocator, SessionPaths } from '../../session/paths.js'
@@ -38,6 +38,17 @@ export interface DiskSessionCheckpointStoreOptions {
38
38
 
39
39
  const DOCUMENT = /^(.+)\.json$/
40
40
 
41
+ /**
42
+ * Which turn a checkpoint document on disk belongs to, as last read, with the
43
+ * size and mtime it had then. A session keeps its checkpoints for every turn
44
+ * in one directory, so listing one turn's used to read and parse every
45
+ * document of the session, and the directory only grows. A document is not
46
+ * read again while a `stat` says it is the one that was; one that belonged to
47
+ * another turn is skipped on that evidence alone.
48
+ */
49
+ const documentTurns = new Map<string, { turnId: string; size: number; mtimeMs: number }>()
50
+ const DOCUMENT_TURNS_KEPT = 20_000
51
+
41
52
  /**
42
53
  * {@link SessionCheckpointStore} at `<session-dir>/checkpoints/<id>.json`.
43
54
  *
@@ -145,10 +156,31 @@ export class DiskSessionCheckpointStore implements SessionCheckpointStore {
145
156
  // Sidecars (`<id>.json.<pid>.<n>.<hex>.tmp`) and strangers are not documents.
146
157
  const id = DOCUMENT.exec(name)?.[1]
147
158
  if (id === undefined || !isEntityId(id, 'checkpoint')) continue
148
- const text = await readIfPresent(this.#paths.checkpointFile(locator, id))
159
+ const path = this.#paths.checkpointFile(locator, id)
160
+ const seen = await stat(path).catch((error: NodeJS.ErrnoException) => {
161
+ if (error.code === 'ENOENT') return null
162
+ throw error
163
+ })
149
164
  // Deleted between the listing and the read: a concurrent prune, not damage.
165
+ if (seen === null) continue
166
+ const known = documentTurns.get(path)
167
+ if (
168
+ known !== undefined &&
169
+ known.turnId !== checked.turnId &&
170
+ known.size === seen.size &&
171
+ known.mtimeMs === seen.mtimeMs
172
+ ) {
173
+ continue
174
+ }
175
+ const text = await readIfPresent(path)
150
176
  if (text === null) continue
151
177
  const checkpoint = parseStoredCheckpoint(text, checked, id)
178
+ if (documentTurns.size >= DOCUMENT_TURNS_KEPT) documentTurns.clear()
179
+ documentTurns.set(path, {
180
+ turnId: checkpoint.turnId,
181
+ size: seen.size,
182
+ mtimeMs: seen.mtimeMs,
183
+ })
152
184
  if (checkpoint.turnId === checked.turnId) checkpoints.push(checkpoint)
153
185
  }
154
186
  return checkpoints.sort(compareCheckpoints)
@@ -79,6 +79,19 @@ export async function selectStoredCheckpointsToPrune(
79
79
  if (!Number.isSafeInteger(keepLast) || keepLast < 0) {
80
80
  throw new RangeError(`keepLast must be a nonnegative integer, got ${String(keepLast)}`)
81
81
  }
82
+ if (log.pruneFacts) {
83
+ // One read of the log answers both questions for every stored
84
+ // checkpoint, instead of one full walk each.
85
+ const facts = await log.pruneFacts(
86
+ scope,
87
+ stored.map((checkpoint) => checkpoint.checkpointId),
88
+ )
89
+ return selectSessionCheckpointsToPrune(
90
+ stored.filter((checkpoint) => facts.written.has(checkpoint.checkpointId)),
91
+ keepLast,
92
+ new Set(facts.open),
93
+ )
94
+ }
82
95
  const committed: Checkpoint[] = []
83
96
  for (const checkpoint of stored) {
84
97
  if ((await log.writtenDocSha256(scope, checkpoint.checkpointId)) !== null) {
@@ -157,22 +157,64 @@ export function createSessionEvidenceSource(
157
157
  })
158
158
  }
159
159
 
160
+ /** What a scan of a session log's complete records, from byte 0 up to `consumed`, found. */
161
+ interface SessionScan {
162
+ readonly identity: string
163
+ /** Offset just past the last complete record scanned. */
164
+ readonly consumed: number
165
+ readonly first: Buffer
166
+ readonly active: string | undefined
167
+ readonly turnClosed: boolean
168
+ }
169
+
170
+ /**
171
+ * Scans already done, by log file and scope, so that an access reads only the
172
+ * records appended since the last one instead of parsing the whole log again.
173
+ * A session log only grows, and every search page makes several accesses.
174
+ */
175
+ const SCAN_CACHE_LIMIT = 64
176
+ const scanCache = new Map<string, SessionScan>()
177
+
178
+ function remember(key: string, scan: SessionScan): void {
179
+ scanCache.delete(key)
180
+ scanCache.set(key, scan)
181
+ if (scanCache.size > SCAN_CACHE_LIMIT) {
182
+ const oldest = scanCache.keys().next().value
183
+ if (oldest !== undefined) scanCache.delete(oldest)
184
+ }
185
+ }
186
+
160
187
  /**
161
188
  * The first record of a session log (its `session_started`) and whether the
162
189
  * scope is closed: no turn of the session active, or — for a scope narrowed
163
190
  * to one turn — that turn settled.
191
+ *
192
+ * The scan resumes from the one a previous access left in `scanCache` when it
193
+ * is of the same file (same identity, first record unchanged on disk) and
194
+ * ended at or before `size`; otherwise it starts again from byte 0.
164
195
  */
165
196
  async function sessionState(
166
197
  handle: FileHandle,
167
198
  size: number,
168
199
  scope: z.infer<typeof scopeSchema>,
200
+ cacheKey: string,
201
+ identity: string,
169
202
  ): Promise<{ first: Buffer; closed: boolean }> {
170
203
  const unmetered: EvidenceBudget = { bytes: 0, limit: Number.MAX_SAFE_INTEGER }
171
204
  let first: Buffer | undefined
172
205
  let active: string | undefined
173
206
  let turnClosed = false
174
- let buffered = Buffer.alloc(0)
175
207
  let offset = 0
208
+ const cached = scanCache.get(cacheKey)
209
+ if (cached && cached.identity === identity && cached.consumed <= size) {
210
+ const head = await readBytes(handle, 0, cached.first.length, unmetered)
211
+ if (head.equals(cached.first)) {
212
+ ;({ first, active, turnClosed } = cached)
213
+ offset = cached.consumed
214
+ }
215
+ }
216
+ let consumed = offset
217
+ let buffered = Buffer.alloc(0)
176
218
  while (offset < size || buffered.length > 0) {
177
219
  let newline = buffered.indexOf(10)
178
220
  if (newline < 0) {
@@ -186,6 +228,7 @@ async function sessionState(
186
228
  const raw = buffered.subarray(0, newline + 1)
187
229
  buffered = buffered.subarray(newline + 1)
188
230
  newline = -1
231
+ consumed += raw.length
189
232
  const record = JSON.parse(decode(raw)) as Record<string, unknown>
190
233
  if (!first) {
191
234
  if (
@@ -206,6 +249,12 @@ async function sessionState(
206
249
  }
207
250
  }
208
251
  if (!first) throw new Error('The session log is empty; evidence is incomplete.')
252
+ // A source anchored earlier in the log scans less than another access has;
253
+ // keep the longer scan for the next one.
254
+ const kept = scanCache.get(cacheKey)
255
+ if (!kept || kept.identity !== identity || kept.consumed <= consumed) {
256
+ remember(cacheKey, { identity, consumed, first, active, turnClosed })
257
+ }
209
258
  return { first, closed: scope.turnId !== undefined ? turnClosed : active === undefined }
210
259
  }
211
260
 
@@ -258,7 +307,13 @@ function createSource(
258
307
  ? await completePrefix(handle, before.size, budget)
259
308
  : before.size
260
309
  }
261
- const state = await sessionState(handle, size, scope)
310
+ const state = await sessionState(
311
+ handle,
312
+ size,
313
+ scope,
314
+ `${logPath}\0${scopeKey}`,
315
+ fileIdentity(before),
316
+ )
262
317
  if (!state.closed && consistency === 'closed') {
263
318
  throw new Error('Evidence scope has an active turn; it is not closed.')
264
319
  }
@@ -0,0 +1,147 @@
1
+ import type { RecordPointer } from '../../types/session/records.js'
2
+ import { type SessionLogEntry, SessionLogIntegrityError } from './chain.js'
3
+ import type { SessionLog } from './core.js'
4
+
5
+ /**
6
+ * Reductions over a whole session log, advanced rather than recomputed.
7
+ *
8
+ * A session log only grows, and every turn asks it the same questions (which
9
+ * checkpoints were committed, which turn ids exist, what the folded context
10
+ * is). Answering each from byte 0 parses and verifies every record the
11
+ * session ever wrote, so a turn cost more the longer the session. A reduction
12
+ * held here remembers the last record it folded and, on the next ask, walks
13
+ * only what was appended after it. `SessionLog.read({ after })` checks that
14
+ * record against the log byte for byte before continuing, so a log that was
15
+ * replaced, truncated or rewritten at that record is noticed and the
16
+ * reduction starts again from byte 0 with the original behaviour (strict reads
17
+ * refuse a broken chain, tolerant reads stop at the break).
18
+ *
19
+ * What a resumed read does not do is re-verify the records before the anchor:
20
+ * they were verified when they were first folded, by this process. A byte
21
+ * flipped in them afterwards, with every later record left as it was, is not
22
+ * noticed by this path; a fresh process (a resume) always starts from byte 0.
23
+ *
24
+ * The cache is keyed by session id and the reduction's own name and mode,
25
+ * holds a few sessions, and is anchored by content, so two logs that share a
26
+ * session id never share a result.
27
+ *
28
+ * Cost that remains: the state of the "every message ever recorded" reduction
29
+ * holds each message the log ever recorded, superseded and compacted ones
30
+ * included, so it grows with the log and not with the live context, and it
31
+ * lives for the life of the process (for the sessions kept). Reading it
32
+ * returns a copy of all of them, which is O(history) per turn. Both are small
33
+ * next to the whole-log walks this file removes, and are left as they are.
34
+ * More than SESSIONS_KEPT sessions in use at once evict one another, and each
35
+ * eviction restarts from byte 0, the cost before this file existed.
36
+ */
37
+
38
+ const SESSIONS_KEPT = 4
39
+
40
+ interface Kept {
41
+ state: unknown
42
+ /** The last record folded; the next read resumes after it. */
43
+ anchor: RecordPointer | undefined
44
+ /** Calls on one reduction take turns: the state is advanced in place. */
45
+ turn: Promise<unknown>
46
+ }
47
+
48
+ const sessions = new Map<string, Map<string, Kept>>()
49
+
50
+ function keptFor(sessionId: string, key: string): Kept {
51
+ let reductions = sessions.get(sessionId)
52
+ if (reductions === undefined) {
53
+ reductions = new Map()
54
+ if (sessions.size >= SESSIONS_KEPT) {
55
+ const oldest = sessions.keys().next().value
56
+ if (oldest !== undefined) sessions.delete(oldest)
57
+ }
58
+ } else {
59
+ sessions.delete(sessionId)
60
+ }
61
+ sessions.set(sessionId, reductions)
62
+ let kept = reductions.get(key)
63
+ if (kept === undefined) {
64
+ kept = { state: undefined, anchor: undefined, turn: Promise.resolve() }
65
+ reductions.set(key, kept)
66
+ }
67
+ return kept
68
+ }
69
+
70
+ export interface IncrementalFold<S, R> {
71
+ /** The reduction's name; one cache slot per name and read mode. */
72
+ readonly name: string
73
+ readonly mode: 'strict' | 'tolerant'
74
+ /** A fresh, empty state. */
75
+ create(): S
76
+ /** Fold one record into the state, in place. */
77
+ apply(state: S, entry: SessionLogEntry): void
78
+ /**
79
+ * The answer, from a state that is current for as long as this call runs.
80
+ * It must not keep or hand out the state itself, which later calls advance.
81
+ */
82
+ result(state: S): Promise<R> | R
83
+ }
84
+
85
+ /**
86
+ * Fold `log` into `fold.create()` through `fold.apply`, resuming from the
87
+ * record the previous call for this session stopped at when the log still
88
+ * holds it, and answer with `fold.result`.
89
+ */
90
+ export async function foldSessionLog<S, R>(
91
+ log: SessionLog,
92
+ fold: IncrementalFold<S, R>,
93
+ ): Promise<R> {
94
+ const kept = keptFor(log.sessionId, `${fold.name}:${fold.mode}`)
95
+ const run = async (): Promise<R> => {
96
+ let state = kept.state as S | undefined
97
+ let anchor = kept.anchor
98
+ for (let attempt = 0; attempt < 2; attempt++) {
99
+ const resuming = state !== undefined && anchor !== undefined
100
+ if (!resuming) {
101
+ state = fold.create()
102
+ anchor = undefined
103
+ }
104
+ let moved = false
105
+ try {
106
+ const walk = log.read({ mode: fold.mode, ...(anchor ? { after: anchor } : {}) })
107
+ let summary: Awaited<ReturnType<typeof walk.next>>
108
+ for (;;) {
109
+ summary = await walk.next()
110
+ if (summary.done) break
111
+ moved = true
112
+ fold.apply(state as S, summary.value)
113
+ }
114
+ const ended = summary.value
115
+ if (resuming && !moved && ended.break?.reason === 'anchor-mismatch') {
116
+ kept.state = undefined
117
+ kept.anchor = undefined
118
+ state = undefined
119
+ anchor = undefined
120
+ continue
121
+ }
122
+ kept.state = state
123
+ kept.anchor = ended.head?.pointer ?? anchor
124
+ return await fold.result(state as S)
125
+ } catch (error) {
126
+ // The state may be half-advanced: never reuse it.
127
+ kept.state = undefined
128
+ kept.anchor = undefined
129
+ if (
130
+ resuming &&
131
+ !moved &&
132
+ error instanceof SessionLogIntegrityError &&
133
+ error.reason === 'anchor-mismatch'
134
+ ) {
135
+ state = undefined
136
+ anchor = undefined
137
+ continue
138
+ }
139
+ throw error
140
+ }
141
+ }
142
+ throw new Error('unreachable')
143
+ }
144
+ const result = kept.turn.then(run, run)
145
+ kept.turn = result.catch(() => undefined)
146
+ return result
147
+ }
@@ -61,6 +61,13 @@ export {
61
61
  withMessageId,
62
62
  } from './fold.js'
63
63
  export { type TornTailRepair, repairRecordDraft } from './heal.js'
64
+ export {
65
+ foldChildOutcomes,
66
+ foldTurnOutcomes,
67
+ parseTurnOutcome,
68
+ readOutcomeProgress,
69
+ readTurnOutcome,
70
+ } from './turn-outcome.js'
64
71
  export {
65
72
  type ReleaseHeldSessionLeasesOptions,
66
73
  type ReleaseHeldSessionLeasesResult,
@@ -0,0 +1,84 @@
1
+ import type { CheckpointId, SessionId, TurnId } from '../../types/ids/index.js'
2
+ import { type TurnOutcome, TurnOutcomeSchema } from '../../types/session/outcome.js'
3
+ import type { SessionRecord } from '../../types/session/records.js'
4
+ import type { SessionLog } from './core.js'
5
+
6
+ /**
7
+ * Reading the turn's outcome record back out of the session log.
8
+ *
9
+ * It lives in three records of its own (`turn_outcome_recorded`,
10
+ * `child_outcome_recorded`, `outcome_progress_written`), each flagged
11
+ * `ignorable`, and never in a field of an older record: a release that
12
+ * validates `turn_completed`, `child_session_ended` or the checkpoint strictly
13
+ * reads a log that has them and loses only the outcome. The fold of messages
14
+ * does not look at them. A body that does not parse as a `TurnOutcome` reads
15
+ * as no outcome, never as a guess.
16
+ */
17
+
18
+ /** `value` as a {@link TurnOutcome}, or `undefined` when it is not one. */
19
+ export function parseTurnOutcome(value: unknown): TurnOutcome | undefined {
20
+ const parsed = TurnOutcomeSchema.safeParse(value)
21
+ return parsed.success ? parsed.data : undefined
22
+ }
23
+
24
+ /**
25
+ * The outcome of each turn that recorded one, from records in log order. A
26
+ * turn that recorded none is absent, so a caller never reads `unchanged` into
27
+ * a turn nobody measured.
28
+ */
29
+ export function foldTurnOutcomes(records: Iterable<SessionRecord>): Map<TurnId, TurnOutcome> {
30
+ const outcomes = new Map<TurnId, TurnOutcome>()
31
+ for (const record of records) {
32
+ if (record.type !== 'turn_outcome_recorded') continue
33
+ const outcome = parseTurnOutcome(record.outcome)
34
+ if (outcome) outcomes.set(record.turnId, outcome)
35
+ else outcomes.delete(record.turnId)
36
+ }
37
+ return outcomes
38
+ }
39
+
40
+ /** The outcome `turnId` recorded, or `undefined`. */
41
+ export async function readTurnOutcome(
42
+ log: SessionLog,
43
+ turnId: TurnId,
44
+ ): Promise<TurnOutcome | undefined> {
45
+ let found: TurnOutcome | undefined
46
+ for await (const { record } of log.read({ mode: 'tolerant' })) {
47
+ if (record.type !== 'turn_outcome_recorded' || record.turnId !== turnId) continue
48
+ found = parseTurnOutcome(record.outcome)
49
+ }
50
+ return found
51
+ }
52
+
53
+ /**
54
+ * The outcome a child's settled turn recorded in the parent's log, by child
55
+ * session id. A child with no record is absent.
56
+ */
57
+ export function foldChildOutcomes(records: Iterable<SessionRecord>): Map<SessionId, TurnOutcome> {
58
+ const outcomes = new Map<SessionId, TurnOutcome>()
59
+ for (const record of records) {
60
+ if (record.type !== 'child_outcome_recorded') continue
61
+ const outcome = parseTurnOutcome(record.outcome)
62
+ if (outcome) outcomes.set(record.childSessionId, outcome)
63
+ else outcomes.delete(record.childSessionId)
64
+ }
65
+ return outcomes
66
+ }
67
+
68
+ /**
69
+ * What the outcome record had gathered when `checkpointId` was written, or
70
+ * `undefined` when no progress record names it (a checkpoint written before
71
+ * the record existed, or by a release that does not write it).
72
+ */
73
+ export async function readOutcomeProgress(
74
+ log: SessionLog,
75
+ checkpointId: CheckpointId,
76
+ ): Promise<Readonly<Record<string, unknown>> | undefined> {
77
+ let found: Readonly<Record<string, unknown>> | undefined
78
+ for await (const { record } of log.read({ mode: 'tolerant' })) {
79
+ if (record.type === 'outcome_progress_written' && record.checkpointId === checkpointId) {
80
+ found = record.progress
81
+ }
82
+ }
83
+ return found
84
+ }
@@ -189,6 +189,7 @@ export function admitToolFacts(value: unknown): ToolFacts | undefined {
189
189
  kind: 'failure',
190
190
  reason,
191
191
  subject: text(value.subject),
192
+ note: text(value.note),
192
193
  })
193
194
  }
194
195
  default: