agentbox-app 0.1.12 → 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 (214) hide show
  1. package/main/acp.mjs +296 -0
  2. package/main/agent-homes.mjs +158 -0
  3. package/main/agent-sessions.mjs +57 -20
  4. package/main/agent-updates.mjs +9 -22
  5. package/main/analytics.mjs +30 -0
  6. package/main/claude-bin.mjs +45 -311
  7. package/main/claude-input.mjs +9 -3
  8. package/main/claude-launch-env.mjs +54 -0
  9. package/main/claude-models.mjs +39 -7
  10. package/main/claude-plan.mjs +16 -16
  11. package/main/claude-usage.mjs +82 -25
  12. package/main/codex-bin.mjs +51 -249
  13. package/main/codex-session.mjs +38 -3
  14. package/main/codex-threads.mjs +41 -8
  15. package/main/codex-watch.mjs +67 -3
  16. package/main/config.mjs +26 -4
  17. package/main/corner-tag.mjs +489 -0
  18. package/main/crash-report.mjs +26 -3
  19. package/main/engine-bin.mjs +595 -0
  20. package/main/engine-setup.mjs +4 -15
  21. package/main/feedback.mjs +11 -6
  22. package/main/harnesses/claude-stream.mjs +208 -0
  23. package/main/harnesses/claude.mjs +84 -0
  24. package/main/harnesses/codex.mjs +65 -0
  25. package/main/harnesses/common.mjs +78 -0
  26. package/main/harnesses/instructions-file.mjs +116 -0
  27. package/main/harnesses/opencode.mjs +234 -0
  28. package/main/harnesses.mjs +65 -0
  29. package/main/importable-threads.mjs +38 -0
  30. package/main/ipc.mjs +165 -31
  31. package/main/local-app-frames.mjs +82 -0
  32. package/main/main.mjs +100 -4
  33. package/main/memory-gate-history.mjs +51 -12
  34. package/main/memory-gate-server.mjs +67 -11
  35. package/main/memory-gate.mjs +49 -8
  36. package/main/memory-log.mjs +124 -0
  37. package/main/notify.mjs +47 -4
  38. package/main/opencode-server.mjs +271 -0
  39. package/main/opencode.mjs +287 -0
  40. package/main/phone-link.mjs +551 -0
  41. package/main/relay-address.mjs +28 -0
  42. package/main/relay-client.mjs +246 -0
  43. package/main/row-label.mjs +6 -6
  44. package/main/runs-on.mjs +32 -35
  45. package/main/serve.mjs +113 -9
  46. package/main/settings.mjs +148 -10
  47. package/main/shell-path.mjs +86 -0
  48. package/main/ship-queue.mjs +24 -6
  49. package/main/sign-in-files.mjs +3 -7
  50. package/main/source-updater.mjs +27 -1
  51. package/main/start-hold.mjs +73 -0
  52. package/main/store.mjs +1 -1
  53. package/main/supervisor.mjs +832 -428
  54. package/main/task-commands.mjs +13 -17
  55. package/main/task-folders-offthread.mjs +6 -1
  56. package/main/task-folders.mjs +490 -98
  57. package/main/task-terminals.mjs +37 -7
  58. package/main/updater.mjs +71 -9
  59. package/main/worktree-include.mjs +494 -0
  60. package/package.json +10 -1
  61. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  62. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  63. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  64. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  65. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  66. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  67. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  68. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  69. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  70. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  71. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  72. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  73. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  74. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  75. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  76. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  77. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  78. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  79. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  80. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  81. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  82. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  83. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  84. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  85. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  86. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  87. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  88. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  89. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  90. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  91. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  92. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  93. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  94. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  95. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  96. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  97. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  98. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  99. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  101. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  102. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  103. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  104. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  105. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  106. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  107. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  108. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  109. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  110. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  111. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  112. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  113. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  114. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  115. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  116. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  117. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  118. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  119. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  120. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  121. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  122. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  123. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  124. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  125. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  126. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  127. package/renderer/dist/corner-tag.html +13 -0
  128. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  129. package/renderer/dist/emulator/data/compression/README.md +11 -0
  130. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  131. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  132. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  133. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  134. package/renderer/dist/emulator/data/cores/README.md +22 -0
  135. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  136. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  137. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  138. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  139. package/renderer/dist/emulator/data/cores/package.json +65 -0
  140. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  141. package/renderer/dist/emulator/data/emulator.css +1614 -0
  142. package/renderer/dist/emulator/data/loader.js +170 -0
  143. package/renderer/dist/emulator/data/localization/README.md +63 -0
  144. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  145. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  146. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  147. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  148. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  149. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  150. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  151. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  152. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  153. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  154. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  155. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  156. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  157. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  158. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  159. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  160. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  161. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  162. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  163. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  164. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  165. package/renderer/dist/emulator/data/src/compression.js +142 -0
  166. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  167. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  168. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  169. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  170. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  171. package/renderer/dist/emulator/data/src/storage.js +125 -0
  172. package/renderer/dist/emulator/data/version.json +5 -0
  173. package/renderer/dist/emulator/host.js +297 -0
  174. package/renderer/dist/emulator/index.html +28 -0
  175. package/renderer/dist/emulator/provenance.json +6 -0
  176. package/renderer/dist/index.html +16 -3
  177. package/scripts/approval-server.sh +24 -5
  178. package/scripts/before-publish.mjs +12 -10
  179. package/shared/agent-import.mjs +2 -0
  180. package/shared/agents.d.mts +1 -0
  181. package/shared/bridge-map.mjs +18 -0
  182. package/shared/claude-commands.generated.mjs +6 -4
  183. package/shared/claude-models.generated.mjs +6 -4
  184. package/shared/corner-tag.d.mts +14 -0
  185. package/shared/corner-tag.mjs +141 -0
  186. package/shared/engines.mjs +20 -5
  187. package/shared/harness-definitions.mjs +103 -0
  188. package/shared/notify-rules.d.mts +25 -0
  189. package/shared/notify-rules.mjs +19 -0
  190. package/shared/provider-commands.mjs +10 -2
  191. package/shared/rank.d.mts +2 -0
  192. package/shared/rank.mjs +50 -0
  193. package/shared/relay-address.mjs +6 -0
  194. package/shared/relay-crypto.mjs +165 -0
  195. package/shared/runs-on.d.mts +0 -10
  196. package/shared/runs-on.mjs +25 -49
  197. package/shared/shortcuts.mjs +10 -4
  198. package/shared/spawn-trouble.mjs +57 -6
  199. package/shared/terminal-input.d.mts +1 -0
  200. package/shared/terminal-input.mjs +30 -0
  201. package/shared/terminal-keys.d.mts +1 -0
  202. package/shared/terminal-keys.mjs +16 -0
  203. package/shared/terminal-size.d.mts +4 -0
  204. package/shared/terminal-size.mjs +29 -0
  205. package/shared/usage.d.mts +3 -0
  206. package/shared/usage.mjs +7 -0
  207. package/shared/where-it-runs-from.d.mts +43 -0
  208. package/shared/where-it-runs-from.mjs +155 -0
  209. package/shared/work-items.mjs +25 -1
  210. package/shared/work-lines.mjs +1 -1
  211. package/shared/xterm-invalid-range.d.mts +2 -0
  212. package/shared/xterm-invalid-range.mjs +58 -0
  213. package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
  214. package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
