agentbox-app 0.1.7 → 0.1.12

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 (255) hide show
  1. package/README.md +220 -58
  2. package/bin/agentbox.mjs +18 -3
  3. package/briefs/message-rules.md +24 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +11 -11
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +116 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/approvals.mjs +14 -5
  16. package/main/artifact-path.mjs +11 -0
  17. package/main/claude-bin.mjs +32 -5
  18. package/main/claude-input.mjs +45 -3
  19. package/main/claude-plan.mjs +25 -13
  20. package/main/code-change.mjs +3 -3
  21. package/main/codex-account.mjs +1 -1
  22. package/main/codex-bin.mjs +18 -0
  23. package/main/codex-launch-env.mjs +31 -0
  24. package/main/codex-memory-gate.mjs +260 -0
  25. package/main/codex-models.mjs +12 -7
  26. package/main/codex-threads.mjs +46 -23
  27. package/main/codex-usage-file.mjs +7 -7
  28. package/main/codex-usage.mjs +4 -4
  29. package/main/codex-watch.mjs +12 -12
  30. package/main/config.mjs +82 -17
  31. package/main/demo.mjs +17 -14
  32. package/main/dev-window.mjs +9 -9
  33. package/main/doc-scheme.mjs +2 -0
  34. package/main/engine-setup.mjs +204 -0
  35. package/main/feedback.mjs +55 -0
  36. package/main/folder-pictures.mjs +48 -0
  37. package/main/frame-scrollbars.mjs +90 -0
  38. package/main/fresh-user.mjs +7 -4
  39. package/main/git-change-offthread.mjs +70 -0
  40. package/main/git-change-thread.mjs +11 -0
  41. package/main/git-change.mjs +9 -3
  42. package/main/img-scheme.mjs +70 -1
  43. package/main/instruction-settings.mjs +134 -60
  44. package/main/ipc.mjs +335 -42
  45. package/main/leftovers.mjs +518 -0
  46. package/main/live-replies.mjs +43 -5
  47. package/main/machine.mjs +48 -1
  48. package/main/main.mjs +138 -15
  49. package/main/memory-gate-history.mjs +254 -0
  50. package/main/memory-gate-server.mjs +461 -0
  51. package/main/memory-gate.mjs +206 -0
  52. package/main/message-priority.mjs +87 -0
  53. package/main/notify.mjs +1 -1
  54. package/main/rail-note.mjs +2 -2
  55. package/main/refresh-agent-models.mjs +21 -0
  56. package/main/repeats.mjs +4 -4
  57. package/main/row-label.mjs +115 -51
  58. package/main/runs-on.mjs +93 -0
  59. package/main/serve.mjs +77 -5
  60. package/main/settings.mjs +245 -26
  61. package/main/ship-queue.mjs +279 -0
  62. package/main/sign-in-files.mjs +39 -0
  63. package/main/source-updater.mjs +298 -0
  64. package/main/store/home.mjs +84 -7
  65. package/main/store/project.mjs +15 -19
  66. package/main/store/work-items.mjs +150 -6
  67. package/main/store.mjs +245 -13
  68. package/main/supervisor.mjs +1634 -417
  69. package/main/task-commands.mjs +8 -10
  70. package/main/task-folders.mjs +94 -16
  71. package/main/task-remote-control.mjs +3 -3
  72. package/main/task-terminals.mjs +14 -2
  73. package/main/team/index.mjs +440 -0
  74. package/main/team/memory-cloud.mjs +247 -0
  75. package/main/team/projects.mjs +162 -0
  76. package/main/team/session.mjs +225 -0
  77. package/main/team/sign-in.mjs +106 -0
  78. package/main/team/supabase-backend.mjs +231 -0
  79. package/main/team/sync.mjs +163 -0
  80. package/main/updater.mjs +15 -5
  81. package/main/write-before-reload.mjs +50 -0
  82. package/mcp/core/page.mjs +10 -12
  83. package/mcp/core/work.mjs +123 -26
  84. package/mcp/tools.mjs +13 -6
  85. package/package.json +21 -9
  86. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  87. package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
  88. package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
  89. package/renderer/dist/index.html +2 -2
  90. package/scripts/after-install.mjs +14 -0
  91. package/scripts/approval-server.sh +7 -2
  92. package/scripts/memory-gate-hook.sh +59 -0
  93. package/shared/agent-import.mjs +39 -16
  94. package/shared/agents.d.mts +26 -8
  95. package/shared/agents.mjs +100 -108
  96. package/shared/analytics-events.mjs +19 -2
  97. package/shared/answers.mjs +12 -0
  98. package/shared/bridge-map.d.mts +12 -0
  99. package/shared/bridge-map.mjs +38 -1
  100. package/shared/claude-commands.generated.mjs +2 -2
  101. package/shared/claude-models.generated.mjs +6 -6
  102. package/shared/claude-usage.mjs +6 -6
  103. package/shared/codex-import.mjs +7 -7
  104. package/shared/codex-modes.d.mts +50 -0
  105. package/shared/codex-modes.mjs +1 -1
  106. package/shared/codex-usage.mjs +7 -8
  107. package/shared/demo-world.mjs +6 -6
  108. package/shared/engines.mjs +27 -10
  109. package/shared/feedback.d.mts +14 -0
  110. package/shared/feedback.mjs +44 -0
  111. package/shared/first-run-examples.mjs +4 -4
  112. package/shared/first-run-practice.d.mts +16 -0
  113. package/shared/first-run-practice.mjs +176 -70
  114. package/shared/fresh-user-home.mjs +34 -4
  115. package/shared/instruction-defaults.json +2 -2
  116. package/shared/product-name.mjs +4 -2
  117. package/shared/project-folder-check.mjs +9 -3
  118. package/shared/rank.d.mts +1 -0
  119. package/shared/rank.mjs +22 -0
  120. package/shared/referenced-files.mjs +1 -1
  121. package/shared/runs-on.d.mts +20 -0
  122. package/shared/runs-on.mjs +79 -0
  123. package/shared/schemes.d.mts +11 -0
  124. package/shared/schemes.mjs +1 -1
  125. package/shared/shortcuts.d.mts +41 -0
  126. package/shared/shortcuts.mjs +236 -0
  127. package/shared/spawn-trouble.d.mts +2 -0
  128. package/shared/spawn-trouble.mjs +40 -19
  129. package/shared/team-rules.d.mts +17 -0
  130. package/shared/team-rules.mjs +230 -0
  131. package/shared/team-status.d.mts +11 -0
  132. package/shared/team-status.mjs +90 -0
  133. package/shared/terminal-links.d.mts +2 -0
  134. package/shared/terminal-links.mjs +55 -0
  135. package/shared/terminal-state.d.mts +1 -0
  136. package/shared/terminal-state.mjs +16 -0
  137. package/shared/thread-cards.d.mts +19 -0
  138. package/shared/thread-cards.mjs +254 -0
  139. package/shared/usage.d.mts +1 -1
  140. package/shared/usage.mjs +6 -7
  141. package/shared/work-items.mjs +258 -28
  142. package/shared/work-lines.mjs +393 -16
  143. package/worker-permissions.json +1 -1
  144. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  145. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  146. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  147. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  148. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  149. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  150. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  151. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  152. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  153. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  154. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  155. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  156. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  157. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  158. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  159. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  160. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  161. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  162. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  163. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  164. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  165. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  166. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  167. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  168. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  169. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  170. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  171. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  172. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  173. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  174. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  175. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  176. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  177. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  178. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  179. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  180. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  181. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  182. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  183. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  184. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  185. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  186. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  187. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  188. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  189. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  190. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  191. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  192. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  193. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  194. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  195. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  196. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  197. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  198. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  199. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  200. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  201. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  202. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  203. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  204. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  205. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  206. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  207. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  208. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  209. package/renderer/dist/emulator/data/compression/README.md +0 -11
  210. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  211. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  212. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  213. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  214. package/renderer/dist/emulator/data/cores/README.md +0 -22
  215. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  216. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  217. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  218. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  219. package/renderer/dist/emulator/data/cores/package.json +0 -65
  220. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  221. package/renderer/dist/emulator/data/emulator.css +0 -1614
  222. package/renderer/dist/emulator/data/loader.js +0 -170
  223. package/renderer/dist/emulator/data/localization/README.md +0 -63
  224. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  225. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  226. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  227. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  228. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  229. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  230. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  231. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  232. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  233. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  234. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  235. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  236. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  237. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  238. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  239. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  240. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  241. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  242. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  243. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  244. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  245. package/renderer/dist/emulator/data/src/compression.js +0 -142
  246. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  247. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  248. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  249. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  250. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  251. package/renderer/dist/emulator/data/src/storage.js +0 -125
  252. package/renderer/dist/emulator/data/version.json +0 -5
  253. package/renderer/dist/emulator/host.js +0 -299
  254. package/renderer/dist/emulator/index.html +0 -28
  255. package/renderer/dist/emulator/provenance.json +0 -6
