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
|
@@ -1,278 +1,176 @@
|
|
|
1
|
-
// The transport to `codex app-server`: one child process
|
|
2
|
-
//
|
|
1
|
+
// The transport to `codex app-server`: one child process for many work items.
|
|
2
|
+
// It speaks newline-delimited JSON-RPC 2.0 over stdio, and when a turn needs an
|
|
3
|
+
// approval the server sends us a request with an id that we must answer.
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// it stopped. That was the blocker, and `git show 258d71d^:main/codex.mjs` is
|
|
10
|
-
// worth reading for what else was tried, but ITS EVENT PARSING IS OBSOLETE and
|
|
11
|
-
// none of it is carried over here.
|
|
5
|
+
// main/supervisor.mjs spawns the process and holds the client,
|
|
6
|
+
// main/codex-session.mjs wraps one thread of it per work item, and
|
|
7
|
+
// main/codex-approvals.mjs is the `onApproval` handler. This file does
|
|
8
|
+
// transport and routing only. The transport is injected so the tests run
|
|
9
|
+
// without `codex` installed.
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
11
|
+
// Deny is the default in every failure mode, and silence is not a refusal:
|
|
12
|
+
// - A card waits for the person it is addressed to and is never denied on a
|
|
13
|
+
// clock; main/codex-approvals.mjs holds the whole argument. It is settled by
|
|
14
|
+
// their answer, by the worker that raised it stopping, or by the app closing,
|
|
15
|
+
// and a late answer is dropped, because one request id cannot be answered
|
|
16
|
+
// twice.
|
|
17
|
+
// - A write that fails, at once or in its callback, is a broken connection.
|
|
18
|
+
// Every waiter is settled and the child is killed.
|
|
19
|
+
// - A stdout line that will not parse may have been an approval request, so it
|
|
20
|
+
// is fatal. Tracing goes to stderr, and blank lines are skipped.
|
|
18
21
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// sleep, and a late answer arriving after it has already denied is DROPPED --
|
|
38
|
-
// the turn was told no and has moved on, and a second answer to one request id
|
|
39
|
-
// is a protocol violation on top of an approval nobody is waiting for.
|
|
40
|
-
//
|
|
41
|
-
// A REFUSAL THAT CANNOT BE WRITTEN IS SILENCE. So a write that throws, a write
|
|
42
|
-
// that fails afterwards in its callback, and an error raised on the pipe itself
|
|
43
|
-
// are all treated as a broken connection: settle everything, tell every thread,
|
|
44
|
-
// take the child down. There is no way to answer a request over a dead pipe,
|
|
45
|
-
// and a supervisor can see a dead worker. It cannot see a hung one.
|
|
46
|
-
//
|
|
47
|
-
// A FRAME WE CANNOT READ MIGHT HAVE BEEN A CARD. stdout is the protocol here --
|
|
48
|
-
// the child's tracing goes to stderr, measured, and the live run read zero
|
|
49
|
-
// unreadable lines. A line on stdout that will not parse is therefore not noise
|
|
50
|
-
// to step over: it is a frame whose id has just been lost, and if it was an
|
|
51
|
-
// approval request then the turn that raised it waits forever. Nothing can tell
|
|
52
|
-
// whether it was, which is exactly why tolerating it is unsafe. Unreadable or
|
|
53
|
-
// unbounded input is fatal, loudly. Blank lines are framing rather than content
|
|
54
|
-
// and are the one thing here that may be skipped.
|
|
55
|
-
//
|
|
56
|
-
// ------------------------------------------------------------------------
|
|
57
|
-
// WHAT WAS MEASURED, ON THIS MAC, 2026-09-04, codex-cli 0.148.0. Some of this
|
|
58
|
-
// contradicts what was written down before; where it does, the machine wins.
|
|
59
|
-
//
|
|
60
|
-
// THE THREAD ID IS AT `result.thread.id`. Off a live `thread/start` response:
|
|
61
|
-
// `result.thread.id` was "01a068f1-be4d-7701-98e3-16137d16dcdf" and
|
|
62
|
-
// `result.threadId` was `undefined`. The obvious guess fails silently -- every
|
|
63
|
-
// thread ends up filed under the id `undefined`, which is one shared bucket --
|
|
64
|
-
// so it is asserted rather than trusted.
|
|
65
|
-
//
|
|
66
|
-
// SERVER REQUEST IDS START AT 0 AND ARE A SEPARATE NUMBER SPACE FROM OURS. In
|
|
67
|
-
// the captured session the first approval carried `id: 0` while our own
|
|
68
|
-
// `initialize` carried `id: 1`. A classifier written `if (msg.id)` drops the
|
|
69
|
-
// first approval of every session, because 0 is falsy. Everything below tests
|
|
70
|
-
// `!== undefined`, and the two spaces never share a map.
|
|
71
|
-
//
|
|
72
|
-
// A REFUSAL IS ACCEPTED EVEN WHEN THE SERVER DID NOT OFFER IT. The live
|
|
73
|
-
// approval arrived with `availableDecisions: ["accept",
|
|
74
|
-
// {acceptWithExecpolicyAmendment: ...}, "cancel"]` -- no "decline" in the list.
|
|
75
|
-
// Answering `{decision: 'decline'}` anyway worked: the item completed with
|
|
76
|
-
// `status: "declined"`, `serverRequest/resolved` fired, and the turn COMPLETED
|
|
77
|
-
// 4.0 seconds later. `availableDecisions` is a hint about which buttons a UI
|
|
78
|
-
// should draw, not a whitelist the answer is validated against. That is the
|
|
79
|
-
// measurement that makes an unconditional refusal safe, and without it the
|
|
80
|
-
// deny-by-default rule above would have been a guess.
|
|
81
|
-
//
|
|
82
|
-
// A JSON-RPC ERROR FRAME REFUSES TOO, MEASURED ON ONE METHOD. The same
|
|
83
|
-
// `item/commandExecution/requestApproval` answered with `{error: {code: -32001,
|
|
84
|
-
// message}}` left the item at `status: "failed"` and the turn still COMPLETED,
|
|
85
|
-
// 5.5 seconds later. BE PRECISE ABOUT THE SCOPE OF THAT: it was measured for
|
|
86
|
-
// command approval and for nothing else, so using it as the refusal for other
|
|
87
|
-
// server requests is a REASONED EXTENSION rather than a measured fact. It is
|
|
88
|
-
// the best one available -- a well-formed error is the only answer that is
|
|
89
|
-
// valid for a result shape nobody has measured -- but the next slice to lean on
|
|
90
|
-
// it for a specific method should measure that method.
|
|
91
|
-
//
|
|
92
|
-
// NO HEAD-OF-LINE BLOCKING. Proven separately: an approval on thread A that is
|
|
93
|
-
// deliberately never answered does not stop thread B from raising its own
|
|
94
|
-
// approval, having it answered, and completing its turn. That is what makes one
|
|
95
|
-
// process for the whole fleet safe, and it is a promise this file has to keep --
|
|
96
|
-
// hence the read loop below never awaits anything.
|
|
97
|
-
//
|
|
98
|
-
// `ephemeral: false` IS THE DEFAULT AND HAS TO BE. `thread/resume` fails with
|
|
99
|
-
// "no rollout found for thread id" on an ephemeral thread, so a thread started
|
|
100
|
-
// ephemerally cannot carry a continuation. the app's whole reply path is
|
|
101
|
-
// continuations, so the default is the one that can come back.
|
|
102
|
-
//
|
|
103
|
-
// TWO THINGS THE WRITTEN BRIEF HAD WRONG, both from the CLI's own
|
|
104
|
-
// `codex app-server generate-json-schema` dump:
|
|
105
|
-
//
|
|
106
|
-
// `abort` IS NOT A v2 APPROVAL DECISION. CommandExecutionApprovalDecision and
|
|
107
|
-
// FileChangeApprovalDecision are accept / acceptForSession / decline / cancel,
|
|
108
|
-
// plus two amendment OBJECTS (`acceptWithExecpolicyAmendment` and
|
|
109
|
-
// `applyNetworkPolicyAmendment`, command approvals only). `abort` belongs to
|
|
110
|
-
// the LEGACY `ReviewDecision` used by `execCommandApproval` and
|
|
111
|
-
// `applyPatchApproval`. Sending it to a v2 approval is a malformed answer, so
|
|
112
|
-
// it is treated as junk here.
|
|
113
|
-
//
|
|
114
|
-
// `item/permissions/requestApproval` TAKES NO `decision` AT ALL. Its response
|
|
115
|
-
// is PermissionsRequestApprovalResponse: `{ permissions, scope?,
|
|
116
|
-
// strictAutoReview? }`, a granted profile rather than a word.
|
|
117
|
-
//
|
|
118
|
-
// AND THERE ARE TEN SERVER-TO-CLIENT REQUEST METHODS IN THE STABLE SCHEMA, NOT
|
|
119
|
-
// THREE. Beyond the three approvals: item/tool/requestUserInput, item/tool/call,
|
|
120
|
-
// mcpServer/elicitation/request, account/chatgptAuthTokens/refresh,
|
|
121
|
-
// attestation/generate, and the legacy applyPatchApproval and
|
|
122
|
-
// execCommandApproval, both of which are still in the stable dump. The
|
|
123
|
-
// experimental schema adds more (currentTime/read), so "ten" is a fact about
|
|
124
|
-
// the stable dump on 0.148.0 and not a ceiling. Every one of them hangs
|
|
125
|
-
// whatever raised it if it is left open, so ANY server request this file does
|
|
126
|
-
// not know how to answer gets the error frame. That is the honest answer and
|
|
127
|
-
// not a placeholder: refusing `account/chatgptAuthTokens/refresh` may well cost
|
|
128
|
-
// a long-lived process its token refresh, and the slice that wants that
|
|
129
|
-
// capability is the slice that gets to measure the shape and answer it
|
|
130
|
-
// properly. A refusal is a bug somebody can see; silence is not.
|
|
131
|
-
//
|
|
132
|
-
// ------------------------------------------------------------------------
|
|
133
|
-
// SCOPE. Transport and routing, nothing else. Nothing here turns an event into
|
|
134
|
-
// Agentbox's work lines or its trace, nothing here touches the approvals spool,
|
|
135
|
-
// and nothing here decides which engine a row runs on.
|
|
136
|
-
//
|
|
137
|
-
// IT IS NO LONGER INERT, AND THIS HEADER SAID IT WAS FOR A WHOLE SLICE AFTER IT
|
|
138
|
-
// HAD STOPPED BEING ONE. Three files depend on it now: main/supervisor.mjs spawns the
|
|
139
|
-
// process and holds the client, main/codex-session.mjs is the per-work-item
|
|
140
|
-
// facade over one thread of it, and main/codex-approvals.mjs is the handler on
|
|
141
|
-
// the other end of `onApproval` that this file was written around. The three
|
|
142
|
-
// sentences above still hold; the fourth was a fact about a Tuesday.
|
|
143
|
-
//
|
|
144
|
-
// The transport is injected so this whole file can be tested without `codex`
|
|
145
|
-
// being installed. That is not tidiness. Four tests in this suite already fail
|
|
146
|
-
// on any machine whose clock or installs differ from hers, and a transport test
|
|
147
|
-
// that shelled out to the real binary would be the fifth.
|
|
22
|
+
// Protocol facts, measured on codex-cli 0.148.0:
|
|
23
|
+
// - The thread id is at `result.thread.id`; `result.threadId` is undefined.
|
|
24
|
+
// - Server request ids start at 0 and are a separate number space from ours,
|
|
25
|
+
// so ids are tested with `!== undefined`.
|
|
26
|
+
// - `decline` is accepted even when `availableDecisions` does not list it,
|
|
27
|
+
// which is what makes an unconditional refusal safe.
|
|
28
|
+
// - An error frame also refuses a command approval. That was measured for that
|
|
29
|
+
// method only; other server requests get it because it is the one answer
|
|
30
|
+
// valid for any result shape.
|
|
31
|
+
// - An open approval on one thread does not block the others, as long as the
|
|
32
|
+
// read loop never awaits.
|
|
33
|
+
// - `thread/resume` fails on an ephemeral thread, so `ephemeral` defaults to
|
|
34
|
+
// false.
|
|
35
|
+
// - `abort` is a word of the legacy `ReviewDecision`, not a v2 approval
|
|
36
|
+
// decision.
|
|
37
|
+
// - `item/permissions/requestApproval` takes a granted profile, not a decision.
|
|
38
|
+
// - Any server request not in APPROVAL_SHAPE gets the error frame, because one
|
|
39
|
+
// left open hangs whatever raised it.
|
|
148
40
|
|
|
149
41
|
import { StringDecoder } from 'node:string_decoder';
|
|
150
42
|
import { NAME, Name, nameSlug } from '../shared/product-name.mjs';
|
|
151
43
|
|
|
152
44
|
/**
|
|
153
|
-
* How this client names itself to the server. It
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* constant -- and it is what appears in Codex's own session records, so it says
|
|
157
|
-
* this app rather than the name of whatever spike wrote it. READ, NEVER TYPED:
|
|
158
|
-
* that measured line is why, it is a record written months ago under a name the
|
|
159
|
-
* app no longer has.
|
|
45
|
+
* How this client names itself to the server. It appears in `userAgent` and in
|
|
46
|
+
* Codex's own session records, so it comes from shared/product-name.mjs rather
|
|
47
|
+
* than being typed here.
|
|
160
48
|
*/
|
|
161
49
|
export const CLIENT_INFO = Object.freeze({ name: nameSlug, title: NAME, version: '1.0.0' });
|
|
162
50
|
|
|
163
51
|
/**
|
|
164
|
-
* The
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* amendment objects, which widen the execpolicy or pin a network rule for
|
|
169
|
-
* future commands. Those are standing policy changes rather than an answer to
|
|
170
|
-
* the question that was asked, they cannot be expressed as a word, and nothing
|
|
171
|
-
* in Agentbox can produce one yet -- so a handler returning one is refused. That
|
|
172
|
-
* is conservative in the safe direction, and it is a deliberate omission rather
|
|
173
|
-
* than an oversight.
|
|
174
|
-
*
|
|
175
|
-
* `abort` is NOT here and reads like it should be. It is a legacy word.
|
|
52
|
+
* The scalar answers a v2 approval takes, from the CLI's schema dump. Command
|
|
53
|
+
* approvals also take two amendment objects, which are standing policy changes
|
|
54
|
+
* rather than an answer, so a handler returning one is refused. `abort` is a
|
|
55
|
+
* legacy word and is not here.
|
|
176
56
|
*/
|
|
177
57
|
export const APPROVAL_DECISIONS = Object.freeze(['accept', 'acceptForSession', 'decline', 'cancel']);
|
|
178
58
|
|
|
179
59
|
/**
|
|
180
|
-
* The
|
|
181
|
-
*
|
|
182
|
-
* has no `acceptForSession`, so an approval that lasts a session cannot be
|
|
183
|
-
* expressed here at all. Read off the CLI's schema dump 2026-09-22.
|
|
60
|
+
* The actions an MCP elicitation takes. `McpServerElicitationRequestResponse`
|
|
61
|
+
* has no `acceptForSession`.
|
|
184
62
|
*/
|
|
185
63
|
export const ELICITATION_ACTIONS = Object.freeze(['accept', 'decline', 'cancel']);
|
|
186
64
|
|
|
187
65
|
/**
|
|
188
|
-
* The refusal. `decline` and
|
|
189
|
-
*
|
|
190
|
-
* else, while `cancel` also interrupts the whole turn. A failure to reach the
|
|
191
|
-
* founder should cost the command, not the work item.
|
|
66
|
+
* The refusal. `decline` lets the agent carry on and try something else, while
|
|
67
|
+
* `cancel` also interrupts the whole turn.
|
|
192
68
|
*/
|
|
193
69
|
export const REFUSAL_DECISION = 'decline';
|
|
194
70
|
|
|
195
|
-
/** Her deadline, matching main/approval-prompt-server.mjs exactly. */
|
|
196
|
-
export const APPROVAL_TIMEOUT_MS = 15 * 60_000;
|
|
197
|
-
|
|
198
71
|
/**
|
|
199
|
-
*
|
|
200
|
-
* because `turn/diff/updated` carries
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* IT APPLIES TO A TERMINATED LINE AS WELL AS TO A RUNAWAY ONE, and it did not.
|
|
205
|
-
* The check ran on the residual buffer AFTER every newline-delimited frame had
|
|
206
|
-
* been taken out of it, so it could only ever see a line with no end. A single
|
|
207
|
-
* complete ten-megabyte line -- which is precisely what an enormous
|
|
208
|
-
* `turn/diff/updated` is -- went straight past the bound to `JSON.parse` and on
|
|
209
|
-
* to the handlers. Both halves are checked now: `read` refuses an oversized
|
|
210
|
-
* line before it parses it, and the residual check still catches the one that
|
|
211
|
-
* never terminates.
|
|
72
|
+
* The longest frame allowed before the connection is called corrupt. Large,
|
|
73
|
+
* because `turn/diff/updated` carries the whole turn's diff. Bounded, because
|
|
74
|
+
* an unterminated line would otherwise grow until the app dies. `read` checks
|
|
75
|
+
* each complete line, and the data handler checks the unterminated rest.
|
|
212
76
|
*/
|
|
213
77
|
export const MAX_FRAME_LENGTH = 16 * 1024 * 1024;
|
|
214
78
|
|
|
215
79
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* THAT SHAPE, and the ones that get a worker as far as raising an approval --
|
|
222
|
-
* `initialize`, `config/read`, `thread/start`, `thread/resume`, `turn/start` --
|
|
223
|
-
* had nothing to settle them at all. A live app-server that accepts the bytes
|
|
224
|
-
* and never answers holds a work item and a fleet slot for the rest of the
|
|
225
|
-
* session; the pipe is not broken, so nothing else here fires either.
|
|
226
|
-
*
|
|
227
|
-
* Two minutes, which is far above anything measured (a handshake and a
|
|
228
|
-
* `config/read` answer in milliseconds, and `turn/start` resolves when the turn
|
|
229
|
-
* is ACCEPTED rather than when it finishes) and far below a founder noticing a
|
|
230
|
-
* row is stuck. The turn itself is not on this clock; only the request that
|
|
231
|
-
* starts it is.
|
|
232
|
-
*
|
|
233
|
-
* ONE REQUEST GOES, NOT THE CONNECTION. A rejection frees the row and the slot,
|
|
234
|
-
* which is the whole failure being closed, and the worker's own chain turns it
|
|
235
|
-
* into a run that says why on stderr. Taking the shared process down over one
|
|
236
|
-
* slow answer would charge every other thread for it.
|
|
80
|
+
* How long an ordinary request may go unanswered. A live server that accepts a
|
|
81
|
+
* request and never answers would otherwise hold a work item forever, since the
|
|
82
|
+
* pipe is not broken. `turn/start` resolves when the turn is accepted, so the
|
|
83
|
+
* turn itself is not on this clock. A timeout rejects that one request and
|
|
84
|
+
* leaves the connection up for the other threads.
|
|
237
85
|
*/
|
|
238
86
|
export const REQUEST_TIMEOUT_MS = 2 * 60_000;
|
|
239
87
|
|
|
240
88
|
/**
|
|
241
|
-
* The server requests we
|
|
242
|
-
*
|
|
243
|
-
* `mcpServer/elicitation/request` IS AN APPROVAL AND NOT ONLY A FORM, which is
|
|
244
|
-
* the correction this table carries. It is how Codex asks whether an MCP TOOL
|
|
245
|
-
* CALL may run. Measured on this Mac 2026-09-22 against codex-cli 0.153.4, a
|
|
246
|
-
* thread started exactly as `workerThreadParams` starts one, with one stdio MCP
|
|
247
|
-
* server on it and the model asked to call its single tool:
|
|
89
|
+
* The server requests we answer, and the shape each answer takes.
|
|
248
90
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* _meta: { codex_approval_kind: "mcp_tool_call",
|
|
254
|
-
* persist: ["session","always"],
|
|
255
|
-
* tool_description: "...", tool_params: {} } }
|
|
256
|
-
*
|
|
257
|
-
* The method was not on this table, so it took the error frame below, and the
|
|
258
|
-
* measured consequence was the whole integration rather than one lost call:
|
|
259
|
-
*
|
|
260
|
-
* answered with the error frame -> item/completed mcpToolCall status=failed,
|
|
261
|
-
* error "user rejected MCP tool call"
|
|
262
|
-
* answered `{action:"accept"}` -> the tool ran and returned its result
|
|
263
|
-
*
|
|
264
|
-
* EVERY STORE TOOL A CODEX WORKER HAS IS AN MCP TOOL CALL, so the first row is
|
|
265
|
-
* a worker that cannot claim, checkpoint or finish anything, and the only words
|
|
266
|
-
* anybody sees are that sentence: a rejection nobody made, over a card that was
|
|
267
|
-
* never drawn. `McpServerElicitationRequestResponse` is `{ action: "accept" |
|
|
268
|
-
* "decline" | "cancel", content?, _meta? }`, which is a third answer shape, and
|
|
269
|
-
* its vocabulary happens to be three of the words the decision shape speaks.
|
|
91
|
+
* `mcpServer/elicitation/request` is how Codex asks whether an MCP tool call
|
|
92
|
+
* may run (`_meta.codex_approval_kind: "mcp_tool_call"`), and every store tool
|
|
93
|
+
* is one. Refused with the error frame, the call fails as "user rejected MCP
|
|
94
|
+
* tool call", so it must reach the handler. Its answer is `{ action }`.
|
|
270
95
|
*/
|
|
271
96
|
const APPROVAL_SHAPE = new Map([
|
|
272
97
|
['item/commandExecution/requestApproval', 'decision'],
|
|
273
98
|
['item/fileChange/requestApproval', 'decision'],
|
|
274
99
|
['item/permissions/requestApproval', 'grant'],
|
|
275
100
|
['mcpServer/elicitation/request', 'elicitation'],
|
|
101
|
+
// THE SAME TWO APPROVALS UNDER THEIR v1 NAMES (w-36c51443df). These are the
|
|
102
|
+
// older spellings of the first two rows, and they used to fall through to a
|
|
103
|
+
// protocol error -- so a Codex speaking v1 on any path would have had every
|
|
104
|
+
// command refused rather than carded. Their answer is `{ decision:
|
|
105
|
+
// ReviewDecision }` and that vocabulary is NOT the v2 one, which is why they
|
|
106
|
+
// get a shape of their own rather than sharing `decision`. See
|
|
107
|
+
// LEGACY_DECISIONS.
|
|
108
|
+
['execCommandApproval', 'legacy'],
|
|
109
|
+
['applyPatchApproval', 'legacy'],
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* THE v1 WORD FOR EACH OF OUR FOUR, read off `ExecCommandApprovalResponse.json`
|
|
114
|
+
* in the 0.160.0 schema dump, 2026-10-09.
|
|
115
|
+
*
|
|
116
|
+
* `ReviewDecision` is `approved` | `approved_for_session` |
|
|
117
|
+
* `approved_mcp_policy_amendment` | `timed_out` | `abort` as bare words, plus
|
|
118
|
+
* `{denied:{rejection}}` and two amendment objects. NOTE THAT `decline` IS NOT
|
|
119
|
+
* IN IT: forwarding our own v2 word would have been refused by the server, and
|
|
120
|
+
* the refusal is an OBJECT here rather than a word, which is exactly the kind of
|
|
121
|
+
* difference that a shared mapping hides. The amendments are left out for the
|
|
122
|
+
* reason APPROVAL_DECISIONS gives: they are standing policy changes rather than
|
|
123
|
+
* an answer to the question asked, and nothing here can produce one.
|
|
124
|
+
*/
|
|
125
|
+
export const LEGACY_DECISIONS = Object.freeze({
|
|
126
|
+
accept: 'approved',
|
|
127
|
+
acceptForSession: 'approved_for_session',
|
|
128
|
+
cancel: 'abort',
|
|
129
|
+
// The one place a refusal gets to say why, so it says why.
|
|
130
|
+
decline: { denied: { rejection: `${Name} did not approve this. Carry on with what you are already allowed to do and leave the rest for her to look at.` } },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* EVERY REQUEST THE SERVER MAY SEND A CLIENT, so the list lives somewhere a
|
|
135
|
+
* reader can check it against the schema rather than inferring it from which
|
|
136
|
+
* branch happens to catch what.
|
|
137
|
+
*
|
|
138
|
+
* Read off every `method` const in `ServerRequest.json`, codex-cli 0.160.0,
|
|
139
|
+
* 2026-10-09. Six raise a card (APPROVAL_SHAPE above) and four cannot be served
|
|
140
|
+
* at all (CANNOT_SERVE below). A method in neither list is NEW, and `onUnknownRequest`
|
|
141
|
+
* exists so that it is news rather than a silence.
|
|
142
|
+
*/
|
|
143
|
+
export const SERVER_REQUESTS = Object.freeze([
|
|
144
|
+
'account/chatgptAuthTokens/refresh',
|
|
145
|
+
'applyPatchApproval',
|
|
146
|
+
'attestation/generate',
|
|
147
|
+
'execCommandApproval',
|
|
148
|
+
'item/commandExecution/requestApproval',
|
|
149
|
+
'item/fileChange/requestApproval',
|
|
150
|
+
'item/permissions/requestApproval',
|
|
151
|
+
'item/tool/call',
|
|
152
|
+
'item/tool/requestUserInput',
|
|
153
|
+
'mcpServer/elicitation/request',
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* THE FOUR THAT ARE REFUSED ON PURPOSE, each with the reason in its own words.
|
|
158
|
+
*
|
|
159
|
+
* They used to share one anonymous error with the approvals, which is how
|
|
160
|
+
* nobody noticed they existed. All four ask the CLIENT to do something rather
|
|
161
|
+
* than asking a person to decide something, and there is no answer here to
|
|
162
|
+
* invent for any of them.
|
|
163
|
+
*/
|
|
164
|
+
const CANNOT_SERVE = new Map([
|
|
165
|
+
['item/tool/call', `it asks this app to run a tool of its own, and ${Name} offers the model no such tool`],
|
|
166
|
+
['account/chatgptAuthTokens/refresh', `it asks this app to mint a fresh ChatGPT login token, which ${Name} does not hold and must not forge`],
|
|
167
|
+
['attestation/generate', `it asks this app to sign an attestation, which ${Name} cannot produce`],
|
|
168
|
+
// THE ONE THAT IS A QUESTION FOR A PERSON, and the one worth coming back to.
|
|
169
|
+
// Its answer is a map of question id to typed answers
|
|
170
|
+
// (`ToolRequestUserInputResponse`), Agentbox has no form to put on screen, and a
|
|
171
|
+
// question may be flagged `isSecret` -- so an invented answer would be worse
|
|
172
|
+
// than a no. Pointing the model at its reply is the road that does reach her.
|
|
173
|
+
['item/tool/requestUserInput', `it asks a question that needs a typed answer and ${Name} has no way to put that question on screen; ask it in your reply instead, where she will read it`],
|
|
276
174
|
]);
|
|
277
175
|
|
|
278
176
|
const REFUSAL_ERROR = Object.freeze({
|
|
@@ -280,6 +178,46 @@ const REFUSAL_ERROR = Object.freeze({
|
|
|
280
178
|
message: `${Name} did not approve this request. Continue with what you are already allowed to do, and leave the rest for the founder to look at.`,
|
|
281
179
|
});
|
|
282
180
|
|
|
181
|
+
/** A refusal that names what was asked, so it is one somebody can act on. */
|
|
182
|
+
const cannotServe = (method, why) => Object.freeze({
|
|
183
|
+
code: -32001,
|
|
184
|
+
message: `${Name} cannot answer ${method}: ${why}.`,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const UNKNOWN_REQUEST = `${Name} does not recognise this request, so it is refused rather than guessed at`;
|
|
188
|
+
|
|
189
|
+
// Refuse in the method's own vocabulary where it has one. A declined
|
|
190
|
+
// elicitation leaves the turn running; the error frame is a protocol failure.
|
|
191
|
+
const refusal = (shape) => {
|
|
192
|
+
if (shape === 'decision') return { result: { decision: REFUSAL_DECISION } };
|
|
193
|
+
if (shape === 'elicitation') return { result: { action: REFUSAL_DECISION } };
|
|
194
|
+
// v1 SAYS NO WITH AN OBJECT, NOT A WORD. `decline` is not in its
|
|
195
|
+
// vocabulary at all, so a timed-out or unowned legacy card answered in our
|
|
196
|
+
// own word would be a protocol error on a question the server is waiting on.
|
|
197
|
+
if (shape === 'legacy') return { result: { decision: LEGACY_DECISIONS.decline } };
|
|
198
|
+
return { error: { ...REFUSAL_ERROR } };
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** This client's yes to `method`, without the JSON-RPC envelope, or null for a request a yes does not answer. */
|
|
202
|
+
export function yesTo(method) {
|
|
203
|
+
const shape = APPROVAL_SHAPE.get(method);
|
|
204
|
+
if (shape === 'decision') return { result: { decision: 'accept' } };
|
|
205
|
+
if (shape === 'elicitation') return { result: { action: 'accept' } };
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The no this client sends `method` when nobody can be asked, without the
|
|
211
|
+
* JSON-RPC envelope: for the cloud server driving a Codex run with the
|
|
212
|
+
* computer shut (cloud/gateway/phone.mjs), so it refuses in these words.
|
|
213
|
+
*/
|
|
214
|
+
export function refusalTo(method) {
|
|
215
|
+
const cannot = CANNOT_SERVE.get(method);
|
|
216
|
+
if (cannot) return { error: cannotServe(method, cannot) };
|
|
217
|
+
const shape = APPROVAL_SHAPE.get(method);
|
|
218
|
+
return shape ? refusal(shape) : { error: cannotServe(method, UNKNOWN_REQUEST) };
|
|
219
|
+
}
|
|
220
|
+
|
|
283
221
|
const plain = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
284
222
|
const only = (keys, allowed) => keys.every((k) => allowed.includes(k));
|
|
285
223
|
|
|
@@ -288,22 +226,22 @@ const only = (keys, allowed) => keys.every((k) => allowed.includes(k));
|
|
|
288
226
|
*
|
|
289
227
|
* Give it either a `transport` (anything child-process-shaped: `stdin.write`,
|
|
290
228
|
* `stdin.end`, `stdout.on('data')`, an optional `stderr`, `on('exit')`,
|
|
291
|
-
* `on('error')`, `kill`) or a `spawn` function that returns one.
|
|
292
|
-
* passes a real child; the tests pass a notepad.
|
|
229
|
+
* `on('error')`, `kill`) or a `spawn` function that returns one.
|
|
293
230
|
*
|
|
294
|
-
* `onNotification`
|
|
295
|
-
*
|
|
296
|
-
* thread at all. There are eighteen of those in the 0.148.0 stable schema --
|
|
297
|
-
* account, config, process, filesystem and remote-control news, among them
|
|
298
|
-
* `account/rateLimits/updated` and `remoteControl/status/changed` -- so this is
|
|
299
|
-
* an ordinary channel rather than an error path.
|
|
231
|
+
* `onNotification` gets every line for no watched thread, including the many
|
|
232
|
+
* account and config notifications that carry no thread at all.
|
|
300
233
|
*/
|
|
301
234
|
export function createCodexAppServer({
|
|
302
235
|
transport = null,
|
|
303
236
|
spawn = null,
|
|
304
237
|
clientInfo = CLIENT_INFO,
|
|
305
238
|
onNotification = null,
|
|
306
|
-
|
|
239
|
+
// A SERVER REQUEST THIS FILE HAS NEVER HEARD OF (w-36c51443df). Six known
|
|
240
|
+
// methods once shared one anonymous refusal, so a NEW one would have been
|
|
241
|
+
// indistinguishable from them and from nothing at all. Handed up rather than
|
|
242
|
+
// logged here, because what to do with it -- a line on the row, a report --
|
|
243
|
+
// belongs to the caller.
|
|
244
|
+
onUnknownRequest = null,
|
|
307
245
|
requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
308
246
|
maxFrameLength = MAX_FRAME_LENGTH,
|
|
309
247
|
stderrLimit = 64 * 1024,
|
|
@@ -313,7 +251,7 @@ export function createCodexAppServer({
|
|
|
313
251
|
|
|
314
252
|
const pending = new Map(); // OUR request id -> { resolve, reject, method, onResult }
|
|
315
253
|
const threads = new Map(); // thread id -> handlers
|
|
316
|
-
const openCards = new
|
|
254
|
+
const openCards = new Set(); // THEIR request ids, each waiting on a person
|
|
317
255
|
const decoder = new StringDecoder('utf8');
|
|
318
256
|
let nextId = 1;
|
|
319
257
|
let buffer = '';
|
|
@@ -321,20 +259,13 @@ export function createCodexAppServer({
|
|
|
321
259
|
let gone = null; // null while live; the closing reason once not
|
|
322
260
|
let drained = false; // stdout has ended, so the diagnostics are complete
|
|
323
261
|
let finished = false;
|
|
324
|
-
|
|
325
|
-
/* ------------------------------- writing -------------------------------- */
|
|
262
|
+
let resumeLeavesTurns = true; // false once this Codex refuses `excludeTurns`
|
|
326
263
|
|
|
327
264
|
/**
|
|
328
|
-
* Put one message on the wire, or take the connection down.
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* or it raises `error` later. The callback is the one that is easy to miss
|
|
333
|
-
* and the one that matters most, because a refusal that was "written" and
|
|
334
|
-
* never arrived is indistinguishable from an approval.
|
|
335
|
-
*
|
|
336
|
-
* A `false` return is NOT a failure: that is ordinary backpressure and the
|
|
337
|
-
* bytes are still queued. Only the callback and the throw are failures.
|
|
265
|
+
* Put one message on the wire, or take the connection down. A pipe can fail
|
|
266
|
+
* by throwing or in the write callback (or later with `error`). The callback
|
|
267
|
+
* matters most: a refusal that never arrived looks the same as no answer. A
|
|
268
|
+
* `false` return is only backpressure, not a failure.
|
|
338
269
|
*/
|
|
339
270
|
function write(msg) {
|
|
340
271
|
if (gone) return false;
|
|
@@ -358,8 +289,7 @@ export function createCodexAppServer({
|
|
|
358
289
|
if (gone) return Promise.reject(new Error(`${method}: codex app-server is no longer running (${gone.reason})`));
|
|
359
290
|
const id = nextId++;
|
|
360
291
|
return new Promise((resolve, reject) => {
|
|
361
|
-
//
|
|
362
|
-
// and then never answered cannot outlive it. See REQUEST_TIMEOUT_MS.
|
|
292
|
+
// Armed before the write. See REQUEST_TIMEOUT_MS.
|
|
363
293
|
const timer = setTimeout(() => {
|
|
364
294
|
if (!pending.delete(id)) return;
|
|
365
295
|
reject(new Error(`${method}: codex app-server accepted this request and never answered it (${Math.round(requestTimeoutMs / 1000)}s)`));
|
|
@@ -376,11 +306,7 @@ export function createCodexAppServer({
|
|
|
376
306
|
|
|
377
307
|
const notify = (method) => write({ jsonrpc: '2.0', method });
|
|
378
308
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// The decoder rather than `chunk.toString`: a chunk boundary can fall in the
|
|
382
|
-
// middle of a UTF-8 sequence, and agent messages stream through here one
|
|
383
|
-
// delta at a time in whatever language the repo is written in.
|
|
309
|
+
// A StringDecoder, because a chunk can end in the middle of a UTF-8 sequence.
|
|
384
310
|
child.stdout.on('data', (chunk) => {
|
|
385
311
|
if (gone) return;
|
|
386
312
|
buffer += typeof chunk === 'string' ? chunk : decoder.write(chunk);
|
|
@@ -395,13 +321,8 @@ export function createCodexAppServer({
|
|
|
395
321
|
}
|
|
396
322
|
});
|
|
397
323
|
|
|
398
|
-
// The child
|
|
399
|
-
//
|
|
400
|
-
// measurements above. Worth keeping for a cause report, and NEVER worth
|
|
401
|
-
// parsing: nothing on this pipe is protocol, however badly formed it looks,
|
|
402
|
-
// so garbage here must not cost the founder a fleet. Only the tail is kept,
|
|
403
|
-
// because a long run's tracing is unbounded and the useful part is always at
|
|
404
|
-
// the end.
|
|
324
|
+
// The child's tracing. Its last lines usually say why something failed, so
|
|
325
|
+
// the tail is kept for cause reports. It is not protocol and is never parsed.
|
|
405
326
|
child.stderr?.on('data', (chunk) => {
|
|
406
327
|
stderrText += typeof chunk === 'string' ? chunk : chunk.toString();
|
|
407
328
|
if (stderrText.length > stderrLimit) stderrText = stderrText.slice(-stderrLimit);
|
|
@@ -415,9 +336,7 @@ export function createCodexAppServer({
|
|
|
415
336
|
for (const event of ['end', 'close']) child.stdout.on?.(event, () => { drained = true; if (gone) finish(); });
|
|
416
337
|
|
|
417
338
|
function read(line) {
|
|
418
|
-
//
|
|
419
|
-
// split can only ever see a frame with no newline in it, so a single
|
|
420
|
-
// complete frame of any size walked past it into `JSON.parse`. See
|
|
339
|
+
// The data handler's check only sees an unterminated line. See
|
|
421
340
|
// MAX_FRAME_LENGTH.
|
|
422
341
|
if (line.length > maxFrameLength) {
|
|
423
342
|
return fail('protocol', { note: `a frame of ${line.length} characters passed the ${maxFrameLength} this connection allows` });
|
|
@@ -435,8 +354,7 @@ export function createCodexAppServer({
|
|
|
435
354
|
return fail('protocol', { note: 'a line on stdout could not be read as a message, and it may have been an approval request' });
|
|
436
355
|
}
|
|
437
356
|
|
|
438
|
-
//
|
|
439
|
-
// server's ids start at zero.
|
|
357
|
+
// `!== undefined`, because the server's ids start at zero.
|
|
440
358
|
if (msg.id !== undefined && msg.method !== undefined) return serverRequest(msg);
|
|
441
359
|
if (msg.id !== undefined) return response(msg);
|
|
442
360
|
if (typeof msg.method === 'string') return notification(msg);
|
|
@@ -452,11 +370,9 @@ export function createCodexAppServer({
|
|
|
452
370
|
waiting.reject(new Error(`codex app-server refused ${waiting.method}: ${msg.error.message ?? JSON.stringify(msg.error)}`));
|
|
453
371
|
return;
|
|
454
372
|
}
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
// resolved promise would miss any notification the server packed in behind
|
|
459
|
-
// the response -- which for `thread/start` is the thread's own first event.
|
|
373
|
+
// `onResult` runs before the next line is read. Registering a thread off
|
|
374
|
+
// the resolved promise would miss notifications sent right behind the
|
|
375
|
+
// response, such as the thread's first event.
|
|
460
376
|
try { waiting.onResult?.(msg.result); } catch { /* the caller's bookkeeping, not ours */ }
|
|
461
377
|
waiting.resolve(msg.result);
|
|
462
378
|
}
|
|
@@ -468,94 +384,60 @@ export function createCodexAppServer({
|
|
|
468
384
|
else call(() => onNotification?.(msg.method, msg.params));
|
|
469
385
|
}
|
|
470
386
|
|
|
471
|
-
/* ------------------------------- approvals ------------------------------ */
|
|
472
|
-
|
|
473
387
|
/**
|
|
474
|
-
* Answer a server request.
|
|
475
|
-
*
|
|
476
|
-
* other thread on this process reads through the same loop.
|
|
388
|
+
* Answer a server request. Never awaits the handler, because one card waits
|
|
389
|
+
* for a person and every thread reads through this loop.
|
|
477
390
|
*/
|
|
478
391
|
function serverRequest(msg) {
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
// `openCards` is keyed by the server's request id, so a duplicate used to
|
|
482
|
-
// overwrite the entry without clearing the timer it replaced. The first
|
|
483
|
-
// card's deadline then went on running and denied the SECOND card at its
|
|
484
|
-
// own hour, and the first card's real answer -- her click -- landed on a map
|
|
485
|
-
// entry that had already been swapped and was dropped.
|
|
486
|
-
//
|
|
487
|
-
// There is no way to answer two open requests that share one id: whichever
|
|
488
|
-
// we write, the server matches it to one of them and the other hangs. That
|
|
489
|
-
// is the same unresolvable input as an unreadable frame, so it gets the same
|
|
490
|
-
// answer this file gives every one of those. Loud, and with `finish` denying
|
|
491
|
-
// every card that was open, so nothing is left waiting on a decision it can
|
|
492
|
-
// never receive.
|
|
392
|
+
// Two open requests on one id cannot both be answered, so treat it like an
|
|
393
|
+
// unreadable frame and end the connection.
|
|
493
394
|
if (openCards.has(msg.id)) {
|
|
494
395
|
return fail('protocol', { note: `codex app-server raised a second request on id ${msg.id} while the first was still open, and one answer cannot settle two` });
|
|
495
396
|
}
|
|
397
|
+
// THREE ROADS NOW, AND A REQUEST TAKES ONE OF THEM BY DECISION (w-36c51443df).
|
|
398
|
+
//
|
|
399
|
+
// There used to be two: a card, or one anonymous error for everything else.
|
|
400
|
+
// Six of the ten requests the schema allows took the second, with nothing
|
|
401
|
+
// anywhere naming which -- so the only way to learn that
|
|
402
|
+
// `account/chatgptAuthTokens/refresh` was being refused was to read the
|
|
403
|
+
// schema by hand. Now a refusal says what it refused, and a method in
|
|
404
|
+
// neither table is REPORTED, because a new one arriving in silence is how
|
|
405
|
+
// this lasted.
|
|
406
|
+
const cannot = CANNOT_SERVE.get(msg.method);
|
|
407
|
+
if (cannot) return write({ jsonrpc: '2.0', id: msg.id, error: cannotServe(msg.method, cannot) });
|
|
408
|
+
|
|
496
409
|
const shape = APPROVAL_SHAPE.get(msg.method);
|
|
497
|
-
if (!shape)
|
|
410
|
+
if (!shape) {
|
|
411
|
+
call(() => onUnknownRequest?.(msg.method, msg.params));
|
|
412
|
+
return write({ jsonrpc: '2.0', id: msg.id, error: cannotServe(msg.method, UNKNOWN_REQUEST) });
|
|
413
|
+
}
|
|
498
414
|
|
|
499
415
|
const id = threadOf(msg.params);
|
|
500
416
|
const handler = id === null ? null : threads.get(id)?.onApproval;
|
|
501
|
-
//
|
|
502
|
-
// nobody to ask, so the turn hears no now rather than in fifteen minutes.
|
|
417
|
+
// Nobody to ask, so refuse now rather than at the deadline.
|
|
503
418
|
if (typeof handler !== 'function') return write({ jsonrpc: '2.0', id: msg.id, ...refusal(shape) });
|
|
504
419
|
|
|
505
|
-
|
|
420
|
+
openCards.add(msg.id);
|
|
506
421
|
Promise.resolve()
|
|
507
422
|
.then(() => handler(msg.method, msg.params))
|
|
508
423
|
.then((answered) => answerCard(msg.id, translate(shape, answered) ?? refusal(shape)))
|
|
509
424
|
.catch(() => answerCard(msg.id, refusal(shape)));
|
|
510
425
|
}
|
|
511
426
|
|
|
512
|
-
/** Start her deadline. Expiry denies, once, and closes the card. */
|
|
513
|
-
function openCard(id, shape) {
|
|
514
|
-
const timer = setTimeout(() => {
|
|
515
|
-
if (openCards.delete(id)) write({ jsonrpc: '2.0', id, ...refusal(shape) });
|
|
516
|
-
}, approvalTimeoutMs);
|
|
517
|
-
timer?.unref?.();
|
|
518
|
-
openCards.set(id, timer);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
427
|
/**
|
|
522
|
-
* The
|
|
523
|
-
*
|
|
524
|
-
* A card the deadline has already denied is CLOSED, so her late click lands
|
|
525
|
-
* here and is dropped rather than approving something the turn stopped
|
|
526
|
-
* waiting for twenty minutes ago. Same for a card whose connection died.
|
|
428
|
+
* The only answer a card gets. A card on a dead connection is already closed,
|
|
429
|
+
* so a late answer is dropped.
|
|
527
430
|
*/
|
|
528
431
|
function answerCard(id, body) {
|
|
529
|
-
if (!openCards.
|
|
530
|
-
clearTimeout(openCards.get(id));
|
|
531
|
-
openCards.delete(id);
|
|
432
|
+
if (!openCards.delete(id)) return;
|
|
532
433
|
write({ jsonrpc: '2.0', id, ...body });
|
|
533
434
|
}
|
|
534
435
|
|
|
535
|
-
// A REFUSAL IS SAID IN THE VOCABULARY THE METHOD TAKES, which for an
|
|
536
|
-
// elicitation is `decline` rather than an error frame. Both reach the model
|
|
537
|
-
// as a no; the difference is that a declined elicitation leaves the tool call
|
|
538
|
-
// declined and the turn running, while the error frame is a protocol failure
|
|
539
|
-
// on a request the server expected an answer to.
|
|
540
|
-
const refusal = (shape) => {
|
|
541
|
-
if (shape === 'decision') return { result: { decision: REFUSAL_DECISION } };
|
|
542
|
-
if (shape === 'elicitation') return { result: { action: REFUSAL_DECISION } };
|
|
543
|
-
return { error: { ...REFUSAL_ERROR } };
|
|
544
|
-
};
|
|
545
|
-
|
|
546
436
|
/**
|
|
547
|
-
* The handler's answer
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* The elicitation's three actions are `accept`, `decline` and `cancel`, read
|
|
552
|
-
* off `McpServerElicitationRequestResponse` in the CLI's own schema dump on
|
|
553
|
-
* 2026-09-22. `acceptForSession` is NOT among them, so a handler that answers
|
|
554
|
-
* a tool call with the word a command approval uses is refused rather than
|
|
555
|
-
* quietly turned into a yes. `content` is omitted: the accept this app sends
|
|
556
|
-
* answers a permission question whose `requestedSchema` has no properties,
|
|
557
|
-
* and a form that really does ask for typed input is not something Agentbox can
|
|
558
|
-
* fill in for her.
|
|
437
|
+
* The handler's answer in the shape the method takes, or null, which means
|
|
438
|
+
* refuse. An elicitation has no `acceptForSession`, so that word is refused
|
|
439
|
+
* rather than turned into a yes. `content` is never sent, because Agentbox
|
|
440
|
+
* only answers elicitations that ask for no input.
|
|
559
441
|
*/
|
|
560
442
|
function translate(shape, answered) {
|
|
561
443
|
if (shape === 'decision') {
|
|
@@ -564,27 +446,24 @@ export function createCodexAppServer({
|
|
|
564
446
|
if (shape === 'elicitation') {
|
|
565
447
|
return ELICITATION_ACTIONS.includes(answered) ? { result: { action: answered } } : null;
|
|
566
448
|
}
|
|
449
|
+
if (shape === 'legacy') {
|
|
450
|
+
// Our word in, Codex's older word out. A word that is not one of our four
|
|
451
|
+
// answers null, which lands on the deny-by-default rule exactly as it
|
|
452
|
+
// does for every other shape.
|
|
453
|
+
const said = Object.hasOwn(LEGACY_DECISIONS, answered) ? LEGACY_DECISIONS[answered] : null;
|
|
454
|
+
return said ? { result: { decision: said } } : null;
|
|
455
|
+
}
|
|
567
456
|
const grant = grantFrom(answered);
|
|
568
457
|
return grant ? { result: grant } : null;
|
|
569
458
|
}
|
|
570
459
|
|
|
571
460
|
/**
|
|
572
|
-
* A permissions grant,
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
* returns the second time. So nothing the handler returned reaches the wire
|
|
579
|
-
* by reference, every value is read exactly once, and an unknown key anywhere
|
|
580
|
-
* -- on the object, in the profile, or in either area -- is a refusal rather
|
|
581
|
-
* than something to pass along. Inherited properties do not count, so a grant
|
|
582
|
-
* on a prototype is not a grant.
|
|
583
|
-
*
|
|
584
|
-
* The `entries` form of the filesystem profile is deliberately NOT accepted.
|
|
585
|
-
* Its shape has three nested variants, none of them measured, and a grant
|
|
586
|
-
* this file cannot check is a grant it has no business sending. The slice
|
|
587
|
-
* that needs it measures it.
|
|
461
|
+
* A permissions grant, checked field by field and built fresh. This answer
|
|
462
|
+
* widens a sandbox, so nothing the handler returned is sent by reference: a
|
|
463
|
+
* getter or `toJSON` could give JSON.stringify a different value from the one
|
|
464
|
+
* checked. Any unknown key is a refusal, and inherited properties do not
|
|
465
|
+
* count. The `entries` form of the filesystem profile is refused because its
|
|
466
|
+
* shape is unmeasured.
|
|
588
467
|
*/
|
|
589
468
|
function grantFrom(answered) {
|
|
590
469
|
if (!plain(answered)) return null;
|
|
@@ -626,8 +505,7 @@ export function createCodexAppServer({
|
|
|
626
505
|
}
|
|
627
506
|
|
|
628
507
|
const grant = { permissions };
|
|
629
|
-
// `turn
|
|
630
|
-
// grant that outlives the question it answered is not that answer.
|
|
508
|
+
// Default to `turn`, the narrower scope.
|
|
631
509
|
if (top.includes('scope')) {
|
|
632
510
|
if (answered.scope !== 'turn' && answered.scope !== 'session') return null;
|
|
633
511
|
grant.scope = answered.scope;
|
|
@@ -641,9 +519,8 @@ export function createCodexAppServer({
|
|
|
641
519
|
return grant;
|
|
642
520
|
}
|
|
643
521
|
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
// first event and therefore the worst one to lose.
|
|
522
|
+
// Most lines carry `params.threadId`, but `thread/started` has
|
|
523
|
+
// `params.thread.id`.
|
|
647
524
|
function threadOf(params) {
|
|
648
525
|
if (!plain(params)) return null;
|
|
649
526
|
if (typeof params.threadId === 'string') return params.threadId;
|
|
@@ -651,17 +528,14 @@ export function createCodexAppServer({
|
|
|
651
528
|
return null;
|
|
652
529
|
}
|
|
653
530
|
|
|
654
|
-
// A listener that throws must not
|
|
655
|
-
// bad handler would otherwise stop every other thread on this process.
|
|
531
|
+
// A listener that throws must not stop the read loop for every other thread.
|
|
656
532
|
function call(fn) {
|
|
657
533
|
try { fn(); } catch { /* the caller's problem, and not worth the fleet */ }
|
|
658
534
|
}
|
|
659
535
|
|
|
660
|
-
/* ------------------------------ the ending ------------------------------ */
|
|
661
|
-
|
|
662
536
|
/** Why this connection ended, in a sentence that names something actionable. */
|
|
663
|
-
function cause() {
|
|
664
|
-
const tail = stderrText.trim().split('\n').slice(-3).join(' ').trim();
|
|
537
|
+
function cause({ withTail = true } = {}) {
|
|
538
|
+
const tail = withTail ? stderrText.trim().split('\n').slice(-3).join(' ').trim() : '';
|
|
665
539
|
const said = tail ? `: ${tail}` : '';
|
|
666
540
|
if (!gone) return `the connection to codex app-server failed on write${said}`;
|
|
667
541
|
switch (gone.reason) {
|
|
@@ -674,14 +548,9 @@ export function createCodexAppServer({
|
|
|
674
548
|
}
|
|
675
549
|
|
|
676
550
|
/**
|
|
677
|
-
* The connection is over,
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
* accepted and no late approval can be written. Finished once stdout has
|
|
681
|
-
* drained, so the tracing tail that explains it is complete rather than
|
|
682
|
-
* whatever happened to have arrived by the moment `exit` fired -- exit
|
|
683
|
-
* routinely beats the last of the pipe, and "exited with code 1" on its own
|
|
684
|
-
* is not a cause anybody can act on.
|
|
551
|
+
* The connection is over. It is marked at once, so no further request or late
|
|
552
|
+
* approval is written, and finished once stdout drains, because `exit` often
|
|
553
|
+
* arrives before the last of the tracing that explains it.
|
|
685
554
|
*/
|
|
686
555
|
function fail(reason, detail = {}) {
|
|
687
556
|
if (gone) return;
|
|
@@ -698,21 +567,17 @@ export function createCodexAppServer({
|
|
|
698
567
|
}
|
|
699
568
|
|
|
700
569
|
/**
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
* in-flight request rejects rather than staying pending, and every watched
|
|
705
|
-
* thread is told rather than left listening to a dead pipe. Deadlines are
|
|
706
|
-
* cancelled and registrations dropped: a card that can never be answered and
|
|
707
|
-
* a thread that can never be spoken to are only holding this client alive.
|
|
570
|
+
* Settles everything once. Pending requests reject, watched threads get
|
|
571
|
+
* `onClosed`, and every timer and registration is dropped, so no work item is
|
|
572
|
+
* left waiting and nothing keeps the client alive.
|
|
708
573
|
*/
|
|
709
574
|
function finish() {
|
|
710
575
|
if (finished) return;
|
|
711
576
|
finished = true;
|
|
712
577
|
gone.stderr = stderrText;
|
|
578
|
+
gone.why = cause({ withTail: false });
|
|
713
579
|
const why = cause();
|
|
714
580
|
|
|
715
|
-
for (const timer of openCards.values()) clearTimeout(timer);
|
|
716
581
|
openCards.clear();
|
|
717
582
|
|
|
718
583
|
for (const [id, waiting] of pending) {
|
|
@@ -725,8 +590,6 @@ export function createCodexAppServer({
|
|
|
725
590
|
for (const handlers of listening) call(() => handlers.onClosed?.(gone));
|
|
726
591
|
}
|
|
727
592
|
|
|
728
|
-
/* -------------------------------- the API ------------------------------- */
|
|
729
|
-
|
|
730
593
|
const watch = (threadId, handlers = {}) => { threads.set(threadId, handlers); return threadId; };
|
|
731
594
|
|
|
732
595
|
return {
|
|
@@ -734,15 +597,10 @@ export function createCodexAppServer({
|
|
|
734
597
|
request: (method, params) => send(method, params),
|
|
735
598
|
|
|
736
599
|
/**
|
|
737
|
-
* The
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* the documented lifecycle puts it before any other operation. 0.148
|
|
742
|
-
* tolerates its absence, which is why the first draft never noticed it was
|
|
743
|
-
* missing; a later one need not. It is sent from the response hook, so it
|
|
744
|
-
* can never overtake the response, and it is never sent at all if the
|
|
745
|
-
* server refused the handshake.
|
|
600
|
+
* The handshake: `initialize`, then the `initialized` notification once the
|
|
601
|
+
* server answers. The protocol wants it before any other call, though 0.148
|
|
602
|
+
* does not enforce that. Sent from the response hook, so it cannot overtake
|
|
603
|
+
* the response and is not sent if the server refuses.
|
|
746
604
|
*/
|
|
747
605
|
initialize: () => send('initialize', { clientInfo }, () => notify('initialized')),
|
|
748
606
|
|
|
@@ -760,27 +618,36 @@ export function createCodexAppServer({
|
|
|
760
618
|
).then((result) => ({ threadId: result?.thread?.id ?? null, thread: result?.thread ?? null, result })),
|
|
761
619
|
|
|
762
620
|
/**
|
|
763
|
-
* Continue a thread
|
|
764
|
-
*
|
|
621
|
+
* Continue a known thread. The watch goes on before the request, so no
|
|
622
|
+
* event of the resumed thread arrives unowned, and comes off if the server
|
|
623
|
+
* refuses the resume.
|
|
765
624
|
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
625
|
+
* Without `excludeTurns` the answer carries every turn in one frame, and a
|
|
626
|
+
* long thread passes `maxFrameLength`, which ends this connection for every
|
|
627
|
+
* thread on it. Codex before 0.155 refuses the field without the
|
|
628
|
+
* experimental API, so that Codex is asked again in full.
|
|
769
629
|
*/
|
|
770
630
|
resumeThread: (params = {}, handlers = {}) => {
|
|
771
631
|
const id = typeof params.threadId === 'string' ? params.threadId : null;
|
|
772
632
|
if (id) watch(id, handlers);
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
633
|
+
const lean = resumeLeavesTurns;
|
|
634
|
+
return send('thread/resume', lean ? { ...params, excludeTurns: true } : params)
|
|
635
|
+
.catch((err) => {
|
|
636
|
+
if (!lean || !/excludeTurns/.test(err?.message ?? '')) throw err;
|
|
637
|
+
resumeLeavesTurns = false;
|
|
638
|
+
return send('thread/resume', params);
|
|
639
|
+
})
|
|
640
|
+
.catch((err) => {
|
|
641
|
+
if (id && threads.get(id) === handlers) threads.delete(id);
|
|
642
|
+
throw err;
|
|
643
|
+
});
|
|
777
644
|
},
|
|
778
645
|
|
|
779
646
|
startTurn: (threadId, params = {}) => send('turn/start', { threadId, ...params }),
|
|
780
647
|
|
|
781
648
|
/** Both ids: a turn is not identified by either one alone. */
|
|
782
649
|
interruptTurn: (threadId, turnId) => send('turn/interrupt', { threadId, turnId }),
|
|
783
|
-
steerTurn: (threadId, expectedTurnId, text) => send('turn/steer', {threadId, expectedTurnId, input:[{type:'text',text}]}),
|
|
650
|
+
steerTurn: (threadId, expectedTurnId, text, pictures = []) => send('turn/steer', {threadId, expectedTurnId, input:[{type:'text',text}, ...pictures.map((p) => ({type:'localImage', path:p}))]}),
|
|
784
651
|
|
|
785
652
|
watch,
|
|
786
653
|
unwatch: (threadId) => threads.delete(threadId),
|