@@ -519,9 +519,10 @@ export function createCodexWorker({
519
519
  // read loop, so anything the server packs in BEHIND the response is caught,
520
520
  // and a `ready` that somehow came in AHEAD of it would belong to no watched
521
521
  // thread and be dropped -- costing a healthy worker its run at the deadline.
522
- // The resume road has no such window, because `resumeThread` watches before
523
- // it sends. If a Codex release ever reorders that, this is the sentence that
524
- // says where to look.
522
+ // The resume road watches before sending, but an already-loaded thread does
523
+ // not repeat startup notifications. On 2026-10-07, 11 resumes of one loaded
524
+ // thread died at this deadline. Check that thread's current MCP connection
525
+ // on resume as well as listening for startup changes.
525
526
  let storeIsUp = !requireMcpServer;
526
527
  let storeTrouble = '';
527
528
  const waiting = [];
@@ -581,6 +582,39 @@ export function createCodexWorker({
581
582
  const timer = setTimeout(() => done(false), mcpReadyMs);
582
583
  timer?.unref?.();
583
584
  waiting.push(done);
585
+ if (resumeThreadId) {
586
+ const checkExistingStore = async () => {
587
+ let cursor;
588
+ const seen = new Set();
589
+ do {
590
+ if (ended || settled || storeIsUp) return;
591
+ const inventory = await server.request('mcpServerStatus/list', {
592
+ threadId: worker.threadId,
593
+ serverName: requireMcpServer,
594
+ detail: 'toolsAndAuthOnly',
595
+ ...(cursor ? { cursor } : {}),
596
+ });
597
+ if (ended || settled || storeIsUp) return;
598
+ const store = inventory?.data?.find(s => s.name === requireMcpServer);
599
+ // Newer Codex reports connection state. Older versions only offer
600
+ // discovered tools; a configured name alone is never proof.
601
+ if (store?.runtimeStatus === 'connected'
602
+ || (store && store.runtimeStatus == null && Object.keys(store.tools ?? {}).length > 0)) {
603
+ readStoreStatus({ name: requireMcpServer, status: 'ready' });
604
+ return;
605
+ }
606
+ if (store?.runtimeStatus) storeTrouble = `its connection is ${store.runtimeStatus}`;
607
+ cursor = inventory?.nextCursor;
608
+ if (cursor && seen.has(cursor)) throw new Error('store inventory repeated its pagination cursor');
609
+ if (cursor) seen.add(cursor);
610
+ } while (cursor);
611
+ };
612
+ // The existing deadline still owns the wait, even if this RPC hangs.
613
+ // A startup notification can also settle it while inventory is read.
614
+ checkExistingStore().catch(err => {
615
+ if (!ended && !settled && !storeIsUp) storeTrouble = err?.message ?? String(err);
616
+ });
617
+ }
584
618
  });
585
619
  };
586
620
 
@@ -595,6 +629,7 @@ export function createCodexWorker({
595
629
  const end = (code, signal = null, how = null) => {
596
630
  if (ended) return;
597
631
  ended = true;
632
+ for (const wake of waiting.splice(0)) wake(false);
598
633
  if (worker.threadId) { try { server.unwatch(worker.threadId); } catch { /* already gone */ } }
599
634
  worker.emit('exit', code, signal, how);
600
635
  };
@@ -27,6 +27,7 @@ import fs from 'node:fs';
27
27
  import os from 'node:os';
28
28
  import path from 'node:path';
29
29
  import { RECENT_DAYS, isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
30
+ import { codexDataDirs } from './agent-homes.mjs';
30
31
  import { WAS_SLUGS, nameSlug } from '../shared/product-name.mjs';
31
32
 
32
33
  export const CODEX_RECENT_DAYS = RECENT_DAYS;
@@ -252,12 +253,40 @@ export const isLive = (mtime, now = Date.now()) => Number(mtime) > 0 && now - Nu
252
253
  /**
253
254
  * THE USER'S CONVERSATIONS, newest first, in the shape the import card and the
254
255
  * import row both read. One entry per thread a person typed into, in a real folder,
255
- * touched in the last `days` days. */
256
- export function readCodexThreads({ home = os.homedir(), codexDir = codexHome(home), days = CODEX_RECENT_DAYS, now = Date.now() } = {}) {
257
- const index = readCodexIndex(codexDir);
256
+ * touched in the last `days` days.
257
+ *
258
+ * EVERY CODEX FOLDER ON THE MACHINE, not only `~/.codex` (w-e46f2e9cea): a
259
+ * second login under `~/.codex-<name>` or a `CODEX_HOME` holds conversations
260
+ * the import never offered. `codexDirs` is the list main/agent-homes.mjs makes;
261
+ * `codexDir` still names exactly one folder for a caller that means one. The
262
+ * same conversation in two folders is one row.
263
+ *
264
+ * WHICH COPY, AND WHICH NAME, decided apart (Codex's review of this change,
265
+ * 2026-10-11). The copy kept is the transcript file written last, judged by
266
+ * the file alone: judging by `when`, which folds in the index's own date, let
267
+ * a stale copy whose folder's index was touched later beat the copy still
268
+ * being written. And the name Codex gave a conversation is read from every
269
+ * folder's index, newest entry winning, so the copy kept still carries a name
270
+ * that only another folder's index knew. */
271
+ export function readCodexThreads({ home = os.homedir(), codexDir = null, codexDirs = null, days = CODEX_RECENT_DAYS, now = Date.now() } = {}) {
272
+ const dirs = codexDirs ?? (codexDir ? [codexDir] : codexDataDirs({ home }));
258
273
  const cutoff = now - Math.max(0, Number(days) || 0) * 86400 * 1000;
259
- const out = [];
260
- const skipped = { notHers: 0, scratch: 0, old: 0, empty: 0, unreadable: 0 };
274
+ const index = new Map();
275
+ for (const dir of dirs) {
276
+ for (const [id, entry] of readCodexIndex(dir)) {
277
+ const had = index.get(id);
278
+ if (!had || (entry.name && (!had.name || entry.updatedAt > had.updatedAt))) index.set(id, entry);
279
+ }
280
+ }
281
+ const byId = new Map();
282
+ const skipped = { notHers: 0, scratch: 0, old: 0, empty: 0, unreadable: 0, copy: 0 };
283
+ for (const dir of dirs) readOneCodexHome(dir, { home, days, now, cutoff, index, byId, skipped });
284
+ const out = [...byId.values()].map(({ fileTime, ...t }) => t);
285
+ out.sort((a, b) => b.when - a.when);
286
+ return { threads: out, skipped };
287
+ }
288
+
289
+ function readOneCodexHome(codexDir, { home, days, now, cutoff, index, byId, skipped }) {
261
290
  for (const file of listRolloutFiles(codexDir, { days, since: cutoff })) {
262
291
  const r = readRollout(file);
263
292
  if (!r) { skipped.unreadable += 1; continue; }
@@ -268,7 +297,13 @@ export function readCodexThreads({ home = os.homedir(), codexDir = codexHome(hom
268
297
  if (when < cutoff) { skipped.old += 1; continue; }
269
298
  const title = named?.name || threadTitle([r.prompt]);
270
299
  if (!title) { skipped.empty += 1; continue; }
271
- out.push({
300
+ const had = byId.get(r.id);
301
+ if (had) {
302
+ skipped.copy += 1;
303
+ if (had.fileTime >= r.mtime) continue;
304
+ }
305
+ byId.set(r.id, {
306
+ fileTime: r.mtime,
272
307
  id: r.id,
273
308
  source: 'codex',
274
309
  folder: r.cwd,
@@ -286,6 +321,4 @@ export function readCodexThreads({ home = os.homedir(), codexDir = codexHome(hom
286
321
  live: isLive(r.mtime, now),
287
322
  });
288
323
  }
289
- out.sort((a, b) => b.when - a.when);
290
- return { threads: out, skipped };
291
324
  }
@@ -10,21 +10,85 @@
10
10
  // The founder, before launch: "after onboarding for both anthropic and openai
11
11
  // harnesses, it should only import on command, not continuously". Importing is
12
12
  // now the walk's last card and ⌘K import, for Claude Code and Codex alike.
13
+ //
14
+ // IT IS ALSO WHERE A CODEX AGENT IS NOTICED (w-1116fbb68a, 2026-10-08). It is
15
+ // the one thing in the app that already knows a person's Codex conversations,
16
+ // and it reads them on a timer rather than on the snapshot, which is what makes
17
+ // it the right place: see `noticeCodexAgents` below.
13
18
  import { readCodexThreads } from './codex-threads.mjs';
14
19
 
15
20
  export const CODEX_POLL_MS = 60_000;
16
21
 
17
- export function scanCodex({ store, readThreads = readCodexThreads, now = Date.now() } = {}) {
22
+ /**
23
+ * HOW LONG AFTER ITS LAST WRITE A CODEX THREAD IS STILL AN AGENT THAT WAS SEEN.
24
+ *
25
+ * A Claude Code session is a process, so `agent_seen` catches one that has sat
26
+ * open since earlier in the sitting (main/ipc.mjs, liveAgents). A Codex thread
27
+ * leaves no process behind: `isLive` is two minutes of file mtime, so counting
28
+ * only live threads would notice a Codex user ONLY when this watch's minute
29
+ * happened to land while they were typing. An hour is one sitting, which is
30
+ * the span the Claude read already covers without being asked to.
31
+ *
32
+ * AND IT IS NOT A DAY. The reader hands over ten days of conversations, and
33
+ * counting those would say every install with Codex on it had an agent up on
34
+ * every day the app was opened, which is the opposite of the under-count this
35
+ * was added to fix.
36
+ */
37
+ export const CODEX_SEEN_MS = 60 * 60_000;
38
+
39
+ /**
40
+ * "AN AGENT WAS SEEN", FOR CODEX TOO (w-1116fbb68a, 2026-10-08).
41
+ *
42
+ * `agent_seen` listed Claude Code processes and nothing else, and about a
43
+ * third of the people on this app are on Codex, so whether they had an agent
44
+ * at all was read off proxies: a run this app started, or a reply. The threads
45
+ * are already in hand once a minute for the mirror refresh, so this costs
46
+ * nothing beyond the comparison.
47
+ *
48
+ * NOTHING ABOUT A THREAD IS SENT. Not its folder, not its title, not its id,
49
+ * which is only ever compared inside this process. `engine` is the whole of
50
+ * the payload and it is one of two fixed words.
51
+ *
52
+ * ONCE PER THREAD PER RUN OF THE APP, for the reason the Claude side keeps a
53
+ * set: this reads the same conversations every minute, so without it one
54
+ * conversation would send an event a minute all day.
55
+ */
56
+ export function noticeCodexAgents(threads, { count = null, seen = null, now = Date.now() } = {}) {
57
+ if (typeof count !== 'function') return 0;
58
+ let sent = 0;
59
+ for (const t of threads ?? []) {
60
+ const id = String(t?.id ?? '');
61
+ if (!id) continue;
62
+ const at = Number(t.lastActive ?? 0);
63
+ if (!t.live && !(at > 0 && now - at < CODEX_SEEN_MS)) continue;
64
+ if (seen) {
65
+ if (seen.has(id)) continue;
66
+ seen.add(id);
67
+ }
68
+ // A count is never worth the watch: the mirror refresh below is what
69
+ // somebody actually sees, and it runs whatever the switch does.
70
+ try { count('agent_seen', { engine: 'codex' }); } catch { /* nothing */ }
71
+ sent += 1;
72
+ }
73
+ return sent;
74
+ }
75
+
76
+ export function scanCodex({ store, readThreads = readCodexThreads, now = Date.now(), count = null, seen = null } = {}) {
18
77
  let threads = [];
19
78
  try { ({ threads } = readThreads({ now })); } catch { return { refreshed: 0, retold: 0, threads: 0 }; }
79
+ noticeCodexAgents(threads, { count, seen, now });
20
80
  const refreshed = store.refreshCodexMirrors(threads, { now });
21
81
  const retold = store.refreshCodexAsks(threads, { now });
22
82
  return { refreshed, retold, threads: threads.length };
23
83
  }
24
84
 
25
- export function startCodexWatch({ store, readThreads = readCodexThreads, pollMs = CODEX_POLL_MS, firstDelayMs = 8_000 } = {}) {
85
+ export function startCodexWatch({ store, readThreads = readCodexThreads, pollMs = CODEX_POLL_MS, firstDelayMs = 8_000, count = null } = {}) {
86
+ // Conversations this run has already counted. In memory on purpose, exactly
87
+ // as the Claude side's set is: a fresh launch seeing the same conversation
88
+ // again is a fresh fact about the app being used.
89
+ const seen = new Set();
26
90
  const run = () => {
27
- try { scanCodex({ store, readThreads }); } catch (e) { console.warn('codex watch:', e?.message ?? e); }
91
+ try { scanCodex({ store, readThreads, count, seen }); } catch (e) { console.warn('codex watch:', e?.message ?? e); }
28
92
  };
29
93
  const first = setTimeout(run, firstDelayMs);
30
94
  const timer = setInterval(run, pollMs);
package/main/config.mjs CHANGED
@@ -10,6 +10,7 @@ import path from 'node:path';
10
10
  import os from 'node:os';
11
11
  import crypto from 'node:crypto';
12
12
  import { resolveClaudeBin, candidatePaths } from './claude-bin.mjs';
13
+ import { warmShellAnswers } from './engine-bin.mjs';
13
14
  import { resolveCodexBin } from './codex-bin.mjs';
14
15
  import { defaultStoreRoot } from './store/home.mjs';
15
16
  import { readPlans, slotsForPlans } from './claude-plan.mjs';
@@ -63,6 +64,11 @@ const DEFAULTS = {
63
64
  // many times "Something felt slow" has been pressed, and only ever subtracts.
64
65
  agentsAuto: true,
65
66
  agentsNudge: 0,
67
+ // THE NUMBER PICKED ON THE MENU, FOR THE WHOLE COMPUTER (w-ec27ff1a51). Null
68
+ // until somebody picks one. It replaced `maxConcurrentSessions` as the thing
69
+ // the menu writes, because that one was per login: two logins turned a 7 into
70
+ // 14 agents on a 16 GB Mac, which then ran out of memory.
71
+ agentsAtOnce: null,
66
72
  // HOLD HEAVY WORK WHEN MEMORY IS SHORT (w-3958c3753d). Off until somebody
67
73
  // turns it on from the Agents page. With it on, every shell command a worker
68
74
  // runs asks main/memory-gate-server.mjs first, and heavy ones wait their turn
@@ -205,6 +211,14 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
205
211
  config.claudeBinConfigured = typeof overrides.claudeBin === 'string' && overrides.claudeBin
206
212
  ? overrides.claudeBin
207
213
  : null;
214
+ // ONE LOGIN SHELL FOR BOTH ENGINES, ASKED BEFORE EITHER SEARCH RUNS. This is
215
+ // on the boot path -- main/main.mjs calls loadConfig inside createWindow and
216
+ // before the window exists -- and a login shell costs about 0.7 s to start
217
+ // whether it is asked about one program or six. Measured on this Mac
218
+ // 2026-10-11, the two searches together took 1.4 s cold and the shell was all
219
+ // of it. It seeds only what it positively found, so a shell that will not
220
+ // start leaves both searches exactly as they were (main/engine-bin.mjs).
221
+ warmShellAnswers();
208
222
  const claude = resolveClaudeBin(config.claudeBinConfigured);
209
223
  config.claudeBin = claude.path ?? candidatePaths()[0];
210
224
  config.claudeFound = claude.found;
@@ -281,10 +295,18 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
281
295
  // same thing said out loud and kept up to date. An explicit `agentsAuto`
282
296
  // outranks the guess, because turning Automatic back on after setting a number
283
297
  // has to be possible.
284
- if (typeof overrides.agentsAuto !== 'boolean') {
285
- config.agentsAuto = typeof overrides.maxConcurrentSessions !== 'number';
286
- }
287
- if (typeof overrides.maxConcurrentSessions !== 'number') {
298
+ //
299
+ // ONLY A NUMBER FOR THE WHOLE COMPUTER IS A CHOICE (w-ec27ff1a51). A
300
+ // `maxConcurrentSessions` in the file was written per login, by the stepper
301
+ // that was here before 5 Oct, and reading it as "you set this" is what ran
302
+ // 14 agents on a Mac Automatic would have run 6 on, with nobody having picked
303
+ // 14. So that install goes back to Automatic, even with `agentsAuto: false`
304
+ // written beside it, and a number picked on today's menu (`agentsAtOnce`)
305
+ // is kept as picked unless Automatic was chosen after it.
306
+ config.agentsAuto = typeof overrides.agentsAtOnce !== 'number' || overrides.agentsAuto === true;
307
+ // And the plan is read whenever the number is Automatic's, old per-login
308
+ // number or not, so a plan that allows one at a time still says so.
309
+ if (typeof overrides.agentsAtOnce !== 'number') {
288
310
  const plans = readPlans(effectiveProfiles(config.authProfiles, { home }), { home });
289
311
  const slots = slotsForPlans(plans, config.machineSlots);
290
312
  config.maxConcurrentSessions = slots;