agentbox-app 0.1.12 → 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 (370) 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/acp.mjs +296 -0
  9. package/main/agent-homes.mjs +158 -0
  10. package/main/agent-sessions.mjs +311 -260
  11. package/main/agent-updates.mjs +9 -22
  12. package/main/agents.mjs +179 -466
  13. package/main/analytics.mjs +42 -2
  14. package/main/answer-modes.mjs +103 -37
  15. package/main/approval-prompt-core.mjs +197 -0
  16. package/main/approval-prompt-server.mjs +4 -138
  17. package/main/approvals.mjs +101 -297
  18. package/main/artifact-path.mjs +47 -18
  19. package/main/claude-bin.mjs +45 -311
  20. package/main/claude-input.mjs +57 -7
  21. package/main/claude-launch-env.mjs +54 -0
  22. package/main/claude-models.mjs +39 -7
  23. package/main/claude-plan.mjs +64 -16
  24. package/main/claude-session-open.mjs +28 -0
  25. package/main/claude-usage-process.mjs +40 -0
  26. package/main/claude-usage.mjs +94 -59
  27. package/main/cloud/account.mjs +179 -0
  28. package/main/cloud/aws.mjs +127 -0
  29. package/main/cloud/browser-return.mjs +58 -0
  30. package/main/cloud/clients.mjs +15 -0
  31. package/main/cloud/codex-cloud.mjs +53 -0
  32. package/main/cloud/config.mjs +107 -0
  33. package/main/cloud/docs.mjs +219 -0
  34. package/main/cloud/exec.mjs +27 -0
  35. package/main/cloud/host.mjs +165 -0
  36. package/main/cloud/kit.mjs +80 -0
  37. package/main/cloud/logins.mjs +543 -0
  38. package/main/cloud/managed.mjs +115 -0
  39. package/main/cloud/marker.mjs +104 -0
  40. package/main/cloud/org.mjs +53 -0
  41. package/main/cloud/reuse.mjs +208 -0
  42. package/main/cloud/run-cli.mjs +177 -0
  43. package/main/cloud/run.mjs +1934 -0
  44. package/main/cloud/usage.mjs +76 -0
  45. package/main/cloud-permission.mjs +51 -0
  46. package/main/codex-account.mjs +78 -0
  47. package/main/codex-app-server.mjs +276 -409
  48. package/main/codex-approvals.mjs +148 -433
  49. package/main/codex-bin.mjs +51 -249
  50. package/main/codex-models.mjs +20 -2
  51. package/main/codex-session.mjs +246 -506
  52. package/main/codex-threads-worker.mjs +12 -0
  53. package/main/codex-threads.mjs +112 -11
  54. package/main/codex-usage-file.mjs +5 -3
  55. package/main/codex-usage.mjs +56 -7
  56. package/main/codex-watch.mjs +83 -5
  57. package/main/codex.mjs +95 -363
  58. package/main/config.mjs +37 -5
  59. package/main/corner-tag.mjs +810 -0
  60. package/main/crash-report.mjs +26 -3
  61. package/main/dev-window.mjs +1 -1
  62. package/main/engine-bin.mjs +545 -0
  63. package/main/engine-setup.mjs +25 -24
  64. package/main/feedback.mjs +14 -31
  65. package/main/folder-pictures.mjs +2 -2
  66. package/main/freeze-recorder-thread.mjs +86 -0
  67. package/main/freeze-recorder.mjs +146 -0
  68. package/main/fresh-user.mjs +20 -9
  69. package/main/git-change-offthread.mjs +17 -3
  70. package/main/git-change-thread.mjs +5 -2
  71. package/main/harnesses/claude-stream.mjs +208 -0
  72. package/main/harnesses/claude.mjs +105 -0
  73. package/main/harnesses/codex.mjs +72 -0
  74. package/main/harnesses/common.mjs +78 -0
  75. package/main/harnesses/instructions-file.mjs +116 -0
  76. package/main/harnesses/opencode.mjs +234 -0
  77. package/main/harnesses.mjs +65 -0
  78. package/main/importable-threads.mjs +96 -0
  79. package/main/ipc.mjs +520 -135
  80. package/main/leftovers.mjs +117 -25
  81. package/main/liquid-glass.mjs +26 -0
  82. package/main/live-replies.mjs +10 -4
  83. package/main/local-app-frames.mjs +82 -0
  84. package/main/main-went-red.mjs +330 -0
  85. package/main/main.mjs +248 -37
  86. package/main/memory-gate-history.mjs +85 -20
  87. package/main/memory-gate-server.mjs +129 -11
  88. package/main/memory-gate.mjs +62 -8
  89. package/main/memory-log.mjs +124 -0
  90. package/main/menu.mjs +4 -2
  91. package/main/message-priority.mjs +2 -2
  92. package/main/mic.mjs +38 -0
  93. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  94. package/main/new-chats-watch.mjs +110 -0
  95. package/main/new-user-window.mjs +25 -0
  96. package/main/notify.mjs +64 -5
  97. package/main/opencode-server.mjs +271 -0
  98. package/main/opencode.mjs +287 -0
  99. package/main/phone-alerts.mjs +117 -0
  100. package/main/phone-inbox.mjs +625 -0
  101. package/main/phone-link.mjs +571 -0
  102. package/main/phone-uploads.mjs +476 -0
  103. package/main/picture-shape.mjs +114 -0
  104. package/main/quit-sequence.mjs +143 -0
  105. package/main/refresh-agent-models.mjs +15 -2
  106. package/main/relay-address.mjs +28 -0
  107. package/main/relay-client.mjs +314 -0
  108. package/main/repeats.mjs +10 -4
  109. package/main/row-label.mjs +18 -47
  110. package/main/run-change.mjs +13 -0
  111. package/main/run-ledger.mjs +135 -0
  112. package/main/runs-on.mjs +32 -35
  113. package/main/save-attachments.mjs +41 -0
  114. package/main/serve.mjs +155 -9
  115. package/main/settings.mjs +364 -50
  116. package/main/shell-path.mjs +234 -0
  117. package/main/ship-conflict-brief.mjs +119 -0
  118. package/main/ship-queue.mjs +134 -20
  119. package/main/sign-in-files.mjs +3 -7
  120. package/main/source-updater.mjs +45 -11
  121. package/main/stage-attachment.mjs +68 -0
  122. package/main/stall-watch.mjs +50 -0
  123. package/main/start-hold.mjs +73 -0
  124. package/main/store/home.mjs +87 -232
  125. package/main/store/paths.mjs +11 -0
  126. package/main/store/teammates-files.mjs +99 -0
  127. package/main/store/work-items.mjs +231 -13
  128. package/main/store.mjs +227 -53
  129. package/main/supervisor.mjs +3865 -699
  130. package/main/task-commands.mjs +13 -17
  131. package/main/task-folders-offthread.mjs +6 -1
  132. package/main/task-folders.mjs +897 -105
  133. package/main/task-terminals.mjs +147 -41
  134. package/main/team/index.mjs +114 -10
  135. package/main/team/memory-cloud.mjs +183 -1
  136. package/main/team/room-hours.mjs +48 -0
  137. package/main/team/room-setup.mjs +31 -0
  138. package/main/team/rooms-memory.mjs +454 -0
  139. package/main/team/rooms-shape.mjs +42 -0
  140. package/main/team/rooms-supabase.mjs +324 -0
  141. package/main/team/rooms.mjs +575 -0
  142. package/main/team/session.mjs +96 -26
  143. package/main/team/supabase-backend.mjs +180 -0
  144. package/main/team/sync.mjs +109 -1
  145. package/main/teammates.mjs +580 -0
  146. package/main/terminal-host.mjs +143 -0
  147. package/main/terminal-hosts.mjs +261 -0
  148. package/main/transcript-search.mjs +80 -0
  149. package/main/updater.mjs +71 -9
  150. package/main/webview-guard.mjs +45 -0
  151. package/main/worktree-include.mjs +365 -0
  152. package/mcp/core/documents.mjs +3 -1
  153. package/mcp/core/repeats.mjs +88 -0
  154. package/mcp/core/teammates.mjs +222 -0
  155. package/mcp/core/work.mjs +76 -4
  156. package/mcp/stdio.mjs +24 -11
  157. package/mcp/tools.mjs +116 -8
  158. package/package.json +30 -5
  159. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
  160. package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
  161. package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
  162. package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
  163. package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
  164. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  165. package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
  166. package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
  167. package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
  168. package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
  169. package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
  170. package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
  171. package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
  172. package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
  173. package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
  174. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
  175. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  176. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  177. package/renderer/dist/assets/cose-bilkent-JH36ORCC-CbkyZOUH.js +1 -0
  178. package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
  179. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
  180. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  181. package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
  182. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  183. package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
  184. package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
  185. package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
  186. package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
  187. package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
  188. package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
  189. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
  190. package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
  191. package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
  192. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
  193. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
  194. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
  195. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  196. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
  197. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  198. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
  199. package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
  200. package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
  201. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  202. package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
  203. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  204. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  205. package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
  206. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
  207. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  208. package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
  209. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
  210. package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
  211. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
  212. package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
  213. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
  214. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
  215. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
  216. package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
  217. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
  218. package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
  219. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  220. package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
  221. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
  222. package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
  223. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
  224. package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
  225. package/renderer/dist/corner-tag.html +13 -0
  226. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  227. package/renderer/dist/emulator/data/compression/README.md +11 -0
  228. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  229. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  230. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  231. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  232. package/renderer/dist/emulator/data/cores/README.md +22 -0
  233. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  234. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  235. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  236. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  237. package/renderer/dist/emulator/data/cores/package.json +65 -0
  238. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  239. package/renderer/dist/emulator/data/emulator.css +1614 -0
  240. package/renderer/dist/emulator/data/loader.js +170 -0
  241. package/renderer/dist/emulator/data/localization/README.md +63 -0
  242. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  243. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  244. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  245. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  246. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  247. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  248. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  249. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  250. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  251. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  252. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  253. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  254. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  255. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  256. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  257. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  258. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  259. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  260. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  261. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  262. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  263. package/renderer/dist/emulator/data/src/compression.js +142 -0
  264. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  265. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  266. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  267. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  268. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  269. package/renderer/dist/emulator/data/src/storage.js +125 -0
  270. package/renderer/dist/emulator/data/version.json +5 -0
  271. package/renderer/dist/emulator/host.js +297 -0
  272. package/renderer/dist/emulator/index.html +28 -0
  273. package/renderer/dist/emulator/provenance.json +6 -0
  274. package/renderer/dist/index.html +16 -3
  275. package/scripts/after-install.mjs +30 -8
  276. package/scripts/approval-server.sh +24 -5
  277. package/scripts/before-publish.mjs +12 -10
  278. package/scripts/cloud-shim.pl +57 -0
  279. package/shared/account-names.d.mts +6 -0
  280. package/shared/account-names.mjs +15 -0
  281. package/shared/agent-hours.mjs +136 -0
  282. package/shared/agent-import.mjs +62 -34
  283. package/shared/agents.d.mts +1 -0
  284. package/shared/agents.mjs +126 -522
  285. package/shared/analytics-events.mjs +12 -0
  286. package/shared/approval-card.mjs +36 -0
  287. package/shared/bridge-map.mjs +45 -1
  288. package/shared/claude-code-shape.mjs +6 -0
  289. package/shared/claude-commands.generated.mjs +6 -4
  290. package/shared/claude-models.generated.mjs +6 -4
  291. package/shared/claude-usage.d.mts +1 -0
  292. package/shared/claude-usage.mjs +42 -1
  293. package/shared/cloud-hand-back.d.mts +7 -0
  294. package/shared/cloud-hand-back.mjs +20 -0
  295. package/shared/cloud-kit.d.mts +219 -0
  296. package/shared/cloud-kit.mjs +341 -0
  297. package/shared/cloud-login-name.mjs +4 -0
  298. package/shared/cloud-start.mjs +17 -0
  299. package/shared/cloud-versions.mjs +23 -0
  300. package/shared/codex-import.mjs +2 -2
  301. package/shared/codex-modes.d.mts +22 -9
  302. package/shared/codex-modes.mjs +67 -11
  303. package/shared/codex-usage.mjs +108 -2
  304. package/shared/corner-tag.d.mts +36 -0
  305. package/shared/corner-tag.mjs +396 -0
  306. package/shared/dead-run-trace.mjs +19 -2
  307. package/shared/engines.mjs +76 -300
  308. package/shared/feedback.d.mts +7 -0
  309. package/shared/feedback.mjs +25 -0
  310. package/shared/first-run-practice.mjs +17 -24
  311. package/shared/harness-definitions.mjs +111 -0
  312. package/shared/instruction-defaults.json +2 -2
  313. package/shared/key-caps.d.mts +14 -0
  314. package/shared/key-caps.mjs +68 -0
  315. package/shared/message-edits.d.mts +7 -0
  316. package/shared/message-edits.mjs +35 -0
  317. package/shared/move-thread.d.mts +24 -0
  318. package/shared/move-thread.mjs +112 -0
  319. package/shared/named-threads.mjs +72 -0
  320. package/shared/not-shipped.d.mts +22 -0
  321. package/shared/not-shipped.mjs +89 -0
  322. package/shared/notify-rules.d.mts +31 -0
  323. package/shared/notify-rules.mjs +55 -8
  324. package/shared/phone-files.mjs +28 -0
  325. package/shared/phone-settings.mjs +118 -0
  326. package/shared/phone-snapshot.d.mts +16 -0
  327. package/shared/phone-snapshot.mjs +348 -0
  328. package/shared/picture-formats.d.mts +1 -0
  329. package/shared/picture-formats.mjs +13 -0
  330. package/shared/provider-commands.mjs +10 -2
  331. package/shared/rank.d.mts +4 -1
  332. package/shared/rank.mjs +82 -19
  333. package/shared/relay-address.mjs +6 -0
  334. package/shared/relay-crypto.mjs +165 -0
  335. package/shared/repeats.d.mts +8 -0
  336. package/shared/repeats.mjs +33 -2
  337. package/shared/room-cards.mjs +167 -0
  338. package/shared/room-links.d.mts +4 -0
  339. package/shared/room-links.mjs +21 -0
  340. package/shared/runs-on.d.mts +3 -13
  341. package/shared/runs-on.mjs +31 -52
  342. package/shared/shipped-note.d.mts +4 -0
  343. package/shared/shipped-note.mjs +6 -0
  344. package/shared/shortcuts.mjs +24 -10
  345. package/shared/spawn-trouble.d.mts +4 -0
  346. package/shared/spawn-trouble.mjs +484 -30
  347. package/shared/team-rules.mjs +9 -2
  348. package/shared/teammates.d.mts +19 -0
  349. package/shared/teammates.mjs +756 -0
  350. package/shared/terminal-focus.d.mts +1 -0
  351. package/shared/terminal-focus.mjs +13 -0
  352. package/shared/terminal-input.d.mts +1 -0
  353. package/shared/terminal-input.mjs +30 -0
  354. package/shared/terminal-keys.d.mts +1 -0
  355. package/shared/terminal-keys.mjs +16 -0
  356. package/shared/terminal-size.d.mts +4 -0
  357. package/shared/terminal-size.mjs +29 -0
  358. package/shared/thread-cards.mjs +4 -1
  359. package/shared/usage.d.mts +12 -0
  360. package/shared/usage.mjs +56 -3
  361. package/shared/websites.d.mts +15 -0
  362. package/shared/websites.mjs +61 -0
  363. package/shared/where-it-runs-from.d.mts +43 -0
  364. package/shared/where-it-runs-from.mjs +155 -0
  365. package/shared/work-items.mjs +150 -29
  366. package/shared/work-lines.mjs +93 -245
  367. package/shared/xterm-invalid-range.d.mts +2 -0
  368. package/shared/xterm-invalid-range.mjs +58 -0
  369. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  370. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -0,0 +1,625 @@
