@nathapp/nax-agent 0.1.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 (398) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +49 -0
  4. package/dist/coding-tools/coding-tool-bash.d.ts +46 -0
  5. package/dist/coding-tools/coding-tool-bash.js +44 -0
  6. package/dist/coding-tools/coding-tool-extras.d.ts +35 -0
  7. package/dist/coding-tools/coding-tool-extras.js +56 -0
  8. package/dist/coding-tools/coding-tool-sandbox.d.ts +74 -0
  9. package/dist/coding-tools/coding-tool-sandbox.js +215 -0
  10. package/dist/coding-tools/coding-tool-support.d.ts +132 -0
  11. package/dist/coding-tools/coding-tool-support.js +159 -0
  12. package/dist/coding-tools/universal-coding-tools.d.ts +11 -0
  13. package/dist/coding-tools/universal-coding-tools.js +5 -0
  14. package/dist/command-interceptor/index.d.ts +55 -0
  15. package/dist/command-interceptor/index.js +60 -0
  16. package/dist/command-interceptor/shell.d.ts +36 -0
  17. package/dist/command-interceptor/shell.js +137 -0
  18. package/dist/command-safety/build.d.ts +19 -0
  19. package/dist/command-safety/build.js +47 -0
  20. package/dist/command-safety/guard.d.ts +32 -0
  21. package/dist/command-safety/guard.js +179 -0
  22. package/dist/command-safety/identifiers.d.ts +24 -0
  23. package/dist/command-safety/identifiers.js +20 -0
  24. package/dist/command-safety/index.d.ts +17 -0
  25. package/dist/command-safety/index.js +11 -0
  26. package/dist/command-safety/questions.d.ts +19 -0
  27. package/dist/command-safety/questions.js +60 -0
  28. package/dist/command-safety/row.d.ts +2 -0
  29. package/dist/command-safety/row.js +14 -0
  30. package/dist/command-safety/rule-scorer.d.ts +9 -0
  31. package/dist/command-safety/rule-scorer.js +391 -0
  32. package/dist/command-safety/shadow.d.ts +33 -0
  33. package/dist/command-safety/shadow.js +188 -0
  34. package/dist/command-safety/systemone-client.d.ts +16 -0
  35. package/dist/command-safety/systemone-client.js +92 -0
  36. package/dist/command-safety/tap.d.ts +37 -0
  37. package/dist/command-safety/tap.js +77 -0
  38. package/dist/command-safety/temp-only.d.ts +6 -0
  39. package/dist/command-safety/temp-only.js +215 -0
  40. package/dist/command-safety/tmp-write.d.ts +2 -0
  41. package/dist/command-safety/tmp-write.js +112 -0
  42. package/dist/command-safety/types.d.ts +185 -0
  43. package/dist/command-safety/types.js +21 -0
  44. package/dist/config/bash-approval.d.ts +33 -0
  45. package/dist/config/bash-approval.js +30 -0
  46. package/dist/config/catalog-overrides.d.ts +94 -0
  47. package/dist/config/catalog-overrides.js +7 -0
  48. package/dist/config/native-agent/index.d.ts +8 -0
  49. package/dist/config/native-agent/index.js +8 -0
  50. package/dist/config/schemas-sandbox.d.ts +31 -0
  51. package/dist/config/schemas-sandbox.js +45 -0
  52. package/dist/cost/core/index.d.ts +8 -0
  53. package/dist/cost/core/index.js +7 -0
  54. package/dist/cost/estimate.d.ts +34 -0
  55. package/dist/cost/estimate.js +65 -0
  56. package/dist/cost/model-spec.d.ts +28 -0
  57. package/dist/cost/model-spec.js +29 -0
  58. package/dist/cost/standard-types.d.ts +9 -0
  59. package/dist/cost/standard-types.js +1 -0
  60. package/dist/cost/usage-math.d.ts +18 -0
  61. package/dist/cost/usage-math.js +46 -0
  62. package/dist/index.d.ts +26 -0
  63. package/dist/index.js +21 -0
  64. package/dist/infra/agent-logger.d.ts +22 -0
  65. package/dist/infra/agent-logger.js +19 -0
  66. package/dist/infra/credentials-config.d.ts +20 -0
  67. package/dist/infra/credentials-config.js +15 -0
  68. package/dist/infra/errors.d.ts +9 -0
  69. package/dist/infra/errors.js +11 -0
  70. package/dist/infra/index.d.ts +7 -0
  71. package/dist/infra/index.js +7 -0
  72. package/dist/infra/nax-error.d.ts +10 -0
  73. package/dist/infra/nax-error.js +16 -0
  74. package/dist/infra/spin-breaker/hash.d.ts +2 -0
  75. package/dist/infra/spin-breaker/hash.js +5 -0
  76. package/dist/infra/spin-breaker/index.d.ts +120 -0
  77. package/dist/infra/spin-breaker/index.js +340 -0
  78. package/dist/internal/agent-output-env.d.ts +44 -0
  79. package/dist/internal/agent-output-env.js +57 -0
  80. package/dist/internal/argv-exec.d.ts +58 -0
  81. package/dist/internal/argv-exec.js +224 -0
  82. package/dist/internal/bounded-io.d.ts +24 -0
  83. package/dist/internal/bounded-io.js +51 -0
  84. package/dist/internal/command-spec/index.d.ts +29 -0
  85. package/dist/internal/command-spec/index.js +36 -0
  86. package/dist/internal/describe-value-type.d.ts +8 -0
  87. package/dist/internal/describe-value-type.js +14 -0
  88. package/dist/internal/exec-framing.d.ts +18 -0
  89. package/dist/internal/exec-framing.js +22 -0
  90. package/dist/internal/file-lock.d.ts +91 -0
  91. package/dist/internal/file-lock.js +212 -0
  92. package/dist/internal/file-size.d.ts +2 -0
  93. package/dist/internal/file-size.js +12 -0
  94. package/dist/internal/git-add.d.ts +43 -0
  95. package/dist/internal/git-add.js +59 -0
  96. package/dist/internal/git-env.d.ts +59 -0
  97. package/dist/internal/git-env.js +93 -0
  98. package/dist/internal/git-exec.d.ts +58 -0
  99. package/dist/internal/git-exec.js +115 -0
  100. package/dist/internal/path-file-lock.d.ts +23 -0
  101. package/dist/internal/path-file-lock.js +25 -0
  102. package/dist/internal/process-alive.d.ts +42 -0
  103. package/dist/internal/process-alive.js +56 -0
  104. package/dist/internal/process-kill.d.ts +27 -0
  105. package/dist/internal/process-kill.js +54 -0
  106. package/dist/internal/realpath.d.ts +29 -0
  107. package/dist/internal/realpath.js +52 -0
  108. package/dist/internal/redact.d.ts +40 -0
  109. package/dist/internal/redact.js +161 -0
  110. package/dist/internal/shell-quote.d.ts +2 -0
  111. package/dist/internal/shell-quote.js +4 -0
  112. package/dist/internal/sort.d.ts +20 -0
  113. package/dist/internal/sort.js +20 -0
  114. package/dist/internal/strip-control-chars.d.ts +19 -0
  115. package/dist/internal/strip-control-chars.js +38 -0
  116. package/dist/internal/thenable.d.ts +10 -0
  117. package/dist/internal/thenable.js +15 -0
  118. package/dist/internal.d.ts +91 -0
  119. package/dist/internal.js +91 -0
  120. package/dist/native/adapter-deps.d.ts +46 -0
  121. package/dist/native/adapter-deps.js +43 -0
  122. package/dist/native/auth-types.d.ts +70 -0
  123. package/dist/native/auth-types.js +13 -0
  124. package/dist/native/auth.d.ts +133 -0
  125. package/dist/native/auth.js +362 -0
  126. package/dist/native/client.d.ts +50 -0
  127. package/dist/native/client.js +174 -0
  128. package/dist/native/complete.d.ts +33 -0
  129. package/dist/native/complete.js +88 -0
  130. package/dist/native/credentials/chained-store.d.ts +30 -0
  131. package/dist/native/credentials/chained-store.js +68 -0
  132. package/dist/native/credentials/change-guard.d.ts +51 -0
  133. package/dist/native/credentials/change-guard.js +136 -0
  134. package/dist/native/credentials/exec-source.d.ts +50 -0
  135. package/dist/native/credentials/exec-source.js +222 -0
  136. package/dist/native/credentials/fingerprint.d.ts +26 -0
  137. package/dist/native/credentials/fingerprint.js +174 -0
  138. package/dist/native/credentials/helper-process.d.ts +79 -0
  139. package/dist/native/credentials/helper-process.js +259 -0
  140. package/dist/native/credentials/helper-protocol.d.ts +36 -0
  141. package/dist/native/credentials/helper-protocol.js +79 -0
  142. package/dist/native/credentials/index.d.ts +60 -0
  143. package/dist/native/credentials/index.js +184 -0
  144. package/dist/native/errors.d.ts +34 -0
  145. package/dist/native/errors.js +109 -0
  146. package/dist/native/index.d.ts +25 -0
  147. package/dist/native/index.js +23 -0
  148. package/dist/native/model-resolver.d.ts +57 -0
  149. package/dist/native/model-resolver.js +73 -0
  150. package/dist/native/models.d.ts +116 -0
  151. package/dist/native/models.js +183 -0
  152. package/dist/native/session/ask-human.d.ts +15 -0
  153. package/dist/native/session/ask-human.js +22 -0
  154. package/dist/native/session/compaction.d.ts +94 -0
  155. package/dist/native/session/compaction.js +170 -0
  156. package/dist/native/session/handle-invalid-tool-call.d.ts +111 -0
  157. package/dist/native/session/handle-invalid-tool-call.js +159 -0
  158. package/dist/native/session/loop-events/cache-boundary.d.ts +11 -0
  159. package/dist/native/session/loop-events/cache-boundary.js +29 -0
  160. package/dist/native/session/loop-events/external-handler.d.ts +38 -0
  161. package/dist/native/session/loop-events/external-handler.js +193 -0
  162. package/dist/native/session/loop-events/index.d.ts +16 -0
  163. package/dist/native/session/loop-events/index.js +15 -0
  164. package/dist/native/session/loop-events/payload-guard.d.ts +9 -0
  165. package/dist/native/session/loop-events/payload-guard.js +31 -0
  166. package/dist/native/session/loop-events/registry.d.ts +84 -0
  167. package/dist/native/session/loop-events/registry.js +305 -0
  168. package/dist/native/session/loop-events/types.d.ts +251 -0
  169. package/dist/native/session/loop-events/types.js +14 -0
  170. package/dist/native/session/loop-handlers.d.ts +73 -0
  171. package/dist/native/session/loop-handlers.js +137 -0
  172. package/dist/native/session/nudge.d.ts +29 -0
  173. package/dist/native/session/nudge.js +35 -0
  174. package/dist/native/session/rate-provenance.d.ts +17 -0
  175. package/dist/native/session/rate-provenance.js +39 -0
  176. package/dist/native/session/session.d.ts +139 -0
  177. package/dist/native/session/session.js +208 -0
  178. package/dist/native/session/tool-input-exemplar.d.ts +2 -0
  179. package/dist/native/session/tool-input-exemplar.js +66 -0
  180. package/dist/native/session/tool-input-validation.d.ts +15 -0
  181. package/dist/native/session/tool-input-validation.js +146 -0
  182. package/dist/native/session/tool-mapping.d.ts +18 -0
  183. package/dist/native/session/tool-mapping.js +27 -0
  184. package/dist/native/session/tool-result.d.ts +37 -0
  185. package/dist/native/session/tool-result.js +25 -0
  186. package/dist/native/session/transcript-store.d.ts +77 -0
  187. package/dist/native/session/transcript-store.js +200 -0
  188. package/dist/native/session/truncation-handler.d.ts +47 -0
  189. package/dist/native/session/truncation-handler.js +83 -0
  190. package/dist/native/session/turn-accumulator.d.ts +45 -0
  191. package/dist/native/session/turn-accumulator.js +67 -0
  192. package/dist/native/session/turn-ask-human.d.ts +23 -0
  193. package/dist/native/session/turn-ask-human.js +41 -0
  194. package/dist/native/session/turn-compaction-step.d.ts +74 -0
  195. package/dist/native/session/turn-compaction-step.js +128 -0
  196. package/dist/native/session/turn-complete-step.d.ts +77 -0
  197. package/dist/native/session/turn-complete-step.js +159 -0
  198. package/dist/native/session/turn-end-event.d.ts +3 -0
  199. package/dist/native/session/turn-end-event.js +17 -0
  200. package/dist/native/session/turn-events.d.ts +63 -0
  201. package/dist/native/session/turn-events.js +48 -0
  202. package/dist/native/session/turn-loop-round-trip.d.ts +116 -0
  203. package/dist/native/session/turn-loop-round-trip.js +318 -0
  204. package/dist/native/session/turn-loop.d.ts +23 -0
  205. package/dist/native/session/turn-loop.js +250 -0
  206. package/dist/native/session/turn-result.d.ts +39 -0
  207. package/dist/native/session/turn-result.js +64 -0
  208. package/dist/native/session/turn-retry.d.ts +120 -0
  209. package/dist/native/session/turn-retry.js +182 -0
  210. package/dist/native/session/turn-tool-batch.d.ts +61 -0
  211. package/dist/native/session/turn-tool-batch.js +259 -0
  212. package/dist/native/session/turn-types.d.ts +164 -0
  213. package/dist/native/session/turn-types.js +36 -0
  214. package/dist/native/session-adapter.d.ts +56 -0
  215. package/dist/native/session-adapter.js +369 -0
  216. package/dist/native/session-affinity.d.ts +36 -0
  217. package/dist/native/session-affinity.js +43 -0
  218. package/dist/native/tier-providers.d.ts +18 -0
  219. package/dist/native/tier-providers.js +20 -0
  220. package/dist/permissions/approval-audit.d.ts +10 -0
  221. package/dist/permissions/approval-audit.js +22 -0
  222. package/dist/permissions/approvals-link.d.ts +20 -0
  223. package/dist/permissions/approvals-link.js +62 -0
  224. package/dist/permissions/approvals-store.d.ts +168 -0
  225. package/dist/permissions/approvals-store.js +259 -0
  226. package/dist/permissions/approvals-taint.d.ts +33 -0
  227. package/dist/permissions/approvals-taint.js +138 -0
  228. package/dist/permissions/ask-chain.d.ts +69 -0
  229. package/dist/permissions/ask-chain.js +32 -0
  230. package/dist/permissions/ask.d.ts +23 -0
  231. package/dist/permissions/ask.js +25 -0
  232. package/dist/permissions/bash-lex.d.ts +59 -0
  233. package/dist/permissions/bash-lex.js +279 -0
  234. package/dist/permissions/grammar.d.ts +12 -0
  235. package/dist/permissions/grammar.js +24 -0
  236. package/dist/permissions/index.d.ts +11 -0
  237. package/dist/permissions/index.js +9 -0
  238. package/dist/permissions/secret-spans.d.ts +19 -0
  239. package/dist/permissions/secret-spans.js +88 -0
  240. package/dist/permissions/types.d.ts +85 -0
  241. package/dist/permissions/types.js +1 -0
  242. package/dist/runtime/index.d.ts +3 -0
  243. package/dist/runtime/index.js +2 -0
  244. package/dist/runtime/node-glob.d.ts +3 -0
  245. package/dist/runtime/node-glob.js +83 -0
  246. package/dist/runtime/node-runtime.d.ts +3 -0
  247. package/dist/runtime/node-runtime.js +126 -0
  248. package/dist/runtime/slot.d.ts +11 -0
  249. package/dist/runtime/slot.js +17 -0
  250. package/dist/runtime/types.d.ts +51 -0
  251. package/dist/runtime/types.js +1 -0
  252. package/dist/runtime/which.d.ts +8 -0
  253. package/dist/runtime/which.js +32 -0
  254. package/dist/sandbox/argv-quote.d.ts +11 -0
  255. package/dist/sandbox/argv-quote.js +13 -0
  256. package/dist/sandbox/defaults.d.ts +27 -0
  257. package/dist/sandbox/defaults.js +47 -0
  258. package/dist/sandbox/git-guards.d.ts +64 -0
  259. package/dist/sandbox/git-guards.js +161 -0
  260. package/dist/sandbox/index.d.ts +11 -0
  261. package/dist/sandbox/index.js +10 -0
  262. package/dist/sandbox/launcher.d.ts +37 -0
  263. package/dist/sandbox/launcher.js +160 -0
  264. package/dist/sandbox/messages.d.ts +17 -0
  265. package/dist/sandbox/messages.js +46 -0
  266. package/dist/sandbox/policy-builder.d.ts +44 -0
  267. package/dist/sandbox/policy-builder.js +149 -0
  268. package/dist/sandbox/policy-inputs.d.ts +53 -0
  269. package/dist/sandbox/policy-inputs.js +76 -0
  270. package/dist/sandbox/probe.d.ts +8 -0
  271. package/dist/sandbox/probe.js +66 -0
  272. package/dist/sandbox/registry.d.ts +17 -0
  273. package/dist/sandbox/registry.js +57 -0
  274. package/dist/sandbox/session-tmp.d.ts +20 -0
  275. package/dist/sandbox/session-tmp.js +95 -0
  276. package/dist/sandbox/srt-backend.d.ts +21 -0
  277. package/dist/sandbox/srt-backend.js +99 -0
  278. package/dist/sandbox/types.d.ts +102 -0
  279. package/dist/sandbox/types.js +1 -0
  280. package/dist/session/adapter-failure.d.ts +60 -0
  281. package/dist/session/adapter-failure.js +11 -0
  282. package/dist/session/agent-stream-events.d.ts +103 -0
  283. package/dist/session/agent-stream-events.js +6 -0
  284. package/dist/session/interaction-handler.d.ts +53 -0
  285. package/dist/session/interaction-handler.js +2 -0
  286. package/dist/session/no-op-interaction-handler.d.ts +9 -0
  287. package/dist/session/no-op-interaction-handler.js +11 -0
  288. package/dist/session/protocol-types.d.ts +12 -0
  289. package/dist/session/protocol-types.js +1 -0
  290. package/dist/session/session-types.d.ts +456 -0
  291. package/dist/session/session-types.js +64 -0
  292. package/dist/session/tool-descriptor.d.ts +23 -0
  293. package/dist/session/tool-descriptor.js +5 -0
  294. package/dist/session/turn-deadline.d.ts +17 -0
  295. package/dist/session/turn-deadline.js +23 -0
  296. package/dist/tools/ask-request.d.ts +35 -0
  297. package/dist/tools/ask-request.js +57 -0
  298. package/dist/tools/bash-cwd.d.ts +82 -0
  299. package/dist/tools/bash-cwd.js +55 -0
  300. package/dist/tools/bash.d.ts +69 -0
  301. package/dist/tools/bash.js +283 -0
  302. package/dist/tools/delete.d.ts +57 -0
  303. package/dist/tools/delete.js +154 -0
  304. package/dist/tools/denial-redirect.d.ts +42 -0
  305. package/dist/tools/denial-redirect.js +266 -0
  306. package/dist/tools/deny-paths.d.ts +35 -0
  307. package/dist/tools/deny-paths.js +76 -0
  308. package/dist/tools/edit-region.d.ts +50 -0
  309. package/dist/tools/edit-region.js +90 -0
  310. package/dist/tools/edit.d.ts +25 -0
  311. package/dist/tools/edit.js +114 -0
  312. package/dist/tools/exec-allowlist-text.d.ts +18 -0
  313. package/dist/tools/exec-allowlist-text.js +24 -0
  314. package/dist/tools/exec-guard.d.ts +69 -0
  315. package/dist/tools/exec-guard.js +151 -0
  316. package/dist/tools/git-commit.d.ts +59 -0
  317. package/dist/tools/git-commit.js +220 -0
  318. package/dist/tools/git-flags/index.d.ts +10 -0
  319. package/dist/tools/git-flags/index.js +10 -0
  320. package/dist/tools/git.d.ts +88 -0
  321. package/dist/tools/git.js +397 -0
  322. package/dist/tools/glob.d.ts +29 -0
  323. package/dist/tools/glob.js +153 -0
  324. package/dist/tools/grep.d.ts +21 -0
  325. package/dist/tools/grep.js +162 -0
  326. package/dist/tools/index.d.ts +36 -0
  327. package/dist/tools/index.js +28 -0
  328. package/dist/tools/narrow-grants.d.ts +19 -0
  329. package/dist/tools/narrow-grants.js +54 -0
  330. package/dist/tools/nax-owned-writes.d.ts +135 -0
  331. package/dist/tools/nax-owned-writes.js +259 -0
  332. package/dist/tools/package-managers-table.d.ts +146 -0
  333. package/dist/tools/package-managers-table.js +325 -0
  334. package/dist/tools/package-managers-types.d.ts +36 -0
  335. package/dist/tools/package-managers-types.js +6 -0
  336. package/dist/tools/package-managers.d.ts +85 -0
  337. package/dist/tools/package-managers.js +391 -0
  338. package/dist/tools/path-list.d.ts +38 -0
  339. package/dist/tools/path-list.js +62 -0
  340. package/dist/tools/policy-bash-raw.d.ts +19 -0
  341. package/dist/tools/policy-bash-raw.js +210 -0
  342. package/dist/tools/policy-bash.d.ts +64 -0
  343. package/dist/tools/policy-bash.js +232 -0
  344. package/dist/tools/policy-command-branch.d.ts +32 -0
  345. package/dist/tools/policy-command-branch.js +63 -0
  346. package/dist/tools/policy-input.d.ts +2 -0
  347. package/dist/tools/policy-input.js +10 -0
  348. package/dist/tools/policy-match.d.ts +75 -0
  349. package/dist/tools/policy-match.js +117 -0
  350. package/dist/tools/policy-paths-branch.d.ts +97 -0
  351. package/dist/tools/policy-paths-branch.js +314 -0
  352. package/dist/tools/policy.d.ts +65 -0
  353. package/dist/tools/policy.js +232 -0
  354. package/dist/tools/protected-paths.d.ts +26 -0
  355. package/dist/tools/protected-paths.js +8 -0
  356. package/dist/tools/provider-adapt.d.ts +4 -0
  357. package/dist/tools/provider-adapt.js +32 -0
  358. package/dist/tools/provider-advertise.d.ts +39 -0
  359. package/dist/tools/provider-advertise.js +50 -0
  360. package/dist/tools/provider-grants.d.ts +36 -0
  361. package/dist/tools/provider-grants.js +72 -0
  362. package/dist/tools/provider-sanitize.d.ts +19 -0
  363. package/dist/tools/provider-sanitize.js +53 -0
  364. package/dist/tools/provider-types.d.ts +41 -0
  365. package/dist/tools/provider-types.js +26 -0
  366. package/dist/tools/read-continuation.d.ts +147 -0
  367. package/dist/tools/read-continuation.js +176 -0
  368. package/dist/tools/read-file.d.ts +32 -0
  369. package/dist/tools/read-file.js +85 -0
  370. package/dist/tools/read.d.ts +25 -0
  371. package/dist/tools/read.js +182 -0
  372. package/dist/tools/registry.d.ts +125 -0
  373. package/dist/tools/registry.js +67 -0
  374. package/dist/tools/request-capability.d.ts +13 -0
  375. package/dist/tools/request-capability.js +23 -0
  376. package/dist/tools/run-command-exec.d.ts +14 -0
  377. package/dist/tools/run-command-exec.js +161 -0
  378. package/dist/tools/run-command.d.ts +107 -0
  379. package/dist/tools/run-command.js +398 -0
  380. package/dist/tools/runtime-calltool.d.ts +146 -0
  381. package/dist/tools/runtime-calltool.js +223 -0
  382. package/dist/tools/runtime.d.ts +149 -0
  383. package/dist/tools/runtime.js +345 -0
  384. package/dist/tools/scratchpad.d.ts +20 -0
  385. package/dist/tools/scratchpad.js +197 -0
  386. package/dist/tools/spill.d.ts +81 -0
  387. package/dist/tools/spill.js +251 -0
  388. package/dist/tools/tool-audit.d.ts +171 -0
  389. package/dist/tools/tool-audit.js +183 -0
  390. package/dist/tools/truncate.d.ts +98 -0
  391. package/dist/tools/truncate.js +297 -0
  392. package/dist/tools/types.d.ts +145 -0
  393. package/dist/tools/types.js +24 -0
  394. package/dist/tools/verb-denial-argv-hint.d.ts +35 -0
  395. package/dist/tools/verb-denial-argv-hint.js +40 -0
  396. package/dist/tools/write.d.ts +9 -0
  397. package/dist/tools/write.js +45 -0
  398. package/package.json +54 -0
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Spill writer and the model-facing composition of the truncation policy
3
+ * (US-003).
4
+ *
5
+ * The policy itself (`truncateForModel`) decides *what* the model may see; this
6
+ * module decides what happens to the rest. On truncation only, the untruncated
7
+ * body — up to `READ_CEILING` — is written under the scratchpad at
8
+ * `spill/<toolName>-<callId>.txt`, and the marker naming that path and both byte
9
+ * counts is appended to the content the model receives. `writeSpill` keeps
10
+ * returning the scratchpad-relative spelling; `applyModelTruncationPolicy`
11
+ * turns it into the path the marker names — root-relative
12
+ * (`.nax/scratchpad/spill/...`) by default, so the session's own tools and
13
+ * shell can open it verbatim, or absolute when the root is not the session's
14
+ * shell root. The spilled body stays recoverable with `offset`/`limit`.
15
+ *
16
+ * The composition lives here, beside the writer, because the marker and the
17
+ * spill are one decision: the path may only appear in the marker when the write
18
+ * actually succeeded. Both transports reach it — the coding-tool runtime applies
19
+ * it to every genuine tool result, and the native session applies it at the
20
+ * `after_tool` chokepoint — so there is exactly one implementation of the
21
+ * model-facing cap rather than one per tool.
22
+ *
23
+ * Filesystem calls are injectable (`_spillDeps`), following `_scratchpadWipeDeps`
24
+ * in src/execution/lifecycle/scratchpad-wipe.ts, and writes go through
25
+ * asynchronous Node filesystem APIs.
26
+ */
27
+ import { mkdir, writeFile } from "node:fs/promises";
28
+ import { dirname, join } from "node:path";
29
+ import { errorMessage } from "#src/infra/errors";
30
+ import { getSafeLogger } from "#src/infra/index";
31
+ import { SCRATCHPAD_DIR } from "./scratchpad.js";
32
+ import { capModelLine, cutToByteCap, MODEL_MAX_BYTES, MODEL_MAX_LINE_CHARS, MODEL_MAX_LINES, READ_CEILING, splitModelLines, truncateForModel, truncationDirectionFor, } from "./truncate.js";
33
+ /** Directory under the scratchpad that holds spilled tool output. */
34
+ export const SPILL_DIR = "spill";
35
+ /** Injectable filesystem seam (see docs/architecture/conventions.md §2). */
36
+ export const _spillDeps = {
37
+ mkdir: (path) => mkdir(path, { recursive: true }),
38
+ writeFile: async (path, data) => {
39
+ await writeFile(path, data, "utf8");
40
+ return Buffer.byteLength(data);
41
+ },
42
+ };
43
+ /** A tool name or call id can only contribute path-safe characters. */
44
+ function fileSafe(value) {
45
+ return value.replace(/[^A-Za-z0-9_-]/g, "_");
46
+ }
47
+ /** `spill/<toolName>-<callId>.txt`, relative to the scratchpad directory. */
48
+ export function spillRelativePath(toolName, callId) {
49
+ return `${SPILL_DIR}/${fileSafe(toolName)}-${fileSafe(callId)}.txt`;
50
+ }
51
+ /**
52
+ * The spill file's body: the untruncated output, bounded by `READ_CEILING`.
53
+ *
54
+ * A body past the ceiling is itself cut, and the cut is recorded on the file's
55
+ * own last line: a spill that silently ends mid-output is indistinguishable
56
+ * from one that ends where the tool did, and the model is being told it holds
57
+ * the whole thing. The note is paid for out of the ceiling, never added on top
58
+ * of it — the file is the tool-layer bound, so it must not exceed it either.
59
+ */
60
+ function spillBody(body) {
61
+ const total = Buffer.byteLength(body, "utf8");
62
+ if (total <= READ_CEILING)
63
+ return body;
64
+ // Two passes because the note quotes how much was kept, which depends on the
65
+ // note's own length. The first pass sizes the note with the largest number it
66
+ // could report (the ceiling), so the second can only make it shorter.
67
+ const noteOf = (kept) => `\n... [spill incomplete: showing ${kept} of ${total} bytes]`;
68
+ const firstNote = noteOf(READ_CEILING);
69
+ const kept = cutToByteCap(body, READ_CEILING - Buffer.byteLength(firstNote, "utf8"));
70
+ return `${kept}${noteOf(Buffer.byteLength(kept, "utf8"))}`;
71
+ }
72
+ /**
73
+ * Write the untruncated body under the scratchpad and return its path relative
74
+ * to the scratchpad, or `undefined` when the write failed.
75
+ *
76
+ * Failure is fail-open by contract: the caller still delivers the truncated
77
+ * result. What it must not do is name a file that is not there, which is why
78
+ * this returns the path rather than a boolean.
79
+ */
80
+ export async function writeSpill(req) {
81
+ const relative = spillRelativePath(req.toolName, req.callId);
82
+ const target = join(req.root, SCRATCHPAD_DIR, relative);
83
+ try {
84
+ await _spillDeps.mkdir(dirname(target));
85
+ await _spillDeps.writeFile(target, spillBody(req.body));
86
+ return relative;
87
+ }
88
+ catch (err) {
89
+ getSafeLogger()?.warn("tools", "[spill] could not write the spilled tool result", {
90
+ tool: req.toolName,
91
+ path: target,
92
+ error: errorMessage(err),
93
+ });
94
+ return undefined;
95
+ }
96
+ }
97
+ /**
98
+ * The marker shapes, widest first. A narrower shape is used when the wider one
99
+ * cannot fit the byte budget — a 40 000-byte ceiling fits a marker naming the
100
+ * spill path, a 32-byte one fits barely more than the word "truncated". The
101
+ * spill path is simply absent from a shape that has no room for it, which is
102
+ * also the shape failure takes: a marker never names a file that is not there.
103
+ */
104
+ function markerShapes(originalBytes, spillPath) {
105
+ const shapes = [];
106
+ if (spillPath !== undefined) {
107
+ shapes.push((d) => `... [truncated: full output at ${spillPath} (open with Read or ScratchpadRead); showing ${d} of ${originalBytes} bytes]`);
108
+ }
109
+ shapes.push((d) => `... [truncated: showing ${d} of ${originalBytes} bytes]`);
110
+ shapes.push(() => "... [truncated]");
111
+ return shapes;
112
+ }
113
+ /** Lines `head` may keep once the marker occupies one of them. */
114
+ function trimToLineBudget(head, maxLines) {
115
+ const lines = splitModelLines(head);
116
+ if (lines.length <= maxLines)
117
+ return head;
118
+ return lines.slice(0, maxLines).join("\n");
119
+ }
120
+ /** A marker line, plus the newline that puts it on one. */
121
+ function renderedMarkerBytes(render, deliveredBytes) {
122
+ return Buffer.byteLength(render(deliveredBytes), "utf8") + 1;
123
+ }
124
+ /**
125
+ * Compose the head-direction result: the policy's content, then the marker on
126
+ * its own last line. The marker is inside the byte and line budgets, never
127
+ * appended past them.
128
+ */
129
+ function composeHead(content, originalBytes, byteCap, spillPath) {
130
+ for (const render of markerShapes(originalBytes, spillPath)) {
131
+ const bodyBudget = byteCap - renderedMarkerBytes(render, originalBytes);
132
+ if (bodyBudget < 0)
133
+ continue;
134
+ const head = trimToLineBudget(cutToByteCap(content, bodyBudget), MODEL_MAX_LINES - 1);
135
+ return `${head}\n${render(Buffer.byteLength(head, "utf8"))}`;
136
+ }
137
+ return cutToByteCap(content, byteCap);
138
+ }
139
+ /**
140
+ * Compose the tail-with-first-line result: the body's first line, the marker,
141
+ * then the tail.
142
+ *
143
+ * The marker sits between them rather than after the tail because the tail is
144
+ * the whole point of the direction — a marker appended last would push the
145
+ * final stderr lines out of view, which is the failure the direction exists to
146
+ * prevent. Command output is kept line-wise, and a line too large for the
147
+ * remaining budget is skipped rather than cut: one 160 KB stdout blob must not
148
+ * spend the entire budget on itself and evict the short lines that follow it.
149
+ */
150
+ function composeTail(body, content, originalBytes, byteCap, spillPath) {
151
+ const lines = splitModelLines(body);
152
+ const fullFirstLine = content.split("\n")[0] ?? "";
153
+ for (const render of markerShapes(originalBytes, spillPath)) {
154
+ // One byte for the newline after the first line, then the marker.
155
+ const forFirstLine = byteCap - Buffer.byteLength(render(originalBytes), "utf8") - 1;
156
+ if (forFirstLine < 0)
157
+ continue;
158
+ const firstLine = Buffer.byteLength(fullFirstLine, "utf8") > forFirstLine
159
+ ? cutToByteCap(fullFirstLine, forFirstLine)
160
+ : fullFirstLine;
161
+ // One more byte for the newline that opens the tail.
162
+ const tailBudget = forFirstLine - Buffer.byteLength(firstLine, "utf8") - 1;
163
+ const tail = tailBudget < 0 ? "" : selectTail(lines, tailBudget);
164
+ const firstLineBytes = Buffer.byteLength(firstLine, "utf8");
165
+ const tailBytes = Buffer.byteLength(tail, "utf8");
166
+ // The delivered count is the body text the model can read back: the retained
167
+ // first line, the body's own newline that still separates it from the tail,
168
+ // and the tail. The newline that puts the marker on a line of its own is not
169
+ // a body byte — here it is the one that opens the tail, and with no tail
170
+ // retained there is no body byte after the first line at all. `composeHead`
171
+ // reports its head with no marker-line byte either; counting one only here
172
+ // made the same marker mean a different width per tool, and reported one
173
+ // byte more than the model can actually read.
174
+ const delivered = tail === "" ? firstLineBytes : firstLineBytes + 1 + tailBytes;
175
+ const marker = render(delivered);
176
+ return tail === "" ? `${firstLine}\n${marker}` : `${firstLine}\n${marker}\n${tail}`;
177
+ }
178
+ return cutToByteCap(content, byteCap);
179
+ }
180
+ /**
181
+ * The trailing lines that fit `budget` bytes, taken from the end of the body.
182
+ *
183
+ * A line whose own length exceeds what is left is skipped, not truncated: the
184
+ * alternative spends the whole budget inside that one line and returns a slice
185
+ * of it in place of the lines that carry the signal (an echoed command, the
186
+ * error text). Each line kept is still shaped by the per-line cap.
187
+ */
188
+ function selectTail(lines, budget) {
189
+ const kept = [];
190
+ let remaining = budget;
191
+ // The first line is retained separately, so the walk starts below it, and the
192
+ // first line plus the marker occupy one line each of the line budget.
193
+ for (let i = lines.length - 1; i >= 1 && kept.length < MODEL_MAX_LINES - 2; i -= 1) {
194
+ const line = lines[i] ?? "";
195
+ const cost = Buffer.byteLength(line, "utf8") + (kept.length === 0 ? 0 : 1);
196
+ if (cost > remaining)
197
+ continue;
198
+ kept.unshift(capModelLine(line, MODEL_MAX_LINE_CHARS));
199
+ remaining -= cost;
200
+ }
201
+ return kept.join("\n");
202
+ }
203
+ /**
204
+ * Apply the model-facing truncation policy to one tool result.
205
+ *
206
+ * Returns the body unchanged when it is within every cap — no spill, no marker.
207
+ * Otherwise the shared policy shapes the content, the untruncated body is
208
+ * spilled when a root is known, and the marker naming both byte counts and the
209
+ * spill path is composed inside the same ceilings the body obeys.
210
+ *
211
+ * The byte ceiling is the caller's `maxBytes`, defaulting to `MODEL_MAX_BYTES`;
212
+ * the line ceilings always come from the shared policy's constants.
213
+ */
214
+ export async function applyModelTruncationPolicy(body, opts) {
215
+ const direction = truncationDirectionFor(opts.toolName);
216
+ const originalBytes = Buffer.byteLength(body, "utf8");
217
+ const byteCap = Math.max(0, opts.maxBytes ?? MODEL_MAX_BYTES);
218
+ // The one call to the shared policy: it applies the per-line cap, the
219
+ // line-count cap and its own byte cap, and tells us whether anything was cut.
220
+ const shaped = truncateForModel(body, { direction });
221
+ if (!shaped.truncated && originalBytes <= byteCap)
222
+ return body;
223
+ const spillPath = await spillMarkerPath(opts, body);
224
+ return direction === "head"
225
+ ? composeHead(shaped.content, originalBytes, byteCap, spillPath)
226
+ : composeTail(body, shaped.content, originalBytes, byteCap, spillPath);
227
+ }
228
+ /**
229
+ * Write the body and return the path as the marker should spell it, or
230
+ * `undefined` when there is no root or the write failed. The spill writer
231
+ * keeps returning the scratchpad-relative `spill/<Tool>-<callId>.txt`; this is
232
+ * the one place that turns it into the model-facing spelling, so the two
233
+ * composers receive a finished path and stay unaware of the style.
234
+ *
235
+ * The path appears whenever the wide marker shape fits the byte budget, for a
236
+ * body of any size: AC1/AC2/AC7/AC8 name the spill path for every truncated
237
+ * result, and the marker's job is to point at the file nax wrote for this call.
238
+ * A body past `READ_CEILING` is written capped (see `spillBody`) — that is the
239
+ * spill writer's own long-standing bound, pinned by `spill-recovery.test.ts`
240
+ * AC9 — and the cap is recorded on the file's last line rather than hidden.
241
+ */
242
+ async function spillMarkerPath(opts, body) {
243
+ if (opts.root === undefined)
244
+ return undefined;
245
+ const relative = await writeSpill({ root: opts.root, toolName: opts.toolName, callId: opts.callId, body });
246
+ if (relative === undefined)
247
+ return undefined;
248
+ return opts.spillPathStyle === "absolute"
249
+ ? join(opts.root, SCRATCHPAD_DIR, relative)
250
+ : `${SCRATCHPAD_DIR}/${relative}`;
251
+ }
@@ -0,0 +1,171 @@
1
+ import type { SandboxRecord } from "../sandbox/index.ts";
2
+ export interface ToolCallRecord {
3
+ readonly tool: string;
4
+ readonly outcome: "ok" | "error" | "denied" | "denied:ask";
5
+ readonly breach?: boolean;
6
+ readonly input: Record<string, unknown>;
7
+ readonly resultBytes: number;
8
+ readonly storyId?: string;
9
+ readonly at: string;
10
+ /**
11
+ * Why a "denied" outcome was refused. runtime.ts's log() computes this for
12
+ * the console logger (under the `error` key) but used to drop it before it
13
+ * reached this sink, so a denied row's `error` field read null and the
14
+ * refusal reason was unrecoverable -- the exact gap this field closes.
15
+ */
16
+ readonly reason?: string;
17
+ /**
18
+ * The argv that actually ran, after normalization (workspace scoping, an
19
+ * appended no-scripts flag). Set only for the argv branch (`Exec`).
20
+ * Deliberately alongside `input.argv` (the model's requested argv) rather
21
+ * than instead of it: either half alone is uninformative -- `input` cannot
22
+ * show what actually ran, and `executed` alone cannot show whether the
23
+ * normalization was faithful to what was requested.
24
+ */
25
+ readonly executed?: readonly string[];
26
+ /** Which workspace root the argv branch executed against. */
27
+ readonly target?: "package" | "repoRoot";
28
+ /**
29
+ * Who approved an ask-matched call and how long the decision took. Recorded
30
+ * on BOTH the allow and deny paths so a human-approved execution is
31
+ * distinguishable in the ledger from a mechanically-allowed one.
32
+ *
33
+ * `remembered` is always false today: `AskVerdict` carries no "remembered"
34
+ * signal, so this is a placeholder for when the cache reports a hit.
35
+ */
36
+ readonly approval?: {
37
+ readonly decidedBy: string;
38
+ readonly remembered: boolean;
39
+ readonly latencyMs: number;
40
+ };
41
+ /**
42
+ * P4: `{ backend, wrapped, reason?, denialHint?, argv? }` on every Bash /
43
+ * Exec row; the exit runs gate on `wrapped`, and `argv` (wrapped calls only)
44
+ * is the argv the sandbox actually executed, beside the logical `executed`.
45
+ * Additive and optional, so `TOOL_AUDIT_SCHEMA_VERSION` stays 1 (as
46
+ * `approval` did).
47
+ */
48
+ readonly sandbox?: SandboxRecord;
49
+ /**
50
+ * Bash rows only: the shell's exit code, present when nax did not kill the
51
+ * process group (absent on timeout and turn abort; see `ToolResult.audit`).
52
+ * `outcome` is unchanged -- every non-zero exit is still `"error"` -- so a
53
+ * reader separates a negative answer (a no-match grep, exit 1) from a real
54
+ * failure by reading `exitCode` together with `executed`. N >= 128 may be a
55
+ * signal nax did not send. Additive and optional, so
56
+ * `TOOL_AUDIT_SCHEMA_VERSION` stays 1 (as `sandbox` did).
57
+ */
58
+ readonly exitCode?: number;
59
+ /** Provider id for a provider-supplied tool; absent for built-ins. */
60
+ readonly provider?: string;
61
+ /**
62
+ * Result size BEFORE the maxBytes slice. `resultBytes` is measured after,
63
+ * so elision is otherwise invisible — a 2 MB result and a 40 KB one both
64
+ * ledger as 40000.
65
+ */
66
+ readonly resultBytesPreTruncation?: number;
67
+ /**
68
+ * The `callOp` invocation this tool call happened under.
69
+ *
70
+ * NOT unique: one callId spans every retry, agent-swap hop and turn of the
71
+ * invocation, so it is 1:N over cost rows (10.0% of groups hold more than
72
+ * one). It is a foreign key. Use `turnId` to select a single cost row.
73
+ *
74
+ * This is the OPERATION-layer callId (`DispatchEventBase.callId`), not the
75
+ * stream-layer field of the same name in `agent-stream-events.ts` — joining
76
+ * on that one produced nax#2045's 0-of-1,940 match rate.
77
+ */
78
+ readonly callId?: string;
79
+ /** Caller-defined region spanning many callOp invocations. */
80
+ readonly scopeId?: string;
81
+ /**
82
+ * The turn this call happened in. One turn is one cost row, so this is the
83
+ * field that prices a tool call. Native sessions only — ACP does not route
84
+ * coding tools through the turn loop.
85
+ */
86
+ readonly turnId?: string;
87
+ /**
88
+ * Model round-trip index WITHIN the turn, 1-based.
89
+ *
90
+ * Not a turn index. `turn-loop.ts` pushes an interaction field named
91
+ * `turnIndex` whose value is this same round-trip counter; the two names
92
+ * have been conflated before. Native sessions only.
93
+ */
94
+ readonly roundTrips?: number;
95
+ /**
96
+ * Provider-assigned `tool_use` id, passed through verbatim — nax never mints
97
+ * or namespaces it. Unique within a session in practice, with no global
98
+ * guarantee, and NOT stable across a retry: a retried turn produces fresh
99
+ * ids. The unique tuple is (runId, sessionName, toolCallId).
100
+ */
101
+ readonly toolCallId?: string;
102
+ }
103
+ export interface ToolAuditSink {
104
+ record(entry: ToolCallRecord): void;
105
+ flush(): Promise<void>;
106
+ }
107
+ /**
108
+ * The sink `createToolAuditSink` returns, plus the shutdown-time flush.
109
+ *
110
+ * `flushPartial()` writes whatever is still buffered with `partial: true` so a
111
+ * sink whose hop `finally` never runs before `process.exit` still leaves its
112
+ * audit file behind. After it runs, the sink's own `flush()` is a no-op — the
113
+ * hop `finally` that runs later must not write a second file.
114
+ */
115
+ export interface RegisteredSink extends ToolAuditSink {
116
+ flushPartial(): Promise<void>;
117
+ }
118
+ /** Register a sink so the run's shutdown can flush whatever it still holds. */
119
+ export declare function registerToolAuditSink(runId: string, sink: RegisteredSink): void;
120
+ /** Drop a sink from the run's registry (see registerToolAuditSink). */
121
+ export declare function unregisterToolAuditSink(runId: string, sink: RegisteredSink): void;
122
+ /**
123
+ * Flush every still-registered sink for `runId` with `partial: true`, then
124
+ * clear them. Never rejects: one sink's failure must not stop the drain.
125
+ */
126
+ export declare function flushOpenToolAuditSinks(runId: string): Promise<void>;
127
+ export declare function createNoOpToolAuditSink(): ToolAuditSink;
128
+ /**
129
+ * tool-audit file schema version.
130
+ *
131
+ * 1 — first versioned generation. Adds a file header (`runId`, `featureName`,
132
+ * `storyId`, `sessionRole`) and per-call correlation ids (`callId`,
133
+ * `scopeId`, `turnId`, `roundTrips`, `toolCallId`).
134
+ *
135
+ * Files written before this field existed carry none of the above and
136
+ * cannot be backfilled: `runId` in particular was never in scope at the
137
+ * sink's construction path, so an unversioned file's only identity is its
138
+ * filename.
139
+ *
140
+ * `sessionName` is a HUMAN LABEL from this version on, never a join key.
141
+ * It is constant-prefixed and stable across re-runs by construction, so it
142
+ * collides: 7.5% of review-audit sessionNames span more than one runId.
143
+ * Join on `runId` plus `callId`/`turnId` instead.
144
+ *
145
+ * NOTE ON `resultBytes`: it is measured AFTER the shared model-truncation
146
+ * policy (applyModelTruncationPolicy, src/tools/spill.ts) and so counts what
147
+ * the model actually received — the spill marker is composed inside the
148
+ * measured content, so its bytes count. That boundary moved at the
149
+ * `native-loop-events` merge (f4b3bbc7a): files written before it measured
150
+ * each tool's own post-truncation `result.content.length` at `ctx.maxBytes`
151
+ * instead. The policy additionally applies `MODEL_MAX_LINES` (1_000),
152
+ * `MODEL_MAX_LINE_CHARS` (2_000) and a `MODEL_MAX_BYTES` (40_000) ceiling,
153
+ * so the two generations are not comparable call for call. The unit was
154
+ * never bytes: it is `String#length`, i.e. UTF-16 code units, so a
155
+ * multi-byte result under-reports against the field name.
156
+ * `resultBytesPreTruncation` is the pre-policy size and, where a tool sets
157
+ * it, is UTF-8 bytes (`Buffer.byteLength` / `Bun.file().size`).
158
+ */
159
+ export declare const TOOL_AUDIT_SCHEMA_VERSION = 1;
160
+ /** Run-scoped identity stamped once per tool-audit file. */
161
+ export interface ToolAuditHeader {
162
+ readonly runId?: string;
163
+ readonly featureName?: string;
164
+ readonly storyId?: string;
165
+ readonly sessionRole?: string;
166
+ }
167
+ export declare function createToolAuditSink(opts: {
168
+ dir: string;
169
+ sessionName: string;
170
+ header?: ToolAuditHeader;
171
+ }): RegisteredSink;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Durable record of every coding-tool call.
3
+ *
4
+ * runtime.ts already logs each outcome, and says why: a refused call that
5
+ * leaves no trace is indistinguishable from a call never made. But it logs
6
+ * through getSafeLogger(), and issue #1359 closed on a measured zero taken off
7
+ * exactly such a counter while the persisted records still held ten in-window
8
+ * findings. The zero meant "no data retained" and was read as "did not happen".
9
+ *
10
+ * So a signal a later decision depends on is written here, not there. The
11
+ * logger keeps its calls for operator visibility; neither replaces the other.
12
+ *
13
+ * File shape mirrors src/review/review-audit.ts: one JSON file per session,
14
+ * named <runId>-<epochMs>-<sessionName>.json when the runId is known, and
15
+ * <epochMs>-<sessionName>.json otherwise.
16
+ */
17
+ import { mkdir, writeFile } from "node:fs/promises";
18
+ import { join } from "node:path";
19
+ import { errorMessage } from "#src/infra/errors";
20
+ import { getSafeLogger } from "#src/infra/index";
21
+ import { redactRowStrings } from "#src/permissions/index";
22
+ /**
23
+ * Still-open sinks, keyed by the `runId` they were created under.
24
+ *
25
+ * A sink is addressable here precisely because its hop `finally` has not run
26
+ * yet: `createToolAuditSink` registers it on creation and unregisters it on its
27
+ * first write, so the registry only ever holds buffers that would otherwise be
28
+ * lost to `process.exit`.
29
+ */
30
+ const openToolAuditSinks = new Map();
31
+ /** Register a sink so the run's shutdown can flush whatever it still holds. */
32
+ export function registerToolAuditSink(runId, sink) {
33
+ const sinks = openToolAuditSinks.get(runId);
34
+ if (sinks === undefined) {
35
+ openToolAuditSinks.set(runId, new Set([sink]));
36
+ return;
37
+ }
38
+ sinks.add(sink);
39
+ }
40
+ /** Drop a sink from the run's registry (see registerToolAuditSink). */
41
+ export function unregisterToolAuditSink(runId, sink) {
42
+ const sinks = openToolAuditSinks.get(runId);
43
+ if (sinks === undefined)
44
+ return;
45
+ sinks.delete(sink);
46
+ if (sinks.size === 0)
47
+ openToolAuditSinks.delete(runId);
48
+ }
49
+ /**
50
+ * Flush every still-registered sink for `runId` with `partial: true`, then
51
+ * clear them. Never rejects: one sink's failure must not stop the drain.
52
+ */
53
+ export async function flushOpenToolAuditSinks(runId) {
54
+ const sinks = openToolAuditSinks.get(runId);
55
+ if (sinks === undefined)
56
+ return;
57
+ // Clear before flushing: the sink's own write path calls
58
+ // unregisterToolAuditSink, and re-entering the registry mid-iteration would
59
+ // mutate the map we are reading.
60
+ openToolAuditSinks.delete(runId);
61
+ // Started concurrently, so a slow or never-settling sink cannot starve the
62
+ // writes of the sinks after it. Each sink owns a distinct file, so the
63
+ // completion order is irrelevant. Every task swallows its own rejection --
64
+ // one sink's failure is logged and the rest still flush.
65
+ await Promise.all([...sinks].map(async (sink) => {
66
+ try {
67
+ await sink.flushPartial();
68
+ }
69
+ catch (error) {
70
+ getSafeLogger()?.warn("tools", "tool-audit partial flush failed", { runId, error: errorMessage(error) });
71
+ }
72
+ }));
73
+ }
74
+ export function createNoOpToolAuditSink() {
75
+ return { record() { }, async flush() { } };
76
+ }
77
+ /**
78
+ * tool-audit file schema version.
79
+ *
80
+ * 1 — first versioned generation. Adds a file header (`runId`, `featureName`,
81
+ * `storyId`, `sessionRole`) and per-call correlation ids (`callId`,
82
+ * `scopeId`, `turnId`, `roundTrips`, `toolCallId`).
83
+ *
84
+ * Files written before this field existed carry none of the above and
85
+ * cannot be backfilled: `runId` in particular was never in scope at the
86
+ * sink's construction path, so an unversioned file's only identity is its
87
+ * filename.
88
+ *
89
+ * `sessionName` is a HUMAN LABEL from this version on, never a join key.
90
+ * It is constant-prefixed and stable across re-runs by construction, so it
91
+ * collides: 7.5% of review-audit sessionNames span more than one runId.
92
+ * Join on `runId` plus `callId`/`turnId` instead.
93
+ *
94
+ * NOTE ON `resultBytes`: it is measured AFTER the shared model-truncation
95
+ * policy (applyModelTruncationPolicy, src/tools/spill.ts) and so counts what
96
+ * the model actually received — the spill marker is composed inside the
97
+ * measured content, so its bytes count. That boundary moved at the
98
+ * `native-loop-events` merge (f4b3bbc7a): files written before it measured
99
+ * each tool's own post-truncation `result.content.length` at `ctx.maxBytes`
100
+ * instead. The policy additionally applies `MODEL_MAX_LINES` (1_000),
101
+ * `MODEL_MAX_LINE_CHARS` (2_000) and a `MODEL_MAX_BYTES` (40_000) ceiling,
102
+ * so the two generations are not comparable call for call. The unit was
103
+ * never bytes: it is `String#length`, i.e. UTF-16 code units, so a
104
+ * multi-byte result under-reports against the field name.
105
+ * `resultBytesPreTruncation` is the pre-policy size and, where a tool sets
106
+ * it, is UTF-8 bytes (`Buffer.byteLength` / `Bun.file().size`).
107
+ */
108
+ export const TOOL_AUDIT_SCHEMA_VERSION = 1;
109
+ /**
110
+ * Monotonic millisecond stamp for audit filenames.
111
+ *
112
+ * `Date.now()` alone is not enough: `flushOpenToolAuditSinks` writes every
113
+ * still-open sink for a run in one tight loop, so two sinks sharing a
114
+ * `sessionName` routinely land on the same millisecond -- the second
115
+ * `writeFile` then silently clobbers the first and one hop's calls are gone.
116
+ * Nudging the stamp forward on a repeat keeps the filename shape
117
+ * (`<runId>-<stamp>-<sessionName>.json`) and guarantees one file per sink.
118
+ */
119
+ let lastFileStamp = 0;
120
+ function nextFileStamp() {
121
+ const now = Date.now();
122
+ lastFileStamp = now > lastFileStamp ? now : lastFileStamp + 1;
123
+ return lastFileStamp;
124
+ }
125
+ export function createToolAuditSink(opts) {
126
+ const calls = [];
127
+ const runId = opts.header?.runId;
128
+ // Set once the shutdown path has written this sink. The hop `finally` that
129
+ // may still run afterwards must not write a second, non-partial file.
130
+ let partialFlushed = false;
131
+ const write = async (partial) => {
132
+ if (calls.length === 0)
133
+ return;
134
+ await mkdir(opts.dir, { recursive: true });
135
+ const body = JSON.stringify({
136
+ schemaVersion: TOOL_AUDIT_SCHEMA_VERSION,
137
+ ...(opts.header ?? {}),
138
+ sessionName: opts.sessionName,
139
+ // Additive and optional: absent on a normal flush, `true` only when the
140
+ // shutdown path persisted a buffer its hop never closed.
141
+ ...(partial ? { partial: true } : {}),
142
+ calls: redactRowStrings(calls),
143
+ }, null, 2);
144
+ const prefix = runId !== undefined ? `${runId}-` : "";
145
+ await writeFile(join(opts.dir, `${prefix}${nextFileStamp()}-${opts.sessionName}.json`), body);
146
+ };
147
+ const sink = {
148
+ record(entry) {
149
+ // The partial file is this sink's last write (a second would duplicate
150
+ // it), so a row arriving after a SUCCESSFUL partial flush cannot be
151
+ // persisted. Drop it loudly rather than losing it without a trace.
152
+ if (partialFlushed) {
153
+ getSafeLogger()?.warn("tools", "tool-audit row recorded after the partial flush was dropped", {
154
+ runId,
155
+ tool: entry.tool,
156
+ });
157
+ return;
158
+ }
159
+ calls.push(entry);
160
+ },
161
+ async flushPartial() {
162
+ if (partialFlushed)
163
+ return;
164
+ await write(true);
165
+ // Only now is the buffer spent. A rejected write leaves the sink open so
166
+ // the hop `finally` can still write the full file instead of nothing.
167
+ partialFlushed = true;
168
+ if (runId !== undefined)
169
+ unregisterToolAuditSink(runId, sink);
170
+ },
171
+ async flush() {
172
+ // A later hop `finally` after the partial flush: nothing left to write.
173
+ if (partialFlushed)
174
+ return;
175
+ if (runId !== undefined)
176
+ unregisterToolAuditSink(runId, sink);
177
+ await write(false);
178
+ },
179
+ };
180
+ if (runId !== undefined)
181
+ registerToolAuditSink(runId, sink);
182
+ return sink;
183
+ }