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/shared/agents.mjs CHANGED
@@ -1,33 +1,12 @@
1
- // PURE. Her Claude Code agents, as rows: which ones Agentbox shows, which one
2
- // reaches her inbox, and what each one says. Measurement lives in
3
- // main/agents.mjs; nothing here touches a process, a socket or a file.
4
- //
5
- // The rules are out here, and not inside the reader, for the reason
6
- // list-rules.ts is: WHAT IS MISSING FROM A LIST HAS NO SYMPTOM. Every inbox
7
- // this could draw looks plausible, and the way it goes wrong is that an agent
8
- // waiting four days is not in it and nothing anywhere says so.
9
- //
10
- // Measured on her machine 2026-08-16, and this is the whole reason the feature
11
- // exists: 16 Claude Code sessions live, 13 of them started by another app, and
12
- // session-71 sitting on `waitingFor: "input needed"` since Wednesday 17:03.
13
- // Nothing on the machine surfaced that.
14
-
15
- /* ------------------------- is that process still it ---------------------- */
16
- // A PID IS NOT AN IDENTITY: a session's state file outlives the process it
17
- // describes and pids get reused, so a record is only believed when a live
18
- // process at that pid started at the same moment.
19
- //
20
- // THE TWO CLOCKS DO NOT AGREE ON THEIR LABEL, and this is the bug this function
21
- // exists for. The state file prints its start time in whatever timezone the
22
- // session was started under; `ps -o lstart` prints local. On her machine
23
- // 2026-08-16 session-71 read "Wed Aug 12 22:57:59 2026" in its state file and
24
- // "Wed Aug 12 15:57:59 2026" in ps — the same instant, seven hours apart on the
25
- // label. Comparing the two strings called all sixteen of her live sessions dead
26
- // and showed her three, which is the whole feature quietly not working.
27
- //
28
- // So they are compared as instants, and a gap that is an exact whole number of
29
- // hours is exactly what a timezone label costs and nothing else: two genuinely
30
- // different processes do not start a whole number of hours apart to the second.
1
+ // Pure rules for the user's own Claude Code agents as rows: which ones Agentbox
2
+ // shows, which reach the inbox, and what each says. main/agents.mjs reads the
3
+ // machine; nothing here touches a process, a socket or a file. The rules live
4
+ // apart from the reader because a row missing from a list has no symptom.
5
+
6
+ // A pid is not an identity, so a record is believed only when the live process
7
+ // at that pid started at the same moment. The state file prints its start time
8
+ // in the session's own timezone and `ps -o lstart` prints local time, so a gap
9
+ // of an exact whole number of hours counts as the same instant.
31
10
  import { NAME } from '../shared/product-name.mjs';
32
11
  const HOUR = 3_600_000;
33
12
 
@@ -39,199 +18,85 @@ export function sameInstant(a, b) {
39
18
  return gap % HOUR === 0 && gap <= 14 * HOUR;
40
19
  }
41
20
 
42
- /* ----------------------------- whose agent is it ------------------------- */
43
- // ZERO'S OWN WORKERS ARE ITS OWN CHILDREN. The supervisor runs `claude -p` from
44
- // the Electron main process, so a worker's ppid is the app's pid and nothing else
45
- // on the machine has that shape. That is the dedupe key, and it needs no name
46
- // matching or guessing.
47
- //
48
- // The sessionId is a second, independent key for the same fact: the supervisor
49
- // already records each worker's sessionId when it spawns it. Either alone is
50
- // enough; both means the fold cannot drift as one of them changes.
51
- //
52
- // This matters because without it Agentbox shouts its own work back at the
53
- // user. In the raw list, Agentbox's own workers, already in the inbox as work
54
- // items, showed up again as a second row about themselves.
21
+ // Agentbox's own workers are already work items, so they must not show again
22
+ // as agent rows. The supervisor spawns them from the Electron main process, so
23
+ // their ppid is the app's pid; the sessionId it records is a second key.
55
24
  export function startedByZero(agent, { pids = new Set(), sessionIds = new Set() } = {}) {
56
25
  if (agent.sessionId && sessionIds.has(agent.sessionId)) return true;
57
26
  return !!agent.ppid && pids.has(agent.ppid);
58
27
  }
59
28
 
60
- /* --------------------------- is it asking anything ----------------------- */
61
- // A ROW THAT ASKS NOTHING DOES NOT GET FILED. A standing rule, and the one
62
- // place this feature could most easily break it: most outside agents on a
63
- // machine are idle at any moment, and filing them all puts a wall of rows in
64
- // the inbox that say "nothing is asked".
65
- //
66
- // So the inbox takes exactly the agents that are waiting on a human. The
67
- // others are still listed, still findable, one keystroke away — they just do
68
- // not interrupt.
29
+ // Only an agent waiting on a person reaches the inbox. Most are idle at any
30
+ // moment; they stay listed but do not interrupt.
69
31
  export function asksSomething(agent) {
70
32
  return agent.status === 'waiting';
71
33
  }
72
34
 
73
- // WHAT A REPLY CAN ACTUALLY FIX, and the distinction is not cosmetic: the two
74
- // waiting states behave completely differently and only one of them is a
75
- // question a message answers.
35
+ // Claude Code has two waiting states. On "input needed" a message written to
36
+ // the session's socket is taken as the next human turn. On "permission prompt"
37
+ // it queues behind the approval box and changes nothing.
76
38
  //
77
- // "input needed" the agent finished its turn and is waiting for a human
78
- // to say something next. A message IS that human turn.
79
- // Proven on this machine 2026-08-16: a message written
80
- // to a live session's socket arrived, was read as the
81
- // human turn, and the agent went back to work.
82
- //
83
- // "permission prompt" the agent is frozen on a box asking to approve a tool
84
- // call. A message queues BEHIND that box. Sending one
85
- // would look like it worked and change nothing, which is
86
- // the failure this codebase cares about most: the system
87
- // swallowing something the user said.
88
- //
89
- // So a reply box appears on the first and never on the second, and the second
90
- // says out loud that it has to be answered where it is running.
91
- //
92
- // `replyReaches` is about the ROW: whether the agent stopped and is waiting on
93
- // her, which is what decides the row's headline and whether it interrupts at
94
- // all. It is NOT the gate on the reply box any more — see `replyIsSwallowed`.
39
+ // This decides the row's headline and whether it interrupts. The reply box is
40
+ // gated by `replyIsSwallowed`, not by this.
95
41
  export function replyReaches(agent) {
96
42
  return asksSomething(agent) && agent.waitingFor === 'input needed';
97
43
  }
98
44
 