@@ -0,0 +1,31 @@
1
+ // An npm Codex executable starts with `#!/usr/bin/env node`. Finding that
2
+ // executable does not put its Node on a desktop app's launchd PATH.
3
+ // Resolve from disk, without running shell profiles or changing the app's env.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { candidatePaths, versionedPaths } from './codex-bin.mjs';
8
+
9
+ function executable(file) {
10
+ try {
11
+ fs.accessSync(file, fs.constants.X_OK);
12
+ return fs.statSync(file).isFile();
13
+ } catch { return false; }
14
+ }
15
+
16
+ export function codexLaunchEnv(bin, source = process.env, {
17
+ home = source.HOME || os.homedir(), exists = executable, readdir,
18
+ } = {}) {
19
+ if (!bin) return { ...source };
20
+ const binDir = path.dirname(bin);
21
+ const inherited = (source.PATH || '').split(path.delimiter).filter(Boolean);
22
+ // Prefer the Node beside this Codex, then the user's existing PATH. A custom
23
+ // npm prefix can hold Codex while its Node lives under nvm, fnm, or asdf.
24
+ const directories = [...new Set([
25
+ binDir, ...inherited,
26
+ ...candidatePaths(home).map(path.dirname),
27
+ ...versionedPaths(home, readdir).map(path.dirname),
28
+ ])];
29
+ const nodeDir = directories.find(dir => exists(path.join(dir, 'node')));
30
+ return { ...source, PATH: [...new Set([binDir, ...(nodeDir ? [nodeDir] : []), ...inherited])].join(path.delimiter) };
31
+ }
@@ -0,0 +1,260 @@
1
+ // HOW A CODEX WORKER'S COMMANDS COME TO ASK ABOUT MEMORY — w-e5225b62ba.
2
+ //
3
+ // w-3958c3753d built the whole of the decision (main/memory-gate.mjs), the
4
+ // coordinator (main/memory-gate-server.mjs) and the hook a worker runs
5
+ // (scripts/memory-gate-hook.sh) for Claude Code, and said plainly that Codex
6
+ // workers were not covered. This file is the Codex half, and it is small on
7
+ // purpose: everything measured below says Codex 0.160 presents a PreToolUse
8
+ // hook the way Claude Code does, so the script, the deny and the coordinator
9
+ // are carried over rather than rewritten.
10
+ //
11
+ // MEASURED ON THIS MAC, codex-cli 0.160.0, a real `codex app-server` and real
12
+ // turns, 2026-10-04. Read none of this as documentation; it was all run.
13
+ //
14
+ // THE PAYLOAD IS CLAUDE CODE'S, FIELD FOR FIELD:
15
+ // { session_id, turn_id, transcript_path, cwd, hook_event_name: "PreToolUse",
16
+ // model, permission_mode, tool_name: "Bash",
17
+ // tool_input: { command }, tool_use_id }
18
+ // and the deny it reads is the same `hookSpecificOutput.permissionDecision`
19
+ // object `denyOutput` already prints. A deny blocked the command and our
20
+ // sentence reached the model verbatim. A hook that slept eight seconds made
21
+ // the command wait and then run.
22
+ //
23
+ // THE MATCHER IS "Bash". A group with matcher "shell" listed as trusted and
24
+ // enabled and never fired once; with no matcher it fired and said its
25
+ // tool_name was "Bash". That one word was the difference between this working
26
+ // and silently doing nothing.
27
+ //
28
+ // THE FILE IS `hooks.json` IN THE CODEX HOME, `{ description?, hooks: {...} }`,
29
+ // whose event keys are PascalCase (`PreToolUse`) even though Codex's own
30
+ // errors name them in snake_case. An unknown event key is dropped in silence,
31
+ // which is why the key is pinned by a test rather than trusted to a comment.
32
+ //
33
+ // A HOOK ONLY RUNS ONCE TRUSTED, AND TRUST IS PER HOOK. `hooks/list` reports
34
+ // every hook with a `key`, a `currentHash` and a `trustStatus` of managed /
35
+ // untrusted / trusted / modified. Writing
36
+ // `hooks.state."<key>".trusted_hash = <currentHash>` through the app-server's
37
+ // own `config/batchWrite` turned OURS trusted and left a second hook in the
38
+ // same file untrusted, with an unrelated `[projects."..."]` table in
39
+ // config.toml untouched. That is the whole of "do not switch off hook trust
40
+ // for anyone's other hooks": we never touch the requirement, we satisfy it for
41
+ // one key. Editing our own command afterwards turned ours "modified" rather
42
+ // than leaving it trusted, so a hook somebody swapped under us does not
43
+ // inherit what we wrote.
44
+ //
45
+ // WHY THE ITEM IS NOT IN THE ENVIRONMENT, which is the one real difference from
46
+ // the Claude side. A Claude worker is a process per work item, so `memoryGateEnv`
47
+ // puts the item on it. ONE app-server serves every Codex thread of a login, so
48
+ // there is one environment for all of them and an item id on it would label
49
+ // every thread as whichever row started first. The thread names itself instead:
50
+ // `session_id` in the payload is the app-server's own `thread.id`, which
51
+ // main/codex.mjs already keeps as `session.sessionId`. The lookup lives in
52
+ // main/memory-gate-server.mjs (`ownerOf`).
53
+
54
+ import fsNode from 'node:fs';
55
+ import path from 'node:path';
56
+
57
+ /** What Codex calls the tool that runs a shell command. Measured, not guessed. */
58
+ export const CODEX_MATCHER = 'Bash';
59
+
60
+ /** The file in a Codex home that holds its hooks. */
61
+ export const HOOKS_FILE = 'hooks.json';
62
+
63
+ /** The two events Codex has that we want. There is no PostToolUseFailure. */
64
+ export const CODEX_EVENTS = ['PreToolUse', 'PostToolUse'];
65
+
66
+ /** How we know a hook in somebody's file is one of ours. */
67
+ const OUR_SCRIPT = 'memory-gate-hook.sh';
68
+
69
+ /**
70
+ * Whether a hook's command is one we wrote. It has to be the script ITSELF and
71
+ * not a file whose name merely starts the same way, or turning the switch off
72
+ * would delete `memory-gate-hook.sh.backup` out of somebody's config.
73
+ */
74
+ export function hookIsOurs(command) {
75
+ if (typeof command !== 'string') return false;
76
+ return new RegExp(`${OUR_SCRIPT}'?\\s+(pre|post)\\s*$`).test(command.trim());
77
+ }
78
+
79
+ /** A path inside a single-quoted shell word, app bundles and apostrophes included. */
80
+ function quoted(scriptPath) {
81
+ return `'${String(scriptPath).replace(/'/g, `'\\''`)}'`;
82
+ }
83
+
84
+ /**
85
+ * Our matcher groups, one per event.
86
+ *
87
+ * The pre hook's limit sits above the longest wait the gate will ever impose,
88
+ * for the reason the Claude side gives: a hook that times out lets its command
89
+ * run, so the app's own refusal has to arrive first. Codex counts it in seconds.
90
+ */
91
+ export function ourHookGroups(scriptPath, { maxWaitSec = 20 * 60 } = {}) {
92
+ const group = (mode, timeout) => ([{
93
+ matcher: CODEX_MATCHER,
94
+ hooks: [{ type: 'command', command: `${quoted(scriptPath)} ${mode}`, timeout }],
95
+ }]);
96
+ return {
97
+ PreToolUse: group('pre', maxWaitSec + 600),
98
+ PostToolUse: group('post', 10),
99
+ };
100
+ }
101
+
102
+ /** A hooks.json object, whatever shape the file on disk turned out to be. */
103
+ function asFile(file) {
104
+ const base = file && typeof file === 'object' && !Array.isArray(file) ? { ...file } : {};
105
+ base.hooks = base.hooks && typeof base.hooks === 'object' && !Array.isArray(base.hooks) ? { ...base.hooks } : {};
106
+ return base;
107
+ }
108
+
109
+ /** Every group in `list` that is not one of ours, in the order it was written. */
110
+ function withoutOurGroups(list) {
111
+ if (!Array.isArray(list)) return [];
112
+ return list.filter((g) => !(g?.hooks ?? []).some?.((h) => hookIsOurs(h?.command)));
113
+ }
114
+
115
+ /**
116
+ * The same hooks.json with our two hooks in it and everything else where it was.
117
+ *
118
+ * Ours go LAST so a hook somebody already had keeps running first, and any older
119
+ * entry of ours is replaced rather than kept beside the new one: the app moves
120
+ * between versions and a stale absolute path is a worker asking a script that is
121
+ * not there. Running this twice gives the same file, so turning the switch on
122
+ * again adds nothing.
123
+ */
124
+ export function withOurHooks(file, scriptPath, options) {
125
+ const out = asFile(file);
126
+ const ours = ourHookGroups(scriptPath, options);
127
+ for (const event of CODEX_EVENTS) {
128
+ out.hooks[event] = [...withoutOurGroups(out.hooks[event]), ...ours[event]];
129
+ }
130
+ return out;
131
+ }
132
+
133
+ /**
134
+ * The same hooks.json with ours taken out, for when the switch goes off. An
135
+ * event left with nothing in it loses its key entirely, so a file that was only
136
+ * ever ours reads as a file with no hooks rather than as empty lists.
137
+ */
138
+ export function withoutOurHooks(file) {
139
+ const out = asFile(file);
140
+ for (const event of Object.keys(out.hooks)) {
141
+ const kept = withoutOurGroups(out.hooks[event]);
142
+ if (kept.length) out.hooks[event] = kept;
143
+ else delete out.hooks[event];
144
+ }
145
+ return out;
146
+ }
147
+
148
+ /** A TOML key as `config/batchWrite` wants it: one quoted segment, escaped. */
149
+ function tomlKey(key) {
150
+ return JSON.stringify(String(key));
151
+ }
152
+
153
+ /**
154
+ * The `config/batchWrite` edits that trust our hooks and nothing else.
155
+ *
156
+ * Takes either a flat list of hook entries or a whole `hooks/list` answer
157
+ * (one record per cwd), because the same hook is reported once per cwd and must
158
+ * be asked for once. A hook already trusted needs nothing; a `managed` one is an
159
+ * admin's decision and is left alone; a `modified` one is ours whose script
160
+ * changed with the app, and re-trusting it is the whole reason this runs at every
161
+ * start rather than once at install.
162
+ */
163
+ export function ourTrustEdits(answer) {
164
+ const entries = [];
165
+ for (const row of Array.isArray(answer) ? answer : []) {
166
+ if (Array.isArray(row?.hooks)) entries.push(...row.hooks);
167
+ else if (row?.key) entries.push(row);
168
+ }
169
+ const edits = new Map();
170
+ for (const h of entries) {
171
+ if (!h?.key || !h?.currentHash) continue;
172
+ if (!hookIsOurs(h.command)) continue;
173
+ if (h.trustStatus === 'trusted' || h.trustStatus === 'managed') continue;
174
+ edits.set(h.key, { keyPath: `hooks.state.${tomlKey(h.key)}.trusted_hash`, mergeStrategy: 'upsert', value: h.currentHash });
175
+ }
176
+ return [...edits.values()];
177
+ }
178
+
179
+ /**
180
+ * PUT OUR HOOK IN A CODEX HOME, OR TAKE IT OUT, AND TRUST ONLY OUR OWN.
181
+ *
182
+ * Runs at every app start while the switch is on, not once at install, because
183
+ * three things change underneath it: the app's path (a new version moves the
184
+ * script, and Codex then calls our hook "modified" and stops running it), the
185
+ * user's own hooks.json (they may add or remove their own entries at any time),
186
+ * and the home itself (a second Codex login is a second home).
187
+ *
188
+ * NOTHING HERE WRITES config.toml. The trust goes through `config/batchWrite`,
189
+ * which is the call the Codex TUI itself uses, so Codex edits its own file and
190
+ * every other setting and every other hook's trust survives untouched. Measured
191
+ * on this Mac: an unrelated `[projects."..."]` table was still there afterwards,
192
+ * and a second hook in the same hooks.json was still `untrusted`.
193
+ *
194
+ * It is all best-effort. A Codex home we cannot write, an app-server that will
195
+ * not answer, a refused config write: each one means Codex workers run ungated,
196
+ * which is exactly where they were before this existed. It must never be a
197
+ * reason the app fails to start.
198
+ *
199
+ * @param {object} how
200
+ * @param {string} how.home The CODEX_HOME to write in.
201
+ * @param {string} how.scriptPath scripts/memory-gate-hook.sh, unpacked.
202
+ * @param {boolean} how.on Whether the memory check is on.
203
+ * @param {(method: string, params: object) => Promise<any>} [how.request] The
204
+ * app-server call. Omitted means "write the file, do not try to trust it".
205
+ * @param {string[]} [how.cwds] Folders to ask `hooks/list` about. A hook in the
206
+ * home is listed for any cwd, so one is enough; the app's own folder is used.
207
+ * @param {object} [how.fs] Injected for tests.
208
+ * @param {(line: string) => void} [how.log]
209
+ * @returns {Promise<{wrote: boolean, trusted: number, error: string|null}>}
210
+ */
211
+ export async function syncCodexMemoryGate({ home, scriptPath, on, request = null, cwds = [], fs = fsNode, log = () => {} }) {
212
+ const out = { wrote: false, trusted: 0, error: null };
213
+ if (!home || (on && !scriptPath)) { out.error = 'no codex home'; return out; }
214
+ const file = path.join(home, HOOKS_FILE);
215
+
216
+ // THEIR FILE IS READ BEFORE IT IS WRITTEN, AND A FILE WE CANNOT UNDERSTAND IS
217
+ // LEFT ALONE. A hooks.json that exists and is not JSON is somebody's own,
218
+ // broken or newer than us; replacing it to install ours would throw their work
219
+ // away. Codex workers then run ungated, which is where they already were.
220
+ let raw = null;
221
+ try { raw = fs.readFileSync(file, 'utf8'); } catch { raw = null; }
222
+ let before = null;
223
+ if (raw !== null && raw.trim()) {
224
+ try { before = JSON.parse(raw); } catch {
225
+ out.error = `${file} is not readable JSON, so it was left alone`;
226
+ log(`memory gate: ${out.error}`);
227
+ return out;
228
+ }
229
+ }
230
+ // Off, and we were never in it: nothing to do, and no file to create.
231
+ if (!on && before === null) return out;
232
+
233
+ const wanted = `${JSON.stringify(on ? withOurHooks(before, scriptPath) : withoutOurHooks(before), null, 2)}\n`;
234
+ if (raw !== wanted) {
235
+ try {
236
+ fs.mkdirSync(home, { recursive: true });
237
+ const tmp = `${file}.${process.pid}.tmp`;
238
+ fs.writeFileSync(tmp, wanted);
239
+ fs.renameSync(tmp, file);
240
+ out.wrote = true;
241
+ } catch (err) {
242
+ out.error = `could not write ${file}: ${err.message}`;
243
+ log(`memory gate: ${out.error}`);
244
+ return out;
245
+ }
246
+ }
247
+ if (!on || typeof request !== 'function') return out;
248
+
249
+ // Trust ours. `hooks/list` is what reports the key and the hash Codex computed,
250
+ // and neither can be worked out from here: the hash is not of the command text.
251
+ try {
252
+ const listed = await request('hooks/list', { cwds });
253
+ const edits = ourTrustEdits(listed?.data ?? listed);
254
+ if (!edits.length) return out;
255
+ await request('config/batchWrite', { edits, reloadUserConfig: true });
256
+ out.trusted = edits.length;
257
+ log(`memory gate: trusted ${edits.length} Codex hook${edits.length === 1 ? '' : 's'} in ${home}`);
258
+ } catch (err) { out.error = `could not trust the Codex hook: ${err.message}`; log(`memory gate: ${out.error}`); }
259
+ return out;
260
+ }
@@ -27,10 +27,9 @@
27
27
  // models (checked against 0.148.0), and even if it did, a picker must never be
28
28
  // the reason a login shell is spawned while she is typing.
29
29
  //
30
- // AND NOTHING HERE IS CACHED IN MEMORY, for the reason main/codex-bin.mjs
31
- // gives about its own stat calls: caching a cheap read is how a screen ends up
32
- // still saying the old answer after the thing changed underneath it. The CLI
33
- // rewrites this file whenever its etag moves, and a spawn is not a hot path.
30
+ // A successful model/list refresh now keeps an account-scoped live catalog in
31
+ // memory. Startup and hourly refreshes replace it; before the first success,
32
+ // the CLI-maintained disk cache remains the fallback.
34
33
  //
35
34
  // ---------------------------------------------------------------------------
36
35
  // TWO QUESTIONS, NOT ONE, AND THE 2026-08-26 ORIGINAL ONLY ASKED THE FIRST.
@@ -60,6 +59,12 @@
60
59
  import fs from 'node:fs';
61
60
  import os from 'node:os';
62
61
  import path from 'node:path';
62
+ const liveCatalogs=new Map();
63
+ export function rememberCodexModels(home,models){
64
+ const rows=models.filter(m=>typeof m.model==='string'&&m.model).map((m,i)=>({slug:m.model,display_name:m.displayName,visibility:m.hidden?'hide':'list',priority:i,default_reasoning_level:m.defaultReasoningEffort,supported_reasoning_levels:(m.supportedReasoningEfforts||[]).map(l=>({effort:l.reasoningEffort,description:l.description}))}));
65
+ if(rows.length)liveCatalogs.set(home,rows);
66
+ }
67
+ const catalogRows=(home,read)=>liveCatalogs.get(home)??modelRows(read(path.join(home,'models_cache.json')));
63
68
 
64
69
  /**
65
70
  * WHICH CODEX HOME. Her own setting first, then the environment, then the
@@ -93,7 +98,7 @@ export function codexHome({ configured = null, env = process.env, home = os.home
93
98
  * supervisor's refusal is built to need a list before it refuses anything.
94
99
  */
95
100
  export function codexModels({ home = codexHome(), read = readJson } = {}) {
96
- return modelRows(read(path.join(home, 'models_cache.json')))
101
+ return catalogRows(home,read)
97
102
  .filter((m) => m.visibility === 'list')
98
103
  .sort((a, b) => priorityOf(a) - priorityOf(b))
99
104
  .map((m) => ({
@@ -112,7 +117,7 @@ export function codexModels({ home = codexHome(), read = readJson } = {}) {
112
117
  * were handed the word, which is a different question from what she may pick.
113
118
  */
114
119
  export function codexKnownSlugs({ home = codexHome(), read = readJson } = {}) {
115
- return new Set(modelRows(read(path.join(home, 'models_cache.json'))).map((m) => String(m.slug)));
120
+ return new Set(catalogRows(home,read).map((m) => String(m.slug)));
116
121
  }
117
122
 
118
123
  /**
@@ -126,7 +131,7 @@ export function codexKnownSlugs({ home = codexHome(), read = readJson } = {}) {
126
131
  * `codexModels`, asked about the other field.
127
132
  */
128
133
  export function codexModelLevels(slug, { home = codexHome(), read = readJson } = {}) {
129
- const cache = read(path.join(home, 'models_cache.json'));
134
+ const cache = liveCatalogs.has(home)?{models:liveCatalogs.get(home)}:read(path.join(home, 'models_cache.json'));
130
135
  if (!cache) return null;
131
136
  const row = modelRows(cache).find((m) => m.slug === slug);
132
137
  if (!row) return null;
@@ -14,11 +14,11 @@
14
14
  // events, of which `user_message` and
15
15
  // `agent_message` are the two this reads.
16
16
  //
17
- // WHICH THREADS ARE HERS. `thread_source` is `user` for one she typed into,
18
- // `guardian_review` for Codex's own reviewer of a turn, and `subagent` for a
19
- // helper a thread spawned. 247 transcripts were touched in the seven days to
20
- // 09-14; 158 of them were guardians. Without this filter the inbox would offer
21
- // her four rows for every conversation she had.
17
+ // WHICH THREADS ARE THE USER'S. `thread_source` is `user` for one a person typed
18
+ // into, `guardian_review` for Codex's own reviewer of a turn, and `subagent` for
19
+ // a helper a thread spawned. Guardians are the majority of transcripts, so
20
+ // without this filter the inbox would offer several rows for every real
21
+ // conversation.
22
22
  //
23
23
  // There is also `~/.codex/state_5.sqlite`, which has everything above in one
24
24
  // table. It is not read: Electron ships no sqlite, the file is written live
@@ -26,9 +26,12 @@
26
26
  import fs from 'node:fs';
27
27
  import os from 'node:os';
28
28
  import path from 'node:path';
29
- import { isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
29
+ import { RECENT_DAYS, isScratchFolder, shortFolder, threadTitle } from './agent-sessions.mjs';
30
+ import { WAS_SLUGS, nameSlug } from '../shared/product-name.mjs';
30
31
 
31
- export const CODEX_RECENT_DAYS = 7;
32
+ export const CODEX_RECENT_DAYS = RECENT_DAYS;
33
+ /** The originator Codex records for a run this app started (CLIENT_INFO.name), now and before. */
34
+ const OUR_ORIGINATORS = new Set([nameSlug, ...WAS_SLUGS]);
32
35
  /** How many day folders the walk opens at most, whatever `days` says. */
33
36
  const MAX_DAY_FOLDERS = 40;
34
37
  /**
@@ -62,22 +65,38 @@ const numericDesc = (dir) => {
62
65
  return names.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
63
66
  };
64
67
 
65
- /** Every transcript in the newest `days` day folders, newest folder first. */
66
- export function listRolloutFiles(codexDir = codexHome(), { days = CODEX_RECENT_DAYS } = {}) {
68
+ /**
69
+ * Every transcript in the newest `days` day folders, newest folder first.
70
+ *
71
+ * AND, WITH `since`, EVERY OLDER ONE WRITTEN TO SINCE THEN. Codex files a
72
+ * conversation under the day it STARTED, so a thread begun two weeks ago and
73
+ * worked in yesterday sits in a folder the newest-`days` walk never opens
74
+ * (w-db6f5e331e). Past those folders only a stat is paid per file, and the
75
+ * walk still stops at MAX_DAY_FOLDERS. */
76
+ export function listRolloutFiles(codexDir = codexHome(), { days = CODEX_RECENT_DAYS, since = null } = {}) {
67
77
  const root = path.join(codexDir, 'sessions');
68
78
  const out = [];
69
79
  let looked = 0;
70
80
  const limit = Math.min(Math.max(1, Number(days) || 1), MAX_DAY_FOLDERS);
81
+ const reach = since == null ? limit : MAX_DAY_FOLDERS;
71
82
  for (const year of numericDesc(root)) {
72
83
  for (const month of numericDesc(path.join(root, year))) {
73
84
  for (const day of numericDesc(path.join(root, year, month))) {
74
- if (looked >= limit) return out;
85
+ if (looked >= reach) return out;
86
+ const older = looked >= limit;
75
87
  looked += 1;
76
88
  const dir = path.join(root, year, month, day);
77
89
  let names = [];
78
90
  try { names = fs.readdirSync(dir); } catch { names = []; }
79
91
  for (const name of names) {
80
- if (/^rollout-.*\.jsonl$/.test(name)) out.push(path.join(dir, name));
92
+ if (!/^rollout-.*\.jsonl$/.test(name)) continue;
93
+ const file = path.join(dir, name);
94
+ if (older) {
95
+ let mtime = 0;
96
+ try { mtime = fs.statSync(file).mtimeMs; } catch { continue; }
97
+ if (mtime < since) continue;
98
+ }
99
+ out.push(file);
81
100
  }
82
101
  }
83
102
  }
@@ -103,17 +122,17 @@ export function cleanPrompt(text) {
103
122
  * WHAT CODEX PUTS IN HER MOUTH. In paginated history mode the user turn is a
104
123
  * `response_item` with several `<recommended_plugins>…`,
105
124
  * `<environment_context>…`, `<user_instructions>…`. Every one of those opens
106
- * with an angle-bracket tag on its first line, and nothing she types does.
107
- * Measured on the thread she screenshotted (2026-09-15): three parts, the
125
+ * with an angle-bracket tag on its first line, and nothing a person types does.
126
+ * Measured on a real thread: three parts, the
108
127
  * first two injected. */
109
128
  export function isInjected(text) {
110
129
  const t = String(text ?? '').trimStart();
111
130
  const first = t.split('\n')[0] ?? '';
112
131
  if (/^<[a-z_]+[^>]*>/i.test(first)) return true;
113
132
  // The folder's AGENTS.md, pasted in as "# AGENTS.md instructions for
114
- // /path" followed by an <INSTRUCTIONS> block. Measured on the thread she
115
- // screenshotted (2026-09-15): it was the second part of her first turn, and
116
- // the first read of it printed it as what she asked.
133
+ // /path" followed by an <INSTRUCTIONS> block. Measured on a real thread: it
134
+ // was the second part of the first turn, and the first read of it printed it
135
+ // as what the user asked.
117
136
  if (/^#\s*AGENTS\.md instructions/i.test(first)) return true;
118
137
  return /^\s*<INSTRUCTIONS>/i.test(t.slice(first.length, first.length + 200));
119
138
  }
@@ -142,9 +161,8 @@ const partsOf = (payload) => (Array.isArray(payload?.content) ? payload.content
142
161
  * lines of type `user_message` / `agent_message` with a `message` string.
143
162
  * Paginated writes none of those: the turns are `response_item` lines of type
144
163
  * `message` with a `role` and a `content` array of `input_text`, `input_image`
145
- * and `output_text` parts. Measured 2026-09-15 across the seven days before:
146
- * all 61 of her own threads were paginated, and the first build read only
147
- * legacy, which is why her row said "Codex has not answered yet." */
164
+ * and `output_text` parts. Current Codex threads are paginated, and a build
165
+ * that read only legacy showed "Codex has not answered yet." on every row. */
148
166
  export function readRollout(file) {
149
167
  let text = '';
150
168
  try {
@@ -177,7 +195,12 @@ export function readRollout(file) {
177
195
  meta = {
178
196
  id: String(p.session_id ?? p.id ?? '').trim(),
179
197
  cwd: String(p.cwd ?? '').trim(),
180
- threadSource: typeof p.thread_source === 'string' ? p.thread_source : (typeof p.source === 'string' ? 'user' : 'other'),
198
+ // A RUN THIS APP STARTED IS NOT HERS TO IMPORT (w-db6f5e331e). Codex
199
+ // 0.160.0 writes no thread_source, so the line after this would call
200
+ // every run a person's; its originator is the clientInfo name we hand
201
+ // Codex, under this name or one the app had before.
202
+ threadSource: OUR_ORIGINATORS.has(String(p.originator ?? '').toLowerCase()) ? 'agentbox'
203
+ : typeof p.thread_source === 'string' ? p.thread_source : (typeof p.source === 'string' ? 'user' : 'other'),
181
204
  startedAt: Number.isFinite(startedAt) ? startedAt : 0,
182
205
  };
183
206
  continue;
@@ -227,15 +250,15 @@ export const LIVE_MS = 2 * 60_000;
227
250
  export const isLive = (mtime, now = Date.now()) => Number(mtime) > 0 && now - Number(mtime) < LIVE_MS;
228
251
 
229
252
  /**
230
- * HER CONVERSATIONS, newest first, in the shape the import card and the
231
- * import row both read. One entry per thread she typed into, in a real folder,
253
+ * THE USER'S CONVERSATIONS, newest first, in the shape the import card and the
254
+ * import row both read. One entry per thread a person typed into, in a real folder,
232
255
  * touched in the last `days` days. */
233
256
  export function readCodexThreads({ home = os.homedir(), codexDir = codexHome(home), days = CODEX_RECENT_DAYS, now = Date.now() } = {}) {
234
257
  const index = readCodexIndex(codexDir);
235
258
  const cutoff = now - Math.max(0, Number(days) || 0) * 86400 * 1000;
236
259
  const out = [];
237
260
  const skipped = { notHers: 0, scratch: 0, old: 0, empty: 0, unreadable: 0 };
238
- for (const file of listRolloutFiles(codexDir, { days })) {
261
+ for (const file of listRolloutFiles(codexDir, { days, since: cutoff })) {
239
262
  const r = readRollout(file);
240
263
  if (!r) { skipped.unreadable += 1; continue; }
241
264
  if (r.threadSource !== 'user') { skipped.notHers += 1; continue; }
@@ -4,17 +4,17 @@
4
4
  // and it is a good one: never spawn anything and never ask OpenAI anything to
5
5
  // draw a picture. Codex's figures ride the app-server the fleet is already
6
6
  // running and arrive unprompted during a turn. The unstated cost of that rule is
7
- // that the app only ever learns a number for work POWERUP ITSELF RAN. She runs
8
- // Codex in its own app all day, so on her Mac the push had never arrived and the
9
- // panel was empty while her weekly limit sat at 100% used.
7
+ // that the app only ever learns a number for work POWERUP ITSELF RAN. Someone
8
+ // who runs Codex in its own app all day never gets the push, so the panel sits
9
+ // empty while their weekly limit is at 100% used.
10
10
  //
11
11
  // SO THERE IS A THIRD SOURCE, AND IT BREAKS NEITHER HALF OF THAT RULE. Codex
12
12
  // writes every rate-limit report it receives into its own session log under
13
13
  // CODEX_HOME, as a `rate_limits` object on the `token_count` events. Reading it
14
14
  // starts no process, sends nothing anywhere, and touches no credential: it is a
15
- // file on her disk that Codex put there. Measured on her Mac 2026-09-18: 1,140
16
- // such records across her twenty-five most recent sessions, the newest written
17
- // the same minute she filed the row.
15
+ // file on the disk that Codex put there. A busy Codex home holds hundreds of
16
+ // such records across its most recent sessions, the newest written within
17
+ // the minute.
18
18
  //
19
19
  // THE PUSH STILL WINS. A live notification is this second's truth and a log is
20
20
  // the last thing anybody wrote down, so main/codex-usage.mjs reads this only when
@@ -111,7 +111,7 @@ function tail(file, bytes = TAIL_BYTES) {
111
111
  *
112
112
  * Lines are walked backwards because the last report is the one that is true. A
113
113
  * report carrying no usable window is skipped rather than accepted: Codex emits
114
- * an all-null snapshot for limit families that do not apply to the account (her
114
+ * an all-null snapshot for limit families that do not apply to the account (a
115
115
  * `premium` family, every field null), and taking one of those would blank a
116
116
  * panel that had a real figure a moment earlier.
117
117
  */
@@ -11,7 +11,7 @@
11
11
  // Codex's figures ride the `codex app-server` THE FLEET IS ALREADY RUNNING:
12
12
  // they arrive unprompted as `account/rateLimits/updated` during a turn.
13
13
  //
14
- // AND NEVER ASK FOR ONE EITHER (the founder, 2026-09-05). This reader briefly
14
+ // AND NEVER ASK FOR ONE EITHER. This reader briefly
15
15
  // also sent an explicit rate-limits read down that pipe whenever its figure
16
16
  // went stale. It is deliberately gone, and this paragraph is here so the next
17
17
  // reader does not put it back as an obvious improvement.
@@ -96,9 +96,9 @@ export class CodexUsage {
96
96
  const limits = codexLimits(this._snapshot);
97
97
  if (limits.length) return { limits, at: this._at };
98
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 she
100
- // runs Codex in its own app, so on her Mac the panel stayed empty while her
101
- // weekly limit was full. main/codex-usage-file.mjs is a read of a file
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
101
+ // while their weekly limit was full. main/codex-usage-file.mjs is a read of a file
102
102
  // under this same CODEX_HOME. It starts no process and sends nothing
103
103
  // anywhere, so neither rule above is bent.
104
104
  return this._logged();
@@ -1,25 +1,25 @@
1
- // THE WATCH THAT NOTICES A CODEX CONVERSATION AND ASKS.
1
+ // THE WATCH THAT KEEPS AN IMPORTED CODEX CONVERSATION CURRENT.
2
2
  //
3
- // Her pick on 19:53: "ask first". So every minute this reads the conversations
4
- // she had in Codex this week, and for each one that runs in a folder one of her
5
- // projects points at and has no row anywhere yet, it files the asking row in
6
- // that project. It also keeps the rows she said yes to current: when Codex
7
- // writes another answer, the row's result follows it.
3
+ // Every minute this reads the user's recent Codex conversations and, for each
4
+ // one they already brought in, makes the row follow Codex: when Codex writes
5
+ // another answer, the row's result follows it. An asking row filed before this
6
+ // change is kept true the same way.
8
7
  //
9
- // A conversation in a folder no project points at gets no row here. It is
10
- // still on the ⌘K import card, under a section that makes the project on the
11
- // press, which is the card's own way of handling a new folder.
8
+ // IT NO LONGER ASKS ABOUT NEW ONES (w-db6f5e331e, 2026-10-05). It used to file
9
+ // an "Import into ...?" row for every new conversation in a project's folder.
10
+ // The founder, before launch: "after onboarding for both anthropic and openai
11
+ // harnesses, it should only import on command, not continuously". Importing is
12
+ // now the walk's last card and ⌘K import, for Claude Code and Codex alike.
12
13
  import { readCodexThreads } from './codex-threads.mjs';
13
14
 
14
15
  export const CODEX_POLL_MS = 60_000;
15
16
 
16
17
  export function scanCodex({ store, readThreads = readCodexThreads, now = Date.now() } = {}) {
17
18
  let threads = [];
18
- try { ({ threads } = readThreads({ now })); } catch { return { asked: 0, refreshed: 0, retold: 0, threads: 0 }; }
19
- const asked = store.askAboutCodexThreads(threads, { now });
19
+ try { ({ threads } = readThreads({ now })); } catch { return { refreshed: 0, retold: 0, threads: 0 }; }
20
20
  const refreshed = store.refreshCodexMirrors(threads, { now });
21
21
  const retold = store.refreshCodexAsks(threads, { now });
22
- return { asked, refreshed, retold, threads: threads.length };
22
+ return { refreshed, retold, threads: threads.length };
23
23
  }
24
24
 
25
25
  export function startCodexWatch({ store, readThreads = readCodexThreads, pollMs = CODEX_POLL_MS, firstDelayMs = 8_000 } = {}) {