agentbox-app 0.1.11 → 0.1.13

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 (239) hide show
  1. package/README.md +24 -16
  2. package/bin/agentbox.mjs +18 -9
  3. package/briefs/message-rules.md +15 -4
  4. package/main/account-tooling.mjs +2 -1
  5. package/main/acp.mjs +296 -0
  6. package/main/agent-homes.mjs +158 -0
  7. package/main/agent-sessions.mjs +57 -20
  8. package/main/agent-updates.mjs +9 -20
  9. package/main/analytics.mjs +30 -0
  10. package/main/approvals.mjs +14 -5
  11. package/main/claude-bin.mjs +45 -310
  12. package/main/claude-input.mjs +9 -3
  13. package/main/claude-launch-env.mjs +54 -0
  14. package/main/claude-models.mjs +39 -7
  15. package/main/claude-plan.mjs +14 -2
  16. package/main/claude-usage.mjs +82 -25
  17. package/main/codex-bin.mjs +51 -248
  18. package/main/codex-launch-env.mjs +31 -0
  19. package/main/codex-session.mjs +38 -3
  20. package/main/codex-threads.mjs +41 -8
  21. package/main/codex-watch.mjs +67 -3
  22. package/main/config.mjs +40 -13
  23. package/main/corner-tag.mjs +489 -0
  24. package/main/crash-report.mjs +26 -3
  25. package/main/demo.mjs +17 -14
  26. package/main/engine-bin.mjs +595 -0
  27. package/main/engine-setup.mjs +27 -27
  28. package/main/feedback.mjs +11 -6
  29. package/main/fresh-user.mjs +1 -1
  30. package/main/git-change.mjs +8 -2
  31. package/main/harnesses/claude-stream.mjs +208 -0
  32. package/main/harnesses/claude.mjs +84 -0
  33. package/main/harnesses/codex.mjs +65 -0
  34. package/main/harnesses/common.mjs +78 -0
  35. package/main/harnesses/instructions-file.mjs +116 -0
  36. package/main/harnesses/opencode.mjs +234 -0
  37. package/main/harnesses.mjs +65 -0
  38. package/main/importable-threads.mjs +38 -0
  39. package/main/instruction-settings.mjs +47 -11
  40. package/main/ipc.mjs +175 -22
  41. package/main/local-app-frames.mjs +82 -0
  42. package/main/machine.mjs +1 -1
  43. package/main/main.mjs +120 -5
  44. package/main/memory-gate-history.mjs +51 -12
  45. package/main/memory-gate-server.mjs +67 -11
  46. package/main/memory-gate.mjs +49 -8
  47. package/main/memory-log.mjs +124 -0
  48. package/main/notify.mjs +48 -5
  49. package/main/opencode-server.mjs +271 -0
  50. package/main/opencode.mjs +287 -0
  51. package/main/phone-link.mjs +551 -0
  52. package/main/refresh-agent-models.mjs +2 -1
  53. package/main/relay-address.mjs +28 -0
  54. package/main/relay-client.mjs +246 -0
  55. package/main/row-label.mjs +8 -7
  56. package/main/runs-on.mjs +90 -0
  57. package/main/serve.mjs +124 -10
  58. package/main/settings.mjs +171 -11
  59. package/main/shell-path.mjs +86 -0
  60. package/main/ship-queue.mjs +25 -7
  61. package/main/sign-in-files.mjs +3 -7
  62. package/main/source-updater.mjs +27 -1
  63. package/main/start-hold.mjs +73 -0
  64. package/main/store/home.mjs +37 -5
  65. package/main/store/project.mjs +15 -19
  66. package/main/store.mjs +10 -4
  67. package/main/supervisor.mjs +1030 -447
  68. package/main/task-commands.mjs +13 -17
  69. package/main/task-folders-offthread.mjs +6 -1
  70. package/main/task-folders.mjs +491 -82
  71. package/main/task-terminals.mjs +49 -8
  72. package/main/team/index.mjs +2 -2
  73. package/main/team/projects.mjs +1 -1
  74. package/main/updater.mjs +71 -9
  75. package/main/worktree-include.mjs +494 -0
  76. package/mcp/core/page.mjs +1 -1
  77. package/mcp/core/work.mjs +34 -3
  78. package/mcp/tools.mjs +2 -1
  79. package/package.json +10 -1
  80. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  81. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  82. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  83. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  84. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  85. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  86. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  87. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  88. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  89. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  90. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  91. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  92. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  93. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  94. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  95. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  96. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  97. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  98. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  99. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  100. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  101. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  102. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  103. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  104. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  105. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  106. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  107. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  108. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  109. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  110. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  111. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  112. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  113. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  114. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  115. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  116. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  117. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  118. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  119. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  120. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  121. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  122. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  123. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  124. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  125. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  126. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  127. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  128. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  129. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  130. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  131. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  132. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  133. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  134. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  135. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  136. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  137. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  138. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  139. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  140. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  141. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  142. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  143. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  144. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  145. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  146. package/renderer/dist/corner-tag.html +13 -0
  147. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  148. package/renderer/dist/emulator/data/compression/README.md +11 -0
  149. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  150. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  151. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  152. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  153. package/renderer/dist/emulator/data/cores/README.md +22 -0
  154. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  155. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  156. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  157. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  158. package/renderer/dist/emulator/data/cores/package.json +65 -0
  159. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  160. package/renderer/dist/emulator/data/emulator.css +1614 -0
  161. package/renderer/dist/emulator/data/loader.js +170 -0
  162. package/renderer/dist/emulator/data/localization/README.md +63 -0
  163. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  164. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  165. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  166. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  167. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  168. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  169. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  170. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  171. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  172. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  173. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  174. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  175. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  176. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  177. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  178. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  179. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  180. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  181. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  182. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  183. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  184. package/renderer/dist/emulator/data/src/compression.js +142 -0
  185. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  186. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  187. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  188. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  189. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  190. package/renderer/dist/emulator/data/src/storage.js +125 -0
  191. package/renderer/dist/emulator/data/version.json +5 -0
  192. package/renderer/dist/emulator/host.js +297 -0
  193. package/renderer/dist/emulator/index.html +28 -0
  194. package/renderer/dist/emulator/provenance.json +6 -0
  195. package/renderer/dist/index.html +16 -3
  196. package/scripts/after-install.mjs +14 -0
  197. package/scripts/approval-server.sh +31 -7
  198. package/scripts/before-publish.mjs +12 -10
  199. package/shared/agent-import.mjs +3 -1
  200. package/shared/agents.d.mts +1 -0
  201. package/shared/bridge-map.mjs +19 -0
  202. package/shared/claude-commands.generated.mjs +8 -6
  203. package/shared/claude-models.generated.mjs +10 -8
  204. package/shared/codex-modes.mjs +1 -1
  205. package/shared/corner-tag.d.mts +14 -0
  206. package/shared/corner-tag.mjs +141 -0
  207. package/shared/demo-world.mjs +1 -1
  208. package/shared/engines.mjs +20 -5
  209. package/shared/fresh-user-home.mjs +12 -1
  210. package/shared/harness-definitions.mjs +103 -0
  211. package/shared/instruction-defaults.json +1 -1
  212. package/shared/notify-rules.d.mts +25 -0
  213. package/shared/notify-rules.mjs +19 -0
  214. package/shared/project-folder-check.mjs +3 -3
  215. package/shared/provider-commands.mjs +10 -2
  216. package/shared/rank.d.mts +2 -0
  217. package/shared/rank.mjs +50 -0
  218. package/shared/relay-address.mjs +6 -0
  219. package/shared/relay-crypto.mjs +165 -0
  220. package/shared/runs-on.d.mts +10 -0
  221. package/shared/runs-on.mjs +55 -0
  222. package/shared/shortcuts.mjs +11 -5
  223. package/shared/spawn-trouble.mjs +58 -7
  224. package/shared/terminal-input.d.mts +1 -0
  225. package/shared/terminal-input.mjs +30 -0
  226. package/shared/terminal-keys.d.mts +1 -0
  227. package/shared/terminal-keys.mjs +16 -0
  228. package/shared/terminal-size.d.mts +4 -0
  229. package/shared/terminal-size.mjs +29 -0
  230. package/shared/usage.d.mts +3 -0
  231. package/shared/usage.mjs +7 -0
  232. package/shared/where-it-runs-from.d.mts +43 -0
  233. package/shared/where-it-runs-from.mjs +155 -0
  234. package/shared/work-items.mjs +28 -2
  235. package/shared/work-lines.mjs +1 -1
  236. package/shared/xterm-invalid-range.d.mts +2 -0
  237. package/shared/xterm-invalid-range.mjs +58 -0
  238. package/renderer/dist/assets/index-B-2Az952.css +0 -32
  239. package/renderer/dist/assets/index-D6qncgai.js +0 -1168