99
- // THE ONE AGENT SHE CANNOT WRITE TO, and it is only ever this one.
100
- //
101
- // There was not one. The box was hidden on every agent that was not waiting on
102
- // `input needed`, which is nearly all of them, and the dock said out loud that
103
- // there was nothing there to answer. That sentence was wrong.
104
- //
105
- // MEASURED 2026-08-17, not reasoned: a throwaway session was left sitting at
106
- // its prompt with nothing asked, the exact state of the card in question, and
107
- // the same line `reply` writes was written into its socket. It arrived, the
108
- // session read it and went back to work and answered it. An idle session is
109
- // listening; idle is what a session at its prompt IS.
110
- //
111
- // So the gate is the single case where a message really is swallowed: the
112
- // agent frozen on a permission box, where anything she types queues behind the
113
- // box instead of clearing it. `main/agents.mjs` already refused exactly that
114
- // one and nothing else, so the UI was the whole of the block.
45
+ // The only agent a reply cannot reach is one stopped on a permission prompt.
46
+ // An idle session at its prompt does read a line written to its socket.
47
+ // `reply` in main/agents.mjs refuses the same case.
115
48
  export function replyIsSwallowed(agent) {
116
49
  return agent?.waitingFor === 'permission prompt';
117
50
  }
118
51
 
119
- /* ------------------------- SHE SPOKE, IT IS WORKING ---------------------- */
120
- // WHAT A REPLY DOES TO THE ROW, which until now was nothing at all.
121
- //
122
- // It had not moved because no rule in this app had ever heard of a reply to an
123
- // agent.
124
- //
125
- // So a replied-to agent goes where a replied-to work item goes: In progress,
126
- // which in this app is a PROMISE that something is on it or about to be. The
127
- // promise has to be one that ends, or the row never comes back and she has a
128
- // list that only grows. It ends the moment the session comes to a stop she can
129
- // see, and there are exactly two of those:
130
- //
131
- // idle it finished the turn and is back at its prompt. That is the
132
- // answer arriving, and the answer belongs in her inbox.
133
- // waiting it stopped to ask her something. Same, more so.
134
- //
135
- // MEASURED, not reasoned, on her machine 2026-08-17 (claude 2.1.234): a session
136
- // publishes `status` as one of busy, shell, idle, waiting, and stamps
137
- // `statusUpdatedAt` every time that moves. 15 of her 16 live sessions were
138
- // publishing one. So the comparison is against a real clock the session keeps
139
- // itself, and the row leaves In progress because the SESSION stopped, never
140
- // because a timer here ran out.
141
- //
142
- // The comparison is `>`, against the stamp and not the status alone, because
143
- // the session is still idle for the moment or two between the user pressing
144
- // send and it picking the message up. Reading the status alone would send the
145
- // row straight back to the inbox, which is the original bug wearing a hat.
146
- //
147
- // AND THE ONE SESSION THIS CANNOT PROMISE ANYTHING ABOUT: one that publishes no
148
- // status at all (an older CLI). Agentbox cannot see that one work
149
- // and cannot see it stop, so it is not moved anywhere and the reply is still
150
- // delivered. A promise that could never be kept is worse than no promise.
52
+ // A replied-to agent sits in In progress until the session stops again (idle
53
+ // or waiting) after the reply. Claude Code publishes `status` (busy, shell,
54
+ // idle, waiting) and stamps `statusUpdatedAt` when it changes. The stamp must
55
+ // be newer than the reply, because the session is still idle for a moment
56
+ // before it picks the message up. An older CLI that publishes no status is
57
+ // never moved, since nothing could show it stopping.
151
58
  export function tookHerReply(agent) {
152
59
  const spoke = agent?.repliedAt ?? 0;
153
60
  if (!spoke) return false;
154
- if (!agent?.status) return false; // nothing published: no promise to make
155
- if (putAway(agent)) return false; // she closed the row; closed is closed
61
+ if (!agent?.status) return false;
62
+ if (putAway(agent)) return false;
156
63
  return !stoppedSince(agent, spoke);
157
64
  }
158
65
 
159
- // Has it come to a stop since the moment given? `statusAt` is the session's own
160
- // stamp on its own status, so a session that never publishes one has never been
161
- // seen to stop and this is false — which is why `tookHerReply` refuses that
162
- // case up front rather than trusting this.
66
+ // `statusAt` is the session's own stamp on its status. A session with none is
67
+ // never seen to stop, so `tookHerReply` handles that case first.
163
68
  export function stoppedSince(agent, since) {
164
69
  const at = agent?.statusAt ?? 0;
165
70
  if (at <= since) return false;
166
71
  return agent?.status === 'idle' || agent?.status === 'waiting';
167
72
  }
168
73
 
169
- /* ------------------------------- until later ----------------------------- */
170
- // AN AGENT ROW SHE CAN PUT OFF. A silent no-op is the failure this codebase
171
- // cares about most wearing different clothes — she pressed a key, the app took
172
- // it, and nothing happened.
173
- //
174
- // What deferring one MEANS is only ever about her inbox. Nothing is sent, no
175
- // process is touched and the session keeps waiting exactly as it was: the row
176
- // stops interrupting her until the moment she named, and it is in Scheduled the
177
- // whole time. That is the honest whole of it, and it is why
178
- // this is a moment kept beside the reading rather than anything written into
179
- // somebody else's terminal.
180
-
181
- // WHICH AGENT SHE PUT OFF, and it cannot be the pid. The row's id is
182
- // `agent:<pid>` because that is what the list needs today, but a pid is not an
183
- // identity (see sameInstant above): pids get reused, and the next session to
184
- // land on 68909 would inherit a deferral she set on session-71. The sessionId
185
- // is the session's own name for itself and is what the transcript, the state
186
- // file and the CLI all agree on, so it is the key whenever there is one. When
187
- // there is not, the pid is qualified by the instant that process started, which
188
- // is the same pair `stillTheSame` trusts.
74
+ // Putting off or closing an agent row only changes the inbox. Nothing is sent
75
+ // and the session is not touched.
76
+
77
+ // The key a deferral or close is stored under. Pids get reused, so it is the
78
+ // sessionId when there is one, otherwise the pid plus its start time.
189
79
  export function agentKey(agent) {
190
80
  if (agent?.sessionId) return `session:${agent.sessionId}`;
191
81
  return `pid:${agent?.pid ?? 0}:${agent?.startedAt ?? 0}`;
192
82
  }
193
83
 
194
- // The moment the user said they would deal with it. A past moment is not a deferral:
195
- // once it passes the row is back, which is the same predicate rule work items
196
- // follow. So a missed one comes back late rather than being lost, and nothing
197
- // has to fire at 8am.
84
+ // A predicate, not a timer, like `isDue` for work items: once `runAt` passes
85
+ // the row is back, so a missed moment comes back late rather than being lost.
198
86
  export function putOff(agent, now = Date.now()) {
199
87
  return (agent?.runAt ?? 0) > now;
200
88
  }
201
89
 
