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,135 @@
1
+ // THE RUN LEDGER: one line per finished run, for agent hours (w-3d0fa23ffb).
2
+ //
3
+ // `<appHome>/runs.jsonl`, written when a run ends (supervisor `_recordRun`).
4
+ // Cloud runs keep their own ledger (main/cloud/usage.mjs) and are read from
5
+ // there. Old runs are read once from their trace logs (`backfillRuns`). Every
6
+ // line has a key, and every reader dedupes on it, so a run written twice is
7
+ // counted once.
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { appHome, machineryDir } from './store/home.mjs';
11
+ import { dedupeRuns, runFromTrace } from '../shared/agent-hours.mjs';
12
+
13
+ const BACKFILL_DAYS = 400;
14
+ const TAIL_BYTES = 400;
15
+ // A log with no exit line that was written to this recently is a run still
16
+ // going; it writes its own line when it ends.
17
+ const STILL_GOING_MS = 30 * 60_000;
18
+
19
+ export function runLedgerPath(home = appHome()) {
20
+ return path.join(home, 'runs.jsonl');
21
+ }
22
+
23
+ export function appendRun(run, { file = runLedgerPath() } = {}) {
24
+ try {
25
+ fs.mkdirSync(path.dirname(file), { recursive: true });
26
+ fs.appendFileSync(file, JSON.stringify(run) + '\n');
27
+ return true;
28
+ } catch {
29
+ // A run that cannot be written never fails the run.
30
+ return false;
31
+ }
32
+ }
33
+
34
+ export function readRuns({ file = runLedgerPath() } = {}) {
35
+ let text;
36
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
37
+ const out = [];
38
+ for (const line of text.split('\n')) {
39
+ if (!line.trim()) continue;
40
+ try { out.push(JSON.parse(line)); } catch { /* one broken line loses only itself */ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ export function runsFromCloudUsage(lines) {
46
+ const out = [];
47
+ for (const l of lines ?? []) {
48
+ const at = Date.parse(l?.at);
49
+ const seconds = Number(l?.wallSeconds);
50
+ if (!Number.isFinite(at) || !(seconds > 0)) continue;
51
+ out.push({
52
+ key: `cloud:${l.runId ?? at}`,
53
+ at,
54
+ start: at - seconds * 1000,
55
+ item: l.item ?? null,
56
+ product: null,
57
+ engine: l.engine ?? null,
58
+ seconds,
59
+ failed: Number.isInteger(l.exitCode) ? l.exitCode !== 0 : null,
60
+ where: 'cloud',
61
+ });
62
+ }
63
+ return out;
64
+ }
65
+
66
+ export function allRuns({ file = runLedgerPath(), cloudLines = [] } = {}) {
67
+ return dedupeRuns([...readRuns({ file }), ...runsFromCloudUsage(cloudLines)]);
68
+ }
69
+
70
+ function tailOf(file) {
71
+ let fd;
72
+ try {
73
+ fd = fs.openSync(file, 'r');
74
+ const size = fs.fstatSync(fd).size;
75
+ const len = Math.min(size, TAIL_BYTES);
76
+ const buf = Buffer.alloc(len);
77
+ fs.readSync(fd, buf, 0, len, size - len);
78
+ return buf.toString('utf8');
79
+ } catch {
80
+ return '';
81
+ } finally {
82
+ if (fd !== undefined) try { fs.closeSync(fd); } catch {}
83
+ }
84
+ }
85
+
86
+ // Reads every run log of this store's projects into the ledger, once per run.
87
+ // Resolves to how many runs it added. It hands the event loop back every
88
+ // YIELD_EVERY logs: on the founder's Mac it reads 25,780 logs in 3.4 s, which
89
+ // in one piece would freeze the window.
90
+ const YIELD_EVERY = 200;
91
+ const breathe = () => new Promise((r) => setImmediate(r));
92
+
93
+ export async function backfillRuns({ home = appHome(), products = [], file = runLedgerPath(home), now = Date.now() } = {}) {
94
+ const have = new Set(readRuns({ file }).map((r) => r?.key));
95
+ const since = now - BACKFILL_DAYS * 86_400_000;
96
+ let added = 0;
97
+ let read = 0;
98
+ let pending = [];
99
+ const flush = () => {
100
+ if (!pending.length) return;
101
+ try {
102
+ fs.mkdirSync(path.dirname(file), { recursive: true });
103
+ fs.appendFileSync(file, pending.map((r) => JSON.stringify(r)).join('\n') + '\n');
104
+ added += pending.length;
105
+ } catch { /* the next backfill tries again */ }
106
+ pending = [];
107
+ };
108
+ for (const p of products) {
109
+ if (!p?.dir) continue;
110
+ const sessions = path.join(machineryDir(p.dir, home), 'sessions');
111
+ let items;
112
+ try { items = fs.readdirSync(sessions, { withFileTypes: true }); } catch { continue; }
113
+ for (const it of items) {
114
+ if (!it.isDirectory()) continue;
115
+ let logs;
116
+ try { logs = fs.readdirSync(path.join(sessions, it.name)); } catch { continue; }
117
+ for (const name of logs) {
118
+ const start = Number(/^(\d{12,14})\.log$/.exec(name)?.[1]);
119
+ if (!Number.isFinite(start) || start < since || start > now) continue;
120
+ if (++read % YIELD_EVERY === 0) { flush(); await breathe(); }
121
+ const full = path.join(sessions, it.name, name);
122
+ let mtimeMs;
123
+ try { mtimeMs = fs.statSync(full).mtimeMs; } catch { continue; }
124
+ const tail = tailOf(full);
125
+ if (!/# exited \(/.test(tail) && now - mtimeMs < STILL_GOING_MS) continue;
126
+ const run = runFromTrace({ name, text: tail, mtimeMs, item: it.name, product: p.slug ?? null });
127
+ if (!run || have.has(run.key)) continue;
128
+ pending.push({ ...run, backfilled: true });
129
+ have.add(run.key);
130
+ }
131
+ }
132
+ }
133
+ flush();
134
+ return added;
135
+ }
@@ -0,0 +1,41 @@
1
+ // SAVING THE FILES A MESSAGE CARRIES, into a project's attachments folder.
2
+ //
3
+ // One copy, used by the reply box (main/ipc.mjs zero:save-attachments) and by
4
+ // a message to a person (main/team/index.mjs), whose conversation may not
5
+ // exist until the message makes it, so it cannot save them beforehand.
6
+ // Relative paths keep them readable by workers through the MCP and renderable
7
+ // in the message view, and they are the names the team sync sends on
8
+ // (main/team/sync.mjs).
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ /**
13
+ * `files` are `{ name, dataBase64 }` or `{ name, srcPath }`. Answers what was
14
+ * saved: `{ rel, name, image }`, in order, skipping one with neither.
15
+ */
16
+ export function saveAttachments(dir, files) {
17
+ const outDir = path.join(dir, 'attachments');
18
+ fs.mkdirSync(outDir, { recursive: true });
19
+ const saved = [];
20
+ for (const file of files ?? []) {
21
+ const safe = String(file?.name ?? 'file').replace(/[^\w.-]+/g, '-').slice(0, 80);
22
+ // A RANDOM SUFFIX, THE SAME ONE THE STAGING PATH ALREADY USES. The name
23
+ // was the millisecond plus the filename, so two files saved in the same
24
+ // millisecond with the same name overwrote each other and the message
25
+ // ended up with two links to one picture. Screenshots are often sent in
26
+ // pairs, which is exactly the shape that collides. It has never been observed
27
+ // happening; it is one line to make impossible.
28
+ const rel = `attachments/${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`;
29
+ const abs = path.join(dir, rel);
30
+ if (file?.dataBase64) fs.writeFileSync(abs, Buffer.from(file.dataBase64, 'base64'));
31
+ else if (file?.srcPath) fs.copyFileSync(file.srcPath, abs);
32
+ else continue;
33
+ saved.push({ rel, name: safe, image: /\.(png|jpe?g|gif|webp)$/i.test(safe) });
34
+ }
35
+ return saved;
36
+ }
37
+
38
+ /** The links a message carries for what was saved, one per line. */
39
+ export function attachmentMarkdown(saved) {
40
+ return saved.map((s) => (s.image ? `![${s.name}](${s.rel})` : `[${s.name}](${s.rel})`)).join('\n');
41
+ }
package/main/serve.mjs CHANGED
@@ -30,7 +30,8 @@ import { fileURLToPath } from 'node:url';
30
30
 
31
31
  import { spawn } from 'node:child_process';
32
32
  import { Readable } from 'node:stream';
33
- import { mediaResponse, mediaType } from './img-scheme.mjs';
33
+ import { mediaResponse, mediaType, servable } from './img-scheme.mjs';
34
+ import { readPictureShape } from './picture-shape.mjs';
34
35
  import { loadConfig } from './config.mjs';
35
36
  import { Store } from './store.mjs';
36
37
  import { Supervisor } from './supervisor.mjs';
@@ -40,7 +41,9 @@ import { reportFromRenderer } from './crash-report.mjs';
40
41
  import * as workItemsDisk from './store/work-items.mjs';
41
42
  import { createTeamService, teamStateFile } from './team/index.mjs';
42
43
  import { loadCloudConfig, supabaseSession, headlessSessionFile } from './team/session.mjs';
44
+ import { roomsSetup } from './team/room-setup.mjs';
43
45
  import { appHome, carryMisplacedMachinery, storeRootEnv } from './store/home.mjs';
46
+ import { keepAnswerModesIn } from './answer-modes.mjs';
44
47
 
45
48
  const here = path.dirname(fileURLToPath(import.meta.url));
46
49
  const repoRoot = path.dirname(here);
@@ -143,6 +146,8 @@ export function answerWhatOnlyTheWindowAnswered(ipcMain) {
143
146
  ipcMain.handle('zero:corner-tag', () => null);
144
147
  ipcMain.handle('zero:notify', () => null);
145
148
  ipcMain.handle('zero:boot-info', () => ({ reloaded: false, builtAt: null, recovered: null }));
149
+ // A room invite link only ever opens the desktop app.
150
+ ipcMain.handle('zero:take-join', () => null);
146
151
  ipcMain.handle('zero:crash', (_e, payload) => { reportFromRenderer(payload ?? {}); return null; });
147
152
  // The phone door belongs to the desktop app (main/phone-link.mjs); a tab is told there is none.
148
153
  ipcMain.handle('zero:phone-link', () => null);
@@ -175,6 +180,7 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
175
180
  const inheritedHome = appHome();
176
181
  Object.assign(process.env, storeRootEnv(config.storeRoot));
177
182
  carryMisplacedMachinery(inheritedHome, config.storeRoot);
183
+ keepAnswerModesIn({ userDir, storeRoot: config.storeRoot });
178
184
  const store = await new Store(config).init();
179
185
  const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
180
186
  const window = broadcastingWindow();
@@ -196,6 +202,7 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
196
202
  accountRoot: config.accountRoot,
197
203
  stateFile: teamStateFile(config.storeRoot),
198
204
  onChange: () => ipc?.push?.(),
205
+ rooms: roomsSetup({ config, store, supervisor, userDir }),
199
206
  }) : null;
200
207
  // AND IT STOPS ITS OWN WORKERS ON THE WAY OUT, as main/main.mjs does for the
201
208
  // desktop. Control-C reaches the whole process group, so without this each
@@ -212,6 +219,23 @@ export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, user
212
219
  /** A token in the url, because the port alone is not a door key. */
213
220
  export const newToken = () => crypto.randomBytes(24).toString('base64url');
214
221
 
222
+ /**
223
+ * THE PICTURES A TAB MAY DRAW: the desktop window's own rule (`servable`,
224
+ * main/img-scheme.mjs) over its own roots (`imageRoots`, main/main.mjs), so a
225
+ * door opened for a screenshot can never hand out `zero.config.json`.
226
+ *
227
+ * Pictures only. A whole document is served under a grant the pane opens one
228
+ * at a time (main/doc-scheme.mjs) and this copy opens none.
229
+ */
230
+ export function picturesTheAppOwns({ config, store }) {
231
+ return {
232
+ allows: (kind, file) => kind === 'img' && servable(file, [
233
+ config?.accountRoot,
234
+ ...(store?.listProducts?.() ?? []).map((p) => p.repoPath),
235
+ ].filter(Boolean)),
236
+ };
237
+ }
238
+
215
239
  /**
216
240
  * THE KEY THAT ONLY READS FILES. A design page runs its own scripts and can
217
241
  * read its own address, so the address a phone loads it from must not carry
@@ -254,14 +278,24 @@ const MIME = {
254
278
  */
255
279
  async function answerFile(req, res, url, { token, files }) {
256
280
  const [, , key, kind, ...rest] = url.pathname.split('/');
257
- if (kind !== 'doc' && kind !== 'img') { res.writeHead(404).end('No such kind of file.'); return; }
281
+ if (kind !== 'doc' && kind !== 'img' && kind !== 'shape') { res.writeHead(404).end('No such kind of file.'); return; }
258
282
  if (!sameKey(key, fileKey(typeof token === 'function' ? token() : token))) { res.writeHead(403).end('Wrong key.'); return; }
259
283
  let file;
260
284
  try { file = path.normalize('/' + rest.map(decodeURIComponent).join('/')); } catch { res.writeHead(400).end('Bad path.'); return; }
261
- if (!path.isAbsolute(file) || !files.allows(kind, file)) { res.writeHead(403).end('Not available.'); return; }
285
+ // A shape is asked of a picture, under the picture's own rule.
286
+ if (!path.isAbsolute(file) || !files.allows(kind === 'shape' ? 'img' : kind, file)) { res.writeHead(403).end('Not available.'); return; }
262
287
  let stat;
263
288
  try { stat = await fs.promises.stat(file); } catch { res.writeHead(404).end('Not there.'); return; }
264
289
  if (!stat.isFile()) { res.writeHead(404).end('Not there.'); return; }
290
+ // ITS WIDTH AND HEIGHT, so a picture the phone has never seen holds its
291
+ // place before its last byte comes (main/picture-shape.mjs).
292
+ if (kind === 'shape') {
293
+ const shape = await readPictureShape(file).catch(() => null);
294
+ if (!shape) { res.writeHead(404).end('Not a picture this can measure.'); return; }
295
+ res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
296
+ res.end(JSON.stringify(shape));
297
+ return;
298
+ }
265
299
  const media = mediaType(file);
266
300
  if (media) {
267
301
  const out = await mediaResponse(file, req.headers.range);
@@ -316,6 +350,14 @@ export function createHandler({ channels, token, listeners = new Set(), dist = p
316
350
  // NOR DOES THE PAGE ITSELF ASK: the tab takes the token out of its address
317
351
  // bar on load, so a reload asks for `/` bare, and refusing that drew
318
352
  // "Wrong or missing token" on every reload (tests/a-browser-tab-survives-a-reload).
353
+ //
354
+ // AND THE HOST HEADER IS NOT CHECKED, asked and answered 2026-10-24.
355
+ // Refusing a Host that is not localhost is the usual guard against a page
356
+ // whose own name resolves to this machine, and it cannot go here:
357
+ // main/phone-link.mjs serves this same handler to a phone through a relay
358
+ // or over Tailscale, where the Host is neither. The token is the key,
359
+ // whatever name the request arrives under
360
+ // (tests/the-doing-half-runs-with-no-electron).
319
361
  const guarded = url.pathname === '/events' || url.pathname.startsWith('/api/') || url.pathname === '/file-key';
320
362
  const given = req.headers['x-agentbox-token'] || url.searchParams.get('token');
321
363
  if (guarded && !sameKey(given, typeof token === 'function' ? token() : token)) {