agentbox-app 0.1.13 → 0.1.16

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 (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
package/main/codex.mjs CHANGED
@@ -1,93 +1,26 @@
1
- // A CODEX TURN, IN THE WORDS AGENTBOX ALREADY DRAWS.
1
+ // Readers that turn Codex app-server notifications into the same session
2
+ // fields, trace lines, tail and code artifact that the Claude Code readers
3
+ // produce, so nothing downstream needs to know which engine ran. They take
4
+ // `(method, params)` because the transport (main/codex-app-server.mjs) has
5
+ // already parsed each event. Each reader catches its own errors, because one
6
+ // that throws inside the supervisor's stream loop goes silent for the rest of
7
+ // the session.
2
8
  //
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.
9
+ // Facts about the Codex protocol that the code depends on:
8
10
  //
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):
11
+ // - A `fileChange` `diff` is a unified diff for an `update` but raw file
12
+ // content for an `add`, so `hunksFor` checks the string, not the kind.
13
+ // - There is no `turn/failed` notification. A failure is `turn/completed` with
14
+ // `turn.status: "failed"` and `turn.error`. `turn/failed` is still handled in
15
+ // case a release adds it.
16
+ // - `TurnStatus` is completed, interrupted, failed or inProgress. An
17
+ // interrupted turn has no answer, the same as a failed one.
18
+ // - `move_path` exists only on the `update` kind, so a rename is an update
19
+ // that moved.
20
+ // - `reasoning` items arrive empty, so they are not traced.
12
21
  //
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.
22
+ // Item types not handled here, such as `mcpToolCall`, `plan` and `webSearch`,
23
+ // are ignored.
91
24
 
92
25
  import fs from 'node:fs';
93
26
  import path from 'node:path';
@@ -95,85 +28,36 @@ import { shortPath } from './code-change.mjs';
95
28
  import { filesFromPatch } from './git-change.mjs';
96
29
 
97
30
  /**
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.
31
+ * The longest live sentence kept in memory. Past it the live text is dropped
32
+ * and the trace delivers the block whole. This is the only copy; the Claude
33
+ * Code stream reader (main/harnesses/claude-stream.mjs) imports it.
110
34
  */
111
35
  export const SAYING_CAP = 20_000;
112
36
 
113
37
  /**
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.
38
+ * Paths that never belong in a run's code artifact. Copied from main/code-change.mjs;
39
+ * tests/a-codex-turn-reads-like-every-other-run-on-her-page.test.mjs checks the
40
+ * two copies still match.
127
41
  */
128
42
  export const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
129
43
  export const SCRATCH = /^\/(private\/)?(tmp|var\/folders)\//;
130
44
 
131
- /* =============================== the answer =============================== */
132
-
133
45
  /**
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.
46
+ * Sets the session's id and its final answer. app-server has no `result`
47
+ * event, so the answer is the `agentMessage` with `phase: "final_answer"`.
48
+ * A `commentary` message is never the answer, or a progress note would read as
49
+ * the finished word. A message with no phase still counts, because Codex's
50
+ * schema says older models may not send one, but never over a final answer.
51
+ * A turn that ends with no final answer gets no result, so the supervisor's own
52
+ * "said nothing" handling takes over.
167
53
  */
168
54
  export function captureCodexEvent(session, method, params) {
169
55
  try {
170
56
  if (!session || typeof method !== 'string') return;
171
57
  const p = params ?? {};
172
58
 
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.
59
+ // The id `thread/resume` takes is at `params.thread.id`. This notification
60
+ // has no `params.threadId`.
177
61
  if (method === 'thread/started') {
178
62
  const id = p.thread?.id;
179
63
  if (id) session.sessionId = String(id);
@@ -211,21 +95,18 @@ export function captureCodexEvent(session, method, params) {
211
95
  session.codexFinalAnswer = true;
212
96
  return;
213
97
  }
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.
98
+ // A turn that failed after a message got through still failed.
216
99
  session.result = turnTrouble(turn, status);
217
100
  session.resultIsError = true;
218
101
  }
219
- } catch { /* a reader that throws is a session that goes quiet */ }
102
+ } catch { /* see the note at the top of this file */ }
220
103
  }
