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,143 @@
1
+ // THE WAY OUT, AND NOTHING ON IT MAY KILL THE APP (w-60c8ddbab6, 2026-10-15).
2
+ //
3
+ // Quitting used to be six bare `app.on('before-quit', ...)` listeners and one
4
+ // `will-quit` that started the analytics flush and did not wait for it. Both
5
+ // halves of that cost us the end of every session.
6
+ //
7
+ // ONE STEP THAT THROWS ENDED THE WHOLE QUIT. On 0.1.13 the phone step did:
8
+ // closing the main window nulls `window`, and `phone.close()` makes the relay
9
+ // report the change back to a handler that asked the window whether it was
10
+ // destroyed (main/main.mjs, the `zero:phone-changed` line). An EventEmitter
11
+ // stops calling listeners at the first one that throws, `main/crash-report.mjs`
12
+ // rethrows an uncaught main error on purpose, and the process then dies
13
+ // fatally. Measured on an Electron harness: `updater.stop()` never ran,
14
+ // `will-quit` never fired, `analytics.flush()` never ran, and `quit` never
15
+ // came. The window's own guard is fixed where it lives, but the shape was the
16
+ // real fault: a quit built so that any one step can swallow the rest is a quit
17
+ // that will do this again for some other reason.
18
+ //
19
+ // AND THE FLUSH WAS FIRE AND FORGET. `will-quit` without `preventDefault` means
20
+ // the process goes as soon as the listeners return, so a batch handed to
21
+ // `posthog-node` a moment earlier is a POST that never leaves. Measured the
22
+ // same way: the flush resolved AFTER `process.on('exit')` had already run, and
23
+ // it only resolved at all because the harness's destination was in the same
24
+ // process. Against us.i.posthog.com it is a race the app loses.
25
+ //
26
+ // WHICH MATTERS BECAUSE THE END OF A SESSION IS WHAT WE READ RETENTION FROM.
27
+ // Of the fourteen installs that arrived on 0.1.13 or 0.1.14, thirteen opened
28
+ // the app once and never opened it again (PostHog, 2026-10-15), so for thirteen
29
+ // of fourteen people the batch dropped here is the LAST thing we would ever
30
+ // have heard, and the crash report explaining it is still sitting on their disk
31
+ // waiting for a launch that is not coming.
32
+
33
+ /** How long the quit is held open for the last batch of counts, and no longer. */
34
+ export const FLUSH_DEADLINE_MS = 2000;
35
+
36
+ /**
37
+ * How long a quit we asked for is given before the process is ended outright.
38
+ * Longer than the flush above on purpose: a quit holding itself open for the
39
+ * last counts is a quit that is working. */
40
+ export const EXIT_DEADLINE_MS = 8000;
41
+
42
+ /**
43
+ * How long the second ask gets, once the counts are out and there is nothing
44
+ * left to wait for. On Linux a quit asked for with a window still open — the
45
+ * menu's Quit, the tray's Quit — ignores it outright
46
+ * (tests/a-quit-held-for-the-last-counts-still-ends-the-app), and by then the
47
+ * window is gone and there is nothing left to show anybody. */
48
+ export const SECOND_QUIT_DEADLINE_MS = 2000;
49
+
50
+ /**
51
+ * A STEP ON THE WAY OUT, which runs once and cannot throw.
52
+ *
53
+ * `name` is in the warning rather than in a comment, because the one thing
54
+ * somebody reading a log needs to know is which step gave up. A swallow with
55
+ * nothing said is how this bug lived through three releases.
56
+ *
57
+ * ONCE, not once per `app.quit()`. Holding the quit for the flush below means
58
+ * calling `app.quit()` a second time, which emits `before-quit` again, and a
59
+ * `killAll` or a `close` run twice is at best wasted and at worst a second
60
+ * teardown of something already gone.
61
+ */
62
+ export function quitStep(app, event, name, fn, { log = console } = {}) {
63
+ let ran = false;
64
+ app.on(event, () => {
65
+ if (ran) return;
66
+ ran = true;
67
+ try {
68
+ fn();
69
+ } catch (err) {
70
+ // Never rethrown: the app is on its way out and every step after this one
71
+ // still has to happen.
72
+ log?.warn?.(`zero: ${name} failed while quitting: ${err?.message ?? err}`);
73
+ }
74
+ });
75
+ }
76
+
77
+ /**
78
+ * HOLD THE QUIT OPEN FOR THE LAST BATCH OF COUNTS.
79
+ *
80
+ * Three things it will not do, each of them a way a quit becomes a hang:
81
+ *
82
+ * - It will not wait past `FLUSH_DEADLINE_MS`. A machine with no network, or
83
+ * a destination that accepts the connection and never answers, must cost
84
+ * somebody two seconds on the way out and not their afternoon.
85
+ * - It will not care whether the flush worked. There is nothing to be done
86
+ * about a flush that failed, and a count is never worth a quit.
87
+ * - It will not hold twice. `will-quit` comes round again when we ask again,
88
+ * and holding that one too is a quit that never happens.
89
+ * - It will not trust the second ask either. On Linux, a quit that began with
90
+ * a window open ignores it and the process stays for ever, which is what
91
+ * `quitThenExit` is for
92
+ * (tests/a-quit-held-for-the-last-counts-still-ends-the-app).
93
+ */
94
+ export function flushBeforeQuit(app, flush, { deadlineMs = FLUSH_DEADLINE_MS, log = console } = {}) {
95
+ let held = false;
96
+ app.on('will-quit', (event) => {
97
+ if (held) return; // the second time round is the one we asked for
98
+ held = true;
99
+ event?.preventDefault?.();
100
+ let gone = false;
101
+ const go = () => {
102
+ if (gone) return;
103
+ gone = true;
104
+ clearTimeout(timer);
105
+ quitThenExit(app, { what: 'the last counts', deadlineMs: SECOND_QUIT_DEADLINE_MS, log });
106
+ };
107
+ const timer = setTimeout(go, deadlineMs);
108
+ // The deadline is armed before the flush is even called, so a `flush` that
109
+ // throws synchronously is covered by the same promise as one that rejects.
110
+ Promise.resolve()
111
+ .then(() => flush())
112
+ .catch((err) => { log?.warn?.(`zero: the last counts did not get out: ${err?.message ?? err}`); })
113
+ .then(go, go);
114
+ });
115
+ }
116
+
117
+ /**
118
+ * ASK TO QUIT, AND GO ANYWAY IF THE QUIT DOES NOT COME.
119
+ *
120
+ * `app.quit()` only asks, and on Linux an app that has already refused one
121
+ * `will-quit` ignores every ask after it: the measurement is in
122
+ * tests/a-quit-held-for-the-last-counts-still-ends-the-app. It is still asked,
123
+ * because that is what runs the quit steps, lets the workers go and fires
124
+ * `quit` for whoever is listening, and the deadline ends the process when the
125
+ * quit has not.
126
+ *
127
+ * Both callers pass a deadline that suits what they are waiting for: a
128
+ * screenshot run has a whole quit ahead of it, the flush above has nothing
129
+ * left but the `quit` event. The timer is unreffed: an app with nothing left
130
+ * to do must not be kept alive by the thing watching it. */
131
+ export function quitThenExit(app, { what = 'this run', deadlineMs = EXIT_DEADLINE_MS, log = console } = {}) {
132
+ let over = false;
133
+ const timer = setTimeout(() => {
134
+ if (over) return;
135
+ over = true;
136
+ log?.warn?.(`zero: ${what} asked the app to quit ${deadlineMs} ms ago and it is still here, so it is ending now`);
137
+ app.exit(0);
138
+ }, deadlineMs);
139
+ timer.unref?.();
140
+ app.once?.('quit', () => { over = true; clearTimeout(timer); });
141
+ app.quit();
142
+ return timer;
143
+ }
@@ -4,11 +4,24 @@ import {rememberCodexModels} from './codex-models.mjs';
4
4
  import {forget,claudeModels} from './claude-models.mjs';
