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
@@ -0,0 +1,99 @@
1
+ // The two files behind teammates, per account: the teammates themselves and
2
+ // the one team chat. Append-only JSON lines, read whole and folded, the way
3
+ // repeats.jsonl is. The app and the store tools both write here, so this is
4
+ // the only code that knows where they live.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import crypto from 'node:crypto';
8
+ import { foldTeammates, foldChat, foldPlaybooks } from '../../shared/teammates.mjs';
9
+
10
+ const MAX_READ = 4 * 1024 * 1024;
11
+ const MAX_LINE = 64 * 1024;
12
+
13
+ export function teamDir(storeRoot, accountId) {
14
+ if (!storeRoot || !accountId) throw new Error('teammates need a store root and an account');
15
+ return path.join(storeRoot, 'teammates', String(accountId).replace(/[^A-Za-z0-9_-]/g, '-'));
16
+ }
17
+
18
+ export const teammatesFile = (dir) => path.join(dir, 'teammates.jsonl');
19
+ export const chatFile = (dir) => path.join(dir, 'chat.jsonl');
20
+ export const playbooksFile = (dir) => path.join(dir, 'playbooks.jsonl');
21
+
22
+ // The screen asks for these on every refresh; a file only read again when it
23
+ // has changed keeps a long chat from costing a parse each time.
24
+ const READ = new Map();
25
+
26
+ function readLines(file) {
27
+ let text;
28
+ let key;
29
+ try {
30
+ const st = fs.statSync(file);
31
+ const size = st.size;
32
+ key = `${size}:${st.mtimeMs}`;
33
+ const had = READ.get(file);
34
+ if (had?.key === key) return had.lines.slice();
35
+ if (size > MAX_READ) {
36
+ const fd = fs.openSync(file, 'r');
37
+ try {
38
+ const buf = Buffer.allocUnsafe(MAX_READ);
39
+ const n = fs.readSync(fd, buf, 0, MAX_READ, size - MAX_READ);
40
+ text = buf.toString('utf8', 0, n);
41
+ text = text.slice(text.indexOf('\n') + 1);
42
+ } finally { fs.closeSync(fd); }
43
+ } else {
44
+ text = fs.readFileSync(file, 'utf8');
45
+ }
46
+ } catch (e) {
47
+ if (e.code === 'ENOENT') return [];
48
+ throw e;
49
+ }
50
+ const out = [];
51
+ for (const raw of text.split('\n')) {
52
+ if (!raw.trim()) continue;
53
+ try { out.push(JSON.parse(raw)); } catch { /* a torn last line from a crash; the rest still reads */ }
54
+ }
55
+ READ.set(file, { key, lines: out });
56
+ return out.slice();
57
+ }
58
+
59
+ function append(file, obj) {
60
+ const buf = Buffer.from(JSON.stringify(obj) + '\n', 'utf8');
61
+ if (buf.length > MAX_LINE) throw new Error(`teammates line too large (${buf.length} bytes)`);
62
+ fs.mkdirSync(path.dirname(file), { recursive: true });
63
+ const fd = fs.openSync(file, 'a');
64
+ try { fs.writeSync(fd, buf, 0, buf.length); } finally { fs.closeSync(fd); }
65
+ return obj;
66
+ }
67
+
68
+ export const newTeammateId = () => `t-${crypto.randomBytes(5).toString('hex')}`;
69
+ export const newMessageId = () => `m-${crypto.randomBytes(6).toString('hex')}`;
70
+ export const newMemoryId = () => `k-${crypto.randomBytes(5).toString('hex')}`;
71
+ export const newPlaybookId = () => `p-${crypto.randomBytes(5).toString('hex')}`;
72
+ export const newLessonId = () => `l-${crypto.randomBytes(5).toString('hex')}`;
73
+ export const newDutyId = () => `d-${crypto.randomBytes(4).toString('hex')}`;
74
+
75
+ export function readTeammates(dir, now = Date.now()) {
76
+ return foldTeammates(readLines(teammatesFile(dir)), now);
77
+ }
78
+
79
+ export function readChat(dir) {
80
+ return foldChat(readLines(chatFile(dir)));
81
+ }
82
+
83
+ export function writeTeammate(dir, id, patch, now = Date.now()) {
84
+ return append(teammatesFile(dir), { id, ts: now, patch });
85
+ }
86
+
87
+ export function writeChat(dir, line, now = Date.now()) {
88
+ const full = { ...line, ts: line.ts ?? now };
89
+ if (full.kind !== 'seen' && !full.id) full.id = newMessageId();
90
+ return append(chatFile(dir), full);
91
+ }
92
+
93
+ export function readPlaybooks(dir) {
94
+ return foldPlaybooks(readLines(playbooksFile(dir)));
95
+ }
96
+
97
+ export function writePlaybook(dir, line, now = Date.now()) {
98
+ return append(playbooksFile(dir), { ...line, ts: line.ts ?? now });
99
+ }
@@ -18,7 +18,7 @@ import crypto from 'node:crypto';
18
18
  import { machineryPath } from './home.mjs';