202
- // AND DONE STAYS DONE.
203
- //
204
- // This used to be a watermark. `doneThrough` held the agent's own last activity
205
- // at the moment she closed the row, and the row came back the moment that
206
- // session did anything newer. The reasoning was that a session which has spoken
207
- // since she dismissed it is news again. A session she has closed goes on
208
- // working in its own terminal, its activity passed the mark within minutes, and
209
- // the row she had dealt with came back.
210
- //
211
- // So the mark is now read as a flag: closed is closed, for as long as that
212
- // session exists. WHAT THIS COSTS, stated rather than hidden: a session she
213
- // closes while it is working will not return to her inbox when it later stops
214
- // and asks something. That is the same sentence read the other way, and it is
215
- // the one that was chosen.
216
- //
217
- // "UNLESS ASKED OTHERWISE" IS THE UNDO, and it is why this stays a number in
218
- // the file instead of becoming a boolean: `close(key, 0)` clears it, which is
219
- // what Z does after she closes a row (`closeAgentRow` in App.tsx). The value
220
- // still records WHEN she closed it, which is worth keeping and is no longer
221
- // compared against anything.
222
- //
223
- // Nothing is sent and the session is not touched, exactly as with putting one
224
- // off: this is a fact about her inbox.
90
+ // A closed row stays closed for the life of the session, even if it later asks
91
+ // something; it is not compared against later activity. `doneThrough` is a
92
+ // timestamp, not a boolean, so `close(key, 0)` can undo it (`closeAgentRow` in
93
+ // App.tsx).
225
94
  export function putAway(agent) {
226
95
  return (agent?.doneThrough ?? 0) > 0;
227
96
  }
228
97
 
