agentbox-app 0.1.13 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -0,0 +1,12 @@
1
+ // Reads Codex transcripts for the watch, off the app's main process
2
+ // (codex-threads.mjs readCodexThreadsOffThread).
3
+ import { parentPort } from 'node:worker_threads';
4
+ import { readCodexThreads } from './codex-threads.mjs';
5
+
6
+ parentPort.on('message', ({ id, opts }) => {
7
+ try {
8
+ parentPort.postMessage({ id, ok: true, value: readCodexThreads(opts) });
9
+ } catch (err) {
10
+ parentPort.postMessage({ id, ok: false, error: String(err?.message ?? err) });
11
+ }
12
+ });
@@ -24,11 +24,13 @@
24
24
  // table. It is not read: Electron ships no sqlite, the file is written live
25
25
  // under WAL, and the two plain files carry what the row needs.
26
26
  import fs from 'node:fs';
27
+ import { Worker } from 'node:worker_threads';
27
28
  import os from 'node:os';
28
29
  import path from 'node:path';
29
30
  import { RECENT_DAYS, isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
30
31
  import { codexDataDirs } from './agent-homes.mjs';
31
32
  import { WAS_SLUGS, nameSlug } from '../shared/product-name.mjs';
33
+ import { howTheChatEnded } from '../shared/agent-import.mjs';
32
34
 
33
35
  export const CODEX_RECENT_DAYS = RECENT_DAYS;
34
36
  /** The originator Codex records for a run this app started (CLIENT_INFO.name), now and before. */
@@ -164,7 +166,25 @@ const partsOf = (payload) => (Array.isArray(payload?.content) ? payload.content
164
166
  * `message` with a `role` and a `content` array of `input_text`, `input_image`
165
167
  * and `output_text` parts. Current Codex threads are paginated, and a build
166
168
  * that read only legacy showed "Codex has not answered yet." on every row. */
169
+ // Each transcript as last read, kept while its size and time are unchanged:
170
+ // her Codex folder can hold thousands of them, and reading every one again
171
+ // each minute held the app for seconds.
172
+ const ROLLOUTS_KEPT = 5000;
173
+ const rollouts = new Map();
174
+
167
175
  export function readRollout(file) {
176
+ let st = null;
177
+ try { st = fs.statSync(file); } catch { return null; }
178
+ const kept = rollouts.get(file);
179
+ if (kept && kept.size === st.size && kept.mtimeMs === st.mtimeMs) return kept.value;
180
+ const value = readRolloutAfresh(file);
181
+ rollouts.delete(file);
182
+ rollouts.set(file, { size: st.size, mtimeMs: st.mtimeMs, value });
183
+ if (rollouts.size > ROLLOUTS_KEPT) rollouts.delete(rollouts.keys().next().value);
184
+ return value;
185
+ }
186
+
187
+ function readRolloutAfresh(file) {
168
188
  let text = '';
169
189
  try {
170
190
  const fd = fs.openSync(file, 'r');
@@ -179,6 +199,8 @@ export function readRollout(file) {
179
199
  let meta = null;
180
200
  let prompt = '';
181
201
  let last = '';
202
+ // Whether the person wrote after Codex's last answer, so it is not waiting.
203
+ let herTurnLast = false;
182
204
  let turns = 0;
183
205
  let images = 0;
184
206
  let lastActive = 0;
@@ -213,11 +235,12 @@ export function readRollout(file) {
213
235
  if (p.type === 'user_message') {
214
236
  const said = cleanPrompt(p.message);
215
237
  if (!prompt && said && !isInjected(said)) prompt = said;
238
+ if (said && !isInjected(said)) herTurnLast = true;
216
239
  turns += 1;
217
240
  sawEventTurns = true;
218
241
  } else if (p.type === 'agent_message') {
219
242
  const said = cleanAnswer(p.message);
220
- if (said) last = said;
243
+ if (said) { last = said; herTurnLast = false; }
221
244
  }
222
245
  continue;
223
246
  }
@@ -231,16 +254,17 @@ export function readRollout(file) {
231
254
  .filter(Boolean);
232
255
  if (!hers.length) continue; // a turn made of nothing but Codex's own preamble
233
256
  if (!prompt) prompt = hers.join('\n\n').trim();
257
+ herTurnLast = true;
234
258
  if (!sawEventTurns) turns += 1;
235
259
  } else if (p.role === 'assistant') {
236
260
  const said = cleanAnswer(partsOf(p).filter((c) => c.type === 'output_text').map((c) => c.text ?? '').join('\n'));
237
- if (said) last = said;
261
+ if (said) { last = said; herTurnLast = false; }
238
262
  }
239
263
  }
240
264
  if (!meta || !meta.id) return null;
241
265
  let mtime = 0;
242
266
  try { mtime = fs.statSync(file).mtimeMs; } catch { mtime = 0; }
243
- return { ...meta, prompt, last, turns, images, lastActive, path: file, mtime };
267
+ return { ...meta, prompt, last, herTurnLast, turns, images, lastActive, path: file, mtime };
244
268
  }
245
269
 
246
270
  /**
@@ -319,6 +343,50 @@ function readOneCodexHome(codexDir, { home, days, now, cutoff, index, byId, skip
319
343
  prompt: r.prompt,
320
344
  last: r.last,
321
345
  live: isLive(r.mtime, now),
346
+ ...(r.herTurnLast ? { waiting: false, ask: '' } : howTheChatEnded(r.last)),
347
+ lastSaid: r.last,
348
+ firstSaid: r.prompt,
322
349
  });
323
350
  }
324
351
  }
352
+
353
+ // THE SAME READ IN A THREAD OF ITS OWN, for the watch that runs every minute:
354
+ // a first read of a large Codex folder takes seconds, and on the main process
355
+ // that is seconds of the app not answering. One worker, kept for the life of
356
+ // the app, so its transcripts stay remembered between reads. Where a worker
357
+ // cannot start, the read runs here as before.
358
+ const OFF_THREAD_DEADLINE_MS = 5 * 60_000;
359
+ let worker = null;
360
+ let asked = 0;
361
+ const waiting = new Map();
362
+ export function readCodexThreadsOffThread(opts = {}) {
363
+ try {
364
+ if (!worker) {
365
+ worker = new Worker(new URL('./codex-threads-worker.mjs', import.meta.url));
366
+ worker.unref();
367
+ worker.on('message', ({ id, ok, value, error }) => {
368
+ const w = waiting.get(id);
369
+ if (!w) return;
370
+ waiting.delete(id);
371
+ clearTimeout(w.timer);
372
+ if (ok) w.resolve(value); else w.reject(new Error(error));
373
+ });
374
+ const fail = (err) => {
375
+ for (const w of waiting.values()) { clearTimeout(w.timer); w.reject(err); }
376
+ waiting.clear();
377
+ worker = null;
378
+ };
379
+ worker.on('error', fail);
380
+ worker.on('exit', () => fail(new Error('the Codex reader stopped')));
381
+ }
382
+ } catch {
383
+ return Promise.resolve().then(() => readCodexThreads(opts));
384
+ }
385
+ const id = (asked += 1);
386
+ return new Promise((resolve, reject) => {
387
+ const timer = setTimeout(() => { waiting.delete(id); reject(new Error('the Codex reader did not answer in time')); }, OFF_THREAD_DEADLINE_MS);
388
+ timer.unref?.();
389
+ waiting.set(id, { resolve, reject, timer });
390
+ worker.postMessage({ id, opts });
391
+ });
392
+ }
@@ -16,9 +16,11 @@
16
16
  // such records across its most recent sessions, the newest written within
17
17
  // the minute.
18
18
  //
19
- // THE PUSH STILL WINS. A live notification is this second's truth and a log is
20
- // the last thing anybody wrote down, so main/codex-usage.mjs reads this only when
21
- // it has nothing of its own. Nothing about the existing path changes.
19
+ // A LIVE PUSH STILL WINS, AND A QUIET ONE NO LONGER DOES (w-7b7182ef61). A
20
+ // notification that arrived a moment ago is this second's truth and nothing on
21
+ // disk can be newer, so main/codex-usage.mjs does not open a file while one is
22
+ // fresh. Past that the two are compared and the newer is read, because an hours
23
+ // old push was being drawn over a newer line in this log.
22
24
  //
23
25
  // IT IS THE SAME ACCOUNT OR IT IS NOT READ. One CODEX_HOME is one login, and the
24
26
  // log lives inside the home, so a reading taken from `~/.codex/sessions` is a
@@ -62,8 +62,38 @@
62
62
  // level down.
63
63
 
64
64
  import { codexLimits, mergeRateLimits } from '../shared/codex-usage.mjs';
65
+ import { limitsStillRunning } from '../shared/usage.mjs';
65
66
  import { loggedCodexLimits } from './codex-usage-file.mjs';
66
67
 
68
+ const LOG_EVERY_MS = 30_000;
69
+
70
+ /**
71
+ * HOW LONG A PUSH IS THIS SECOND'S TRUTH (w-7b7182ef61).
72
+ *
73
+ * "The push still wins" holds while pushes are arriving, which is every few
74
+ * seconds during a turn; nothing on disk can be newer than one of those, so the
75
+ * busy case never opens a file. A push from four hours ago is just the oldest
76
+ * thing anybody knows, so past this it is compared against the log instead.
77
+ */
78
+ const PUSH_FRESH_MS = 60_000;
79
+
80
+ /**
81
+ * Every Codex login's own reading, for the Settings page: each out of the
82
+ * session logs under its own home, and the one in use from `current`, which
83
+ * the push keeps newer while it works. A login that has never run here gets
84
+ * no limits rather than zeroes, and a window that has reset since is dropped,
85
+ * because a login not in use can go days without a new reading.
86
+ */
87
+ export function codexAccountReadings({ profiles, homeOf, current = null, currentHome = null, now = Date.now(), readLog = loggedCodexLimits }) {
88
+ return profiles.map((profile) => {
89
+ const home = homeOf(profile);
90
+ const live = !!current?.limits?.length && home === currentHome;
91
+ const reading = live ? current : readLog(home, { now });
92
+ const limits = limitsStillRunning(live ? current.limits : codexLimits(reading?.snapshot), now);
93
+ return limits.length ? { profile, limits, at: reading.at } : { profile };
94
+ });
95
+ }
96
+
67
97
  export class CodexUsage {
68
98
  /**
69
99
  * @param {object} how @param { => string|null} how.home Which CODEX_HOME the
@@ -94,23 +124,42 @@ export class CodexUsage {
94
124
  read() {
95
125
  if (this._takenUnder !== null && this._takenUnder !== this._home()) this._forget();
96
126
  const limits = codexLimits(this._snapshot);
97
- if (limits.length) return { limits, at: this._at };
98
- // NOTHING HAS PUSHED, SO ASK WHAT CODEX ALREADY WROTE DOWN. This is the
99
- // whole of: the push only ever arrives for work the app itself ran, and a
100
- // person who runs Codex in its own app would see the panel stay empty
127
+ const pushed = limits.length ? { limits, at: this._at } : null;
128
+ if (pushed && this._now() - pushed.at < PUSH_FRESH_MS) return pushed;
129
+ // NOTHING HAS PUSHED LATELY, SO ASK WHAT CODEX ALREADY WROTE DOWN. This is
130
+ // the whole of: the push only ever arrives for work the app itself ran, and
131
+ // a person who runs Codex in its own app would see the panel stay empty
101
132
  // while their weekly limit was full. main/codex-usage-file.mjs is a read of a file
102
133
  // under this same CODEX_HOME. It starts no process and sends nothing
103
134
  // anywhere, so neither rule above is bent.
104
- return this._logged();
135
+ const logged = this._logged();
136
+ if (!pushed) return logged;
137
+ return logged && logged.at > pushed.at ? logged : pushed;
138
+ }
139
+
140
+ /**
141
+ * `read()` with the log cache dropped, for a person who pressed Refresh:
142
+ * inside `LOG_EVERY_MS` a press would otherwise change nothing. It asks
143
+ * nobody anything, so both rules at the top of this file still hold. */
144
+ refreshNow() {
145
+ this._loggedRead = null;
146
+ return this.read();
105
147
  }
106
148
 
107
149
  /**
108
150
  * The last figure in Codex's own session log under this home, shaped exactly
109
151
  * like a pushed one. Null on a Mac where Codex has never run. */
110
152
  _logged() {
111
- const reading = this._readLog(this._home(), { now: this._now() });
153
+ // Asked on every request the window makes; the log is walked at most
154
+ // every half minute.
155
+ const home = this._home();
156
+ const now = this._now();
157
+ if (this._loggedRead && this._loggedRead.home === home && now - this._loggedRead.at < LOG_EVERY_MS) return this._loggedRead.value;
158
+ const reading = this._readLog(home, { now });
112
159
  const limits = codexLimits(reading?.snapshot);
113
- return limits.length ? { limits, at: reading.at } : null;
160
+ const value = limits.length ? { limits, at: reading.at } : null;
161
+ this._loggedRead = { home, at: now, value };
162
+ return value;
114
163
  }
115
164
 
116
165
  /**
@@ -73,9 +73,17 @@ export function noticeCodexAgents(threads, { count = null, seen = null, now = Da
73
73
  return sent;
74
74
  }
75
75
 
76
+ // Answers at once for a reader that answers at once, and as a promise for one
77
+ // that reads off the main process (readCodexThreadsOffThread).
76
78
  export function scanCodex({ store, readThreads = readCodexThreads, now = Date.now(), count = null, seen = null } = {}) {
77
- let threads = [];
78
- try { ({ threads } = readThreads({ now })); } catch { return { refreshed: 0, retold: 0, threads: 0 }; }
79
+ const none = { refreshed: 0, retold: 0, threads: 0 };
80
+ let read;
81
+ try { read = readThreads({ now }); } catch { return none; }
82
+ if (typeof read?.then === 'function') return read.then(({ threads }) => applyCodexThreads(threads, { store, now, count, seen }), () => none);
83
+ return applyCodexThreads(read?.threads ?? [], { store, now, count, seen });
84
+ }
85
+
86
+ function applyCodexThreads(threads, { store, now, count, seen }) {
79
87
  noticeCodexAgents(threads, { count, seen, now });
80
88
  const refreshed = store.refreshCodexMirrors(threads, { now });
81
89
  const retold = store.refreshCodexAsks(threads, { now });
@@ -87,8 +95,14 @@ export function startCodexWatch({ store, readThreads = readCodexThreads, pollMs
87
95
  // as the Claude side's set is: a fresh launch seeing the same conversation
88
96
  // again is a fresh fact about the app being used.
89
97
  const seen = new Set();
98
+ // One scan at a time: a read slower than the minute must not stack up.
99
+ let scanning = null;
90
100
  const run = () => {
91
- try { scanCodex({ store, readThreads, count, seen }); } catch (e) { console.warn('codex watch:', e?.message ?? e); }
101
+ if (scanning) return scanning;
102
+ scanning = Promise.resolve().then(() => scanCodex({ store, readThreads, count, seen }))
103
+ .catch((e) => console.warn('codex watch:', e?.message ?? e))
104
+ .finally(() => { scanning = null; });
105
+ return scanning;
92
106
  };
93
107
  const first = setTimeout(run, firstDelayMs);
94
108
  const timer = setInterval(run, pollMs);