agentbox-app 0.1.6

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 (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. package/worker-permissions.json +6 -0
package/main/codex.mjs ADDED
@@ -0,0 +1,757 @@
1
+ // A CODEX TURN, IN THE WORDS AGENTBOX ALREADY DRAWS.
2
+ //
3
+ // main/codex-app-server.mjs is the transport: one child process speaking
4
+ // newline-delimited JSON-RPC 2.0, approvals answered or refused, threads
5
+ // routed. It hands its caller notifications and nothing else. THIS file is the
6
+ // layer above it, and its whole job is that a Codex run reaches her page
7
+ // looking like every other run.
8
+ //
9
+ // The supervisor runs four independent readers over every line a Claude Code
10
+ // worker prints (main/supervisor.mjs:4085-4290), and a fifth reader over the
11
+ // finished session (main/code-change.mjs):
12
+ //
13
+ // captureStream -> session.sessionId, session.result, session.resultIsError
14
+ // streamingText -> session.saying, the sentence being typed right now
15
+ // traceStreamLine -> the persisted trace, "HH:MM:SS [Bash] <cmd>"
16
+ // summarizeStreamLine -> the short In Progress tail
17
+ // changeFromTranscript-> the artifact she opens, files and hunks and rows
18
+ //
19
+ // Each has a twin below producing THE SAME OUTPUT SHAPE, so a later slice can
20
+ // swap readers per engine and nothing downstream — the thread, the byline, the
21
+ // file chips, the diff pane, the header arithmetic — needs to know which engine
22
+ // ran. That is not a tidiness preference.
23
+ //
24
+ // ONE STRUCTURAL DIFFERENCE, AND IT IS IN EVERY SIGNATURE HERE. Claude Code's
25
+ // readers take a raw LINE and parse it themselves, five times over, because
26
+ // stdout is where the events arrive. app-server's events arrive already parsed:
27
+ // the transport does the framing, the JSON and the routing, and calls
28
+ // `onNotification(method, params)`. So every reader below takes `(method,
29
+ // params)` instead of a line, and none of them contains a `JSON.parse`. The
30
+ // try/catch is still there, because a reader that throws inside the
31
+ // supervisor's stdout loop takes the reader out for the rest of the session,
32
+ // which is a worker that keeps running and reports nothing — the exact silent
33
+ // shape CLAUDE.md files under "A WORKER THAT DID NOT FINISH DELIVERED NOTHING".
34
+ //
35
+ // `git show 258d71d^:main/codex.mjs` is the 08-25 original, worth reading for
36
+ // intent. ITS EVENT PARSING IS OBSOLETE TWICE OVER: it targeted `codex exec
37
+ // --json`, whose item types are snake_case (`agent_message`, `file_change`) and
38
+ // which carries neither `phase` nor a real diff. One thing is carried over
39
+ // deliberately and credited here rather than rediscovered — see the trace.
40
+ //
41
+ // ------------------------------------------------------------------------
42
+ // MEASURED ON THIS MAC 2026-09-04 AGAINST codex-cli 0.148.0, over a 223-frame
43
+ // capture of one real turn, plus the CLI's own
44
+ // `codex app-server generate-json-schema` dump for the cases that turn did not
45
+ // produce. Six things differ from the written brief, and each is a whole
46
+ // feature that fails silently rather than loudly:
47
+ //
48
+ // 1. A `fileChange` OF KIND `add` IS NOT A DIFF. The `update` entry read
49
+ // "@@ -1 +1 @@\n-...\n+...\n"; the `add` entry in the same event was the
50
+ // string "hello\n" — raw file content, no `@@` header. A reader that
51
+ // assumes a unified diff per file finds zero hunks in every file a run
52
+ // creates. `hunksFor` therefore branches on the EVIDENCE IN THE STRING, not
53
+ // on the kind word, which also means a `delete` (which that run did not
54
+ // produce) is read correctly whichever of the two shapes it turns out to be.
55
+ //
56
+ // 2. THERE IS NO `turn/failed` NOTIFICATION. `ServerNotification` lists
57
+ // `turn/started`, `turn/completed` and `turn/moderationMetadata`, and the
58
+ // string "turn/failed" appears in none of the schema files. Failure arrives
59
+ // as `turn/completed` with `turn.status: "failed"` and a populated
60
+ // `turn.error` — "Only populated when the Turn's status is failed". A reader
61
+ // waiting for `turn/failed` marks every failed run a success. It is still
62
+ // ANSWERED below, because a notification we drop is a run that reads as
63
+ // unfinished forever, and being wrong in that direction costs nothing.
64
+ //
65
+ // 3. `TurnStatus` HAS FOUR VALUES: completed | interrupted | failed |
66
+ // inProgress. `interrupted` is what `turn/interrupt` leaves behind and it is a
67
+ // run with no answer exactly as much as `failed` is.
68
+ //
69
+ // 4. A COMMAND IN FLIGHT IS `inProgress`, camelCase, not `in_progress`.
70
+ // `CommandExecutionStatus` = inProgress | completed | failed | declined, and
71
+ // `declined` is the status an approval this app refuses produces.
72
+ //
73
+ // 5. `reasoning` ITEMS WERE EMPTY — all five of them, `summary: []` and
74
+ // `content: []`. See the trace for what that settles.
75
+ //
76
+ // 6. `move_path` EXISTS ONLY ON THE `update` KIND. `PatchChangeKind` is a
77
+ // three-way union and only `UpdatePatchChangeKind` carries the field. A
78
+ // rename is an update that moved; `add` and `delete` have nothing but
79
+ // `type`.
80
+ //
81
+ // WHAT THIS FILE DELIBERATELY DOES NOT READ, so the next slice does not have to
82
+ // re-derive that it is missing rather than broken. 0.148.0's `ThreadItem` union
83
+ // has eighteen members; five are handled here and the rest are ignored. The one
84
+ // that will matter first is `mcpToolCall` (`{server, tool, arguments, status,
85
+ // result, error, durationMs}`), because the store is an MCP server and a
86
+ // worker's claim/update/finish calls are therefore invisible in a Codex trace
87
+ // today where they are `[mcp__<store>__update_item]` lines in a Claude one.
88
+ // It is left out because no measured run has produced one and this repo's rule
89
+ // is that a mapping is verified by running it; `plan`, `webSearch`,
90
+ // `subAgentActivity` and `contextCompaction` are the same. None of them throws.
91
+
92
+ import fs from 'node:fs';
93
+ import path from 'node:path';
94
+ import { shortPath } from './code-change.mjs';
95
+ import { filesFromPatch } from './git-change.mjs';
96
+
97
+ /**
98
+ * A block of prose long enough that keeping it in memory is a bug rather than
99
+ * a feature. Her longest single line of agent prose over 1,664 traces is 423
100
+ * characters, so this is fifty times the worst real one; past it the block is
101
+ * dropped and the trace is left to deliver it whole, which costs her the live
102
+ * view of one paragraph and cannot cost the process anything.
103
+ *
104
+ * AND IT IS NOW THE ONLY COPY. It was written twice for one slice, because the
105
+ * supervisor kept a module-private `const` that the reader slice was not
106
+ * allowed to edit. Two copies of a rule become two different rules within a
107
+ * week, and this pair drifting apart would be a live sentence one engine drops
108
+ * at 20,000 characters and the other keeps, sitting under its own traced copy
109
+ * on her screen forever. main/supervisor.mjs imports this one.
110
+ */
111
+ export const SAYING_CAP = 20_000;
112
+
113
+ /**
114
+ * The two guards that decide whether a path belongs in her artifact at all.
115
+ *
116
+ * COPIED, CHARACTER FOR CHARACTER, from `collector` in main/code-change.mjs,
117
+ * which is module-private there and which this slice may not modify. CLAUDE.md
118
+ * is right that two copies of a rule become two different rules within a week,
119
+ * so the copies are EXPORTED and
120
+ * tests/a-codex-turn-reads-like-every-other-run-on-her-page.test.mjs asserts
121
+ * they still match the source text of the original. Edit either and the other
122
+ * goes red naming the file it has to move with.
123
+ *
124
+ * They matter more here than they do there. Codex reports an absolute path for
125
+ * every file it touches, including its own /tmp scratch, so these are the
126
+ * common case rather than the corner one.
127
+ */
128
+ export const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
129
+ export const SCRATCH = /^\/(private\/)?(tmp|var\/folders)\//;
130
+
131
+ /* =============================== the answer =============================== */
132
+
133
+ /**
134
+ * The final word of a turn, and the id a reply resumes.
135
+ *
136
+ * THE HARD DIFFERENCE FROM CLAUDE CODE, AND THE WHOLE REASON THIS FUNCTION IS
137
+ * NOT A RENAME OF `captureStream`: app-server has no `result` event. Claude
138
+ * Code's stream ends with one `{type:'result'}` line carrying the answer and an
139
+ * `is_error` flag, and `captureStream` keys on it. There is nothing shaped like
140
+ * that here. What replaces it is `phase` on an `agentMessage`: measured over
141
+ * one real turn, five agent messages, four of them `commentary` and EXACTLY ONE
142
+ * `final_answer`.
143
+ *
144
+ * So the answer is the final_answer message and nothing else. The 08-25 build
145
+ * took the LAST message it saw, because `codex exec` gave it no phase to read;
146
+ * doing that here puts a progress note on her row as the worker's finished
147
+ * word, and `awaitingHer` (supervisor.mjs) then reports a row as waiting on her
148
+ * when the work never finished. That is her 2026-08-06 incident — a `done` she
149
+ * never saw — arriving through a new door.
150
+ *
151
+ * LAST-WINS SURVIVES AS THE COMPATIBILITY PATH AND ONLY THAT. The schema says
152
+ * plainly that "providers do not emit this consistently, so callers must treat
153
+ * None as 'phase unknown' and keep compatibility behavior for legacy models".
154
+ * An unphased message therefore sets the answer — but never over a real
155
+ * final_answer, and never after one has arrived.
156
+ *
157
+ * A TURN THAT ENDS WITH NO FINAL ANSWER GETS NO RESULT, ON PURPOSE. That is
158
+ * what `saidNothingSheCanUse` is for: `result == null` is the first of the
159
+ * three shapes CLAUDE.md names, "said nothing at all -> the supervisor speaks
160
+ * for it", and there is already a writer for it in OUR words. Inventing a
161
+ * result here would take that row away from the writer that exists and hand her
162
+ * a guess in the agent's voice. The one thing worth doing is looking once more
163
+ * where the answer might still be: `turn/completed` carries `turn.items`, and
164
+ * in the measured run that array held exactly the final agentMessage even
165
+ * though `itemsView` was "summary". So a run whose item events were missed
166
+ * still has its answer in the last frame, for free.
167
+ */
168
+ export function captureCodexEvent(session, method, params) {
169
+ try {
170
+ if (!session || typeof method !== 'string') return;
171
+ const p = params ?? {};
172
+
173
+ // The id `thread/resume` takes, and it is at `params.thread.id`. There is
174
+ // no `params.threadId` on this notification; reading the one that does not
175
+ // exist leaves every session unresumable with no symptom until she types a
176
+ // reply on a finished row.
177
+ if (method === 'thread/started') {
178
+ const id = p.thread?.id;
179
+ if (id) session.sessionId = String(id);
180
+ return;
181
+ }
182
+
183
+ if (method === 'item/completed') {
184
+ const item = p.item ?? {};
185
+ if (item.type === 'exitedReviewMode' && typeof item.review === 'string' && item.review.trim()) {
186
+ session.result = item.review;
187
+ session.resultIsError = false;
188
+ session.codexFinalAnswer = true;
189
+ return;
190
+ }
191
+ if (item?.type !== 'agentMessage') return;
192
+ const phase = typeof item.phase === 'string' ? item.phase : null;
193
+ if (phase === 'commentary') return;
194
+ if (session.codexFinalAnswer && phase !== 'final_answer') return;
195
+ session.result = String(item.text ?? '');
196
+ session.resultIsError = false;
197
+ if (phase === 'final_answer') session.codexFinalAnswer = true;
198
+ return;
199
+ }
200
+
201
+ if (method === 'turn/completed' || method === 'turn/failed') {
202
+ const turn = p.turn;
203
+ if (!turn || typeof turn !== 'object') return;
204
+ const status = turnStatus(method, turn);
205
+ if (status === 'completed') {
206
+ if (session.result != null) return;
207
+ const answer = finalAnswerIn(turn.items);
208
+ if (answer == null) return;
209
+ session.result = answer;
210
+ session.resultIsError = false;
211
+ session.codexFinalAnswer = true;
212
+ return;
213
+ }
214
+ // A turn that failed after saying something still failed, and her row
215
+ // must not read as answered because a message got through first.
216
+ session.result = turnTrouble(turn, status);
217
+ session.resultIsError = true;
218
+ }
219
+ } catch { /* a reader that throws is a session that goes quiet */ }
220
+ }
221
+
222
+ /** completed | interrupted | failed | inProgress, defaulting to the method. */
223
+ function turnStatus(method, turn) {
224
+ if (typeof turn?.status === 'string') return turn.status;
225
+ return method === 'turn/failed' ? 'failed' : 'completed';
226
+ }
227
+
228
+ /** Why a turn did not finish, in whatever words the server gave us. */
229
+ function turnTrouble(turn, status) {
230
+ const why = String(turn?.error?.message ?? '').trim();
231
+ const more = String(turn?.error?.additionalDetails ?? '').trim();
232
+ const head = why || (status === 'interrupted' ? 'the turn was interrupted' : `the turn ${status}`);
233
+ return more ? `${head}\n${more}` : head;
234
+ }
235
+
236
+ /**
237
+ * The final answer inside a turn's own item list, or null.
238
+ *
239
+ * ONLY a `final_answer` counts, never the last message in the list. The list
240
+ * arrives as a summary view and a summary of a turn that narrated and then
241
+ * died is narration.
242
+ */
243
+ function finalAnswerIn(items) {
244
+ if (!Array.isArray(items)) return null;
245
+ for (let i = items.length - 1; i >= 0; i -= 1) {
246
+ const item = items[i];
247
+ if (item?.type === 'exitedReviewMode' && typeof item.review === 'string' && item.review.trim()) return item.review;
248
+ if (item?.type === 'agentMessage' && item?.phase === 'final_answer') return String(item.text ?? '');
249
+ }
250
+ return null;
251
+ }
252
+
253
+ /* ============================ the live sentence =========================== */
254
+
255
+ /**
256
+ * THE SENTENCE THE AGENT IS TYPING RIGHT NOW, over app-server's own deltas.
257
+ *
258
+ * Claude Code's half of that is `streamingText` reading
259
+ * `--include-partial-messages` frames; `item/agentMessage/delta` is the same
260
+ * channel here, 157 frames in the measured turn, with the text at
261
+ * `params.delta`.
262
+ *
263
+ * The three rules are the supervisor's, unchanged, and each of them is load
264
+ * bearing (see supervisor.mjs:4131 for the long form):
265
+ *
266
+ * - ONE BLOCK AT A TIME. An `item/started` for an agentMessage starts the next
267
+ * one from empty, so the live string and the string the trace will carry are
268
+ * the same string, and the thread can drop the live copy the moment the
269
+ * traced one arrives rather than guessing.
270
+ * - NOT CLEARED WHEN THE MESSAGE FINISHES. The trace is written on one channel
271
+ * and read on another; clearing here blinks the sentence off her screen for
272
+ * however long the read takes and then puts it back.
273
+ * - DROPPED AT THE CAP, NEVER CUT. A cut string never matches the traced one
274
+ * and would sit under it on her screen forever.
275
+ *
276
+ * Returns true when the string moved, which is what earns a push.
277
+ */
278
+ export function codexStreamingText(session, method, params) {
279
+ try {
280
+ if (!session) return false;
281
+ const p = params ?? {};
282
+
283
+ if (method === 'item/started') {
284
+ // Only prose resets it. A command starting or a patch being applied
285
+ // means this turn has moved on from talking, and the block already typed
286
+ // stays on screen until the trace delivers the same words.
287
+ if (p.item?.type !== 'agentMessage') return false;
288
+ session.saying = '';
289
+ session.sayingAt = Date.now();
290
+ return true;
291
+ }
292
+
293
+ if (method !== 'item/agentMessage/delta') return false;
294
+ const piece = String(p.delta ?? '');
295
+ if (!piece) return false;
296
+ if (!session.sayingAt) session.sayingAt = Date.now();
297
+ const next = (session.saying ?? '') + piece;
298
+ session.saying = next.length > SAYING_CAP ? '' : next;
299
+ return true;
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+
305
+ /* ================================ the trace =============================== */
306
+
307
+ /**
308
+ * The persisted trace line, in the format renderer/src/run-files.ts parses:
309
+ * `/^\d\d:\d\d:\d\d {2}(.*)$/`, with a tool in brackets and its key argument
310
+ * after it. Fuller than the tail, because "tool: Bash" answers nothing when the
311
+ * founder asks what a session actually did.
312
+ *
313
+ * ONLY `item/completed` IS TRACED, AND THE 08-25 BUILD ALREADY KNEW WHY: the
314
+ * same item arrives on `item/started` with an empty output and again on
315
+ * `item/completed` with the real one. Tracing both puts every command on her
316
+ * page twice — three commands became six lines in the measured turn.
317
+ *
318
+ * A READ DONE WITH `sed` IS TRACED AS THE SHELL COMMAND IT WAS. Codex reads and
319
+ * searches by running shell commands rather than by calling tools named Read
320
+ * and Grep, so its work lines really are `[Bash] sed -n '1,200p' math.js` where
321
+ * Claude Code's would be `[Read] math.js`. Rewriting one into the other is
322
+ * Agentbox claiming a tool call Codex never made — and the command is also the
323
+ * thing an approval card is raised about, so the honest spelling is the only
324
+ * one that stays true next to the card. Her peers instruction cuts this way as
325
+ * well as the other: equivalent does not mean identical.
326
+ *
327
+ * AND THE BRACKETS ON A FILE CHANGE ARE `Write` AND `Edit` ANYWAY, which is the
328
+ * same call read the other way round. The bracket is the trace's own shared
329
+ * vocabulary — `[Bash]` is already Claude Code's word for a shell command and
330
+ * both engines use it — and renderer/src/run-files.ts matches
331
+ * `/^(Write|Edit|NotebookEdit)$/` on it to find the files a run made for her.
332
+ * A Codex worker that wrote her an .html through its patch tool would lose the
333
+ * chip on its card under any other word. THAT is what marking Codex as lesser
334
+ * would actually look like in the product. `Delete` and `Move` are new words
335
+ * because Claude Code has no tool that does either, so there is nothing to
336
+ * borrow and nothing to misrepresent.
337
+ *
338
+ * NOTHING IS WRITTEN FOR A `reasoning` ITEM. Claude Code's trace carries no
339
+ * thinking, one surface should not grow two vocabularies, and the measurement
340
+ * settles it besides: all five reasoning items in the captured turn had
341
+ * `summary: []` and `content: []`, so a reasoning line would have been five
342
+ * bare timestamps in a twenty-eight second run.
343
+ */
344
+ export function traceCodexEvent(method, params) {
345
+ try {
346
+ const p = params ?? {};
347
+ const at = new Date().toISOString().slice(11, 19);
348
+
349
+ if (method === 'item/completed') {
350
+ const item = p.item ?? {};
351
+
352
+ if (item.type === 'exitedReviewMode' && typeof item.review === 'string') {
353
+ return item.review.trim() ? `${at} ${item.review.trim()}` : null;
354
+ }
355
+
356
+ if (item?.type === 'agentMessage') {
357
+ const text = String(item.text ?? '').trim();
358
+ return text ? `${at} ${text}` : null;
359
+ }
360
+
361
+ if (item?.type === 'commandExecution') {
362
+ const command = String(item.command ?? '').trim();
363
+ return command ? `${at} [Bash] ${command.slice(0, 200)}` : null;
364
+ }
365
+
366
+ if (item?.type === 'fileChange') {
367
+ const lines = changesOf(item)
368
+ .map(fileVerb)
369
+ .filter(Boolean)
370
+ .map((body) => `${at} ${body}`);
371
+ return lines.length ? lines.join('\n') : null;
372
+ }
373
+
374
+ return null;
375
+ }
376
+
377
+ if (method === 'turn/completed' || method === 'turn/failed') {
378
+ const turn = p.turn;
379
+ if (!turn || typeof turn !== 'object') return null;
380
+ const status = turnStatus(method, turn);
381
+ const bad = status !== 'completed';
382
+ // Claude Code's footer counts turns; Codex has exactly one turn per run,
383
+ // so that number would read "1 turns" forever and tell her nothing. The
384
+ // duration goes in its place, which is the fact the turn actually
385
+ // carries (`durationMs`, 27746 in the measured run).
386
+ const span = Number.isFinite(turn.durationMs) ? `${(turn.durationMs / 1000).toFixed(1)}s` : '?s';
387
+ const said = bad ? turnTrouble(turn, status) : (finalAnswerIn(turn.items) ?? '');
388
+ return `\n${at} == RESULT (${status}${bad ? ' ERROR' : ''} · ${span}) ==\n${said.slice(0, 4000)}`;
389
+ }
390
+
391
+ return null;
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+
397
+ /** The changes on a fileChange item, always an array. */
398
+ function changesOf(item) {
399
+ return Array.isArray(item?.changes) ? item.changes : [];
400
+ }
401
+
402
+ /**
403
+ * WHAT ONE FILE CHANGE DID, IN ONE WORD, AND THE ONLY COPY OF THAT RULE. Null
404
+ * when the change names no file, which is the one shape that cannot be said out
405
+ * loud at all.
406
+ *
407
+ * Exported because main/codex-approvals.mjs needs the same four words on the
408
+ * approval card she reads BEFORE a patch is applied. A second copy of it would
409
+ * be one edit away from a trace line saying `[Write]` under a card that said
410
+ * `Edit` about the same change. `move_path` lives only on the `update` kind
411
+ * (measurement 6 at the top of this file), so a rename is an update that moved.
412
+ */
413
+ export function fileVerbWord(change) {
414
+ const from = String(change?.path ?? '').trim();
415
+ if (!from) return null;
416
+ const kind = change?.kind ?? {};
417
+ const to = typeof kind.move_path === 'string' ? kind.move_path.trim() : '';
418
+ if (kind.type === 'add') return 'Write';
419
+ if (kind.type === 'delete') return 'Delete';
420
+ if (to && to !== from) return 'Move';
421
+ if (kind.type === 'update') return 'Edit';
422
+ return null;
423
+ }
424
+
425
+ /** One file change as its bracket and its path. Null when there is no path. */
426
+ function fileVerb(change) {
427
+ const word = fileVerbWord(change);
428
+ if (!word) return null;
429
+ const from = String(change.path).trim().slice(0, 200);
430
+ // Both ends of a move, because the path she would go looking for is the one
431
+ // it is at now and the one above it is the one it left.
432
+ if (word === 'Move') return `[Move] ${from} -> ${String(change.kind.move_path).trim().slice(0, 200)}`;
433
+ return `[${word}] ${from}`;
434
+ }
435
+
436
+ /* ================================= the tail =============================== */
437
+
438
+ /** The In Progress tail wants the gist, in the same words Claude Code's does. */
439
+ export function summarizeCodexEvent(method, params) {
440
+ try {
441
+ const p = params ?? {};
442
+
443
+ if (method === 'item/completed') {
444
+ const item = p.item ?? {};
445
+ if (item?.type === 'agentMessage') return String(item.text ?? '').slice(0, 300) || null;
446
+ if (item?.type === 'commandExecution') return String(item.command ?? '').trim() ? 'tool: Bash' : null;
447
+ if (item?.type === 'fileChange') {
448
+ const words = [];
449
+ for (const change of changesOf(item)) {
450
+ const verb = fileVerb(change);
451
+ if (!verb) continue;
452
+ const word = verb.slice(1, verb.indexOf(']'));
453
+ if (!words.includes(word)) words.push(word);
454
+ }
455
+ return words.length ? `tool: ${words.join(', ')}` : null;
456
+ }
457
+ return null;
458
+ }
459
+
460
+ if (method === 'turn/completed' || method === 'turn/failed') {
461
+ const turn = p.turn;
462
+ if (!turn || typeof turn !== 'object') return null;
463
+ const status = turnStatus(method, turn);
464
+ const said = status === 'completed' ? (finalAnswerIn(turn.items) ?? '') : turnTrouble(turn, status);
465
+ return `done: ${said.slice(0, 200)}`;
466
+ }
467
+
468
+ return null;
469
+ } catch {
470
+ return null;
471
+ }
472
+ }
473
+
474
+ /* =============================== the change =============================== */
475
+
476
+ /**
477
+ * HOW MANY FRAMES OF ONE RUN ARE WORTH HOLDING FOR ITS ARTIFACT.
478
+ *
479
+ * The other engine's conversation is on disk and is read once at the exit, so
480
+ * it costs nothing while the run goes on. There is no such file here: an
481
+ * app-server notification is delivered once and gone, so the only place the
482
+ * conversation half of a Codex artifact can be assembled from is memory.
483
+ *
484
+ * The measured turn was 223 frames end to end and left five of the two kinds
485
+ * this keeps, so five thousand is more than twenty times a whole real run's
486
+ * frames and a thousand times its edits. Past it nothing further is kept and
487
+ * the artifact is short by whatever came after -- which is a partial card and
488
+ * not a broken one, because the DISK half is read independently and wins
489
+ * wherever the two disagree (`mergeChange`, main/code-change.mjs). Growing
490
+ * without a bound is the alternative, and a run that edits in a loop would take
491
+ * her app down with it.
492
+ */
493
+ export const CHANGE_FRAME_CAP = 5_000;
494
+
495
+ /**
496
+ * AND HOW MANY BYTES, BECAUSE A FRAME CAP IS NOT A MEMORY CAP.
497
+ *
498
+ * The count above bounds how MANY frames are kept and says nothing about how
499
+ * big each one is, and each one may carry up to the transport's own frame limit
500
+ * (MAX_FRAME_LENGTH, 16 MiB). Five thousand of those is eighty gigabytes, which
501
+ * is not a bound at all: it is her app dying on the exact run this cap was
502
+ * written to survive, the one that edits in a loop.
503
+ *
504
+ * Sixteen mebibytes, which is one whole frame's worth of everything. The
505
+ * measured turn's five kept frames were a few kilobytes between them, so this
506
+ * is thousands of times a real run and still a number the process can hold. Past
507
+ * it nothing further is kept and the artifact is short by whatever came after --
508
+ * a partial card and not a broken one, because the DISK half is read
509
+ * independently and wins wherever the two disagree (`mergeChange`,
510
+ * main/code-change.mjs).
511
+ */
512
+ export const CHANGE_BYTE_CAP = 16 * 1024 * 1024;
513
+
514
+ /**
515
+ * KEEP THE FRAMES THE ARTIFACT WILL NEED, AND NOTHING ELSE.
516
+ *
517
+ * Two kinds out of the eighteen in `ThreadItem`: the `fileChange` items, which
518
+ * are the edits themselves, and the `agentMessage` items, which are where the
519
+ * sentence beside a hunk comes from. Their ORDER is the whole of how the second
520
+ * attaches to the first (`changeFromCodexTurn` takes the nearest preceding
521
+ * message), so they are kept in one list rather than two.
522
+ *
523
+ * Called on the session's own event path, so it is written to be unable to
524
+ * throw there: a reader that throws inside the supervisor's stream loop takes
525
+ * itself out for the rest of the session, which is a worker that keeps running
526
+ * and reports nothing.
527
+ */
528
+ export function rememberCodexChange(session, method, params) {
529
+ try {
530
+ if (!session || method !== 'item/completed') return;
531
+ const type = params?.item?.type;
532
+ if (type !== 'fileChange' && type !== 'agentMessage') return;
533
+ const kept = session.codexChange ?? (session.codexChange = []);
534
+ if (kept.length >= CHANGE_FRAME_CAP) return;
535
+ // AND THE SIZE OF WHAT IS ALREADY HELD, WHICH THE COUNT DOES NOT BOUND.
536
+ // A fileChange frame carries the whole per-file diff, so five thousand of
537
+ // them is only a bound if each one is small, and the one run this cap was
538
+ // written for -- the one that edits in a loop -- is exactly the run where
539
+ // they are not. Measured off the frame itself rather than off a guess: a
540
+ // diff is a string, and its length is the number that matters.
541
+ const size = frameSize(params);
542
+ const held = session.codexChangeBytes ?? 0;
543
+ if (held + size > CHANGE_BYTE_CAP) return;
544
+ session.codexChangeBytes = held + size;
545
+ kept.push({ method, params });
546
+ } catch { /* an artifact is never worth a session */ }
547
+ }
548
+
549
+ /**
550
+ * Roughly how much of one frame this is going to hold on to.
551
+ *
552
+ * The two fields that actually carry weight are the per-file `diff` and the
553
+ * agent's `text`; everything else on a `ThreadItem` is ids and enums. Counted
554
+ * rather than JSON.stringify'd because this runs on the session's own event
555
+ * path, once per item, and serialising a ten-megabyte diff to measure it is the
556
+ * cost the cap exists to avoid.
557
+ */
558
+ function frameSize(params) {
559
+ const item = params?.item ?? {};
560
+ let n = String(item.text ?? '').length;
561
+ for (const change of changesOf(item)) n += String(change?.diff ?? '').length;
562
+ return n;
563
+ }
564
+
565
+ /**
566
+ * WHAT THE RUN CHANGED, in the shape main/code-change.mjs already produces.
567
+ *
568
+ * The pane that draws it (renderer/src/components/CodeArtifact.tsx), the
569
+ * arithmetic under it (renderer/src/code-artifact.ts) and the chip on the card
570
+ * (renderer/src/run-files.ts) are untouched by this slice, so matching the
571
+ * shape exactly IS the deliverable: `{files:[{path, abs, hunks:[{rows, plus,
572
+ * minus, said?, at?}], plus, minus, at}], roots, plus, minus, editCount,
573
+ * withProse, startedAt, endedAt}`.
574
+ *
575
+ * AND CODEX HANDS US THE HALF THAT WAS EXPENSIVE. The 08-25 build needed a
576
+ * 161-line V4A patch parser to reconstruct what a run had changed out of
577
+ * `codex exec`'s patch text. `item/completed` now carries a real per-file diff,
578
+ * so the rows come out of the same unified-diff reader git's own changes go
579
+ * through (`filesFromPatch`, main/git-change.mjs) rather than a second copy of
580
+ * one. The only thing synthesized is the two-line file header that reader
581
+ * expects; the hunk bodies are the server's own bytes.
582
+ *
583
+ * `events` are the notifications as the transport delivered them, `{method,
584
+ * params}`, in order. `roots` are the checkouts this run's work happened in and
585
+ * `since` is the moment to start from, both exactly as code-change.mjs means
586
+ * them. `rememberCodexChange` below is what collects them.
587
+ */
588
+ export function changeFromCodexTurn(events, { roots = [], since = 0 } = {}) {
589
+ const into = collector({ roots });
590
+ // The agent's own sentence beside the hunk. Claude Code takes it from the
591
+ // text blocks of the same message the edit was in; Codex narrates in a
592
+ // separate agentMessage just before it applies the patch, so the nearest
593
+ // preceding message is the same sentence.
594
+ let said = '';
595
+
596
+ for (const event of Array.isArray(events) ? events : []) {
597
+ if (event?.method !== 'item/completed') continue;
598
+ const p = event.params ?? {};
599
+ const item = p.item ?? {};
600
+
601
+ if (item?.type === 'agentMessage') {
602
+ said = proseFrom(item.text);
603
+ continue;
604
+ }
605
+ if (item?.type !== 'fileChange') continue;
606
+
607
+ const stamp = Number(p.completedAtMs ?? event.emittedAtMs ?? 0) || 0;
608
+ if (since && stamp && stamp < since) continue;
609
+
610
+ for (const change of changesOf(item)) {
611
+ const abs = destinationOf(change);
612
+ if (!abs) continue;
613
+ for (const hunk of hunksFor(change)) into.add(abs, hunk.rows, { said, stamp });
614
+ }
615
+ }
616
+
617
+ return into.done();
618
+ }
619
+
620
+ /**
621
+ * Where the file IS once the change has landed.
622
+ *
623
+ * A rename is filed under the path it moved to, which is the rule
624
+ * main/git-change.mjs already states for git's own renames. A change she
625
+ * opens has to lead to a file that exists.
626
+ */
627
+ function destinationOf(change) {
628
+ const to = typeof change?.kind?.move_path === 'string' ? change.kind.move_path.trim() : '';
629
+ const from = String(change?.path ?? '').trim();
630
+ return to || from || null;
631
+ }
632
+
633
+ /**
634
+ * One file change as hunks of rows.
635
+ *
636
+ * THE BRANCH IS ON THE STRING, NOT ON THE KIND, and that is the measurement
637
+ * from the top of this file: an `update` arrived as "@@ -1 +1 @@\n-...\n+...",
638
+ * an `add` in the SAME event arrived as "hello\n". A delete did not occur in
639
+ * that run at all, so guessing which shape it uses would be exactly the kind of
640
+ * unverified mapping this repo refuses; asking the string instead answers for
641
+ * every kind, including ones this CLI has not shipped yet.
642
+ */
643
+ function hunksFor(change) {
644
+ const diff = String(change?.diff ?? '');
645
+ if (!diff) return [];
646
+ if (diff.startsWith('@@')) {
647
+ // The two header lines `filesFromPatch` opens a file on. A placeholder
648
+ // name rather than the real path, because that reader strips `a/` and `b/`
649
+ // prefixes and trims, and a real path with a space in it would come back
650
+ // wrong; the path is already known here and is never read back out of this.
651
+ return filesFromPatch(`--- a/f\n+++ b/f\n${diff}`)[0]?.hunks ?? [];
652
+ }
653
+ const mark = change?.kind?.type === 'delete' ? '-' : '+';
654
+ const rows = splitLines(diff).map((line) => [mark, line]);
655
+ return rows.length ? [{ rows }] : [];
656
+ }
657
+
658
+ /** A trailing newline is a line terminator, not an empty last line. */
659
+ function splitLines(text) {
660
+ const out = String(text ?? '').split('\n');
661
+ if (out.length > 1 && out[out.length - 1] === '') out.pop();
662
+ return out;
663
+ }
664
+
665
+ /** One line of what the agent said, the same shape code-change.mjs keeps. */
666
+ function proseFrom(text) {
667
+ const said = String(text ?? '').trim();
668
+ if (!said) return '';
669
+ const first = said.split('\n').map((l) => l.trim()).filter(Boolean).pop() ?? '';
670
+ return first.slice(0, 400);
671
+ }
672
+
673
+ /**
674
+ * Each root, and also what the filesystem really calls it.
675
+ *
676
+ * Measured in main/code-change.mjs on 2026-08-26 and it bites harder here: the
677
+ * app knew a checkout as `/tmp/proof/widget-repo` and an edit against
678
+ * `/private/tmp/proof/widget-repo/math.js` matched no root and was dropped, so
679
+ * a run that really did change her code finished with an artifact that did not
680
+ * mention it. Codex reports the RESOLVED spelling for every path it touches, so
681
+ * for this engine that is the ordinary case rather than the corner one.
682
+ *
683
+ * Resolved off the deepest ancestor that still exists rather than off the root
684
+ * itself, because this runs after the work is over and CLAUDE.md's process
685
+ * etiquette asks a session to remove its worktree as the last thing it does.
686
+ */
687
+ function rootSpellings(roots) {
688
+ const out = new Set();
689
+ for (const root of roots.filter(Boolean)) {
690
+ const given = path.resolve(root);
691
+ out.add(given);
692
+ let head = given;
693
+ const tail = [];
694
+ for (let i = 0; i < 64; i += 1) {
695
+ try {
696
+ const real = fs.realpathSync(head);
697
+ out.add(tail.length ? path.join(real, ...tail) : real);
698
+ break;
699
+ } catch { /* not there; try its parent */ }
700
+ const parent = path.dirname(head);
701
+ if (parent === head) break;
702
+ tail.unshift(path.basename(head));
703
+ head = parent;
704
+ }
705
+ }
706
+ return [...out];
707
+ }
708
+
709
+ /**
710
+ * The tally, and the two rules that decide whether a file belongs in her
711
+ * artifact at all. The same collector code-change.mjs keeps private, over
712
+ * absolute paths that arrived in an event instead of in a transcript.
713
+ */
714
+ function collector({ roots = [] } = {}) {
715
+ const files = new Map();
716
+ const totals = { plus: 0, minus: 0, editCount: 0, withProse: 0, startedAt: 0, endedAt: 0 };
717
+ const spellings = rootSpellings(roots);
718
+
719
+ return {
720
+ add(abs, rows, { said = '', stamp = 0 } = {}) {
721
+ abs = String(abs ?? '').trim();
722
+ if (!abs || NOT_CODE.test(abs)) return;
723
+ const rel = shortPath(abs, spellings);
724
+ // Inside one of this run's own roots it is the product, wherever it
725
+ // sits. Outside all of them AND under scratch, it is nobody's.
726
+ if (rel === null && SCRATCH.test(abs)) return;
727
+ if (!rows.length) return;
728
+ const p = rows.filter((r) => r[0] === '+').length;
729
+ const m = rows.filter((r) => r[0] === '-').length;
730
+ if (!p && !m) return;
731
+ const key = rel ?? abs;
732
+ const file = files.get(key) ?? { path: key, abs, hunks: [], plus: 0, minus: 0, at: stamp };
733
+ file.hunks.push({ rows, said: said || undefined, at: stamp || undefined, plus: p, minus: m });
734
+ file.plus += p; file.minus += m;
735
+ files.set(key, file);
736
+ totals.plus += p; totals.minus += m;
737
+ totals.editCount += 1;
738
+ if (said) totals.withProse += 1;
739
+ if (stamp) {
740
+ if (!totals.startedAt || stamp < totals.startedAt) totals.startedAt = stamp;
741
+ if (stamp > totals.endedAt) totals.endedAt = stamp;
742
+ }
743
+ },
744
+ done() {
745
+ return {
746
+ files: [...files.values()],
747
+ roots: [...roots].filter(Boolean),
748
+ plus: totals.plus,
749
+ minus: totals.minus,
750
+ editCount: totals.editCount,
751
+ withProse: totals.withProse,
752
+ startedAt: totals.startedAt || undefined,
753
+ endedAt: totals.endedAt || undefined,
754
+ };
755
+ },
756
+ };
757
+ }