agentbox-app 0.1.13 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -61,12 +61,53 @@
61
61
  // to her subscription and never to a key (CLAUDE.md), so passing that advice on
62
62
  // would be this app telling her to do the one thing it refuses to do.
63
63
  import { NAME, Name } from '../shared/product-name.mjs';
64
+ import { harnessDefinition } from './harness-definitions.mjs';
65
+ import { instantIn, resetDay, wallIn } from './usage.mjs';
66
+
67
+ // A CLAUDE CODE TOO OLD FOR THE LINE AGENTBOX STARTS IT WITH. Its argument
68
+ // parser refuses the line before a single request, in one of two shapes,
69
+ // measured on npm releases 2.1.70 back to 1.0.90 (2026-10-15):
70
+ //
71
+ // error: option '--permission-mode <mode>' argument 'auto' is invalid. ...
72
+ // error: unknown option '--effort'
73
+ //
74
+ // ONLY FLAGS THE APP ITSELF SENDS, and only the mode value it sends. Somebody's
75
+ // own session arguments can carry a typo, and an update does not fix a typo,
76
+ // so an unknown flag of theirs stays 'unknown'. Anchored to the start of a
77
+ // line because that is where the CLI prints it, and an agent quoting the same
78
+ // words from some other program's output is not this.
79
+ const CLAUDE = harnessDefinition('claude');
80
+ const OUR_FLAGS = [
81
+ ...JSON.stringify(CLAUDE.run).match(/--[A-Za-z-]+/g),
82
+ '--allowedTools', '--disallowedTools', '--permission-mode', '--mcp-config', '--permission-prompt-tool', '--settings', '--model',
83
+ ];
84
+ const TOO_OLD = [
85
+ new RegExp(`^error: unknown option '(${OUR_FLAGS.join('|')})'`, 'm'),
86
+ /^error: option '--permission-mode <mode>' argument 'auto' is invalid/m,
87
+ ];
64
88
  const ORG_BLOCKED = [
65
89
  /organi[sz]ation has disabled/i,
66
90
  /disabled for (your|this) organi[sz]ation/i,
67
91
  /ask your admin/i,
68
92
  ];
69
93
 
