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
@@ -12,7 +12,9 @@
12
12
  //
13
13
  // WHERE A THREAD LIVES. Two places, because she named two:
14
14
  //
15
- // ~/.claude/projects/<folder>/<id>.jsonl every terminal session
15
+ // ~/.claude/projects/<folder>/<id>.jsonl every terminal session (and the
16
+ // same under every other Claude
17
+ // data folder: main/agent-homes.mjs)
16
18
  // ~/Library/Application Support/Claude/
17
19
  // claude-code-sessions/**/local_<id>.json the desktop app
18
20
  //
@@ -49,6 +51,7 @@
49
51
  import fs from 'node:fs';
50
52
  import os from 'node:os';
51
53
  import path from 'node:path';
54
+ import { claudeDataDirs } from './agent-homes.mjs';
52
55
 
53
56
  /**
54
57
  * "Recent = last few days", in days.
@@ -149,9 +152,22 @@ export function isScratchProjectDir(name, home = os.homedir()) {
149
152
  *
150
153
  * An entrypoint we have never seen is treated as not hers. The cost of being
151
154
  * wrong that way is one missing row on a card; the other way is the hundreds
152
- * she is complaining about. */
155
+ * she is complaining about.
156
+ *
157
+ * `claude-vscode` IS A PERSON TOO (w-e46f2e9cea, 2026-10-11). It is what the
158
+ * Claude Code extension writes in VS Code, Cursor and Windsurf, and leaving it
159
+ * out threw away every session of everybody who runs Claude Code in an editor.
160
+ * Read out of the 2.1.293 binary, which maps it to `claude_code_vscode`. The
161
+ * `remote*` family stays out: those run in the cloud, not on this disk. */
162
+ const BY_HAND = new Set(['cli', 'claude-desktop', 'claude-vscode']);
153
163
  export function startedByHand(entrypoint) {
154
- return entrypoint === 'cli' || entrypoint === 'claude-desktop';
164
+ return BY_HAND.has(entrypoint);
165
+ }
166
+
167
+ /** Which of the three places a person starts Claude Code a session came from. */
168
+ export function sourceOf(entrypoint) {
169
+ if (entrypoint === 'claude-desktop') return 'desktop';
170
+ return entrypoint === 'claude-vscode' ? 'editor' : 'terminal';
155
171
  }
156
172
 
157
173
  /**
@@ -251,9 +267,11 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_B
251
267
  const n = fs.readSync(fd, buf, 0, bytes, 0);
252
268
  const text = buf.slice(0, n).toString('utf8');
253
269
  // The last line of a fixed read is usually cut in half. Dropping it costs
254
- // nothing, because what is wanted is at the top.
270
+ // nothing, because what is wanted is at the top. UNLESS THE WHOLE FILE FIT:
271
+ // then the last line is complete, and a file whose one row has no newline
272
+ // after it lost that row (found by Codex's review, 2026-10-11).
255
273
  const lines = text.split('\n');
256
- const cut = lines.pop();
274
+ const cut = n < bytes ? null : lines.pop();
257
275
  let cwd = null;
258
276
  let entrypoint = null;
259
277
  const said = [];
@@ -278,7 +296,7 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_B
278
296
  // only when nothing she said was found and the session is not already
279
297
  // known to be the app's own, so the thousands of worker transcripts are
280
298
  // never read past their head.
281
- if (!said.length && n === bytes && cut && /"type":"user"/.test(cut.slice(0, 2048))
299
+ if (!said.length && n === bytes && cut && /"type"\s*:\s*"user"/.test(cut.slice(0, 2048))
282
300
  && (!entrypoint || startedByHand(entrypoint))) {
283
301
  const rest = restOfLine(fd, n, most - n);
284
302
  if (rest) take(Buffer.concat([buf.subarray(buf.lastIndexOf(0x0a, n - 1) + 1, n), rest]).toString('utf8'));
@@ -331,25 +349,35 @@ function messageText(row) {
331
349
  * `skipped` comes back with it because the counts are the argument for the
332
350
  * filters, and a session that changes one of them should be able to see what
333
351
  * it changed rather than take this comment's word for it. */
