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
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
// ONE CODEX WORK ITEM, SHAPED LIKE A CHILD PROCESS.
|
|
2
|
+
//
|
|
3
|
+
// Agentbox spawns one child per work item and hangs everything off it: the
|
|
4
|
+
// supervisor's `session.child` is a ChildProcess and every other part of
|
|
5
|
+
// `spawnWorker` is written against that. A Codex worker has no per-item
|
|
6
|
+
// process. It is ONE shared `codex app-server` plus a thread inside it, proven
|
|
7
|
+
// on 2026-09-04 by running several concurrently: independent thread ids,
|
|
8
|
+
// interleaved progress, and no head-of-line blocking when one of them parked on
|
|
9
|
+
// an unanswered approval.
|
|
10
|
+
//
|
|
11
|
+
// So the question this file answers is whether the supervisor has to learn
|
|
12
|
+
// about threads, and the measured answer is no. `session.child` is referenced
|
|
13
|
+
// in exactly ONE place in the whole 4,273-line supervisor
|
|
14
|
+
// (`session.child.kill`), and the local `child` handle inside `spawnWorker` is
|
|
15
|
+
// used in exactly five ways:
|
|
16
|
+
//
|
|
17
|
+
// child.once('spawn') child.stderr.on('data') child.kill
|
|
18
|
+
// child.stdout.on('data') child.on('exit')
|
|
19
|
+
//
|
|
20
|
+
// That five-method contract is the entire seam, and this module is a facade
|
|
21
|
+
// over one thread that satisfies it. The supervisor keeps being a file about
|
|
22
|
+
// running sessions rather than a file about two vendors' flag names, which is
|
|
23
|
+
// the sentence the deleted 08-25 design used to justify the same choice.
|
|
24
|
+
//
|
|
25
|
+
// FOUR OF THE FIVE ARE REAL AND ONE IS DELIBERATELY MISSING.
|
|
26
|
+
//
|
|
27
|
+
// `stdout` IS NOT FAKED. It would be easy to re-serialise every notification
|
|
28
|
+
// into a JSON line so the existing readers worked untouched, and it would be
|
|
29
|
+
// objects -> JSONL -> objects for no gain: it would also hide the real
|
|
30
|
+
// camelCase/snake_case divergence between `codex app-server` and the
|
|
31
|
+
// `codex exec` stream the removed August build parsed, which is precisely the
|
|
32
|
+
// thing that must stay visible. So this facade emits `event(method, params)`
|
|
33
|
+
// and the supervisor swaps its three readers once per session instead of once
|
|
34
|
+
// per line. See `readersFor` in main/supervisor.mjs.
|
|
35
|
+
//
|
|
36
|
+
// `stderr` IS REAL, and it is why this file writes to it at all. The supervisor
|
|
37
|
+
// classifies a dead run by reading the `stderr:` lines off the session tail
|
|
38
|
+
// (`sayTheRunDied` -> `troubleCause`), and Codex's own refusal wording -- "Not
|
|
39
|
+
// inside a trusted directory and --skip-git-repo-check was not specified" -- is
|
|
40
|
+
// what makes the difference between her reading "the folder is not one Codex
|
|
41
|
+
// will work in" and reading the generic sentence. A failure that never reaches
|
|
42
|
+
// this pipe is a failure filed as 'unknown'.
|
|
43
|
+
//
|
|
44
|
+
// AND THE ONE THING THIS FILE OWES `settleDelivery`: AN ENDING THAT WAS NOT THE
|
|
45
|
+
// WORK ITEM'S FAULT MUST BE TELLABLE FROM ONE THAT WAS. CLAUDE.md's rule is that a session WE killed does not
|
|
46
|
+
// spend one of an item's three delivery attempts, "because every restart kills
|
|
47
|
+
// the fleet and charging that to the item is how restarts strand work". The
|
|
48
|
+
// supervisor's own `_kill` writes `stoppedByUs` on the session, and this facade
|
|
49
|
+
// ALSO sets `signal` on the exit -- both halves, deliberately, because
|
|
50
|
+
// `settleDelivery` reads `signal || session.stoppedByUs` and a facade that
|
|
51
|
+
// reported a clean `null` signal on an interrupt would be lying about the one
|
|
52
|
+
// fact that protects her work.
|
|
53
|
+
//
|
|
54
|
+
// THE EXIT THEREFORE CARRIES A THIRD ARGUMENT, which a real ChildProcess never
|
|
55
|
+
// sends and the supervisor reads as undefined for every Claude worker. It says
|
|
56
|
+
// one thing today: the shared app-server died under this thread. ONE process
|
|
57
|
+
// carries the whole Codex fleet, so a single unreadable frame reaches every live
|
|
58
|
+
// thread at once, and each of those threads is a work item -- ending them with a
|
|
59
|
+
// bare `end(1, null)` spent one of every item's three delivery attempts on a
|
|
60
|
+
// fault none of them caused. See `onClosed` below.
|
|
61
|
+
|
|
62
|
+
import { EventEmitter } from 'node:events';
|
|
63
|
+
import fs from 'node:fs';
|
|
64
|
+
import os from 'node:os';
|
|
65
|
+
import path from 'node:path';
|
|
66
|
+
import { Name } from '../shared/product-name.mjs';
|
|
67
|
+
import { codexPosture } from '../shared/codex-modes.mjs';
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* WHAT A WORKER THREAD IS ALLOWED TO DO. The sandbox is the wall; the approval
|
|
71
|
+
* policy only decides who gets interrupted.
|
|
72
|
+
*
|
|
73
|
+
* THIS WAS `untrusted` UNTIL 2026-09-23 AND IT MADE CODEX UNUSABLE. The founder
|
|
74
|
+
* reported that Codex asked about nearly everything it wanted to run, to the
|
|
75
|
+
* point that the only viable way to use it was to leave this app and drive the
|
|
76
|
+
* Codex app directly (w-07f98d5a1c).
|
|
77
|
+
*
|
|
78
|
+
* She was right, and the note that stood here was wrong in a way worth writing
|
|
79
|
+
* down because it is easy to make twice. It said the other policies "did not
|
|
80
|
+
* card a command" and concluded they gated nothing. NOT CARDING AND NOT
|
|
81
|
+
* STOPPING ARE DIFFERENT THINGS. Under `workspace-write` the sandbox refuses on
|
|
82
|
+
* its own, silently, and a card was never what held the wall up.
|
|
83
|
+
*
|
|
84
|
+
* MEASURED 2026-09-23 on codex-cli 0.153.4. Both scripts are kept:
|
|
85
|
+
*
|
|
86
|
+
* scripts/count-what-codex-asks.mjs -- one ordinary task: read a file, edit
|
|
87
|
+
* it, run node to check the result.
|
|
88
|
+
* untrusted 3 interruptions, task completed
|
|
89
|
+
* on-request 0 interruptions, task completed, same edit
|
|
90
|
+
*
|
|
91
|
+
* scripts/can-codex-escape-on-request.mjs -- the counter-test: write to $HOME
|
|
92
|
+
* and curl the open internet.
|
|
93
|
+
* untrusted 2 cards, both denied, nothing escaped
|
|
94
|
+
* on-request 0 cards, AND NOTHING ESCAPED EITHER. The sandbox answered
|
|
95
|
+
* "operation not permitted" for the write and "could not
|
|
96
|
+
* resolve host" for the curl.
|
|
97
|
+
*
|
|
98
|
+
* So `untrusted` bought three interruptions on ordinary work and no containment
|
|
99
|
+
* `workspace-write` was not already giving us.
|
|
100
|
+
*
|
|
101
|
+
* WHAT THE SCHEMA SAYS, read off `codex app-server generate-json-schema` on that
|
|
102
|
+
* same version rather than remembered: `AskForApproval` is
|
|
103
|
+
* `untrusted | on-request | never | {granular:{...}}`, and
|
|
104
|
+
* `SandboxWorkspaceWrite.network_access` DEFAULTS TO FALSE. Note that the
|
|
105
|
+
* interactive CLI has dropped `untrusted` from `--ask-for-approval` altogether
|
|
106
|
+
* and offers only `on-request` and `never`; the app-server protocol still
|
|
107
|
+
* accepts it, which is how this went on working while being the wrong choice.
|
|
108
|
+
*
|
|
109
|
+
* `on-request` IS WHAT THE CODEX APP ITSELF RUNS, which is the whole of what she
|
|
110
|
+
* asked for: work inside the sandbox unasked, ask only to get out. It keeps the
|
|
111
|
+
* escape hatch rather than removing it, so a run that genuinely needs the
|
|
112
|
+
* network can still request it and she still gets a card. One caveat stated
|
|
113
|
+
* rather than implied: the escape probe TOLD the model not to work around a
|
|
114
|
+
* refusal, so its zero cards prove the sandbox refuses by default, not that
|
|
115
|
+
* `on-request` never asks for escalation.
|
|
116
|
+
*
|
|
117
|
+
* THE PATCH IS STILL CARDED WHERE IT MATTERS and that machinery is untouched:
|
|
118
|
+
* `FileChangeRequestApprovalParams` carries no path and no diff, so
|
|
119
|
+
* main/codex-approvals.mjs draws the card off the `item/started` for the same
|
|
120
|
+
* itemId, which arrives first and carries the paths and the change.
|
|
121
|
+
*
|
|
122
|
+
* `workspace-write` is the sandbox and it does not move: a worker may write in
|
|
123
|
+
* the folder it was given and nowhere else, and has no network. That is what
|
|
124
|
+
* `--permission-mode acceptEdits` buys on the other engine.
|
|
125
|
+
*/
|
|
126
|
+
export const WORKER_APPROVAL_POLICY = 'on-request';
|
|
127
|
+
export const WORKER_SANDBOX = 'workspace-write';
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* How long a worker waits for its store server before giving up on the run.
|
|
131
|
+
*
|
|
132
|
+
* Measured 2026-09-04: a working server went from `starting` to `ready` in well
|
|
133
|
+
* under a second, and a broken one failed, was retried by Codex, and failed
|
|
134
|
+
* again inside twenty-five seconds. Thirty covers the retry and still costs a
|
|
135
|
+
* genuinely dead machine half a minute rather than her fifteen.
|
|
136
|
+
*/
|
|
137
|
+
export const MCP_READY_MS = 30_000;
|
|
138
|
+
|
|
139
|
+
/* * * TURN THE FOUNDER'S OWN MCP SERVERS OFF FOR A WORKER, BY NAME. * * A bare `thread/start`
|
|
140
|
+
inherits every MCP server in her `~/.codex/config.toml`. * Measured 2026-09-04 on codex-cli
|
|
141
|
+
0.148.0, thread start only and no turn: a * plain start reported
|
|
142
|
+
`mcpServer/startupStatus/updated` for `playwright`, * `context7` and `codex_apps`, so a
|
|
143
|
+
the app worker would silently launch two npx * processes of hers on every spawn. Nobody asked
|
|
144
|
+
for that and nothing on her * screen would say it happened. * * THE OBVIOUS FIX DOES NOT
|
|
145
|
+
WORK, WHICH IS WHY THIS IS A LIST OF NAMES AND NOT * AN EMPTY TABLE. All measured the same
|
|
146
|
+
day, same probe: * * `config: { mcp_servers: {} }` -> all three still started. It used to
|
|
147
|
+
also be what an * unreadable `config/read` degraded to, and that was the whole of the bug:
|
|
148
|
+
an * empty list is `{}`, `{}` is the no-op measured above, and the no-op starts * every
|
|
149
|
+
server she has inside a headless worker. * * `codex_apps` IS NOT ON THIS TABLE AND CANNOT
|
|
150
|
+
BE, measured 2026-09-04 on * codex-cli 0.148.0, thread start only: * * with `codex_apps:
|
|
151
|
+
{enabled:false}` beside her two -> `thread/start` REFUSED, * -32600 "failed to load
|
|
152
|
+
configuration: invalid transport in * `mcp_servers.codex_apps`" * with her two named off
|
|
153
|
+
and `codex_apps` left alone -> the only startup * notifications on the thread were
|
|
154
|
+
`codex_apps=starting`, `codex_apps=ready` * * An `enabled: false` entry is a MERGE onto a
|
|
155
|
+
server that already exists in her * config; Codex's own built-in app connector is not in
|
|
156
|
+
her config and has no * transport to merge onto, so naming it refuses the thread rather
|
|
157
|
+
than * disabling it. It therefore stays enabled on every worker, deliberately. It is *
|
|
158
|
+
Codex's own connector rather than a process of hers, so nothing of hers runs * because of
|
|
159
|
+
it -- but it is not nothing, and the slice that needs it off is * the slice that finds the
|
|
160
|
+
switch Codex actually offers for it. * * That measurement is also why the names must be
|
|
161
|
+
READ AT EVERY START rather * than cached for the app-server's life: a name that is stale
|
|
162
|
+
because she * deleted the server does not merely fail to disable anything, it refuses the *
|
|
163
|
+
thread outright. * * AND THE STORE SERVER GOES BACK IN THROUGH THE SAME TABLE. Switching
|
|
164
|
+
every * server off and adding nothing is what the first slice shipped, and it left a *
|
|
165
|
+
Codex worker unable to claim, checkpoint or finish a row: CLAUDE.md's * 2026-08-05
|
|
166
|
+
incident, "five sessions of analysis written to nowhere", arriving * through a second
|
|
167
|
+
engine. Measured 2026-09-04 on codex-cli 0.148.0, one * `thread/start` and no turn: an
|
|
168
|
+
entry added BESIDE the `enabled: false` ones * started and reported ready while
|
|
169
|
+
`playwright` and `context7` still produced * nothing at all, so one table carries both
|
|
170
|
+
halves and the isolation is intact. * * Ours is written LAST, deliberately. If she ever
|
|
171
|
+
runs a server of her own * under this name the store is the one that has to win, because a
|
|
172
|
+
worker with * no store is worse than a worker without her tool.
|
|
173
|
+
*/
|
|
174
|
+
export function mcpIsolation(names = [], storeServer = null) {
|
|
175
|
+
// NULL-PROTOTYPE, BECAUSE ONE OF HER SERVERS MAY BE CALLED `__proto__`.
|
|
176
|
+
// `{}[('__proto__')] = x` reaches Object.prototype's setter: the assignment
|
|
177
|
+
// appears to work, `Object.keys` does not list it, and JSON.stringify does
|
|
178
|
+
// not emit it -- so the one server named after the thing that hides it would
|
|
179
|
+
// be the one server left running inside a worker. Nothing else here changes:
|
|
180
|
+
// JSON.stringify treats a null-prototype object exactly as a plain one.
|
|
181
|
+
const mcp_servers = Object.create(null);
|
|
182
|
+
for (const name of names) {
|
|
183
|
+
if (typeof name === 'string' && name) mcp_servers[name] = { enabled: false };
|
|
184
|
+
}
|
|
185
|
+
if (storeServer?.name && storeServer.command) {
|
|
186
|
+
const entry = { command: storeServer.command, env: storeServer.env ?? {} };
|
|
187
|
+
// `args` IS CARRIED WHEN THERE IS ONE, and Agentbox's own launcher has none:
|
|
188
|
+
// `storeMcpCommand` is a single executable, exactly as the Claude path
|
|
189
|
+
// passes it. It is here because dropping a caller's argv in silence is a
|
|
190
|
+
// server that starts and never handshakes, which was measured against a
|
|
191
|
+
// real app-server while writing this -- the process came up, waited on
|
|
192
|
+
// stdin forever, and the only symptom was a run that stopped saying the
|
|
193
|
+
// store never reported ready.
|
|
194
|
+
if (Array.isArray(storeServer.args) && storeServer.args.length) entry.args = [...storeServer.args];
|
|
195
|
+
mcp_servers[storeServer.name] = entry;
|
|
196
|
+
}
|
|
197
|
+
return mcp_servers;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* THE ONE FOLDER THIS THREAD RUNS IN, DECLARED UNTRUSTED TO CODEX.
|
|
202
|
+
*
|
|
203
|
+
* `mcpIsolation` above switches off the servers named in HER config, and that
|
|
204
|
+
* is all it can do, because those are the only names `config/read` returns. A
|
|
205
|
+
* `.codex/config.toml` sitting in the product folder is a config LAYER of
|
|
206
|
+
* Codex's own, and the per-thread override is a deep merge, so a server that
|
|
207
|
+
* layer declares was never named and never switched off. Measured on this Mac
|
|
208
|
+
* 2026-09-05, codex-cli 0.148.0, real `codex app-server`, scratch CODEX_HOME:
|
|
209
|
+
*
|
|
210
|
+
* proj/.codex/config.toml declares `planted` = sh -c "echo pwned > MARK"
|
|
211
|
+
* thread/start cwd=proj, mcp_servers = { herone: { enabled: false } }
|
|
212
|
+
* -> mcpServer/startupStatus/updated planted=starting, and MARK was on
|
|
213
|
+
* disk. The plant RAN.
|
|
214
|
+
*
|
|
215
|
+
* So a repository she cloned -- or an earlier worker writing inside one --
|
|
216
|
+
* plants a command the next Codex thread LAUNCHES, outside the command sandbox
|
|
217
|
+
* and outside the approval path, with no card and nothing on her screen.
|
|
218
|
+
*
|
|
219
|
+
* NAMING IT THE WAY `mcpIsolation` NAMES HERS DOES NOT WORK. Measured the same
|
|
220
|
+
* hour: `{ planted: { enabled: false } }` answered -32600 "failed to load
|
|
221
|
+
* configuration: invalid transport in `mcp_servers.planted`", because an
|
|
222
|
+
* `enabled: false` entry is a merge onto a server in the TRUSTED config and an
|
|
223
|
+
* untrusted project layer is not in it. Asking `config/read` WITH the thread's
|
|
224
|
+
* cwd is no help either: it returned the same home names, and only
|
|
225
|
+
* `includeLayers: true` showed the layer at all -- carrying a `disabledReason`
|
|
226
|
+
* while `thread/start` went ahead and started the server regardless.
|
|
227
|
+
*
|
|
228
|
+
* CODEX'S OWN PROJECT-TRUST GATE IS THE SWITCH, AND IT IS PER THREAD. Measured
|
|
229
|
+
* on the same probe:
|
|
230
|
+
*
|
|
231
|
+
* config.projects[<cwd>].trust_level = "untrusted"
|
|
232
|
+
* -> no startup notification at all, MARK never written, and the store
|
|
233
|
+
* server Agentbox puts there itself still started beside it.
|
|
234
|
+
* with [projects."<proj>"] trust_level = "trusted" already in the home
|
|
235
|
+
* config.toml, the per-thread untrusted STILL won.
|
|
236
|
+
* cwd two levels below the `.codex` folder: unguarded the plant ran, and
|
|
237
|
+
* naming only the cwd stopped it.
|
|
238
|
+
*
|
|
239
|
+
* AND IT STOPS AGENTBOX WRITING IN HER CODEX CONFIG, which nothing had noticed.
|
|
240
|
+
* Without this, `thread/start` appended [projects."<cwd>"] trust_level =
|
|
241
|
+
* "trusted" to CODEX_HOME's own config.toml -- permanently, and for her
|
|
242
|
+
* terminal too, so the first Agentbox run in a folder quietly made that folder's
|
|
243
|
+
* `.codex` live for every `codex` she types afterwards. With it, nothing was
|
|
244
|
+
* written.
|
|
245
|
+
*
|
|
246
|
+
* A cwd that is not a real string answers `{}` rather than a guess: a table
|
|
247
|
+
* with an empty key is a table Codex refuses, and that would cost the run over
|
|
248
|
+
* a fact nobody stated.
|
|
249
|
+
*/
|
|
250
|
+
export function projectIsolation(cwd) {
|
|
251
|
+
// NULL-PROTOTYPE for the reason `mcpIsolation` gives: a path is any string,
|
|
252
|
+
// `__proto__` included, and a plain `{}` drops that one key in silence --
|
|
253
|
+
// leaving the folder named after the thing that hides it the one folder that
|
|
254
|
+
// is still trusted.
|
|
255
|
+
const projects = Object.create(null);
|
|
256
|
+
if (typeof cwd === 'string' && cwd) projects[cwd] = { trust_level: 'untrusted' };
|
|
257
|
+
return projects;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* THE NAMES IN A `config/read` RESULT, OR A REFUSAL TO GUESS.
|
|
262
|
+
*
|
|
263
|
+
* This used to answer `[]` for every shape it did not recognise, on the reading
|
|
264
|
+
* that isolation which cannot read her config should degrade to "turn nothing
|
|
265
|
+
* off". IT DEGRADED TO THE OPPOSITE. `[]` builds `{}`, and `{}` is the deep
|
|
266
|
+
* merge measured above that disables nothing at all -- so the failure path
|
|
267
|
+
* started every MCP server she has inside a headless worker, on this Mac two
|
|
268
|
+
* `npx` processes per spawn, with nothing on her screen saying it happened.
|
|
269
|
+
*
|
|
270
|
+
* So there are two answers now and they are not the same answer:
|
|
271
|
+
*
|
|
272
|
+
* SHE HAS NONE -> `[]`. An empty table, no `mcp_servers` key at all, or an
|
|
273
|
+
* explicit null. The empty override really is correct here, because there
|
|
274
|
+
* is nothing of hers for it to fail to disable.
|
|
275
|
+
* WE COULD NOT TELL -> THROW. A read that errored, a result with no `config`,
|
|
276
|
+
* an `mcp_servers` that is a list or a string. The caller stops the worker
|
|
277
|
+
* before `thread/start` rather than starting one that inherits everything.
|
|
278
|
+
*
|
|
279
|
+
* A throw is safe on the spawn path by construction: `createCodexWorker` awaits
|
|
280
|
+
* this inside its own promise chain, and the `catch` at the end of that chain
|
|
281
|
+
* ends the worker with the message on the stderr `troubleCause` reads.
|
|
282
|
+
*/
|
|
283
|
+
export function mcpServerNames(configRead) {
|
|
284
|
+
const cannot = () => {
|
|
285
|
+
throw new Error('could not read which MCP servers to switch off for this worker, so it is not starting rather than running every server the founder has');
|
|
286
|
+
};
|
|
287
|
+
if (!plainObject(configRead) || !plainObject(configRead.config)) cannot();
|
|
288
|
+
const servers = configRead.config.mcp_servers;
|
|
289
|
+
if (servers === undefined || servers === null) return [];
|
|
290
|
+
if (!plainObject(servers)) cannot();
|
|
291
|
+
return Object.keys(servers);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const plainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The `thread/start` params for one worker.
|
|
298
|
+
*
|
|
299
|
+
* `ephemeral: false` IS NOT A DEFAULT WORTH RELYING ON HERE, even though
|
|
300
|
+
* main/codex-app-server.mjs supplies the same value. It is the difference
|
|
301
|
+
* between a row that can be replied to and one that cannot: measured
|
|
302
|
+
* 2026-09-04, `thread/resume` on an ephemeral thread fails `no rollout found`,
|
|
303
|
+
* and continuation-on-reply is most of what this app does. Written where a
|
|
304
|
+
* reader of this file can see it rather than inherited from another one.
|
|
305
|
+
*
|
|
306
|
+
* `projects` IS THE SECOND HALF OF THE ISOLATION AND NOT AN EXTRA. `mcp_servers`
|
|
307
|
+
* can only name the servers in HER config; `projects` is what keeps the config
|
|
308
|
+
* layer inside the folder the thread runs in from contributing any of its own.
|
|
309
|
+
* See `projectIsolation` for the measurement.
|
|
310
|
+
*
|
|
311
|
+
* `features.apps` IS THE THIRD, AND IT IS THE SWITCH `mcpIsolation` SPENT A
|
|
312
|
+
* SLICE FAILING TO FIND. `codex_apps` cannot be named in `mcp_servers` -- that
|
|
313
|
+
* refuses the thread, measured 2026-09-04 -- because it is not one of her
|
|
314
|
+
* servers at all: it is Codex's own connector to the apps she has linked to her
|
|
315
|
+
* ChatGPT account, and it is gated by a FEATURE. Measured 2026-09-05,
|
|
316
|
+
* codex-cli 0.148.0, real app-server:
|
|
317
|
+
*
|
|
318
|
+
* experimentalFeature/list -> { name: "apps", stage: "stable",
|
|
319
|
+
* enabled: true, defaultEnabled: true }
|
|
320
|
+
* thread/start config.features.apps = false -> accepted, no refusal
|
|
321
|
+
* experimentalFeature/list { threadId } for that thread -> apps enabled FALSE
|
|
322
|
+
* the same call for a thread started without it -> apps enabled true
|
|
323
|
+
*
|
|
324
|
+
* (`config.apps` is not it and was tried first: `{ apps: { enabled: false } }`
|
|
325
|
+
* answered "invalid type: boolean `false`, expected struct AppConfig in
|
|
326
|
+
* `apps`".)
|
|
327
|
+
*
|
|
328
|
+
* WHAT IS NOT MEASURED, said plainly rather than implied away: the connector
|
|
329
|
+
* needs a signed-in ChatGPT account and every probe ran against a scratch
|
|
330
|
+
* CODEX_HOME that is signed out, so `codex_apps` never started in any of them.
|
|
331
|
+
* What was watched is Codex's own registry reporting the flag off for the
|
|
332
|
+
* thread, not the connector process failing to appear. Her real `~/.codex` was
|
|
333
|
+
* not opened to close that gap. `createCodexWorker` therefore also WATCHES the
|
|
334
|
+
* result -- see `sayUnasked` below -- so a flag that stops working is a line on
|
|
335
|
+
* the run rather than a silence.
|
|
336
|
+
*/
|
|
337
|
+
export function workerThreadParams({ cwd, model = null, instructions = null, mcpServers = [], storeServer = null, mode = null } = {}) {
|
|
338
|
+
// THE MODE SHE PICKED, OR THE PAIR OF CONSTANTS. `mode` is one of the three in
|
|
339
|
+
// shared/codex-modes.mjs, and it decides BOTH the sandbox and the approval
|
|
340
|
+
// policy, because on this engine those two are one choice rather than two
|
|
341
|
+
// (that file says why). Passing nothing means the default posture, which is
|
|
342
|
+
// what every caller did before there was a picker, so a caller that has not
|
|
343
|
+
// been updated keeps the behaviour it had.
|
|
344
|
+
const posture = mode
|
|
345
|
+
? codexPosture(mode)
|
|
346
|
+
: { approvalPolicy: WORKER_APPROVAL_POLICY, sandbox: WORKER_SANDBOX };
|
|
347
|
+
const params = {
|
|
348
|
+
cwd,
|
|
349
|
+
ephemeral: false,
|
|
350
|
+
approvalPolicy: posture.approvalPolicy,
|
|
351
|
+
sandbox: posture.sandbox,
|
|
352
|
+
...(posture.approvalsReviewer ? { approvalsReviewer: posture.approvalsReviewer } : {}),
|
|
353
|
+
config: {
|
|
354
|
+
mcp_servers: mcpIsolation(mcpServers, storeServer),
|
|
355
|
+
projects: projectIsolation(cwd),
|
|
356
|
+
// Plugins can contribute MCP servers outside config.mcp_servers.
|
|
357
|
+
// Keep this worker on the same explicit integration boundary.
|
|
358
|
+
features: { apps: false, plugins: false, remote_plugin: false },
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
if (model) params.model = model;
|
|
362
|
+
// Her standing instructions and the project's, in the slot that is this
|
|
363
|
+
// engine's `--append-system-prompt`: a rule pasted above a 200-line brief is
|
|
364
|
+
// a rule that gets buried, which is the reason the Claude path uses a system
|
|
365
|
+
// prompt too.
|
|
366
|
+
if (instructions) params.developerInstructions = instructions;
|
|
367
|
+
return params;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* THE SAME THREAD, CONTINUED, AND IT HAS TO CARRY THE WHOLE TABLE AGAIN.
|
|
372
|
+
*
|
|
373
|
+
* The harness resumed with `{threadId}` and nothing else, on the reasonable
|
|
374
|
+
* assumption that a thread remembers what it was started with. IT DOES NOT.
|
|
375
|
+
* Measured 2026-09-04 on codex-cli 0.148.0, one thread and three connections to
|
|
376
|
+
* the same rollout:
|
|
377
|
+
*
|
|
378
|
+
* resume WITH this override -> the store server started and reported ready, and
|
|
379
|
+
* `playwright` and `context7` stayed off.
|
|
380
|
+
* resume with `{threadId}` -> `playwright` and `context7` BOTH started and
|
|
381
|
+
* reported ready, and the thread had no store
|
|
382
|
+
* server on it at all. Two npx processes of
|
|
383
|
+
* hers were left running on the Mac by that
|
|
384
|
+
* probe and had to be killed by hand.
|
|
385
|
+
*
|
|
386
|
+
* A reply she writes is a continuation, and continuations are most of what this
|
|
387
|
+
* app does, so without this the isolation held for a row's first turn only and
|
|
388
|
+
* every reply afterwards ran her servers and no store.
|
|
389
|
+
*
|
|
390
|
+
* `ephemeral` IS DROPPED RATHER THAN PASSED THROUGH. ThreadResumeParams takes
|
|
391
|
+
* approvalPolicy, approvalsReviewer, baseInstructions, config, cwd,
|
|
392
|
+
* developerInstructions, model, modelProvider, personality, sandbox,
|
|
393
|
+
* serviceTier and threadId, and `ephemeral` is not among them: it is a fact
|
|
394
|
+
* about creating a thread, and this one already exists.
|
|
395
|
+
*/
|
|
396
|
+
export function resumeThreadParams(threadId, params = {}) {
|
|
397
|
+
const { ephemeral, ...rest } = params ?? {};
|
|
398
|
+
return { threadId, ...rest };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* WHERE CODEX PUT THE TRANSCRIPT OF ONE SESSION.
|
|
403
|
+
*
|
|
404
|
+
* Claude Code keeps `<home>/projects/<slug>/<id>.jsonl`, so the supervisor can
|
|
405
|
+
* test one path and be done. Codex writes
|
|
406
|
+
* `~/.codex/sessions/YYYY/MM/DD/rollout-<ISO>-<uuid>.jsonl` and the id is
|
|
407
|
+
* INSIDE the filename, so finding one means looking. There were 1,700 of these
|
|
408
|
+
* on the machine this was written on, counted with `find ~/.codex/sessions -name
|
|
409
|
+
* 'rollout-*.jsonl' | wc -l`, so "look at all of them" is not an answer.
|
|
410
|
+
*
|
|
411
|
+
* NEWEST FIRST AND BOUNDED. The directory tree is already sorted by date, so
|
|
412
|
+
* walking it year-desc, month-desc, day-desc finds a recent session in one
|
|
413
|
+
* readdir and gives up after `days` day-folders rather than scanning the lot.
|
|
414
|
+
* Sixty is the bound because the only caller is a "does this still exist"
|
|
415
|
+
* check before a resume, and a session two months old is one nothing is going
|
|
416
|
+
* back to.
|
|
417
|
+
*
|
|
418
|
+
* THE MATCH IS THE WHOLE FILENAME, NOT A SUBSTRING. `includes(id)` would match
|
|
419
|
+
* a neighbouring rollout whose own uuid merely ends with the same characters,
|
|
420
|
+
* and handing `thread/resume` the wrong thread is worse than handing it
|
|
421
|
+
* nothing: it would resume a stranger's conversation onto her row.
|
|
422
|
+
*/
|
|
423
|
+
export function codexTranscriptFile(sessionId, { home = path.join(os.homedir(), '.codex'), days = 60 } = {}) {
|
|
424
|
+
if (!sessionId || typeof sessionId !== 'string') return null;
|
|
425
|
+
const wanted = new RegExp(`^rollout-\\d{4}-\\d{2}-\\d{2}T\\d{2}-\\d{2}-\\d{2}-${escapeForRegExp(sessionId)}\\.jsonl$`);
|
|
426
|
+
const root = path.join(home, 'sessions');
|
|
427
|
+
let looked = 0;
|
|
428
|
+
for (const year of numericDesc(root)) {
|
|
429
|
+
for (const month of numericDesc(path.join(root, year))) {
|
|
430
|
+
for (const day of numericDesc(path.join(root, year, month))) {
|
|
431
|
+
if (looked >= days) return null;
|
|
432
|
+
looked += 1;
|
|
433
|
+
const dir = path.join(root, year, month, day);
|
|
434
|
+
let names = [];
|
|
435
|
+
try { names = fs.readdirSync(dir); } catch { names = []; }
|
|
436
|
+
const hit = names.find((name) => wanted.test(name));
|
|
437
|
+
if (hit) return path.join(dir, hit);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function escapeForRegExp(text) {
|
|
445
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Newest first, and only the entries that are dated: a stray file or a folder
|
|
449
|
+
// Codex renames under us must not stop the walk or reorder it.
|
|
450
|
+
function numericDesc(dir) {
|
|
451
|
+
let names = [];
|
|
452
|
+
try { names = fs.readdirSync(dir); } catch { return []; }
|
|
453
|
+
return names.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* * * ONE WORK ITEM'S THREAD, WEARING A CHILD PROCESS'S FACE. * * `server` is a
|
|
457
|
+
main/codex-app-server.mjs client, shared with every other * worker on this Mac. `ready` is
|
|
458
|
+
the handshake: `initialize` must land before * `thread/start`, and awaiting it here rather
|
|
459
|
+
than at every call site is what * lets the supervisor treat starting a Codex worker as one
|
|
460
|
+
synchronous call * that returns a handle, exactly like `spawn`. * * WHAT IT EMITS, AND
|
|
461
|
+
WHEN: * * 'spawn' the work is really up: the thread exists AND the * store server it needs
|
|
462
|
+
has reported ready. Without it the transport refuses every * server request on this thread
|
|
463
|
+
immediately -- which is still the correct * behaviour and is what a worker with nobody to
|
|
464
|
+
ask should hear, rather than * parking for fifteen minutes. main/codex-approvals.mjs is the
|
|
465
|
+
handler the * supervisor passes. * * `requireMcpServer` IS THE STORE, AND THE TURN WAITS
|
|
466
|
+
FOR IT. Measured * 2026-09-04: a `mcp_servers` entry whose command does not exist leaves *
|
|
467
|
+
`thread/start` resolving normally and the turn free to run, with the failure * arriving
|
|
468
|
+
only as a notification. That is a worker doing a whole job it cannot * file anywhere, which
|
|
469
|
+
CLAUDE.md prices at five sessions. So the turn does not * start until
|
|
470
|
+
`mcpServer/startupStatus/updated` says `ready` for this server, * and if that never comes
|
|
471
|
+
the run dies with the reason on stderr. Null when no * store server was configured, which
|
|
472
|
+
is every downloaded copy of Agentbox: there * is nothing to wait for and a worker that
|
|
473
|
+
waited would never run. * * ONE `failed` IS NOT THE VERDICT, THE DEADLINE IS. Codex retries
|
|
474
|
+
a server that * failed to start -- measured, twice inside twenty-five seconds -- so a run *
|
|
475
|
+
killed on the first failure would be a run killed on a hiccup. The last * failure's own
|
|
476
|
+
words are kept, and they are what the run dies saying.
|
|
477
|
+
*/
|
|
478
|
+
export function createCodexWorker({
|
|
479
|
+
server,
|
|
480
|
+
ready = Promise.resolve(),
|
|
481
|
+
threadParams = {},
|
|
482
|
+
turnParams = {},
|
|
483
|
+
resumeThreadId = null,
|
|
484
|
+
onApproval = null,
|
|
485
|
+
requireMcpServer = null,
|
|
486
|
+
mcpReadyMs = MCP_READY_MS,
|
|
487
|
+
} = {}) {
|
|
488
|
+
if (!server) throw new Error('createCodexWorker needs a codex app-server client');
|
|
489
|
+
|
|
490
|
+
const worker = new EventEmitter();
|
|
491
|
+
const stderr = new EventEmitter();
|
|
492
|
+
worker.stderr = stderr;
|
|
493
|
+
worker.threadId = resumeThreadId ?? null;
|
|
494
|
+
worker.turnId = null;
|
|
495
|
+
|
|
496
|
+
let ended = false;
|
|
497
|
+
worker.steer = async text => {
|
|
498
|
+
if (ended || !worker.threadId || !worker.turnId) throw Error('The turn is not running. Send again when the conversation is ready.');
|
|
499
|
+
const expected=worker.turnId;
|
|
500
|
+
const result=await server.steerTurn(worker.threadId,expected,text);
|
|
501
|
+
if(result?.turnId!==expected)throw Error('The provider did not confirm the active turn.');
|
|
502
|
+
return result;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const say = (text) => {
|
|
506
|
+
const line = String(text ?? '').trim();
|
|
507
|
+
if (line) stderr.emit('data', line);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/* --------------------------- the store's gate --------------------------- */
|
|
511
|
+
|
|
512
|
+
// `ready` more than once is ordinary: measured, the store server reported ready at
|
|
513
|
+
// the thread's start and again while the turn ran. So the wait settles once
|
|
514
|
+
// and every later notification is a no-op.
|
|
515
|
+
//
|
|
516
|
+
// THIS DEPENDS ON THE STARTUP NOTIFICATION ARRIVING AFTER THE RESPONSE THAT
|
|
517
|
+
// NAMES THE THREAD, which is what was observed on every measured run: the
|
|
518
|
+
// watch is installed inside `startThread`'s result hook, synchronously in the
|
|
519
|
+
// read loop, so anything the server packs in BEHIND the response is caught,
|
|
520
|
+
// and a `ready` that somehow came in AHEAD of it would belong to no watched
|
|
521
|
+
// thread and be dropped -- costing a healthy worker its run at the deadline.
|
|
522
|
+
// The resume road has no such window, because `resumeThread` watches before
|
|
523
|
+
// it sends. If a Codex release ever reorders that, this is the sentence that
|
|
524
|
+
// says where to look.
|
|
525
|
+
let storeIsUp = !requireMcpServer;
|
|
526
|
+
let storeTrouble = '';
|
|
527
|
+
const waiting = [];
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A SERVER CAME UP ON THIS THREAD THAT AGENTBOX DID NOT PUT THERE.
|
|
531
|
+
*
|
|
532
|
+
* The isolation is three tables now -- her servers off by name, the folder's
|
|
533
|
+
* own config layer gated, and Codex's apps connector switched off by feature
|
|
534
|
+
* -- and every one of them is a claim about a version of `codex app-server`.
|
|
535
|
+
* A claim nobody checks is how the last two holes lasted: an empty
|
|
536
|
+
* `mcp_servers` table "disabled" everything for a slice, and `codex_apps` was
|
|
537
|
+
* left running for a slice after that, both without a symptom on any screen.
|
|
538
|
+
*
|
|
539
|
+
* So the result is watched rather than assumed. `starting` is the status that
|
|
540
|
+
* means a process was really launched (`failed` launched nothing, and a
|
|
541
|
+
* `ready` follows a `starting` we have already spoken about), and this is the
|
|
542
|
+
* pipe `troubleCause` reads and the run trace records, so the line reaches her
|
|
543
|
+
* row rather than a log nobody opens.
|
|
544
|
+
*
|
|
545
|
+
* ONCE PER SERVER. A line repeated on every beat is a line she stops reading,
|
|
546
|
+
* which is the 2026-08-05 card flood in a different shape.
|
|
547
|
+
*
|
|
548
|
+
* AND THE WORDING IS LOAD-BEARING. `troubleCause`'s WORKSPACE list matches the
|
|
549
|
+
* bare word "untrusted" and its INTERRUPTED list matches "connection", so a
|
|
550
|
+
* sentence carrying either would relabel every Codex run that died of
|
|
551
|
+
* something else. tests/a-codex-worker-does-not-reach-her-connected-apps
|
|
552
|
+
* pins that against the real classifier rather than against this note.
|
|
553
|
+
*/
|
|
554
|
+
const spoken = new Set();
|
|
555
|
+
const sayUnasked = (params) => {
|
|
556
|
+
const name = typeof params?.name === 'string' ? params.name : '';
|
|
557
|
+
if (!name || name === requireMcpServer) return;
|
|
558
|
+
if (params.status !== 'starting' && params.status !== 'ready') return;
|
|
559
|
+
if (spoken.has(name)) return;
|
|
560
|
+
spoken.add(name);
|
|
561
|
+
say(`${Name} did not put the MCP server "${name}" on this run and it started anyway; whatever it offers is outside the command sandbox and outside the approval path.`);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const readStoreStatus = (params) => {
|
|
565
|
+
sayUnasked(params);
|
|
566
|
+
if (!requireMcpServer || params?.name !== requireMcpServer) return;
|
|
567
|
+
if (params.status === 'ready') {
|
|
568
|
+
storeIsUp = true;
|
|
569
|
+
for (const wake of waiting.splice(0)) wake(true);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (params.status !== 'failed' && params.status !== 'cancelled') return;
|
|
573
|
+
storeTrouble = String(params.error ?? '').trim() || `it ${params.status}`;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const storeReady = () => {
|
|
577
|
+
if (storeIsUp) return Promise.resolve(true);
|
|
578
|
+
return new Promise((resolve) => {
|
|
579
|
+
let settled = false;
|
|
580
|
+
const done = (up) => { if (settled) return; settled = true; clearTimeout(timer); resolve(up); };
|
|
581
|
+
const timer = setTimeout(() => done(false), mcpReadyMs);
|
|
582
|
+
timer?.unref?.();
|
|
583
|
+
waiting.push(done);
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The one ending this worker will ever have.
|
|
589
|
+
*
|
|
590
|
+
* The thread is RELEASED here and not only on a kill: a watch left installed
|
|
591
|
+
* on a finished thread hands every later line carrying that id to a listener
|
|
592
|
+
* for a session that no longer exists, and the transport keeps the whole
|
|
593
|
+
* client alive holding it.
|
|
594
|
+
*/
|
|
595
|
+
const end = (code, signal = null, how = null) => {
|
|
596
|
+
if (ended) return;
|
|
597
|
+
ended = true;
|
|
598
|
+
if (worker.threadId) { try { server.unwatch(worker.threadId); } catch { /* already gone */ } }
|
|
599
|
+
worker.emit('exit', code, signal, how);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const handlers = {
|
|
603
|
+
onNotification: (method, params) => {
|
|
604
|
+
// The turn id arrives on the notification as well as on the `turn/start`
|
|
605
|
+
// response, and this is the earlier of the two: `turn/start` resolves
|
|
606
|
+
// when the turn is ACCEPTED, and the server can pack `turn/started` in
|
|
607
|
+
// behind that response. `turn/interrupt` needs both ids, so the earliest
|
|
608
|
+
// honest moment to know the second one is here.
|
|
609
|
+
const id = params?.turn?.id ?? params?.turnId;
|
|
610
|
+
if (typeof id === 'string' && id) worker.turnId = id;
|
|
611
|
+
|
|
612
|
+
if (method === 'mcpServer/startupStatus/updated') readStoreStatus(params);
|
|
613
|
+
|
|
614
|
+
worker.emit('event', method, params);
|
|
615
|
+
|
|
616
|
+
if (method !== 'turn/completed' && method !== 'turn/failed') return;
|
|
617
|
+
// `turn/failed` is handled even though the 0.148.0 schema does not list
|
|
618
|
+
// it: failure arrives as `turn/completed` with `turn.status: "failed"`.
|
|
619
|
+
// Dropping a notification means a run that reads as unfinished forever,
|
|
620
|
+
// and being wrong in this direction is free.
|
|
621
|
+
const status = typeof params?.turn?.status === 'string'
|
|
622
|
+
? params.turn.status
|
|
623
|
+
: (method === 'turn/failed' ? 'failed' : 'completed');
|
|
624
|
+
if (status === 'inProgress') return;
|
|
625
|
+
if (status !== 'completed') say(troubleText(params?.turn));
|
|
626
|
+
end(status === 'completed' ? 0 : 1, null);
|
|
627
|
+
},
|
|
628
|
+
// THE CONNECTION DIED UNDER THIS THREAD, AND THAT IS NOT THIS ITEM'S DOING.
|
|
629
|
+
//
|
|
630
|
+
// The process crashed, the pipe broke, one line nobody could read took the
|
|
631
|
+
// whole app-server down, or Agentbox closed it. `gone.stderr` is the
|
|
632
|
+
// app-server's own tracing tail, which is where Codex's refusal wording
|
|
633
|
+
// actually appears.
|
|
634
|
+
//
|
|
635
|
+
// ONE app-server carries the WHOLE fleet, so every one of these events
|
|
636
|
+
// reaches every live thread at once. Ending them `end(1, null)` -- no
|
|
637
|
+
// signal, nothing saying it was not the item -- had `settleDelivery` read
|
|
638
|
+
// each of them as a run that tried and failed and spend one of that item's
|
|
639
|
+
// three delivery attempts. Three such events and rows she had answered read
|
|
640
|
+
// "stopped", which CLAUDE.md calls the loudest word this app has and says
|
|
641
|
+
// must never mean "and nothing will retry". A whole-turn `turn/diff/updated`
|
|
642
|
+
// is the biggest frame on this wire, so the fault arrives exactly when the
|
|
643
|
+
// most work has been done.
|
|
644
|
+
//
|
|
645
|
+
// `transportFault` is the fact, told rather than faked. A signal would have
|
|
646
|
+
// been the cheap way to reach the same branch and it would be a lie: nothing
|
|
647
|
+
// signalled this worker, and `noteExitForBackoff` must still record the
|
|
648
|
+
// failure against the Codex account, because a broken app-server IS evidence
|
|
649
|
+
// about Codex on this Mac. Only the DELIVERY attempt is spared.
|
|
650
|
+
onClosed: (gone) => {
|
|
651
|
+
say(gone?.stderr);
|
|
652
|
+
end(1, null, { transportFault: true });
|
|
653
|
+
},
|
|
654
|
+
};
|
|
655
|
+
// Only when there is somebody to ask. The transport tests for a FUNCTION and
|
|
656
|
+
// refuses the request outright when it finds none, which is the right answer
|
|
657
|
+
// for a worker whose card would reach nobody.
|
|
658
|
+
if (typeof onApproval === 'function') handlers.onApproval = onApproval;
|
|
659
|
+
|
|
660
|
+
ready
|
|
661
|
+
// A FUNCTION, BECAUSE ONE OF THE PARAMS IS NOT KNOWN YET WHEN THE
|
|
662
|
+
// SUPERVISOR ASKS FOR A WORKER. The MCP servers a thread must switch off
|
|
663
|
+
// are read from the app-server itself, and her servers launch at
|
|
664
|
+
// `thread/start` -- so the list has to be in hand before the start rather
|
|
665
|
+
// than filled in on the second spawn. Whatever `ready` resolves to is
|
|
666
|
+
// handed here, and a plain object is still accepted unchanged.
|
|
667
|
+
.then((settled) => {
|
|
668
|
+
// CANCELLATION IS CHECKED IN FRONT OF EVERY RPC, NOT ONLY AFTER THE FIRST
|
|
669
|
+
// ONE. The first `ended` test used to sit AFTER the start had already been
|
|
670
|
+
// sent, so a worker killed during the handshake still launched a thread,
|
|
671
|
+
// still started whatever MCP processes that thread carries, and still
|
|
672
|
+
// installed a watcher -- all for a run the supervisor had already given
|
|
673
|
+
// up on. A quit kills the whole fleet at once, so this is the ordinary
|
|
674
|
+
// case rather than a corner one.
|
|
675
|
+
if (ended) return null;
|
|
676
|
+
const params = typeof threadParams === 'function' ? threadParams(settled) : threadParams;
|
|
677
|
+
// THE SAME PARAMS ON BOTH ROADS. A resume that carries only the thread id
|
|
678
|
+
// gets her own MCP servers back and no store at all (measured; see
|
|
679
|
+
// `resumeThreadParams`), and a reply is the commonest thing this app does.
|
|
680
|
+
return resumeThreadId
|
|
681
|
+
? server.resumeThread(resumeThreadParams(resumeThreadId, params), handlers).then(() => ({ threadId: resumeThreadId }))
|
|
682
|
+
: server.startThread(params, handlers);
|
|
683
|
+
})
|
|
684
|
+
.then((started) => {
|
|
685
|
+
if (!started) return null;
|
|
686
|
+
const threadId = started.threadId ?? resumeThreadId ?? null;
|
|
687
|
+
// A THREAD THAT ARRIVES AFTER THE KILL IS STILL A THREAD, AND IT IS OURS
|
|
688
|
+
// TO LET GO OF. `end` releases `worker.threadId`, and at a kill during
|
|
689
|
+
// startup there was not one yet, so the watch this response just installed
|
|
690
|
+
// would have outlived the worker: every later line carrying that id
|
|
691
|
+
// handed to a listener for a session that no longer exists, with the
|
|
692
|
+
// transport kept alive holding it.
|
|
693
|
+
if (ended) {
|
|
694
|
+
if (threadId) { try { server.unwatch(threadId); } catch { /* already gone */ } }
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
worker.threadId = threadId;
|
|
698
|
+
if (!worker.threadId) throw new Error('codex app-server started a thread with no id');
|
|
699
|
+
return storeReady();
|
|
700
|
+
})
|
|
701
|
+
.then((up) => {
|
|
702
|
+
if (ended) return null;
|
|
703
|
+
if (!up) {
|
|
704
|
+
// The loudest thing this file can do, on the pipe `troubleCause` reads.
|
|
705
|
+
// A run that stops here has written nothing and spent nothing, which is
|
|
706
|
+
// the whole point: the alternative is a worker doing the entire job and
|
|
707
|
+
// filing it nowhere.
|
|
708
|
+
say(`the ${requireMcpServer} store server did not start (${storeTrouble || 'it never reported ready'}), so this run is stopping rather than working with no way to write to her store`);
|
|
709
|
+
end(1, null);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
worker.emit('spawn');
|
|
713
|
+
return turnParams.reviewTarget
|
|
714
|
+
? server.request('review/start', { threadId: worker.threadId, target: turnParams.reviewTarget, delivery: 'inline' })
|
|
715
|
+
: server.startTurn(worker.threadId, turnParams);
|
|
716
|
+
})
|
|
717
|
+
.then((accepted) => {
|
|
718
|
+
const id = accepted?.turn?.id;
|
|
719
|
+
if (typeof id === 'string' && id) worker.turnId = id;
|
|
720
|
+
})
|
|
721
|
+
.catch((err) => {
|
|
722
|
+
// A thread that never started, a resume the server refused, a turn it
|
|
723
|
+
// would not accept. All three are a worker that did no work, and all
|
|
724
|
+
// three carry the sentence that says why -- including the app-server's
|
|
725
|
+
// stderr tail, which main/codex-app-server.mjs appends to every rejection.
|
|
726
|
+
// No signal here: a kill has already ended this worker with one, and
|
|
727
|
+
// `end` only ever fires once, so anything reaching this line is a real
|
|
728
|
+
// failure rather than something we did.
|
|
729
|
+
say(err?.message ?? String(err));
|
|
730
|
+
end(1, null);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* OUR KILL, AND IT LOOKS LIKE ONE FROM THE OUTSIDE.
|
|
735
|
+
*
|
|
736
|
+
* `turn/interrupt` takes BOTH ids -- a turn is not identified by either one
|
|
737
|
+
* alone -- so a kill before the turn id has arrived can only release the
|
|
738
|
+
* thread, which it does. The exit is emitted immediately rather than waiting
|
|
739
|
+
* for the server to confirm: the supervisor kills the whole fleet on quit and
|
|
740
|
+
* on every restart, and an ending that waits on a process we are shutting
|
|
741
|
+
* down is an ending that may never come.
|
|
742
|
+
*
|
|
743
|
+
* `signal` is set here and nowhere else. That is the fact `settleDelivery`
|
|
744
|
+
* reads to keep our own kill from spending one of an item's three delivery
|
|
745
|
+
* attempts, and a later `turn/completed` saying `interrupted` cannot undo it
|
|
746
|
+
* because `end` only fires once.
|
|
747
|
+
*/
|
|
748
|
+
worker.kill = () => {
|
|
749
|
+
if (ended) return false;
|
|
750
|
+
if (worker.threadId && worker.turnId) {
|
|
751
|
+
try { server.interruptTurn(worker.threadId, worker.turnId)?.catch?.(() => {}); } catch { /* dying anyway */ }
|
|
752
|
+
}
|
|
753
|
+
// 143 is what the Claude CLI reports for the same event (it traps SIGTERM
|
|
754
|
+
// and exits under its own power), so the two engines hand the exit handler
|
|
755
|
+
// the same pair of numbers for the same thing.
|
|
756
|
+
end(143, 'SIGTERM');
|
|
757
|
+
return true;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
return worker;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Why a turn did not finish, in whatever words the server gave us -- AND IN THE
|
|
765
|
+
* ONE WORD IT GIVES US THAT IS NOT PROSE.
|
|
766
|
+
*
|
|
767
|
+
* `TurnError` is `{ message, additionalDetails, codexErrorInfo }`, and
|
|
768
|
+
* `CodexErrorInfo` is an enum: `usageLimitExceeded`, `unauthorized`,
|
|
769
|
+
* `contextWindowExceeded`, `sessionBudgetExceeded`, `other`, and a handful of
|
|
770
|
+
* nested variants carrying an HTTP status. That is off `codex app-server
|
|
771
|
+
* generate-json-schema`, the binary's own generator, run on this Mac against
|
|
772
|
+
* codex-cli 0.148.0 on 2026-09-05.
|
|
773
|
+
*
|
|
774
|
+
* THE CODE WAS BEING THROWN AWAY AND THE SENTENCE CLASSIFIED INSTEAD, which is
|
|
775
|
+
* the wrong way round: shared/spawn-trouble.mjs has to decide whether a dead
|
|
776
|
+
* run was a signed-out login or a limit, and a name published by the vendor
|
|
777
|
+
* cannot drift the way a sentence can. Measured the same day: a signed-out run
|
|
778
|
+
* does not even produce Codex's own words -- the app-server passes the raw HTTP
|
|
779
|
+
* text through, so the prose is the transport's rather than the tool's.
|
|
780
|
+
*
|
|
781
|
+
* IN FRONT, NOT BEHIND. `noteExitForBackoff` classifies the LAST stderr line
|
|
782
|
+
* cut to 300 characters, and the measured 401 message is already 200 of them
|
|
783
|
+
* before any detail is appended; a code on the end would be the first thing
|
|
784
|
+
* lost. Only a plain string code is carried -- a nested variant is an object and
|
|
785
|
+
* would arrive as "[object Object]" -- and `other` is dropped, because it is the
|
|
786
|
+
* schema's own way of saying nothing and would be noise in front of the sentence
|
|
787
|
+
* that says something.
|
|
788
|
+
*/
|
|
789
|
+
function troubleText(turn) {
|
|
790
|
+
const why = String(turn?.error?.message ?? '').trim();
|
|
791
|
+
const more = String(turn?.error?.additionalDetails ?? '').trim();
|
|
792
|
+
const info = turn?.error?.codexErrorInfo;
|
|
793
|
+
const code = typeof info === 'string' && info && info !== 'other' ? info : '';
|
|
794
|
+
const head = why || `the turn ${turn?.status ?? 'did not finish'}`;
|
|
795
|
+
const said = code ? `${code}: ${head}` : head;
|
|
796
|
+
return more ? `${said}\n${more}` : said;
|
|
797
|
+
}
|