94
+ // A CODEX SIGNED IN WITH AN API KEY. Our own line, said before the worker
95
+ // starts (main/codex-account.mjs `codexKeyRefusal`), because Codex reads the
96
+ // key out of auth.json past the worker scrub and bills every call to it.
97
+ // Before SIGNED_OUT, whose /invalid api key/ is a different event.
98
+ const API_KEY = [/signed in with an api key/i];
99
+
100
+ // A CODEX config.toml CODEX WILL NOT READ. Codex's own words, off real runs of
101
+ // codex-cli 0.160.0 on 2026-10-15: the app-server refusing `config/read` for a
102
+ // quote left open or an old `profile = "..."` line, and the terminal's
103
+ // "Error loading config.toml". Not benched like a login: fixing the file
104
+ // changes no sign-in file, so a benched account would never come back.
105
+ const CODEX_CONFIG = [
106
+ /refused config\/read: failed to (read configuration layers|resolve feature override)/i,
107
+ /error loading config\.toml/i,
108
+ /config\.toml:\d+:\d+: /i,
109
+ ];
110
+
70
111
  const SIGNED_OUT = [
71
112
  /oauth/i,
72
113
  /session expired/i,
@@ -145,6 +186,34 @@ const INTERRUPTED = [
145
186
  /overloaded/i,
146
187
  ];
147
188
 
189
+ // A NETWORK THAT WILL NOT LET A SECURE CONNECTION THROUGH (2026-10-15).
190
+ //
191
+ // A company network that inspects TLS swaps in its own certificate, and a
192
+ // proxy can ask for a login. Neither clears by waiting, which is what made
193
+ // 'interrupted' the wrong home for them: the real Claude Code 2.1.301 behind a
194
+ // self-signed proxy died in 371 ms with "API Error: Unable to connect to API:
195
+ // Self-signed certificate detected (DEPTH_ZERO_SELF_SIGNED_CERT) ...", "unable
196
+ // to connect" filed it as a dropped connection, and the screen said there was
197
+ // nothing to do. Node's own spellings, Codex's (rustls and OpenSSL) and a
198
+ // proxy's 407 are here too. A REFUSED connection is not: "Connection refused
199
+ // — a firewall or proxy may be blocking it (ECONNREFUSED)" is also what an
200
+ // offline laptop prints.
201
+ //
202
+ // The fix is a setting the person's terminal already has, and since
203
+ // main/shell-path.mjs the app reads it from their shell profile at launch, so
204
+ // the sentence says to put it there and reopen the app.
205
+ const NETWORK = [
206
+ /self[- ]signed certificate/i,
207
+ /unable to get local issuer certificate/i,
208
+ /unable to verify the first certificate/i,
209
+ /\b(DEPTH_ZERO_SELF_SIGNED_CERT|SELF_SIGNED_CERT_IN_CHAIN|UNABLE_TO_GET_ISSUER_CERT_LOCALLY|UNABLE_TO_VERIFY_LEAF_SIGNATURE)\b/,
210
+ /tls_untrusted_ca/i,
211
+ /invalid peer certificate/i,
212
+ /certificate verify failed/i,
213
+ /proxy authentication required/i,
214
+ ];
215
+ const NETWORK_FIX = 'export HTTPS_PROXY and NODE_EXTRA_CA_CERTS in your shell profile';
216
+
148
217
  // A workspace the CLI refuses to run in. Distinct from the two above because
149
218
  // the fix is a folder, not an account.
150
219
  const WORKSPACE = [
@@ -173,26 +242,45 @@ const WORKSPACE = [
173
242
  // login on a bigger plan runs this one. Codex's own words, off two dead runs on
174
243
  // 2026-10-09 and 2026-10-11 (codex-cli 0.160.0, a free-plan login handed
175
244
  // gpt-6.1-sol): "The 'gpt-6.1-sol' model is not supported when using Codex with
176
- // a ChatGPT account." The second line is ours, thrown before the thread starts
177
- // by `codexModelRefusal` when the login's own list already says so.
245
+ // a ChatGPT account." The other two are ours, thrown before the thread starts
246
+ // by `codexModelRefusal`: this login's list lacks it, or no login's list here
247
+ // has it, which is what a paid plan that has ended looks like.
178
248
  const MODEL_NOT_ON_PLAN = [
179
249
  /model is not supported when using codex with a chatgpt account/i,
180
250
  /not a model the plan of the codex account/i,
251
+ /not a model the codex on this computer knows/i,
181
252
  ];
182
253
 
254
+ // Ours too, from `codexEffortRefusal`.
255
+ const LEVEL_NOT_OFFERED = [/not a level that model offers/i];
256
+
257
+ // Codex lets one process write to a conversation at a time, and refuses our
258
+ // resume while the Codex app or a terminal has it open. codex-cli 0.163.0:
259
+ // "thread <id> already has an active writer".
260
+ const OPEN_ELSEWHERE = [/already has an active writer/i];
261
+
183
262
  // WHICH OF THE THREE THINGS WENT WRONG, from whatever the tool printed.
184
263
  // 'unknown' is a first-class answer, not a failure: an honest generic sentence
185
264
  // beats a confident wrong one.
186
265
  export function troubleCause(raw) {
187
266
  const text = String(raw ?? '');
188
267
  if (!text.trim()) return 'unknown';
268
+ // The CLI's own argument check, before any account is asked anything.
269
+ if (TOO_OLD.some((re) => re.test(text))) return 'too-old';
270
+ if (CODEX_CONFIG.some((re) => re.test(text))) return 'codex-config';
271
+ if (API_KEY.some((re) => re.test(text))) return 'api-key';
189
272
  // FIRST, because an organization's refusal mentions the subscription and
190
273
  // reads as a signed-out login to the list below.
191
274
  if (ORG_BLOCKED.some((re) => re.test(text))) return 'org-blocked';
192
275
  // BEFORE SIGNED-OUT, because Codex's sentence says "ChatGPT account".
193
276
  if (MODEL_NOT_ON_PLAN.some((re) => re.test(text))) return 'model-not-on-plan';
277
+ if (LEVEL_NOT_OFFERED.some((re) => re.test(text))) return 'level-not-offered';
278
+ if (OPEN_ELSEWHERE.some((re) => re.test(text))) return 'open-elsewhere';
194
279
  if (SIGNED_OUT.some((re) => re.test(text))) return 'signed-out';
195
280
  if (AT_LIMIT.some((re) => re.test(text))) return 'at-limit';
281
+ // BEFORE WORKSPACE, whose /untrusted/ would read a certificate as a folder,
282
+ // and before INTERRUPTED, whose "unable to connect" is in Claude Code's line.
283
+ if (NETWORK.some((re) => re.test(text))) return 'network';
196
284
  if (WORKSPACE.some((re) => re.test(text))) return 'workspace';
197
285
  // LAST ON PURPOSE, so every text that already had a name keeps it. A dropped
198
286
  // connection is the weakest of the four claims: an account that is signed out
@@ -208,26 +296,112 @@ export function troubleCause(raw) {
208
296
  // workspace policy prints, so the text alone cannot tell them apart. The
209
297
  // login's own file can: `plan` is main/claude-plan.mjs's reading of it, and
210
298
  // `ended` is its billing gone to "none". Anything else keeps its cause.
211
- export function refineCause(cause, plan) {
212
- return cause === 'org-blocked' && plan?.ended ? 'subscription-ended' : cause;
299
+ // A LOGIN THAT RAN OUT AND ONE NOBODY EVER MADE REFUSE IN THE SAME WORDS, and
300
+ // the second is what somebody who skipped setup meets: "Sign that account back
301
+ // in" about an account they never had. `everSignedIn` is the caller's reading
302
+ // of the login's own files; false only where that reading is proof.
303
+ export function refineCause(cause, plan, { everSignedIn = true } = {}) {
304
+ if (cause === 'org-blocked' && plan?.ended) return 'subscription-ended';
305
+ if (cause === 'signed-out' && !everSignedIn) return 'no-plan';
306
+ return cause;
307
+ }
308
+
309
+ // THE CLOCK A LIMIT LINE NAMES, AND THE ZONE IT NAMED IT IN.
310
+ //
311
+ // The zone used to be dropped on the floor, on the assumption that the CLI
312
+ // prints the clock the Mac is already set to. A run on the cloud prints the
313
+ // cloud machine's: "resets 12:10am (UTC)" reached a Mac on Pacific time
314
+ // (2026-10-16), and both the hour on the row and the moment the fleet waited
315
+ // for were seven hours out. `offset` is minutes east of UTC, and null when
316
+ // the line printed no zone at all — which is still most lines, and still
317
+ // means local. This reads any offset, and reads it for the sentence as well
318
+ // as for the bench, which the first fix for that incident did not.
319
+ //
320
+ // Both wordings are real: the CLI's own line is "resets 6pm" and the API's is
321
+ // "Your weekly limit resets at 4pm", so the "at" is optional.
322
+ function resetClock(raw) {
323
+ const m = /resets\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*(?:\(\s*(?:UTC|GMT)\s*(?:([+-])(\d{1,2})(?::?(\d{2}))?)?\s*\))?/i
324
+ .exec(String(raw ?? ''));
325
+ if (!m) return null;
326
+ const zoned = /\(\s*(?:UTC|GMT)/i.test(m[0]);
327
+ return {
328
+ h: (Number(m[1]) % 12) + (m[3].toLowerCase() === 'pm' ? 12 : 0),
329
+ min: Number(m[2] ?? 0),
330
+ offset: zoned ? (m[4] === '-' ? -1 : 1) * (Number(m[5] ?? 0) * 60 + Number(m[6] ?? 0)) : null,
331
+ };
332
+ }
333
+
334
+ // The CLI's own wording for a clock time, so a converted hour reads exactly
335
+ // the way a quoted one always has.
336
+ function clockWords(h, min) {
337
+ return `${h % 12 || 12}${min ? `:${String(min).padStart(2, '0')}` : ''}${h < 12 ? 'am' : 'pm'}`;
338
+ }
339
+
340
+ // "3:15pm" today, "Wed 2:30pm" this week, "Wed Oct 28 1:05pm" further out,
341
+ // the same day words the account meters use.
342
+ function momentWords(ms, from) {
343
+ const d = new Date(ms);
344
+ const day = resetDay(ms, from);
345
+ return `${day ? `${day} ` : ''}${clockWords(d.getHours(), d.getMinutes())}`;
213
346
  }
214
347
 
215
- // WHEN THE LIMIT LETS GO, straight off the line the CLI printed.
348
+ // WHEN THE LIMIT LETS GO, IN THE CLOCK SHE IS LOOKING AT.
216
349
  //
217
350
  // carries the one thing she actually wants to know, and we were throwing it
218
- // away and then telling her the vaguer "when the limit resets". The timezone
219
- // is dropped rather than shown: the limit line prints the zone the Mac is
220
- // already set to, so naming it would only add a word she has to read. Null when there is no time to quote, and then
221
- // the sentence falls back to the wording that has always been there.
222
- export function limitResetsAt(raw) {
223
- // Both wordings are real: the CLI's own line is "resets 6pm" and the API's
224
- // is "Your weekly limit resets at 4pm", so the "at" is optional.
225
- const m = /resets\s+(?:at\s+)?(\d{1,2}(?::\d{2})?\s*(?:am|pm))/i.exec(String(raw ?? ''));
226
- return m ? m[1].replace(/\s+/g, '').toLowerCase() : null;
351
+ // away and then telling her the vaguer "when the limit resets". A line that
352
+ // names its own zone is converted to this Mac's clock rather than quoted, so
353
+ // the hour on the row is an hour she can act on; a line that names none is
354
+ // already local and is quoted as it stands. Null when there is no time to
355
+ // quote, and then the sentence falls back to the wording that has always been
356
+ // there.
357
+ export function limitResetsAt(raw, from = Date.now()) {
358
+ const clock = resetClock(raw);
359
+ if (!clock) {
360
+ // A reset on another day ("resets Oct 22 at 4pm", Codex's "try again at
361
+ // Oct 28th, 2026 1:05 PM") has no bare hour to quote.
362
+ const moment = limitResetMoment(raw, from);
363
+ return moment == null ? null : momentWords(moment, from);
364
+ }
365
+ if (clock.offset == null) return clockWords(clock.h, clock.min);
366
+ const at = new Date(limitResetMoment(raw, from));
367
+ return clockWords(at.getHours(), at.getMinutes());
227
368
  }
228
369
 
229
370
  const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
230
371
 
372
+ const CLAUDE_DAY = /resets\s+([a-z]{3})[a-z]*\.?\s+(\d{1,2})\s+at\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*(?:\(\s*([^)]+?)\s*\))?/i;
373
+ const UTC_ZONE = /^(?:UTC|GMT)\s*(?:([+-])(\d{1,2})(?::?(\d{2}))?)?$/i;
374
+
375
+ // "UTC", "GMT+2" or a place such as "America/Los_Angeles". A zone this
376
+ // computer cannot name, or none, is its own clock.
377
+ function zoneOf(word) {
378
+ const utc = UTC_ZONE.exec(word ?? '');
379
+ if (utc) return { offset: (utc[1] === '-' ? -1 : 1) * (Number(utc[2] ?? 0) * 60 + Number(utc[3] ?? 0)), name: null };
380
+ return { offset: null, name: word && wallIn(Date.now(), word) ? word : null };
381
+ }
382
+
383
+ // The moment a dated Claude Code reset names, or null. The line has no year,
384
+ // so it is the year in that zone at `from`, or the next one when that puts the
385
+ // reset more than a day behind (December reading a January date).
386
+ function claudeResetDay(raw, from) {
387
+ const m = CLAUDE_DAY.exec(String(raw ?? ''));
388
+ if (!m) return null;
389
+ const month = MONTHS.indexOf(m[1].toLowerCase());
390
+ if (month < 0) return null;
391
+ const day = Number(m[2]);
392
+ const h = (Number(m[3]) % 12) + (m[5].toLowerCase() === 'pm' ? 12 : 0);
393
+ const min = Number(m[4] ?? 0);
394
+ const zone = zoneOf(m[6]);
395
+ const at = (year) => zone.offset != null
396
+ ? Date.UTC(year, month, day, h, min) - zone.offset * 60_000
397
+ : instantIn(year, month, day, h, min, zone.name);
398
+ const year = zone.offset != null
399
+ ? new Date(from + zone.offset * 60_000).getUTCFullYear()
400
+ : (wallIn(from, zone.name)?.year ?? new Date(from).getFullYear());
401
+ const moment = at(year);
402
+ return moment < from - 86_400_000 ? at(year + 1) : moment;
403
+ }
404
+
231
405
  // THE SAME HOUR AS A MOMENT, so the fleet can wait exactly until it
232
406
  // (a limit that reset at, say, 6:50pm could hold every agent until 7:03pm,
233
407
  // because the app waited a fixed thirty minutes instead of reading this line).
@@ -245,15 +419,26 @@ export function limitResetMoment(raw, from = Date.now()) {
245
419
  return new Date(Number(dated[3]), month, Number(dated[2]), h, Number(dated[5]), 0, 0).getTime();
246
420
  }
247
421
  }
248
- const hour = limitResetsAt(raw);
249
- if (!hour) return null;
250
- const m = /^(\d{1,2})(?::(\d{2}))?(am|pm)$/.exec(hour);
251
- if (!m) return null;
252
- const h = (Number(m[1]) % 12) + (m[3] === 'pm' ? 12 : 0);
253
- const d = new Date(from);
254
- d.setHours(h, Number(m[2] ?? 0), 0, 0);
255
- if (d.getTime() < from - 60_000) d.setDate(d.getDate() + 1);
256
- return d.getTime();
422
+ // Claude Code names the day when the reset is not today: "resets Oct 22 at
423
+ // 4pm (America/Los_Angeles)".
424
+ const datedReset = claudeResetDay(raw, from);
425
+ if (datedReset != null) return datedReset;
426
+ const clock = resetClock(raw);
427
+ if (!clock) return null;
428
+ if (clock.offset == null) {
429
+ const d = new Date(from);
430
+ d.setHours(clock.h, clock.min, 0, 0);
431
+ if (d.getTime() < from - 60_000) d.setDate(d.getDate() + 1);
432
+ return d.getTime();
433
+ }
434
+ // The named zone's own calendar day is not this Mac's, so the candidate is
435
+ // built in UTC and then walked to the one occurrence that sits in the next
436
+ // twenty-four hours. The grace is for the two clocks disagreeing by seconds.
437
+ const asked = new Date(from);
438
+ let t = Date.UTC(asked.getUTCFullYear(), asked.getUTCMonth(), asked.getUTCDate(), clock.h, clock.min) - clock.offset * 60_000;
439
+ while (t < from - 60_000) t += 86_400_000;
440
+ while (t - from >= 86_400_000) t -= 86_400_000;
441
+ return t;
257
442
  }
258
443
 
259
444
  // AN ACCOUNT THAT CANNOT BE USED AT ALL UNTIL A PERSON DOES SOMETHING.
@@ -265,19 +450,32 @@ export function needsHerHands(cause) {
265
450
  // same person: one is her, in a terminal; one is an admin of a workspace she
266
451
  // may not run. What they share is the only thing this question asks — trying
267
452
  // again in thirty minutes cannot possibly help.
268
- return cause === 'signed-out' || cause === 'org-blocked' || cause === 'subscription-ended';
453
+ // An out-of-date Claude Code is hers too: no retry updates it, and every run
454
+ // until she does spends nothing but a reply's tries.
455
+ // A Codex signed in with a key is hers as well, and `codex login` rewrites
456
+ // auth.json, which is what brings a benched account back.
457
+ return cause === 'signed-out' || cause === 'no-plan' || cause === 'org-blocked' || cause === 'subscription-ended' || cause === 'too-old' || cause === 'api-key';
269
458
  }
270
459
 
460
+ // THE ONE COMMAND THAT UPDATES IT, WHEREVER IT CAME FROM. `claude update`
461
+ // updates a native install itself and, on a Homebrew one, prints the brew line
462
+ // to run instead (read in 2.1.80's own code), so it is the right first thing
463
+ // to type whatever she installed it with.
464
+ const UPDATE = `run claude update in a terminal (${Name} needs version ${CLAUDE.minVersion} or newer)`;
465
+
271
466
  // WHAT SHE READS WHEN NOT ONE AGENT CAN START ANYWHERE.
272
467
  // Not "an error occurred". The subject of the sentence is the thing she cares
273
468
  // about, which is her agents, and the reason follows it in plain words.
274
469
  export function troubleSentence(cause) {
275
470
  if (cause === 'signed-out') return 'No agents can start. Your Claude login has run out.';
471
+ if (cause === 'no-plan') return 'No agents can start. No AI plan is set up on this computer.';
276
472
  if (cause === 'org-blocked') return 'No agents can start. Your organization has turned Claude Code off.';
277
473
  if (cause === 'subscription-ended') return 'No agents can start. Your Claude subscription has ended.';
278
474
  if (cause === 'at-limit') return 'No agents can start. Your Claude plan is at its limit.';
279
475
  if (cause === 'workspace') return 'No agents can start. Claude will not run in this folder.';
280
476
  if (cause === 'interrupted') return 'No agents can start. The connection to Claude keeps dropping.';
477
+ if (cause === 'network') return 'No agents can start. This network will not let them connect securely.';
478
+ if (cause === 'too-old') return 'No agents can start. Claude Code on this computer is too old.';
281
479
  return 'No agents can start on this computer right now.';
282
480
  }
283
481
 
@@ -288,11 +486,14 @@ export function troubleRemedy(cause) {
288
486
  // ). "Sign in to Claude again" is a thing to go and work out; /login is a
289
487
  // thing to type.
290
488
  if (cause === 'signed-out') return 'Open Claude in a terminal, type /login, and they pick straight back up.';
489
+ if (cause === 'no-plan') return `Set one up ${SET_UP_A_PLAN} and they pick straight up.`;
291
490
  if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
292
- if (cause === 'subscription-ended') return 'Renew it at claude.ai, or pick another account in Settings.';
491
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai and they pick straight up.';
293
492
  if (cause === 'at-limit') return 'They start again on their own when it resets.';
294
493
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
295
494
  if (cause === 'interrupted') return `Nothing to do. ${Name} tries them again on its own.`;
495
+ if (cause === 'network') return `If your company uses a proxy or its own certificate, ${NETWORK_FIX} and reopen ${Name}.`;
496
+ if (cause === 'too-old') return `To update it, ${UPDATE}. They start again on their own.`;
296
497
  return `${Name} keeps trying on its own.`;
297
498
  }
298
499
 
@@ -320,9 +521,14 @@ export function accountSentence(cause, dir) {
320
521
  return 'Signed in, but its organization has turned Claude Code off. Only an admin of that workspace can turn it back on.';
321
522
  }
322
523
  if (cause === 'subscription-ended') return 'Signed in, but its Claude subscription has ended. Renew it at claude.ai to use this account again.';
323
- if (cause === 'at-limit') return 'At its limit. It rejoins on its own when the limit resets.';
524
+ if (cause === 'at-limit') return 'At its limit. Agents start again on their own when it resets.';
324
525
  if (cause === 'workspace') return 'Claude would not run in the folder this was asked to work in.';
325
526
  if (cause === 'interrupted') return `Its runs keep getting cut off. ${Name} is trying them again.`;
527
+ // NOT THE ACCOUNT, and said so, because this is the page where she would
528
+ // otherwise go and sign it in again.
529
+ if (cause === 'network') return `The account is fine; this network will not let it connect securely. Set your company's proxy or certificate in your shell profile and reopen ${Name}.`;
530
+ // Not this account either: every account here runs the same Claude Code.
531
+ if (cause === 'too-old') return `Claude Code on this computer is too old. To update it, ${UPDATE}.`;
326
532
  // A cause we cannot name points at the account, not at the app (2026-10-11).
327
533
  return 'Claude Code would not start on this account. Check that its plan is active at claude.ai.';
328
534
  }
@@ -361,6 +567,9 @@ export function engineTroubleNote(cause, engineWord = 'Codex') {
361
567
  if (cause === 'at-limit') return `${engineWord} is at its usage limit, so its tasks are waiting. They start again on their own when it resets.`;
362
568
  if (cause === 'workspace') return `${engineWord} would not run in the folder a task asked to work in. Open that folder in ${engineWord} once and allow it.`;
363
569
  if (cause === 'interrupted') return `${engineWord} runs keep getting cut off. ${Name} is trying them again on its own.`;
570
+ if (cause === 'network') return `${engineWord} cannot connect securely through this network, so its tasks are not moving. Set your company's proxy or certificate in your shell profile and reopen ${Name}.`;
571
+ if (cause === 'api-key') return `${engineWord} is signed in with an API key, which ${Name} never bills, so nothing runs on it. Open a terminal, run codex login, and sign in with ChatGPT.`;
572
+ if (cause === 'codex-config') return `${engineWord} could not read its config.toml, so nothing runs on it. Run codex in a terminal to see the line it points at, and fix it.`;
364
573
  return `${engineWord} sessions are dying on arrival, so its tasks are not moving. ${Name} keeps trying.`;
365
574
  }
366
575
 
@@ -473,9 +682,38 @@ const DEAD_RUN_OPENERS = {
473
682
  'at-limit': 'is at its usage limit, so nothing has been done on this task.',
474
683
  interrupted: 'was cut off in the middle, so nothing has been done on this task.',
475
684
  'model-not-on-plan': "could not use this task's model on that account's plan, so nothing has been done.",
685
+ 'level-not-offered': "could not run this task's model at the thinking level it asks for, so nothing has been done.",
686
+ network: 'could not connect securely through this network, so nothing has been done on this task.',
687
+ 'too-old': `on this computer is older than ${Name} needs, so nothing has been done on this task.`,
688
+ 'api-key': `is signed in with an API key, which ${Name} never bills, so nothing has been done.`,
689
+ 'codex-config': 'could not read its config.toml, so nothing has been done on this task.',
690
+ 'open-elsewhere': 'has this conversation open in another window, so nothing has been done here.',
691
+ // THE ONE OPENER WITH NO ENGINE IN FRONT OF IT: somebody who has set nothing
692
+ // up has never heard of Codex, and the plan screens never say the word.
693
+ 'no-plan': 'No AI plan is set up on this computer, so nothing has been done on this task.',
476
694
  unknown: 'stopped before it did anything, so this task has not been started.',
477
695
  };
478
696
 
697
+ // WHERE A PLAN GETS SET UP: the bar across the top of the inbox (`PlanBar`),
698
+ // which is the only way back into setup. Said once, for the four places that
699
+ // point at it.
700
+ const SET_UP_A_PLAN = 'at the top of your inbox';
701
+
702
+ // The same, for a run that used a tool before it died, so "nothing has been
703
+ // done" would be false. Kept out of DEAD_RUN_OPENERS so `deadRunCause` does
704
+ // not count the row among those nothing has started on. A cause missing here
705
+ // can only stop a run before its first tool, so on a run that worked it is a
706
+ // misreading and gets the plain line.
707
+ const WORKED_OPENERS = {
708
+ 'signed-out': 'was signed out partway through, so this task is not finished.',
709
+ 'org-blocked': 'was switched off for that account partway through, so this task is not finished.',
710
+ 'subscription-ended': "stopped partway through because that account's subscription ended.",
711
+ 'at-limit': 'hit its usage limit partway through, so this task is not finished.',
712
+ interrupted: 'was cut off partway through, so this task is not finished.',
713
+ network: 'lost its secure connection partway through, so this task is not finished.',
714
+ unknown: 'stopped partway through and did not finish this task.',
715
+ };
716
+
479
717
  /**
480
718
  * THE LINE IN A THREAD WHERE A RUN STOPPED, in our words.
481
719
  *
@@ -494,6 +732,12 @@ const RUN_STOPPED = {
494
732
  workspace: "This run stopped because it would not start in this project's folder.",
495
733
  interrupted: 'This run was cut off in the middle.',
496
734
  'model-not-on-plan': "This run stopped because that account's plan does not include the model.",
735
+ 'level-not-offered': 'This run stopped because the model does not offer that thinking level.',
736
+ network: 'This run stopped because this network would not let it connect securely.',
737
+ 'too-old': `This run stopped because Claude Code on this computer is older than ${Name} needs.`,
738
+ 'api-key': 'This run stopped because Codex is signed in with an API key.',
739
+ 'codex-config': 'This run stopped because Codex could not read its config.toml.',
740
+ 'open-elsewhere': 'This run stopped because the conversation is open in another Codex window.',
497
741
  unknown: 'This run stopped before it finished.',
498
742
  };
499
743
 
@@ -501,35 +745,135 @@ export function runStoppedLine(raw) {
501
745
  return RUN_STOPPED[troubleCause(raw)] ?? RUN_STOPPED.unknown;
502
746
  }
503
747
 
504
- // `elsewhere` is only read for 'model-not-on-plan': whether another signed-in
505
- // login's plan has the model, so the task moves there rather than waiting on her.
506
- export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null, elsewhere = false } = {}) {
507
- const opener = `${engineWord} ${DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown}`;
748
+ // A cause we cannot name still has a likeliest culprit, and it is the agent's
749
+ // plan, not the app (2026-10-19). Engines with no plan to point at get none.
750
+ const UNNAMED_CHECK = {
751
+ Codex: 'Check that your ChatGPT plan is active at chatgpt.com, or run codex in a terminal to see what it says. ',
752
+ 'Claude Code': 'Check that your Claude plan is active at claude.ai, or run claude in a terminal to see what it says. ',
753
+ };
754
+
755
+ function dayWords(ms) {
756
+ return new Date(ms).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
757
+ }
758
+
759
+ // What to do before replying to a thread the app has stopped trying on, in the
760
+ // same words `deadRunSentence` uses for each cause. Null when the reply is all.
761
+ function fixBeforeReplying({ cause, engineWord, resetsAt, model, planEnded }) {
762
+ if (cause === 'signed-out') return 'Sign that account back in.';
763
+ if (cause === 'no-plan') return `Set up a plan ${SET_UP_A_PLAN}.`;
764
+ if (cause === 'org-blocked') return 'Only an admin of that Claude workspace can turn it back on.';
765
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai.';
766
+ if (cause === 'at-limit') return resetsAt ? `The limit resets at ${resetsAt}.` : 'Wait for the limit to reset.';
767
+ if (cause === 'too-old') return `To update it, ${UPDATE}.`;
768
+ if (cause === 'api-key') return 'Run codex login in a terminal to sign in with your ChatGPT plan.';
769
+ if (cause === 'codex-config') return 'Run codex in a terminal to see the line it points at and fix it.';
770
+ if (cause === 'open-elsewhere') return 'Close it there.';
771
+ if (cause === 'model-not-on-plan') {
772
+ return planEnded
773
+ ? `That account's ChatGPT subscription ended on ${dayWords(planEnded)}, and the free plan does not include ${model || 'this model'}. Renew it at chatgpt.com, or pick another model for this task.`
774
+ : 'Pick another model for this task.';
775
+ }
776
+ if (cause === 'level-not-offered') return 'Pick another thinking level for this task.';
777
+ if (cause === 'network') return `If your company uses a proxy or its own certificate, ${NETWORK_FIX} and reopen ${Name}.`;
778
+ if (cause === 'workspace') return `Open this project's folder in ${engineWord} once and say yes to trusting it.`;
779
+ if (cause === 'unknown') return UNNAMED_CHECK[engineWord]?.trim() || null;
780
+ return null;
781
+ }
782
+
783
+ // NO SENTENCE HERE OFFERS ANOTHER ACCOUNT (w-dac993a554). There used to be an
784
+ // `elsewhere` flag: another signed-in login had room, or had the model, so the
785
+ // line promised the move instead of the wait. Nothing moves work between
786
+ // accounts any more, so a limit is named with the hour it resets and a model no
787
+ // plan includes is answered with another model.
788
+ //
789
+ // `planEnded` is when the account's paid ChatGPT plan ended (main/codex-account.mjs
790
+ // `codexPlanEnded`), which is why its list lacks `model`.
791
+ //
792
+ // `gaveUp` is the app having stopped carrying a reply (`settleDeliveryAndSay`).
793
+ // Nothing restarts the thread until somebody replies or resumes it, so the
794
+ // sentence asks for that and promises nothing of its own.
795
+ // `account` is the login this run was spending, already in words
796
+ // (shared/account-names.mjs). It is null on a Mac with one login, where naming
797
+ // it tells nobody anything, and then every sentence here is the one it has
798
+ // always been. There is no `instead`: nothing takes the task to another login
799
+ // (w-dac993a554), so a limit is named and waited out.
800
+ //
801
+ // `partway` is a run that was picking up a thread an earlier run worked on: it
802
+ // may have done nothing itself, but the task has been started. A cause with no
803
+ // partway words stopped this run before it began, and keeps its own opener.
804
+ //
805
+ // `retryAt` is when the rest ladder starts it again (`restingUntil` in
806
+ // main/supervisor.mjs), named only on the causes that wait for nothing else.
807
+ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null, worked: usedATool = false, partway = false, model = null, planEnded = null, gaveUp = false, account = null, retryAt = null, now = Date.now() } = {}) {
808
+ if (usedATool && !WORKED_OPENERS[cause]) cause = 'unknown';
809
+ const worked = usedATool || (partway && !!WORKED_OPENERS[cause]);
810
+ // A LIMIT IS A FACT ABOUT ONE LOGIN, so that is the one cause whose opener
811
+ // takes the name. The others are about the tool or the task.
812
+ const limitOpener = cause === 'at-limit' && account
813
+ ? (worked
814
+ ? `hit its usage limit on ${account} partway through, so this task is not finished.`
815
+ : `is at its usage limit on ${account}, so nothing has been done on this task.`)
816
+ : null;
817
+ const opener = cause === 'no-plan'
818
+ ? DEAD_RUN_OPENERS['no-plan']
819
+ : `${engineWord} ${limitOpener ?? (worked ? WORKED_OPENERS[cause] ?? WORKED_OPENERS.unknown : DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown)}`;
508
820
 
509
821
  const again = runs > 1 ? ` This has happened ${runs} times on this task.` : '';
510
822
 
823
+ if (gaveUp) {
824
+ const fix = fixBeforeReplying({ cause, engineWord, resetsAt, model, planEnded });
825
+ return `${opener}${again} ${fix ? `${fix} Then reply` : 'Reply'} here to start it again.`;
826
+ }
827
+
828
+ const when = retryAt > now ? ` at ${momentWords(retryAt, now)}` : '';
511
829
  // THE TIME THE LIMIT PRINTED, when there is one. "when the limit resets"
512
830
  // leaves her with a question; "at 6pm" is an answer, and it is the CLI's own
513
831
  // word for it rather than anything we worked out.
514
832
  const next = cause === 'signed-out'
515
833
  ? ' Sign that account back in and it picks straight up.'
834
+ : cause === 'no-plan'
835
+ ? ` Set one up ${SET_UP_A_PLAN} and it picks straight up.`
516
836
  : cause === 'org-blocked'
517
837
  ? ' Only an admin of that Claude workspace can turn it back on.'
518
838
  : cause === 'subscription-ended'
519
- ? ' Renew it at claude.ai, or pick another account in Settings.'
839
+ ? ' Renew it at claude.ai and it picks straight up.'
520
840
  : cause === 'at-limit'
521
841
  ? (resetsAt ? ` It starts again on its own at ${resetsAt}.` : ' It starts again on its own when the limit resets.')
842
+ : cause === 'too-old'
843
+ ? ` To update it, ${UPDATE}, and it starts again on its own.`
844
+ : cause === 'api-key'
845
+ ? ' Run codex login in a terminal to sign in with your ChatGPT plan, and it picks straight up.'
846
+ : cause === 'codex-config'
847
+ ? ` Run codex in a terminal to see the line it points at and fix it; ${Name} tries again on its own.`
848
+ : cause === 'open-elsewhere'
849
+ ? ` Close it there and ${Name} tries again on its own.`
522
850
  : cause === 'interrupted'
523
- ? ` ${Name} runs it again on its own.`
851
+ ? ` ${Name} runs it again on its own${when}.`
524
852
  : cause === 'model-not-on-plan'
525
- ? (elsewhere
526
- ? ` ${Name} runs it again on a Codex account whose plan has it.`
527
- : ' Pick another model for this task, or sign in a Codex account whose plan has it.')
528
- : ` ${Name} keeps trying, and it will keep failing the same way until this is fixed.`;
853
+ ? (planEnded
854
+ ? ` That account's ChatGPT subscription ended on ${dayWords(planEnded)}, and the free plan does not include ${model || 'this model'}. Renew it at chatgpt.com, or pick another model for this task.`
855
+ : ' Pick another model for this task.')
856
+ : cause === 'level-not-offered'
857
+ ? ' Pick another thinking level for this task and it will run.'
858
+ : cause === 'network'
859
+ ? ` It will keep failing until this is fixed: if your company uses a proxy or its own certificate, ${NETWORK_FIX} and reopen ${Name}.`
860
+ : worked
861
+ ? ` ${Name} tries it again on its own${when}.`
862
+ : cause === 'workspace'
863
+ ? ` ${Name} keeps trying, and it will keep failing the same way until this is fixed.`
864
+ : ` ${UNNAMED_CHECK[engineWord] ?? ''}${Name} tries again on its own${when}.`;
529
865
 
530
866
  return `${opener}${again}${next}`;
531
867
  }
532
868
 
869
+ // Whether `deadRunSentence` tells her the app runs the task again by itself.
870
+ // The fresh-work pass keeps a row in the queue on exactly these, so a row is
871
+ // never promised a retry and then read as an answer waiting on her.
872
+ export function deadRunPromisesARetry({ cause = 'unknown', gaveUp = false } = {}) {
873
+ if (gaveUp) return false;
874
+ return cause !== 'org-blocked' && cause !== 'level-not-offered' && cause !== 'model-not-on-plan';
875
+ }
876
+
533
877
  /* ------------- a run that finished and still delivered nothing ------------ */
534
878
  // WHAT GOES ON HER ROW WHEN THE RUN WAS FINE AND HER ANSWER STILL WENT NOWHERE.
535
879
  //
@@ -558,7 +902,43 @@ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', r
558
902
  export function undeliveredAnswerSentence({ engineWord = 'The agent', attempts = 1 } = {}) {
559
903
  const n = Math.max(1, Math.trunc(attempts ?? 1));
560
904
  const runs = n === 1 ? 'once and finished' : `${n} times and each run finished`;
561
- return `Your reply did not reach this task. ${engineWord} ran ${runs} without writing anything back on the row, so ${NAME} has stopped carrying it. Reply again and it goes to a fresh session.`;
905
+ return `${UNDELIVERED} ${engineWord} ran ${runs} without writing anything back on the row, so ${NAME} has stopped carrying it. Reply again and it goes to a fresh session.`;
906
+ }
907
+
908
+ const UNDELIVERED = 'Your reply did not reach this task.';
909
+
910
+ // A THREAD THE APP STOPPED STARTING (`_noteHowTheRunStarted` in
911
+ // main/supervisor.mjs). `tries` is how many times it has tried again on its
912
+ // own since, and `next` when it tries again, null once it has stopped trying.
913
+ const STOPPED_STARTING = `${Name} stopped starting this thread`;
914
+ const clock = (t) => new Date(t).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }).toLowerCase().replace(/\s/g, '');
915
+
916
+ export function stoppedStartingSentence({ starts = 6, minutes = 10, tries = 0, triedAt = null, said = null, next = null } = {}) {
917
+ const what = tries === 0
918
+ ? `It was started ${starts} times in ${minutes} minutes, and each run ended before its agent began.`
919
+ : tries === 1
920
+ ? `It tried again on its own${triedAt ? ` at ${clock(triedAt)}` : ''}, and that run ended before its agent began too.`
921
+ : `It tried again on its own ${tries === 2 ? 'twice' : `${tries} times`}, and each run ended before its agent began.`;
922
+ const last = said ? ` The last one said "${said.slice(0, 300)}".` : '';
923
+ const then = next ? ` It tries again on its own at ${clock(next)}. Reply here to try again now.` : ' Reply here to try again.';
924
+ return `${STOPPED_STARTING}. ${what}${last}${then}`;
925
+ }
926
+
927
+ // THE APP'S OWN WORD THAT A RUN GOT NOWHERE: a dead-run sentence, from a run
928
+ // that used a tool or one that did not, the reply that never landed, or a
929
+ // thread it stopped starting. On a thread carrying a reply, each is written
930
+ // only once nothing will carry the reply on its own, so that thread waits on a
931
+ // person (`gaveUpOnTheAnswer`, renderer/src/list-rules.ts).
932
+ const DEAD_RUN_HALVES = {
933
+ 'at-limit': ['is at its usage limit', 'so nothing has been done on this task.'],
934
+ };
935
+
936
+ export function saysARunGotNowhere(text) {
937
+ const s = String(text ?? '');
938
+ if (!s.trim()) return false;
939
+ if (deadRunCause(s)) return true;
940
+ if (Object.values(WORKED_OPENERS).some((opener) => s.includes(opener))) return true;
941
+ return s.startsWith(UNDELIVERED) || s.startsWith(STOPPED_STARTING);
562
942
  }
563
943
 
564
944
  // WHOSE SENTENCE IS ON THIS ROW, AND WHAT IT SAYS HAPPENED.
@@ -575,12 +955,20 @@ export function undeliveredAnswerSentence({ engineWord = 'The agent', attempts =
575
955
  // So a result is asked whose it is. Null means somebody wrote a real answer
576
956
  // there and the row is not stranded; a cause means the last word on that row is
577
957
  // still the app saying nothing ran.
958
+ //
959
+ // A NAMED LOGIN SITS INSIDE ONE OPENER ("is at its usage limit on
960
+ // alex@acme.com, so nothing has been done"), so that one is read back by its
961
+ // two halves. Without this a row stopped counting as one nothing had run on
962
+ // the moment it said which account was spent.
578
963
  export function deadRunCause(text) {
579
964
  const s = String(text ?? '');
580
965
  if (!s.trim()) return null;
581
966
  for (const [cause, opener] of Object.entries(DEAD_RUN_OPENERS)) {
582
967
  if (s.includes(opener)) return cause;
583
968
  }
969
+ for (const [cause, halves] of Object.entries(DEAD_RUN_HALVES)) {
970
+ if (halves.every((half) => s.includes(half))) return cause;
971
+ }
584
972
  return null;
585
973
  }
586
974
 
@@ -603,12 +991,17 @@ export function deadRunCause(text) {
603
991
  // where the shape has no room for a sentence, which is most of them.
604
992
  export function causeWord(cause) {
605
993
  if (cause === 'signed-out') return 'a signed-out login';
994
+ if (cause === 'no-plan') return 'no plan to run on';
606
995
  if (cause === 'org-blocked') return 'an organization that blocks Claude Code';
607
996
  if (cause === 'subscription-ended') return 'an ended Claude subscription';
608
997
  if (cause === 'at-limit') return 'your Claude limit';
609
998
  if (cause === 'workspace') return 'a folder Claude will not run in';
610
999
  if (cause === 'interrupted') return 'runs getting cut off';
611
1000
  if (cause === 'model-not-on-plan') return "a model the account's plan does not include";
1001
+ if (cause === 'level-not-offered') return 'a thinking level its model does not offer';
1002
+ if (cause === 'network') return 'a network that blocks secure connections';
1003
+ if (cause === 'too-old') return 'an out-of-date Claude Code';
1004
+ if (cause === 'open-elsewhere') return 'a conversation open in another Codex window';
612
1005
  return 'something we cannot name';
613
1006
  }
614
1007
 
@@ -618,12 +1011,17 @@ export function causeWord(cause) {
618
1011
  export function causeWhat(cause, count) {
619
1012
  const n = Math.max(0, Math.trunc(count ?? 0));
620
1013
  if (cause === 'signed-out') return `${n} stopped by a signed-out Claude login`;
1014
+ if (cause === 'no-plan') return `${n} waiting on a plan to run on`;
621
1015
  if (cause === 'org-blocked') return `${n} stopped by an organization that blocks Claude Code`;
622
1016
  if (cause === 'subscription-ended') return `${n} stopped by an ended Claude subscription`;
623
1017
  if (cause === 'at-limit') return `${n} waiting on your Claude limit`;
624
1018
  if (cause === 'workspace') return `${n} in a folder Claude will not run in`;
625
1019
  if (cause === 'interrupted') return `${n} cut off mid-run`;
626
1020
  if (cause === 'model-not-on-plan') return `${n} asking for a model no signed-in Codex plan includes`;
1021
+ if (cause === 'level-not-offered') return `${n} asking for a thinking level its model does not offer`;
1022
+ if (cause === 'network') return `${n} stopped by a network that blocks secure connections`;
1023
+ if (cause === 'too-old') return `${n} stopped by an out-of-date Claude Code`;
1024
+ if (cause === 'open-elsewhere') return `${n} open in another Codex window`;
627
1025
  return `${n} stopped for a reason we cannot name`;
628
1026
  }
629
1027
 
@@ -631,12 +1029,17 @@ export function causeWhat(cause, count) {
631
1029
  // comes from the limit line the CLI printed, never from our own clock.
632
1030
  export function causeNext(cause, resetsAt = null) {
633
1031
  if (cause === 'signed-out') return 'Type /login in a terminal and they pick straight up.';
1032
+ if (cause === 'no-plan') return `Set one up ${SET_UP_A_PLAN}.`;
634
1033
  if (cause === 'org-blocked') return 'Ask an admin of that Claude workspace to turn Claude Code back on.';
635
- if (cause === 'subscription-ended') return 'Renew it at claude.ai, or pick another account in Settings.';
1034
+ if (cause === 'subscription-ended') return 'Renew it at claude.ai and they pick straight up.';
636
1035
  if (cause === 'at-limit') return resetsAt ? `They start again on their own at ${resetsAt}.` : 'They start again on their own when it resets.';
637
1036
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
638
1037
  if (cause === 'interrupted') return `Nothing to do. ${Name} is trying them again.`;
639
- if (cause === 'model-not-on-plan') return 'Pick another model on each, or sign in a Codex account whose plan has it.';
1038
+ if (cause === 'model-not-on-plan') return 'Pick another model on each.';
1039
+ if (cause === 'level-not-offered') return 'Pick another thinking level on each.';
1040
+ if (cause === 'network') return troubleRemedy('network');
1041
+ if (cause === 'too-old') return troubleRemedy('too-old');
1042
+ if (cause === 'open-elsewhere') return 'Close them in the other Codex window.';
640
1043
  return `${Name} keeps trying on its own.`;
641
1044
  }
642
1045
 
@@ -665,7 +1068,7 @@ export function causeBreakdown(rows = [], resetsAt = null) {
665
1068
  what: causeWhat(cause, count),
666
1069
  next: causeNext(cause, cause === 'at-limit' ? resetsAt : null),
667
1070
  act: causeAct(cause),
668
- hers: needsHerHands(cause) || cause === 'workspace' || cause === 'model-not-on-plan',
1071
+ hers: needsHerHands(cause) || cause === 'workspace' || cause === 'model-not-on-plan' || cause === 'level-not-offered' || cause === 'network' || cause === 'open-elsewhere',
669
1072
  }))
670
1073
  .sort((a, b) => (b.hers - a.hers) || (b.count - a.count) || a.cause.localeCompare(b.cause));
671
1074
  }