19
19
  import { withProjectLock } from './project-lock.mjs';
20
20
  import {
21
- foldWorkItems, normalizeLine, buildLine, buildClaimLine, buildHeartbeatLine, buildReleaseLine,
21
+ foldWorkItems, foldRawInto, finishItem, normalizeLine, buildLine, buildClaimLine, buildHeartbeatLine, buildReleaseLine,
22
22
  isClaimable, sortForPull, matchesFilter, isWorkItemId, MAX_LINE_BYTES, LEASE_MS,
23
23
  } from '../../shared/work-items.mjs';
24
24
 
@@ -140,32 +140,190 @@ function readLines(file) {
140
140
  // themselves would let one caller's edit become every later caller's truth.
141
141
  const folded = new Map();
142
142
  const MAX_FOLDS_HELD = 64;
143
+ const REWINDOW_BYTES = 2 * 1024 * 1024;
143
144
 
144
- function ledgerStamp(file) {
145
- try {
146
- const st = fs.statSync(file);
147
- return `${st.ino}:${st.size}:${st.mtimeMs}`;
148
- } catch (err) {
149
- if (err.code === 'ENOENT') return 'absent';
150
- throw err;
145
+ // A STAMP OF SIZE AND TIME CANNOT TELL A REPLACED LEDGER FROM AN UNTOUCHED ONE,
146
+ // so the file that was folded is held open.
147
+ //
148
+ // What broke (2026-10-19, on GitHub's Linux runner): a ledger deleted and
149
+ // written again at the same length answered with the fold of the old one.
150
+ // Deleting it freed its inode number for the new file to take, and the clock
151
+ // that filesystem stamps a write with is coarse enough that both writes landed
152
+ // on one mtime, so all three parts of the stamp matched. On APFS the two writes
153
+ // get different nanoseconds, which is why no Mac ever saw it.
154
+ //
155
+ // An open descriptor settles it: while one is held, that inode number cannot be
156
+ // given to another file, so a number we do not recognise means a different
157
+ // file. The generation below carries that into the stamp, so a replaced ledger
158
+ // is read from the start by every cache keyed on it, not just the fold.
159
+ const pinned = new Map();
160
+ const MAX_PINS = 64;
161
+ let generations = 0;
162
+
163
+ function dropPin(file) {
164
+ const pin = pinned.get(file);
165
+ if (!pin) return;
166
+ pinned.delete(file);
167
+ try { fs.closeSync(pin.fd); } catch { /* already gone */ }
168
+ }
169
+
170
+ function ledgerPin(file) {
171
+ const absent = { stamp: 'absent', st: null, fd: null, gen: 0 };
172
+ let st = null;
173
+ try { st = fs.statSync(file); } catch (err) { if (err.code !== 'ENOENT') throw err; }
174
+ if (!st) { dropPin(file); return absent; }
175
+ let pin = pinned.get(file);
176
+ if (!pin || pin.ino !== st.ino) {
177
+ dropPin(file);
178
+ let fd;
179
+ try { fd = fs.openSync(file, 'r'); } catch (err) {
180
+ if (err.code !== 'ENOENT') throw err;
181
+ return absent;
182
+ }
183
+ // The file that ended up held, which is a different one from the stat above
184
+ // if it was replaced between the two calls.
185
+ st = fs.fstatSync(fd);
186
+ generations += 1;
187
+ if (pinned.size >= MAX_PINS) dropPin(pinned.keys().next().value);
188
+ pin = { fd, ino: st.ino, gen: generations };
189
+ pinned.set(file, pin);
151
190
  }
191
+ return { stamp: `${pin.gen}:${st.size}:${st.mtimeMs}`, st, fd: pin.fd, gen: pin.gen };
192
+ }
193
+
194
+ function ledgerStamp(file) {
195
+ return ledgerPin(file).stamp;
152
196
  }
153
197
 
198
+ const usable = (l) => l && Buffer.byteLength(l, 'utf8') <= MAX_LINE_BYTES;
199
+
200
+ // A ledger that has only grown is folded on from where it was: every running
201
+ // agent's heartbeat changes the file, and folding all 8 MB again each time
202
+ // held up every request the window made. A file that shrank, was replaced, or
203
+ // changed without growing is read from the start.
154
204
  function foldedLedger(file) {
155
- const stamp = ledgerStamp(file);
205
+ const pin = ledgerPin(file);
206
+ const st = pin.st;
156
207
  const held = folded.get(file);
157
- if (held && held.stamp === stamp) return held.items;
208
+ if (held && held.stamp === pin.stamp) return held.items;
209
+ // While it still holds no more than the 8 MB a fresh read would, give or
210
+ // take REWINDOW_BYTES added since: a fold kept much longer would show
211
+ // threads a fresh read cuts off.
212
+ const inWindow = held && st && (held.from === 0 ? st.size <= MAX_LEDGER_BYTES : st.size - held.from <= MAX_LEDGER_BYTES + REWINDOW_BYTES);
213
+ if (held && st && held.gen === pin.gen && st.size > held.offset && held.raw && inWindow) {
214
+ const buf = Buffer.allocUnsafe(st.size - held.offset);
215
+ const n = fs.readSync(pin.fd, buf, 0, buf.length, held.offset);
216
+ // Whole lines only, by the byte: a line still being written waits for
217
+ // the next read rather than being cut through a character.
218
+ const end = n ? buf.lastIndexOf(0x0a, n - 1) + 1 : 0;
219
+ const chunk = buf.toString('utf8', 0, end);
220
+ held.offset += end;
221
+ const touched = new Set();
222
+ foldRawInto(held.raw, chunk.split('\n').filter(usable), touched);
223
+ // Only threads new to this fold or reopened in it: the rest were looked at
224
+ // already, and one with no title anywhere would be read for on every line.
225
+ if (held.from > 0) {
226
+ const was = (id) => held.items.get(id);
227
+ const arrived = [...touched].filter((id) => !was(id) || was(id).status === 'done');
228
+ restoreCutHeads(held.raw, arrived, pin.fd, held.from, held.offset);
229
+ }
230
+ for (const id of touched) held.items.set(id, finishItem(held.raw.get(id), 0));
231
+ held.stamp = pin.stamp;
232
+ return held.items;
233
+ }
158
234
  // Time zero, so the lease pass inside the fold expires nothing: a lease is
159
235
  // only ever lapsed against the clock of the call asking, and that is `aged`.
160
- const items = stamp === 'absent' ? new Map() : foldWorkItems(readLines(file), 0);
236
+ const raw = new Map();
237
+ let offset = 0;
238
+ let from = 0;
239
+ if (st) {
240
+ const read = readWindow(file, st.size, pin.fd);
241
+ foldRawInto(raw, read.text.split('\n').filter(usable));
242
+ if (read.from > 0) restoreCutHeads(raw, raw.keys(), pin.fd, read.from, read.end);
243
+ offset = read.end;
244
+ from = read.from;
245
+ }
246
+ const items = new Map();
247
+ for (const [id, item] of raw) items.set(id, finishItem(item, 0));
161
248
  // One entry per ledger, so a real store has a few dozen. The cap is for the
162
249
  // long-lived MCP server, which walks temporary project dirs that never come
163
250
  // back; a Map iterates in insertion order, so the oldest key goes first.
164
251
  if (folded.size >= MAX_FOLDS_HELD) folded.delete(folded.keys().next().value);
165
- folded.set(file, { stamp, items });
252
+ folded.set(file, { stamp: pin.stamp, items, raw, gen: pin.gen, offset, from });
166
253
  return items;
167
254
  }
168
255
 
256
+ // The ledger's trailing window, as text, to its last whole line, and the byte
257
+ // that ends at, read through the descriptor the fold holds open.
258
+ function readWindow(file, size, fd) {
259
+ if (size > MAX_LEDGER_BYTES && !warnedOversize.has(file)) {
260
+ warnedOversize.add(file);
261
+ console.warn(`work-items: ${file} is ${size} bytes; reading only the last ${MAX_LEDGER_BYTES}`);
262
+ }
263
+ const from = Math.max(0, size - MAX_LEDGER_BYTES);
264
+ const buf = Buffer.allocUnsafe(size - from);
265
+ const n = fs.readSync(fd, buf, 0, buf.length, from);
266
+ const end = n ? buf.lastIndexOf(0x0a, n - 1) + 1 : 0;
267
+ return { text: buf.toString('utf8', 0, end), end: from + end, from };
268
+ }
269
+
270
+ // AN UNFINISHED THREAD WHOSE FIRST LINE IS BEFORE THE WINDOW is folded from its
271
+ // later lines alone: no title, so a blank row in the inbox. A title is written
272
+ // once, by the line that makes a thread, so no title means the cut went through
273
+ // it, and its own lines are read from the start of the file and folded again.
274
+ // A finished one is left to `readOlderWorkItems`, so a ledger without such a
275
+ // thread is read exactly as it always was.
276
+ //
277
+ // THE CLAIM STAYS THE WINDOW'S. `claimWorkItem` numbers a claim from the window,
278
+ // so one made after the cut can be lower than one before it, and the older one
279
+ // would fence off everything its agent wrote since.
280
+ function restoreCutHeads(raw, ids, fd, from, end) {
281
+ const cut = new Set();
282
+ for (const id of ids) {
283
+ const item = raw.get(id);
284
+ if (!item.wrote.title && item.status !== 'done') cut.add(id);
285
+ }
286
+ if (!cut.size) return;
287
+ const lines = linesUpTo(fd, end, cut);
288
+ const restored = new Map();
289
+ foldRawInto(restored, lines.filter((l) => l.at < from).map((l) => l.text));
290
+ const found = [...restored.keys()].filter((id) => cut.has(id));
291
+ for (const id of found) Object.assign(restored.get(id), { epoch: 0, claim: null });
292
+ foldRawInto(restored, lines.filter((l) => l.at >= from).map((l) => l.text));
293
+ for (const id of found) raw.set(id, restored.get(id));
294
+ }
295
+
296
+ // The whole lines before byte `end` that carry one of `ids`, with the byte each
297
+ // starts at, read a megabyte at a time. Searched as bytes, so a line that is
298
+ // not one of them is never decoded; the fold checks whose each line really is.
299
+ const PIECE_BYTES = 1024 * 1024;
300
+ const ID_KEY = Buffer.from('"id":"');
301
+ function linesUpTo(fd, end, ids) {
302
+ const out = [];
303
+ let rest = Buffer.alloc(0);
304
+ let restAt = 0;
305
+ for (let pos = 0; pos < end;) {
306
+ const buf = Buffer.allocUnsafe(Math.min(PIECE_BYTES, end - pos));
307
+ const n = fs.readSync(fd, buf, 0, buf.length, pos);
308
+ if (!n) break;
309
+ pos += n;
310
+ const bytes = rest.length ? Buffer.concat([rest, buf.subarray(0, n)]) : buf.subarray(0, n);
311
+ const whole = bytes.lastIndexOf(0x0a) + 1;
312
+ const starts = new Set();
313
+ for (let at = bytes.indexOf(ID_KEY); at !== -1 && at < whole; at = bytes.indexOf(ID_KEY, at + ID_KEY.length)) {
314
+ const from = at + ID_KEY.length;
315
+ if (ids.has(bytes.toString('latin1', from, bytes.indexOf(0x22, from)))) starts.add(bytes.lastIndexOf(0x0a, at) + 1);
316
+ }
317
+ for (const start of starts) {
318
+ const text = bytes.toString('utf8', start, bytes.indexOf(0x0a, start));
319
+ if (usable(text)) out.push({ text, at: restAt + start });
320
+ }
321
+ rest = bytes.subarray(whole);
322
+ restAt += whole;
323
+ }
324
+ return out;
325
+ }
326
+
169
327
  // One item as of `now`: a copy, with the lease rule that `foldWorkItems` applies
170
328
  // at the end of its own pass applied here instead. The two mutable fields a
171
329
  // caller can reach through a spread are copied with it.
@@ -238,6 +396,56 @@ export function readWorkItem(projectDir, id, now = Date.now()) {
238
396
  return item ? aged(item, now) : null;
239
397
  }
240
398
 
399
+ /**
400
+ * ONE THREAD'S LINES, OLDEST FIRST, without the lease lines (heartbeats and
401
+ * releases), the last `max` of them.
402
+ *
403
+ * The phone pass (main/phone-inbox.mjs) asks this for every thread of a
404
+ * project, up to 300, and each ask used to read and parse the whole ledger:
405
+ * 20,929 ms of blocked main thread on the real 17,797-line agentbox-team
406
+ * ledger, against 101 ms for one read (2026-10-14, the endless beachball). So
407
+ * the ledger is read once per change of the file, grouped by thread, and kept,
408
+ * keyed on the file's identity exactly as the fold is. The lines handed out are
409
+ * the kept ones, frozen, so no caller can edit what the next one reads.
410
+ */
411
+ const histories = new Map();
412
+ const HISTORIES_HELD = 8;
413
+ function deepFreeze(v) {
414
+ if (v && typeof v === 'object' && !Object.isFrozen(v)) {
415
+ Object.freeze(v);
416
+ for (const k of Object.keys(v)) deepFreeze(v[k]);
417
+ }
418
+ return v;
419
+ }
420
+ function historiesOf(file) {
421
+ const stamp = ledgerStamp(file);
422
+ const held = histories.get(file);
423
+ if (held && held.stamp === stamp) return held.byId;
424
+ const byId = new Map();
425
+ if (stamp !== 'absent') {
426
+ for (const raw of readLines(file)) {
427
+ let line;
428
+ try { line = JSON.parse(raw); } catch { continue; }
429
+ if (!line || typeof line.id !== 'string' || line.heartbeat || line.release) continue;
430
+ let list = byId.get(line.id);
431
+ if (!list) byId.set(line.id, (list = []));
432
+ list.push(deepFreeze(line));
433
+ }
434
+ }
435
+ histories.delete(file);
436
+ if (histories.size >= HISTORIES_HELD) histories.delete(histories.keys().next().value);
437
+ histories.set(file, { stamp, byId });
438
+ return byId;
439
+ }
440
+ // `before` keeps the lines written at or before that moment, and the cap then
441
+ // counts back from there rather than from the newest line.
442
+ export function readItemHistory(projectDir, id, { max = 400, before } = {}) {
443
+ const lines = historiesOf(ledgerPath(projectDir)).get(id);
444
+ if (!lines) return [];
445
+ const upTo = Number.isFinite(before) ? lines.filter((l) => l.ts <= before) : lines;
446
+ return upTo.slice(-max);
447
+ }
448
+
241
449
  /* -------------------------------- mutating ------------------------------- */
242
450
  export function newWorkItemId() {
243
451
  return `w-${crypto.randomBytes(5).toString('hex')}`;
@@ -440,4 +648,14 @@ export function readLinesFrom(projectDir, offset = 0) {
440
648
  }
441
649
 
442
650
  export { LEASE_MS };
443
- export const _internals = { appendLine, readLines, ledgerPath, forgetFolds: () => folded.clear() };
651
+ export const _internals = {
652
+ appendLine,
653
+ readLines,
654
+ ledgerPath,
655
+ forgetFolds: () => {
656
+ folded.clear();
657
+ histories.clear();
658
+ wholeFolds.clear();
659
+ for (const file of [...pinned.keys()]) dropPin(file);
660
+ },
661
+ };