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.
- package/README.md +220 -58
- package/bin/agentbox.mjs +18 -3
- package/briefs/message-rules.md +24 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +11 -11
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +116 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/approvals.mjs +14 -5
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +32 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +25 -13
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +18 -0
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +82 -17
- package/main/demo.mjs +17 -14
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +204 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +7 -4
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +9 -3
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +134 -60
- package/main/ipc.mjs +335 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +48 -1
- package/main/main.mjs +138 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/notify.mjs +1 -1
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +21 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +115 -51
- package/main/runs-on.mjs +93 -0
- package/main/serve.mjs +77 -5
- package/main/settings.mjs +245 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +84 -7
- package/main/store/project.mjs +15 -19
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +245 -13
- package/main/supervisor.mjs +1634 -417
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +94 -16
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +14 -2
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +10 -12
- package/mcp/core/work.mjs +123 -26
- package/mcp/tools.mjs +13 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
- package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +7 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +39 -16
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +38 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +6 -6
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-modes.mjs +1 -1
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +6 -6
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +34 -4
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +9 -3
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/runs-on.d.mts +20 -0
- package/shared/runs-on.mjs +79 -0
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +40 -19
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +258 -28
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- 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
|
+
}
|
package/main/codex-models.mjs
CHANGED
|
@@ -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
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
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
|
|
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(
|
|
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;
|
package/main/codex-threads.mjs
CHANGED
|
@@ -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
|
|
18
|
-
// `guardian_review` for Codex's own reviewer of a turn, and `subagent` for
|
|
19
|
-
// helper a thread spawned.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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 =
|
|
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
|
-
/**
|
|
66
|
-
|
|
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 >=
|
|
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 (
|
|
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
|
|
107
|
-
* Measured on
|
|
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
|
|
115
|
-
//
|
|
116
|
-
//
|
|
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.
|
|
146
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
231
|
-
* import row both read. One entry per thread
|
|
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.
|
|
8
|
-
// Codex in its own app all day
|
|
9
|
-
//
|
|
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
|
|
16
|
-
// such records across
|
|
17
|
-
// the
|
|
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 (
|
|
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
|
*/
|
package/main/codex-usage.mjs
CHANGED
|
@@ -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
|
|
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
|
|
100
|
-
// runs Codex in its own app
|
|
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();
|
package/main/codex-watch.mjs
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
// THE WATCH THAT
|
|
1
|
+
// THE WATCH THAT KEEPS AN IMPORTED CODEX CONVERSATION CURRENT.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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 {
|
|
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 {
|
|
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 } = {}) {
|