334
- function readTerminalThreads({ home, cutoff, max, skipIds = new Set() }) {
335
- const root = path.join(home, '.claude', 'projects');
336
- const skipped = { old: 0, scratch: 0, started: 0, subagent: 0, empty: 0, unread: 0, desktopDupe: 0 };
352
+ function readTerminalThreads({ home, dirs, cutoff, max, skipIds = new Set() }) {
353
+ const skipped = { old: 0, scratch: 0, started: 0, subagent: 0, empty: 0, unread: 0, desktopDupe: 0, copy: 0 };
337
354
  const out = [];
338
- const all = [];
339
- walkTranscripts(root, all);
340
355
  // Stat everything, keep the recent, newest first, and only then start opening
341
356
  // files. This is the order that makes `max` safe: whatever it cuts off is
342
357
  // older than everything it kept.
358
+ //
359
+ // EVERY DATA FOLDER, NOT ONLY ~/.claude (main/agent-homes.mjs). They are
360
+ // pooled before the sort, so the cap and the time budget are spent on the
361
+ // newest sessions on the machine, whichever folder they are in.
343
362
  const recent = [];
344
- for (const file of all) {
345
- let when = 0;
346
- try { when = fs.statSync(file).mtimeMs; } catch { continue; }
347
- if (when < cutoff) { skipped.old += 1; continue; }
348
- if (isSubagentPath(file.slice(root.length))) { skipped.subagent += 1; continue; }
349
- if (isScratchProjectDir(path.relative(root, file).split(path.sep)[0], home)) { skipped.scratch += 1; continue; }
350
- recent.push({ file, when });
363
+ for (const dir of dirs) {
364
+ const root = path.join(dir, 'projects');
365
+ const all = [];
366
+ walkTranscripts(root, all);
367
+ for (const file of all) {
368
+ let when = 0;
369
+ try { when = fs.statSync(file).mtimeMs; } catch { continue; }
370
+ if (when < cutoff) { skipped.old += 1; continue; }
371
+ if (isSubagentPath(file.slice(root.length))) { skipped.subagent += 1; continue; }
372
+ if (isScratchProjectDir(path.relative(root, file).split(path.sep)[0], home)) { skipped.scratch += 1; continue; }
373
+ recent.push({ file, when });
374
+ }
351
375
  }
352
376
  recent.sort((a, b) => b.when - a.when);
377
+ // ONE SESSION IS ONE ROW, even when two folders hold it: a backup folder with
378
+ // a copy of a conversation is ordinary. Newest first, so the copy kept is the
379
+ // one still being written to.
380
+ const seenIds = new Set();
353
381
  // THE CAP COUNTS WHAT IS KEPT, NOT WHAT IS OPENED (w-ec62ab6b38, 2026-09-28).
354
382
  // On a busy machine the last seven days held 9,073 transcripts, the newest
355
383
  // 1,200 were opened, and every one of those was started by agentbox itself,
@@ -373,14 +401,18 @@ function readTerminalThreads({ home, cutoff, max, skipIds = new Set() }) {
373
401
  // one desktop thread in her store. Its own record wins, because the app
374
402
  // wrote a real title into it and this side would have to guess one.
375
403
  if (skipIds.has(id)) { skipped.desktopDupe += 1; continue; }
404
+ if (seenIds.has(id)) { skipped.copy += 1; continue; }
376
405
  const { cwd, entrypoint, said } = readTranscriptHead(file);
406
+ // A copy is only the copy once it could be read. A damaged newer one, cut
407
+ // off mid-write, would otherwise hide the good older one in another folder.
408
+ if (cwd || entrypoint) seenIds.add(id);
377
409
  if (!startedByHand(entrypoint)) { skipped.started += 1; continue; }
378
410
  if (isScratchFolder(cwd, home)) { skipped.scratch += 1; continue; }
379
411
  const title = threadTitle(said);
380
412
  if (!title) { skipped.empty += 1; continue; }
381
413
  out.push({
382
414
  id,
383
- source: entrypoint === 'claude-desktop' ? 'desktop' : 'terminal',
415
+ source: sourceOf(entrypoint),
384
416
  folder: cwd,
385
417
  folderName: path.basename(cwd),
386
418
  short: shortFolder(cwd, home),
@@ -484,6 +516,11 @@ function walkDesktop(dir, into, max, depth = 3) {
484
516
  * the card groups by it, the same way it already groups agent files. */
485
517
  export function readSessionThreads({
486
518
  home = os.homedir(),
519
+ // Every Claude Code data folder to read (main/agent-homes.mjs). The app hands
520
+ // in the list it made with its own environment and settings; left out, it is
521
+ // the folders under `home`, and nothing from whatever CLAUDE_CONFIG_DIR the
522
+ // caller's process happens to carry.
523
+ dirs = claudeDataDirs({ home }),
487
524
  days = RECENT_DAYS,
488
525
  now = Date.now(),
489
526
  max = MAX_READS,
@@ -492,7 +529,7 @@ export function readSessionThreads({
492
529
  // The desktop side goes first so its ids are known before the transcripts are
493
530
  // walked, which is what stops one thread appearing twice.
494
531
  const desk = readDesktopThreads({ home, cutoff, max });
495
- const term = readTerminalThreads({ home, cutoff, max, skipIds: desk.owned });
532
+ const term = readTerminalThreads({ home, dirs, cutoff, max, skipIds: desk.owned });
496
533
  const threads = [...term.threads, ...desk.threads].sort((a, b) => b.when - a.when);
497
534
  return {
498
535
  threads,
@@ -2,10 +2,11 @@
2
2
  // started only by the app's Update now action, never by a poll or terminal open.
3
3
  import fs from 'node:fs';
4
4
  import os from 'node:os';
5
- import path from 'node:path';
6
5
  import {execFile} from 'node:child_process';
7
6
  import {promisify} from 'node:util';
8
7
  import {TaskTerminals,terminalEnv} from './task-terminals.mjs';
8
+ import { harnessFor } from './harnesses.mjs';
9
+ import { harnessDefinition } from '../shared/harness-definitions.mjs';
9
10
  const exec=promisify(execFile);
10
11
  const valid=v=>typeof v==='string'&&/^\d+\.\d+\.\d+$/.test(v);
11
12
  export function newerVersion(installed,latest){
@@ -14,27 +15,15 @@ export function newerVersion(installed,latest){
14
15
  for(let i=0;i<3;i++){if(b[i]!==a[i])return b[i]>a[i];}return false;
15
16
  }
16
17
  export function updatePlan({engine,bin,real,home=os.homedir(),channel='latest'}){
17
- if(!['claude','codex'].includes(engine)||!path.isAbsolute(bin||'')||!real)return null;
18
- const name=engine==='claude'?'Claude Code':'Codex';
19
- const brew=real.match(/^(\/opt\/homebrew|\/usr\/local)\/Caskroom\/(claude-code(?:@latest)?|codex)\//);
20
- if(brew&&((engine==='claude'&&brew[2].startsWith('claude-code'))||(engine==='codex'&&brew[2]==='codex')))
21
- return {name,file:`${brew[1]}/bin/brew`,args:['upgrade','--cask',brew[2]],feed:`https://formulae.brew.sh/api/cask/${brew[2]}.json`,format:'brew'};
22
- const pkg=engine==='claude'?'@anthropic-ai/claude-code':'@openai/codex';
23
- const tail=`/lib/node_modules/${pkg}/`;
24
- const at=real.indexOf(tail);
25
- if(at>0){const prefix=real.slice(0,at);return {name,file:path.join(prefix,'bin/npm'),args:['install','--global','--prefix',prefix,`${pkg}@${channel}`],env:{PATH:`${prefix}/bin:${process.env.PATH||''}`},feed:`https://registry.npmjs.org/${pkg}/${channel}`,format:'npm'};}
26
- if(engine==='claude'&&real.startsWith(path.join(home,'.local/share/claude/versions')+'/'))return {name,file:bin,args:['update'],feed:`https://downloads.claude.ai/claude-code-releases/${channel}`,format:'text'};
27
- const standalone=real.match(/^(.*)\/packages\/standalone\/(?:releases|current)\//);
28
- if(engine==='codex'&&standalone)return {name,file:'/bin/bash',args:['-o','pipefail','-c','/usr/bin/curl -fsSL --connect-timeout 10 --max-time 60 https://chatgpt.com/codex/install.sh | /bin/sh'],env:{CODEX_HOME:standalone[1],CODEX_INSTALL_DIR:path.dirname(bin)},feed:'https://releases.openai.com/codex/channels/latest',format:'codex'};
29
- return null;
18
+ if(!harnessDefinition(engine))return null;
19
+ return harnessFor(engine).updatePlan({bin,real,home,channel});
30
20
  }
31
- const json=file=>{try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return {}}};
32
21
  export async function installedAgent(engine,config){
33
- const bin=engine==='codex'?config.codexBin:config.claudeBin;
22
+ const harness=harnessFor(engine), bin=harness.binary(config);
34
23
  if(!bin)throw Error('This agent is not installed.');
35
24
  const home=config.home||os.homedir();
36
- const settings=engine==='claude'?json(path.join(process.env.CLAUDE_CONFIG_DIR||path.join(home,'.claude'),'settings.json')):{};
37
- const env=terminalEnv();
25
+ const settings=harness.updateSettings({home});
26
+ const env=harness.launchEnv(bin,terminalEnv());
38
27
  // Version inspection must not itself auto-update anything.
39
28
  const {stdout}=await exec(bin,['--version'],{timeout:8000,maxBuffer:16384,env:{...env,DISABLE_AUTOUPDATER:'1'}});
40
29
  const version=stdout.match(/\b\d+\.\d+\.\d+(?:-[\w.-]+)?\b/)?.[0];
@@ -57,7 +46,7 @@ export class AgentUpdates{
57
46
  }
58
47
  status(engine){return this.entries.get(engine)||{engine,state:'unknown'};}
59
48
  check(engine,force=false){
60
- if(!['claude','codex'].includes(engine))return Promise.reject(Error('Unknown agent.'));
49
+ if(!harnessDefinition(engine))return Promise.reject(Error('Unknown agent.'));
61
50
  if(this.starts.has(engine)||this.status(engine).state==='running')return Promise.resolve(this.status(engine));
62
51
  if(this.checks.has(engine))return this.checks.get(engine);
63
52
  const saved=this.status(engine);if(!force&&saved.checkedAt&&this.now()-saved.checkedAt<3600000)return Promise.resolve(saved);
@@ -103,7 +92,7 @@ export class AgentUpdates{
103
92
  if(!this.plans.has(engine))throw Error('Click Update now first.');
104
93
  switch(p.action){
105
94
  case 'open':return this.terminals.read(engine,0);
106
- case 'read':return this.terminals.read(engine,p.offset);
95
+ case 'read':return this.terminals.read(engine,p.offset,p.wait);
107
96
  case 'write':return this.terminals.write(engine,p.data);
108
97
  case 'resize':return this.terminals.resize(engine,p.cols,p.rows);
109
98
  case 'close':return this.terminals.close(engine);
@@ -178,6 +178,36 @@ export function createAnalytics({ config, version, dir, client, env = process.en
178
178
  report_version: Number(payload.v ?? 1),
179
179
  error: payload.error ?? null,
180
180
  happened_at: String(payload.ts ?? ''),
181
+ // THE VERSION IT CRASHED ON, WHICH IS NOT ALWAYS THE ONE SENDING IT.
182
+ // A report written by a dying process is drained on the NEXT launch,
183
+ // and that launch may be a newer app: `base.app_version` then files
184
+ // 0.1.11's crash against 0.1.12. It did, for three of the six window
185
+ // errors the launch report attributes to 0.1.12 (PostHog, 2026-10-08),
186
+ // and a version asked to answer for the bugs of the one before it is
187
+ // how a release that fixed something reads as a release that broke it.
188
+ // The report has carried the right answer all along.
189
+ app_version: String(payload.app?.version ?? base.app_version),
190
+ // WHY THE WINDOW DIED, which is the only thing a `renderer-gone`
191
+ // report is for and the one thing it was not carrying.
192
+ // `installCrashReports` collects Electron's `details.reason` and
193
+ // `details.exitCode` and says in its own comment why they are safe:
194
+ // the reason is Electron's closed vocabulary (`crashed`, `oom`,
195
+ // `killed`, `launch-failed` and the rest) and carries nothing of
196
+ // anyone's. They were written to disk and then dropped here, because
197
+ // this list names its properties by hand. So the launch report's one
198
+ // dead window says `render process gone` and nothing else, and `oom`
199
+ // after three and a half hours (a leak) could not be told apart from a
200
+ // hard fault (not a leak). Measured 2026-10-08.
201
+ //
202
+ // Absent rather than empty when there is nothing to say: a null reason
203
+ // on the thirteen crashes that never had one reads as an answer in a
204
+ // chart. A reason longer than Electron's longest word is a bug
205
+ // upstream rather than a reason, and is left out too.
206
+ ...(typeof payload.reason === 'string' && payload.reason && payload.reason.length <= 40
207
+ ? { crash_reason: payload.reason } : {}),
208
+ ...(Number.isFinite(payload.exitCode) ? { exit_code: Number(payload.exitCode) } : {}),
209
+ ...(typeof payload.processType === 'string' && payload.processType && payload.processType.length <= 40
210
+ ? { process_type: payload.processType } : {}),
181
211
  });
182
212
  },
183
213
 
@@ -387,12 +387,20 @@ const REQUEST_SUFFIX = '.request.json';
387
387
  * FORWARD to now. A worker can therefore make its own request look younger than
388
388
  * it is, which costs it the front of the stack, and can never make it look
389
389
  * older. Nanoseconds because APFS keeps them and two files written back to back
390
- * differ by about 170 microseconds; equal stamps keep `readdir` order, which is
391
- * as good as anything when two things really did happen at once.
390
+ * differ by about 170 microseconds.
391
+ *
392
+ * A coarse clock stamps both files the same. tmpfs does this: two writes in
393
+ * one poll share a ctime, and `readdir` then comes back in name order, which
394
+ * is the forgery the paragraph above is about. The inode breaks that tie.
395
+ * The kernel assigns it, a later create in a fresh directory gets a higher
396
+ * one, and a worker cannot move it backwards. Equal inode and equal ctime
397
+ * keep the order we already had.
392
398
  */
393
399
  function bornAt(dir, name) {
394
- try { return fs.lstatSync(path.join(dir, name), { bigint: true }).ctimeNs; }
395
- catch { return 2n ** 63n; } // gone or unreadable: never in front of something real
400
+ try {
401
+ const st = fs.lstatSync(path.join(dir, name), { bigint: true });
402
+ return { ctime: st.ctimeNs, ino: st.ino };
403
+ } catch { return { ctime: 2n ** 63n, ino: 2n ** 63n }; } // gone or unreadable: never in front of something real
396
404
  }
397
405
 
398
406
  export function listPending(storeRoot, { now = Date.now } = {}) {
@@ -409,7 +417,8 @@ export function listPending(storeRoot, { now = Date.now } = {}) {
409
417
  for (const { f } of names
410
418
  .filter((f) => !seenAt.has(prefix + f.slice(0, -REQUEST_SUFFIX.length)))
411
419
  .map((f) => ({ f, born: bornAt(dir, f) }))
412
- .sort((a, b) => (a.born < b.born ? -1 : a.born > b.born ? 1 : 0))) {
420
+ .sort((a, b) => (a.born.ctime < b.born.ctime ? -1 : a.born.ctime > b.born.ctime ? 1
421
+ : a.born.ino < b.born.ino ? -1 : a.born.ino > b.born.ino ? 1 : 0))) {
413
422
  seenAt.set(prefix + f.slice(0, -REQUEST_SUFFIX.length), { at, seq: sightings += 1 });
414
423
  }
415
424
  const present = new Set();