agentbox-app 0.1.6
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/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- 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/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.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-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- 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/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +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/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- 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 +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
package/main/codex.mjs
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
// A CODEX TURN, IN THE WORDS AGENTBOX ALREADY DRAWS.
|
|
2
|
+
//
|
|
3
|
+
// main/codex-app-server.mjs is the transport: one child process speaking
|
|
4
|
+
// newline-delimited JSON-RPC 2.0, approvals answered or refused, threads
|
|
5
|
+
// routed. It hands its caller notifications and nothing else. THIS file is the
|
|
6
|
+
// layer above it, and its whole job is that a Codex run reaches her page
|
|
7
|
+
// looking like every other run.
|
|
8
|
+
//
|
|
9
|
+
// The supervisor runs four independent readers over every line a Claude Code
|
|
10
|
+
// worker prints (main/supervisor.mjs:4085-4290), and a fifth reader over the
|
|
11
|
+
// finished session (main/code-change.mjs):
|
|
12
|
+
//
|
|
13
|
+
// captureStream -> session.sessionId, session.result, session.resultIsError
|
|
14
|
+
// streamingText -> session.saying, the sentence being typed right now
|
|
15
|
+
// traceStreamLine -> the persisted trace, "HH:MM:SS [Bash] <cmd>"
|
|
16
|
+
// summarizeStreamLine -> the short In Progress tail
|
|
17
|
+
// changeFromTranscript-> the artifact she opens, files and hunks and rows
|
|
18
|
+
//
|
|
19
|
+
// Each has a twin below producing THE SAME OUTPUT SHAPE, so a later slice can
|
|
20
|
+
// swap readers per engine and nothing downstream — the thread, the byline, the
|
|
21
|
+
// file chips, the diff pane, the header arithmetic — needs to know which engine
|
|
22
|
+
// ran. That is not a tidiness preference.
|
|
23
|
+
//
|
|
24
|
+
// ONE STRUCTURAL DIFFERENCE, AND IT IS IN EVERY SIGNATURE HERE. Claude Code's
|
|
25
|
+
// readers take a raw LINE and parse it themselves, five times over, because
|
|
26
|
+
// stdout is where the events arrive. app-server's events arrive already parsed:
|
|
27
|
+
// the transport does the framing, the JSON and the routing, and calls
|
|
28
|
+
// `onNotification(method, params)`. So every reader below takes `(method,
|
|
29
|
+
// params)` instead of a line, and none of them contains a `JSON.parse`. The
|
|
30
|
+
// try/catch is still there, because a reader that throws inside the
|
|
31
|
+
// supervisor's stdout loop takes the reader out for the rest of the session,
|
|
32
|
+
// which is a worker that keeps running and reports nothing — the exact silent
|
|
33
|
+
// shape CLAUDE.md files under "A WORKER THAT DID NOT FINISH DELIVERED NOTHING".
|
|
34
|
+
//
|
|
35
|
+
// `git show 258d71d^:main/codex.mjs` is the 08-25 original, worth reading for
|
|
36
|
+
// intent. ITS EVENT PARSING IS OBSOLETE TWICE OVER: it targeted `codex exec
|
|
37
|
+
// --json`, whose item types are snake_case (`agent_message`, `file_change`) and
|
|
38
|
+
// which carries neither `phase` nor a real diff. One thing is carried over
|
|
39
|
+
// deliberately and credited here rather than rediscovered — see the trace.
|
|
40
|
+
//
|
|
41
|
+
// ------------------------------------------------------------------------
|
|
42
|
+
// MEASURED ON THIS MAC 2026-09-04 AGAINST codex-cli 0.148.0, over a 223-frame
|
|
43
|
+
// capture of one real turn, plus the CLI's own
|
|
44
|
+
// `codex app-server generate-json-schema` dump for the cases that turn did not
|
|
45
|
+
// produce. Six things differ from the written brief, and each is a whole
|
|
46
|
+
// feature that fails silently rather than loudly:
|
|
47
|
+
//
|
|
48
|
+
// 1. A `fileChange` OF KIND `add` IS NOT A DIFF. The `update` entry read
|
|
49
|
+
// "@@ -1 +1 @@\n-...\n+...\n"; the `add` entry in the same event was the
|
|
50
|
+
// string "hello\n" — raw file content, no `@@` header. A reader that
|
|
51
|
+
// assumes a unified diff per file finds zero hunks in every file a run
|
|
52
|
+
// creates. `hunksFor` therefore branches on the EVIDENCE IN THE STRING, not
|
|
53
|
+
// on the kind word, which also means a `delete` (which that run did not
|
|
54
|
+
// produce) is read correctly whichever of the two shapes it turns out to be.
|
|
55
|
+
//
|
|
56
|
+
// 2. THERE IS NO `turn/failed` NOTIFICATION. `ServerNotification` lists
|
|
57
|
+
// `turn/started`, `turn/completed` and `turn/moderationMetadata`, and the
|
|
58
|
+
// string "turn/failed" appears in none of the schema files. Failure arrives
|
|
59
|
+
// as `turn/completed` with `turn.status: "failed"` and a populated
|
|
60
|
+
// `turn.error` — "Only populated when the Turn's status is failed". A reader
|
|
61
|
+
// waiting for `turn/failed` marks every failed run a success. It is still
|
|
62
|
+
// ANSWERED below, because a notification we drop is a run that reads as
|
|
63
|
+
// unfinished forever, and being wrong in that direction costs nothing.
|
|
64
|
+
//
|
|
65
|
+
// 3. `TurnStatus` HAS FOUR VALUES: completed | interrupted | failed |
|
|
66
|
+
// inProgress. `interrupted` is what `turn/interrupt` leaves behind and it is a
|
|
67
|
+
// run with no answer exactly as much as `failed` is.
|
|
68
|
+
//
|
|
69
|
+
// 4. A COMMAND IN FLIGHT IS `inProgress`, camelCase, not `in_progress`.
|
|
70
|
+
// `CommandExecutionStatus` = inProgress | completed | failed | declined, and
|
|
71
|
+
// `declined` is the status an approval this app refuses produces.
|
|
72
|
+
//
|
|
73
|
+
// 5. `reasoning` ITEMS WERE EMPTY — all five of them, `summary: []` and
|
|
74
|
+
// `content: []`. See the trace for what that settles.
|
|
75
|
+
//
|
|
76
|
+
// 6. `move_path` EXISTS ONLY ON THE `update` KIND. `PatchChangeKind` is a
|
|
77
|
+
// three-way union and only `UpdatePatchChangeKind` carries the field. A
|
|
78
|
+
// rename is an update that moved; `add` and `delete` have nothing but
|
|
79
|
+
// `type`.
|
|
80
|
+
//
|
|
81
|
+
// WHAT THIS FILE DELIBERATELY DOES NOT READ, so the next slice does not have to
|
|
82
|
+
// re-derive that it is missing rather than broken. 0.148.0's `ThreadItem` union
|
|
83
|
+
// has eighteen members; five are handled here and the rest are ignored. The one
|
|
84
|
+
// that will matter first is `mcpToolCall` (`{server, tool, arguments, status,
|
|
85
|
+
// result, error, durationMs}`), because the store is an MCP server and a
|
|
86
|
+
// worker's claim/update/finish calls are therefore invisible in a Codex trace
|
|
87
|
+
// today where they are `[mcp__<store>__update_item]` lines in a Claude one.
|
|
88
|
+
// It is left out because no measured run has produced one and this repo's rule
|
|
89
|
+
// is that a mapping is verified by running it; `plan`, `webSearch`,
|
|
90
|
+
// `subAgentActivity` and `contextCompaction` are the same. None of them throws.
|
|
91
|
+
|
|
92
|
+
import fs from 'node:fs';
|
|
93
|
+
import path from 'node:path';
|
|
94
|
+
import { shortPath } from './code-change.mjs';
|
|
95
|
+
import { filesFromPatch } from './git-change.mjs';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A block of prose long enough that keeping it in memory is a bug rather than
|
|
99
|
+
* a feature. Her longest single line of agent prose over 1,664 traces is 423
|
|
100
|
+
* characters, so this is fifty times the worst real one; past it the block is
|
|
101
|
+
* dropped and the trace is left to deliver it whole, which costs her the live
|
|
102
|
+
* view of one paragraph and cannot cost the process anything.
|
|
103
|
+
*
|
|
104
|
+
* AND IT IS NOW THE ONLY COPY. It was written twice for one slice, because the
|
|
105
|
+
* supervisor kept a module-private `const` that the reader slice was not
|
|
106
|
+
* allowed to edit. Two copies of a rule become two different rules within a
|
|
107
|
+
* week, and this pair drifting apart would be a live sentence one engine drops
|
|
108
|
+
* at 20,000 characters and the other keeps, sitting under its own traced copy
|
|
109
|
+
* on her screen forever. main/supervisor.mjs imports this one.
|
|
110
|
+
*/
|
|
111
|
+
export const SAYING_CAP = 20_000;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The two guards that decide whether a path belongs in her artifact at all.
|
|
115
|
+
*
|
|
116
|
+
* COPIED, CHARACTER FOR CHARACTER, from `collector` in main/code-change.mjs,
|
|
117
|
+
* which is module-private there and which this slice may not modify. CLAUDE.md
|
|
118
|
+
* is right that two copies of a rule become two different rules within a week,
|
|
119
|
+
* so the copies are EXPORTED and
|
|
120
|
+
* tests/a-codex-turn-reads-like-every-other-run-on-her-page.test.mjs asserts
|
|
121
|
+
* they still match the source text of the original. Edit either and the other
|
|
122
|
+
* goes red naming the file it has to move with.
|
|
123
|
+
*
|
|
124
|
+
* They matter more here than they do there. Codex reports an absolute path for
|
|
125
|
+
* every file it touches, including its own /tmp scratch, so these are the
|
|
126
|
+
* common case rather than the corner one.
|
|
127
|
+
*/
|
|
128
|
+
export const NOT_CODE = /(^|\/)(node_modules|\.git)\//;
|
|
129
|
+
export const SCRATCH = /^\/(private\/)?(tmp|var\/folders)\//;
|
|
130
|
+
|
|
131
|
+
/* =============================== the answer =============================== */
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The final word of a turn, and the id a reply resumes.
|
|
135
|
+
*
|
|
136
|
+
* THE HARD DIFFERENCE FROM CLAUDE CODE, AND THE WHOLE REASON THIS FUNCTION IS
|
|
137
|
+
* NOT A RENAME OF `captureStream`: app-server has no `result` event. Claude
|
|
138
|
+
* Code's stream ends with one `{type:'result'}` line carrying the answer and an
|
|
139
|
+
* `is_error` flag, and `captureStream` keys on it. There is nothing shaped like
|
|
140
|
+
* that here. What replaces it is `phase` on an `agentMessage`: measured over
|
|
141
|
+
* one real turn, five agent messages, four of them `commentary` and EXACTLY ONE
|
|
142
|
+
* `final_answer`.
|
|
143
|
+
*
|
|
144
|
+
* So the answer is the final_answer message and nothing else. The 08-25 build
|
|
145
|
+
* took the LAST message it saw, because `codex exec` gave it no phase to read;
|
|
146
|
+
* doing that here puts a progress note on her row as the worker's finished
|
|
147
|
+
* word, and `awaitingHer` (supervisor.mjs) then reports a row as waiting on her
|
|
148
|
+
* when the work never finished. That is her 2026-08-06 incident — a `done` she
|
|
149
|
+
* never saw — arriving through a new door.
|
|
150
|
+
*
|
|
151
|
+
* LAST-WINS SURVIVES AS THE COMPATIBILITY PATH AND ONLY THAT. The schema says
|
|
152
|
+
* plainly that "providers do not emit this consistently, so callers must treat
|
|
153
|
+
* None as 'phase unknown' and keep compatibility behavior for legacy models".
|
|
154
|
+
* An unphased message therefore sets the answer — but never over a real
|
|
155
|
+
* final_answer, and never after one has arrived.
|
|
156
|
+
*
|
|
157
|
+
* A TURN THAT ENDS WITH NO FINAL ANSWER GETS NO RESULT, ON PURPOSE. That is
|
|
158
|
+
* what `saidNothingSheCanUse` is for: `result == null` is the first of the
|
|
159
|
+
* three shapes CLAUDE.md names, "said nothing at all -> the supervisor speaks
|
|
160
|
+
* for it", and there is already a writer for it in OUR words. Inventing a
|
|
161
|
+
* result here would take that row away from the writer that exists and hand her
|
|
162
|
+
* a guess in the agent's voice. The one thing worth doing is looking once more
|
|
163
|
+
* where the answer might still be: `turn/completed` carries `turn.items`, and
|
|
164
|
+
* in the measured run that array held exactly the final agentMessage even
|
|
165
|
+
* though `itemsView` was "summary". So a run whose item events were missed
|
|
166
|
+
* still has its answer in the last frame, for free.
|
|
167
|
+
*/
|
|
168
|
+
export function captureCodexEvent(session, method, params) {
|
|
169
|
+
try {
|
|
170
|
+
if (!session || typeof method !== 'string') return;
|
|
171
|
+
const p = params ?? {};
|
|
172
|
+
|
|
173
|
+
// The id `thread/resume` takes, and it is at `params.thread.id`. There is
|
|
174
|
+
// no `params.threadId` on this notification; reading the one that does not
|
|
175
|
+
// exist leaves every session unresumable with no symptom until she types a
|
|
176
|
+
// reply on a finished row.
|
|
177
|
+
if (method === 'thread/started') {
|
|
178
|
+
const id = p.thread?.id;
|
|
179
|
+
if (id) session.sessionId = String(id);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (method === 'item/completed') {
|
|
184
|
+
const item = p.item ?? {};
|
|
185
|
+
if (item.type === 'exitedReviewMode' && typeof item.review === 'string' && item.review.trim()) {
|
|
186
|
+
session.result = item.review;
|
|
187
|
+
session.resultIsError = false;
|
|
188
|
+
session.codexFinalAnswer = true;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (item?.type !== 'agentMessage') return;
|
|
192
|
+
const phase = typeof item.phase === 'string' ? item.phase : null;
|
|
193
|
+
if (phase === 'commentary') return;
|
|
194
|
+
if (session.codexFinalAnswer && phase !== 'final_answer') return;
|
|
195
|
+
session.result = String(item.text ?? '');
|
|
196
|
+
session.resultIsError = false;
|
|
197
|
+
if (phase === 'final_answer') session.codexFinalAnswer = true;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (method === 'turn/completed' || method === 'turn/failed') {
|
|
202
|
+
const turn = p.turn;
|
|
203
|
+
if (!turn || typeof turn !== 'object') return;
|
|
204
|
+
const status = turnStatus(method, turn);
|
|
205
|
+
if (status === 'completed') {
|
|
206
|
+
if (session.result != null) return;
|
|
207
|
+
const answer = finalAnswerIn(turn.items);
|
|
208
|
+
if (answer == null) return;
|
|
209
|
+
session.result = answer;
|
|
210
|
+
session.resultIsError = false;
|
|
211
|
+
session.codexFinalAnswer = true;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// A turn that failed after saying something still failed, and her row
|
|
215
|
+
// must not read as answered because a message got through first.
|
|
216
|
+
session.result = turnTrouble(turn, status);
|
|
217
|
+
session.resultIsError = true;
|
|
218
|
+
}
|
|
219
|
+
} catch { /* a reader that throws is a session that goes quiet */ }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** completed | interrupted | failed | inProgress, defaulting to the method. */
|
|
223
|
+
function turnStatus(method, turn) {
|
|
224
|
+
if (typeof turn?.status === 'string') return turn.status;
|
|
225
|
+
return method === 'turn/failed' ? 'failed' : 'completed';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Why a turn did not finish, in whatever words the server gave us. */
|
|
229
|
+
function turnTrouble(turn, status) {
|
|
230
|
+
const why = String(turn?.error?.message ?? '').trim();
|
|
231
|
+
const more = String(turn?.error?.additionalDetails ?? '').trim();
|
|
232
|
+
const head = why || (status === 'interrupted' ? 'the turn was interrupted' : `the turn ${status}`);
|
|
233
|
+
return more ? `${head}\n${more}` : head;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The final answer inside a turn's own item list, or null.
|
|
238
|
+
*
|
|
239
|
+
* ONLY a `final_answer` counts, never the last message in the list. The list
|
|
240
|
+
* arrives as a summary view and a summary of a turn that narrated and then
|
|
241
|
+
* died is narration.
|
|
242
|
+
*/
|
|
243
|
+
function finalAnswerIn(items) {
|
|
244
|
+
if (!Array.isArray(items)) return null;
|
|
245
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
246
|
+
const item = items[i];
|
|
247
|
+
if (item?.type === 'exitedReviewMode' && typeof item.review === 'string' && item.review.trim()) return item.review;
|
|
248
|
+
if (item?.type === 'agentMessage' && item?.phase === 'final_answer') return String(item.text ?? '');
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ============================ the live sentence =========================== */
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* THE SENTENCE THE AGENT IS TYPING RIGHT NOW, over app-server's own deltas.
|
|
257
|
+
*
|
|
258
|
+
* Claude Code's half of that is `streamingText` reading
|
|
259
|
+
* `--include-partial-messages` frames; `item/agentMessage/delta` is the same
|
|
260
|
+
* channel here, 157 frames in the measured turn, with the text at
|
|
261
|
+
* `params.delta`.
|
|
262
|
+
*
|
|
263
|
+
* The three rules are the supervisor's, unchanged, and each of them is load
|
|
264
|
+
* bearing (see supervisor.mjs:4131 for the long form):
|
|
265
|
+
*
|
|
266
|
+
* - ONE BLOCK AT A TIME. An `item/started` for an agentMessage starts the next
|
|
267
|
+
* one from empty, so the live string and the string the trace will carry are
|
|
268
|
+
* the same string, and the thread can drop the live copy the moment the
|
|
269
|
+
* traced one arrives rather than guessing.
|
|
270
|
+
* - NOT CLEARED WHEN THE MESSAGE FINISHES. The trace is written on one channel
|
|
271
|
+
* and read on another; clearing here blinks the sentence off her screen for
|
|
272
|
+
* however long the read takes and then puts it back.
|
|
273
|
+
* - DROPPED AT THE CAP, NEVER CUT. A cut string never matches the traced one
|
|
274
|
+
* and would sit under it on her screen forever.
|
|
275
|
+
*
|
|
276
|
+
* Returns true when the string moved, which is what earns a push.
|
|
277
|
+
*/
|
|
278
|
+
export function codexStreamingText(session, method, params) {
|
|
279
|
+
try {
|
|
280
|
+
if (!session) return false;
|
|
281
|
+
const p = params ?? {};
|
|
282
|
+
|
|
283
|
+
if (method === 'item/started') {
|
|
284
|
+
// Only prose resets it. A command starting or a patch being applied
|
|
285
|
+
// means this turn has moved on from talking, and the block already typed
|
|
286
|
+
// stays on screen until the trace delivers the same words.
|
|
287
|
+
if (p.item?.type !== 'agentMessage') return false;
|
|
288
|
+
session.saying = '';
|
|
289
|
+
session.sayingAt = Date.now();
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (method !== 'item/agentMessage/delta') return false;
|
|
294
|
+
const piece = String(p.delta ?? '');
|
|
295
|
+
if (!piece) return false;
|
|
296
|
+
if (!session.sayingAt) session.sayingAt = Date.now();
|
|
297
|
+
const next = (session.saying ?? '') + piece;
|
|
298
|
+
session.saying = next.length > SAYING_CAP ? '' : next;
|
|
299
|
+
return true;
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ================================ the trace =============================== */
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The persisted trace line, in the format renderer/src/run-files.ts parses:
|
|
309
|
+
* `/^\d\d:\d\d:\d\d {2}(.*)$/`, with a tool in brackets and its key argument
|
|
310
|
+
* after it. Fuller than the tail, because "tool: Bash" answers nothing when the
|
|
311
|
+
* founder asks what a session actually did.
|
|
312
|
+
*
|
|
313
|
+
* ONLY `item/completed` IS TRACED, AND THE 08-25 BUILD ALREADY KNEW WHY: the
|
|
314
|
+
* same item arrives on `item/started` with an empty output and again on
|
|
315
|
+
* `item/completed` with the real one. Tracing both puts every command on her
|
|
316
|
+
* page twice — three commands became six lines in the measured turn.
|
|
317
|
+
*
|
|
318
|
+
* A READ DONE WITH `sed` IS TRACED AS THE SHELL COMMAND IT WAS. Codex reads and
|
|
319
|
+
* searches by running shell commands rather than by calling tools named Read
|
|
320
|
+
* and Grep, so its work lines really are `[Bash] sed -n '1,200p' math.js` where
|
|
321
|
+
* Claude Code's would be `[Read] math.js`. Rewriting one into the other is
|
|
322
|
+
* Agentbox claiming a tool call Codex never made — and the command is also the
|
|
323
|
+
* thing an approval card is raised about, so the honest spelling is the only
|
|
324
|
+
* one that stays true next to the card. Her peers instruction cuts this way as
|
|
325
|
+
* well as the other: equivalent does not mean identical.
|
|
326
|
+
*
|
|
327
|
+
* AND THE BRACKETS ON A FILE CHANGE ARE `Write` AND `Edit` ANYWAY, which is the
|
|
328
|
+
* same call read the other way round. The bracket is the trace's own shared
|
|
329
|
+
* vocabulary — `[Bash]` is already Claude Code's word for a shell command and
|
|
330
|
+
* both engines use it — and renderer/src/run-files.ts matches
|
|
331
|
+
* `/^(Write|Edit|NotebookEdit)$/` on it to find the files a run made for her.
|
|
332
|
+
* A Codex worker that wrote her an .html through its patch tool would lose the
|
|
333
|
+
* chip on its card under any other word. THAT is what marking Codex as lesser
|
|
334
|
+
* would actually look like in the product. `Delete` and `Move` are new words
|
|
335
|
+
* because Claude Code has no tool that does either, so there is nothing to
|
|
336
|
+
* borrow and nothing to misrepresent.
|
|
337
|
+
*
|
|
338
|
+
* NOTHING IS WRITTEN FOR A `reasoning` ITEM. Claude Code's trace carries no
|
|
339
|
+
* thinking, one surface should not grow two vocabularies, and the measurement
|
|
340
|
+
* settles it besides: all five reasoning items in the captured turn had
|
|
341
|
+
* `summary: []` and `content: []`, so a reasoning line would have been five
|
|
342
|
+
* bare timestamps in a twenty-eight second run.
|
|
343
|
+
*/
|
|
344
|
+
export function traceCodexEvent(method, params) {
|
|
345
|
+
try {
|
|
346
|
+
const p = params ?? {};
|
|
347
|
+
const at = new Date().toISOString().slice(11, 19);
|
|
348
|
+
|
|
349
|
+
if (method === 'item/completed') {
|
|
350
|
+
const item = p.item ?? {};
|
|
351
|
+
|
|
352
|
+
if (item.type === 'exitedReviewMode' && typeof item.review === 'string') {
|
|
353
|
+
return item.review.trim() ? `${at} ${item.review.trim()}` : null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (item?.type === 'agentMessage') {
|
|
357
|
+
const text = String(item.text ?? '').trim();
|
|
358
|
+
return text ? `${at} ${text}` : null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (item?.type === 'commandExecution') {
|
|
362
|
+
const command = String(item.command ?? '').trim();
|
|
363
|
+
return command ? `${at} [Bash] ${command.slice(0, 200)}` : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (item?.type === 'fileChange') {
|
|
367
|
+
const lines = changesOf(item)
|
|
368
|
+
.map(fileVerb)
|
|
369
|
+
.filter(Boolean)
|
|
370
|
+
.map((body) => `${at} ${body}`);
|
|
371
|
+
return lines.length ? lines.join('\n') : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (method === 'turn/completed' || method === 'turn/failed') {
|
|
378
|
+
const turn = p.turn;
|
|
379
|
+
if (!turn || typeof turn !== 'object') return null;
|
|
380
|
+
const status = turnStatus(method, turn);
|
|
381
|
+
const bad = status !== 'completed';
|
|
382
|
+
// Claude Code's footer counts turns; Codex has exactly one turn per run,
|
|
383
|
+
// so that number would read "1 turns" forever and tell her nothing. The
|
|
384
|
+
// duration goes in its place, which is the fact the turn actually
|
|
385
|
+
// carries (`durationMs`, 27746 in the measured run).
|
|
386
|
+
const span = Number.isFinite(turn.durationMs) ? `${(turn.durationMs / 1000).toFixed(1)}s` : '?s';
|
|
387
|
+
const said = bad ? turnTrouble(turn, status) : (finalAnswerIn(turn.items) ?? '');
|
|
388
|
+
return `\n${at} == RESULT (${status}${bad ? ' ERROR' : ''} · ${span}) ==\n${said.slice(0, 4000)}`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return null;
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** The changes on a fileChange item, always an array. */
|
|
398
|
+
function changesOf(item) {
|
|
399
|
+
return Array.isArray(item?.changes) ? item.changes : [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* WHAT ONE FILE CHANGE DID, IN ONE WORD, AND THE ONLY COPY OF THAT RULE. Null
|
|
404
|
+
* when the change names no file, which is the one shape that cannot be said out
|
|
405
|
+
* loud at all.
|
|
406
|
+
*
|
|
407
|
+
* Exported because main/codex-approvals.mjs needs the same four words on the
|
|
408
|
+
* approval card she reads BEFORE a patch is applied. A second copy of it would
|
|
409
|
+
* be one edit away from a trace line saying `[Write]` under a card that said
|
|
410
|
+
* `Edit` about the same change. `move_path` lives only on the `update` kind
|
|
411
|
+
* (measurement 6 at the top of this file), so a rename is an update that moved.
|
|
412
|
+
*/
|
|
413
|
+
export function fileVerbWord(change) {
|
|
414
|
+
const from = String(change?.path ?? '').trim();
|
|
415
|
+
if (!from) return null;
|
|
416
|
+
const kind = change?.kind ?? {};
|
|
417
|
+
const to = typeof kind.move_path === 'string' ? kind.move_path.trim() : '';
|
|
418
|
+
if (kind.type === 'add') return 'Write';
|
|
419
|
+
if (kind.type === 'delete') return 'Delete';
|
|
420
|
+
if (to && to !== from) return 'Move';
|
|
421
|
+
if (kind.type === 'update') return 'Edit';
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** One file change as its bracket and its path. Null when there is no path. */
|
|
426
|
+
function fileVerb(change) {
|
|
427
|
+
const word = fileVerbWord(change);
|
|
428
|
+
if (!word) return null;
|
|
429
|
+
const from = String(change.path).trim().slice(0, 200);
|
|
430
|
+
// Both ends of a move, because the path she would go looking for is the one
|
|
431
|
+
// it is at now and the one above it is the one it left.
|
|
432
|
+
if (word === 'Move') return `[Move] ${from} -> ${String(change.kind.move_path).trim().slice(0, 200)}`;
|
|
433
|
+
return `[${word}] ${from}`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ================================= the tail =============================== */
|
|
437
|
+
|
|
438
|
+
/** The In Progress tail wants the gist, in the same words Claude Code's does. */
|
|
439
|
+
export function summarizeCodexEvent(method, params) {
|
|
440
|
+
try {
|
|
441
|
+
const p = params ?? {};
|
|
442
|
+
|
|
443
|
+
if (method === 'item/completed') {
|
|
444
|
+
const item = p.item ?? {};
|
|
445
|
+
if (item?.type === 'agentMessage') return String(item.text ?? '').slice(0, 300) || null;
|
|
446
|
+
if (item?.type === 'commandExecution') return String(item.command ?? '').trim() ? 'tool: Bash' : null;
|
|
447
|
+
if (item?.type === 'fileChange') {
|
|
448
|
+
const words = [];
|
|
449
|
+
for (const change of changesOf(item)) {
|
|
450
|
+
const verb = fileVerb(change);
|
|
451
|
+
if (!verb) continue;
|
|
452
|
+
const word = verb.slice(1, verb.indexOf(']'));
|
|
453
|
+
if (!words.includes(word)) words.push(word);
|
|
454
|
+
}
|
|
455
|
+
return words.length ? `tool: ${words.join(', ')}` : null;
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (method === 'turn/completed' || method === 'turn/failed') {
|
|
461
|
+
const turn = p.turn;
|
|
462
|
+
if (!turn || typeof turn !== 'object') return null;
|
|
463
|
+
const status = turnStatus(method, turn);
|
|
464
|
+
const said = status === 'completed' ? (finalAnswerIn(turn.items) ?? '') : turnTrouble(turn, status);
|
|
465
|
+
return `done: ${said.slice(0, 200)}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return null;
|
|
469
|
+
} catch {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* =============================== the change =============================== */
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* HOW MANY FRAMES OF ONE RUN ARE WORTH HOLDING FOR ITS ARTIFACT.
|
|
478
|
+
*
|
|
479
|
+
* The other engine's conversation is on disk and is read once at the exit, so
|
|
480
|
+
* it costs nothing while the run goes on. There is no such file here: an
|
|
481
|
+
* app-server notification is delivered once and gone, so the only place the
|
|
482
|
+
* conversation half of a Codex artifact can be assembled from is memory.
|
|
483
|
+
*
|
|
484
|
+
* The measured turn was 223 frames end to end and left five of the two kinds
|
|
485
|
+
* this keeps, so five thousand is more than twenty times a whole real run's
|
|
486
|
+
* frames and a thousand times its edits. Past it nothing further is kept and
|
|
487
|
+
* the artifact is short by whatever came after -- which is a partial card and
|
|
488
|
+
* not a broken one, because the DISK half is read independently and wins
|
|
489
|
+
* wherever the two disagree (`mergeChange`, main/code-change.mjs). Growing
|
|
490
|
+
* without a bound is the alternative, and a run that edits in a loop would take
|
|
491
|
+
* her app down with it.
|
|
492
|
+
*/
|
|
493
|
+
export const CHANGE_FRAME_CAP = 5_000;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* AND HOW MANY BYTES, BECAUSE A FRAME CAP IS NOT A MEMORY CAP.
|
|
497
|
+
*
|
|
498
|
+
* The count above bounds how MANY frames are kept and says nothing about how
|
|
499
|
+
* big each one is, and each one may carry up to the transport's own frame limit
|
|
500
|
+
* (MAX_FRAME_LENGTH, 16 MiB). Five thousand of those is eighty gigabytes, which
|
|
501
|
+
* is not a bound at all: it is her app dying on the exact run this cap was
|
|
502
|
+
* written to survive, the one that edits in a loop.
|
|
503
|
+
*
|
|
504
|
+
* Sixteen mebibytes, which is one whole frame's worth of everything. The
|
|
505
|
+
* measured turn's five kept frames were a few kilobytes between them, so this
|
|
506
|
+
* is thousands of times a real run and still a number the process can hold. Past
|
|
507
|
+
* it nothing further is kept and the artifact is short by whatever came after --
|
|
508
|
+
* a partial card and not a broken one, because the DISK half is read
|
|
509
|
+
* independently and wins wherever the two disagree (`mergeChange`,
|
|
510
|
+
* main/code-change.mjs).
|
|
511
|
+
*/
|
|
512
|
+
export const CHANGE_BYTE_CAP = 16 * 1024 * 1024;
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* KEEP THE FRAMES THE ARTIFACT WILL NEED, AND NOTHING ELSE.
|
|
516
|
+
*
|
|
517
|
+
* Two kinds out of the eighteen in `ThreadItem`: the `fileChange` items, which
|
|
518
|
+
* are the edits themselves, and the `agentMessage` items, which are where the
|
|
519
|
+
* sentence beside a hunk comes from. Their ORDER is the whole of how the second
|
|
520
|
+
* attaches to the first (`changeFromCodexTurn` takes the nearest preceding
|
|
521
|
+
* message), so they are kept in one list rather than two.
|
|
522
|
+
*
|
|
523
|
+
* Called on the session's own event path, so it is written to be unable to
|
|
524
|
+
* throw there: a reader that throws inside the supervisor's stream loop takes
|
|
525
|
+
* itself out for the rest of the session, which is a worker that keeps running
|
|
526
|
+
* and reports nothing.
|
|
527
|
+
*/
|
|
528
|
+
export function rememberCodexChange(session, method, params) {
|
|
529
|
+
try {
|
|
530
|
+
if (!session || method !== 'item/completed') return;
|
|
531
|
+
const type = params?.item?.type;
|
|
532
|
+
if (type !== 'fileChange' && type !== 'agentMessage') return;
|
|
533
|
+
const kept = session.codexChange ?? (session.codexChange = []);
|
|
534
|
+
if (kept.length >= CHANGE_FRAME_CAP) return;
|
|
535
|
+
// AND THE SIZE OF WHAT IS ALREADY HELD, WHICH THE COUNT DOES NOT BOUND.
|
|
536
|
+
// A fileChange frame carries the whole per-file diff, so five thousand of
|
|
537
|
+
// them is only a bound if each one is small, and the one run this cap was
|
|
538
|
+
// written for -- the one that edits in a loop -- is exactly the run where
|
|
539
|
+
// they are not. Measured off the frame itself rather than off a guess: a
|
|
540
|
+
// diff is a string, and its length is the number that matters.
|
|
541
|
+
const size = frameSize(params);
|
|
542
|
+
const held = session.codexChangeBytes ?? 0;
|
|
543
|
+
if (held + size > CHANGE_BYTE_CAP) return;
|
|
544
|
+
session.codexChangeBytes = held + size;
|
|
545
|
+
kept.push({ method, params });
|
|
546
|
+
} catch { /* an artifact is never worth a session */ }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Roughly how much of one frame this is going to hold on to.
|
|
551
|
+
*
|
|
552
|
+
* The two fields that actually carry weight are the per-file `diff` and the
|
|
553
|
+
* agent's `text`; everything else on a `ThreadItem` is ids and enums. Counted
|
|
554
|
+
* rather than JSON.stringify'd because this runs on the session's own event
|
|
555
|
+
* path, once per item, and serialising a ten-megabyte diff to measure it is the
|
|
556
|
+
* cost the cap exists to avoid.
|
|
557
|
+
*/
|
|
558
|
+
function frameSize(params) {
|
|
559
|
+
const item = params?.item ?? {};
|
|
560
|
+
let n = String(item.text ?? '').length;
|
|
561
|
+
for (const change of changesOf(item)) n += String(change?.diff ?? '').length;
|
|
562
|
+
return n;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* WHAT THE RUN CHANGED, in the shape main/code-change.mjs already produces.
|
|
567
|
+
*
|
|
568
|
+
* The pane that draws it (renderer/src/components/CodeArtifact.tsx), the
|
|
569
|
+
* arithmetic under it (renderer/src/code-artifact.ts) and the chip on the card
|
|
570
|
+
* (renderer/src/run-files.ts) are untouched by this slice, so matching the
|
|
571
|
+
* shape exactly IS the deliverable: `{files:[{path, abs, hunks:[{rows, plus,
|
|
572
|
+
* minus, said?, at?}], plus, minus, at}], roots, plus, minus, editCount,
|
|
573
|
+
* withProse, startedAt, endedAt}`.
|
|
574
|
+
*
|
|
575
|
+
* AND CODEX HANDS US THE HALF THAT WAS EXPENSIVE. The 08-25 build needed a
|
|
576
|
+
* 161-line V4A patch parser to reconstruct what a run had changed out of
|
|
577
|
+
* `codex exec`'s patch text. `item/completed` now carries a real per-file diff,
|
|
578
|
+
* so the rows come out of the same unified-diff reader git's own changes go
|
|
579
|
+
* through (`filesFromPatch`, main/git-change.mjs) rather than a second copy of
|
|
580
|
+
* one. The only thing synthesized is the two-line file header that reader
|
|
581
|
+
* expects; the hunk bodies are the server's own bytes.
|
|
582
|
+
*
|
|
583
|
+
* `events` are the notifications as the transport delivered them, `{method,
|
|
584
|
+
* params}`, in order. `roots` are the checkouts this run's work happened in and
|
|
585
|
+
* `since` is the moment to start from, both exactly as code-change.mjs means
|
|
586
|
+
* them. `rememberCodexChange` below is what collects them.
|
|
587
|
+
*/
|
|
588
|
+
export function changeFromCodexTurn(events, { roots = [], since = 0 } = {}) {
|
|
589
|
+
const into = collector({ roots });
|
|
590
|
+
// The agent's own sentence beside the hunk. Claude Code takes it from the
|
|
591
|
+
// text blocks of the same message the edit was in; Codex narrates in a
|
|
592
|
+
// separate agentMessage just before it applies the patch, so the nearest
|
|
593
|
+
// preceding message is the same sentence.
|
|
594
|
+
let said = '';
|
|
595
|
+
|
|
596
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
597
|
+
if (event?.method !== 'item/completed') continue;
|
|
598
|
+
const p = event.params ?? {};
|
|
599
|
+
const item = p.item ?? {};
|
|
600
|
+
|
|
601
|
+
if (item?.type === 'agentMessage') {
|
|
602
|
+
said = proseFrom(item.text);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (item?.type !== 'fileChange') continue;
|
|
606
|
+
|
|
607
|
+
const stamp = Number(p.completedAtMs ?? event.emittedAtMs ?? 0) || 0;
|
|
608
|
+
if (since && stamp && stamp < since) continue;
|
|
609
|
+
|
|
610
|
+
for (const change of changesOf(item)) {
|
|
611
|
+
const abs = destinationOf(change);
|
|
612
|
+
if (!abs) continue;
|
|
613
|
+
for (const hunk of hunksFor(change)) into.add(abs, hunk.rows, { said, stamp });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return into.done();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Where the file IS once the change has landed.
|
|
622
|
+
*
|
|
623
|
+
* A rename is filed under the path it moved to, which is the rule
|
|
624
|
+
* main/git-change.mjs already states for git's own renames. A change she
|
|
625
|
+
* opens has to lead to a file that exists.
|
|
626
|
+
*/
|
|
627
|
+
function destinationOf(change) {
|
|
628
|
+
const to = typeof change?.kind?.move_path === 'string' ? change.kind.move_path.trim() : '';
|
|
629
|
+
const from = String(change?.path ?? '').trim();
|
|
630
|
+
return to || from || null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* One file change as hunks of rows.
|
|
635
|
+
*
|
|
636
|
+
* THE BRANCH IS ON THE STRING, NOT ON THE KIND, and that is the measurement
|
|
637
|
+
* from the top of this file: an `update` arrived as "@@ -1 +1 @@\n-...\n+...",
|
|
638
|
+
* an `add` in the SAME event arrived as "hello\n". A delete did not occur in
|
|
639
|
+
* that run at all, so guessing which shape it uses would be exactly the kind of
|
|
640
|
+
* unverified mapping this repo refuses; asking the string instead answers for
|
|
641
|
+
* every kind, including ones this CLI has not shipped yet.
|
|
642
|
+
*/
|
|
643
|
+
function hunksFor(change) {
|
|
644
|
+
const diff = String(change?.diff ?? '');
|
|
645
|
+
if (!diff) return [];
|
|
646
|
+
if (diff.startsWith('@@')) {
|
|
647
|
+
// The two header lines `filesFromPatch` opens a file on. A placeholder
|
|
648
|
+
// name rather than the real path, because that reader strips `a/` and `b/`
|
|
649
|
+
// prefixes and trims, and a real path with a space in it would come back
|
|
650
|
+
// wrong; the path is already known here and is never read back out of this.
|
|
651
|
+
return filesFromPatch(`--- a/f\n+++ b/f\n${diff}`)[0]?.hunks ?? [];
|
|
652
|
+
}
|
|
653
|
+
const mark = change?.kind?.type === 'delete' ? '-' : '+';
|
|
654
|
+
const rows = splitLines(diff).map((line) => [mark, line]);
|
|
655
|
+
return rows.length ? [{ rows }] : [];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** A trailing newline is a line terminator, not an empty last line. */
|
|
659
|
+
function splitLines(text) {
|
|
660
|
+
const out = String(text ?? '').split('\n');
|
|
661
|
+
if (out.length > 1 && out[out.length - 1] === '') out.pop();
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** One line of what the agent said, the same shape code-change.mjs keeps. */
|
|
666
|
+
function proseFrom(text) {
|
|
667
|
+
const said = String(text ?? '').trim();
|
|
668
|
+
if (!said) return '';
|
|
669
|
+
const first = said.split('\n').map((l) => l.trim()).filter(Boolean).pop() ?? '';
|
|
670
|
+
return first.slice(0, 400);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Each root, and also what the filesystem really calls it.
|
|
675
|
+
*
|
|
676
|
+
* Measured in main/code-change.mjs on 2026-08-26 and it bites harder here: the
|
|
677
|
+
* app knew a checkout as `/tmp/proof/widget-repo` and an edit against
|
|
678
|
+
* `/private/tmp/proof/widget-repo/math.js` matched no root and was dropped, so
|
|
679
|
+
* a run that really did change her code finished with an artifact that did not
|
|
680
|
+
* mention it. Codex reports the RESOLVED spelling for every path it touches, so
|
|
681
|
+
* for this engine that is the ordinary case rather than the corner one.
|
|
682
|
+
*
|
|
683
|
+
* Resolved off the deepest ancestor that still exists rather than off the root
|
|
684
|
+
* itself, because this runs after the work is over and CLAUDE.md's process
|
|
685
|
+
* etiquette asks a session to remove its worktree as the last thing it does.
|
|
686
|
+
*/
|
|
687
|
+
function rootSpellings(roots) {
|
|
688
|
+
const out = new Set();
|
|
689
|
+
for (const root of roots.filter(Boolean)) {
|
|
690
|
+
const given = path.resolve(root);
|
|
691
|
+
out.add(given);
|
|
692
|
+
let head = given;
|
|
693
|
+
const tail = [];
|
|
694
|
+
for (let i = 0; i < 64; i += 1) {
|
|
695
|
+
try {
|
|
696
|
+
const real = fs.realpathSync(head);
|
|
697
|
+
out.add(tail.length ? path.join(real, ...tail) : real);
|
|
698
|
+
break;
|
|
699
|
+
} catch { /* not there; try its parent */ }
|
|
700
|
+
const parent = path.dirname(head);
|
|
701
|
+
if (parent === head) break;
|
|
702
|
+
tail.unshift(path.basename(head));
|
|
703
|
+
head = parent;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return [...out];
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* The tally, and the two rules that decide whether a file belongs in her
|
|
711
|
+
* artifact at all. The same collector code-change.mjs keeps private, over
|
|
712
|
+
* absolute paths that arrived in an event instead of in a transcript.
|
|
713
|
+
*/
|
|
714
|
+
function collector({ roots = [] } = {}) {
|
|
715
|
+
const files = new Map();
|
|
716
|
+
const totals = { plus: 0, minus: 0, editCount: 0, withProse: 0, startedAt: 0, endedAt: 0 };
|
|
717
|
+
const spellings = rootSpellings(roots);
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
add(abs, rows, { said = '', stamp = 0 } = {}) {
|
|
721
|
+
abs = String(abs ?? '').trim();
|
|
722
|
+
if (!abs || NOT_CODE.test(abs)) return;
|
|
723
|
+
const rel = shortPath(abs, spellings);
|
|
724
|
+
// Inside one of this run's own roots it is the product, wherever it
|
|
725
|
+
// sits. Outside all of them AND under scratch, it is nobody's.
|
|
726
|
+
if (rel === null && SCRATCH.test(abs)) return;
|
|
727
|
+
if (!rows.length) return;
|
|
728
|
+
const p = rows.filter((r) => r[0] === '+').length;
|
|
729
|
+
const m = rows.filter((r) => r[0] === '-').length;
|
|
730
|
+
if (!p && !m) return;
|
|
731
|
+
const key = rel ?? abs;
|
|
732
|
+
const file = files.get(key) ?? { path: key, abs, hunks: [], plus: 0, minus: 0, at: stamp };
|
|
733
|
+
file.hunks.push({ rows, said: said || undefined, at: stamp || undefined, plus: p, minus: m });
|
|
734
|
+
file.plus += p; file.minus += m;
|
|
735
|
+
files.set(key, file);
|
|
736
|
+
totals.plus += p; totals.minus += m;
|
|
737
|
+
totals.editCount += 1;
|
|
738
|
+
if (said) totals.withProse += 1;
|
|
739
|
+
if (stamp) {
|
|
740
|
+
if (!totals.startedAt || stamp < totals.startedAt) totals.startedAt = stamp;
|
|
741
|
+
if (stamp > totals.endedAt) totals.endedAt = stamp;
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
done() {
|
|
745
|
+
return {
|
|
746
|
+
files: [...files.values()],
|
|
747
|
+
roots: [...roots].filter(Boolean),
|
|
748
|
+
plus: totals.plus,
|
|
749
|
+
minus: totals.minus,
|
|
750
|
+
editCount: totals.editCount,
|
|
751
|
+
withProse: totals.withProse,
|
|
752
|
+
startedAt: totals.startedAt || undefined,
|
|
753
|
+
endedAt: totals.endedAt || undefined,
|
|
754
|
+
};
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|