@@ -133,7 +133,11 @@ const INTERRUPTED = [
133
133
  /connection (closed|lost|dropped)/i,
134
134
  /response stopped arriving/i,
135
135
  /may be incomplete/i,
136
- /econnreset|enotfound|etimedout|econnrefused/i,
136
+ /econnreset|enotfound|etimedout|econnrefused|enetunreach|ehostunreach|eai_again/i,
137
+ // Codex also prefixes non-network failures with "stream disconnected".
138
+ // Match the request failure itself so incomplete responses keep their limit.
139
+ /error sending request for url/i,
140
+ /failed to send websocket request/i,
137
141
  /unable to connect/i,
138
142
  /can.{0,3}t reach the api/i,
139
143
  /timed out/i,
@@ -164,6 +168,18 @@ const WORKSPACE = [
164
168
  /skip-git-repo-check/i,
165
169
  ];
166
170
 
171
+ // A MODEL THE LOGIN'S PLAN DOES NOT INCLUDE. Not the account being out and not
172
+ // the model being unknown: the same login runs every other model, and another
173
+ // login on a bigger plan runs this one. Codex's own words, off two dead runs on
174
+ // 2026-10-09 and 2026-10-11 (codex-cli 0.160.0, a free-plan login handed
175
+ // 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.
178
+ const MODEL_NOT_ON_PLAN = [
179
+ /model is not supported when using codex with a chatgpt account/i,
180
+ /not a model the plan of the codex account/i,
181
+ ];
182
+
167
183
  // WHICH OF THE THREE THINGS WENT WRONG, from whatever the tool printed.
168
184
  // 'unknown' is a first-class answer, not a failure: an honest generic sentence
169
185
  // beats a confident wrong one.
@@ -173,6 +189,8 @@ export function troubleCause(raw) {
173
189
  // FIRST, because an organization's refusal mentions the subscription and
174
190
  // reads as a signed-out login to the list below.
175
191
  if (ORG_BLOCKED.some((re) => re.test(text))) return 'org-blocked';
192
+ // BEFORE SIGNED-OUT, because Codex's sentence says "ChatGPT account".
193
+ if (MODEL_NOT_ON_PLAN.some((re) => re.test(text))) return 'model-not-on-plan';
176
194
  if (SIGNED_OUT.some((re) => re.test(text))) return 'signed-out';
177
195
  if (AT_LIMIT.some((re) => re.test(text))) return 'at-limit';
178
196
  if (WORKSPACE.some((re) => re.test(text))) return 'workspace';
@@ -184,6 +202,16 @@ export function troubleCause(raw) {
184
202
  return 'unknown';
185
203
  }
186
204
 
205
+ // A SUBSCRIPTION THAT HAS ENDED IS REFUSED IN AN ADMIN'S WORDS. Claude Code
206
+ // 2.1.293 told a lapsed personal Max login "Your organization has disabled
207
+ // Claude subscription access for Claude Code" (2026-10-11), the same line a
208
+ // workspace policy prints, so the text alone cannot tell them apart. The
209
+ // login's own file can: `plan` is main/claude-plan.mjs's reading of it, and
210
+ // `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;
213
+ }
214
+
187
215
  // WHEN THE LIMIT LETS GO, straight off the line the CLI printed.
188
216
  //
189
217
  // carries the one thing she actually wants to know, and we were throwing it
@@ -237,7 +265,7 @@ export function needsHerHands(cause) {
237
265
  // same person: one is her, in a terminal; one is an admin of a workspace she
238
266
  // may not run. What they share is the only thing this question asks — trying
239
267
  // again in thirty minutes cannot possibly help.
240
- return cause === 'signed-out' || cause === 'org-blocked';
268
+ return cause === 'signed-out' || cause === 'org-blocked' || cause === 'subscription-ended';
241
269
  }
242
270
 
243
271
  // WHAT SHE READS WHEN NOT ONE AGENT CAN START ANYWHERE.
@@ -246,10 +274,11 @@ export function needsHerHands(cause) {
246
274
  export function troubleSentence(cause) {
247
275
  if (cause === 'signed-out') return 'No agents can start. Your Claude login has run out.';
248
276
  if (cause === 'org-blocked') return 'No agents can start. Your organization has turned Claude Code off.';
277
+ if (cause === 'subscription-ended') return 'No agents can start. Your Claude subscription has ended.';
249
278
  if (cause === 'at-limit') return 'No agents can start. Your Claude plan is at its limit.';
250
279
  if (cause === 'workspace') return 'No agents can start. Claude will not run in this folder.';
251
280
  if (cause === 'interrupted') return 'No agents can start. The connection to Claude keeps dropping.';
252
- return 'No agents can start on this Mac right now.';
281
+ return 'No agents can start on this computer right now.';
253
282
  }
254
283
 
255
284
  // THE SECOND LINE: what happens next, so the first line is never a dead end.
@@ -260,6 +289,7 @@ export function troubleRemedy(cause) {
260
289
  // thing to type.
261
290
  if (cause === 'signed-out') return 'Open Claude in a terminal, type /login, and they pick straight back up.';
262
291
  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.';
263
293
  if (cause === 'at-limit') return 'They start again on their own when it resets.';
264
294
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
265
295
  if (cause === 'interrupted') return `Nothing to do. ${Name} tries them again on its own.`;
@@ -289,10 +319,12 @@ export function accountSentence(cause, dir) {
289
319
  if (cause === 'org-blocked') {
290
320
  return 'Signed in, but its organization has turned Claude Code off. Only an admin of that workspace can turn it back on.';
291
321
  }
322
+ if (cause === 'subscription-ended') return 'Signed in, but its Claude subscription has ended. Renew it at claude.ai to use this account again.';
292
323
  if (cause === 'at-limit') return 'At its limit. It rejoins on its own when the limit resets.';
293
324
  if (cause === 'workspace') return 'Claude would not run in the folder this was asked to work in.';
294
325
  if (cause === 'interrupted') return `Its runs keep getting cut off. ${Name} is trying them again.`;
295
- return `Its sessions are dying on arrival. ${Name} is keeping the other one busy.`;
326
+ // A cause we cannot name points at the account, not at the app (2026-10-11).
327
+ return 'Claude Code would not start on this account. Check that its plan is active at claude.ai.';
296
328
  }
297
329
 
298
330
  /* ------------------ the same fact about a whole ENGINE -------------------- */
@@ -437,8 +469,10 @@ const DEAD_RUN_OPENERS = {
437
469
  workspace: "would not start in this project's folder, so nothing has been done here.",
438
470
  'signed-out': 'could not sign in, so nothing has been done on this task.',
439
471
  'org-blocked': 'is switched off for that account by its organization, so nothing has been done.',
472
+ 'subscription-ended': "cannot run because that account's Claude subscription has ended, so nothing has been done.",
440
473
  'at-limit': 'is at its usage limit, so nothing has been done on this task.',
441
474
  interrupted: 'was cut off in the middle, so nothing has been done on this task.',
475
+ 'model-not-on-plan': "could not use this task's model on that account's plan, so nothing has been done.",
442
476
  unknown: 'stopped before it did anything, so this task has not been started.',
443
477
  };
444
478
 
@@ -453,10 +487,13 @@ const DEAD_RUN_OPENERS = {
453
487
  */
454
488
  const RUN_STOPPED = {
455
489
  'signed-out': 'This run stopped because the account was signed out.',
456
- 'org-blocked': 'This run stopped because that account is switched off by its organization.',
490
+ // Same refusal text for an admin's block and an ended subscription, and this
491
+ // line has only the text, so it says what is true of both.
492
+ 'org-blocked': 'This run stopped because Claude Code will not run on that account.',
457
493
  'at-limit': 'This run stopped because the account hit its usage limit.',
458
494
  workspace: "This run stopped because it would not start in this project's folder.",
459
495
  interrupted: 'This run was cut off in the middle.',
496
+ 'model-not-on-plan': "This run stopped because that account's plan does not include the model.",
460
497
  unknown: 'This run stopped before it finished.',
461
498
  };
462
499
 
@@ -464,7 +501,9 @@ export function runStoppedLine(raw) {
464
501
  return RUN_STOPPED[troubleCause(raw)] ?? RUN_STOPPED.unknown;
465
502
  }
466
503
 
467
- export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', runs = 1, resetsAt = null } = {}) {
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 } = {}) {
468
507
  const opener = `${engineWord} ${DEAD_RUN_OPENERS[cause] ?? DEAD_RUN_OPENERS.unknown}`;
469
508
 
470
509
  const again = runs > 1 ? ` This has happened ${runs} times on this task.` : '';
@@ -476,10 +515,16 @@ export function deadRunSentence({ engineWord = 'The agent', cause = 'unknown', r
476
515
  ? ' Sign that account back in and it picks straight up.'
477
516
  : cause === 'org-blocked'
478
517
  ? ' Only an admin of that Claude workspace can turn it back on.'
518
+ : cause === 'subscription-ended'
519
+ ? ' Renew it at claude.ai, or pick another account in Settings.'
479
520
  : cause === 'at-limit'
480
521
  ? (resetsAt ? ` It starts again on its own at ${resetsAt}.` : ' It starts again on its own when the limit resets.')
481
522
  : cause === 'interrupted'
482
523
  ? ` ${Name} runs it again on its own.`
524
+ : 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.')
483
528
  : ` ${Name} keeps trying, and it will keep failing the same way until this is fixed.`;
484
529
 
485
530
  return `${opener}${again}${next}`;
@@ -559,9 +604,11 @@ export function deadRunCause(text) {
559
604
  export function causeWord(cause) {
560
605
  if (cause === 'signed-out') return 'a signed-out login';
561
606
  if (cause === 'org-blocked') return 'an organization that blocks Claude Code';
607
+ if (cause === 'subscription-ended') return 'an ended Claude subscription';
562
608
  if (cause === 'at-limit') return 'your Claude limit';
563
609
  if (cause === 'workspace') return 'a folder Claude will not run in';
564
610
  if (cause === 'interrupted') return 'runs getting cut off';
611
+ if (cause === 'model-not-on-plan') return "a model the account's plan does not include";
565
612
  return 'something we cannot name';
566
613
  }
567
614
 
@@ -572,9 +619,11 @@ export function causeWhat(cause, count) {
572
619
  const n = Math.max(0, Math.trunc(count ?? 0));
573
620
  if (cause === 'signed-out') return `${n} stopped by a signed-out Claude login`;
574
621
  if (cause === 'org-blocked') return `${n} stopped by an organization that blocks Claude Code`;
622
+ if (cause === 'subscription-ended') return `${n} stopped by an ended Claude subscription`;
575
623
  if (cause === 'at-limit') return `${n} waiting on your Claude limit`;
576
624
  if (cause === 'workspace') return `${n} in a folder Claude will not run in`;
577
625
  if (cause === 'interrupted') return `${n} cut off mid-run`;
626
+ if (cause === 'model-not-on-plan') return `${n} asking for a model no signed-in Codex plan includes`;
578
627
  return `${n} stopped for a reason we cannot name`;
579
628
  }
580
629
 
@@ -583,9 +632,11 @@ export function causeWhat(cause, count) {
583
632
  export function causeNext(cause, resetsAt = null) {
584
633
  if (cause === 'signed-out') return 'Type /login in a terminal and they pick straight up.';
585
634
  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.';
586
636
  if (cause === 'at-limit') return resetsAt ? `They start again on their own at ${resetsAt}.` : 'They start again on their own when it resets.';
587
637
  if (cause === 'workspace') return 'Open the folder in Claude once and say yes to trusting it.';
588
638
  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.';
589
640
  return `${Name} keeps trying on its own.`;
590
641
  }
591
642
 
@@ -614,7 +665,7 @@ export function causeBreakdown(rows = [], resetsAt = null) {
614
665
  what: causeWhat(cause, count),
615
666
  next: causeNext(cause, cause === 'at-limit' ? resetsAt : null),
616
667
  act: causeAct(cause),
617
- hers: needsHerHands(cause) || cause === 'workspace',
668
+ hers: needsHerHands(cause) || cause === 'workspace' || cause === 'model-not-on-plan',
618
669
  }))
619
670
  .sort((a, b) => (b.hers - a.hers) || (b.count - a.count) || a.cause.localeCompare(b.cause));
620
671
  }
@@ -0,0 +1 @@
1
+ export function inputQueue(send: (data: string) => Promise<unknown>, options?: { chunk?: number; onError?: (e: unknown) => void }): { push(data: string): void };
@@ -0,0 +1,30 @@
1
+ // KEYS TYPED WHILE ONE WRITE IS ON ITS WAY GO OUT TOGETHER, as the next write.
2
+ //
3
+ // The panel used to send every key as its own round trip to main, each one
4
+ // waiting for the last, so holding Delete built a line of trips that drained
5
+ // after she let go. Now the first key goes at once, and whatever she types
6
+ // while it travels is one write behind it, in order. A paste still goes in
7
+ // pieces main accepts (64 KB is its limit; `chunk` stays well under it).
8
+ //
9
+ // A write that fails (the shell exited) drops what was waiting behind it: those
10
+ // keys were for a shell that is gone, and the failure is reported once.
11
+ export function inputQueue(send, { chunk = 16384, onError } = {}) {
12
+ let queued = '', sending = false;
13
+ async function drain() {
14
+ if (sending) return;
15
+ sending = true;
16
+ try {
17
+ while (queued) {
18
+ const data = queued.slice(0, chunk);
19
+ queued = queued.slice(data.length);
20
+ await send(data);
21
+ }
22
+ } catch (e) {
23
+ queued = '';
24
+ onError?.(e);
25
+ } finally {
26
+ sending = false;
27
+ }
28
+ }
29
+ return { push(data) { queued += data; void drain(); } };
30
+ }
@@ -0,0 +1 @@
1
+ export function optionWordKey(event: { type: string; key: string; altKey: boolean; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean }, isMac: boolean): string | undefined;
@@ -0,0 +1,16 @@
1
+ // OPTION+LEFT AND OPTION+RIGHT JUMP A WORD, as they do in Mac Terminal.
2
+ // xterm 6 sends them as ESC[1;3D and ESC[1;3C, which zsh and bash do not bind,
3
+ // so a shell typed `;3D` into the line instead. Mac Terminal sends ESC b and
4
+ // ESC f, the backward-word and forward-word every shell already knows.
5
+ //
6
+ // Returns what to send for this key, '' to swallow the keyup of one we
7
+ // handled, or undefined to leave the key to xterm. Off a Mac nothing changes:
8
+ // Linux terminals send the same ESC[1;3D xterm does.
9
+ const WORD = { ArrowLeft: '\x1bb', ArrowRight: '\x1bf' };
10
+
11
+ export function optionWordKey(event, isMac) {
12
+ if (!isMac || !event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return undefined;
13
+ const sequence = WORD[event.key];
14
+ if (!sequence) return undefined;
15
+ return event.type === 'keydown' ? sequence : '';
16
+ }
@@ -0,0 +1,4 @@
1
+ export const SIZE_KEY: string;
2
+ export const TERMINAL_MIN: { height: number; width: number };
3
+ export function dragSize(dimension: 'height' | 'width', start: number, delta: number, room: number): number;
4
+ export function readSavedSize(raw: string | null | undefined): { height?: number; width?: number };
@@ -0,0 +1,29 @@
1
+ // HOW BIG THE TERMINAL MAY BE DRAGGED (renderer/src/components/TaskTerminal.tsx).
2
+ //
3
+ // Small enough still shows a few lines; large enough still leaves the page it
4
+ // sits in a strip to read (the thread's title and a line or two above it, or a
5
+ // column of the conversation beside it). The size is one for the whole app,
6
+ // remembered under SIZE_KEY, the way an editor remembers its panel.
7
+ export const SIZE_KEY = 'zero.terminal.size';
8
+ export const TERMINAL_MIN = { height: 120, width: 280 };
9
+ const LEAVE = { height: 120, width: 320 };
10
+ const LARGEST = 4000;
11
+
12
+ /** `start` px, the edge moved `delta` px toward growing, in a pane `room` px across. */
13
+ export function dragSize(dimension, start, delta, room) {
14
+ const min = TERMINAL_MIN[dimension];
15
+ const max = Math.max(min, room - LEAVE[dimension]);
16
+ return Math.round(Math.min(max, Math.max(min, start + delta)));
17
+ }
18
+
19
+ export function readSavedSize(raw) {
20
+ let saved;
21
+ try { saved = JSON.parse(raw ?? ''); } catch { return {}; }
22
+ if (!saved || typeof saved !== 'object' || Array.isArray(saved)) return {};
23
+ const out = {};
24
+ for (const dimension of ['height', 'width']) {
25
+ const v = saved[dimension];
26
+ if (Number.isFinite(v) && v >= TERMINAL_MIN[dimension] && v <= LARGEST) out[dimension] = v;
27
+ }
28
+ return out;
29
+ }
@@ -3,6 +3,9 @@
3
3
  // rule about which coding agent the corner is about, the zone primitives the
4
4
  // Claude reader is built on, and the words the panel prints.
5
5
 
6
+ /** What a usage check that got nothing readable says, for any cause we have not named. */
7
+ export const USAGE_UNREADABLE: string;
8
+
6
9
  export interface UsageLimit {
7
10
  span: 'session' | 'week';
8
11
  qualifier: string | null;
package/shared/usage.mjs CHANGED
@@ -33,6 +33,13 @@ import { DEFAULT_ENGINE } from './engines.mjs';
33
33
 
34
34
  /* ------------------ WHICH CODING AGENT THE CORNER IS ABOUT ---------------- */
35
35
 
36
+ // WHAT A USAGE CHECK THAT GOT NOTHING READABLE SAYS, for every reason we have
37
+ // not named. It used to be "Could not refresh usage. Try again.", which reads
38
+ // as the app having broken; the measured case (2026-10-11) was an account whose
39
+ // subscription had ended, and Claude Code simply printed no limits for it. So
40
+ // it points at the account, which is where an unnamed cause almost always is.
41
+ export const USAGE_UNREADABLE = "Claude Code didn't report usage for this account. Check that its plan is active at claude.ai.";
42
+
36
43
  /**
37
44
  * THE AGENT MOST OF HER LIVE WORK IS ON, or the workspace's own when nothing is
38
45
  * running and when the fleet is level.
@@ -0,0 +1,43 @@
1
+ // Types for shared/where-it-runs-from.mjs, which is plain ESM so that main
2
+ // (Node) and the screen (Vite) share one copy of the rule about whether this
3
+ // copy of the app can update itself from where it is, and one copy of the
4
+ // words that say so. The screen asks for the words and `sayWhere`; the places
5
+ // and the move are main's.
6
+
7
+ /** The place a copy of the app is in, as far as updating itself is concerned. */
8
+ export type AppPlace =
9
+ | 'disk-image' | 'translocated' | 'applications' | 'elsewhere' | 'source' | 'not-mac';
10
+
11
+ export const PLACE: {
12
+ diskImage: 'disk-image';
13
+ translocated: 'translocated';
14
+ applications: 'applications';
15
+ elsewhere: 'elsewhere';
16
+ source: 'source';
17
+ notMac: 'not-mac';
18
+ };
19
+
20
+ export const MOVE_SAY: {
21
+ cannotUpdateHere: string;
22
+ button: string;
23
+ title: string;
24
+ where: Record<string, string>;
25
+ shortWhere: Record<string, string>;
26
+ fix: string;
27
+ retry: string;
28
+ };
29
+
30
+ /** The sentence for a place, or null where there is nothing to say. */
31
+ export function sayWhere(place: string | null | undefined, how?: { short?: boolean }): string | null;
32
+
33
+ export function placeOfApp(facts?: {
34
+ platform?: string;
35
+ packaged?: boolean;
36
+ exePath?: string | null;
37
+ inApplications?: boolean;
38
+ }): { place: AppPlace; canUpdate: boolean; offerMove: boolean };
39
+
40
+ export function createPlace(host: { app: unknown; log?: unknown }): {
41
+ state(): { place: AppPlace; canUpdate: boolean; offerMove: boolean };
42
+ move(): { moved: boolean; error: string | null };
43
+ };
@@ -0,0 +1,155 @@
1
+ // AN APP IN THE WRONG PLACE CAN NEVER UPDATE ITSELF, AND NEVER SAID SO.
2
+ //
3
+ // macOS has two places it will happily run Agentbox from and never let it
4
+ // replace itself:
5
+ //
6
+ // THE DISK IMAGE. You double-click the .dmg, you double-click the app
7
+ // inside it, and it works. The volume is read-only, so Squirrel.Mac refuses
8
+ // the swap with "Cannot update while running on a read-only volume".
9
+ //
10
+ // A TRANSLOCATED COPY. You drag the app to Downloads and open it. Because
11
+ // it still carries the quarantine flag and is not in Applications, macOS
12
+ // runs it from a read-only copy it makes under
13
+ // `/private/var/folders/…/AppTranslocation/<uuid>/d/`. Same refusal, and
14
+ // this is why Squirrel's own sentence talks about the Downloads directory.
15
+ //
16
+ // Measured in PostHog on 2026-10-12: 23 crash reports over three weeks, two
17
+ // installs, both on 0.1.11, one report every six hours (`EVERY_MS` in
18
+ // main/updater.mjs) for as long as the app was open. The reports stopped being
19
+ // filed as crashes in 292d419. The two Macs are still on 0.1.11.
20
+ //
21
+ // SO THE APP READS WHERE IT IS AND OFFERS THE ONE PRESS THAT FIXES IT. Electron
22
+ // has had `moveToApplicationsFolder` since 4: it moves the bundle, replaces an
23
+ // older copy if you say so, and relaunches from the new place. After that the
24
+ // ordinary six-hourly check works and the person never thinks about it again.
25
+ //
26
+ // EVERYTHING HERE IS A FACT AND NOT A GUESS, which is why the decision is a
27
+ // pure function with the four facts passed in: a wrong "you are in the wrong
28
+ // place" is a nag on a healthy install, and two of those are worse than the
29
+ // silence this replaces.
30
+
31
+ import { Name } from './product-name.mjs';
32
+
33
+ // THE WORDS, IN ONE PLACE, and main and the renderer both read them from here.
34
+ // `cannotUpdateHere` is the sentence `shortError` in main/updater.mjs puts in
35
+ // place of Squirrel's own paragraph, which is 240 characters with a URL in the
36
+ // middle and was arriving on the Settings row cut off at 160.
37
+ //
38
+ // Every sentence says where the app is and what one press does about it, and
39
+ // none of them says "read-only volume": that is the mechanism, and nobody
40
+ // holding the Mac asked about the mechanism.
41
+ export const MOVE_SAY = {
42
+ cannotUpdateHere: `${Name} cannot update itself from where it is.`,
43
+ button: 'Move to Applications',
44
+ /** The sidebar card's heading. */
45
+ title: 'Updates are stuck',
46
+ /** What the place is, as a person would say it. */
47
+ where: {
48
+ 'disk-image': `${Name} is running from the disk image it was downloaded in.`,
49
+ translocated: `${Name} is running from a read-only copy macOS made of it.`,
50
+ },
51
+ /** The same fact in half the words, for the sidebar card, which is one
52
+ * column wide and had the row's sentence running to six lines in it. */
53
+ shortWhere: {
54
+ 'disk-image': `${Name} can’t update itself from the disk image.`,
55
+ translocated: `${Name} can’t update itself from the copy macOS made.`,
56
+ },
57
+ /** And the press, which is the same either way. */
58
+ fix: 'Moving it to your Applications folder takes a few seconds and gets updates working again.',
59
+ retry: 'Pressing it again tries again.',
60
+ };
61
+
62
+ /** The sentence for a place, or null where there is nothing to say. `short` is
63
+ * the sidebar card's half of it: the button beside it already says the fix. */
64
+ export function sayWhere(place, { short = false } = {}) {
65
+ const words = short ? MOVE_SAY.shortWhere : MOVE_SAY.where;
66
+ return words[place] ?? null;
67
+ }
68
+
69
+ /** Where a copy of the app can be, as far as updating itself is concerned. */
70
+ export const PLACE = {
71
+ /** Run out of the mounted .dmg. Read-only; cannot update. */
72
+ diskImage: 'disk-image',
73
+ /** macOS is running a read-only copy of its own. Cannot update. */
74
+ translocated: 'translocated',
75
+ /** Where it belongs. */
76
+ applications: 'applications',
77
+ /** Somewhere writable that is not Applications: updates itself fine. */
78
+ elsewhere: 'elsewhere',
79
+ /** `npm start` from a checkout: main/source-updater.mjs, nothing to move. */
80
+ source: 'source',
81
+ /** No Applications folder and no `moveToApplicationsFolder`. */
82
+ notMac: 'not-mac',
83
+ };
84
+
85
+ // The folder macOS puts a translocated copy in. The path is always under
86
+ // `/private/var/folders`, but the one segment that names the mechanism is this
87
+ // one, and it is the only part Apple has kept stable. Written with "/" and
88
+ // not `path.sep`: these are only ever read on a Mac (`placeOfApp` returns
89
+ // before them anywhere else), and the screen imports this file, so it may not
90
+ // import anything from Node.
91
+ const TRANSLOCATED = '/AppTranslocation/';
92
+ // Every mounted volume lives here, including the disk image the download is.
93
+ const VOLUMES = '/Volumes/';
94
+
95
+ /**
96
+ * PURE. The whole decision, off four facts, so every case can be tested
97
+ * without a packaged app, a disk image, or a Mac.
98
+ *
99
+ * `exePath` is `app.getPath('exe')`, the binary inside the bundle;
100
+ * `inApplications` is Electron's own `app.isInApplicationsFolder()`, which
101
+ * knows about `~/Applications` as well as `/Applications` and is therefore
102
+ * never re-derived from the path here.
103
+ */
104
+ export function placeOfApp({ platform, packaged, exePath, inApplications } = {}) {
105
+ const no = (place) => ({ place, canUpdate: true, offerMove: false });
106
+ if (platform !== 'darwin') return no(PLACE.notMac);
107
+ if (!packaged) return no(PLACE.source);
108
+ if (inApplications) return no(PLACE.applications);
109
+ if (typeof exePath !== 'string' || !exePath) return no(PLACE.elsewhere);
110
+ const where = exePath.includes(TRANSLOCATED) ? PLACE.translocated
111
+ : exePath.startsWith(VOLUMES) ? PLACE.diskImage
112
+ : null;
113
+ // THE PLACES THAT ARE NOT APPLICATIONS AND UPDATE THEMSELVES ANYWAY are left
114
+ // alone on purpose. A copy sitting in Downloads that macOS did not
115
+ // translocate is on a writable disk and Squirrel swaps it happily; offering
116
+ // to move it would be the app asking for something it does not need.
117
+ if (!where) return no(PLACE.elsewhere);
118
+ return { place: where, canUpdate: false, offerMove: true };
119
+ }
120
+
121
+ /**
122
+ * The live one, over Electron's `app`. Read ONCE, at construction: the bundle
123
+ * cannot move under a running process except by the move below, which
124
+ * relaunches.
125
+ */
126
+ export function createPlace({ app, log = console }) {
127
+ const where = placeOfApp({
128
+ platform: app?.platform ?? process.platform,
129
+ packaged: !!app?.isPackaged,
130
+ exePath: (() => { try { return app?.getPath?.('exe') ?? null; } catch { return null; } })(),
131
+ inApplications: (() => { try { return !!app?.isInApplicationsFolder?.(); } catch { return false; } })(),
132
+ });
133
+
134
+ return {
135
+ state: () => ({ ...where }),
136
+
137
+ /**
138
+ * THE PRESS. Electron moves the bundle and relaunches, so nothing after a
139
+ * `true` here ever runs. It answers false when the person cancels its own
140
+ * "a copy is already there" box, which is an answer and not a failure.
141
+ */
142
+ move() {
143
+ if (!where.offerMove) return { moved: false, error: null };
144
+ try {
145
+ const moved = !!app.moveToApplicationsFolder();
146
+ return { moved, error: null };
147
+ } catch (e) {
148
+ // One line, because Settings shows this string to a person.
149
+ const error = String(e?.message ?? e ?? 'The move did not work.').split('\n')[0].trim();
150
+ log.warn?.('[place] could not move the app into Applications:', error);
151
+ return { moved: false, error };
152
+ }
153
+ },
154
+ };
155
+ }
@@ -108,7 +108,7 @@ export const CLAUDE_PERMISSION_MODES = [
108
108
  // search matches either name.
109
109
  export const WORK_ITEM_FIELDS = [
110
110
  'title', 'label', 'body', 'kind', 'labels', 'priority', 'parent',
111
- 'status', 'result', 'note', 'answer', 'product', 'runAt', 'answeredThrough',
111
+ 'status', 'result', 'note', 'answer', 'product', 'runAt', 'snoozedUntil', 'answeredThrough',
112
112
  'engine', 'model', 'effort',
113
113
  // WHETHER THIS HAS BEEN STARTED AT ALL. 'later' is a thread written down and
114
114
  // deliberately not begun: it waits for a person, not for a clock, and
@@ -366,6 +366,14 @@ function str(value, max) {
366
366
  return trimmed.slice(0, lo) + mark(trimmed.length - lo);
367
367
  }
368
368
 
369
+ // A text field whose empty string MEANS something: nothing chosen. Only a
370
+ // string may clear it, so a `null` that reached here without going through
371
+ // `answerItem` is still dropped rather than guessed at.
372
+ function clearable(value, max) {
373
+ if (typeof value !== 'string') return undefined;
374
+ return value.trim() ? str(value, max) : '';
375
+ }
376
+
369
377
  function coerceField(field, value) {
370
378
  switch (field) {
371
379
  case 'title': return str(value, MAX_TITLE);
@@ -388,7 +396,23 @@ function coerceField(field, value) {
388
396
  // shared/effort-levels.mjs, and the spawn gates on them, so a value this
389
397
  // module does not recognise is a run at Claude Code's own choice rather
390
398
  // than a row that never runs.
391
- case 'kind': case 'product': case 'parent': case 'engine': case 'model': case 'effort': return str(value, MAX_SHORT);
399
+ case 'kind': case 'product': case 'parent': case 'engine': return str(value, MAX_SHORT);
400
+ // THESE TWO ARE CLEARED BY WRITING '' AND SO '' IS A REAL VALUE, exactly
401
+ // as runAt's 0 and start's 'now' are, and for the same reason: a field that
402
+ // coerces to undefined is dropped by the fold, so without a falsy value
403
+ // that survives, a model could be chosen and never unchosen.
404
+ //
405
+ // They used to sit on the line above, which cost us the most common crash
406
+ // of launch week. The reply box's drawer hands a pick back as null for
407
+ // "nothing chosen" and sends `{ model, effort }` whenever it was touched at
408
+ // all; `answerItem` turns null into '' because `modelForEngine` reads the
409
+ // empty string as nothing chosen. `str()` then dropped it, `pickFields`
410
+ // answered null, and `buildLine` threw — before the answer was written,
411
+ // because the model is written first on purpose. So opening the drawer,
412
+ // picking a model and pressing send lost the message, with nothing on
413
+ // screen to say so. Five of the 14 crashes since the Oct 6 launch, on two
414
+ // installs (PostHog, 2026-10-08).
415
+ case 'model': case 'effort': return clearable(value, MAX_SHORT);
392
416
  case 'status': return WORK_ITEM_STATUSES.includes(value) ? value : undefined;
393
417
  case 'priority': return Number.isFinite(value) ? Math.trunc(value) : undefined;
394
418
  // When this item may next be acted on, in epoch ms. 0 means unscheduled,
@@ -396,6 +420,8 @@ function coerceField(field, value) {
396
420
  // that coerces to undefined is dropped by the fold, so without a falsy
397
421
  // sentinel a schedule could be set and never cleared.
398
422
  case 'runAt': return Number.isFinite(value) ? Math.trunc(value) : undefined;
423
+ // An inbox reminder, independent of when work may start. 0 brings it back.
424
+ case 'snoozedUntil': return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : undefined;
399
425
  // 'later' or 'now', and 'now' is the falsy sentinel for the same reason
400
426
  // runAt keeps 0: a field that coerces to undefined is dropped by the fold,
401
427
  // so without a real value for "started" nothing could ever clear it.
@@ -637,7 +637,7 @@ export function groupWork(events, { min = RUN_MIN } = {}) {
637
637
  for (const e of events ?? []) {
638
638
  // SOMETHING SHE DID IS NOT THE AGENT'S WORK and never folds into it: three
639
639
  // snoozes are three lines, not one "You put it off" (w-49b4e45403).
640
- if (e && e.kind === 'work' && !e.yours) { run.push(e); continue; }
640
+ if (e && e.kind === 'work' && !e.yours && !e.standalone) { run.push(e); continue; }
641
641
  flush();
642
642
  out.push(e);
643
643
  }
@@ -0,0 +1,2 @@
1
+ export const INVALID_RANGE: string;
2
+ export function guardInvalidRange(terminal: unknown, options?: { warn?: (message: string) => void }): boolean;
@@ -0,0 +1,58 @@
1
+ // XTERM'S OWN ASSERTION, CAUGHT WHERE WE CAN REACH IT.
2
+ //
3
+ // @xterm/xterm 6.0.0's AccessibilityManager listens on the document for
4
+ // 'selectionchange', maps the selection onto terminal rows and columns, and
5
+ // does `throw new Error('invalid range')` when the result covers no cells,
6
+ // under its own comment "This should not happen unless we have some bugs". It
7
+ // happens. Three ordinary selections reach it, measured against the published
8
+ // bundle; the test beside this file
9
+ // (tests/a-selection-that-covers-no-terminal-cells-is-not-a-crash.test.mjs)
10
+ // lists them and says how.
11
+ //
12
+ // Only a screen reader user gets that handler at all, because the pane asks
13
+ // `app.accessibilitySupportEnabled` before turning `screenReaderMode` on. For
14
+ // them the throw escapes as an unhandled renderer error, and the terminal keeps
15
+ // whatever was selected before it, so a copy afterwards takes stale text.
16
+ //
17
+ // WHY A WRAPPER AND NOT A PATCHED BUNDLE. What we build against is minified
18
+ // `lib/xterm.mjs`, and rewriting a file inside node_modules at build time is
19
+ // machinery that has to hold through `npm ci` on two CI runners and every
20
+ // release. xterm dispatches the handler as `() => this._handleSelectionChange()`,
21
+ // off the instance, so one own property on the manager shadows it and the
22
+ // wrapper dies with the terminal.
23
+ //
24
+ // IT SWALLOWS ONE MESSAGE AND NOTHING ELSE. The same handler warns about two
25
+ // genuine race conditions and returns; anything that is not this exact
26
+ // assertion is somebody's bug and goes on up.
27
+ //
28
+ // This is a workaround, and the upstream fix is the assertion becoming a
29
+ // clearSelection and a return. When an xterm release carries that, the test
30
+ // above turns red on the bundle it reads, and this file goes.
31
+ export const INVALID_RANGE = 'invalid range';
32
+
33
+ /**
34
+ * Wrap the accessibility manager's selection handler on `terminal`, which must
35
+ * already be open. Answers whether there was one to wrap: false when
36
+ * screenReaderMode is off, which is every install not running a screen reader,
37
+ * and false when it is already wrapped.
38
+ */
39
+ export function guardInvalidRange(terminal, { warn = console.warn } = {}) {
40
+ const manager = terminal?._core?._accessibilityManager?.value;
41
+ const handler = manager?._handleSelectionChange;
42
+ if (typeof handler !== 'function' || handler.guardsInvalidRange) return false;
43
+ const guarded = function (...args) {
44
+ try {
45
+ return handler.apply(this, args);
46
+ } catch (e) {
47
+ // Message, not instanceof: an Error from another realm is still this bug.
48
+ if (e?.message !== INVALID_RANGE) throw e;
49
+ warn(`xterm mapped the selection onto no cells and threw "${INVALID_RANGE}"; cleared the terminal's selection instead`);
50
+ // It threw before mirroring the selection, so the terminal is still
51
+ // holding the previous one. Nothing is selected; say that.
52
+ terminal.clearSelection();
53
+ }
54
+ };
55
+ guarded.guardsInvalidRange = true;
56
+ manager._handleSelectionChange = guarded;
57
+ return true;
58
+ }