229
- /* -------------------------------- the words ------------------------------ */
230
- // Plain sentences, no jargon, no metaphor, and the first line of the body is
231
- // the one thing the user has to do about it, in bold, one
232
- // sentence, under fifteen words (instructions.md). The row clips at
233
- // SUMMARY_BUDGET = 112 characters on the last full stop inside it, so that
234
- // sentence has to carry the whole point on its own.
98
+ // The words. The row clips the summary at SUMMARY_BUDGET (112 characters) on
99
+ // the last full stop, so the first sentence has to carry the point alone.
235
100
  export function howLong(ms) {
236
101
  if (!Number.isFinite(ms) || ms < 0) return 'a moment';
237
102
  const days = Math.floor(ms / 86400000);
@@ -242,9 +107,6 @@ export function howLong(ms) {
242
107
  return `${mins} minute${mins === 1 ? '' : 's'}`;
243
108
  }
244
109
 
245
- // The folder it is working in, said the way she would say it. Not the path: A
246
- // long "/Users/you/Desktop/dev/<repo>" is four words of noise around the one
247
- // word that identifies it.
248
110
  export function whereItRuns(agent) {
249
111
  const owner = agent.productName;
250
112
  if (owner) return owner;
@@ -253,13 +115,8 @@ export function whereItRuns(agent) {
253
115
  return leaf || 'an unknown folder';
254
116
  }
255
117
 
256
- // Somebody's typing, flattened to one readable line. Markdown markers go
257
- // because the card quotes this inside its own markdown; a fenced code block
258
- // goes because a card is not a diff viewer.
259
- //
260
- // Split out from `oneSentence` when `howItEnded` needed the same cleaning from
261
- // the other end of the message. One copy, so a quote of the user's and a quote of its
262
- // answer can never be tidied by two different rules.
118
+ // Flattens typing to one line. Markdown markers go because the card quotes
119
+ // this inside its own markdown.
263
120
  function readableWords(raw) {
264
121
  return String(raw ?? '')
265
122
  .replace(/```[\s\S]*?```/g, ' ')
@@ -270,8 +127,7 @@ function readableWords(raw) {
270
127
  .trim();
271
128
  }
272
129
 
273
- // One line of somebody's typing, cut to a sentence, FROM THE FRONT. That is
274
- // right for her and wrong for an agent; see `howItEnded`.
130
+ // Cuts from the front, which suits a person's message. See `howItEnded`.
275
131
  function oneSentence(raw, budget = 180) {
276
132
  const text = readableWords(raw);
277
133
  if (!text) return '';
@@ -286,37 +142,21 @@ export function lastWord(agent, budget = 180) {
286
142
  return oneSentence(agent.lastSaid, budget);
287
143
  }
288
144
 
289
- // HOW ITS ANSWER ENDED, WHICH IS WHERE THE ANSWER IS.
290
- //
291
- // `oneSentence` takes the FRONT of a message, and for her that is right: she
292
- // opens with the ask. An agent does the opposite. It opens with what it looked
293
- // at and closes with what it found, so quoting its first sentence on the card
294
- // reliably showed her the preamble and hid the point.
295
- //
296
- // So this reads the same message from the other end. Same cleaning as
297
- // `oneSentence`, then whole sentences off the BACK until the budget is full,
298
- // with a leading ellipsis when there was more in front of them.
145
+ // Whole sentences from the end of a message, because an agent puts what it
146
+ // found last.
299
147
  export function howItEnded(raw, budget = 320) {
300
148
  const text = readableWords(raw);
301
149
  if (!text) return '';
302
150
  if (text.length <= budget) return text;
303
151
  const tail = text.slice(-budget);
304
- // Start at a sentence, so the quote never opens mid-clause. If the last
305
- // sentence is itself longer than the budget there is no boundary to find, and
306
- // a clean word break is the best honest cut.
307
152
  const starts = [tail.indexOf('. '), tail.indexOf('? '), tail.indexOf('! ')].filter((i) => i >= 0);
308
153
  const at = starts.length ? Math.min(...starts) + 2 : -1;
309
154
  const cut = at >= 0 && at < budget - 40 ? tail.slice(at) : tail.slice(tail.indexOf(' ') + 1);
310
155
  return `…${cut.trim()}`;
311
156
  }
312
157
 
313
- // WHEN IT LAST SPOKE, AND WHETHER THAT WAS AN ANSWER TO HER.
314
- //
315
- // The card used to head the quote "It stopped here:" with no time on it at all,
316
- // so a session that had answered every one of her messages and a session that
317
- // had said nothing for two days drew the identical heading. Six cards that
318
- // looked the same because the one fact that told them apart was not on any of
319
- // them.
158
+ // The heading over the agent's last words: when it spoke, and whether that came
159
+ // after the user's latest message.
320
160
  function saidWhen(agent, now) {
321
161
  const at = Number(agent?.saidAt) || 0;
322
162
  if (!at) return '**It stopped here:**';
@@ -329,26 +169,15 @@ function saidWhen(agent, now) {
329
169
  return `**It answered ${when}:**`;
330
170
  }
331
171
 
332
- // THE LAST THING THE USER TYPED INTO IT. Read straight off the session's own
333
- // `last-prompt` line, so it is the user's turn and never a tool result wearing a user
334
- // message's clothes.
172
+ // The last thing the user typed, from the session's own `last-prompt` line,
173
+ // which never holds a tool result.
335
174
  export function lastAsk(agent, budget = 180) {
336
175
  return oneSentence(agent.lastAsked, budget);
337
176
  }
338
177
 
339
- /* ------------------------------- what it IS ------------------------------ */
340
- // A card whose loudest word is `session-71` says nothing. A session name is a
341
- // handle, not a memory.
342
- //
343
- // Every Claude Code session writes a one-line title of ITSELF into its own
344
- // transcript and keeps it current. Every outside session measured had one, and
345
- // it is usually the very sentence a person would use to recall the work,
346
- // sitting in a file Agentbox was already reading and throwing away.
347
- //
348
- // So the title of the row is what the work IS, with the session's name in front
349
- // of it because two sessions can run in the same folder and the name is how
350
- // they are told apart. When a session has no title of its own the row says
351
- // what it used to say, which is better than a blank.
178
+ // Claude Code keeps a one-line title of each session in its transcript; that
179
+ // is `about`. The row title puts the session's name in front of it, because
180
+ // two sessions can run in the same folder.
352
181
  export function whatItIs(agent) {
353
182
  return String(agent?.about ?? '').replace(/\s+/g, ' ').trim();
354
183
  }
@@ -358,62 +187,25 @@ export function agentTitle(agent, now = Date.now()) {
358
187
  if (about) return `${agent.name}: ${about}`;
359
188
  if (replyReaches(agent)) return `${agent.name} is waiting for you`;
360
189
  if (asksSomething(agent)) return `${agent.name} needs you where it is running`;
361
- // NOT "QUIET" ONCE THE USER HAS SPOKEN TO IT. Without this the row just
362
- // replied to sat in In progress under a heading saying it had been quiet for
363
- // three days, which is the same sentence it wore before anything was said.
364
190
  if (tookHerReply(agent)) return `${agent.name} is working on what you told it`;
365
191
  return `${agent.name} has been quiet for ${howLong(now - (agent.lastActiveAt ?? agent.startedAt ?? now))}`;
366
192
  }
367
193
 
368
- /* ------------------------------ RECALLING IT ----------------------------- */
369
- // A card that carries only the title and the last thing typed is not enough to
370
- // recall a session.
371
- //
372
- // WHAT WAS MISSING WAS THE BEGINNING, and it is the one thing on a transcript
373
- // that never changes. In every live session measured, the FIRST message was
374
- // the ask, and no card had ever shown it. `last-prompt` is the LAST thing the
375
- // user typed, and days into a session that is usually a short follow-up that
376
- // is perfectly true and no help whatsoever in remembering what the thing is for.
377
- //
378
- // The human side of a conversation is also small enough to just print: a
379
- // handful of turns per session, a sentence or two each. The megabytes in a
380
- // transcript are the agent's half. So the recall is the user's own words, read
381
- // off the file, and no model sees any of it.
382
- //
383
- // ONE SHAPE, NOT THREE, AND IT IS NOT CONFIGURABLE.
384
- //
385
- // So the card does the same thing on every session: the message the user
386
- // started it with, up to three later messages of theirs, where it stopped, and
387
- // where it is working. `ask` and `trail` are gone, and there is deliberately no
388
- // setting for this: deciding per session which of them a card deserves is a
389
- // judgement that should not be made, whether by a model or by a knob.
390
-
391
- // The messages the user typed, oldest first, with the newest ones kept whole and the
392
- // opening always present.
393
- //
394
- // AN EMPTY LIST IS NOT A FALLBACK TO `lastAsked`, and the difference is the
395
- // whole point of this change rather than a nicety. A session whose transcript
396
- // could not be read has exactly one thing the user typed on record, and it is
397
- // the LAST one. So the caller is told which of the two it has. WHAT THE USER
398
- // SAID FROM AGENTBOX IS ALSO WHAT THE USER SAID.
399
- //
400
- // So the card went on saying only what the session was started with.
401
- //
402
- // `spoke` is the other half, written by main/agent-schedule.mjs at the moment
403
- // of delivery. It is a RECORD, not a guess: Agentbox does not try to work out
404
- // which wrapped peer message in a transcript was the user's, because another of
405
- // their sessions pinging this one is a real peer and drawing that as the user's
406
- // words would put a sentence they never typed on the one card built to be trusted.
194
+ // Recalling a session. The card shows the user's own words read off the
195
+ // transcript (no model sees them): the opening ask, up to three later messages,
196
+ // where it stopped and where it works. The same shape on every session, with no
197
+ // setting.
198
+
199
+ // The user's messages, opening first, then the rest by time. `asked` comes from
200
+ // the transcript. `spoke` is what main/agent-schedule.mjs recorded as delivered
201
+ // from Agentbox, because a peer message in the transcript cannot be told apart
202
+ // from the user's. Deduped on text in case the session also recorded it.
407
203
  //
408
- // Merged by time, so the thread reads forwards however it arrived, and deduped
409
- // on the text because a session that DOES record the user's injected turn as
410
- // its own (a future Claude Code, or one typed into directly) must not show it
411
- // twice.
204
+ // An unreadable transcript gives an empty list, never a fallback to
205
+ // `lastAsked`, so the caller knows it only has the last message.
412
206
  export function herWords(agent) {
413
207
  const asked = (agent?.asked ?? []).filter((t) => t && t.text);
414
208
  const spoke = (agent?.spoke ?? []).filter((t) => t && t.text);
415
- // AN EMPTY LIST STAYS EMPTY, and that is not laziness. Nothing here is
416
- // allowed to invent an opening; see the note above this function.
417
209
  if (!spoke.length || !asked.length) return asked;
418
210
  const seen = new Set(asked.map((t) => t.text.replace(/\s+/g, ' ').trim()));
419
211
  const merged = [...asked];
@@ -423,21 +215,15 @@ export function herWords(agent) {
423
215
  seen.add(flat);
424
216
  merged.push({ at: t.at, text: flat });
425
217
  }
426
- // The opening keeps its place even if a clock skewed: it is the ask, and the
427
- // card labels it as the thing she started with.
218
+ // The opening stays first even if a clock skewed, because the card labels it
219
+ // as the start.
428
220
  const [first, ...rest] = merged;
429
221
  rest.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
430
222
  return first ? [first, ...rest] : [];
431
223
  }
432
224
 
433
- // WHICH OF THE USER'S MESSAGES ACTUALLY RECALL ANYTHING, and the cheapest
434
- // honest answer is how long they are. Much of what a person types while an
435
- // agent works is driving it, not describing it: short nudges to restart,
436
- // carry on or approve are almost always under sixty characters, while a turn
437
- // that describes the work typically runs well past a hundred.
438
- //
439
- // So the long ones are preferred, and the short ones are still there when
440
- // there is nothing longer, because a card that drops to silence is worse.
225
+ // Short messages are usually nudges ("carry on", "approve"), so longer ones
226
+ // are preferred when there are any.
441
227
  const RECALLS_SOMETHING = 60;
442
228
 
443
229
  function worthRecalling(turns) {
@@ -445,8 +231,6 @@ function worthRecalling(turns) {
445
231
  return meaty.length ? meaty : turns;
446
232
  }
447
233
 
448
- // How long ago, said the way she would say it, and never a clock time: a
449
- // weekday name five days later is a small puzzle and "5 days ago" is not.
450
234
  function ago(at, now) {
451
235
  if (!at) return '';
452
236
  const gap = now - at;
@@ -455,8 +239,6 @@ function ago(at, now) {
455
239
  return `${howLong(gap)} ago`;
456
240
  }
457
241
 
458
- // The rest of the context, in the user's sentences: what they asked for, where the
459
- // thread got to, what it last said back, and what it has had its hands on.
460
242
  function context(agent, now = Date.now()) {
461
243
  const words = herWords(agent);
462
244
  const said = howItEnded(agent.lastSaid);
@@ -468,14 +250,10 @@ function context(agent, now = Date.now()) {
468
250
  const when = ago(opening.at, now);
469
251
  lines.push(`**You started it${when ? ` ${when}` : ''} with:**\n\n“${oneSentence(opening.text, 420)}”`);
470
252
  } else {
471
- // No history, so the only thing on record is the LAST thing the user typed, and
472
- // it gets the label that is true of it.
473
253
  const last = lastAsk(agent);
474
254
  if (last) lines.push(`**The last thing you told it:**\n\n“${last}”`);
475
255
  }
476
256
 
477
- // Everything else the user typed, oldest first, so the thread reads forwards.
478
- // Three of them; more than that is the transcript rather than a card.
479
257
  if (rest.length) {
480
258
  const recent = worthRecalling(rest).slice(-3);
481
259
  const bullets = recent.map((t) => {
@@ -486,9 +264,8 @@ function context(agent, now = Date.now()) {
486
264
  lines.push(`**Then${over} you said:**\n\n${bullets.join('\n')}`);
487
265
  }
488
266
 
489
- // `howItEnded` has already cut this to a quotable length from the right end,
490
- // so it is not run through `oneSentence` again: doing that took the tail it
491
- // had just chosen and handed back the front of it.
267
+ // Already cut from the end by `howItEnded`; `oneSentence` would cut it again
268
+ // from the front.
492
269
  if (said) lines.push(`${saidWhen(agent, now)}\n\n“${said}”`);
493
270
 
494
271
  const files = (agent.touched ?? []).slice(-3);
@@ -508,13 +285,7 @@ export function agentRow(agent, now = Date.now()) {
508
285
  const title = agentTitle(agent, now);
509
286
  const rest = context(agent, now);
510
287
 
511
- // SHE HAS SPOKEN TO IT AND IT IS WORKING. First, because every branch below
512
- // describes a session that has stopped, and this one has not: the row that
513
- // sent her back here was in her inbox under "It is asking for nothing and has
514
- // done nothing for 1 minute" moments after she told it to do something. This
515
- // row asks her for nothing on purpose — it is in In progress, which is where
516
- // the app puts what it is getting on with — so the opening line is the news
517
- // rather than a question.
288
+ // First, because every branch below describes a session that has stopped.
518
289
  if (tookHerReply(agent)) {
519
290
  return {
520
291
  title,
@@ -533,86 +304,47 @@ export function agentRow(agent, now = Date.now()) {
533
304
  body: `**Answer ${agent.name} in its own window, not here.**\n\nIt is asking to approve something, and a message from ${NAME} would wait behind that box instead of clearing it.${rest}`,
534
305
  };
535
306
  }
536
- // A QUIET AGENT IS IN HER INBOX NOW, so it has to say what she is meant to do
537
- // with it, which is the one thing it never said before: close it. Closing one
538
- // is about her inbox and nothing else — the session is not touched — and the
539
- // body says exactly that, because a row that looks like it might kill
540
- // somebody's terminal is a row she cannot press at speed.
307
+ // The body says the session keeps running, so closing the row does not look
308
+ // like it might kill a terminal.
541
309
  return {
542
310
  title,
543
311
  body: `**Close ${agent.name} if you no longer need it here.**\n\nIt is asking for nothing and has done nothing for ${waited}. Closing it takes it out of your inbox and leaves the session running.${rest}`,
544
312
  };
545
313
  }
546
314
 
547
- /* -------------------------------- ONE LIST ------------------------------- */
548
- // THERE IS NO SECOND TAB.So `Your agents` is gone and every outside agent is
549
- // either in her inbox or nowhere, which is the whole of "merge the inbox into
550
- // one".
551
- //
552
- // WHICH OF THEM IS IN IT IS HER SETTING, because she named both sides of it
553
- // herself in the same message:
315
+ // There is no separate agents tab: an outside agent is in the inbox or
316
+ // nowhere. Which ones is a setting.
554
317
  export const AGENT_MODES = ['all', 'waiting', 'off'];
555
318
 
556
319
  export function reachesInbox(agent, now = Date.now(), mode = 'all') {
557
320
  if (agent.startedByZero) return false;
558
321
  if (mode === 'off') return false;
559
- // ONE ROW, ONE LIST. A session she just spoke to is in In progress until it
560
- // stops for her again; showing it here at the same time is the "one row, two
561
- // tabs" failure this codebase has already had once, and it would still be
562
- // sitting under a headline saying it asks for nothing.
322
+ // A replied-to session is in In progress instead (`progressAfterReply`).
563
323
  if (tookHerReply(agent)) return false;
564
- // A SESSION THAT CANNOT SAY WHAT IT IS NEVER FILES A ROW, WHATEVER THE
565
- // MODE.That sentence is what `agentTitle` falls back to when a session has
566
- // published no title of its own, and the body under it asks her to close a
567
- // row that asked her nothing, which instructions.md says should never have
568
- // been filed at all. It cannot be fixed by closing one either: a throwaway
569
- // session gets a fresh pid every time, so E is keyed to an id that never
570
- // comes back and the next one arrives just as loud.
571
- //
572
- // The sweep `all` was built for is untouched. A forgotten session that has a
573
- // title still carries it here, which is the one she can recognise and decide
574
- // about; this drops only the ones with nothing to show her.
324
+ // An idle session with no title has nothing to show, and closing it does not
325
+ // help: a throwaway session gets a fresh pid each time.
575
326
  if (!asksSomething(agent) && !whatItIs(agent)) return false;
576
327
  if (mode === 'waiting' && !asksSomething(agent)) return false;
577
328
  return !putOff(agent, now) && !putAway(agent);
578
329
  }
579
330
 
580
- // AND THE LIST IT IS IN INSTEAD. Same shape as `belongsInProgress` for work
581
- // items and the same promise: something is on this right now. The deferral
582
- // argument is not taken because a reply cancels a deferral (`replyClearsSchedule`
583
- // does the same for work items) — she spoke to it, so it is not put off any more.
584
- //
585
- // THIS IS NOT `reachesProgress`, WHICH WAS REMOVED, and the name is different
586
- // on purpose. It stays dead: a working session the user has not spoken to is in
587
- // the inbox, exactly as they left it. What moves a row here is THE USER, and
588
- // only the user: the reply they wrote.
331
+ // The In progress counterpart of `belongsInProgress` for work items. Only the
332
+ // user's reply moves an agent here; a working session nobody spoke to stays in
333
+ // the inbox. A reply cancels a deferral, as `replyClearsSchedule` does for work
334
+ // items, so `putOff` is not checked.
589
335
  export function progressAfterReply(agent, now = Date.now(), mode = 'all') {
590
336
  if (agent.startedByZero) return false;
591
337
  if (mode === 'off') return false;
592
338
  return tookHerReply(agent);
593
339
  }
594
340
 
595
- // Everything Agentbox will admit exists: every live session except its own
596
- // workers, which are already in her inbox as work items.
597
341
  export function listed(agent) {
598
342
  return !agent.startedByZero;
599
343
  }
600
344
 
601
- /* --------------------- ACTIVE AGENTS, IN THE SIDEBAR --------------------- */
602
- // The design: one line per session in its own words,
603
- // the clock on the right, and no dot, mark, colour, wash, edge or line of any
604
- // kind.
605
- //
606
- // THE CUT IS A DAY, AND IT IS THE ONLY FILTER.The premise is worth correcting
607
- // where the rule lives, because it decides what the rule can be: nothing
608
- // FINISHED is ever on this list and never has been. `main/agents.mjs` drops any
609
- // record whose process is gone, so a session that finishes exits and disappears
610
- // by itself. Measured: most live sessions were terminals sitting at a prompt
611
- // for between two and seven days.
612
- // So length comes from sessions that are alive and forgotten, and a day is
613
- // where the app's own clock (`format.ts`) stops saying hours and starts saying
614
- // `1d`. The panel therefore holds exactly what the clock beside each row can
615
- // say in hours, which is the same sentence read twice.
345
+ // The sidebar shows sessions active in the last day, the point where the
346
+ // app's clock (`format.ts`) switches from hours to `1d`. Finished sessions
347
+ // never appear, because main/agents.mjs drops records whose process is gone.
616
348
  export const RAIL_CUT_MS = 24 * 60 * 60 * 1000;
617
349
 
618
350
  export function onTheRail(agent, now = Date.now()) {
@@ -621,45 +353,20 @@ export function onTheRail(agent, now = Date.now()) {
621
353
  return moved > 0 && now - moved <= RAIL_CUT_MS;
622
354
  }
623
355
 
624
- // WHAT ONE ROW SAYS, and it is the session's own sentence rather than ours.
625
- // `about` is the one-line title every Claude Code session keeps current for
626
- // itself; `lastSaid` is the fallback for a session Agentbox started, which has no
627
- // title. Neither is cut here: the row is one line with `text-overflow` on it,
628
- // so the width she is actually looking at does the cutting, once.
356
+ // Not cut here; the row's CSS `text-overflow` cuts it to the width shown.
629
357
  export function railLine(agent) {
630
358
  return whatItIs(agent) || lastWord(agent) || 'Has not said anything yet';
631
359
  }
632
360
 
633
- // MOST RECENTLY ACTIVE FIRST, which is the order every other list in this app
634
- // is in, and the order the day headings above the rows assume. Sorting the
635
- // stopped one to the top instead read well in a sentence and drew badly: the
636
- // list groups by day as it goes, so a waiting agent from Wednesday above a
637
- // row from July put "Last 7 days" on screen twice, once at the top and once
638
- // eight rows down (measured in the built app, 2026-08-16).
639
- //
640
- // The stopped one does not need pinning here. It is scored in the inbox by the
641
- // same rule everything else there is scored by, which puts an agent asking
642
- // something above one asking nothing without this having to know about it.
361
+ // Pure recency, because the list draws day headings as it goes and any other
362
+ // order repeats a heading. Waiting agents are ranked in the inbox, not here.
643
363
  export function byRecency(a, b) {
644
364
  return (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0);
645
365
  }
646
366
 
647
- /* --------------------------- THE CONVERSATION ---------------------------- */
648
- // THE CARD IS THE RECALL AND THIS IS THE REST OF IT. The card is drawn for the
649
- // user whether they asked or not, so it is four sentences; this is only ever
650
- // drawn because they pressed something, so it can be the conversation.
651
- //
652
- // STILL NO MODEL, AND NOW NO COST AT ALL UNTIL IT IS PRESSED. Measured: the
653
- // readable conversation is 0.6% of a transcript's bytes, and the worst single
654
- // file measured (160MB) carries 3,800 readable turns, 2.6MB of text, 729ms to
655
- // pull out. That last number is why the read is streamed and never happens on
656
- // a draw.
657
- //
658
- // WHY IT IS A WINDOW AND NOT THE WHOLE FILE. 3,800 turns is not a conversation
659
- // anyone can get up to speed on, it is a scroll bar. So it is the opening,
660
- // which is the ask in every session measured, and the end, which is where it got to,
661
- // and one plain line saying how many are in between rather than pretending
662
- // there are none.
367
+ // The full conversation, read only when the user opens it, since a large
368
+ // transcript takes most of a second to read. It shows the opening turns and the
369
+ // latest ones, with a line counting what is in between.
663
370
  export const TRAIL_OPENING = 3;
664
371
  export const TRAIL_TURNS = 40;
665
372
 
@@ -671,116 +378,56 @@ export function conversationWindow(turns, { opening = TRAIL_OPENING, keep = TRAI
671
378
  return { turns: [...head, ...tail], omitted: all.length - head.length - tail.length };
672
379
  }
673
380
 
674
- // The one line that stands where the missing middle is. It says the number
675
- // because "some messages" is the kind of sentence that makes her wonder what
676
- // else the app is rounding off.
677
- //
678
- // And it is not rare. Rebuilding every row's thread the way the pane draws it,
679
- // nearly half the rows with a conversation hide part of it, many of them
680
- // hiding messages the user wrote, and the line was a plain div with nothing to
681
- // press. "Not shown" was true and it was also the end of the road, so a thread
682
- // that had eaten a follow-up simply said so and stopped.
683
- //
684
- // So the words say what pressing does. The window itself is untouched: it still
685
- // opens on the end of a long conversation, because an 18,100px page is the
686
- // thing it was built for. What changed is that the middle can be asked for.
381
+ // The line that stands for the hidden middle. It gives the count, and pressing
382
+ // it shows those messages.
687
383
  export function conversationGap(omitted) {
688
384
  if (!omitted) return '';
689
385
  return `Show the ${omitted} message${omitted === 1 ? '' : 's'} in between`;
690
386
  }
691
387
 
692
- /* ------------------------------- THE WORK ---------------------------------- */
693
- // So the item is the conversation, and each thing the agent ran sits on its own
694
- // quiet line between the messages, in the order it happened. This half of the
695
- // file decides what one of those lines SAYS. What it looks like is the frosted
696
- // treatment chosen in round two and lives in the stylesheet.
697
- //
698
- // WHY IT IS BOUNDED THREE WAYS, all measured over the 40 largest transcripts
699
- // available:
700
- //
701
- // * one session held 6,070 tool calls and 8MB of output. So output is capped
702
- // per line (WORK_CHARS) rather than sent whole.
703
- // * the longest unbroken run of tool calls between two messages was 111, and
704
- // the median session's longest run was 25. So a run is capped (WORK_RUN)
705
- // and says how many it is not showing, because 111 quiet lines standing
706
- // between a question and its answer is the original problem wearing a
707
- // different hat.
708
- // * a windowed thread of 40 messages could still carry thousands of work
709
- // lines, so the whole thread is capped too (WORK_TOTAL), oldest first.
710
- //
711
- // Every cap says its own number out loud where it bites. Nothing here rounds
712
- // off silently: "some output" is the kind of sentence that makes her wonder
713
- // what else the app is not telling her.
388
+ // Each tool call the agent ran is a line between the messages. Caps: output
389
+ // per line (WORK_CHARS), one unbroken run of calls (WORK_RUN), and the whole
390
+ // thread (WORK_TOTAL, oldest dropped first). Every cap shows how much it hid.
714
391
  export const WORK_CHARS = 2000;
715
392
  export const WORK_RUN = 20;
716
393
  export const WORK_TOTAL = 200;
717
394
 
718
- // WHAT A TOOL CALL SAYS lives in its own file now (`shared/work-lines.mjs`),
719
- // because a report on the imported threads turned one rule into
720
- // three: the verb, the subject and the fold. It is re-exported here so that
721
- // nothing that already reads this file has to learn a second import.
395
+ // Re-exported so existing importers of this file keep working.
722
396
  export {
723
397
  workVerb, workSubject, fullSubject, shortPath, plainCommand, commandWork,
724
398
  groupWork, runSummary, runFailures, RUN_MIN, isBookkeeping,
725
399
  changedFile, fileInChange, CHANGES_A_FILE,
726
400
  } from './work-lines.mjs';
727
401
 
728
- // The line that stands where a capped run of work is. Same rule as the message
729
- // gap above: say the number.
730
402
  export function runOverflow(more) {
731
403
  if (!more) return '';
732
404
  return `and ${more} more thing${more === 1 ? '' : 's'} it ran, not shown`;
733
405
  }
734
406
 
735
- // The line at the foot of an output that was longer than we keep.
736
407
  export function outputCut(shownLines, allLines) {
737
408
  if (!allLines || allLines <= shownLines) return '';
738
409
  return `${allLines - shownLines} more line${allLines - shownLines === 1 ? '' : 's'} came back, not kept`;
739
410
  }
740
411
 
741
- // THE WINDOW, MESSAGE-AWARE. `conversationWindow` above counts turns, and a
742
- // thread with work in it must not let a run of tool calls push her own messages
743
- // out of the window: the whole point of the shape is that her question and its
744
- // answer sit next to each other. So the window is computed over the MESSAGES,
745
- // exactly as before, and the work that sits between two kept messages comes
746
- // with them. `omitted` still counts messages, because that is what the line
747
- // under the gap says.
748
- //
749
- // `whole` IS WHAT THE GAP LINE PRESSES. Every message stands, and nothing else
750
- // changes: the work is still capped by `capRuns` below, because 530 tool calls
751
- // is not a conversation and what is wanted is the follow-ups, not the output
752
- // under them. Opening it is the user's decision and it lasts until they leave the row,
753
- // so no thread is ever born at this size.
412
+ // Like `conversationWindow`, but over a thread with work lines in it. The
413
+ // window counts messages only, so tool calls cannot push messages out, and work
414
+ // between two kept messages comes with them. `whole` is what pressing the gap
415
+ // line asks for: every message, with work still capped by `capRuns`.
754
416
  export function threadWindow(events, { opening = TRAIL_OPENING, keep = TRAIL_TURNS, whole = false } = {}) {
755
417
  const all = (events ?? []).filter((e) => e && (e.kind === 'work' ? true : !!e.text));
756
418
  const saidAt = [];
757
419
  all.forEach((e, i) => { if (e.kind !== 'work') saidAt.push(i); });
758
420
  if (whole || saidAt.length <= keep) return { events: capRuns(all), omitted: 0 };
759
421
 
760
- // The first index that goes, and the first index that comes back. Everything
761
- // between them is dropped whole, work and messages together, so the thread
762
- // never shows output belonging to a message that is not on screen.
422
+ // Everything between these is dropped, work and messages together, so no
423
+ // output shows for a message that is not on screen.
763
424
  const from = saidAt[opening];
764
425
  const to = saidAt[saidAt.length - (keep - opening)];
765
426
 
766
- // THE CONVERSATION RESUMES WITH A NAME ON IT.
767
- //
768
- // A `same` block is one agent STILL TALKING: it wears no name and no time
769
- // because the message above it is its own first half. Across the cut that is
770
- // a lie. Whatever it was continuing is in the missing middle, so it draws as
771
- // a nameless paragraph hanging off whichever message the window happened to
772
- // end the opening with, which is a different run from a different hour.
773
- //
774
- // Measured on her store, 2026-08-28: of the 123 rows with a gap in them, 93
775
- // resumed on a nameless block.
776
- //
777
- // `resumed` RATHER THAN CLEARING `same`, and the difference matters. `same`
778
- // is what `saidCount` folds on, so a block that stops being a continuation
779
- // becomes a second message in the header's count while still being one
780
- // message in the row. Measured before this note was written: clearing it put
781
- // `omitted` at -1 on 15 of her rows, which is the gap line quoting her a
782
- // number that cannot be true. So the fold is left exactly as it was and only
783
- // the DRAWING changes: this block gets its name and its time back.
427
+ // A `same` block continues the one above and draws with no name or time. The
428
+ // first block after the cut continues something hidden, so it is marked
429
+ // `resumed` to draw its name again. Clearing `same` instead would change
430
+ // `saidCount` and make `omitted` disagree with it.
784
431
  const tail = all.slice(to);
785
432
  if (tail.length && tail[0].kind !== 'work' && tail[0].same) tail[0] = { ...tail[0], resumed: true };
786
433
  const kept = [...all.slice(0, from), ...tail];
@@ -788,23 +435,10 @@ export function threadWindow(events, { opening = TRAIL_OPENING, keep = TRAIL_TUR
788
435
  }
789
436
 
790
437
  /**
791
- * WHERE THE GAP LINE STANDS, in the windowed thread `threadWindow` returned.
792
- *
793
- * IT LIVES HERE BECAUSE THE CUT LIVES HERE. The renderer used to work this out
794
- * for itself with a second, different count: it walked the messages and SKIPPED
795
- * `same` continuations, while the cut above keeps the first `opening` BLOCKS
796
- * with continuations counted. The two agree only when no run's second sentence
797
- * lands in the opening, and the ordinary shape of a thread is the ask, a run's
798
- * first line, that run's second line.
799
- *
800
- * Measured over her own store, 2026-08-28, rebuilding all 267 rows the way the
801
- * pane draws them: 123 have a gap, and on 56 of those the line was printed away
802
- * from the seam, by as much as 30 messages. It sat under messages she could read
803
- * saying some were missing, and where they had really gone there was nothing.
804
- *
805
- * Returns the index of the LAST event the opening kept, so work belonging to the
806
- * opening stays above the line rather than below it, and -1 when nothing is
807
- * missing.
438
+ * Where the gap line stands in a thread from `threadWindow`: the index of the
439
+ * last event the opening kept, or -1 when nothing is missing. It counts blocks,
440
+ * `same` continuations included, exactly as `threadWindow` cuts, which is why
441
+ * the renderer must not work this out itself.
808
442
  */
809
443
  export function gapIndex(events, omitted, { opening = TRAIL_OPENING } = {}) {
810
444
  if (!omitted) return -1;
@@ -814,13 +448,11 @@ export function gapIndex(events, omitted, { opening = TRAIL_OPENING } = {}) {
814
448
  blocks += 1;
815
449
  if (blocks === opening + 1) return i - 1;
816
450
  }
817
- // Every block on the screen belongs to the opening: the seam is the end of it.
818
451
  return (events?.length ?? 0) - 1;
819
452
  }
820
453
 
821
- // Both work caps, applied in one pass over the windowed thread. A run is cut to
822
- // WORK_RUN and told how many it lost; the thread as a whole is cut to
823
- // WORK_TOTAL, oldest work first, because the newest is the part she came for.
454
+ // A run past WORK_RUN records how many it dropped in `more`. Past WORK_TOTAL
455
+ // the oldest work lines go and every message stays.
824
456
  export function capRuns(events, { run = WORK_RUN, total = WORK_TOTAL } = {}) {
825
457
  const out = [];
826
458
  let inRun = 0;
@@ -840,7 +472,6 @@ export function capRuns(events, { run = WORK_RUN, total = WORK_TOTAL } = {}) {
840
472
 
841
473
  const work = out.filter((e) => e.kind === 'work').length;
842
474
  if (work <= total) return out;
843
- // Drop the oldest work lines and leave every message standing.
844
475
  let over = work - total;
845
476
  const trimmed = [];
846
477
  for (const e of out) {
@@ -850,19 +481,9 @@ export function capRuns(events, { run = WORK_RUN, total = WORK_TOTAL } = {}) {
850
481
  return trimmed;
851
482
  }
852
483
 
853
- // HOW MANY MESSAGES THAT IS, which is not how many blocks are on the screen.
854
- // Shape B draws a reply that stopped twice for a tool as three blocks in the
855
- // order they happened, and that is right: it is what the session actually did.
856
- // But a person counting would say the agent sent ONE message, so the header
857
- // that reads "40 messages, oldest first" counts messages and not blocks.
858
- // Getting this wrong would put "128 messages" over a conversation she
859
- // remembers having six times.
860
- //
861
- // The reader marks every block that is a continuation of the one above it, and
862
- // it is the only thing that knows: a block is a continuation when nothing but
863
- // work stands between it and the same side's last block, and when nothing
864
- // ARRIVED in the gap. A message that arrived ends the answer above it even
865
- // when it is never drawn, which is the whole of's first defect.
484
+ // Messages, not blocks: a reply split by tool calls draws as several blocks
485
+ // but counts once. The transcript reader sets `same` on each continuation
486
+ // block, and only it can tell.
866
487
  export function saidCount(events) {
867
488
  let n = 0;
868
489
  for (const e of events ?? []) {
@@ -872,31 +493,14 @@ export function saidCount(events) {
872
493
  return n;
873
494
  }
874
495
 
875
- // TOOL-CALL SCAFFOLDING THAT LEAKED INTO THE PROSE.
876
- //
877
- // A provider writing a tool call sometimes emits its closing tags as ordinary
878
- // text, and the field it was writing then holds the answer with the machinery
879
- // of the call still stuck to the end. On w-b108b1d596 the result that reached
880
- // the pane ended "7,369 tests both times.</result>\n</invoke>" and was shown as
881
- // is. Measured: dozens of field writes across dozens of rows carried it.
882
- //
883
- // IT ALSO BREAKS THE FOLD THAT SAYS ONE THING ONCE. That run noticed the leak
884
- // and wrote its result again, clean. The fold keeps the FULLER of two copies of
885
- // one answer, and the junk made the broken copy the fuller one, so the leak was
886
- // what stayed on the screen and the good rewrite was the copy thrown away. That
887
- // is what happened on that row, and it is why the cut happens before anything
888
- // compares two messages.
889
- //
890
- // The cut runs from the first tag to the end, because nothing after one is
891
- // prose: it is the rest of a call that was never meant to be said out loud. On
892
- // that row it was a `note` parameter, which the next write then sent properly.
496
+ // A model writing a tool call sometimes emits the call's closing tags as plain
497
+ // text inside a field. Everything from the first tag on is cut. This must run
498
+ // before any comparison of two messages, because the fold keeps the fuller
499
+ // copy and the leaked junk would make the broken copy win.
893
500
  const LEAKED = /<\/(?:antml:)?(?:result|invoke|parameter|function_calls)>|<(?:antml:)?parameter\s+name=/;
894
501
 
895
- // A TAG INSIDE CODE WAS TYPED ON PURPOSE AND IS NOT A LEAK. The row that asked
896
- // for this is the proof: the answer explaining the bug to her quotes the tags in
897
- // backticks, mid-paragraph, and a cut at the first one anywhere would have eaten
898
- // the rest of her answer to make the example go away. So the spans in backticks,
899
- // inline and fenced, are stepped over and only prose is cut.
502
+ // A tag inside backticks, inline or fenced, was typed on purpose and is not a
503
+ // leak, so code spans are skipped.
900
504
  function firstLeak(text) {
901
505
  const code = [];
902
506
  let i = 0;