221
104
 
222
- /** completed | interrupted | failed | inProgress, defaulting to the method. */
223
105
  function turnStatus(method, turn) {
224
106
  if (typeof turn?.status === 'string') return turn.status;
225
107
  return method === 'turn/failed' ? 'failed' : 'completed';
226
108
  }
227
109
 
228
- /** Why a turn did not finish, in whatever words the server gave us. */
229
110
  function turnTrouble(turn, status) {
230
111
  const why = String(turn?.error?.message ?? '').trim();
231
112
  const more = String(turn?.error?.additionalDetails ?? '').trim();
@@ -234,11 +115,8 @@ function turnTrouble(turn, status) {
234
115
  }
235
116
 
236
117
  /**
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.
118
+ * The final answer in `turn.items`, which still holds it when the item events
119
+ * were missed. Only a `final_answer` counts, never the last message.
242
120
  */
243
121
  function finalAnswerIn(items) {
244
122
  if (!Array.isArray(items)) return null;
@@ -250,30 +128,16 @@ function finalAnswerIn(items) {
250
128
  return null;
251
129
  }
252
130
 
253
- /* ============================ the live sentence =========================== */
254
-
255
131
  /**
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.
132
+ * The sentence the agent is typing now, built from `item/agentMessage/delta`.
133
+ * Returns true when `session.saying` changed.
275
134
  *
276
- * Returns true when the string moved, which is what earns a push.
135
+ * - Each agentMessage starts from empty, so the live text matches the traced
136
+ * block and the thread can drop it when the trace arrives.
137
+ * - It is not cleared when the message finishes, or it would blink off screen
138
+ * until the trace is read.
139
+ * - Past `SAYING_CAP` it is dropped, never cut, because a cut string never
140
+ * matches the traced one.
277
141
  */
278
142
  export function codexStreamingText(session, method, params) {
279
143
  try {
@@ -281,9 +145,8 @@ export function codexStreamingText(session, method, params) {
281
145
  const p = params ?? {};
282
146
 
283
147
  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.
148
+ // Only a new message resets it. A command or patch starting leaves the
149
+ // typed block on screen until the trace delivers it.
287
150
  if (p.item?.type !== 'agentMessage') return false;
288
151
  session.saying = '';
289
152
  session.sayingAt = Date.now();
@@ -302,44 +165,14 @@ export function codexStreamingText(session, method, params) {
302
165
  }
303
166
  }
304
167
 
305
- /* ================================ the trace =============================== */
306
-
307
168
  /**
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.
169
+ * One persisted trace line, in the format renderer/src/run-files.ts parses:
170
+ * `HH:MM:SS [Tool] argument`.
326
171
  *
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.
172
+ * Only `item/completed` is traced, because `item/started` carries the same item
173
+ * with empty output. Codex reads files with shell commands, and they are traced
174
+ * as `[Bash]`, the command it really ran. File changes use `Write` and `Edit`,
175
+ * because run-files.ts finds a run's files by those words.
343
176
  */
344
177
  export function traceCodexEvent(method, params) {
345
178
  try {
@@ -379,10 +212,8 @@ export function traceCodexEvent(method, params) {
379
212
  if (!turn || typeof turn !== 'object') return null;
380
213
  const status = turnStatus(method, turn);
381
214
  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).
215
+ // A Codex run is always one turn, so the footer shows the duration where
216
+ // Claude Code's shows a turn count.
386
217
  const span = Number.isFinite(turn.durationMs) ? `${(turn.durationMs / 1000).toFixed(1)}s` : '?s';
387
218
  const said = bad ? turnTrouble(turn, status) : (finalAnswerIn(turn.items) ?? '');
388
219
  return `\n${at} == RESULT (${status}${bad ? ' ERROR' : ''} · ${span}) ==\n${said.slice(0, 4000)}`;
@@ -394,21 +225,14 @@ export function traceCodexEvent(method, params) {
394
225
  }
395
226
  }
396
227
 
397
- /** The changes on a fileChange item, always an array. */
398
228
  function changesOf(item) {
399
229
  return Array.isArray(item?.changes) ? item.changes : [];
400
230
  }
401
231
 
402
232
  /**
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.
233
+ * What one file change did, as Write, Delete, Move or Edit, or null when it
234
+ * names no file. The approval card in main/codex-approvals.mjs uses the same
235
+ * function, so the card and the trace use the same word.
412
236
  */
413
237
  export function fileVerbWord(change) {
414
238
  const from = String(change?.path ?? '').trim();
@@ -422,20 +246,15 @@ export function fileVerbWord(change) {
422
246
  return null;
423
247
  }
424
248
 
425
- /** One file change as its bracket and its path. Null when there is no path. */
426
249
  function fileVerb(change) {
427
250
  const word = fileVerbWord(change);
428
251
  if (!word) return null;
429
252
  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
253
  if (word === 'Move') return `[Move] ${from} -> ${String(change.kind.move_path).trim().slice(0, 200)}`;
433
254
  return `[${word}] ${from}`;
434
255
  }
435
256
 
436
- /* ================================= the tail =============================== */
437
-
438
- /** The In Progress tail wants the gist, in the same words Claude Code's does. */
257
+ /** The short In Progress tail, in the same words the Claude Code reader uses. */
439
258
  export function summarizeCodexEvent(method, params) {
440
259
  try {
441
260
  const p = params ?? {};
@@ -471,59 +290,25 @@ export function summarizeCodexEvent(method, params) {
471
290
  }
472
291
  }
473
292
 
474
- /* =============================== the change =============================== */
475
-
476
293
  /**
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.
294
+ * The most frames one run keeps for its code artifact. app-server events are
295
+ * not saved to disk, so this half of the artifact can only be built from
296
+ * memory. Past the cap the artifact is partial, and the disk half read by
297
+ * `mergeChange` (main/code-change.mjs) still wins where the two disagree.
492
298
  */
493
299
  export const CHANGE_FRAME_CAP = 5_000;
494
300
 
495
301
  /**
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).
302
+ * The most characters of diff and text one run keeps, because a frame can be
303
+ * as large as the transport's 16 MiB frame limit, so a frame count alone does
304
+ * not bound memory.
511
305
  */
512
306
  export const CHANGE_BYTE_CAP = 16 * 1024 * 1024;
513
307
 
514
308
  /**
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.
309
+ * Keeps the `fileChange` and `agentMessage` frames the artifact needs, in one
310
+ * list, because `changeFromCodexTurn` pairs each change with the message just
311
+ * before it.
527
312
  */
528
313
  export function rememberCodexChange(session, method, params) {
529
314
  try {
@@ -532,28 +317,17 @@ export function rememberCodexChange(session, method, params) {
532
317
  if (type !== 'fileChange' && type !== 'agentMessage') return;
533
318
  const kept = session.codexChange ?? (session.codexChange = []);
534
319
  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
320
  const size = frameSize(params);
542
321
  const held = session.codexChangeBytes ?? 0;
543
322
  if (held + size > CHANGE_BYTE_CAP) return;
544
323
  session.codexChangeBytes = held + size;
545
324
  kept.push({ method, params });
546
- } catch { /* an artifact is never worth a session */ }
325
+ } catch { /* see the note at the top of this file */ }
547
326
  }
548
327
 
549
328
  /**
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.
329
+ * Roughly how big one frame is: the length of its `text` and each `diff`.
330
+ * Not JSON.stringify, which would cost the very memory the cap guards.
557
331
  */
558
332
  function frameSize(params) {
559
333
  const item = params?.item ?? {};
@@ -563,34 +337,17 @@ function frameSize(params) {
563
337
  }
564
338
 
565
339
  /**
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}`.
340
+ * What the run changed, in exactly the shape main/code-change.mjs produces:
341
+ * `{files:[{path, abs, hunks:[{rows, plus, minus, said?, at?}], plus, minus,
342
+ * at}], roots, plus, minus, editCount, withProse, startedAt, endedAt}`.
574
343
  *
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.
344
+ * `events` are the `{method, params}` frames `rememberCodexChange` kept, in
345
+ * order. `roots` and `since` mean what they mean in code-change.mjs.
587
346
  */
588
347
  export function changeFromCodexTurn(events, { roots = [], since = 0 } = {}) {
589
348
  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.
349
+ // Codex narrates in a separate agentMessage just before it applies a patch,
350
+ // so the sentence beside a hunk is the nearest preceding message.
594
351
  let said = '';
595
352
 
596
353
  for (const event of Array.isArray(events) ? events : []) {
@@ -617,13 +374,7 @@ export function changeFromCodexTurn(events, { roots = [], since = 0 } = {}) {
617
374
  return into.done();
618
375
  }
619
376
 
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
- */
377
+ /** Where the file is after the change. A rename is filed under its new path, as in main/git-change.mjs. */
627
378
  function destinationOf(change) {
628
379
  const to = typeof change?.kind?.move_path === 'string' ? change.kind.move_path.trim() : '';
629
380
  const from = String(change?.path ?? '').trim();
@@ -631,23 +382,16 @@ function destinationOf(change) {
631
382
  }
632
383
 
633
384
  /**
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.
385
+ * One file change as hunks of rows. It checks whether the string is a unified
386
+ * diff rather than trusting the kind, because an add arrives as raw content and
387
+ * the shape of a delete has not been observed.
642
388
  */
643
389
  function hunksFor(change) {
644
390
  const diff = String(change?.diff ?? '');
645
391
  if (!diff) return [];
646
392
  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.
393
+ // `filesFromPatch` needs a file header. A placeholder name is used because
394
+ // that reader trims names, and the real path is never read back from it.
651
395
  return filesFromPatch(`--- a/f\n+++ b/f\n${diff}`)[0]?.hunks ?? [];
652
396
  }
653
397
  const mark = change?.kind?.type === 'delete' ? '-' : '+';
@@ -662,7 +406,7 @@ function splitLines(text) {
662
406
  return out;
663
407
  }
664
408
 
665
- /** One line of what the agent said, the same shape code-change.mjs keeps. */
409
+ /** The last non-empty line of what the agent said, as code-change.mjs keeps it. */
666
410
  function proseFrom(text) {
667
411
  const said = String(text ?? '').trim();
668
412
  if (!said) return '';
@@ -671,18 +415,10 @@ function proseFrom(text) {
671
415
  }
672
416
 
673
417
  /**
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.
418
+ * Each root as given and as its real path. Codex reports resolved paths, such
419
+ * as /private/tmp for /tmp on macOS, which would otherwise match no root. The
420
+ * real path is taken from the deepest ancestor that still exists, because the
421
+ * worktree may already be removed when this runs.
686
422
  */
687
423
  function rootSpellings(roots) {
688
424
  const out = new Set();
@@ -706,11 +442,7 @@ function rootSpellings(roots) {
706
442
  return [...out];
707
443
  }
708
444
 
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
- */
445
+ /** The same tally as the private collector in code-change.mjs, over absolute paths from events. */
714
446
  function collector({ roots = [] } = {}) {
715
447
  const files = new Map();
716
448
  const totals = { plus: 0, minus: 0, editCount: 0, withProse: 0, startedAt: 0, endedAt: 0 };
@@ -721,8 +453,8 @@ function collector({ roots = [] } = {}) {
721
453
  abs = String(abs ?? '').trim();
722
454
  if (!abs || NOT_CODE.test(abs)) return;
723
455
  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.
456
+ // A path inside a root always counts, even under /tmp. Scratch outside
457
+ // every root is dropped.
726
458
  if (rel === null && SCRATCH.test(abs)) return;
727
459
  if (!rows.length) return;
728
460
  const p = rows.filter((r) => r[0] === '+').length;