agentbox-app 0.1.13 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
package/main/codex-session.mjs
CHANGED
|
@@ -1,196 +1,83 @@
|
|
|
1
|
-
//
|
|
1
|
+
// One Codex work item, shaped like a child process.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
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.
|
|
3
|
+
// A Codex worker is a thread inside one shared `codex app-server`, not a process
|
|
4
|
+
// of its own. The supervisor uses five things on a worker's child:
|
|
5
|
+
// `once('spawn')`, `stdout.on('data')`, `stderr.on('data')`, `on('exit')` and
|
|
6
|
+
// `kill`. This module is a facade over one thread that provides them.
|
|
10
7
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// used in exactly five ways:
|
|
8
|
+
// It emits `event(method, params)` instead of faking JSON lines on stdout, so
|
|
9
|
+
// the field-name differences between app-server and `codex exec` stay visible.
|
|
10
|
+
// The supervisor swaps its readers per session (`readersFor` in
|
|
11
|
+
// main/supervisor.mjs).
|
|
16
12
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
13
|
+
// stderr is real. The supervisor classifies a dead run from its stderr lines
|
|
14
|
+
// (`troubleCause`), and Codex's own refusal wording is what lets it say why.
|
|
19
15
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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.
|
|
16
|
+
// The exit sets `signal` on our own kill, because `settleDelivery` reads it to
|
|
17
|
+
// keep a kill from spending a delivery attempt. It also carries a third
|
|
18
|
+
// argument a real ChildProcess never sends: `{ transportFault: true }` when the
|
|
19
|
+
// shared app-server died under this thread. See `onClosed` below.
|
|
61
20
|
|
|
62
21
|
import { EventEmitter } from 'node:events';
|
|
63
22
|
import fs from 'node:fs';
|
|
64
23
|
import os from 'node:os';
|
|
65
24
|
import path from 'node:path';
|
|
66
25
|
import { Name } from '../shared/product-name.mjs';
|
|
67
|
-
import { codexPosture } from '../shared/codex-modes.mjs';
|
|
26
|
+
import { codexPosture, codexReviewer } from '../shared/codex-modes.mjs';
|
|
68
27
|
|
|
69
28
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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.
|
|
29
|
+
* What a worker thread may do. The sandbox is the wall; the approval policy
|
|
30
|
+
* only decides who gets interrupted.
|
|
31
|
+
*
|
|
32
|
+
* `workspace-write` lets a worker write in its own folder and nowhere else,
|
|
33
|
+
* with no network (`network_access` defaults to false). The sandbox refuses
|
|
34
|
+
* anything else on its own, without a card. `on-request` is what the Codex app
|
|
35
|
+
* runs: work inside the sandbox without asking, and ask only to get out.
|
|
36
|
+
* `untrusted` asked about ordinary commands and contained nothing more.
|
|
37
|
+
*
|
|
38
|
+
* The sandbox refusing is only half of it: the model then asks to be let out,
|
|
39
|
+
* and who answers that ask is `approvalsReviewer`, not this policy. See
|
|
40
|
+
* `workerThreadParams`, which names it on every start.
|
|
41
|
+
*
|
|
42
|
+
* File changes are still carded. The approval request carries no path or diff,
|
|
43
|
+
* so main/codex-approvals.mjs draws the card from the `item/started` for the
|
|
44
|
+
* same itemId.
|
|
125
45
|
*/
|
|
126
46
|
export const WORKER_APPROVAL_POLICY = 'on-request';
|
|
127
47
|
export const WORKER_SANDBOX = 'workspace-write';
|
|
128
48
|
|
|
129
49
|
/**
|
|
130
50
|
* 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.
|
|
51
|
+
* Codex retries a server that fails to start, and this leaves room for the retry.
|
|
136
52
|
*/
|
|
137
53
|
export const MCP_READY_MS = 30_000;
|
|
138
54
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
*/
|
|
55
|
+
/**
|
|
56
|
+
* The `mcp_servers` override for a worker: every server in the user's Codex
|
|
57
|
+
* config switched off by name, plus the store server.
|
|
58
|
+
*
|
|
59
|
+
* Codex merges this onto the user's config, so an empty table switches nothing
|
|
60
|
+
* off and each server has to be named with `enabled: false`. Only names that
|
|
61
|
+
* exist may be used: an `enabled: false` entry with no server under it makes
|
|
62
|
+
* `thread/start` refuse with "invalid transport". That is why the names are
|
|
63
|
+
* read at every start, and why Codex's built-in `codex_apps` is not named here
|
|
64
|
+
* (`features.apps` in `workerThreadParams` turns it off).
|
|
65
|
+
*
|
|
66
|
+
* The store server is written last, so it wins over a server of the user's
|
|
67
|
+
* with the same name.
|
|
68
|
+
*/
|
|
174
69
|
export function mcpIsolation(names = [], storeServer = null) {
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
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.
|
|
70
|
+
// Null prototype, because a server may be named `__proto__`. On a plain
|
|
71
|
+
// object that key hits the prototype setter, is never serialised, and the
|
|
72
|
+
// server would stay on.
|
|
181
73
|
const mcp_servers = Object.create(null);
|
|
182
74
|
for (const name of names) {
|
|
183
75
|
if (typeof name === 'string' && name) mcp_servers[name] = { enabled: false };
|
|
184
76
|
}
|
|
185
77
|
if (storeServer?.name && storeServer.command) {
|
|
186
78
|
const entry = { command: storeServer.command, env: storeServer.env ?? {} };
|
|
187
|
-
//
|
|
188
|
-
//
|
|
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.
|
|
79
|
+
// Agentbox's own launcher has no args, but a server whose args are dropped
|
|
80
|
+
// starts and never completes its handshake, so pass them when given.
|
|
194
81
|
if (Array.isArray(storeServer.args) && storeServer.args.length) entry.args = [...storeServer.args];
|
|
195
82
|
mcp_servers[storeServer.name] = entry;
|
|
196
83
|
}
|
|
@@ -198,87 +85,36 @@ export function mcpIsolation(names = [], storeServer = null) {
|
|
|
198
85
|
}
|
|
199
86
|
|
|
200
87
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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.
|
|
88
|
+
* Marks the thread's folder untrusted to Codex, so a `.codex/config.toml` inside
|
|
89
|
+
* it cannot add MCP servers.
|
|
90
|
+
*
|
|
91
|
+
* That file is a config layer of its own. `config/read` does not return its
|
|
92
|
+
* servers, and naming one with `enabled: false` makes `thread/start` refuse.
|
|
93
|
+
* Without this, a cloned repository can plant a command that Codex launches
|
|
94
|
+
* outside the sandbox and the approval path. A per-thread `trust_level` of
|
|
95
|
+
* "untrusted" stops it, wins over a "trusted" entry in the home config, and
|
|
96
|
+
* works even when the `.codex` folder sits above the cwd. It also stops
|
|
97
|
+
* `thread/start` from writing a "trusted" entry for the cwd into the user's own
|
|
98
|
+
* Codex config.
|
|
99
|
+
*
|
|
100
|
+
* A cwd that is not a non-empty string gives `{}`, because Codex refuses a
|
|
101
|
+
* table with an empty key.
|
|
249
102
|
*/
|
|
250
103
|
export function projectIsolation(cwd) {
|
|
251
|
-
//
|
|
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.
|
|
104
|
+
// Null prototype for the reason `mcpIsolation` gives: a path can be `__proto__`.
|
|
255
105
|
const projects = Object.create(null);
|
|
256
106
|
if (typeof cwd === 'string' && cwd) projects[cwd] = { trust_level: 'untrusted' };
|
|
257
107
|
return projects;
|
|
258
108
|
}
|
|
259
109
|
|
|
260
110
|
/**
|
|
261
|
-
*
|
|
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:
|
|
111
|
+
* The server names in a `config/read` result.
|
|
271
112
|
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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.
|
|
113
|
+
* No servers at all (no `mcp_servers`, null, or an empty table) gives `[]`.
|
|
114
|
+
* Any shape we cannot read is a THROW rather than a guess, because `[]` would
|
|
115
|
+
* build an empty override that switches nothing off and the worker would start
|
|
116
|
+
* every server the user has. `createCodexWorker` catches the throw and ends the
|
|
117
|
+
* worker with the message on stderr.
|
|
282
118
|
*/
|
|
283
119
|
export function mcpServerNames(configRead) {
|
|
284
120
|
const cannot = () => {
|
|
@@ -296,60 +132,37 @@ const plainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
|
296
132
|
/**
|
|
297
133
|
* The `thread/start` params for one worker.
|
|
298
134
|
*
|
|
299
|
-
* `ephemeral: false`
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* `
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
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.
|
|
135
|
+
* `ephemeral: false` is set here on purpose: `thread/resume` on an ephemeral
|
|
136
|
+
* thread fails with "no rollout found", and every reply is a resume.
|
|
137
|
+
*
|
|
138
|
+
* The isolation has three parts. `mcp_servers` switches off the user's
|
|
139
|
+
* servers, `projects` blocks the folder's own config layer (see
|
|
140
|
+
* `projectIsolation`), and `features.apps: false` turns off Codex's connector
|
|
141
|
+
* to the apps linked to the user's ChatGPT account, which cannot be named in
|
|
142
|
+
* `mcp_servers`. (`config.apps` is not that switch; Codex rejects a boolean
|
|
143
|
+
* there.) The `apps` flag was only checked against Codex's own feature list
|
|
144
|
+
* while signed out, so `createCodexWorker` also reports any server that starts
|
|
145
|
+
* unasked (`sayUnasked`).
|
|
336
146
|
*/
|
|
337
|
-
export function workerThreadParams({ cwd, model = null, instructions = null, mcpServers = [], storeServer = null, mode = null } = {}) {
|
|
338
|
-
//
|
|
339
|
-
//
|
|
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.
|
|
147
|
+
export function workerThreadParams({ cwd, model = null, instructions = null, mcpServers = [], storeServer = null, mode = null, reviewer = null } = {}) {
|
|
148
|
+
// `mode` is one of shared/codex-modes.mjs and sets both the sandbox and the
|
|
149
|
+
// approval policy. No mode means the default posture.
|
|
344
150
|
const posture = mode
|
|
345
|
-
? codexPosture(mode)
|
|
346
|
-
: { approvalPolicy: WORKER_APPROVAL_POLICY, sandbox: WORKER_SANDBOX };
|
|
151
|
+
? codexPosture(mode, { reviewer })
|
|
152
|
+
: { approvalPolicy: WORKER_APPROVAL_POLICY, sandbox: WORKER_SANDBOX, approvalsReviewer: codexReviewer(reviewer) };
|
|
347
153
|
const params = {
|
|
348
154
|
cwd,
|
|
349
155
|
ephemeral: false,
|
|
350
156
|
approvalPolicy: posture.approvalPolicy,
|
|
351
157
|
sandbox: posture.sandbox,
|
|
352
|
-
|
|
158
|
+
// Always sent, never conditional. Omitted, it is not a neutral field:
|
|
159
|
+
// Codex reads `approvals_reviewer` from the top of whichever CODEX_HOME
|
|
160
|
+
// bills the run, and a home that says `auto_review` has its own subagent
|
|
161
|
+
// answer the escalation. Measured 2026-10-09 on codex-cli 0.160.0: a
|
|
162
|
+
// worker wrote into the home directory, reached three external sites, and
|
|
163
|
+
// raised zero cards. `scripts/qa-codex-live.mjs approvals default home`
|
|
164
|
+
// is that run; a third argument of `user` shows the card it suppressed.
|
|
165
|
+
approvalsReviewer: posture.approvalsReviewer,
|
|
353
166
|
config: {
|
|
354
167
|
mcp_servers: mcpIsolation(mcpServers, storeServer),
|
|
355
168
|
projects: projectIsolation(cwd),
|
|
@@ -359,39 +172,17 @@ export function workerThreadParams({ cwd, model = null, instructions = null, mcp
|
|
|
359
172
|
},
|
|
360
173
|
};
|
|
361
174
|
if (model) params.model = model;
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
// a rule that gets buried, which is the reason the Claude path uses a system
|
|
365
|
-
// prompt too.
|
|
175
|
+
// Standing instructions go in this engine's equivalent of
|
|
176
|
+
// `--append-system-prompt`, not in the prompt.
|
|
366
177
|
if (instructions) params.developerInstructions = instructions;
|
|
367
178
|
return params;
|
|
368
179
|
}
|
|
369
180
|
|
|
370
181
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
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.
|
|
182
|
+
* The `thread/resume` params. They carry the whole start table again, because
|
|
183
|
+
* a resumed thread does not remember its overrides: resumed with only
|
|
184
|
+
* `{threadId}`, the user's servers start and the store server is missing.
|
|
185
|
+
* `ephemeral` is dropped because ThreadResumeParams does not take it.
|
|
395
186
|
*/
|
|
396
187
|
export function resumeThreadParams(threadId, params = {}) {
|
|
397
188
|
const { ephemeral, ...rest } = params ?? {};
|
|
@@ -399,26 +190,13 @@ export function resumeThreadParams(threadId, params = {}) {
|
|
|
399
190
|
}
|
|
400
191
|
|
|
401
192
|
/**
|
|
402
|
-
*
|
|
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.
|
|
193
|
+
* Where Codex put one session's transcript, or null.
|
|
410
194
|
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
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.
|
|
195
|
+
* Codex writes `~/.codex/sessions/YYYY/MM/DD/rollout-<ISO>-<uuid>.jsonl`, with
|
|
196
|
+
* the id inside the filename, and there can be thousands. So the walk goes
|
|
197
|
+
* newest first and gives up after `days` day folders; the only caller checks
|
|
198
|
+
* that a session still exists before resuming it. The whole filename must
|
|
199
|
+
* match, because a substring match could resume the wrong conversation.
|
|
422
200
|
*/
|
|
423
201
|
export function codexTranscriptFile(sessionId, { home = path.join(os.homedir(), '.codex'), days = 60 } = {}) {
|
|
424
202
|
if (!sessionId || typeof sessionId !== 'string') return null;
|
|
@@ -445,36 +223,30 @@ function escapeForRegExp(text) {
|
|
|
445
223
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
446
224
|
}
|
|
447
225
|
|
|
448
|
-
// Newest first,
|
|
449
|
-
//
|
|
226
|
+
// Newest first, numeric names only, so a stray entry cannot stop or reorder
|
|
227
|
+
// the walk.
|
|
450
228
|
function numericDesc(dir) {
|
|
451
229
|
let names = [];
|
|
452
230
|
try { names = fs.readdirSync(dir); } catch { return []; }
|
|
453
231
|
return names.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
|
|
454
232
|
}
|
|
455
233
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
*/
|
|
234
|
+
/**
|
|
235
|
+
* One work item's thread, shaped like a child process.
|
|
236
|
+
*
|
|
237
|
+
* `server` is a main/codex-app-server.mjs client shared by every worker.
|
|
238
|
+
* `ready` is the handshake (`initialize` must land before `thread/start`),
|
|
239
|
+
* awaited here so the supervisor can start a worker with one synchronous call,
|
|
240
|
+
* like `spawn`.
|
|
241
|
+
*
|
|
242
|
+
* 'spawn' fires once the thread exists and the store server
|
|
243
|
+
* (`requireMcpServer`) has reported ready. A store server that fails to start
|
|
244
|
+
* does not stop `thread/start` or the turn; Codex only sends a notification.
|
|
245
|
+
* So the turn waits for `ready`, and if the deadline passes the run ends with
|
|
246
|
+
* the reason on stderr. Codex retries a failed server, so one `failed` is not
|
|
247
|
+
* the end, but its words are kept for that reason. With no store server
|
|
248
|
+
* configured, `requireMcpServer` is null and nothing waits.
|
|
249
|
+
*/
|
|
478
250
|
export function createCodexWorker({
|
|
479
251
|
server,
|
|
480
252
|
ready = Promise.resolve(),
|
|
@@ -494,10 +266,10 @@ export function createCodexWorker({
|
|
|
494
266
|
worker.turnId = null;
|
|
495
267
|
|
|
496
268
|
let ended = false;
|
|
497
|
-
worker.steer = async text => {
|
|
269
|
+
worker.steer = async (text, pictures = []) => {
|
|
498
270
|
if (ended || !worker.threadId || !worker.turnId) throw Error('The turn is not running. Send again when the conversation is ready.');
|
|
499
271
|
const expected=worker.turnId;
|
|
500
|
-
const result=await server.steerTurn(worker.threadId,expected,text);
|
|
272
|
+
const result=await server.steerTurn(worker.threadId,expected,text,pictures);
|
|
501
273
|
if(result?.turnId!==expected)throw Error('The provider did not confirm the active turn.');
|
|
502
274
|
return result;
|
|
503
275
|
};
|
|
@@ -507,50 +279,25 @@ export function createCodexWorker({
|
|
|
507
279
|
if (line) stderr.emit('data', line);
|
|
508
280
|
};
|
|
509
281
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
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 watches before sending, but an already-loaded thread does
|
|
523
|
-
// not repeat startup notifications. On 2026-10-07, 11 resumes of one loaded
|
|
524
|
-
// thread died at this deadline. Check that thread's current MCP connection
|
|
525
|
-
// on resume as well as listening for startup changes.
|
|
282
|
+
// The store can report `ready` more than once, so the wait settles once.
|
|
283
|
+
// On a start, the watch goes on in `startThread`'s result hook, which catches
|
|
284
|
+
// a startup notification sent right behind the response; one sent before it
|
|
285
|
+
// would be dropped. A resumed thread that is already loaded does not repeat
|
|
286
|
+
// its startup notifications, so a resume also asks for the store's current
|
|
287
|
+
// status.
|
|
526
288
|
let storeIsUp = !requireMcpServer;
|
|
527
289
|
let storeTrouble = '';
|
|
528
290
|
const waiting = [];
|
|
529
291
|
|
|
530
292
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
* -- and every one of them is a claim about a version of `codex app-server`.
|
|
536
|
-
* A claim nobody checks is how the last two holes lasted: an empty
|
|
537
|
-
* `mcp_servers` table "disabled" everything for a slice, and `codex_apps` was
|
|
538
|
-
* left running for a slice after that, both without a symptom on any screen.
|
|
539
|
-
*
|
|
540
|
-
* So the result is watched rather than assumed. `starting` is the status that
|
|
541
|
-
* means a process was really launched (`failed` launched nothing, and a
|
|
542
|
-
* `ready` follows a `starting` we have already spoken about), and this is the
|
|
543
|
-
* pipe `troubleCause` reads and the run trace records, so the line reaches her
|
|
544
|
-
* row rather than a log nobody opens.
|
|
545
|
-
*
|
|
546
|
-
* ONCE PER SERVER. A line repeated on every beat is a line she stops reading,
|
|
547
|
-
* which is the 2026-08-05 card flood in a different shape.
|
|
293
|
+
* Says on stderr, once per server, when a server starts on this thread that
|
|
294
|
+
* Agentbox did not put there. The isolation rests on Codex behaviour that can
|
|
295
|
+
* change between versions, so the result is checked. `failed` launched
|
|
296
|
+
* nothing, so only `starting` and `ready` count.
|
|
548
297
|
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
* something else. tests/a-codex-worker-does-not-reach-her-connected-apps
|
|
553
|
-
* pins that against the real classifier rather than against this note.
|
|
298
|
+
* `troubleCause` matches "untrusted" and "connection", so the message must
|
|
299
|
+
* contain neither. Pinned by
|
|
300
|
+
* tests/a-codex-worker-does-not-reach-her-connected-apps.test.mjs.
|
|
554
301
|
*/
|
|
555
302
|
const spoken = new Set();
|
|
556
303
|
const sayUnasked = (params) => {
|
|
@@ -619,12 +366,8 @@ export function createCodexWorker({
|
|
|
619
366
|
};
|
|
620
367
|
|
|
621
368
|
/**
|
|
622
|
-
* The
|
|
623
|
-
*
|
|
624
|
-
* The thread is RELEASED here and not only on a kill: a watch left installed
|
|
625
|
-
* on a finished thread hands every later line carrying that id to a listener
|
|
626
|
-
* for a session that no longer exists, and the transport keeps the whole
|
|
627
|
-
* client alive holding it.
|
|
369
|
+
* The only ending this worker has. It releases the thread's watch, so later
|
|
370
|
+
* lines for that id do not reach a dead session or keep the client alive.
|
|
628
371
|
*/
|
|
629
372
|
const end = (code, signal = null, how = null) => {
|
|
630
373
|
if (ended) return;
|
|
@@ -636,11 +379,8 @@ export function createCodexWorker({
|
|
|
636
379
|
|
|
637
380
|
const handlers = {
|
|
638
381
|
onNotification: (method, params) => {
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
// when the turn is ACCEPTED, and the server can pack `turn/started` in
|
|
642
|
-
// behind that response. `turn/interrupt` needs both ids, so the earliest
|
|
643
|
-
// honest moment to know the second one is here.
|
|
382
|
+
// `turn/started` can arrive before the `turn/start` response resolves,
|
|
383
|
+
// and `turn/interrupt` needs the turn id, so take it from the earlier one.
|
|
644
384
|
const id = params?.turn?.id ?? params?.turnId;
|
|
645
385
|
if (typeof id === 'string' && id) worker.turnId = id;
|
|
646
386
|
|
|
@@ -649,10 +389,8 @@ export function createCodexWorker({
|
|
|
649
389
|
worker.emit('event', method, params);
|
|
650
390
|
|
|
651
391
|
if (method !== 'turn/completed' && method !== 'turn/failed') return;
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
// Dropping a notification means a run that reads as unfinished forever,
|
|
655
|
-
// and being wrong in this direction is free.
|
|
392
|
+
// The 0.148.0 schema reports failure as `turn/completed` with status
|
|
393
|
+
// "failed" and has no `turn/failed`, but handling it costs nothing.
|
|
656
394
|
const status = typeof params?.turn?.status === 'string'
|
|
657
395
|
? params.turn.status
|
|
658
396
|
: (method === 'turn/failed' ? 'failed' : 'completed');
|
|
@@ -660,58 +398,33 @@ export function createCodexWorker({
|
|
|
660
398
|
if (status !== 'completed') say(troubleText(params?.turn));
|
|
661
399
|
end(status === 'completed' ? 0 : 1, null);
|
|
662
400
|
},
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
//
|
|
670
|
-
//
|
|
671
|
-
// reaches every live thread at once. Ending them `end(1, null)` -- no
|
|
672
|
-
// signal, nothing saying it was not the item -- had `settleDelivery` read
|
|
673
|
-
// each of them as a run that tried and failed and spend one of that item's
|
|
674
|
-
// three delivery attempts. Three such events and rows she had answered read
|
|
675
|
-
// "stopped", which CLAUDE.md calls the loudest word this app has and says
|
|
676
|
-
// must never mean "and nothing will retry". A whole-turn `turn/diff/updated`
|
|
677
|
-
// is the biggest frame on this wire, so the fault arrives exactly when the
|
|
678
|
-
// most work has been done.
|
|
679
|
-
//
|
|
680
|
-
// `transportFault` is the fact, told rather than faked. A signal would have
|
|
681
|
-
// been the cheap way to reach the same branch and it would be a lie: nothing
|
|
682
|
-
// signalled this worker, and `noteExitForBackoff` must still record the
|
|
683
|
-
// failure against the Codex account, because a broken app-server IS evidence
|
|
684
|
-
// about Codex on this Mac. Only the DELIVERY attempt is spared.
|
|
401
|
+
// The shared app-server died under this thread (a crash, a broken pipe, an
|
|
402
|
+
// unreadable frame, or Agentbox closed it). That is not this item's fault,
|
|
403
|
+
// and it reaches every live thread at once. `transportFault` lets
|
|
404
|
+
// `settleDelivery` spare the delivery attempt, while `noteExitForBackoff`
|
|
405
|
+
// still counts the failure against the Codex account. A fake signal would
|
|
406
|
+
// skip that count. `gone.stderr` is the app-server's tracing tail, where
|
|
407
|
+
// Codex's refusal wording appears; it goes last because the account books
|
|
408
|
+
// read the last line for a cause.
|
|
685
409
|
onClosed: (gone) => {
|
|
410
|
+
say(gone?.why);
|
|
686
411
|
say(gone?.stderr);
|
|
687
412
|
end(1, null, { transportFault: true });
|
|
688
413
|
},
|
|
689
414
|
};
|
|
690
|
-
// Only when there is
|
|
691
|
-
//
|
|
692
|
-
// for a worker whose card would reach nobody.
|
|
415
|
+
// Only when there is someone to ask. With no handler, the transport refuses
|
|
416
|
+
// every server request at once.
|
|
693
417
|
if (typeof onApproval === 'function') handlers.onApproval = onApproval;
|
|
694
418
|
|
|
695
419
|
ready
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
// are read from the app-server itself, and her servers launch at
|
|
699
|
-
// `thread/start` -- so the list has to be in hand before the start rather
|
|
700
|
-
// than filled in on the second spawn. Whatever `ready` resolves to is
|
|
701
|
-
// handed here, and a plain object is still accepted unchanged.
|
|
420
|
+
// `threadParams` may be a function of what `ready` resolves to, because
|
|
421
|
+
// the servers to switch off are read from the app-server before the start.
|
|
702
422
|
.then((settled) => {
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
// sent, so a worker killed during the handshake still launched a thread,
|
|
706
|
-
// still started whatever MCP processes that thread carries, and still
|
|
707
|
-
// installed a watcher -- all for a run the supervisor had already given
|
|
708
|
-
// up on. A quit kills the whole fleet at once, so this is the ordinary
|
|
709
|
-
// case rather than a corner one.
|
|
423
|
+
// Check for a kill before every request, so a worker killed during the
|
|
424
|
+
// handshake starts no thread and no MCP servers.
|
|
710
425
|
if (ended) return null;
|
|
711
426
|
const params = typeof threadParams === 'function' ? threadParams(settled) : threadParams;
|
|
712
|
-
//
|
|
713
|
-
// gets her own MCP servers back and no store at all (measured; see
|
|
714
|
-
// `resumeThreadParams`), and a reply is the commonest thing this app does.
|
|
427
|
+
// A resume needs the same params as a start. See `resumeThreadParams`.
|
|
715
428
|
return resumeThreadId
|
|
716
429
|
? server.resumeThread(resumeThreadParams(resumeThreadId, params), handlers).then(() => ({ threadId: resumeThreadId }))
|
|
717
430
|
: server.startThread(params, handlers);
|
|
@@ -719,12 +432,8 @@ export function createCodexWorker({
|
|
|
719
432
|
.then((started) => {
|
|
720
433
|
if (!started) return null;
|
|
721
434
|
const threadId = started.threadId ?? resumeThreadId ?? null;
|
|
722
|
-
// A
|
|
723
|
-
//
|
|
724
|
-
// startup there was not one yet, so the watch this response just installed
|
|
725
|
-
// would have outlived the worker: every later line carrying that id
|
|
726
|
-
// handed to a listener for a session that no longer exists, with the
|
|
727
|
-
// transport kept alive holding it.
|
|
435
|
+
// A thread that arrives after a kill still needs its watch released,
|
|
436
|
+
// because `end` ran before there was a thread id to release.
|
|
728
437
|
if (ended) {
|
|
729
438
|
if (threadId) { try { server.unwatch(threadId); } catch { /* already gone */ } }
|
|
730
439
|
return null;
|
|
@@ -736,10 +445,8 @@ export function createCodexWorker({
|
|
|
736
445
|
.then((up) => {
|
|
737
446
|
if (ended) return null;
|
|
738
447
|
if (!up) {
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
// the whole point: the alternative is a worker doing the entire job and
|
|
742
|
-
// filing it nowhere.
|
|
448
|
+
// Stop before the turn rather than let a worker do the job with no way
|
|
449
|
+
// to write it to the store.
|
|
743
450
|
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`);
|
|
744
451
|
end(1, null);
|
|
745
452
|
return null;
|
|
@@ -754,40 +461,29 @@ export function createCodexWorker({
|
|
|
754
461
|
if (typeof id === 'string' && id) worker.turnId = id;
|
|
755
462
|
})
|
|
756
463
|
.catch((err) => {
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
//
|
|
760
|
-
// stderr tail, which main/codex-app-server.mjs appends to every rejection.
|
|
761
|
-
// No signal here: a kill has already ended this worker with one, and
|
|
762
|
-
// `end` only ever fires once, so anything reaching this line is a real
|
|
763
|
-
// failure rather than something we did.
|
|
464
|
+
// No thread, a refused resume, or a refused turn. The message includes the
|
|
465
|
+
// app-server's stderr tail. No signal: a kill has already ended this
|
|
466
|
+
// worker, and `end` fires only once.
|
|
764
467
|
say(err?.message ?? String(err));
|
|
765
468
|
end(1, null);
|
|
766
469
|
});
|
|
767
470
|
|
|
768
471
|
/**
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
* alone -- so a kill before the turn id has arrived can only release the
|
|
773
|
-
* thread, which it does. The exit is emitted immediately rather than waiting
|
|
774
|
-
* for the server to confirm: the supervisor kills the whole fleet on quit and
|
|
775
|
-
* on every restart, and an ending that waits on a process we are shutting
|
|
776
|
-
* down is an ending that may never come.
|
|
472
|
+
* Our kill. `turn/interrupt` needs both the thread and turn ids, so before
|
|
473
|
+
* the turn id arrives this only releases the thread. The exit fires at once
|
|
474
|
+
* rather than waiting for the server, which may be shutting down too.
|
|
777
475
|
*
|
|
778
|
-
* `signal` is set here
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
* because `end` only fires once.
|
|
476
|
+
* `signal` is set only here. `settleDelivery` reads it so our kill does not
|
|
477
|
+
* spend a delivery attempt, and a later "interrupted" turn cannot undo that
|
|
478
|
+
* because `end` fires once.
|
|
782
479
|
*/
|
|
783
480
|
worker.kill = () => {
|
|
784
481
|
if (ended) return false;
|
|
785
482
|
if (worker.threadId && worker.turnId) {
|
|
786
483
|
try { server.interruptTurn(worker.threadId, worker.turnId)?.catch?.(() => {}); } catch { /* dying anyway */ }
|
|
787
484
|
}
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
// the same pair of numbers for the same thing.
|
|
485
|
+
// The Claude CLI traps SIGTERM and exits 143, so both engines report a kill
|
|
486
|
+
// the same way.
|
|
791
487
|
end(143, 'SIGTERM');
|
|
792
488
|
return true;
|
|
793
489
|
};
|
|
@@ -796,30 +492,39 @@ export function createCodexWorker({
|
|
|
796
492
|
}
|
|
797
493
|
|
|
798
494
|
/**
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
* `
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
495
|
+
* A WORKER THAT WAS NEVER STARTED, AND SAYS WHY, in the shape `spawnWorker`
|
|
496
|
+
* holds: the sentence on stderr and an exit of 1, both on a later tick,
|
|
497
|
+
* because the supervisor attaches its listeners after this returns (the same
|
|
498
|
+
* reason `codexThreadParamsFor` throws inside the worker's chain rather than
|
|
499
|
+
* speaking early). No 'spawn' is emitted, so no delivery mark is written and
|
|
500
|
+
* no one-off permission mode is spent.
|
|
501
|
+
*/
|
|
502
|
+
export function refusedCodexWorker(sentence) {
|
|
503
|
+
const worker = new EventEmitter();
|
|
504
|
+
worker.stderr = new EventEmitter();
|
|
505
|
+
worker.threadId = null;
|
|
506
|
+
worker.turnId = null;
|
|
507
|
+
worker.steer = async () => { throw Error('The turn is not running.'); };
|
|
508
|
+
let ended = false;
|
|
509
|
+
worker.kill = () => false;
|
|
510
|
+
setImmediate(() => {
|
|
511
|
+
if (ended) return;
|
|
512
|
+
ended = true;
|
|
513
|
+
worker.stderr.emit('data', sentence);
|
|
514
|
+
worker.emit('exit', 1, null);
|
|
515
|
+
});
|
|
516
|
+
return worker;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Why a turn did not finish: the server's message, with the `codexErrorInfo`
|
|
521
|
+
* code in front when it is a plain string.
|
|
522
|
+
*
|
|
523
|
+
* shared/spawn-trouble.mjs classifies on that code (for example
|
|
524
|
+
* `usageLimitExceeded` or `unauthorized`), which cannot drift the way prose
|
|
525
|
+
* can; a signed-out run carries raw HTTP text, not Codex's words. The code goes
|
|
526
|
+
* first because `noteExitForBackoff` cuts the last stderr line to 300
|
|
527
|
+
* characters. Object variants and `other` are left out.
|
|
823
528
|
*/
|
|
824
529
|
function troubleText(turn) {
|
|
825
530
|
const why = String(turn?.error?.message ?? '').trim();
|