5
5
  import {terminalEnv} from './task-terminals.mjs';
6
6
  import {codexLaunchEnv} from './codex-launch-env.mjs';
7
- export async function refreshAgentModels(engine,config,home){
7
+ import {ensureCodexHome} from './codex-account.mjs';
8
+ // `homes` is every Codex login to ask, one after another. A login that cannot
9
+ // answer keeps its last list and does not cost the others theirs; only all of
10
+ // them failing is an error.
11
+ export async function refreshAgentModels(engine,config,homes,{open=home=>openAppServer(config,home)}={}){
8
12
  if(engine==='claude'){forget();claudeModels({bin:config.claudeBin});return;}
9
13
  // Codex is optional and codexBin is null without it: nothing to ask, and spawn(null) only threw into the terminal.
10
14
  if(!config.codexBin)return;
11
- const client=createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
15
+ const list=[...new Set([homes].flat().filter(Boolean))];const errors=[];
16
+ for(const home of list){try{await refreshOne(open(home),home);}catch(error){errors.push(error);}}
17
+ if(list.length&&errors.length===list.length)throw errors[0];
18
+ }
19
+ function openAppServer(config,home){
20
+ // codex-cli refuses a CODEX_HOME that does not exist rather than making it.
21
+ ensureCodexHome(home);
22
+ return createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
23
+ }
24
+ async function refreshOne(client,home){
12
25
  const deadline=setTimeout(()=>client.close('Model refresh timed out'),15000);
13
26
  try{
14
27
  await client.initialize();let cursor=null;const models=[];const seen=new Set();
@@ -0,0 +1,28 @@
1
+ // WHICH RELAY THIS MAC USES (tests/the-mac-finds-the-relay-the-deploy-wrote-down).
2
+ // AGENTBOX_RELAY_URL first, for trying one by hand; then the address
3
+ // `npm run relay:deploy` wrote to relay/relay.local.json; then the one built
4
+ // into the app, which is empty until a deployed address is committed.
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { RELAY_URL } from '../shared/relay-address.mjs';
8
+
9
+ function usable(url) {
10
+ if (typeof url !== 'string' || !url) return null;
11
+ let u;
12
+ try { u = new URL(url); } catch { return null; }
13
+ const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1';
14
+ if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) return null;
15
+ return url.replace(/\/+$/, '');
16
+ }
17
+
18
+ export function relayAddress({ env = process.env, appDir } = {}) {
19
+ const byHand = usable(env.AGENTBOX_RELAY_URL);
20
+ if (byHand) return byHand;
21
+ if (appDir) {
22
+ try {
23
+ const deployed = usable(JSON.parse(fs.readFileSync(path.join(appDir, 'relay', 'relay.local.json'), 'utf8'))?.url);
24
+ if (deployed) return deployed;
25
+ } catch { /* not deployed from this folder */ }
26
+ }
27
+ return usable(RELAY_URL) ?? RELAY_URL;
28
+ }
@@ -0,0 +1,314 @@
1
+ // THE MAC'S END OF THE RELAY (relay/worker.mjs is the middle, relay/phone/ the
2
+ // far end). With phones let in from "Anywhere", the Mac holds one socket open
3
+ // out to the relay, so nothing has to reach in to it: no port opened, no
4
+ // router touched, nothing installed on the phone.
5
+ //
6
+ // Each request that arrives is opened with the key only the Mac and the phone
7
+ // have (shared/relay-crypto.mjs), checked for a replay, and then asked of this
8
+ // Mac's own phone server on 127.0.0.1, exactly as a phone on the Wi-Fi would
9
+ // ask it. So the token, the files and every channel are guarded by the same
10
+ // code either way. The answer goes back sealed, a frame at a time, so the
11
+ // live event stream and a large picture both work.
12
+ import {
13
+ relayIds, relayKeys, open, seal, unpackRequest, FRAME, frameAad, TO_MAC, FROM_MAC, ID_BYTES, tag,
14
+ } from '../shared/relay-crypto.mjs';
15
+
16
+ /** How far a phone's clock may be from this Mac's before a request is stale. */
17
+ const SKEW_MS = 5 * 60_000;
18
+ /** One sealed frame's most. Cloudflare takes up to 1MB per socket message. */
19
+ const CHUNK = 256 * 1024;
20
+ /** The most one request may carry, as the relay allows (relay/worker.mjs). */
21
+ const MAX_REQUEST = 32 * 1024 * 1024;
22
+ /** What a phone's request may carry on to the phone server. */
23
+ const PASS = ['accept', 'content-type', 'range', 'user-agent', 'if-none-match', 'x-agentbox-token', 'x-agentbox-device'];
24
+ /**
25
+ * How often a slow answer says "still working" (FRAME.WAIT), well inside the
26
+ * 30 s the room gives a Mac to start answering (relay/room.mjs).
27
+ */
28
+ const WAIT_MS = 10_000;
29
+ /**
30
+ * THE LINK IS CHECKED, NOT TRUSTED (tests/the-computer-notices-a-dead-relay-link-and-reconnects).
31
+ * A socket that carries nothing cannot tell this end it has died: a laptop that
32
+ * slept or changed networks kept one it believed open while the relay had let
33
+ * it go, and phones were told the computer was not connected. So this end says
34
+ * "ping" this often and the relay answers "pong" without waking the room.
35
+ */
36
+ export const PING_MS = 25_000;
37
+ /** How long past a ping's turn a missing pong is allowed before the link is called dead. */
38
+ export const PONG_GRACE_MS = 10_000;
39
+ /** What the platform sends as text on the socket. Never a request: those are bytes. */
40
+ const PING = 'ping';
41
+ const PONG = 'pong';
42
+ /** What the phone does not want back: the relay's stream is its own framing. */
43
+ const DROP = new Set(['connection', 'keep-alive', 'transfer-encoding', 'content-length', 'content-encoding']);
44
+
45
+ const enc = new TextEncoder();
46
+ const hex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
47
+
48
+ export function createRelayClient({
49
+ relayUrl, key, port, WebSocketImpl = globalThis.WebSocket, fetchImpl = (...a) => globalThis.fetch(...a),
50
+ log = console, onChange = () => {}, waitMs = WAIT_MS, pingMs = PING_MS, graceMs = PONG_GRACE_MS,
51
+ }) {
52
+ let ws = null;
53
+ let pinger = null;
54
+ /** When the relay last answered a ping on this link; null until it ever has. */
55
+ let ponged = null;
56
+ /** When the ping timer last ran, to tell a sleep from a slow tick. */
57
+ let ticked = 0;
58
+ let want = false;
59
+ let connected = false;
60
+ let error = null;
61
+ let retry = 1000;
62
+ let timer = null;
63
+ let gen = 0;
64
+ let forKey = null;
65
+ let ids = null;
66
+ let keys = null;
67
+ /** Requests answered lately, by nonce, so a recorded one sent again is refused. */
68
+ const answered = new Map();
69
+ /** Answers being sent, so a phone that gives up stops them. */
70
+ const live = new Map();
71
+ /** Pieces of requests too big for one message, until their last piece. */
72
+ const parts = new Map();
73
+
74
+ async function ready() {
75
+ const k = key();
76
+ if (k === forKey) return;
77
+ ids = await relayIds(k);
78
+ keys = await relayKeys(k);
79
+ forKey = k;
80
+ answered.clear();
81
+ }
82
+
83
+ function fresh(head) {
84
+ const now = Date.now();
85
+ for (const [n, at] of answered) if (now - at > 2 * SKEW_MS) answered.delete(n);
86
+ if (typeof head?.nonce !== 'string' || head.nonce.length < 16) return false;
87
+ if (!Number.isFinite(head.ts) || Math.abs(now - head.ts) > SKEW_MS) return false;
88
+ if (answered.has(head.nonce)) return false;
89
+ answered.set(head.nonce, now);
90
+ return true;
91
+ }
92
+
93
+ const send = (sock, bytes) => { try { sock.send(bytes); } catch { /* the socket went; the phone hears from the relay */ } };
94
+
95
+ async function answer(sock, id, sealed) {
96
+ const k = keys;
97
+ let head;
98
+ let body;
99
+ try {
100
+ ({ head, body } = unpackRequest(await open(k.up, sealed, 'req')));
101
+ } catch {
102
+ send(sock, tag(FROM_MAC.REFUSED, id));
103
+ return;
104
+ }
105
+ if (!fresh(head) || typeof head.path !== 'string' || !head.path.startsWith('/')) {
106
+ send(sock, tag(FROM_MAC.REFUSED, id));
107
+ return;
108
+ }
109
+ const ctl = new AbortController();
110
+ const idh = hex(id);
111
+ live.set(idh, ctl);
112
+ let seq = 0;
113
+ // One at a time, in order: each frame's number is sealed into it, and a
114
+ // "still working" frame from the timer must not overtake the head.
115
+ let queue = Promise.resolve();
116
+ const frame = (type, payload) => (queue = queue.then(async () => {
117
+ const plain = new Uint8Array(1 + payload.length);
118
+ plain[0] = type;
119
+ plain.set(payload, 1);
120
+ send(sock, tag(FROM_MAC.FRAME, id, await seal(k.down, plain, frameAad(head.nonce, seq++))));
121
+ }));
122
+ // Until the answer starts, a phone that asked for it hears "still working".
123
+ let waiting = head.wait ? setInterval(() => { frame(FRAME.WAIT, new Uint8Array(0)).catch(() => {}); }, waitMs) : null;
124
+ const answering = () => { if (waiting) clearInterval(waiting); waiting = null; };
125
+ let sentHead = false;
126
+ try {
127
+ const headers = { 'x-agentbox-relay': '1', 'x-forwarded-for': 'relay' };
128
+ for (const name of PASS) if (head.headers?.[name]) headers[name] = String(head.headers[name]);
129
+ const method = String(head.method || 'GET').toUpperCase();
130
+ const res = await fetchImpl(`http://127.0.0.1:${port()}${head.path}`, {
131
+ method, headers, body: method === 'GET' || method === 'HEAD' ? undefined : body, signal: ctl.signal,
132
+ });
133
+ answering();
134
+ const out = {};
135
+ res.headers.forEach((v, name) => { if (!DROP.has(name)) out[name] = v; });
136
+ sentHead = true;
137
+ await frame(FRAME.HEAD, enc.encode(JSON.stringify({ status: res.status, headers: out })));
138
+ if (res.body) {
139
+ const reader = res.body.getReader();
140
+ for (;;) {
141
+ const { value, done } = await reader.read();
142
+ if (done) break;
143
+ for (let i = 0; i < value.length; i += CHUNK) await frame(FRAME.DATA, value.subarray(i, i + CHUNK));
144
+ }
145
+ }
146
+ await frame(FRAME.END, new Uint8Array(0));
147
+ send(sock, tag(FROM_MAC.DONE, id));
148
+ } catch (err) {
149
+ answering();
150
+ if (ctl.signal.aborted) return;
151
+ log.warn?.('[relay] answering failed:', err?.message ?? err);
152
+ try {
153
+ await queue.catch(() => {});
154
+ if (!sentHead) {
155
+ await frame(FRAME.HEAD, enc.encode(JSON.stringify({ status: 502, headers: { 'content-type': 'application/json' } })));
156
+ await frame(FRAME.DATA, enc.encode(JSON.stringify({ error: 'Your computer could not answer that.' })));
157
+ }
158
+ await frame(FRAME.END, new Uint8Array(0));
159
+ } catch { /* nothing more to say */ }
160
+ send(sock, tag(FROM_MAC.DONE, id));
161
+ } finally {
162
+ answering();
163
+ live.delete(idh);
164
+ }
165
+ }
166
+
167
+ async function onMessage(sock, data) {
168
+ if (typeof data === 'string') {
169
+ if (data === PONG) ponged = Date.now();
170
+ return;
171
+ }
172
+ const bytes = data instanceof ArrayBuffer ? new Uint8Array(data)
173
+ : data instanceof Uint8Array ? data
174
+ : new Uint8Array(await data.arrayBuffer());
175
+ if (bytes.length < 1 + ID_BYTES) return;
176
+ const id = bytes.slice(1, 1 + ID_BYTES);
177
+ const idh = hex(id);
178
+ if (bytes[0] === TO_MAC.CANCEL) {
179
+ live.get(idh)?.abort();
180
+ live.delete(idh);
181
+ parts.delete(idh);
182
+ } else if (bytes[0] === TO_MAC.PART) {
183
+ // A big request comes in pieces (relay/room.mjs); keep them until the
184
+ // last, within a limit, so a stream of pieces cannot fill memory.
185
+ const held = parts.get(idh) ?? { chunks: [], size: 0 };
186
+ held.chunks.push(bytes.slice(1 + ID_BYTES));
187
+ held.size += bytes.length - 1 - ID_BYTES;
188
+ if (held.size > MAX_REQUEST) { parts.delete(idh); send(sock, tag(FROM_MAC.REFUSED, id)); return; }
189
+ parts.set(idh, held);
190
+ } else if (bytes[0] === TO_MAC.REQUEST) {
191
+ const held = parts.get(idh);
192
+ parts.delete(idh);
193
+ const last = bytes.slice(1 + ID_BYTES);
194
+ if (!held) { await answer(sock, id, last); return; }
195
+ const whole = new Uint8Array(held.size + last.length);
196
+ let at = 0;
197
+ for (const c of held.chunks) { whole.set(c, at); at += c.length; }
198
+ whole.set(last, at);
199
+ await answer(sock, id, whole);
200
+ }
201
+ }
202
+
203
+ // Two ways a link is known dead without waiting on the operating system: a
204
+ // relay that has answered pings stops answering, or this timer ran far too
205
+ // late, which is a laptop waking from sleep with every socket gone under it.
206
+ // A relay that never answers (one deployed before pings) is only ever judged
207
+ // by the second, so an old relay is not mistaken for a dead link.
208
+ function startPinging(sock) {
209
+ stopPinging();
210
+ ponged = null;
211
+ ticked = Date.now();
212
+ pinger = setInterval(() => {
213
+ const now = Date.now();
214
+ const slept = now - ticked > pingMs * 2;
215
+ ticked = now;
216
+ const silent = ponged !== null && now - ponged > pingMs + graceMs;
217
+ if (slept || silent) {
218
+ log.info?.(`[relay] the link went quiet (${slept ? 'woke from sleep' : 'no answer to ping'}); reconnecting`);
219
+ replace(sock);
220
+ return;
221
+ }
222
+ send(sock, PING);
223
+ }, pingMs);
224
+ }
225
+
226
+ function stopPinging() {
227
+ if (pinger) clearInterval(pinger);
228
+ pinger = null;
229
+ }
230
+
231
+ /** Let a dead link go and open a new one now, not after the backoff. */
232
+ function replace(sock) {
233
+ if (ws !== sock) return;
234
+ stopPinging();
235
+ ws = null;
236
+ connected = false;
237
+ for (const c of live.values()) c.abort();
238
+ live.clear();
239
+ try { sock.close(4002, 'No answer; reconnecting.'); } catch { /* already gone */ }
240
+ if (timer) { clearTimeout(timer); timer = null; }
241
+ retry = 1000;
242
+ onChange();
243
+ void connect();
244
+ }
245
+
246
+ function schedule() {
247
+ if (!want || timer) return;
248
+ timer = setTimeout(() => { timer = null; void connect(); }, retry);
249
+ retry = Math.min(retry * 2, 30_000);
250
+ }
251
+
252
+ async function connect() {
253
+ const mine = ++gen;
254
+ try { await ready(); } catch (err) { error = String(err?.message ?? err); onChange(); schedule(); return; }
255
+ if (!want || mine !== gen) return;
256
+ const url = `${relayUrl.replace(/^http/, 'ws').replace(/\/+$/, '')}/mac/${ids.room}?proof=${encodeURIComponent(ids.proof)}`;
257
+ let sock;
258
+ try { sock = new WebSocketImpl(url); } catch (err) { error = `Could not reach the relay: ${err?.message ?? err}`; onChange(); schedule(); return; }
259
+ ws = sock;
260
+ sock.binaryType = 'arraybuffer';
261
+ sock.onopen = () => {
262
+ if (ws !== sock) return;
263
+ connected = true;
264
+ error = null;
265
+ retry = 1000;
266
+ startPinging(sock);
267
+ onChange();
268
+ };
269
+ sock.onmessage = (event) => { if (ws === sock) void onMessage(sock, event.data); };
270
+ sock.onerror = () => { if (ws === sock && !connected) error = 'Could not reach the relay.'; };
271
+ sock.onclose = (event) => {
272
+ if (ws !== sock) return;
273
+ stopPinging();
274
+ ws = null;
275
+ const was = connected;
276
+ connected = false;
277
+ for (const c of live.values()) c.abort();
278
+ live.clear();
279
+ if (event?.code === 4001) error = 'The relay turned this computer away.';
280
+ else if (was) error = null;
281
+ onChange();
282
+ schedule();
283
+ };
284
+ }
285
+
286
+ return {
287
+ start() {
288
+ if (want) return;
289
+ want = true;
290
+ retry = 1000;
291
+ void connect();
292
+ },
293
+ stop() {
294
+ want = false;
295
+ gen += 1;
296
+ stopPinging();
297
+ if (timer) { clearTimeout(timer); timer = null; }
298
+ const sock = ws;
299
+ ws = null;
300
+ connected = false;
301
+ for (const c of live.values()) c.abort();
302
+ live.clear();
303
+ try { sock?.close(1000, 'Phones from anywhere turned off.'); } catch { /* already */ }
304
+ onChange();
305
+ },
306
+ /** The key was reset: the room is a new one. */
307
+ restart() {
308
+ if (!want) return;
309
+ this.stop();
310
+ this.start();
311
+ },
312
+ state: () => ({ connected, error, connecting: want && !connected && !error }),
313
+ };
314
+ }
package/main/repeats.mjs CHANGED
@@ -16,6 +16,7 @@ import fs from 'node:fs';
16
16
  import path from 'node:path';
17
17
  import {
18
18
  DAY_NAMES, foldRepeats, isOwed, isRepeatRule, occurrenceId, periodKey, repeatLabel,
19
+ standingInstruction, PROTOCOL_OPENS, INSTRUCTION_MARK,
19
20
  } from '../shared/repeats.mjs';
20
21
  import { machineryPath } from './store/home.mjs';
21
22
  import { NAME } from '../shared/product-name.mjs';
@@ -41,8 +42,8 @@ const MISSES_BEFORE_ALERT = 3;
41
42
  // last can be truncated away entirely, and a worker that never saw it cannot
42
43
  // know that a quiet finish has to be marked.
43
44
  export const OCCURRENCE_PROTOCOL = [
44
- 'THIS IS ONE RUN OF A REPEATING TASK. It runs again tomorrow whatever happens',
45
- 'here, so this run does not have to carry the future.',
45
+ `${PROTOCOL_OPENS} It runs again at its next time whatever`,
46
+ 'happens here, so this run does not have to carry the future.',
46
47
  '',
47
48
  'FINISH ONE OF TWO WAYS.',
48
49
  '',
@@ -60,7 +61,12 @@ export const OCCURRENCE_PROTOCOL = [
60
61
  `only place in ${NAME} where finishing quietly hides something, so the marker is`,
61
62
  'required to hide, never to show.',
62
63
  '',
63
- 'HER INSTRUCTION FOLLOWS.',
64
+ 'IF THEY TELL YOU HOW THIS TASK SHOULD GO FROM NOW ON, CHANGE IT YOURSELF with',
65
+ 'the store tool set_repeating_task. Editing this run or writing a memory note',
66
+ 'reaches no later run: this body is a copy, and every run gets a new task',
67
+ 'folder. Never ask them to go and edit the task by hand.',
68
+ '',
69
+ INSTRUCTION_MARK,
64
70
  ].join('\n');
65
71
 
66
72
  export class Repeats {
@@ -212,7 +218,7 @@ export class Repeats {
212
218
 
213
219
  const { created } = workItemsDisk.createWorkItemIfAbsent(productDir, id, {
214
220
  title: `${rule.title} · ${dayStamp(key)}`,
215
- body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${rule.body ?? rule.title}`,
221
+ body: `${OCCURRENCE_PROTOCOL.replace('<REPEAT_LABEL>', repeatLabel(ruleId))}\n\n${standingInstruction(rule)}`,
216
222
  kind: 'directive',
217
223
  priority: rule.priority ?? 5,
218
224
  labels: ['founder', repeatLabel(ruleId)],
@@ -1,3 +1,5 @@
1
+ import { harnessFor } from './harnesses.mjs';
2
+ import { teammateIdOf } from '../shared/teammates.mjs';
1
3
  // THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM THE FIRST SENTENCE.
2
4
  //
3
5
  // A dictated message keeps its first line as its title, verbatim and forever
@@ -61,51 +63,18 @@ export const NAME_MAX = 64;
61
63
  * archive; everything else is a row that can still appear in a list. */
62
64
  const LIVE = new Set(['open', 'claimed', 'blocked']);
63
65
 
64
- /**
65
- * PURE. Is this title already a name somebody wrote?
66
- *
67
- * The one question that decides whether this module speaks at all, and it is
68
- * answered off the words alone because that is all there is: nothing on a row
69
- * records whether its title was typed or derived.
70
- *
71
- * Three marks, and together they separate the two shapes `splitMessage` makes
72
- * (renderer/src/message-split.ts). A first line of 120 characters or less is
73
- * kept VERBATIM and is the name its person wrote. A longer one is clipped into
74
- * a title by the app, and every clip it can produce is either a whole sentence
75
- * of at least 25 characters or a hard cut ending in an ellipsis.
76
- *
77
- * as short as a name we would write NAME_MAX, so a title already that tight
78
- * has nothing for us to improve,
79
- * as few words as a name has the same eight `cleanName` allows,
80
- * not ending as a sentence does '.', '!', '?' and '…' are how a clipped
81
- * title ends and not how a name does.
82
- *
83
- * It errs towards leaving the words alone, which is the right way to be wrong:
84
- * the cost is a short typed sentence keeping its own first line, and the cost
85
- * the other way is somebody losing the thread they named.
86
- */
87
- export function isWrittenName(title) {
88
- const t = String(title ?? '').trim();
89
- if (!t) return false;
90
- if (t.length > NAME_MAX) return false;
91
- if (t.split(/\s+/).length > 8) return false;
92
- return !/[.!?…]$/.test(t);
93
- }
94
-
95
66
  /**
96
67
  * PURE. Does this row want naming?
97
68
  *
98
- * Four ways to say no, and the last two are the fix for a tester losing their
99
- * own threads on 2026-10-01: "Add comment to cart.js" became "Improve cart.js
100
- * header comment" and then "Update cart file comment" inside two minutes.
69
+ * Three ways to say no:
101
70
  *
102
71
  * - it is done, so nobody is scanning for it,
103
72
  * - it has nothing to name from, which is a row with no words on it yet,
104
- * - ITS PERSON ALREADY NAMED IT. A short clear title is the name they will
105
- * look for, and on a team it is the name they will point someone else at.
106
- * Improving it is not an improvement.
107
73
  * - IT HAS BEEN NAMED. Once, when the thread was made, and then never again.
108
74
  *
75
+ * A short typed title is named too: a list where some rows are typed lines and
76
+ * the rest are names reads as two styles. The prompt keeps such a line's words.
77
+ *
109
78
  * That last line used to read `wroteAt < contentTouchedAt(item)`, so the name
110
79
  * followed where the thread stood: an agent writes a note and a result on every
111
80
  * run, so every run earned a new name. Following the thread was the intended
@@ -121,8 +90,8 @@ export function isWrittenName(title) {
121
90
  */
122
91
  export function wantsName(item) {
123
92
  if (!item || !LIVE.has(item.status)) return false;
93
+ if (teammateIdOf(item)) return false;
124
94
  if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
125
- if (isWrittenName(item.title)) return false;
126
95
  return !String(item.label ?? '').trim();
127
96
  }
128
97
 
@@ -138,12 +107,15 @@ export function namePrompt(item) {
138
107
  'Here is a work thread: the message that opened it, and the latest thing',
139
108
  'written on it. Write a short name for the thread AS IT STANDS NOW: 3 to 7',
140
109
  'words, sentence case, no quotes, no trailing punctuation, naming the thing',
141
- 'being worked on rather than restating the sentence. Reply with the name and',
142
- 'nothing else.',
110
+ 'being worked on rather than restating the sentence. If the opening line is',
111
+ 'already a short name, keep its words and only fix the case. Reply with the',
112
+ 'name and nothing else.',
143
113
  '',
144
114
  '--- opened with ---',
145
115
  cut(item?.title, 300),
146
- cut(item?.body, 1200),
116
+ // A teammate's run is titled with its own part of a message that may have
117
+ // woken several (shared/teammates.mjs `runTitle`); the whole would mix them.
118
+ (item?.labels ?? []).some((l) => String(l).startsWith('teammate:')) ? '' : cut(item?.body, 1200),
147
119
  '--- latest ---',
148
120
  latest || '(nothing written yet)',
149
121
  '---',
@@ -188,10 +160,7 @@ export function codexNameArgs(item) {
188
160
  }
189
161
 
190
162
  function codexArgs(prompt) {
191
- return [
192
- 'exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules',
193
- '-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt,
194
- ];
163
+ return harnessFor('codex').smallModelArgs(prompt);
195
164
  }
196
165
 
197
166
  /**
@@ -218,11 +187,13 @@ export function nameRow(item, opts = {}) {
218
187
  */
219
188
  export function askSmallModel(prompt, clean, { claudeBin, codexBin = null, engine = 'claude', model = NAME_MODEL, timeoutMs = NAME_TIMEOUT_MS, env } = {}) {
220
189
  return new Promise((resolve) => {
221
- const bin = engine === 'codex' ? codexBin : claudeBin;
190
+ let harness;
191
+ try { harness = harnessFor(engine); } catch { return resolve(''); }
192
+ const bin = harness.binary({ claudeBin, codexBin });
222
193
  if (!bin) return resolve('');
223
194
  let child;
224
195
  try {
225
- child = spawn(bin, engine === 'codex' ? codexArgs(prompt) : ['-p', prompt, '--model', model], {
196
+ child = spawn(bin, harness.smallModelArgs(prompt, model), {
226
197
  // Somewhere that is nobody's project. A naming call must not pick up a
227
198
  // CLAUDE.md, a settings file or a hook from whatever folder it lands in.
228
199
  cwd: '/tmp',