1
+ // HER MAC'S SIDE OF THE PHONE WITH THE MAC SHUT (docs/cloud/PHONE-PLAN.md, step 1).
2
+ //
3
+ // For each project she put on her phone, this writes a readable copy of every
4
+ // thread worth carrying (shared/phone-snapshot.mjs) to her own rows in the
5
+ // cloud, and takes the commands her phone left while the Mac was away: a
6
+ // reply, a new thread, a snooze, a close. Each command goes through the very
7
+ // handler the window uses for the same thing, so a reply from the phone is a
8
+ // reply, with everything a reply does. Then it is marked taken, which is what
9
+ // turns "Waiting for your computer" on her phone into done.
10
+ //
11
+ // THIS COMPUTER IS ONE DEVICE (Codex's review of step 1, finding 4). Its
12
+ // projects online carry its own id, and it takes only the commands left on
13
+ // them, so a second computer signed into the same account neither takes this
14
+ // one's commands nor overwrites a project that happens to share a name. The id
15
+ // belongs to this installation and this store together (main/main.mjs
16
+ // phoneDevice), so a store copied to another computer gets another id
17
+ // (second review, finding 7).
18
+ //
19
+ // EVERYTHING IT SENDS IS SENT AS ONE PERSON (second review, finding 1). The
20
+ // inbox is made for whoever is signed in, names them on every write, and asks
21
+ // `alive()` before each one: once she signs out, a pass already running stops
22
+ // rather than carrying her projects into whoever signs in next.
23
+ //
24
+ // A COMMAND IS DONE ONCE (finding 3). Before it is done, that it is being done
25
+ // is written down here, and what it writes carries the command's own id
26
+ // (`fromPhone`). If this computer stopped half way, the next pass looks for a
27
+ // line or a row with that id before doing it again (second and third reviews:
28
+ // matching the words, or the moment, took an earlier "yes" for a second one).
29
+ // Closing and snoozing are the same twice over. Once done,
30
+ // the outcome is kept here before the cloud is told, so a mark that fails is
31
+ // retried and the command never runs twice. One that cannot be done is marked
32
+ // with why.
33
+ import fs from 'node:fs';
34
+ import path from 'node:path';
35
+ import crypto from 'node:crypto';
36
+ import os from 'node:os';
37
+ import { threadSnapshot, threadsWorthCarrying, snapshotKey, checkCommand, madeHere, hideSecrets, bytesOf, phoneChoices, phoneApproval, SNAPSHOT_LIMITS } from '../shared/phone-snapshot.mjs';
38
+ import { phoneSettingsOf, settingRoute } from '../shared/phone-settings.mjs';
39
+ import { createPhoneUploads, UPLOAD_LIMITS } from './phone-uploads.mjs';
40
+ import { createPhoneAlerts } from './phone-alerts.mjs';
41
+ import { PHONE_FILES_MAX, phoneFilesIn, phoneFileSafeName, phoneAttachmentRel } from '../shared/phone-files.mjs';
42
+ import { runOfClaim, cloudCard } from '../shared/cloud-kit.mjs';
43
+
44
+ // A file her phone sent with a reply or a new thread (shared/phone-files.mjs).
45
+ export { phoneFilesIn };
46
+ const HEARTBEAT_MS = 5 * 60_000;
47
+ class Refused extends Error {}
48
+
49
+ // How long a thread started on the phone is remembered as the row it became:
50
+ // long past any reply still waiting for it (second review, finding 8).
51
+ const MADE_KEPT_MS = 60 * 24 * 3600_000;
52
+ // A copy the cloud would not take is tried again, less and less often.
53
+ const RETRY_MS = [60_000, 5 * 60_000, 30 * 60_000, 3600_000];
54
+ // A running thread's step goes up at most this often, and its copy is sent
55
+ // again this often while it runs, so a phone can tell a Mac that went to sleep
56
+ // mid-run (cloud-bridge.ts LIVE_FOR_MS) from one still working.
57
+ const STEP_EVERY_MS = 30_000;
58
+ const STILL_RUNNING_MS = 2 * 60_000;
59
+ const SWEEP_EVERY_MS = 3600_000;
60
+ // How far back a computer looks for runs the cloud started for it, and how
61
+ // long it remembers one it made its own: past the bucket's week of results.
62
+ const ADOPT_WITHIN_MS = 7 * 24 * 3600_000;
63
+ const ADOPTED_KEPT_MS = 8 * 24 * 3600_000;
64
+ const ADOPT_TRIES = 3;
65
+ // Reading the settings parses Codex's model cache (199KB) on a Mac with both agents.
66
+ const SETTINGS_EVERY_MS = 60_000;
67
+
68
+ export class SignedOut extends Error {}
69
+
70
+ export function createPhoneInbox({
71
+ backend,
72
+ // Who this inbox sends as, and whether they still are.
73
+ owner,
74
+ alive = () => true,
75
+ // This computer's id online; made here and kept with the rest when none is given.
76
+ device: given = null,
77
+ // The projects on her phone, read fresh each pass: [{ slug, name, dir?, repoPath? }].
78
+ projects,
79
+ listItems, // (slug) -> folded rows
80
+ historyOf, // (slug, id, { all }) -> the thread's history lines (store.readHistory); all of them when asked
81
+ apply, // async (command) -> the handler's answer; throws to refuse
82
+ stateFile,
83
+ home = os.homedir(),
84
+ now = Date.now,
85
+ log = () => {},
86
+ // How a thread's copy is made; a test counts the calls.
87
+ build = threadSnapshot,
88
+ // (slug, id) -> { since, where: 'mac' | 'cloud', doing: [{ label, startedAt }] } while an agent runs on it, else null.
89
+ liveOf = () => null,
90
+ // The permission cards waiting on her, each with the fingerprint of the card as drawn: [{ ...request, card }].
91
+ approvalsOf = () => [],
92
+ // async () -> the window's settings model (main/settings.mjs readSettings); none sends no settings.
93
+ readSettings = null,
94
+ version = '',
95
+ // This computer as her phone names it.
96
+ name = os.hostname().replace(/\.local$/i, ''),
97
+ // How long since she last touched this Mac; none sends no alerts (main/phone-alerts.mjs).
98
+ idleMs = null,
99
+ // How an alert reaches Expo; a test stands in for it.
100
+ sendPush,
101
+ // THE PHONE WITH THE LID SHUT (shared/cloud-kit.mjs). Told which projects
102
+ // are on her phone each pass; asked whether this computer leaves its
103
+ // threads ready for the cloud (said on the project); and handed each run
104
+ // the cloud server started for a reply, to make it the thread's own
105
+ // (Supervisor#adoptCloudRun): 'adopted', 'no-kit' or 'busy'.
106
+ carrying = null,
107
+ kitsFor = null,
108
+ adopt = null,
109
+ // A reply the server fed a run after this computer took it; whether that run is home.
110
+ cloudReplies = null,
111
+ runHome = null,
112
+ // A NEW THREAD the cloud started from the phone with the lid shut: asked
113
+ // each pass to keep the projects' new-thread kits, and handed each such run
114
+ // to make its row and folder and join it (Supervisor#adoptCloudNewThread).
115
+ newThreadKits = null,
116
+ adoptNew = null,
117
+ }) {
118
+ const state = readState(stateFile);
119
+ const save = () => writeState(stateFile, state);
120
+ if (!given && !state.device) { state.device = crypto.randomBytes(6).toString('hex'); save(); }
121
+ const device = given ?? state.device;
122
+ if (!/^[0-9a-f]{12}$/.test(device)) throw new Error('a computer id is twelve hex digits');
123
+ // WHAT WAS SENT WAS SENT AS ONE COMPUTER (Codex's third review, finding 2).
124
+ // Under another id (a store copied here, or the id made a new way) nothing
125
+ // is online yet, so it all goes again; the other id's rows are that
126
+ // computer's, and are left to it.
127
+ // Which rows the phone's new threads became is about this store, not the
128
+ // id, and is kept (Codex's fourth review, finding 3).
129
+ if (state.forDevice !== device) {
130
+ state.forDevice = device;
131
+ state.sent = {};
132
+ state.failed = {};
133
+ state.uploaded = {};
134
+ save();
135
+ }
136
+ const still = () => { if (!alive()) throw new SignedOut('signed out'); };
137
+ // The files threads name, when this cloud can hold them (main/phone-uploads.mjs).
138
+ const uploads = backend.putPhoneFile ? createPhoneUploads({ backend, owner, device, alive, state, save, home, now, log }) : null;
139
+ const alerts = idleMs && backend.pushDevices ? createPhoneAlerts({ backend, owner, device, state, save, now, log, idleMs, send: sendPush, still }) : null;
140
+ let swept = 0;
141
+ const homeWritten = (p) => (p && home && (p === home || p.startsWith(`${home}/`)) ? `~${p.slice(home.length)}` : p ?? null);
142
+
143
+ // WHAT EACH THREAD'S COPY WAS LAST BUILT FROM, so a pass skips building the
144
+ // copies of threads that did not change. Building them is the costly part of
145
+ // a pass (261 of 361 ms for 300 threads of the real agentbox-team ledger,
146
+ // 2026-10-14), and it runs on the thread that draws the window every few
147
+ // seconds. A copy is made of the row and its lines and nothing else, so the
148
+ // same row and lines make the same copy.
149
+ const built = new Map();
150
+ const inputsOf = (item, history) => crypto.createHash('sha1').update(JSON.stringify(item)).update('\0').update(JSON.stringify(history)).digest('base64');
151
+ // What each running thread's copy last said about the run, by thread.
152
+ const lives = new Map();
153
+ const liveNow = (sk, slug, id) => {
154
+ let raw = null;
155
+ try { raw = liveOf(slug, id); } catch { /* the supervisor's trouble, not the phone's */ }
156
+ if (!raw) { lives.delete(sk); return null; }
157
+ const doing = (Array.isArray(raw.doing) ? raw.doing : []).slice(0, 3).map((d) => {
158
+ const label = hideSecrets(String(d?.label ?? ''), { home }).replace(/\s+/g, ' ').trim();
159
+ return { label: label.length > 160 ? `${label.slice(0, 159)}…` : label, startedAt: Number(d?.startedAt) || now() };
160
+ });
161
+ const was = lives.get(sk);
162
+ const same = was && was.since === raw.since && was.where === raw.where;
163
+ const steps = (l) => JSON.stringify(l.doing.map((d) => d.label));
164
+ if (same && now() - was.at < STEP_EVERY_MS) return was;
165
+ if (same && now() - was.at < STILL_RUNNING_MS && steps(was) === steps({ doing })) return was;
166
+ const live = { since: Number(raw.since) || now(), where: raw.where === 'cloud' ? 'cloud' : 'mac', doing, at: now() };
167
+ lives.set(sk, live);
168
+ return live;
169
+ };
170
+ // The window gets a turn between projects and every few threads: 300 threads
171
+ // of one project in one go held it for up to half a second.
172
+ const breathe = () => new Promise((resolve) => setImmediate(resolve));
173
+ const THREADS_PER_TURN = 20;
174
+
175
+ const beats = new Map();
176
+ async function publish() {
177
+ let sent = 0;
178
+ const chosen = projects();
179
+ try { carrying?.({ device, slugs: chosen.map((p) => p.slug) }); } catch { /* the supervisor's trouble, not the phone's */ }
180
+ try { newThreadKits?.({ device, slugs: chosen.map((p) => p.slug) }); } catch { /* the supervisor's trouble, not the phone's */ }
181
+ let asks = [];
182
+ try { asks = approvalsOf() ?? []; } catch { /* the cards' trouble, not the phone's */ }
183
+ const known = new Set(chosen.map((p) => p.slug));
184
+ for (const p of chosen) {
185
+ await breathe();
186
+ const key = `project:${p.slug}`;
187
+ let cloud = false;
188
+ try { cloud = !!kitsFor?.(p.slug); } catch { /* says no */ }
189
+ const said = cloud ? `${p.name}\0cloud` : p.name;
190
+ // Written again every few minutes unchanged: its moment is how the phone knows this Mac is still here.
191
+ if (state.sent[key] !== said || !(now() - (beats.get(key) ?? -Infinity) < HEARTBEAT_MS)) {
192
+ still();
193
+ await backend.putPhoneProject({ owner, device, project: p.slug, name: p.name, ...(kitsFor ? { cloud } : {}) });
194
+ beats.set(key, now());
195
+ if (state.sent[key] !== said) { state.sent[key] = said; save(); }
196
+ }
197
+ const rows = threadsWorthCarrying(listItems(p.slug), { now: now() });
198
+ const batch = [];
199
+ const here = new Set();
200
+ for (const item of rows) {
201
+ if (here.size && here.size % THREADS_PER_TURN === 0) await breathe();
202
+ here.add(item.id);
203
+ const sk = `${p.slug}:${item.id}`;
204
+ const history = historyOf(p.slug, item.id);
205
+ const inputs = inputsOf(item, history);
206
+ const live = liveNow(sk, p.slug, item.id);
207
+ const waitingOn = asks.filter((a) => a.product === p.slug && a.item === item.id && a.card).map((a) => phoneApproval(a, a.card, { home }));
208
+ const askKey = JSON.stringify(waitingOn);
209
+ const was = built.get(sk);
210
+ // Only the run or its files moved: the copy of the row and its lines is the one already made.
211
+ let base = was?.inputs === inputs ? was.base : null;
212
+ const made = () => (base ??= build(item, history, SNAPSHOT_LIMITS, { home }));
213
+ const files = uploads && p.dir ? uploads.manifest(sk, made, { dir: p.dir, repoPath: p.repoPath, updatedAt: item.updatedAt ?? 0, base: inputs }) : null;
214
+ if (was && was.inputs === inputs && was.live === live && was.files === files && was.askKey === askKey && state.sent[sk] === was.key) continue;
215
+ let snap = live ? { ...made(), live } : made();
216
+ if (waitingOn.length) snap = { ...snap, approvals: waitingOn };
217
+ // Where the files are on this Mac, written from her home as the words are, so the phone can follow a path the words give in full.
218
+ if (files) {
219
+ const attached = { dir: homeWritten(p.dir), ...(p.repoPath ? { repo: homeWritten(p.repoPath) } : {}), ...files };
220
+ if (bytesOf(snap) + bytesOf(attached) < SNAPSHOT_LIMITS.bytes) snap = { ...snap, attached };
221
+ }
222
+ const k = snapshotKey(snap);
223
+ built.set(sk, { inputs, key: k, live, files, askKey, base: live || files || waitingOn.length ? base : null });
224
+ if (state.sent[sk] === k) continue;
225
+ const failed = state.failed[sk];
226
+ if (failed && failed.key === k && now() < failed.next) continue;
227
+ batch.push({ item: item.id, snapshot: snap, key: k });
228
+ }
229
+ sent += await sendAll(p.slug, batch);
230
+ // Threads no longer carried (archived long ago, or gone) leave the phone.
231
+ const gone = Object.keys(state.sent).filter((k) => k.startsWith(`${p.slug}:`) && !here.has(k.slice(p.slug.length + 1)));
232
+ if (gone.length) {
233
+ still();
234
+ await backend.dropPhoneThreads(device, p.slug, gone.map((k) => k.slice(p.slug.length + 1)), owner);
235
+ for (const k of gone) { delete state.sent[k]; delete state.failed[k]; delete state.alerted[k]; built.delete(k); }
236
+ save();
237
+ }
238
+ }
239
+ // A project taken off her phone takes its threads with it.
240
+ for (const k of Object.keys(state.sent).filter((k) => k.startsWith('project:') && !known.has(k.slice(8)))) {
241
+ const slug = k.slice(8);
242
+ still();
243
+ await backend.dropPhoneProject(device, slug, owner);
244
+ for (const t of Object.keys(state.sent).filter((t) => t.startsWith(`${slug}:`))) { delete state.sent[t]; built.delete(t); }
245
+ for (const t of Object.keys(state.failed).filter((t) => t.startsWith(`${slug}:`))) delete state.failed[t];
246
+ delete state.sent[k];
247
+ save();
248
+ }
249
+ uploads?.kick();
250
+ if (uploads && now() - swept > SWEEP_EVERY_MS) { swept = now(); try { await uploads.sweep(); } catch (err) { log(`phone: could not clear old files: ${err?.message ?? err}`); } }
251
+ return sent;
252
+ }
253
+
254
+ // THIS COMPUTER'S SETTINGS FOR HER PHONE'S SETTINGS PAGES. A failure waits
255
+ // for the next read, a minute on, and is said once until it changes.
256
+ const settings = { readAt: -Infinity, sentAt: -Infinity, key: null, trouble: null };
257
+ const computer = String(name || 'This computer').slice(0, 200);
258
+ const troubled = (what, err) => {
259
+ const line = `phone: could not ${what} your settings: ${err?.message ?? err}`;
260
+ if (settings.trouble !== line) log(line);
261
+ settings.trouble = line;
262
+ };
263
+ async function publishSettings(soon) {
264
+ if (!readSettings || !backend.putPhoneSettings) return false;
265
+ if (!soon && now() - settings.readAt < SETTINGS_EVERY_MS) return false;
266
+ settings.readAt = now();
267
+ let carried = null;
268
+ try { carried = phoneSettingsOf(await readSettings(), { version }); } catch (err) { troubled('read', err); return false; }
269
+ if (!carried) return false;
270
+ const key = JSON.stringify(carried);
271
+ if (key === settings.key && now() - settings.sentAt < HEARTBEAT_MS) return false;
272
+ still();
273
+ try {
274
+ await backend.putPhoneSettings({ owner, device, name: computer, settings: carried });
275
+ } catch (err) {
276
+ if (err instanceof SignedOut) throw err;
277
+ troubled('send', err);
278
+ return false;
279
+ }
280
+ Object.assign(settings, { key, sentAt: now(), trouble: null });
281
+ return true;
282
+ }
283
+
284
+ // In one go when it can; one at a time when the cloud refuses the lot, so
285
+ // a thread it will not take never holds up the others (finding 9). A copy
286
+ // that fails is NOT counted as sent: it is tried again, later each time
287
+ // (second review, finding 5), and at once when it changes.
288
+ async function sendAll(slug, batch) {
289
+ if (!batch.length) return 0;
290
+ const sk = (b) => `${slug}:${b.item}`;
291
+ try {
292
+ still();
293
+ await backend.putPhoneThreads(device, slug, batch.map(({ item, snapshot }) => ({ item, snapshot })), owner);
294
+ for (const b of batch) { state.sent[sk(b)] = b.key; delete state.failed[sk(b)]; }
295
+ save();
296
+ return batch.length;
297
+ } catch (err) {
298
+ if (err instanceof SignedOut) throw err;
299
+ if (batch.length === 1) {
300
+ const b = batch[0];
301
+ const tries = state.failed[sk(b)]?.key === b.key ? state.failed[sk(b)].tries + 1 : 1;
302
+ state.failed[sk(b)] = { key: b.key, tries, next: now() + RETRY_MS[Math.min(tries, RETRY_MS.length) - 1] };
303
+ save();
304
+ log(`phone: could not send ${b.item} (try ${tries}): ${err?.message ?? err}`);
305
+ return 0;
306
+ }
307
+ let n = 0;
308
+ for (const b of batch) n += await sendAll(slug, [b]);
309
+ return n;
310
+ }
311
+ }
312
+
313
+ // The command with each file its phone sent saved in the project's
314
+ // attachments, and linked there. A file that is missing or is not what its
315
+ // name says refuses the command; trouble reaching the cloud leaves it for
316
+ // the next pass.
317
+ async function withPhoneFiles(cmd) {
318
+ const field = cmd.kind === 'reply' ? 'text' : cmd.kind === 'new' ? 'body' : null;
319
+ const words = field && typeof cmd.payload?.[field] === 'string' ? cmd.payload[field] : '';
320
+ const found = phoneFilesIn(words);
321
+ if (!found.length) return cmd;
322
+ if (!backend.getPhoneFile) throw new Refused('this computer cannot take files from your phone');
323
+ if (found.length > PHONE_FILES_MAX) throw new Refused(`more than ${PHONE_FILES_MAX} files at once`);
324
+ const dir = projects().find((p) => p.slug === cmd.project)?.dir;
325
+ if (!dir) throw new Refused('this project has no folder for files');
326
+ let out = words;
327
+ for (const f of found) {
328
+ const data = await backend.getPhoneFile({ owner, device, name: f.name });
329
+ if (!data) throw new Refused('a file from your phone did not reach this computer');
330
+ const hash = crypto.createHash('sha256').update(data).digest('hex');
331
+ if (data.length > UPLOAD_LIMITS.fileBytes || f.name !== `${hash}${f.name.slice(64)}`) throw new Refused('a file from your phone was not the one it was sent as');
332
+ // Where the cloud server puts the same file for a turn it runs, so both link one.
333
+ const rel = phoneAttachmentRel(f);
334
+ const abs = path.join(dir, rel);
335
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
336
+ if (!fs.existsSync(abs) || fs.statSync(abs).size !== data.length) fs.writeFileSync(abs, data);
337
+ out = out.replace(f.link, () => `${f.picture ? '!' : ''}[${phoneFileSafeName(f)}](${rel})`);
338
+ // The same file the uploader would send for the thread, already there.
339
+ if (uploads) { state.uploaded[f.name] = { bytes: data.length, named: now() }; save(); }
340
+ }
341
+ return { ...cmd, payload: { ...cmd.payload, [field]: out } };
342
+ }
343
+
344
+ // What a command already did, if this computer did it before it stopped:
345
+ // the line or the row that carries the command's own id (`fromPhone`,
346
+ // shared/work-items.mjs). Nothing else counts, however alike its words.
347
+ function landed(cmd, item) {
348
+ if (cmd.kind === 'reply') return historyOf(cmd.project, item, { all: true }).some((l) => l?.patch?.fromPhone === cmd.id) ? item : null;
349
+ if (cmd.kind === 'new') return listItems(cmd.project).find((i) => i.fromPhone === cmd.id)?.id ?? null;
350
+ return null;
351
+ }
352
+
353
+ async function takeCommands() {
354
+ const slugs = projects().map((p) => p.slug);
355
+ still();
356
+ const waiting = (await backend.waitingPhoneCommands(device)).sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
357
+ let taken = 0;
358
+ let changedSettings = false;
359
+ for (const cmd of waiting) {
360
+ still();
361
+ // The cloud started this one with the lid shut; it is joined, never done again (`adoptCloudRuns`).
362
+ if (cmd.claimedBy) continue;
363
+ // Her answer to a card the cloud put on the phone is the cloud server's to give (cloud/gateway/phone.mjs).
364
+ if (cmd.kind === 'approve' && cloudCard(cmd.payload?.approval)) continue;
365
+ let outcome = state.done[cmd.id];
366
+ if (!outcome) {
367
+ // A reply to a thread started on the phone goes to the row it became.
368
+ const item = cmd.item && state.made[cmd.item] ? state.made[cmd.item].id : cmd.item;
369
+ const resolved = { ...cmd, item };
370
+ const why = checkCommand(resolved, { projects: slugs, hasItem: (project, id) => listItems(project).some((i) => i.id === id) });
371
+ if (why) outcome = `refused: ${why}`;
372
+ else {
373
+ // Claimed in the cloud first: after this the phone cannot take it
374
+ // back, and if she already did, it is not done (third review, 3).
375
+ const claim = await backend.claimPhoneCommand(cmd.id, owner);
376
+ const already = claim === 'gone' || claim === 'cloud' ? null : state.doing[cmd.id] || claim === 'ours' ? landed(resolved, item) : null;
377
+ if (claim === 'gone' || claim === 'cloud') {
378
+ delete state.doing[cmd.id];
379
+ save();
380
+ continue;
381
+ }
382
+ if (already) {
383
+ if (cmd.kind === 'new') state.made[madeHere(cmd.id)] = { id: already, at: now() };
384
+ outcome = 'done';
385
+ } else {
386
+ state.doing[cmd.id] = { at: now() };
387
+ save();
388
+ let ready = null;
389
+ try { ready = await withPhoneFiles(resolved); } catch (err) {
390
+ if (err instanceof SignedOut) throw err;
391
+ // The cloud not answering just now: this one waits for the next pass, the rest go on.
392
+ if (!(err instanceof Refused)) { log(`phone: could not take a file for ${cmd.id} yet: ${err?.message ?? err}`); continue; }
393
+ outcome = `refused: ${err.message}`;
394
+ }
395
+ if (ready) {
396
+ try {
397
+ const out = await apply(ready);
398
+ if (cmd.kind === 'new' && typeof out?.id === 'string') state.made[madeHere(cmd.id)] = { id: out.id, at: now() };
399
+ if (cmd.kind === 'setting') changedSettings = true;
400
+ outcome = 'done';
401
+ } catch (err) {
402
+ outcome = `refused: ${String(err?.message || err).slice(0, 200)}`;
403
+ }
404
+ }
405
+ }
406
+ }
407
+ // Remembered here before the cloud is told, so a failed mark never
408
+ // means a second reply.
409
+ state.done[cmd.id] = outcome;
410
+ delete state.doing[cmd.id];
411
+ for (const [k, v] of Object.entries(state.made)) if (!(now() - (v?.at ?? 0) < MADE_KEPT_MS)) delete state.made[k];
412
+ save();
413
+ }
414
+ still();
415
+ await backend.markPhoneCommand(cmd.id, outcome, owner);
416
+ delete state.done[cmd.id];
417
+ save();
418
+ taken += 1;
419
+ }
420
+ return { taken, changedSettings };
421
+ }
422
+
423
+ // RUNS THE CLOUD STARTED WHILE THIS COMPUTER WAS SHUT, made the threads'
424
+ // own once each, with every reply the run took (a run stays open for her
425
+ // follow-ups, so several commands share one claim): her replies go on the
426
+ // row in order and the run is joined or brought home
427
+ // (Supervisor#adoptCloudRun). A reply the server fed it after that goes on
428
+ // the row too. Its commands are marked taken once it is home. A run that
429
+ // cannot be made its own after a few tries is said once and let go.
430
+ const adoptTrouble = new Set();
431
+ const sayOnce = (kind, line) => { if (!adoptTrouble.has(kind)) log(line); adoptTrouble.add(kind); };
432
+ async function adoptCloudRuns() {
433
+ if (!adopt || !backend.cloudPhoneCommands) return 0;
434
+ still();
435
+ const claimed = (await backend.cloudPhoneCommands(device, new Date(now() - ADOPT_WITHIN_MS).toISOString()))
436
+ .sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
437
+ const runs = new Map();
438
+ for (const cmd of claimed) {
439
+ const runId = runOfClaim(cmd.claimedBy);
440
+ if (!runId) continue;
441
+ if (!runs.has(runId)) runs.set(runId, []);
442
+ runs.get(runId).push(cmd);
443
+ }
444
+ let n = 0;
445
+ for (const [runId, cmds] of runs) {
446
+ still();
447
+ const rec = state.adopted[runId] ?? { at: now(), tries: 0, placed: [], marked: [] };
448
+ if (rec.over) continue;
449
+ const first = cmds[0];
450
+ const project = first.project;
451
+ // A run the cloud started for a new thread runs under the id the phone gave it.
452
+ const started = first.kind === 'new' ? first : null;
453
+ const item = started ? madeHere(started.id) : first.item && state.made[first.item] ? state.made[first.item].id : first.item;
454
+ const fresh = cmds.filter((c) => c.kind === 'reply' && !rec.placed.includes(c.id));
455
+ const replies = [];
456
+ for (const c of fresh) {
457
+ let text = typeof c.payload?.text === 'string' ? c.payload.text : '';
458
+ try { text = (await withPhoneFiles({ ...c, item })).payload.text; } catch (err) { if (err instanceof SignedOut) throw err; }
459
+ replies.push(text);
460
+ }
461
+ if (!rec.done) {
462
+ let out = 'not-a-reply';
463
+ if (started && adoptNew) {
464
+ const p = started.payload ?? {};
465
+ const title = String(p.title ?? '').trim() || String(p.body ?? '').trim().split('\n')[0].slice(0, 200);
466
+ // Its files saved here where the cloud put them, so the row links them.
467
+ let body = String(p.body ?? '');
468
+ try { body = (await withPhoneFiles({ ...started, kind: 'new' })).payload.body ?? body; } catch (err) { if (err instanceof SignedOut) throw err; }
469
+ try { out = await adoptNew({ project, item, runId, title, body, command: started.id, replies, choices: phoneChoices('new', p) }); } catch (err) {
470
+ out = 'failed';
471
+ sayOnce(`adopt:${err?.message}`, `phone: could not take in the new thread the cloud started (${item}): ${err?.message ?? err}`);
472
+ }
473
+ } else if (item && fresh.length) {
474
+ try { out = await adopt({ project, item, runId, replies, commands: fresh.map((c) => c.id) }); } catch (err) {
475
+ out = 'failed';
476
+ sayOnce(`adopt:${err?.message}`, `phone: could not join the cloud's run for ${item}: ${err?.message ?? err}`);
477
+ }
478
+ }
479
+ if (out === 'busy') { sayOnce(`busy:${runId}`, `phone: the cloud's run for ${item} waits for other cloud work to come home first`); continue; }
480
+ rec.tries += 1;
481
+ if (out === 'failed' && rec.tries < ADOPT_TRIES) { state.adopted[runId] = rec; save(); continue; }
482
+ if (out === 'no-kit') sayOnce(`kit:${runId}`, `phone: no kit here for ${item}, so her reply runs on this computer`);
483
+ Object.assign(rec, { at: now(), done: true, home: out !== 'adopted' });
484
+ rec.placed.push(...fresh.map((c) => c.id));
485
+ n += 1;
486
+ } else if (fresh.length && item) {
487
+ try { await cloudReplies?.({ project, item, runId, replies }); } catch (err) { sayOnce(`late:${runId}`, `phone: could not put a late reply on ${item}: ${err?.message ?? err}`); }
488
+ rec.placed.push(...fresh.map((c) => c.id));
489
+ }
490
+ if (!rec.home) {
491
+ try { rec.home = !runHome || !!runHome({ project, item, runId }); } catch { rec.home = false; }
492
+ }
493
+ if (rec.home) {
494
+ for (const c of cmds) {
495
+ if (c.takenAt || rec.marked.includes(c.id)) continue;
496
+ try { still(); await backend.markPhoneCommand(c.id, 'done', owner); rec.marked.push(c.id); } catch (err) { if (err instanceof SignedOut) throw err; sayOnce('mark', `phone: could not mark a cloud reply taken: ${err?.message ?? err}`); }
497
+ }
498
+ if (cmds.every((c) => c.takenAt || rec.marked.includes(c.id))) rec.over = true;
499
+ }
500
+ state.adopted[runId] = rec;
501
+ save();
502
+ }
503
+ for (const [k, v] of Object.entries(state.adopted)) if (!(now() - (v?.at ?? 0) < ADOPTED_KEPT_MS)) delete state.adopted[k];
504
+ return n;
505
+ }
506
+
507
+ // A thread's row when its copy is on the phone, each project read once a pass.
508
+ let alertTrouble = null;
509
+ const carriedNow = () => {
510
+ const rowsOf = new Map();
511
+ return (slug, id) => {
512
+ if (!state.sent[`${slug}:${id}`]) return null;
513
+ if (!rowsOf.has(slug)) rowsOf.set(slug, new Map(listItems(slug).map((i) => [i.id, i])));
514
+ return rowsOf.get(slug).get(id) ?? null;
515
+ };
516
+ };
517
+
518
+ return {
519
+ device,
520
+ /** The uploads going on behind the passes; a test waits on them. */
521
+ filesSettled: () => uploads?.kick() ?? Promise.resolve(),
522
+ /** What the desktop's banner was told arrived (main/notify.mjs), and the inbox then: [{ id, product }]. */
523
+ arrivals: (list, needsYou) => alerts?.arrived(list, needsYou),
524
+ async syncOnce() {
525
+ try {
526
+ const { taken, changedSettings } = await takeCommands();
527
+ let adopted = 0;
528
+ try { adopted = await adoptCloudRuns(); } catch (err) {
529
+ if (err instanceof SignedOut) throw err;
530
+ sayOnce(`read:${err?.message}`, `phone: could not read what the cloud took: ${err?.message ?? err}`);
531
+ }
532
+ const sent = await publish();
533
+ await publishSettings(changedSettings);
534
+ if (alerts) {
535
+ if (taken) alerts.phoneUsed();
536
+ try { await alerts.pass(carriedNow()); alertTrouble = null; } catch (err) {
537
+ if (err instanceof SignedOut) throw err;
538
+ const line = `phone: could not alert: ${err?.message ?? err}`;
539
+ if (line !== alertTrouble) log(line);
540
+ alertTrouble = line;
541
+ }
542
+ }
543
+ if (taken || sent || adopted) log(`phone: took ${taken}, sent ${sent}${adopted ? `, joined ${adopted} from the cloud` : ''}`);
544
+ return { taken, sent, adopted };
545
+ } catch (err) {
546
+ if (err instanceof SignedOut) return { taken: 0, sent: 0, stopped: true };
547
+ throw err;
548
+ }
549
+ },
550
+ };
551
+ }
552
+
553
+ // THIS COMPUTER'S ID ONLINE: this installation's own random id, kept in the
554
+ // app's own folder and never in the store, joined with which store it runs.
555
+ // A store copied to another computer, or restored there, is another device
556
+ // (Codex's second review, finding 7); two stores on one computer are two.
557
+ export function phoneDevice(appDataDir, storeRoot) {
558
+ const file = path.join(appDataDir, 'phone-install-id');
559
+ let id = null;
560
+ try { id = fs.readFileSync(file, 'utf8').trim(); } catch { /* first time */ }
561
+ if (!/^[0-9a-f]{32}$/.test(id ?? '')) {
562
+ id = crypto.randomBytes(16).toString('hex');
563
+ fs.mkdirSync(appDataDir, { recursive: true });
564
+ fs.writeFileSync(file, id, { mode: 0o600 });
565
+ }
566
+ return crypto.createHash('sha256').update(`${id}\0${path.resolve(storeRoot)}`).digest('hex').slice(0, 12);
567
+ }
568
+
569
+ // A COMMAND, DONE BY THE HANDLER THE WINDOW USES FOR THE SAME THING
570
+ // (main/ipc.mjs), reached the way the phone link reaches them: the Map
571
+ // main/main.mjs files every channel in. A handler that answers `ok: false`
572
+ // turned it down, and that is what the phone is told.
573
+ const ROUTES = {
574
+ reply: (c) => {
575
+ const { unschedule, ...picked } = phoneChoices('reply', c.payload);
576
+ return ['zero:answer', { product: c.project, id: c.item, answer: c.payload.text, fromPhone: c.id, ...picked },
577
+ // As the window does: a reply brings a thread put off to a time back now.
578
+ unschedule ? ['zero:schedule', { product: c.project, id: c.item, runAt: 0 }] : null];
579
+ },
580
+ close: (c) => ['zero:answer', { product: c.project, id: c.item, status: 'done' }],
581
+ snooze: (c) => ['zero:snooze', { product: c.project, id: c.item, snoozedUntil: c.payload.until }],
582
+ stop: (c) => ['zero:stop-session', { product: c.project, id: c.item }],
583
+ 'run-now': (c) => ['zero:run-now', { product: c.project, id: c.item }],
584
+ 'send-now': (c) => ['zero:send-now', { product: c.project, id: c.item }],
585
+ reopen: (c) => ['zero:reopen', { product: c.project, id: c.item }],
586
+ schedule: (c) => ['zero:schedule', { product: c.project, id: c.item, runAt: c.payload.runAt }],
587
+ approve: (c) => ['zero:approve', { id: c.payload.approval, allow: c.payload.allow === true, ...(typeof c.payload.note === 'string' ? { note: c.payload.note } : {}), card: c.payload.card }],
588
+ new: (c) => ['zero:compose', { product: c.project, title: c.payload.title ?? '', body: c.payload.body ?? '', fromPhone: c.id, ...phoneChoices('new', c.payload) }],
589
+ };
590
+ export function applyThrough(channels) {
591
+ const routeOf = async (cmd) => {
592
+ if (cmd.kind !== 'setting') return ROUTES[cmd.kind]?.(cmd) ?? [];
593
+ const read = channels.get('zero:settings-read');
594
+ if (!read) return [];
595
+ return settingRoute(cmd.payload, await read({ sender: null }));
596
+ };
597
+ return async (cmd) => {
598
+ const [channel, args, then] = await routeOf({ ...cmd, payload: cmd.payload || {} });
599
+ const fn = channel && channels.get(channel);
600
+ if (!fn) throw new Error('this computer cannot do that yet');
601
+ const out = await fn({ sender: null }, args);
602
+ if (out && out.ok === false) throw new Error(out.error || (out.reason ? `the app turned it down (${out.reason})` : 'the app turned it down'));
603
+ // Done already; what follows is tidying, and its trouble is not the command's.
604
+ if (then && channels.get(then[0])) { try { await channels.get(then[0])({ sender: null }, then[1]); } catch { /* the schedule stays */ } }
605
+ return out;
606
+ };
607
+ }
608
+
609
+ function readState(file) {
610
+ try {
611
+ const s = JSON.parse(fs.readFileSync(file, 'utf8'));
612
+ const made = {};
613
+ for (const [k, v] of Object.entries(s.made || {})) made[k] = typeof v === 'string' ? { id: v, at: Date.now() } : v;
614
+ return { device: typeof s.device === 'string' && /^[0-9a-f]{12}$/.test(s.device) ? s.device : null, forDevice: s.forDevice ?? null, sent: s.sent || {}, failed: s.failed || {}, done: s.done || {}, doing: s.doing || {}, made, uploaded: s.uploaded && typeof s.uploaded === 'object' ? s.uploaded : {}, alerted: s.alerted && typeof s.alerted === 'object' ? s.alerted : {}, adopted: s.adopted && typeof s.adopted === 'object' ? s.adopted : {} };
615
+ } catch {
616
+ return { device: null, forDevice: null, sent: {}, failed: {}, done: {}, doing: {}, made: {}, uploaded: {}, alerted: {}, adopted: {} };
617
+ }
618
+ }
619
+
620
+ function writeState(file, state) {
621
+ fs.mkdirSync(path.dirname(file), { recursive: true });
622
+ const tmp = `${file}.${process.pid}.tmp`;
623
+ fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
624
+ fs.renameSync(tmp, file);
625
+ }