botmux 3.10.0 → 3.11.0
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/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/herdr-backend.d.ts +4 -3
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +11 -8
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/pty-backend.d.ts +1 -1
- package/dist/adapters/backend/pty-backend.d.ts.map +1 -1
- package/dist/adapters/backend/pty-backend.js +4 -1
- package/dist/adapters/backend/pty-backend.js.map +1 -1
- package/dist/adapters/backend/riff-backend.d.ts +29 -5
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +302 -31
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/sandbox.d.ts +6 -0
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +7 -0
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +4 -1
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +37 -3
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zellij-backend.d.ts +3 -3
- package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-backend.js +9 -3
- package/dist/adapters/backend/zellij-backend.js.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.d.ts +3 -3
- package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zellij-observe-backend.js +12 -7
- package/dist/adapters/backend/zellij-observe-backend.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +11 -2
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/read-isolation.d.ts +35 -2
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +88 -7
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/runner-input.d.ts +15 -14
- package/dist/adapters/cli/runner-input.d.ts.map +1 -1
- package/dist/adapters/cli/runner-input.js +64 -26
- package/dist/adapters/cli/runner-input.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +20 -5
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +24 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +110 -5
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/fleet-shutdown.d.ts +47 -0
- package/dist/cli/fleet-shutdown.d.ts.map +1 -0
- package/dist/cli/fleet-shutdown.js +387 -0
- package/dist/cli/fleet-shutdown.js.map +1 -0
- package/dist/cli/pm2-descriptor-guard.d.ts +19 -0
- package/dist/cli/pm2-descriptor-guard.d.ts.map +1 -0
- package/dist/cli/pm2-descriptor-guard.js +122 -0
- package/dist/cli/pm2-descriptor-guard.js.map +1 -0
- package/dist/cli/pm2-exact-start.d.ts +16 -0
- package/dist/cli/pm2-exact-start.d.ts.map +1 -0
- package/dist/cli/pm2-exact-start.js +63 -0
- package/dist/cli/pm2-exact-start.js.map +1 -0
- package/dist/cli/pm2-god-admission.d.ts +8 -0
- package/dist/cli/pm2-god-admission.d.ts.map +1 -0
- package/dist/cli/pm2-god-admission.js +25 -0
- package/dist/cli/pm2-god-admission.js.map +1 -0
- package/dist/cli/pm2-jlist.d.ts +13 -0
- package/dist/cli/pm2-jlist.d.ts.map +1 -0
- package/dist/cli/pm2-jlist.js +108 -0
- package/dist/cli/pm2-jlist.js.map +1 -0
- package/dist/cli/pm2-preflight.d.ts +10 -0
- package/dist/cli/pm2-preflight.d.ts.map +1 -0
- package/dist/cli/pm2-preflight.js +23 -0
- package/dist/cli/pm2-preflight.js.map +1 -0
- package/dist/cli/pm2-shutdown-capability.d.ts +27 -0
- package/dist/cli/pm2-shutdown-capability.d.ts.map +1 -0
- package/dist/cli/pm2-shutdown-capability.js +161 -0
- package/dist/cli/pm2-shutdown-capability.js.map +1 -0
- package/dist/cli/pm2-start-transaction.d.ts +55 -0
- package/dist/cli/pm2-start-transaction.d.ts.map +1 -0
- package/dist/cli/pm2-start-transaction.js +220 -0
- package/dist/cli/pm2-start-transaction.js.map +1 -0
- package/dist/cli/send-dispatch.d.ts +12 -0
- package/dist/cli/send-dispatch.d.ts.map +1 -1
- package/dist/cli/send-dispatch.js +31 -4
- package/dist/cli/send-dispatch.js.map +1 -1
- package/dist/cli/session-list-liveness.d.ts +1 -0
- package/dist/cli/session-list-liveness.d.ts.map +1 -1
- package/dist/cli/session-list-liveness.js +6 -0
- package/dist/cli/session-list-liveness.js.map +1 -1
- package/dist/cli/session-picker-layout.d.ts +26 -0
- package/dist/cli/session-picker-layout.d.ts.map +1 -0
- package/dist/cli/session-picker-layout.js +123 -0
- package/dist/cli/session-picker-layout.js.map +1 -0
- package/dist/cli/session-picker-viewport.d.ts +49 -0
- package/dist/cli/session-picker-viewport.d.ts.map +1 -0
- package/dist/cli/session-picker-viewport.js +46 -0
- package/dist/cli/session-picker-viewport.js.map +1 -0
- package/dist/cli/supervisor-shutdown-client.d.ts +31 -0
- package/dist/cli/supervisor-shutdown-client.d.ts.map +1 -0
- package/dist/cli/supervisor-shutdown-client.js +146 -0
- package/dist/cli/supervisor-shutdown-client.js.map +1 -0
- package/dist/cli/terminal-width.d.ts +48 -0
- package/dist/cli/terminal-width.d.ts.map +1 -0
- package/dist/cli/terminal-width.js +119 -0
- package/dist/cli/terminal-width.js.map +1 -0
- package/dist/cli/vc-agent.d.ts.map +1 -1
- package/dist/cli/vc-agent.js +1 -1
- package/dist/cli/vc-agent.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2269 -716
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +1630 -125
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/core/ask-api.d.ts +8 -1
- package/dist/core/ask-api.d.ts.map +1 -1
- package/dist/core/ask-api.js +18 -0
- package/dist/core/ask-api.js.map +1 -1
- package/dist/core/ask-broker.d.ts +20 -0
- package/dist/core/ask-broker.d.ts.map +1 -1
- package/dist/core/ask-broker.js +467 -47
- package/dist/core/ask-broker.js.map +1 -1
- package/dist/core/ask-persist-store.d.ts +126 -0
- package/dist/core/ask-persist-store.d.ts.map +1 -0
- package/dist/core/ask-persist-store.js +188 -0
- package/dist/core/ask-persist-store.js.map +1 -0
- package/dist/core/ask-types.d.ts +80 -0
- package/dist/core/ask-types.d.ts.map +1 -1
- package/dist/core/ask-types.js +62 -0
- package/dist/core/ask-types.js.map +1 -1
- package/dist/core/cli-session-id-publisher.d.ts +20 -0
- package/dist/core/cli-session-id-publisher.d.ts.map +1 -0
- package/dist/core/cli-session-id-publisher.js +21 -0
- package/dist/core/cli-session-id-publisher.js.map +1 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +427 -255
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts +13 -0
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +385 -26
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +9 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +842 -288
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/explicit-session-backing-cleanup.d.ts +49 -0
- package/dist/core/explicit-session-backing-cleanup.d.ts.map +1 -0
- package/dist/core/explicit-session-backing-cleanup.js +76 -0
- package/dist/core/explicit-session-backing-cleanup.js.map +1 -0
- package/dist/core/idle-worker-sweeper.d.ts +20 -0
- package/dist/core/idle-worker-sweeper.d.ts.map +1 -1
- package/dist/core/idle-worker-sweeper.js +18 -0
- package/dist/core/idle-worker-sweeper.js.map +1 -1
- package/dist/core/inflight-input-tracker.d.ts +10 -0
- package/dist/core/inflight-input-tracker.d.ts.map +1 -1
- package/dist/core/inflight-input-tracker.js +7 -0
- package/dist/core/inflight-input-tracker.js.map +1 -1
- package/dist/core/managed-origin-attestation.d.ts +51 -0
- package/dist/core/managed-origin-attestation.d.ts.map +1 -0
- package/dist/core/managed-origin-attestation.js +208 -0
- package/dist/core/managed-origin-attestation.js.map +1 -0
- package/dist/core/managed-origin-capability.d.ts +62 -3
- package/dist/core/managed-origin-capability.d.ts.map +1 -1
- package/dist/core/managed-origin-capability.js +419 -9
- package/dist/core/managed-origin-capability.js.map +1 -1
- package/dist/core/pending-repo-journal.d.ts +6 -0
- package/dist/core/pending-repo-journal.d.ts.map +1 -0
- package/dist/core/pending-repo-journal.js +85 -0
- package/dist/core/pending-repo-journal.js.map +1 -0
- package/dist/core/persistent-backend.d.ts +6 -1
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +12 -6
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/process-start-identity.d.ts +3 -0
- package/dist/core/process-start-identity.d.ts.map +1 -0
- package/dist/core/process-start-identity.js +62 -0
- package/dist/core/process-start-identity.js.map +1 -0
- package/dist/core/raw-command-writer.d.ts +34 -0
- package/dist/core/raw-command-writer.d.ts.map +1 -0
- package/dist/core/raw-command-writer.js +48 -0
- package/dist/core/raw-command-writer.js.map +1 -0
- package/dist/core/reply-target.d.ts +85 -22
- package/dist/core/reply-target.d.ts.map +1 -1
- package/dist/core/reply-target.js +267 -32
- package/dist/core/reply-target.js.map +1 -1
- package/dist/core/resource-monitor/runtime.d.ts.map +1 -1
- package/dist/core/resource-monitor/runtime.js +4 -1
- package/dist/core/resource-monitor/runtime.js.map +1 -1
- package/dist/core/restart-report.d.ts +6 -1
- package/dist/core/restart-report.d.ts.map +1 -1
- package/dist/core/restart-report.js +22 -4
- package/dist/core/restart-report.js.map +1 -1
- package/dist/core/riff-shutdown-detach.d.ts +164 -0
- package/dist/core/riff-shutdown-detach.d.ts.map +1 -0
- package/dist/core/riff-shutdown-detach.js +787 -0
- package/dist/core/riff-shutdown-detach.js.map +1 -0
- package/dist/core/riff-worker-shutdown-readiness.d.ts +17 -0
- package/dist/core/riff-worker-shutdown-readiness.d.ts.map +1 -0
- package/dist/core/riff-worker-shutdown-readiness.js +21 -0
- package/dist/core/riff-worker-shutdown-readiness.js.map +1 -0
- package/dist/core/session-manager.d.ts +6 -6
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1227 -594
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-marker.d.ts +1 -1
- package/dist/core/session-marker.d.ts.map +1 -1
- package/dist/core/session-marker.js +2 -2
- package/dist/core/session-marker.js.map +1 -1
- package/dist/core/session-mutation-guard.d.ts +17 -0
- package/dist/core/session-mutation-guard.d.ts.map +1 -0
- package/dist/core/session-mutation-guard.js +33 -0
- package/dist/core/session-mutation-guard.js.map +1 -0
- package/dist/core/shutdown-budgets.d.ts +27 -0
- package/dist/core/shutdown-budgets.d.ts.map +1 -0
- package/dist/core/shutdown-budgets.js +47 -0
- package/dist/core/shutdown-budgets.js.map +1 -0
- package/dist/core/supervisor-shutdown-ipc.d.ts +10 -0
- package/dist/core/supervisor-shutdown-ipc.d.ts.map +1 -0
- package/dist/core/supervisor-shutdown-ipc.js +10 -0
- package/dist/core/supervisor-shutdown-ipc.js.map +1 -0
- package/dist/core/supervisor-shutdown-protocol.d.ts +16 -0
- package/dist/core/supervisor-shutdown-protocol.d.ts.map +1 -0
- package/dist/core/supervisor-shutdown-protocol.js +16 -0
- package/dist/core/supervisor-shutdown-protocol.js.map +1 -0
- package/dist/core/trigger-final-suppression.d.ts +4 -0
- package/dist/core/trigger-final-suppression.d.ts.map +1 -1
- package/dist/core/trigger-final-suppression.js +6 -11
- package/dist/core/trigger-final-suppression.js.map +1 -1
- package/dist/core/trigger-session.d.ts +70 -0
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +923 -145
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +94 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +9 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +194 -13
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2597 -547
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +76 -2
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +2255 -773
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +20 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +24 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/feed-groups.d.ts +17 -0
- package/dist/dashboard/feed-groups.d.ts.map +1 -0
- package/dist/dashboard/feed-groups.js +75 -0
- package/dist/dashboard/feed-groups.js.map +1 -0
- package/dist/dashboard/session-card-model.d.ts +1 -1
- package/dist/dashboard/session-card-model.d.ts.map +1 -1
- package/dist/dashboard/session-card-model.js +8 -5
- package/dist/dashboard/session-card-model.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +4 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +139 -46
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +4 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +40 -3
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +95 -2
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +86 -40
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/kanban-model.js +1 -1
- package/dist/dashboard/web/kanban-model.js.map +1 -1
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js +3 -2
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/overview.d.ts +3 -0
- package/dist/dashboard/web/overview.d.ts.map +1 -1
- package/dist/dashboard/web/overview.js +2 -0
- package/dist/dashboard/web/overview.js.map +1 -1
- package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-kanban.js +2 -0
- package/dist/dashboard/web/sessions-kanban.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +103 -2
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +3 -2
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/ui.d.ts +8 -0
- package/dist/dashboard/web/ui.d.ts.map +1 -1
- package/dist/dashboard/web/ui.js +15 -0
- package/dist/dashboard/web/ui.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-GED4FEMS.js +10 -0
- package/dist/dashboard-web/chunks/chunk-6J5LS5AU.js +51 -0
- package/dist/dashboard-web/chunks/{chunk-ZU3UUKAR.js → chunk-HK7NIBJG.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-3UE7S77S.js → chunk-KRHWZEXU.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-VBXJOJFH.js +1 -0
- package/dist/dashboard-web/chunks/connectors-page-EELM66G3.js +1 -0
- package/dist/dashboard-web/chunks/groups-page-WKUV6E42.js +8 -0
- package/dist/dashboard-web/chunks/{insights-page-T2ZX2RWL.js → insights-page-54UBPPOC.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-YNVBAGR6.js → monitor-room-PORHVKRH.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-LHM55YWN.js → monitoring-page-XGA7AXNL.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-ATXVP7PW.js → office-page-AO3AFG45.js} +1 -1
- package/dist/dashboard-web/chunks/overview-page-F5BUTYR6.js +1 -0
- package/dist/dashboard-web/chunks/{roles-page-56VWM6CD.js → roles-page-M5GIASJU.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-S5K6U4EO.js → schedules-page-O232WQRR.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-YFRPSTIL.js +6 -0
- package/dist/dashboard-web/chunks/{settings-page-QUZDQARR.js → settings-page-OJAZLZQT.js} +1 -1
- package/dist/dashboard-web/chunks/{skills-page-IFY7AEND.js → skills-page-EFZQPWV4.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-C7FJQPFZ.js → team-federation-page-ZJVAP7HW.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-2LMF3WWR.js → v3-page-GYWTRGO2.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-3XC6QRMY.js → whiteboards-page-MSDAG6P7.js} +1 -1
- package/dist/dashboard-web/style.css +255 -0
- package/dist/dashboard.js +172 -7
- package/dist/dashboard.js.map +1 -1
- package/dist/desktop/renderer/app.js +21 -0
- package/dist/desktop/renderer/app.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +35 -9
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +35 -9
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/ask-card.d.ts +23 -0
- package/dist/im/lark/ask-card.d.ts.map +1 -1
- package/dist/im/lark/ask-card.js +94 -4
- package/dist/im/lark/ask-card.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +6 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +112 -15
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +496 -310
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +7 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +39 -26
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/doc-comment.d.ts +7 -4
- package/dist/im/lark/doc-comment.d.ts.map +1 -1
- package/dist/im/lark/doc-comment.js +19 -6
- package/dist/im/lark/doc-comment.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +16 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +88 -23
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/forward-followup-content.d.ts.map +1 -1
- package/dist/im/lark/forward-followup-content.js +9 -1
- package/dist/im/lark/forward-followup-content.js.map +1 -1
- package/dist/im/lark/grant-command.d.ts.map +1 -1
- package/dist/im/lark/grant-command.js +7 -4
- package/dist/im/lark/grant-command.js.map +1 -1
- package/dist/im/lark/md-card.d.ts +11 -0
- package/dist/im/lark/md-card.d.ts.map +1 -1
- package/dist/im/lark/md-card.js +46 -0
- package/dist/im/lark/md-card.js.map +1 -1
- package/dist/im/lark/message-parser.d.ts +23 -2
- package/dist/im/lark/message-parser.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +58 -3
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/im/lark/overview-card.d.ts.map +1 -1
- package/dist/im/lark/overview-card.js +2 -1
- package/dist/im/lark/overview-card.js.map +1 -1
- package/dist/services/async-trigger-state.d.ts +6 -1
- package/dist/services/async-trigger-state.d.ts.map +1 -1
- package/dist/services/async-trigger-state.js +21 -0
- package/dist/services/async-trigger-state.js.map +1 -1
- package/dist/services/async-trigger-store.d.ts +23 -1
- package/dist/services/async-trigger-store.d.ts.map +1 -1
- package/dist/services/async-trigger-store.js +96 -13
- package/dist/services/async-trigger-store.js.map +1 -1
- package/dist/services/bridge-fallback-gate.d.ts +40 -0
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +124 -24
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/codex-app-runner-protocol.d.ts +8 -0
- package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
- package/dist/services/codex-app-runner-protocol.js +9 -2
- package/dist/services/codex-app-runner-protocol.js.map +1 -1
- package/dist/services/codex-bridge-queue.d.ts +1 -0
- package/dist/services/codex-bridge-queue.d.ts.map +1 -1
- package/dist/services/codex-bridge-queue.js +1 -0
- package/dist/services/codex-bridge-queue.js.map +1 -1
- package/dist/services/codex-service-tier.d.ts +2 -0
- package/dist/services/codex-service-tier.d.ts.map +1 -1
- package/dist/services/codex-service-tier.js +1 -0
- package/dist/services/codex-service-tier.js.map +1 -1
- package/dist/services/codex-transcript.d.ts +27 -0
- package/dist/services/codex-transcript.d.ts.map +1 -1
- package/dist/services/codex-transcript.js +340 -2
- package/dist/services/codex-transcript.js.map +1 -1
- package/dist/services/grant-policy.d.ts +3 -0
- package/dist/services/grant-policy.d.ts.map +1 -1
- package/dist/services/grant-policy.js +5 -0
- package/dist/services/grant-policy.js.map +1 -1
- package/dist/services/grant-prefs-store.d.ts +6 -3
- package/dist/services/grant-prefs-store.d.ts.map +1 -1
- package/dist/services/grant-prefs-store.js +42 -12
- package/dist/services/grant-prefs-store.js.map +1 -1
- package/dist/services/hook-runner.d.ts +20 -1
- package/dist/services/hook-runner.d.ts.map +1 -1
- package/dist/services/hook-runner.js +17 -13
- package/dist/services/hook-runner.js.map +1 -1
- package/dist/services/idempotency-store.d.ts +134 -0
- package/dist/services/idempotency-store.d.ts.map +1 -0
- package/dist/services/idempotency-store.js +299 -0
- package/dist/services/idempotency-store.js.map +1 -0
- package/dist/services/jsonl-cursor.d.ts +7 -3
- package/dist/services/jsonl-cursor.d.ts.map +1 -1
- package/dist/services/jsonl-cursor.js +55 -31
- package/dist/services/jsonl-cursor.js.map +1 -1
- package/dist/services/open-platform-rename.d.ts.map +1 -1
- package/dist/services/open-platform-rename.js +1 -108
- package/dist/services/open-platform-rename.js.map +1 -1
- package/dist/services/restart-intent-store.d.ts +22 -2
- package/dist/services/restart-intent-store.d.ts.map +1 -1
- package/dist/services/restart-intent-store.js +160 -18
- package/dist/services/restart-intent-store.js.map +1 -1
- package/dist/services/sandbox-store.d.ts +22 -0
- package/dist/services/sandbox-store.d.ts.map +1 -1
- package/dist/services/sandbox-store.js +37 -8
- package/dist/services/sandbox-store.js.map +1 -1
- package/dist/services/send-policy.d.ts +41 -33
- package/dist/services/send-policy.d.ts.map +1 -1
- package/dist/services/send-policy.js +39 -40
- package/dist/services/send-policy.js.map +1 -1
- package/dist/services/session-store.d.ts +66 -0
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +403 -67
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/submit-notification.d.ts +3 -0
- package/dist/services/submit-notification.d.ts.map +1 -0
- package/dist/services/submit-notification.js +15 -0
- package/dist/services/submit-notification.js.map +1 -0
- package/dist/services/traex-transcript.d.ts +25 -2
- package/dist/services/traex-transcript.d.ts.map +1 -1
- package/dist/services/traex-transcript.js +133 -1
- package/dist/services/traex-transcript.js.map +1 -1
- package/dist/services/trigger-types.d.ts +13 -1
- package/dist/services/trigger-types.d.ts.map +1 -1
- package/dist/services/trigger-types.js +39 -0
- package/dist/services/trigger-types.js.map +1 -1
- package/dist/services/voice/index.d.ts +3 -2
- package/dist/services/voice/index.d.ts.map +1 -1
- package/dist/services/voice/index.js +5 -5
- package/dist/services/voice/index.js.map +1 -1
- package/dist/services/voice/openai.d.ts +2 -1
- package/dist/services/voice/openai.d.ts.map +1 -1
- package/dist/services/voice/openai.js +2 -1
- package/dist/services/voice/openai.js.map +1 -1
- package/dist/services/voice/sami.d.ts +5 -2
- package/dist/services/voice/sami.d.ts.map +1 -1
- package/dist/services/voice/sami.js +19 -4
- package/dist/services/voice/sami.js.map +1 -1
- package/dist/setup/bots-store.d.ts.map +1 -1
- package/dist/setup/bots-store.js +10 -4
- package/dist/setup/bots-store.js.map +1 -1
- package/dist/setup/lark-scopes.json +2 -0
- package/dist/setup/open-platform-automation.d.ts +9 -1
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +44 -15
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/setup/open-platform-visibility.d.ts +42 -0
- package/dist/setup/open-platform-visibility.d.ts.map +1 -0
- package/dist/setup/open-platform-visibility.js +133 -0
- package/dist/setup/open-platform-visibility.js.map +1 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -1
- package/dist/setup/verify-permissions.js +6 -0
- package/dist/setup/verify-permissions.js.map +1 -1
- package/dist/skills/definitions.js +2 -2
- package/dist/types.d.ts +290 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bot-routing.d.ts +1 -0
- package/dist/utils/bot-routing.d.ts.map +1 -1
- package/dist/utils/bot-routing.js +1 -1
- package/dist/utils/bot-routing.js.map +1 -1
- package/dist/utils/child-env.d.ts +1 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +9 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/codex-app-control.d.ts +438 -0
- package/dist/utils/codex-app-control.d.ts.map +1 -0
- package/dist/utils/codex-app-control.js +2336 -0
- package/dist/utils/codex-app-control.js.map +1 -0
- package/dist/utils/codex-app-dispatch-ledger.d.ts +78 -0
- package/dist/utils/codex-app-dispatch-ledger.d.ts.map +1 -0
- package/dist/utils/codex-app-dispatch-ledger.js +149 -0
- package/dist/utils/codex-app-dispatch-ledger.js.map +1 -0
- package/dist/utils/codex-app-turn-dispatch.d.ts +77 -0
- package/dist/utils/codex-app-turn-dispatch.d.ts.map +1 -0
- package/dist/utils/codex-app-turn-dispatch.js +141 -0
- package/dist/utils/codex-app-turn-dispatch.js.map +1 -0
- package/dist/utils/codex-app-turn-liveness.d.ts +125 -0
- package/dist/utils/codex-app-turn-liveness.d.ts.map +1 -0
- package/dist/utils/codex-app-turn-liveness.js +301 -0
- package/dist/utils/codex-app-turn-liveness.js.map +1 -0
- package/dist/utils/idle-detector.d.ts +12 -0
- package/dist/utils/idle-detector.d.ts.map +1 -1
- package/dist/utils/idle-detector.js +19 -0
- package/dist/utils/idle-detector.js.map +1 -1
- package/dist/utils/input-gate.d.ts +87 -0
- package/dist/utils/input-gate.d.ts.map +1 -1
- package/dist/utils/input-gate.js +86 -0
- package/dist/utils/input-gate.js.map +1 -1
- package/dist/utils/lark-upload.d.ts.map +1 -1
- package/dist/utils/lark-upload.js +52 -1
- package/dist/utils/lark-upload.js.map +1 -1
- package/dist/utils/pending-input-queue.d.ts +15 -0
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js +5 -1
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/utils/user-token.d.ts.map +1 -1
- package/dist/utils/user-token.js +53 -2
- package/dist/utils/user-token.js.map +1 -1
- package/dist/worker-ipc-preload.d.ts +12 -0
- package/dist/worker-ipc-preload.d.ts.map +1 -0
- package/dist/worker-ipc-preload.js +44 -0
- package/dist/worker-ipc-preload.js.map +1 -0
- package/dist/worker.js +2604 -325
- package/dist/worker.js.map +1 -1
- package/dist/workflows/v3/session-relay-client.d.ts +1 -0
- package/dist/workflows/v3/session-relay-client.d.ts.map +1 -1
- package/dist/workflows/v3/session-relay-client.js +12 -3
- package/dist/workflows/v3/session-relay-client.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-LTVOUDIS.js +0 -10
- package/dist/dashboard-web/chunks/chunk-DCS35UMD.js +0 -1
- package/dist/dashboard-web/chunks/chunk-MFQPCVHY.js +0 -51
- package/dist/dashboard-web/chunks/connectors-page-5KWDIMKC.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-FHOKYXMW.js +0 -8
- package/dist/dashboard-web/chunks/overview-page-QYI3KBDD.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-WBP4N7D2.js +0 -6
package/dist/codex-app-runner.js
CHANGED
|
@@ -1,16 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn, spawnSync } from 'node:child_process';
|
|
3
|
-
import {
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { createConnection } from 'node:net';
|
|
5
|
+
import { buildCodexAppTurnStartParams, isCleanInputCapabilityError, isCodexAppTurnInput, parseCodexVersion, supportsClientUserMessageId, } from './adapters/cli/codex-app-turn.js';
|
|
4
6
|
import { RunnerControlWriter } from './adapters/cli/runner-control-channel.js';
|
|
5
|
-
import {
|
|
6
|
-
import { CODEX_APP_INPUT_PREFIX, decodeCodexAppRunnerInput, } from './services/codex-app-runner-protocol.js';
|
|
7
|
+
import { CODEX_APP_CONTROL_BOOTSTRAP_ENV, CODEX_APP_CONTROL_FINAL_CHUNK_BYTES, CODEX_APP_CONTROL_FINAL_MAX_BYTES, CodexAppControlEndpointTracker, CodexAppControlLineDecoder, CodexAppControlRunnerHandshake, armCodexAppControlHandshakeTimeout, armCodexAppControlStartupTimeout, consumeCodexAppControlBootstrap, encodeCodexAppControlAuth, encodeCodexAppSignedControlMarker, parseCodexAppControlWireRecord, takeCodexAppControlLocatorEndpoint, } from './utils/codex-app-control.js';
|
|
7
8
|
import { TurnTokenUsageAccumulator, parseTokenUsagePair, } from './services/codex-app-token-usage.js';
|
|
8
9
|
const output = new RunnerControlWriter();
|
|
10
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
11
|
+
const RECONCILIATION_TIMEOUT_MS = 5_000;
|
|
12
|
+
const RECONCILIATION_PAGE_LIMIT = 3;
|
|
13
|
+
const RECONCILIATION_PAGE_SIZE = 50;
|
|
14
|
+
class AppServerRpcError extends Error {
|
|
15
|
+
method;
|
|
16
|
+
code;
|
|
17
|
+
data;
|
|
18
|
+
constructor(method, code, data, message) {
|
|
19
|
+
super(`${method}: ${message}`);
|
|
20
|
+
this.method = method;
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.data = data;
|
|
23
|
+
this.name = 'AppServerRpcError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class AppServerRequestTimeoutError extends Error {
|
|
27
|
+
method;
|
|
28
|
+
timeoutMs;
|
|
29
|
+
constructor(method, timeoutMs) {
|
|
30
|
+
super(`${method}: timed out after ${timeoutMs}ms; request acceptance is unknown`);
|
|
31
|
+
this.method = method;
|
|
32
|
+
this.timeoutMs = timeoutMs;
|
|
33
|
+
this.name = 'AppServerRequestTimeoutError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function asError(value) {
|
|
37
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
38
|
+
}
|
|
9
39
|
function parseArgs(argv) {
|
|
40
|
+
const controlBootstrapPath = process.env[CODEX_APP_CONTROL_BOOTSTRAP_ENV];
|
|
41
|
+
// app-server and every model-launched tool inherit process.env. Remove even
|
|
42
|
+
// the non-secret bootstrap path before either can start; private key material
|
|
43
|
+
// was never present in env/argv/layout.
|
|
44
|
+
delete process.env[CODEX_APP_CONTROL_BOOTSTRAP_ENV];
|
|
10
45
|
const out = {
|
|
11
46
|
sessionId: '',
|
|
12
47
|
codexBin: 'codex',
|
|
13
48
|
cwd: process.cwd(),
|
|
49
|
+
controlGeneration: '',
|
|
50
|
+
controlPrivateKey: undefined,
|
|
14
51
|
};
|
|
15
52
|
for (let i = 0; i < argv.length; i++) {
|
|
16
53
|
const key = argv[i];
|
|
@@ -54,11 +91,15 @@ function parseArgs(argv) {
|
|
|
54
91
|
}
|
|
55
92
|
if (!out.sessionId)
|
|
56
93
|
throw new Error('--session-id is required');
|
|
94
|
+
if (!controlBootstrapPath)
|
|
95
|
+
throw new Error(`${CODEX_APP_CONTROL_BOOTSTRAP_ENV} is required`);
|
|
96
|
+
const control = consumeCodexAppControlBootstrap(controlBootstrapPath, out.sessionId);
|
|
97
|
+
out.controlGeneration = control.generation;
|
|
98
|
+
out.controlPrivateKey = control.privateKey;
|
|
99
|
+
out.controlSocketPath = control.socketPath;
|
|
100
|
+
out.controlLocatorPath = control.locatorPath;
|
|
57
101
|
return out;
|
|
58
102
|
}
|
|
59
|
-
function emitMarker(kind, payload) {
|
|
60
|
-
output.marker(kind, payload);
|
|
61
|
-
}
|
|
62
103
|
function writeLine(text = '') {
|
|
63
104
|
output.line(text);
|
|
64
105
|
}
|
|
@@ -98,7 +139,6 @@ class AppServerClient {
|
|
|
98
139
|
pending = new Map();
|
|
99
140
|
notificationHandlers = [];
|
|
100
141
|
requestHandlers = [];
|
|
101
|
-
fatalHandlers = [];
|
|
102
142
|
lastStderr = '';
|
|
103
143
|
fatalError;
|
|
104
144
|
constructor(codexBin, cwd) {
|
|
@@ -110,7 +150,7 @@ class AppServerClient {
|
|
|
110
150
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
151
|
});
|
|
112
152
|
this.child.stdout.on('data', chunk => this.onStdout(chunk.toString('utf8')));
|
|
113
|
-
this.child.stdin.on('error', err => this.failAll(new
|
|
153
|
+
this.child.stdin.on('error', err => this.failAll(new Error(`Codex app-server stdin error: ${err.message}`)));
|
|
114
154
|
this.child.stderr.on('data', chunk => {
|
|
115
155
|
const text = chunk.toString('utf8');
|
|
116
156
|
this.lastStderr = (this.lastStderr + text).slice(-8000);
|
|
@@ -121,10 +161,10 @@ class AppServerClient {
|
|
|
121
161
|
const hint = err.code === 'ENOENT'
|
|
122
162
|
? '\nHint: install the Codex CLI, or set cliPathOverride to the Codex App bundled binary, for example /Applications/Codex.app/Contents/Resources/codex.'
|
|
123
163
|
: '';
|
|
124
|
-
this.failAll(new
|
|
164
|
+
this.failAll(new Error(`Failed to start Codex app-server with "${codexBin}": ${err.message}${hint}`));
|
|
125
165
|
});
|
|
126
166
|
this.child.on('exit', (code, signal) => {
|
|
127
|
-
const err = this.fatalError ?? new
|
|
167
|
+
const err = this.fatalError ?? new Error(`Codex app-server exited (code=${code}, signal=${signal})${this.lastStderr ? `\n${this.lastStderr}` : ''}`);
|
|
128
168
|
this.failAll(err);
|
|
129
169
|
});
|
|
130
170
|
}
|
|
@@ -134,28 +174,39 @@ class AppServerClient {
|
|
|
134
174
|
onRequest(handler) {
|
|
135
175
|
this.requestHandlers.push(handler);
|
|
136
176
|
}
|
|
137
|
-
|
|
138
|
-
this.fatalHandlers.push(handler);
|
|
139
|
-
if (this.fatalError)
|
|
140
|
-
handler(this.fatalError);
|
|
141
|
-
}
|
|
142
|
-
async initialize() {
|
|
177
|
+
async initialize(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
143
178
|
await this.request('initialize', {
|
|
144
179
|
clientInfo: { name: 'botmux-codex-app', version: '0.0.0' },
|
|
145
180
|
capabilities: { experimentalApi: true },
|
|
146
|
-
});
|
|
181
|
+
}, { timeoutMs });
|
|
147
182
|
this.notify('initialized');
|
|
148
183
|
}
|
|
149
|
-
request(method, params) {
|
|
184
|
+
request(method, params, options = {}) {
|
|
150
185
|
const id = this.nextId++;
|
|
151
186
|
return new Promise((resolve, reject) => {
|
|
152
|
-
|
|
187
|
+
const timeoutMs = options.timeoutMs;
|
|
188
|
+
const pending = { resolve, reject, method };
|
|
189
|
+
if (timeoutMs !== undefined) {
|
|
190
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
191
|
+
reject(new AppServerRequestTimeoutError(method, Math.max(0, timeoutMs)));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
pending.timer = setTimeout(() => {
|
|
195
|
+
if (!this.pending.delete(id))
|
|
196
|
+
return;
|
|
197
|
+
reject(new AppServerRequestTimeoutError(method, timeoutMs));
|
|
198
|
+
}, timeoutMs);
|
|
199
|
+
pending.timer.unref?.();
|
|
200
|
+
}
|
|
201
|
+
this.pending.set(id, pending);
|
|
153
202
|
try {
|
|
154
203
|
this.write({ jsonrpc: '2.0', id, method, params });
|
|
155
204
|
}
|
|
156
205
|
catch (err) {
|
|
157
|
-
|
|
158
|
-
|
|
206
|
+
this.pending.delete(id);
|
|
207
|
+
if (pending.timer)
|
|
208
|
+
clearTimeout(pending.timer);
|
|
209
|
+
reject(asError(err));
|
|
159
210
|
}
|
|
160
211
|
});
|
|
161
212
|
}
|
|
@@ -180,18 +231,14 @@ class AppServerClient {
|
|
|
180
231
|
this.child.stdin.write(JSON.stringify(msg) + '\n');
|
|
181
232
|
}
|
|
182
233
|
failAll(err) {
|
|
183
|
-
|
|
184
|
-
this.fatalError = this.fatalError ?? (err instanceof CodexAppTransportError
|
|
185
|
-
? err
|
|
186
|
-
: new CodexAppTransportError(err.message));
|
|
234
|
+
this.fatalError = this.fatalError ?? err;
|
|
187
235
|
const fatal = this.fatalError;
|
|
188
|
-
for (const pending of this.pending.values())
|
|
236
|
+
for (const pending of this.pending.values()) {
|
|
237
|
+
if (pending.timer)
|
|
238
|
+
clearTimeout(pending.timer);
|
|
189
239
|
pending.reject(fatal);
|
|
190
|
-
this.pending.clear();
|
|
191
|
-
if (firstFailure) {
|
|
192
|
-
for (const handler of this.fatalHandlers)
|
|
193
|
-
handler(fatal);
|
|
194
240
|
}
|
|
241
|
+
this.pending.clear();
|
|
195
242
|
}
|
|
196
243
|
onStdout(data) {
|
|
197
244
|
this.stdoutBuffer += data;
|
|
@@ -219,8 +266,11 @@ class AppServerClient {
|
|
|
219
266
|
if (!pending)
|
|
220
267
|
return;
|
|
221
268
|
this.pending.delete(msg.id);
|
|
222
|
-
if (
|
|
223
|
-
|
|
269
|
+
if (pending.timer)
|
|
270
|
+
clearTimeout(pending.timer);
|
|
271
|
+
if (msg.error) {
|
|
272
|
+
pending.reject(new AppServerRpcError(pending.method, typeof msg.error.code === 'number' ? msg.error.code : undefined, msg.error.data, typeof msg.error.message === 'string' ? msg.error.message : JSON.stringify(msg.error)));
|
|
273
|
+
}
|
|
224
274
|
else
|
|
225
275
|
pending.resolve(msg.result);
|
|
226
276
|
return;
|
|
@@ -247,15 +297,242 @@ catch (err) {
|
|
|
247
297
|
output.error(`${err?.message ?? err}\n`);
|
|
248
298
|
process.exit(2);
|
|
249
299
|
}
|
|
250
|
-
|
|
300
|
+
let controlSeq = 0;
|
|
301
|
+
let controlAckedSeq = 0;
|
|
302
|
+
let controlSentThrough = 0;
|
|
303
|
+
const controlQueue = [];
|
|
304
|
+
let controlFatal = false;
|
|
305
|
+
let controlSocket;
|
|
306
|
+
let controlChallenge;
|
|
307
|
+
let controlAccepted = false;
|
|
308
|
+
let controlAcceptanceCount = 0;
|
|
309
|
+
let controlReconnectTimer;
|
|
310
|
+
let resolveControlReady;
|
|
311
|
+
const controlReady = new Promise(resolve => { resolveControlReady = resolve; });
|
|
312
|
+
const CONTROL_QUEUE_MAX_RECORDS = 2_048;
|
|
313
|
+
const controlEndpoints = new CodexAppControlEndpointTracker();
|
|
314
|
+
function scheduleControlReconnect() {
|
|
315
|
+
if (controlFatal || controlReconnectTimer)
|
|
316
|
+
return;
|
|
317
|
+
controlReconnectTimer = setTimeout(() => {
|
|
318
|
+
controlReconnectTimer = undefined;
|
|
319
|
+
connectControlSocket();
|
|
320
|
+
}, 250);
|
|
321
|
+
}
|
|
322
|
+
function flushControlQueue() {
|
|
323
|
+
const socket = controlSocket;
|
|
324
|
+
const challenge = controlChallenge;
|
|
325
|
+
if (controlFatal || !socket || socket.destroyed || !controlAccepted || !challenge)
|
|
326
|
+
return;
|
|
327
|
+
try {
|
|
328
|
+
for (const marker of controlQueue) {
|
|
329
|
+
if (marker.seq <= controlSentThrough)
|
|
330
|
+
continue;
|
|
331
|
+
socket.write(`${encodeCodexAppSignedControlMarker(args.controlPrivateKey, args.sessionId, args.controlGeneration, challenge, marker.seq, marker.kind, marker.payload)}\n`);
|
|
332
|
+
controlSentThrough = marker.seq;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
controlFatal = true;
|
|
337
|
+
console.error(`Codex App control channel failed closed: ${err?.message ?? err}`);
|
|
338
|
+
process.exit(2);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function nextControlEndpoint() {
|
|
342
|
+
if (args.controlSocketPath)
|
|
343
|
+
return { endpoint: args.controlSocketPath };
|
|
344
|
+
if (!args.controlLocatorPath)
|
|
345
|
+
return undefined;
|
|
346
|
+
return takeCodexAppControlLocatorEndpoint({
|
|
347
|
+
locatorPath: args.controlLocatorPath,
|
|
348
|
+
sessionId: args.sessionId,
|
|
349
|
+
tracker: controlEndpoints,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
function connectControlSocket() {
|
|
353
|
+
if (controlFatal || (controlSocket && !controlSocket.destroyed))
|
|
354
|
+
return;
|
|
355
|
+
const target = nextControlEndpoint();
|
|
356
|
+
if (!target) {
|
|
357
|
+
scheduleControlReconnect();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// A never-accepted locator endpoint may retry with the existing 250ms
|
|
361
|
+
// reconnect backoff; the protected 256-bit locator epoch is still required
|
|
362
|
+
// before acceptance. Once accepted, its endpoint is permanently burned and
|
|
363
|
+
// only a newly published locator can be used.
|
|
364
|
+
const socket = createConnection(target.endpoint);
|
|
365
|
+
const handshakeTimer = armCodexAppControlHandshakeTimeout(() => {
|
|
366
|
+
socket.destroy(new Error('Codex App control endpoint handshake timed out'));
|
|
367
|
+
});
|
|
368
|
+
handshakeTimer.unref?.();
|
|
369
|
+
const decoder = new CodexAppControlLineDecoder();
|
|
370
|
+
const handshake = new CodexAppControlRunnerHandshake(args.sessionId, args.controlGeneration, target.epoch);
|
|
371
|
+
controlSocket = socket;
|
|
372
|
+
controlChallenge = undefined;
|
|
373
|
+
controlAccepted = false;
|
|
374
|
+
controlSentThrough = controlAckedSeq;
|
|
375
|
+
socket.setNoDelay(true);
|
|
376
|
+
socket.on('data', chunk => {
|
|
377
|
+
const decoded = decoder.push(chunk);
|
|
378
|
+
if (decoded.droppedMalformed) {
|
|
379
|
+
socket.destroy(new Error('oversized Codex App control response'));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
for (const line of decoded.lines) {
|
|
383
|
+
if (controlSocket !== socket) {
|
|
384
|
+
socket.destroy(new Error('unexpected Codex App control response'));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const action = handshake.handle(parseCodexAppControlWireRecord(line), controlSentThrough);
|
|
388
|
+
if (action.type === 'authenticate') {
|
|
389
|
+
controlChallenge = action.challenge;
|
|
390
|
+
socket.write(`${encodeCodexAppControlAuth(args.controlPrivateKey, args.sessionId, args.controlGeneration, action.challenge)}\n`);
|
|
391
|
+
}
|
|
392
|
+
else if (action.type === 'accepted') {
|
|
393
|
+
// `accepted` is intentionally unsigned. Its authority is the protected
|
|
394
|
+
// locator's independent epoch plus the already-bound random endpoint.
|
|
395
|
+
// Ed25519 still authenticates every runner marker to the worker.
|
|
396
|
+
controlAccepted = true;
|
|
397
|
+
clearTimeout(handshakeTimer);
|
|
398
|
+
if (args.controlLocatorPath)
|
|
399
|
+
controlEndpoints.noteAccepted(target.endpoint);
|
|
400
|
+
controlAcceptanceCount++;
|
|
401
|
+
resolveControlReady();
|
|
402
|
+
// The first acceptance happens before app-server initialization; it is
|
|
403
|
+
// not a ready boundary. Re-authentication can publish the live state
|
|
404
|
+
// only after initialization has completed.
|
|
405
|
+
if (controlAcceptanceCount > 1 && runnerReady)
|
|
406
|
+
emitRunnerState();
|
|
407
|
+
flushControlQueue();
|
|
408
|
+
}
|
|
409
|
+
else if (action.type === 'ack' && controlAccepted) {
|
|
410
|
+
if (action.seq > controlAckedSeq)
|
|
411
|
+
controlAckedSeq = action.seq;
|
|
412
|
+
while (controlQueue[0] && controlQueue[0].seq <= controlAckedSeq)
|
|
413
|
+
controlQueue.shift();
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
socket.destroy(new Error('out-of-order Codex App control response'));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
socket.on('error', () => { });
|
|
422
|
+
socket.on('close', () => {
|
|
423
|
+
clearTimeout(handshakeTimer);
|
|
424
|
+
if (controlSocket === socket) {
|
|
425
|
+
controlSocket = undefined;
|
|
426
|
+
controlChallenge = undefined;
|
|
427
|
+
controlAccepted = false;
|
|
428
|
+
}
|
|
429
|
+
scheduleControlReconnect();
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function emitMarker(kind, payload) {
|
|
433
|
+
if (controlQueue.length >= CONTROL_QUEUE_MAX_RECORDS) {
|
|
434
|
+
controlFatal = true;
|
|
435
|
+
console.error('Codex App control queue exceeded its fail-closed bound');
|
|
436
|
+
process.exit(2);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
controlQueue.push({ seq: ++controlSeq, kind, payload });
|
|
440
|
+
flushControlQueue();
|
|
441
|
+
}
|
|
442
|
+
function emitFinalMarker(payload, opts = {}) {
|
|
443
|
+
// Usage draining is caller-controlled so an N-final expansion of one native
|
|
444
|
+
// completion does not let the FIRST (superseded) transaction steal the usage:
|
|
445
|
+
// only the LAST real final drains the accumulator. Default (no opts) drains,
|
|
446
|
+
// preserving every existing single-final caller. A `steer_superseded`
|
|
447
|
+
// transaction never carries usage and never drains.
|
|
448
|
+
const superseded = opts.disposition === 'steer_superseded';
|
|
449
|
+
const drainUsage = superseded ? false : (opts.drainUsage ?? true);
|
|
450
|
+
// Attach this turn's token usage (if the accumulator saw coherent totals) and
|
|
451
|
+
// drain its accumulator. Keyed by the codex native turn id; omitted when no
|
|
452
|
+
// usage was observed / a protocol anomaly was detected — never zeros.
|
|
453
|
+
const usageKey = typeof payload.nativeTurnId === 'string' && payload.nativeTurnId.length > 0
|
|
454
|
+
? payload.nativeTurnId
|
|
455
|
+
: undefined;
|
|
456
|
+
const acc = usageKey && drainUsage ? usageAccumulators.get(usageKey) : undefined;
|
|
457
|
+
const usage = acc?.result() ?? undefined;
|
|
458
|
+
if (acc?.warning && !usage) {
|
|
459
|
+
// Surface a protocol anomaly rather than silently omitting usage — a
|
|
460
|
+
// regression/negative-baseline should be visible in the runner log.
|
|
461
|
+
writeLine(`[codex-app] token usage dropped for turn ${usageKey ?? '?'}: ${acc.warning}`);
|
|
462
|
+
}
|
|
463
|
+
if (usageKey && drainUsage)
|
|
464
|
+
usageAccumulators.delete(usageKey);
|
|
465
|
+
// Dual-name the turn identity so both protocol vocabularies ride the same
|
|
466
|
+
// signed final transaction:
|
|
467
|
+
// - PR #597: `turnId` (botmux/Lark id) + `nativeTurnId` (codex app-server id)
|
|
468
|
+
// — consumed by the worker's serial dispatch FIFO (settleFinal).
|
|
469
|
+
// - master: `replyTurnId` (botmux/Lark id) + `appTurnId` (codex id) — the
|
|
470
|
+
// names the runner-protocol normalizer + steer-ack correlation expect.
|
|
471
|
+
// `appTurnId` is always present: a turn that never bound a native id (e.g. a
|
|
472
|
+
// turn/start rejection) gets a synthetic codex-app-error id, matching master's
|
|
473
|
+
// controller failure-id contract so the worker can still key the completion.
|
|
474
|
+
const replyTurnId = typeof payload.turnId === 'string' && payload.turnId.length > 0
|
|
475
|
+
? payload.turnId
|
|
476
|
+
: undefined;
|
|
477
|
+
const appTurnId = usageKey ?? `codex-app-error-${Date.now()}-${++finalFailureSequence}`;
|
|
478
|
+
const original = Buffer.from(String(payload.content ?? ''), 'utf8');
|
|
479
|
+
const truncated = original.length > CODEX_APP_CONTROL_FINAL_MAX_BYTES;
|
|
480
|
+
const content = truncated
|
|
481
|
+
? Buffer.concat([
|
|
482
|
+
original.subarray(0, CODEX_APP_CONTROL_FINAL_MAX_BYTES - 64),
|
|
483
|
+
Buffer.from('\n\n[botmux: final output truncated at control limit]', 'utf8'),
|
|
484
|
+
])
|
|
485
|
+
: original;
|
|
486
|
+
const id = `${String(payload.turnId ?? 'turn')}:${String(payload.completedAtMs ?? Date.now())}`;
|
|
487
|
+
const total = Math.ceil(content.length / CODEX_APP_CONTROL_FINAL_CHUNK_BYTES);
|
|
488
|
+
const { content: _content, ...metadata } = payload;
|
|
489
|
+
emitMarker('final-start', {
|
|
490
|
+
id,
|
|
491
|
+
total,
|
|
492
|
+
truncated,
|
|
493
|
+
...metadata,
|
|
494
|
+
appTurnId,
|
|
495
|
+
...(replyTurnId ? { replyTurnId } : {}),
|
|
496
|
+
...(usage ? { usage } : {}),
|
|
497
|
+
...(superseded ? { disposition: 'steer_superseded' } : {}),
|
|
498
|
+
});
|
|
499
|
+
for (let index = 0; index < total; index++) {
|
|
500
|
+
const start = index * CODEX_APP_CONTROL_FINAL_CHUNK_BYTES;
|
|
501
|
+
emitMarker('final-chunk', {
|
|
502
|
+
id,
|
|
503
|
+
index,
|
|
504
|
+
data: content.subarray(start, start + CODEX_APP_CONTROL_FINAL_CHUNK_BYTES).toString('base64'),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
emitMarker('final-end', { id, total });
|
|
508
|
+
}
|
|
509
|
+
connectControlSocket();
|
|
510
|
+
let client;
|
|
251
511
|
let threadId = args.threadId;
|
|
252
512
|
let threadReady = false;
|
|
513
|
+
let activeTurn = null;
|
|
514
|
+
let activeTurnEpoch = 0;
|
|
515
|
+
/** App-server may start a Goal continuation without a Botmux input. Keep that
|
|
516
|
+
* native lifecycle separate from `activeTurn`; otherwise the next Lark input
|
|
517
|
+
* is incorrectly sent with turn/start and its completion can be discarded as
|
|
518
|
+
* belonging to an "unexpected" native turn. */
|
|
519
|
+
let nativeActiveTurnId;
|
|
520
|
+
const queue = [];
|
|
253
521
|
let inputBuffer = '';
|
|
522
|
+
let processing = false;
|
|
523
|
+
/** At most one pre-final Lark `turn/steer` admission runs at a time (checkpoint:
|
|
524
|
+
* startResponsePending ≠ steerInFlight — this guards ONLY the steer RPC). */
|
|
525
|
+
let steerAdmitting = false;
|
|
526
|
+
/** An unknown turn/start|turn/steer outcome fenced the generation. Stops all
|
|
527
|
+
* dequeue / steer / final / idle-state emission until the process is torn down
|
|
528
|
+
* (the worker's authenticated `fatal` lifecycle fails the control generation). */
|
|
529
|
+
let generationFenced = false;
|
|
530
|
+
let runnerReady = false;
|
|
531
|
+
let cleanInputUnsupported = false;
|
|
254
532
|
let codexVersionChecked = false;
|
|
255
533
|
let codexVersion;
|
|
256
534
|
let cleanVersionWarningShown = false;
|
|
257
|
-
|
|
258
|
-
/** Per-turn token accumulators keyed by codex appTurnId. Fed by
|
|
535
|
+
/** Per-turn token accumulators keyed by codex native turn id. Fed by
|
|
259
536
|
* thread/tokenUsage/updated notifications; drained (and deleted) when the
|
|
260
537
|
* matching turn's final marker is emitted. Bounded by turn lifetime — a turn
|
|
261
538
|
* that never finalizes leaves at most one stale entry, cleared on next final. */
|
|
@@ -263,6 +540,10 @@ const usageAccumulators = new Map();
|
|
|
263
540
|
/** Only one turn is active at a time; a small cap bounds leakage from turns
|
|
264
541
|
* that never emit a final marker. */
|
|
265
542
|
const MAX_USAGE_ACCUMULATORS = 8;
|
|
543
|
+
/** Monotonic counter for synthetic appTurnId values on finals that never bound a
|
|
544
|
+
* codex native turn id (e.g. turn/start rejections). Mirrors master's controller
|
|
545
|
+
* failure-id contract (`codex-app-error-<now>-<seq>`). */
|
|
546
|
+
let finalFailureSequence = 0;
|
|
266
547
|
/** Get (or create, with bounded pruning) the usage accumulator for a turn. */
|
|
267
548
|
function getOrCreateUsageAccumulator(turnId) {
|
|
268
549
|
let acc = usageAccumulators.get(turnId);
|
|
@@ -279,6 +560,17 @@ function getOrCreateUsageAccumulator(turnId) {
|
|
|
279
560
|
}
|
|
280
561
|
return acc;
|
|
281
562
|
}
|
|
563
|
+
function emitRunnerState(busy = processing || queue.length > 0 || nativeActiveTurnId !== undefined, tracksTurn = activeTurn !== null) {
|
|
564
|
+
emitMarker('state', {
|
|
565
|
+
busy,
|
|
566
|
+
atMs: Date.now(),
|
|
567
|
+
// Input is accepted only after the runner has initialized and emitted this
|
|
568
|
+
// signed state. The worker uses this field as a runtime type-ahead gate;
|
|
569
|
+
// authentication alone never releases a prompt.
|
|
570
|
+
acceptingInput: runnerReady,
|
|
571
|
+
...(busy && !tracksTurn ? { tracksTurn: false } : {}),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
282
574
|
function detectedCodexVersion() {
|
|
283
575
|
if (codexVersionChecked)
|
|
284
576
|
return codexVersion;
|
|
@@ -297,6 +589,47 @@ function detectedCodexVersion() {
|
|
|
297
589
|
}
|
|
298
590
|
return codexVersion;
|
|
299
591
|
}
|
|
592
|
+
function makeTurn(clientUserMessageId, requestKind) {
|
|
593
|
+
let resolveDone;
|
|
594
|
+
const done = new Promise(resolve => { resolveDone = resolve; });
|
|
595
|
+
return {
|
|
596
|
+
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
597
|
+
epoch: ++activeTurnEpoch,
|
|
598
|
+
identityConflictReported: false,
|
|
599
|
+
completed: false,
|
|
600
|
+
requestKind,
|
|
601
|
+
requestAccepted: false,
|
|
602
|
+
pendingCompletions: [],
|
|
603
|
+
pendingNotifications: [],
|
|
604
|
+
startedAtMs: Date.now(),
|
|
605
|
+
lastActivityMarkerAtMs: 0,
|
|
606
|
+
serverStarted: false,
|
|
607
|
+
finalText: '',
|
|
608
|
+
allAgentText: '',
|
|
609
|
+
itemText: new Map(),
|
|
610
|
+
phase: 'starting',
|
|
611
|
+
done,
|
|
612
|
+
resolveDone,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const TURN_ACTIVITY_MARKER_MIN_INTERVAL_MS = 5_000;
|
|
616
|
+
/**
|
|
617
|
+
* Expose app-server lifecycle activity to the parent worker without polluting
|
|
618
|
+
* the visible terminal. Progress markers are throttled because token-delta
|
|
619
|
+
* notifications can arrive many times per second; submitted/completed edges
|
|
620
|
+
* are always emitted.
|
|
621
|
+
*/
|
|
622
|
+
function emitTurnActivity(turn, phase, force = false) {
|
|
623
|
+
const atMs = Date.now();
|
|
624
|
+
if (!force && atMs - turn.lastActivityMarkerAtMs < TURN_ACTIVITY_MARKER_MIN_INTERVAL_MS)
|
|
625
|
+
return;
|
|
626
|
+
turn.lastActivityMarkerAtMs = atMs;
|
|
627
|
+
emitMarker('activity', {
|
|
628
|
+
phase,
|
|
629
|
+
atMs,
|
|
630
|
+
...(turn.nativeTurnId ? { turnId: turn.nativeTurnId } : {}),
|
|
631
|
+
});
|
|
632
|
+
}
|
|
300
633
|
function handleServerRequest(msg) {
|
|
301
634
|
const method = msg.method;
|
|
302
635
|
if (method === 'item/commandExecution/requestApproval') {
|
|
@@ -329,17 +662,150 @@ function handleServerRequest(msg) {
|
|
|
329
662
|
}
|
|
330
663
|
return false;
|
|
331
664
|
}
|
|
332
|
-
function
|
|
665
|
+
function exactClientItemIndexes(turn, clientUserMessageId) {
|
|
666
|
+
if (turn?.itemsView !== 'full' || !Array.isArray(turn?.items))
|
|
667
|
+
return [];
|
|
668
|
+
const indexes = [];
|
|
669
|
+
for (let index = 0; index < turn.items.length; index++) {
|
|
670
|
+
const item = turn.items[index];
|
|
671
|
+
if (item?.type === 'userMessage' && item.clientId === clientUserMessageId)
|
|
672
|
+
indexes.push(index);
|
|
673
|
+
}
|
|
674
|
+
return indexes;
|
|
675
|
+
}
|
|
676
|
+
function isTerminalNativeTurn(turn) {
|
|
677
|
+
return turn?.status === undefined
|
|
678
|
+
|| turn.status === 'completed'
|
|
679
|
+
|| turn.status === 'failed'
|
|
680
|
+
|| turn.status === 'interrupted';
|
|
681
|
+
}
|
|
682
|
+
/** Rebuild only from content causally after the exact user item. Never reuse
|
|
683
|
+
* streamed text from a different native turn during identity reconciliation. */
|
|
684
|
+
function rebuildReconciledFinal(turn, userItemIndex) {
|
|
685
|
+
const following = Array.isArray(turn.items) ? turn.items.slice(userItemIndex + 1) : [];
|
|
686
|
+
const finalAnswers = following.filter((item) => item?.type === 'agentMessage' && item.phase === 'final_answer');
|
|
687
|
+
if (finalAnswers.length > 0)
|
|
688
|
+
return String(finalAnswers.at(-1)?.text ?? '');
|
|
689
|
+
if (turn?.error?.message)
|
|
690
|
+
return `Codex App turn failed: ${String(turn.error.message)}`;
|
|
691
|
+
return '';
|
|
692
|
+
}
|
|
693
|
+
function completeActiveTurnFromNative(turn, nativeTurn, exactIndex) {
|
|
694
|
+
if (activeTurn !== turn || turn.completed || !isTerminalNativeTurn(nativeTurn))
|
|
695
|
+
return;
|
|
696
|
+
if (exactIndex !== undefined) {
|
|
697
|
+
turn.finalText = rebuildReconciledFinal(nativeTurn, exactIndex);
|
|
698
|
+
turn.allAgentText = '';
|
|
699
|
+
}
|
|
700
|
+
else if (nativeTurn?.error?.message && !turn.finalText) {
|
|
701
|
+
turn.finalText = `Codex App turn failed: ${String(nativeTurn.error.message)}`;
|
|
702
|
+
}
|
|
703
|
+
if (typeof nativeTurn?.id === 'string') {
|
|
704
|
+
turn.nativeTurnId = nativeTurn.id;
|
|
705
|
+
if (nativeActiveTurnId === nativeTurn.id)
|
|
706
|
+
nativeActiveTurnId = undefined;
|
|
707
|
+
}
|
|
708
|
+
turn.completed = true;
|
|
709
|
+
emitTurnActivity(turn, 'completed', true);
|
|
710
|
+
turn.resolveDone();
|
|
711
|
+
}
|
|
712
|
+
function reportIdentityConflict(turn, observedNativeTurnId, reason = 'no exact client id match') {
|
|
713
|
+
if (activeTurn !== turn || turn.identityConflictReported)
|
|
714
|
+
return;
|
|
715
|
+
turn.identityConflictReported = true;
|
|
716
|
+
const stableTurnId = turn.clientUserMessageId;
|
|
717
|
+
const message = `Codex App native turn identity conflict (${reason}); refusing to attribute a completion without an exact clientUserMessageId match`;
|
|
718
|
+
writeLine(`[codex-app] ${message}`);
|
|
719
|
+
emitMarker('diagnostic', {
|
|
720
|
+
code: 'native_turn_identity_conflict',
|
|
721
|
+
message,
|
|
722
|
+
...(stableTurnId ? { turnId: stableTurnId } : {}),
|
|
723
|
+
...(turn.nativeTurnId ? { expectedNativeTurnId: turn.nativeTurnId } : {}),
|
|
724
|
+
...(observedNativeTurnId ? { observedNativeTurnId } : {}),
|
|
725
|
+
atMs: Date.now(),
|
|
726
|
+
});
|
|
727
|
+
// Attribution failed closed, but the logical Botmux turn must still settle.
|
|
728
|
+
// Leaving turn.done unresolved permanently poisons the runner FIFO and keeps
|
|
729
|
+
// the session "busy" forever. Emit one explicit error final under the stable
|
|
730
|
+
// client turn id; never reuse untrusted native text.
|
|
731
|
+
turn.finalText = `Codex App turn failed: ${message}`;
|
|
732
|
+
turn.allAgentText = '';
|
|
733
|
+
if (turn.nativeTurnId && nativeActiveTurnId === turn.nativeTurnId) {
|
|
734
|
+
nativeActiveTurnId = undefined;
|
|
735
|
+
}
|
|
736
|
+
turn.completed = true;
|
|
737
|
+
emitTurnActivity(turn, 'completed', true);
|
|
738
|
+
turn.resolveDone();
|
|
739
|
+
}
|
|
740
|
+
async function reconcileCompletedTurn(turn, observedNativeTurnId) {
|
|
741
|
+
if (turn.reconciliation || turn.completed)
|
|
742
|
+
return turn.reconciliation;
|
|
743
|
+
const clientUserMessageId = turn.clientUserMessageId;
|
|
744
|
+
if (!clientUserMessageId || !threadId) {
|
|
745
|
+
reportIdentityConflict(turn, observedNativeTurnId, 'legacy input has no clientUserMessageId');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const epoch = turn.epoch;
|
|
749
|
+
const deadlineAtMs = Date.now() + RECONCILIATION_TIMEOUT_MS;
|
|
750
|
+
turn.reconciliation = (async () => {
|
|
751
|
+
const matches = [];
|
|
752
|
+
let cursor;
|
|
753
|
+
for (let page = 0; page < RECONCILIATION_PAGE_LIMIT; page++) {
|
|
754
|
+
const remaining = deadlineAtMs - Date.now();
|
|
755
|
+
if (remaining <= 0)
|
|
756
|
+
break;
|
|
757
|
+
const result = await client.request('thread/turns/list', {
|
|
758
|
+
threadId,
|
|
759
|
+
...(cursor ? { cursor } : {}),
|
|
760
|
+
limit: RECONCILIATION_PAGE_SIZE,
|
|
761
|
+
sortDirection: 'desc',
|
|
762
|
+
itemsView: 'full',
|
|
763
|
+
}, { timeoutMs: remaining });
|
|
764
|
+
for (const candidate of Array.isArray(result?.data) ? result.data : []) {
|
|
765
|
+
if (!isTerminalNativeTurn(candidate))
|
|
766
|
+
continue;
|
|
767
|
+
const indexes = exactClientItemIndexes(candidate, clientUserMessageId);
|
|
768
|
+
if (indexes.length === 1)
|
|
769
|
+
matches.push({ turn: candidate, itemIndex: indexes[0] });
|
|
770
|
+
else if (indexes.length > 1) {
|
|
771
|
+
reportIdentityConflict(turn, observedNativeTurnId, 'client id appears more than once in one turn');
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
cursor = typeof result?.nextCursor === 'string' ? result.nextCursor : null;
|
|
776
|
+
if (!cursor)
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
if (activeTurn !== turn || turn.epoch !== epoch || turn.completed)
|
|
780
|
+
return;
|
|
781
|
+
if (matches.length === 1) {
|
|
782
|
+
completeActiveTurnFromNative(turn, matches[0].turn, matches[0].itemIndex);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
reportIdentityConflict(turn, observedNativeTurnId, matches.length === 0 ? 'bounded history lookup found no match' : 'bounded history lookup found multiple matches');
|
|
786
|
+
})().catch(err => {
|
|
787
|
+
if (activeTurn === turn && !turn.completed) {
|
|
788
|
+
reportIdentityConflict(turn, observedNativeTurnId, `bounded history lookup failed: ${asError(err).message}`);
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
await turn.reconciliation;
|
|
792
|
+
}
|
|
793
|
+
function handleNotification(msg, replayedAfterResponse = false) {
|
|
794
|
+
const params = msg.params ?? {};
|
|
795
|
+
if (params.threadId !== threadId)
|
|
796
|
+
return;
|
|
797
|
+
const notificationTurnId = params.turnId ?? params.turn?.id;
|
|
333
798
|
// Per-turn token usage rides on thread/tokenUsage/updated (NOT turn/completed).
|
|
334
|
-
// Feed the accumulator for the matching
|
|
335
|
-
//
|
|
799
|
+
// Feed the accumulator for the matching native turn id; it is drained onto the
|
|
800
|
+
// final marker under the same id when the turn's final transaction is emitted.
|
|
336
801
|
if (msg.method === 'thread/tokenUsage/updated') {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
802
|
+
const usageTurnId = typeof notificationTurnId === 'string' && notificationTurnId.length > 0
|
|
803
|
+
? notificationTurnId
|
|
804
|
+
: undefined;
|
|
805
|
+
if (usageTurnId) {
|
|
340
806
|
const usage = (params.tokenUsage ?? {});
|
|
341
807
|
const parsed = parseTokenUsagePair(usage.total, usage.last);
|
|
342
|
-
const acc = getOrCreateUsageAccumulator(
|
|
808
|
+
const acc = getOrCreateUsageAccumulator(usageTurnId);
|
|
343
809
|
if (parsed) {
|
|
344
810
|
acc.update(parsed.total, parsed.last);
|
|
345
811
|
}
|
|
@@ -357,10 +823,290 @@ function handleNotification(msg) {
|
|
|
357
823
|
// protocol warning rather than dropping it entirely silently.
|
|
358
824
|
writeLine('[codex-app] tokenUsage notification without turnId (ignored)');
|
|
359
825
|
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (msg.method === 'turn/started') {
|
|
829
|
+
const startedId = typeof notificationTurnId === 'string' ? notificationTurnId : undefined;
|
|
830
|
+
if (startedId && (!replayedAfterResponse
|
|
831
|
+
|| nativeActiveTurnId === undefined
|
|
832
|
+
|| nativeActiveTurnId === startedId))
|
|
833
|
+
nativeActiveTurnId = startedId;
|
|
834
|
+
const turn = activeTurn;
|
|
835
|
+
if (turn && startedId) {
|
|
836
|
+
const exact = turn.clientUserMessageId
|
|
837
|
+
? exactClientItemIndexes(params.turn, turn.clientUserMessageId)
|
|
838
|
+
: [];
|
|
839
|
+
// B1/R4-B2: an exact-client `turn/started` proves the canonical native id
|
|
840
|
+
// even before the turn/start response returns. First-proof-wins: bind only
|
|
841
|
+
// if canonical is unset OR already this id; a different already-proven
|
|
842
|
+
// canonical fences (contradiction). Same id opens the group + kicks.
|
|
843
|
+
if (exact.length === 1) {
|
|
844
|
+
if (!proveCanonicalExact(turn, startedId, 'exact_started'))
|
|
845
|
+
return;
|
|
846
|
+
if (!turn.nativeTurnId)
|
|
847
|
+
turn.nativeTurnId = startedId;
|
|
848
|
+
if (turn.phase === undefined || turn.phase === 'starting')
|
|
849
|
+
turn.phase = 'open';
|
|
850
|
+
}
|
|
851
|
+
if (turn.nativeTurnId === startedId) {
|
|
852
|
+
turn.serverStarted = true;
|
|
853
|
+
emitTurnActivity(turn, 'progress', true);
|
|
854
|
+
// Kick only when the canonical id is proven exact (not a bare id echo)
|
|
855
|
+
// so a follow-up steers into the proven turn during a pending response.
|
|
856
|
+
if ((turn.identityProof === 'exact_started' || turn.identityProof === 'exact_completed')
|
|
857
|
+
&& !turn.completed)
|
|
858
|
+
void tryAdmitSteer();
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
// app-server is allowed to publish turn/started before replying to
|
|
862
|
+
// turn/start. Without the response we do not yet know whether this is
|
|
863
|
+
// our native turn, but dropping it also loses the first (and sometimes
|
|
864
|
+
// only) progress edge. Replay it after the RPC binds nativeTurnId.
|
|
865
|
+
if (!turn.requestAccepted && turn.requestKind === 'start') {
|
|
866
|
+
const alreadyBufferedStart = turn.pendingNotifications.some(notification => notification.method === 'turn/started'
|
|
867
|
+
&& (notification.params?.turnId ?? notification.params?.turn?.id) === startedId);
|
|
868
|
+
if (!alreadyBufferedStart)
|
|
869
|
+
turn.pendingNotifications.push(msg);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
// A Goal continuation is native work, not a Botmux turn. Keep the worker
|
|
874
|
+
// busy while explicitly advertising that the initialized runner can accept
|
|
875
|
+
// a Lark follow-up through turn/steer.
|
|
876
|
+
if (runnerReady)
|
|
877
|
+
emitRunnerState(true, false);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (msg.method === 'turn/completed') {
|
|
881
|
+
const nativeTurn = params.turn ?? {};
|
|
882
|
+
const completedId = typeof notificationTurnId === 'string' ? notificationTurnId : undefined;
|
|
883
|
+
if (completedId && nativeActiveTurnId === completedId)
|
|
884
|
+
nativeActiveTurnId = undefined;
|
|
885
|
+
const turn = activeTurn;
|
|
886
|
+
if (!turn) {
|
|
887
|
+
// An autonomous Goal turn finished with no tracked Botmux turn. If a
|
|
888
|
+
// false-flag head was parked behind it (B3 gate), nativeActiveTurnId is
|
|
889
|
+
// now cleared (line above) so re-kick the drain to start it as its own
|
|
890
|
+
// turn — otherwise it would sleep forever. drainQueue no-ops when idle.
|
|
891
|
+
if (runnerReady)
|
|
892
|
+
emitRunnerState();
|
|
893
|
+
if (queue.length > 0 && nativeActiveTurnId === undefined)
|
|
894
|
+
void drainQueue();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
// Group mode (R3): a steerable root that actually began ordered-steer work
|
|
898
|
+
// routes EVERY completion through group-aware settlement — never the
|
|
899
|
+
// single-turn completeActiveTurnFromNative / single-root reconcile, which
|
|
900
|
+
// finalizeAcceptedGroup would then wrongly expand into N finals from a turn
|
|
901
|
+
// that does not contain the whole group.
|
|
902
|
+
if (inGroupMode(turn)) {
|
|
903
|
+
turn.completionSeen = true;
|
|
904
|
+
turn.steeringClosed = true;
|
|
905
|
+
// A steer RPC racing this completion, or a still-pending root start
|
|
906
|
+
// response, is a barrier: buffer and let that continuation settle the group
|
|
907
|
+
// once it appends its member / binds canonical. (canonical-only barrier.)
|
|
908
|
+
const isCanonical = turn.canonicalNativeTurnId !== undefined
|
|
909
|
+
&& completedId === turn.canonicalNativeTurnId;
|
|
910
|
+
if (isCanonical) {
|
|
911
|
+
turn.terminalCompletion = nativeTurn;
|
|
912
|
+
if (turn.steerInFlight || turn.startResponsePending) {
|
|
913
|
+
emitLifecycle({ kind: 'completion_race', appTurnId: completedId, category: 'steer_in_flight' });
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
settleSteeredCompletion(turn, nativeTurn);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
// A NON-canonical completion cannot itself close the group. If a steer is
|
|
920
|
+
// still racing / the start response is pending, wait for that continuation
|
|
921
|
+
// (it may prove the canonical id). Otherwise scan bounded history for the
|
|
922
|
+
// one terminal turn that contains the full ordered group; fail closed
|
|
923
|
+
// (identity error, no foreign text) on 0 / multiple / partial matches.
|
|
924
|
+
if (turn.steerInFlight || turn.startResponsePending) {
|
|
925
|
+
emitLifecycle({ kind: 'completion_race', appTurnId: completedId ?? '', category: 'steer_in_flight' });
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
void reconcileSteeredGroupFromHistory(turn, completedId);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (turn.nativeTurnId && completedId === turn.nativeTurnId) {
|
|
932
|
+
const exact = turn.clientUserMessageId
|
|
933
|
+
? exactClientItemIndexes(nativeTurn, turn.clientUserMessageId)
|
|
934
|
+
: [];
|
|
935
|
+
// R3-B2: an exact-client completion arriving BEFORE the start response is
|
|
936
|
+
// upgraded to canonical identity proof — atomically bind the canonical id,
|
|
937
|
+
// set an independent proof, and require a late start response to match it.
|
|
938
|
+
// Until that atomic upgrade, the completion is only a candidate (buffered);
|
|
939
|
+
// it must never single-turn-settle here in a way a late response could then
|
|
940
|
+
// silently overwrite. Only a steerable root can upgrade (Goal steer / plain
|
|
941
|
+
// legacy keep the original single-turn behavior below).
|
|
942
|
+
if (exact.length === 1
|
|
943
|
+
&& !turn.requestAccepted
|
|
944
|
+
&& turn.startResponsePending
|
|
945
|
+
&& turn.accepted?.[0]?.input.codexAppSteerable === true
|
|
946
|
+
&& typeof completedId === 'string') {
|
|
947
|
+
// R4-B2 first-proof-wins: a prior exact proof for a DIFFERENT id fences;
|
|
948
|
+
// same id is idempotent. Never overwrite an already-proven canonical.
|
|
949
|
+
if (!proveCanonicalExact(turn, completedId, 'exact_completed'))
|
|
950
|
+
return;
|
|
951
|
+
turn.completionSeen = true;
|
|
952
|
+
turn.steeringClosed = true;
|
|
953
|
+
turn.terminalCompletion = nativeTurn;
|
|
954
|
+
emitLifecycle({ kind: 'completion_race', appTurnId: completedId, category: 'steer_in_flight' });
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (exact.length === 1) {
|
|
958
|
+
completeActiveTurnFromNative(turn, nativeTurn, exact[0]);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (!turn.requestAccepted) {
|
|
962
|
+
turn.pendingCompletions.push(nativeTurn);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (turn.requestKind === 'steer') {
|
|
966
|
+
void reconcileCompletedTurn(turn, completedId);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
completeActiveTurnFromNative(turn, nativeTurn);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const exact = turn.clientUserMessageId
|
|
973
|
+
? exactClientItemIndexes(nativeTurn, turn.clientUserMessageId)
|
|
974
|
+
: [];
|
|
975
|
+
// R3-B2: an exact-client full-items completion arriving BEFORE the start
|
|
976
|
+
// response binds nativeTurnId is upgraded to canonical identity proof for a
|
|
977
|
+
// steerable root (atomic: bind canonical id + independent proof + buffer as
|
|
978
|
+
// candidate + require the late response to match). It must NOT single-turn
|
|
979
|
+
// settle here — a late response with a different id would otherwise be unable
|
|
980
|
+
// to correct an already-settled wrong attribution. The response path fences
|
|
981
|
+
// on id mismatch and settles the buffered candidate on match.
|
|
982
|
+
if (exact.length === 1
|
|
983
|
+
&& !turn.requestAccepted
|
|
984
|
+
&& turn.startResponsePending
|
|
985
|
+
&& turn.accepted?.[0]?.input.codexAppSteerable === true
|
|
986
|
+
&& typeof completedId === 'string') {
|
|
987
|
+
// R4-B2 first-proof-wins: a prior exact proof for a DIFFERENT id fences;
|
|
988
|
+
// same id is idempotent. Never overwrite an already-proven canonical.
|
|
989
|
+
if (!proveCanonicalExact(turn, completedId, 'exact_completed'))
|
|
990
|
+
return;
|
|
991
|
+
turn.completionSeen = true;
|
|
992
|
+
turn.steeringClosed = true;
|
|
993
|
+
turn.terminalCompletion = nativeTurn;
|
|
994
|
+
emitLifecycle({ kind: 'completion_race', appTurnId: completedId, category: 'steer_in_flight' });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (exact.length === 1) {
|
|
998
|
+
completeActiveTurnFromNative(turn, nativeTurn, exact[0]);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (!turn.requestAccepted) {
|
|
1002
|
+
turn.pendingCompletions.push(nativeTurn);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
void reconcileCompletedTurn(turn, completedId);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const turn = activeTurn;
|
|
1009
|
+
if (!turn)
|
|
1010
|
+
return;
|
|
1011
|
+
if (!turn.requestAccepted) {
|
|
1012
|
+
// turn/start notifications may beat their response. Buffer only that
|
|
1013
|
+
// request's candidate native events and replay after the response chooses
|
|
1014
|
+
// the authoritative id. For turn/steer, pre-response events can be old
|
|
1015
|
+
// autonomous output and are deliberately never promoted into the final.
|
|
1016
|
+
if (turn.requestKind === 'start' && typeof notificationTurnId === 'string') {
|
|
1017
|
+
turn.pendingNotifications.push(msg);
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (turn.nativeTurnId && notificationTurnId && notificationTurnId !== turn.nativeTurnId)
|
|
1022
|
+
return;
|
|
1023
|
+
// Every notification for the active app-server turn is evidence of forward
|
|
1024
|
+
// progress, including reasoning/status events that do not render text.
|
|
1025
|
+
emitTurnActivity(turn, 'progress');
|
|
1026
|
+
if (msg.method === 'item/started') {
|
|
1027
|
+
const item = params.item;
|
|
1028
|
+
if (item?.type === 'commandExecution') {
|
|
1029
|
+
writeLine(`\n$ ${item.command}`);
|
|
1030
|
+
}
|
|
1031
|
+
else if (item?.type === 'fileChange') {
|
|
1032
|
+
writeLine('\n[files changed]');
|
|
1033
|
+
}
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (msg.method === 'item/agentMessage/delta') {
|
|
1037
|
+
const delta = String(params.delta ?? '');
|
|
1038
|
+
const itemId = String(params.itemId ?? '');
|
|
1039
|
+
turn.itemText.set(itemId, (turn.itemText.get(itemId) ?? '') + delta);
|
|
1040
|
+
turn.allAgentText += delta;
|
|
1041
|
+
output.display(delta);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (msg.method === 'item/commandExecution/outputDelta' || msg.method === 'item/fileChange/outputDelta') {
|
|
1045
|
+
output.display(String(params.delta ?? ''));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (msg.method === 'item/completed') {
|
|
1049
|
+
const item = params.item;
|
|
1050
|
+
if (item?.type === 'agentMessage') {
|
|
1051
|
+
if (item.phase === 'final_answer')
|
|
1052
|
+
turn.finalText = String(item.text ?? '');
|
|
1053
|
+
else if (!turn.itemText.has(item.id) && item.text) {
|
|
1054
|
+
turn.allAgentText += String(item.text);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function startupRequestTimeout(deadlineAtMs, method) {
|
|
1061
|
+
if (deadlineAtMs === undefined)
|
|
1062
|
+
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1063
|
+
const remaining = deadlineAtMs - Date.now();
|
|
1064
|
+
if (remaining <= 0)
|
|
1065
|
+
throw new AppServerRequestTimeoutError(method, 0);
|
|
1066
|
+
return remaining;
|
|
1067
|
+
}
|
|
1068
|
+
function isExplicitMissingThread(error) {
|
|
1069
|
+
if (!(error instanceof AppServerRpcError))
|
|
1070
|
+
return false;
|
|
1071
|
+
return /(thread|rollout|conversation).*(not found|does not exist|missing|unknown)|not found.*(thread|rollout|conversation)/i
|
|
1072
|
+
.test(error.message);
|
|
1073
|
+
}
|
|
1074
|
+
function isExplicitExpectedTurnInactive(error) {
|
|
1075
|
+
return error instanceof AppServerRpcError
|
|
1076
|
+
&& /(expected|active).*(turn).*(not active|no longer active|mismatch|does not match)|(turn).*(not active|no longer active).*(expected)/i
|
|
1077
|
+
.test(error.message);
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Whether an RPC error definitively proves the operation was NOT accepted — safe
|
|
1081
|
+
* to treat as a clean rejection (no fence, no in-doubt final). Mirrors the
|
|
1082
|
+
* controller's isDefiniteSteerRejection (nit): JSON-RPC parse/method/params
|
|
1083
|
+
* errors (-32600/-32601/-32602) prove non-acceptance, plus the app-server's
|
|
1084
|
+
* explicit "not steerable / expected turn" rejection phrases. Anything else
|
|
1085
|
+
* (transport, timeout, generic -32000, protocol anomaly) is an UNKNOWN outcome
|
|
1086
|
+
* that must fence rather than guess a disposition.
|
|
1087
|
+
*/
|
|
1088
|
+
function isDefiniteRpcRejection(error) {
|
|
1089
|
+
if (!(error instanceof AppServerRpcError))
|
|
1090
|
+
return false;
|
|
1091
|
+
if (error.code === -32600 || error.code === -32601 || error.code === -32602)
|
|
1092
|
+
return true;
|
|
1093
|
+
let dataText = '';
|
|
1094
|
+
try {
|
|
1095
|
+
dataText = JSON.stringify(error.data ?? '').toLowerCase();
|
|
360
1096
|
}
|
|
361
|
-
|
|
1097
|
+
catch { /* untrusted data */ }
|
|
1098
|
+
const detail = `${error.message} ${dataText}`.toLowerCase();
|
|
1099
|
+
return detail.includes('no active turn to steer')
|
|
1100
|
+
|| detail.includes('activeturnnotsteerable')
|
|
1101
|
+
|| detail.includes('active turn not steerable')
|
|
1102
|
+
|| detail.includes('expectedturnid')
|
|
1103
|
+
|| detail.includes('expected turn id')
|
|
1104
|
+
|| detail.includes('cannot steer a review turn')
|
|
1105
|
+
|| detail.includes('cannot steer a compact turn')
|
|
1106
|
+
|| detail.includes('input must not be empty')
|
|
1107
|
+
|| isExplicitExpectedTurnInactive(error);
|
|
362
1108
|
}
|
|
363
|
-
async function ensureThread() {
|
|
1109
|
+
async function ensureThread(startupDeadlineAtMs) {
|
|
364
1110
|
if (threadReady && threadId)
|
|
365
1111
|
return threadId;
|
|
366
1112
|
if (threadId) {
|
|
@@ -382,7 +1128,7 @@ async function ensureThread() {
|
|
|
382
1128
|
// Keep Codex App's rich history in sync with turns created by this
|
|
383
1129
|
// external runner so the desktop UI can render follow-up messages.
|
|
384
1130
|
persistExtendedHistory: true,
|
|
385
|
-
});
|
|
1131
|
+
}, { timeoutMs: startupRequestTimeout(startupDeadlineAtMs, 'thread/resume') });
|
|
386
1132
|
const resumedThreadId = String(resumed.thread.id);
|
|
387
1133
|
threadId = resumedThreadId;
|
|
388
1134
|
threadReady = true;
|
|
@@ -390,6 +1136,11 @@ async function ensureThread() {
|
|
|
390
1136
|
return resumedThreadId;
|
|
391
1137
|
}
|
|
392
1138
|
catch (err) {
|
|
1139
|
+
// A transport error or timeout is an ambiguous acceptance boundary. It
|
|
1140
|
+
// must never fork history by silently creating a fresh thread. Only an
|
|
1141
|
+
// explicit app-server "missing thread" rejection permits fallback.
|
|
1142
|
+
if (!isExplicitMissingThread(err))
|
|
1143
|
+
throw err;
|
|
393
1144
|
writeLine(`[codex-app] resume failed, starting a fresh thread: ${err?.message ?? err}`);
|
|
394
1145
|
threadId = undefined;
|
|
395
1146
|
threadReady = false;
|
|
@@ -417,102 +1168,831 @@ async function ensureThread() {
|
|
|
417
1168
|
// Keep Codex App's rich history in sync with turns created by this
|
|
418
1169
|
// external runner so the desktop UI can render follow-up messages.
|
|
419
1170
|
persistExtendedHistory: true,
|
|
420
|
-
});
|
|
1171
|
+
}, { timeoutMs: startupRequestTimeout(startupDeadlineAtMs, 'thread/start') });
|
|
421
1172
|
const startedThreadId = String(started.thread.id);
|
|
422
1173
|
threadId = startedThreadId;
|
|
423
1174
|
threadReady = true;
|
|
424
1175
|
emitMarker('thread', { threadId: startedThreadId });
|
|
1176
|
+
void client.request('thread/name/set', {
|
|
1177
|
+
threadId: startedThreadId,
|
|
1178
|
+
name: `botmux ${args.sessionId.slice(0, 8)}`,
|
|
1179
|
+
}, { timeoutMs: 2_000 }).catch(() => { });
|
|
1180
|
+
return startedThreadId;
|
|
1181
|
+
}
|
|
1182
|
+
/** Emit a signed lifecycle marker (steer_attempt / steer_accepted / completion_race
|
|
1183
|
+
* / unknown_outcome / fatal ...). The worker relays steer_accepted back to the
|
|
1184
|
+
* daemon as a "收到,引导成功" edge and treats fatal as a control-generation kill. */
|
|
1185
|
+
function emitLifecycle(event) {
|
|
1186
|
+
emitMarker('lifecycle', { atMs: Date.now(), ...event });
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* Fence the runner generation on an UNKNOWN turn/start|turn/steer outcome
|
|
1190
|
+
* (transport / rpc / protocol — never a definite rejection). This is a
|
|
1191
|
+
* control-plane action, not a log line: it emits a signed unknown_outcome + a
|
|
1192
|
+
* signed fatal so the worker calls failCodexAppControlGeneration, and it stops
|
|
1193
|
+
* every subsequent dequeue / steer / final / idle emission. It NEVER emits a
|
|
1194
|
+
* failure final — guessing a final would advance the worker FIFO past a turn
|
|
1195
|
+
* whose true disposition is unknown.
|
|
1196
|
+
*/
|
|
1197
|
+
function fenceUnknown(operation, category, turn) {
|
|
1198
|
+
if (generationFenced)
|
|
1199
|
+
return;
|
|
1200
|
+
generationFenced = true;
|
|
1201
|
+
if (turn)
|
|
1202
|
+
turn.phase = 'fenced';
|
|
1203
|
+
emitLifecycle({ kind: 'unknown_outcome', operation, category });
|
|
1204
|
+
emitLifecycle({ kind: 'fatal', operation, category });
|
|
1205
|
+
writeLine(`[codex-app] fenced generation on unknown ${operation} outcome (${category})`);
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* First-proof-wins canonical binding for an exact-client proof (R4-B2). The
|
|
1209
|
+
* FIRST exact `turn/started` or exact `turn/completed` to prove the canonical
|
|
1210
|
+
* native id wins; any LATER exact proof carrying a DIFFERENT id is a protocol
|
|
1211
|
+
* anomaly (the app-server contradicted itself) → fence. The same id is
|
|
1212
|
+
* idempotent. Returns true if the caller may proceed (proof accepted / already
|
|
1213
|
+
* bound to same id), false if it fenced (caller must stop).
|
|
1214
|
+
*/
|
|
1215
|
+
function proveCanonicalExact(turn, id, proof) {
|
|
1216
|
+
if (turn.canonicalNativeTurnId === undefined) {
|
|
1217
|
+
turn.canonicalNativeTurnId = id;
|
|
1218
|
+
turn.identityProof = proof;
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
if (turn.canonicalNativeTurnId === id)
|
|
1222
|
+
return true; // idempotent, same proof id
|
|
1223
|
+
// A different exact id after canonical was already proven: contradiction.
|
|
1224
|
+
fenceUnknown('turn/start', 'protocol', turn);
|
|
1225
|
+
if (!turn.completed) {
|
|
1226
|
+
turn.completed = true;
|
|
1227
|
+
turn.resolveDone();
|
|
1228
|
+
}
|
|
1229
|
+
return false;
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Whether this turn is in "group mode": a steerable root that has actually begun
|
|
1233
|
+
* ordered-steer work (grew past its root, has a steer RPC in flight, or already
|
|
1234
|
+
* closed steering with an accepted member). In group mode EVERY completion and
|
|
1235
|
+
* failure path must be group-aware — a single-turn settle (completeActiveTurnFromNative
|
|
1236
|
+
* / single-root reconcile) followed by finalizeAcceptedGroup's N-final expansion
|
|
1237
|
+
* would mis-map a foreign/partial turn's content onto the last follower. A bare
|
|
1238
|
+
* steerable root that never steered is NOT in group mode (stays single-turn).
|
|
1239
|
+
*/
|
|
1240
|
+
function inGroupMode(turn) {
|
|
1241
|
+
return turn.accepted?.[0]?.input.codexAppSteerable === true
|
|
1242
|
+
&& (turn.steerInFlight !== undefined || (turn.accepted?.length ?? 0) > 1);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Whether the runner may steer `head` into the active native turn right now.
|
|
1246
|
+
* Mirrors codex's canSteer contract (Blocking 1): the group must be open, its
|
|
1247
|
+
* canonical native id proven, no steer in flight, and BOTH the root and the
|
|
1248
|
+
* follow-up head explicitly authorized by the daemon admission gate. A Goal
|
|
1249
|
+
* root additionally requires its own matching steer to have been accepted.
|
|
1250
|
+
*/
|
|
1251
|
+
function canSteer(turn, head) {
|
|
1252
|
+
return !generationFenced
|
|
1253
|
+
&& !turn.completed
|
|
1254
|
+
&& turn.phase !== 'fenced'
|
|
1255
|
+
&& turn.phase !== 'closing'
|
|
1256
|
+
&& !turn.steeringClosed
|
|
1257
|
+
&& !turn.steerInFlight
|
|
1258
|
+
&& !steerAdmitting
|
|
1259
|
+
&& turn.canonicalNativeTurnId !== undefined
|
|
1260
|
+
&& turn.accepted !== undefined
|
|
1261
|
+
&& turn.accepted.length > 0
|
|
1262
|
+
&& turn.accepted[0].input.codexAppSteerable === true
|
|
1263
|
+
&& head.codexAppSteerable === true;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Verify a candidate terminal turn contains the full ordered steered-group
|
|
1267
|
+
* subsequence: EVERY member that actually sent a clientUserMessageId appears
|
|
1268
|
+
* exactly once, and those user-item indexes are strictly increasing in accepted
|
|
1269
|
+
* order. Returns the LAST member's item index on success (to rebuild the real
|
|
1270
|
+
* answer from it), or a reason string on failure. When no member sent a
|
|
1271
|
+
* clientId, or the turn has no full items, returns { lastIndex: -1 } (caller
|
|
1272
|
+
* trusts streamed text — matches the single-turn contract).
|
|
1273
|
+
*/
|
|
1274
|
+
function verifyGroupSubsequence(turn, nativeTurn) {
|
|
1275
|
+
const sentIdMembers = (turn.accepted ?? []).filter(m => m.clientUserMessageId);
|
|
1276
|
+
if (nativeTurn?.itemsView !== 'full' || sentIdMembers.length === 0)
|
|
1277
|
+
return { lastIndex: -1 };
|
|
1278
|
+
let previousIndex = -1;
|
|
1279
|
+
for (const member of sentIdMembers) {
|
|
1280
|
+
const indexes = exactClientItemIndexes(nativeTurn, member.clientUserMessageId);
|
|
1281
|
+
if (indexes.length !== 1 || indexes[0] <= previousIndex) {
|
|
1282
|
+
return {
|
|
1283
|
+
reason: indexes.length === 0
|
|
1284
|
+
? 'steered group member missing from terminal turn items'
|
|
1285
|
+
: indexes.length > 1
|
|
1286
|
+
? 'steered group member appears more than once in terminal turn'
|
|
1287
|
+
: 'steered group members are out of order in terminal turn',
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
previousIndex = indexes[0];
|
|
1291
|
+
}
|
|
1292
|
+
return { lastIndex: previousIndex };
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Settle a steered group against a terminal turn already proven to be its
|
|
1296
|
+
* canonical completion (completedId === canonicalNativeTurnId). Resolves
|
|
1297
|
+
* `turn.done` exactly once; runTurn then expands the group into N finals.
|
|
1298
|
+
*
|
|
1299
|
+
* B5 group-aware identity defense: EVERY member that sent a clientUserMessageId
|
|
1300
|
+
* must appear exactly once in strictly-increasing order (verifyGroupSubsequence);
|
|
1301
|
+
* the real answer is rebuilt from the LAST member's user item so a prior
|
|
1302
|
+
* member's agent text is never mistaken for the final reply. Any violation fails
|
|
1303
|
+
* closed via reportIdentityConflict — the settled final then carries an explicit
|
|
1304
|
+
* identity error, never foreign model text.
|
|
1305
|
+
*/
|
|
1306
|
+
function settleSteeredCompletion(turn, nativeTurn) {
|
|
1307
|
+
if (activeTurn !== turn || turn.completed)
|
|
1308
|
+
return;
|
|
1309
|
+
const verdict = verifyGroupSubsequence(turn, nativeTurn);
|
|
1310
|
+
if ('reason' in verdict) {
|
|
1311
|
+
reportIdentityConflict(turn, typeof nativeTurn?.id === 'string' ? nativeTurn.id : undefined, verdict.reason);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (verdict.lastIndex >= 0) {
|
|
1315
|
+
// Rebuild the real answer from the LAST accepted member's user item.
|
|
1316
|
+
turn.finalText = rebuildReconciledFinal(nativeTurn, verdict.lastIndex);
|
|
1317
|
+
turn.allAgentText = '';
|
|
1318
|
+
}
|
|
1319
|
+
turn.terminalCompletion = nativeTurn;
|
|
1320
|
+
if (typeof nativeTurn?.id === 'string' && nativeActiveTurnId === nativeTurn.id) {
|
|
1321
|
+
nativeActiveTurnId = undefined;
|
|
1322
|
+
}
|
|
1323
|
+
turn.completed = true;
|
|
1324
|
+
turn.phase = 'closing';
|
|
1325
|
+
emitTurnActivity(turn, 'completed', true);
|
|
1326
|
+
turn.resolveDone();
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Group-aware bounded-history reconcile (codex R3 boundary): a group-mode turn
|
|
1330
|
+
* received a completion whose native id is NOT its canonical id. A non-canonical
|
|
1331
|
+
* completion cannot itself close the group — instead scan bounded history for the
|
|
1332
|
+
* ONE terminal turn that contains the full ordered group subsequence. Only a
|
|
1333
|
+
* unique complete match settles (rebuilding the real answer from its last
|
|
1334
|
+
* member). Zero / multiple / partial (root-only) matches fail closed with an
|
|
1335
|
+
* explicit identity error that carries NO foreign model text — never expand a
|
|
1336
|
+
* follower final from a turn that does not contain the whole group.
|
|
1337
|
+
*/
|
|
1338
|
+
async function reconcileSteeredGroupFromHistory(turn, observedNativeTurnId) {
|
|
1339
|
+
if (turn.reconciliation || turn.completed)
|
|
1340
|
+
return turn.reconciliation;
|
|
1341
|
+
if (!threadId) {
|
|
1342
|
+
reportIdentityConflict(turn, observedNativeTurnId, 'steered group reconcile has no thread');
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const epoch = turn.epoch;
|
|
1346
|
+
const deadlineAtMs = Date.now() + RECONCILIATION_TIMEOUT_MS;
|
|
1347
|
+
turn.reconciliation = (async () => {
|
|
1348
|
+
const matches = [];
|
|
1349
|
+
let cursor;
|
|
1350
|
+
for (let page = 0; page < RECONCILIATION_PAGE_LIMIT; page++) {
|
|
1351
|
+
const remaining = deadlineAtMs - Date.now();
|
|
1352
|
+
if (remaining <= 0)
|
|
1353
|
+
break;
|
|
1354
|
+
const result = await client.request('thread/turns/list', {
|
|
1355
|
+
threadId,
|
|
1356
|
+
...(cursor ? { cursor } : {}),
|
|
1357
|
+
limit: RECONCILIATION_PAGE_SIZE,
|
|
1358
|
+
sortDirection: 'desc',
|
|
1359
|
+
itemsView: 'full',
|
|
1360
|
+
}, { timeoutMs: remaining });
|
|
1361
|
+
for (const candidate of Array.isArray(result?.data) ? result.data : []) {
|
|
1362
|
+
if (!isTerminalNativeTurn(candidate))
|
|
1363
|
+
continue;
|
|
1364
|
+
const verdict = verifyGroupSubsequence(turn, candidate);
|
|
1365
|
+
// Only a turn containing the FULL ordered group counts. A root-only turn
|
|
1366
|
+
// yields a 'reason' (later members missing) and is skipped, never matched.
|
|
1367
|
+
if ('lastIndex' in verdict && verdict.lastIndex >= 0) {
|
|
1368
|
+
matches.push({ turn: candidate, lastIndex: verdict.lastIndex });
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
cursor = typeof result?.nextCursor === 'string' ? result.nextCursor : null;
|
|
1372
|
+
if (!cursor)
|
|
1373
|
+
break;
|
|
1374
|
+
}
|
|
1375
|
+
if (activeTurn !== turn || turn.epoch !== epoch || turn.completed)
|
|
1376
|
+
return;
|
|
1377
|
+
if (matches.length === 1) {
|
|
1378
|
+
const { turn: matched, lastIndex } = matches[0];
|
|
1379
|
+
turn.finalText = rebuildReconciledFinal(matched, lastIndex);
|
|
1380
|
+
turn.allAgentText = '';
|
|
1381
|
+
turn.terminalCompletion = matched;
|
|
1382
|
+
// R4-B3: the matched turn is now the authoritative native identity for this
|
|
1383
|
+
// group — the final marker, native id, and usage drain must all key off
|
|
1384
|
+
// matched.id, not the stale pre-history canonical. Compare-and-clear the OLD
|
|
1385
|
+
// canonical from the global native-busy slot ONLY if it is still the head
|
|
1386
|
+
// there; if a newer autonomous Goal C has since claimed nativeActiveTurnId,
|
|
1387
|
+
// leave C untouched (never clear/overwrite a newer lifecycle).
|
|
1388
|
+
if (typeof matched?.id === 'string') {
|
|
1389
|
+
const priorCanonical = turn.canonicalNativeTurnId;
|
|
1390
|
+
if (priorCanonical !== undefined && nativeActiveTurnId === priorCanonical) {
|
|
1391
|
+
nativeActiveTurnId = undefined;
|
|
1392
|
+
}
|
|
1393
|
+
turn.nativeTurnId = matched.id;
|
|
1394
|
+
turn.canonicalNativeTurnId = matched.id;
|
|
1395
|
+
if (nativeActiveTurnId === matched.id)
|
|
1396
|
+
nativeActiveTurnId = undefined;
|
|
1397
|
+
}
|
|
1398
|
+
turn.completed = true;
|
|
1399
|
+
turn.phase = 'closing';
|
|
1400
|
+
emitTurnActivity(turn, 'completed', true);
|
|
1401
|
+
turn.resolveDone();
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
reportIdentityConflict(turn, observedNativeTurnId, matches.length === 0
|
|
1405
|
+
? 'steered group: no terminal turn contains the full ordered group'
|
|
1406
|
+
: 'steered group: multiple terminal turns match the full group');
|
|
1407
|
+
})().catch(err => {
|
|
1408
|
+
if (activeTurn === turn && !turn.completed) {
|
|
1409
|
+
reportIdentityConflict(turn, observedNativeTurnId, `steered group history lookup failed: ${asError(err).message}`);
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
await turn.reconciliation;
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Opportunistically admit the queue head as a pre-final `turn/steer` into the
|
|
1416
|
+
* active steerable group (Blocking 1). Kicked from enqueueLine, from runTurn
|
|
1417
|
+
* once the root's canonical id is proven, and re-kicked after each success to
|
|
1418
|
+
* chain successive follow-ups. At most one steer RPC runs at a time
|
|
1419
|
+
* (steerAdmitting + steerInFlight). The queue head is shifted ONLY after the
|
|
1420
|
+
* steer is accepted; a definite rejection closes steering; an unknown outcome
|
|
1421
|
+
* fences the generation (never falls back to a fresh start that would reorder).
|
|
1422
|
+
*/
|
|
1423
|
+
async function tryAdmitSteer() {
|
|
1424
|
+
const turn = activeTurn;
|
|
1425
|
+
if (!turn)
|
|
1426
|
+
return;
|
|
1427
|
+
const head = queue[0];
|
|
1428
|
+
if (!head || !canSteer(turn, head))
|
|
1429
|
+
return;
|
|
1430
|
+
steerAdmitting = true;
|
|
1431
|
+
const expectedTurnId = turn.canonicalNativeTurnId;
|
|
1432
|
+
const replyTurnId = head.replyTurnId;
|
|
1433
|
+
const version = head.codexAppInput ? detectedCodexVersion() : undefined;
|
|
1434
|
+
const requestClientId = head.codexAppInput
|
|
1435
|
+
&& !cleanInputUnsupported
|
|
1436
|
+
&& supportsClientUserMessageId(version)
|
|
1437
|
+
&& replyTurnId
|
|
1438
|
+
? replyTurnId
|
|
1439
|
+
: head.codexAppInput?.clientUserMessageId;
|
|
1440
|
+
const dispatch = {
|
|
1441
|
+
input: head,
|
|
1442
|
+
...(replyTurnId ? { replyTurnId } : {}),
|
|
1443
|
+
...(requestClientId ? { clientUserMessageId: requestClientId } : {}),
|
|
1444
|
+
receivedAtMs: head.receivedAtMs ?? Date.now(),
|
|
1445
|
+
};
|
|
1446
|
+
turn.steerInFlight = { dispatch, expectedTurnId };
|
|
1447
|
+
const built = buildCodexAppTurnStartParams({
|
|
1448
|
+
threadId: threadId,
|
|
1449
|
+
cwd: args.cwd,
|
|
1450
|
+
legacyContent: head.content,
|
|
1451
|
+
codexAppInput: head.codexAppInput,
|
|
1452
|
+
codexVersion: version,
|
|
1453
|
+
structuredDisabled: cleanInputUnsupported,
|
|
1454
|
+
});
|
|
1455
|
+
if (built.structured && requestClientId)
|
|
1456
|
+
built.params.clientUserMessageId = requestClientId;
|
|
1457
|
+
// steer_attempt/steer_accepted share appTurnId (the canonical native id) so the
|
|
1458
|
+
// worker can correlate them; replyTurnId selects the follow-up's reply routing.
|
|
1459
|
+
emitLifecycle({
|
|
1460
|
+
kind: 'steer_attempt',
|
|
1461
|
+
appTurnId: expectedTurnId,
|
|
1462
|
+
...(replyTurnId ? { replyTurnId } : {}),
|
|
1463
|
+
});
|
|
1464
|
+
let result;
|
|
425
1465
|
try {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
1466
|
+
const { input, clientUserMessageId, additionalContext } = built.params;
|
|
1467
|
+
result = await client.request('turn/steer', {
|
|
1468
|
+
threadId: threadId,
|
|
1469
|
+
input,
|
|
1470
|
+
expectedTurnId,
|
|
1471
|
+
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
1472
|
+
...(additionalContext ? { additionalContext } : {}),
|
|
429
1473
|
});
|
|
430
1474
|
}
|
|
431
|
-
catch {
|
|
432
|
-
|
|
1475
|
+
catch (err) {
|
|
1476
|
+
turn.steerInFlight = undefined;
|
|
1477
|
+
steerAdmitting = false;
|
|
1478
|
+
if (isDefiniteRpcRejection(err)) {
|
|
1479
|
+
// Definite rejection (nit: full controller rejection vocabulary, incl.
|
|
1480
|
+
// -32600/-32601/-32602 and the explicit "not steerable / expected turn"
|
|
1481
|
+
// phrases): the head did NOT land. Do not shift/append. Close steering;
|
|
1482
|
+
// once this native turn completes the head starts its own turn.
|
|
1483
|
+
turn.steeringClosed = true;
|
|
1484
|
+
emitLifecycle({ kind: 'steer_rejected_fallback', appTurnId: expectedTurnId, category: 'definite_rejection' });
|
|
1485
|
+
if (turn.completionSeen && turn.terminalCompletion)
|
|
1486
|
+
settleSteeredCompletion(turn, turn.terminalCompletion);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
// Unknown outcome (transport/timeout/generic rpc/protocol): fence — never
|
|
1490
|
+
// guess a final for a turn whose acceptance is in doubt.
|
|
1491
|
+
const category = err instanceof AppServerRpcError ? 'rpc' : 'transport';
|
|
1492
|
+
fenceUnknown('turn/steer', category, turn);
|
|
1493
|
+
if (!turn.completed) {
|
|
1494
|
+
turn.completed = true;
|
|
1495
|
+
turn.resolveDone();
|
|
1496
|
+
}
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
// Only `result.turnId === expectedTurnId` proves acceptance into this group.
|
|
1500
|
+
if (result?.turnId !== expectedTurnId) {
|
|
1501
|
+
turn.steerInFlight = undefined;
|
|
1502
|
+
steerAdmitting = false;
|
|
1503
|
+
fenceUnknown('turn/steer', 'protocol', turn);
|
|
1504
|
+
if (!turn.completed) {
|
|
1505
|
+
turn.completed = true;
|
|
1506
|
+
turn.resolveDone();
|
|
1507
|
+
}
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
// The accepted steer must correspond to the exact queue head we sent (nit): a
|
|
1511
|
+
// divergence means an out-of-band mutation reordered the FIFO under us, so the
|
|
1512
|
+
// append below would attribute this acceptance to the wrong input. Fence
|
|
1513
|
+
// rather than silently append a mismatched member.
|
|
1514
|
+
if (queue[0] !== head) {
|
|
1515
|
+
turn.steerInFlight = undefined;
|
|
1516
|
+
steerAdmitting = false;
|
|
1517
|
+
fenceUnknown('turn/steer', 'protocol', turn);
|
|
1518
|
+
if (!turn.completed) {
|
|
1519
|
+
turn.completed = true;
|
|
1520
|
+
turn.resolveDone();
|
|
1521
|
+
}
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
queue.shift();
|
|
1525
|
+
turn.accepted.push(dispatch);
|
|
1526
|
+
turn.steerInFlight = undefined;
|
|
1527
|
+
steerAdmitting = false;
|
|
1528
|
+
emitLifecycle({
|
|
1529
|
+
kind: 'steer_accepted',
|
|
1530
|
+
appTurnId: expectedTurnId,
|
|
1531
|
+
...(replyTurnId ? { replyTurnId } : {}),
|
|
1532
|
+
});
|
|
1533
|
+
// A completion may have arrived while this steer was in flight (barrier): settle
|
|
1534
|
+
// now that the group is final. Otherwise chain the next queued follow-up.
|
|
1535
|
+
if (turn.completionSeen && turn.terminalCompletion) {
|
|
1536
|
+
settleSteeredCompletion(turn, turn.terminalCompletion);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
void tryAdmitSteer();
|
|
433
1540
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
1541
|
+
/**
|
|
1542
|
+
* Emit the final transaction(s) for a completed native turn, expanding the
|
|
1543
|
+
* ordered `accepted` group into N signed finals (Blocking 1, decision A/B/C).
|
|
1544
|
+
*
|
|
1545
|
+
* - N === 1 (every existing path: single start, Goal-steer root, legacy): emits
|
|
1546
|
+
* exactly one final, byte-identical to the pre-driver single-final contract.
|
|
1547
|
+
* - N > 1 (a plain-Lark root that absorbed pre-final `turn/steer` follow-ups):
|
|
1548
|
+
* the FIRST N−1 members get an empty `steer_superseded` final (no content, no
|
|
1549
|
+
* usage, not delivered — only advances the worker FIFO); the LAST member owns
|
|
1550
|
+
* the real model answer + usage. The real final's startedAtMs is the last
|
|
1551
|
+
* accepted dispatch's receivedAtMs so an early botmux-send marker from before
|
|
1552
|
+
* the follow-up arrived cannot suppress the true answer.
|
|
1553
|
+
*
|
|
1554
|
+
* A fenced turn (unknown outcome) emits ZERO finals — the signed fatal lifecycle
|
|
1555
|
+
* already tore the generation down; guessing a final would advance the FIFO.
|
|
1556
|
+
*/
|
|
1557
|
+
function finalizeAcceptedGroup(turn) {
|
|
1558
|
+
if (turn.phase === 'fenced')
|
|
1559
|
+
return;
|
|
1560
|
+
const group = turn.accepted && turn.accepted.length > 0
|
|
1561
|
+
? turn.accepted
|
|
1562
|
+
: [{ input: { content: '' }, receivedAtMs: turn.startedAtMs }];
|
|
1563
|
+
const finalText = (turn.finalText || turn.allAgentText).trim();
|
|
1564
|
+
const completedAtMs = Date.now();
|
|
1565
|
+
const lastIndex = group.length - 1;
|
|
1566
|
+
for (let index = 0; index < group.length; index++) {
|
|
1567
|
+
const dispatch = group[index];
|
|
1568
|
+
const isReal = index === lastIndex;
|
|
1569
|
+
const replyTurnId = dispatch.replyTurnId;
|
|
1570
|
+
emitFinalMarker({
|
|
1571
|
+
...(replyTurnId ? { turnId: replyTurnId } : {}),
|
|
1572
|
+
...(turn.nativeTurnId ? { nativeTurnId: turn.nativeTurnId } : {}),
|
|
1573
|
+
content: isReal ? finalText : '',
|
|
1574
|
+
// The real answer's clock starts at the LAST accepted dispatch so a
|
|
1575
|
+
// botmux-send marker emitted before the follow-up landed cannot race
|
|
1576
|
+
// ahead and suppress it. A single-member group keeps the turn's own
|
|
1577
|
+
// startedAtMs (byte-identical to the pre-driver contract).
|
|
1578
|
+
startedAtMs: isReal
|
|
1579
|
+
? (group.length > 1 ? dispatch.receivedAtMs : turn.startedAtMs)
|
|
1580
|
+
: turn.startedAtMs,
|
|
1581
|
+
completedAtMs,
|
|
1582
|
+
}, isReal ? {} : { disposition: 'steer_superseded' });
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async function runTurn(message) {
|
|
1586
|
+
const tid = await ensureThread();
|
|
1587
|
+
// The botmux/Lark reply identity is the top-level replyTurnId (which already
|
|
1588
|
+
// fell back to the sidecar clientUserMessageId at decode time). This is the id
|
|
1589
|
+
// exposed on the final marker for reply routing; the app-server's native turn
|
|
1590
|
+
// id is tracked separately as nativeTurnId.
|
|
1591
|
+
const replyTurnId = message.replyTurnId;
|
|
1592
|
+
const version = message.codexAppInput ? detectedCodexVersion() : undefined;
|
|
1593
|
+
// When structured input is supported, thread the reply id through as the
|
|
1594
|
+
// request's clientUserMessageId so app-server echoes it as item.clientId
|
|
1595
|
+
// (used for exact native-turn attribution). Mirrors master's prepareControllerInput.
|
|
1596
|
+
const requestClientId = message.codexAppInput
|
|
1597
|
+
&& !cleanInputUnsupported
|
|
1598
|
+
&& supportsClientUserMessageId(version)
|
|
1599
|
+
&& replyTurnId
|
|
1600
|
+
? replyTurnId
|
|
1601
|
+
: message.codexAppInput?.clientUserMessageId;
|
|
1602
|
+
let expectedSteerTurnId = nativeActiveTurnId;
|
|
1603
|
+
const turn = makeTurn(requestClientId, expectedSteerTurnId ? 'steer' : 'start');
|
|
1604
|
+
// The root dispatch is accepted[0]. Follow-up Lark inputs that win a pre-final
|
|
1605
|
+
// turn/steer are appended by the steer-admission path; at native completion
|
|
1606
|
+
// finalizeAcceptedGroup expands the group into N signed finals.
|
|
1607
|
+
turn.accepted = [{
|
|
1608
|
+
input: message,
|
|
1609
|
+
...(replyTurnId ? { replyTurnId } : {}),
|
|
1610
|
+
...(requestClientId ? { clientUserMessageId: requestClientId } : {}),
|
|
1611
|
+
receivedAtMs: message.receivedAtMs ?? turn.startedAtMs,
|
|
1612
|
+
}];
|
|
1613
|
+
if (expectedSteerTurnId) {
|
|
1614
|
+
turn.nativeTurnId = expectedSteerTurnId;
|
|
1615
|
+
turn.serverStarted = true;
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
// B1: a plain start request is outstanding. startResponsePending is SEPARATE
|
|
1619
|
+
// from steerInFlight — an exact `turn/started` can prove the canonical id and
|
|
1620
|
+
// open pre-final steering while this start response is still pending, and a
|
|
1621
|
+
// late start response must still bind/verify the same canonical id.
|
|
1622
|
+
turn.startResponsePending = true;
|
|
1623
|
+
}
|
|
1624
|
+
activeTurn = turn;
|
|
1625
|
+
// This edge proves the runner decoded and dequeued Botmux's control line,
|
|
1626
|
+
// even if app-server stalls before acknowledging turn/start.
|
|
1627
|
+
emitTurnActivity(turn, 'submitted', true);
|
|
1628
|
+
let built = buildCodexAppTurnStartParams({
|
|
1629
|
+
threadId: tid,
|
|
440
1630
|
cwd: args.cwd,
|
|
441
1631
|
legacyContent: message.content,
|
|
442
1632
|
codexAppInput: message.codexAppInput,
|
|
443
1633
|
codexVersion: version,
|
|
444
|
-
structuredDisabled,
|
|
1634
|
+
structuredDisabled: cleanInputUnsupported,
|
|
445
1635
|
});
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
1636
|
+
// Override the built clientUserMessageId with the reply id when the sidecar
|
|
1637
|
+
// omitted one (e.g. a top-level replyTurnId with no clientUserMessageId).
|
|
1638
|
+
if (built.structured && requestClientId) {
|
|
1639
|
+
built.params.clientUserMessageId = requestClientId;
|
|
1640
|
+
}
|
|
1641
|
+
if (message.codexAppInput && !built.structured && !cleanInputUnsupported && !cleanVersionWarningShown) {
|
|
450
1642
|
cleanVersionWarningShown = true;
|
|
451
1643
|
const found = version ? `${version.major}.${version.minor}.${version.patch}` : 'unknown';
|
|
452
1644
|
writeLine(`[codex-app] clean input requires codex >= 0.135.0 (found ${found}); using legacy prompt`);
|
|
453
1645
|
}
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1646
|
+
for (const path of built.skippedImages) {
|
|
1647
|
+
writeLine(`[codex-app] skipped unreadable local image: ${path}`);
|
|
1648
|
+
}
|
|
1649
|
+
writeLine();
|
|
1650
|
+
writeLine('[user]');
|
|
1651
|
+
writeLine(built.structured && message.codexAppInput ? message.codexAppInput.text : message.content);
|
|
1652
|
+
writeLine();
|
|
1653
|
+
const requestBuiltTurn = (candidate) => {
|
|
1654
|
+
if (!expectedSteerTurnId)
|
|
1655
|
+
return client.request('turn/start', candidate.params);
|
|
1656
|
+
const { threadId, input, clientUserMessageId, additionalContext } = candidate.params;
|
|
1657
|
+
return client.request('turn/steer', {
|
|
1658
|
+
threadId,
|
|
1659
|
+
input,
|
|
1660
|
+
expectedTurnId: expectedSteerTurnId,
|
|
1661
|
+
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
1662
|
+
...(additionalContext ? { additionalContext } : {}),
|
|
1663
|
+
});
|
|
469
1664
|
};
|
|
1665
|
+
let result;
|
|
1666
|
+
let capabilityRetried = false;
|
|
1667
|
+
let inactiveSteerFallback = false;
|
|
1668
|
+
for (;;) {
|
|
1669
|
+
try {
|
|
1670
|
+
result = await requestBuiltTurn(built);
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
catch (err) {
|
|
1674
|
+
if (expectedSteerTurnId
|
|
1675
|
+
&& !inactiveSteerFallback
|
|
1676
|
+
&& nativeActiveTurnId === undefined
|
|
1677
|
+
&& isExplicitExpectedTurnInactive(err)) {
|
|
1678
|
+
// turn/steer was explicitly rejected before acceptance and the signed
|
|
1679
|
+
// native completion already proved the captured Goal turn is gone.
|
|
1680
|
+
// Starting the same client-id input is safe; timeout/transport/generic
|
|
1681
|
+
// errors never enter this branch.
|
|
1682
|
+
inactiveSteerFallback = true;
|
|
1683
|
+
expectedSteerTurnId = undefined;
|
|
1684
|
+
turn.requestKind = 'start';
|
|
1685
|
+
turn.nativeTurnId = undefined;
|
|
1686
|
+
turn.serverStarted = false;
|
|
1687
|
+
turn.pendingCompletions.length = 0;
|
|
1688
|
+
turn.pendingNotifications.length = 0;
|
|
1689
|
+
turn.finalText = '';
|
|
1690
|
+
turn.allAgentText = '';
|
|
1691
|
+
turn.itemText.clear();
|
|
1692
|
+
writeLine('[codex-app] captured native turn completed before steer acceptance; starting the same client-id input as a new turn');
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (capabilityRetried
|
|
1696
|
+
|| !built.structured
|
|
1697
|
+
|| (!expectedSteerTurnId && turn.serverStarted)
|
|
1698
|
+
|| !isCleanInputCapabilityError(err)) {
|
|
1699
|
+
// Nit: the fenced RPC's operation reflects what we actually sent — a Goal
|
|
1700
|
+
// root steers (expectedSteerTurnId set), a plain root starts — so the
|
|
1701
|
+
// signed unknown_outcome/fatal diagnostic is accurate.
|
|
1702
|
+
const fenceOperation = expectedSteerTurnId ? 'turn/steer' : 'turn/start';
|
|
1703
|
+
// R3-B1 fail-closed on positive evidence: once ANY evidence exists that
|
|
1704
|
+
// this native turn may have started or grown — an exact `turn/started`
|
|
1705
|
+
// proof, a server-started edge, or an already-accepted follow-up (group
|
|
1706
|
+
// grew past its root) — a start-RPC failure of ANY kind (even an explicit
|
|
1707
|
+
// RPC error) must FENCE, never synthesize a failure final. Synthesizing a
|
|
1708
|
+
// single root failure would (a) misreport an actually-started turn and
|
|
1709
|
+
// (b) if a follow-up was already steer-accepted and shifted, settle only
|
|
1710
|
+
// the root and strand the follow-up in the worker FIFO (poison). The
|
|
1711
|
+
// durable recovery contract has no authoritative response record proving
|
|
1712
|
+
// this generation can safely continue, so we tear it down.
|
|
1713
|
+
const positiveEvidence = turn.serverStarted
|
|
1714
|
+
|| turn.identityProof !== undefined
|
|
1715
|
+
|| turn.canonicalNativeTurnId !== undefined
|
|
1716
|
+
|| (turn.accepted?.length ?? 0) > 1;
|
|
1717
|
+
if (positiveEvidence) {
|
|
1718
|
+
fenceUnknown(fenceOperation, err instanceof AppServerRpcError ? 'rpc' : 'transport', turn);
|
|
1719
|
+
if (!turn.completed) {
|
|
1720
|
+
turn.completed = true;
|
|
1721
|
+
turn.resolveDone();
|
|
1722
|
+
}
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
// No positive evidence yet. A non-RPC failure (timeout / transport drop)
|
|
1726
|
+
// leaves acceptance genuinely UNKNOWN → fence. An explicit server error
|
|
1727
|
+
// response (any AppServerRpcError, e.g. -32000 "model overloaded")
|
|
1728
|
+
// definitively proves the turn did NOT run → stays on the
|
|
1729
|
+
// throw→failure-final path (a clean, safely-attributed failure).
|
|
1730
|
+
if (!(err instanceof AppServerRpcError)) {
|
|
1731
|
+
fenceUnknown(fenceOperation, 'transport', turn);
|
|
1732
|
+
if (!turn.completed) {
|
|
1733
|
+
turn.completed = true;
|
|
1734
|
+
turn.resolveDone();
|
|
1735
|
+
}
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
throw err;
|
|
1739
|
+
}
|
|
1740
|
+
// The app-server explicitly rejected the experimental field before a turn
|
|
1741
|
+
// started. Disable structured input for this runner lifetime and retry the
|
|
1742
|
+
// preserved legacy prompt exactly once.
|
|
1743
|
+
capabilityRetried = true;
|
|
1744
|
+
cleanInputUnsupported = true;
|
|
1745
|
+
writeLine('[codex-app] clean input unsupported by app-server; retrying this turn with the legacy prompt');
|
|
1746
|
+
built = buildCodexAppTurnStartParams({
|
|
1747
|
+
threadId: tid,
|
|
1748
|
+
cwd: args.cwd,
|
|
1749
|
+
legacyContent: message.content,
|
|
1750
|
+
codexAppInput: message.codexAppInput,
|
|
1751
|
+
codexVersion: version,
|
|
1752
|
+
structuredDisabled: true,
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
const responseNativeId = result.turn?.id ?? result.turnId ?? turn.nativeTurnId;
|
|
1757
|
+
// B1/R4-B2 late start response: if an exact proof (turn/started OR completed)
|
|
1758
|
+
// already bound a canonical id, the start response MUST match it — a divergent
|
|
1759
|
+
// id is a protocol anomaly whose true turn is unknown → fence, never bind a
|
|
1760
|
+
// second id. Otherwise the start response is what proves canonical.
|
|
1761
|
+
if ((turn.identityProof === 'exact_started' || turn.identityProof === 'exact_completed')
|
|
1762
|
+
&& turn.canonicalNativeTurnId
|
|
1763
|
+
&& responseNativeId
|
|
1764
|
+
&& responseNativeId !== turn.canonicalNativeTurnId) {
|
|
1765
|
+
turn.startResponsePending = false;
|
|
1766
|
+
fenceUnknown('turn/start', 'protocol', turn);
|
|
1767
|
+
if (!turn.completed) {
|
|
1768
|
+
turn.completed = true;
|
|
1769
|
+
turn.resolveDone();
|
|
1770
|
+
}
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
turn.nativeTurnId = responseNativeId;
|
|
1774
|
+
turn.requestAccepted = true;
|
|
1775
|
+
turn.startResponsePending = false;
|
|
1776
|
+
// The root request is accepted → its native id is now canonical for the group,
|
|
1777
|
+
// so pre-final follow-up steers may bind against it (canSteer). A plain start
|
|
1778
|
+
// is proven by its start-response; a Goal-continuation root is proven by the
|
|
1779
|
+
// accepted matching steer (identityProof left as the Goal snapshot). An exact
|
|
1780
|
+
// `turn/started` may already have upgraded identityProof to 'exact_started'.
|
|
1781
|
+
if (!turn.completed && turn.nativeTurnId) {
|
|
1782
|
+
turn.canonicalNativeTurnId = turn.nativeTurnId;
|
|
1783
|
+
if (!turn.identityProof) {
|
|
1784
|
+
turn.identityProof = turn.requestKind === 'steer' ? 'goal_snapshot' : 'start_response';
|
|
1785
|
+
}
|
|
1786
|
+
if (turn.phase === undefined || turn.phase === 'starting')
|
|
1787
|
+
turn.phase = 'open';
|
|
1788
|
+
}
|
|
1789
|
+
// A response may arrive after its turn completed or after a Goal
|
|
1790
|
+
// continuation B already started. Never resurrect the completed lifecycle,
|
|
1791
|
+
// and never let late response A overwrite the newer global lifecycle. If A
|
|
1792
|
+
// is still pending and no newer turn exists, temporarily restoring A is safe
|
|
1793
|
+
// because the buffered A completion below clears it immediately.
|
|
1794
|
+
if (!turn.completed
|
|
1795
|
+
&& turn.nativeTurnId
|
|
1796
|
+
&& (nativeActiveTurnId === undefined || nativeActiveTurnId === turn.nativeTurnId)) {
|
|
1797
|
+
nativeActiveTurnId = turn.nativeTurnId;
|
|
1798
|
+
}
|
|
1799
|
+
// B4: replay buffered notifications/completions BEFORE any steer kick. A
|
|
1800
|
+
// completion buffered during the RPC (completion-before-response) must set
|
|
1801
|
+
// completionSeen / close steering here so the kick below sees a closed group
|
|
1802
|
+
// and refuses — otherwise we would steer into an already-completed turn.
|
|
1803
|
+
// B1+B4 / R3-B2: if a completion was buffered as a barrier while this start
|
|
1804
|
+
// response was pending — either a grown steered group (completion_race), or a
|
|
1805
|
+
// single steerable root whose exact-completion was upgraded to canonical proof
|
|
1806
|
+
// before the response — settle it now that the response resolved and matched
|
|
1807
|
+
// the canonical id. settleSteeredCompletion verifies the ordered group
|
|
1808
|
+
// subsequence (1-member is a valid group) and fails closed on any mismatch, so
|
|
1809
|
+
// a foreign/partial buffered completion can never expand a wrong final.
|
|
1810
|
+
// R4-B2 defense-in-depth: the buffered terminal's id MUST equal the proven
|
|
1811
|
+
// canonical id before we settle from it — a first-proof-wins violation upstream
|
|
1812
|
+
// would otherwise let terminal A's content ship under a different native id.
|
|
1813
|
+
const bufferedTerminalMatchesCanonical = turn.terminalCompletion !== undefined
|
|
1814
|
+
&& (typeof turn.terminalCompletion.id !== 'string'
|
|
1815
|
+
|| turn.terminalCompletion.id === turn.canonicalNativeTurnId);
|
|
1816
|
+
const settleBufferedCanonical = !turn.completed
|
|
1817
|
+
&& turn.completionSeen
|
|
1818
|
+
&& turn.terminalCompletion
|
|
1819
|
+
&& !turn.steerInFlight
|
|
1820
|
+
&& bufferedTerminalMatchesCanonical
|
|
1821
|
+
&& turn.accepted?.[0]?.input.codexAppSteerable === true
|
|
1822
|
+
&& ((turn.accepted?.length ?? 0) > 1
|
|
1823
|
+
|| turn.identityProof === 'exact_started'
|
|
1824
|
+
|| turn.identityProof === 'exact_completed');
|
|
1825
|
+
if (settleBufferedCanonical) {
|
|
1826
|
+
settleSteeredCompletion(turn, turn.terminalCompletion);
|
|
1827
|
+
}
|
|
1828
|
+
const pendingNotifications = turn.pendingNotifications.splice(0);
|
|
1829
|
+
for (const notification of pendingNotifications) {
|
|
1830
|
+
const notificationTurnId = notification.params?.turnId ?? notification.params?.turn?.id;
|
|
1831
|
+
if (notificationTurnId === turn.nativeTurnId)
|
|
1832
|
+
handleNotification(notification, true);
|
|
1833
|
+
}
|
|
1834
|
+
const pendingCompletions = turn.pendingCompletions.splice(0);
|
|
1835
|
+
if (pendingCompletions.length > 0 && !turn.completed) {
|
|
1836
|
+
const attributionClientId = turn.clientUserMessageId;
|
|
1837
|
+
const exactMatches = attributionClientId
|
|
1838
|
+
? pendingCompletions.flatMap(completion => {
|
|
1839
|
+
const indexes = exactClientItemIndexes(completion, attributionClientId);
|
|
1840
|
+
return indexes.length === 1 ? [{ completion, itemIndex: indexes[0] }] : [];
|
|
1841
|
+
})
|
|
1842
|
+
: [];
|
|
1843
|
+
const nativeMatches = pendingCompletions.filter(completion => completion?.id === turn.nativeTurnId);
|
|
1844
|
+
if (exactMatches.length === 1) {
|
|
1845
|
+
completeActiveTurnFromNative(turn, exactMatches[0].completion, exactMatches[0].itemIndex);
|
|
1846
|
+
}
|
|
1847
|
+
else if (exactMatches.length > 1 || nativeMatches.length > 1) {
|
|
1848
|
+
reportIdentityConflict(turn, turn.nativeTurnId, 'multiple pre-response completions matched one request');
|
|
1849
|
+
}
|
|
1850
|
+
else if (turn.requestKind === 'steer' && nativeMatches.length === 1) {
|
|
1851
|
+
void reconcileCompletedTurn(turn, turn.nativeTurnId);
|
|
1852
|
+
}
|
|
1853
|
+
else if (turn.requestKind === 'start' && nativeMatches.length === 1) {
|
|
1854
|
+
completeActiveTurnFromNative(turn, nativeMatches[0]);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
// B4: only NOW, after buffered completions were replayed, admit a follow-up.
|
|
1858
|
+
// A follow-up that arrived during the RPC (before canonical was proven) steers
|
|
1859
|
+
// here; if the group already closed (completion-before-response), canSteer
|
|
1860
|
+
// refuses and it stays serial. Never kick before the replay above.
|
|
1861
|
+
if (!turn.completed)
|
|
1862
|
+
void tryAdmitSteer();
|
|
1863
|
+
await turn.done;
|
|
1864
|
+
// Expand the ordered accepted group into N signed finals (N===1 for every
|
|
1865
|
+
// pre-driver path — single start, Goal-steer root, legacy — byte-identical to
|
|
1866
|
+
// the old single-final contract). clientUserMessageId is the daemon-frozen
|
|
1867
|
+
// botmux/Lark turn identity; the app-server generates a different native id
|
|
1868
|
+
// for the same logical turn, exposing which as `turnId` would break daemon
|
|
1869
|
+
// wait maps / VC suppression / reply routing. When no structured sidecar
|
|
1870
|
+
// exists, turnId is omitted so the worker resolves it from its own FIFO head.
|
|
1871
|
+
finalizeAcceptedGroup(turn);
|
|
1872
|
+
writeLine();
|
|
1873
|
+
activeTurn = null;
|
|
1874
|
+
}
|
|
1875
|
+
async function drainQueue() {
|
|
1876
|
+
if (processing)
|
|
1877
|
+
return;
|
|
1878
|
+
processing = true;
|
|
1879
|
+
try {
|
|
1880
|
+
while (queue.length > 0) {
|
|
1881
|
+
// A fenced generation must not consume any more input: an unknown
|
|
1882
|
+
// turn/start|turn/steer outcome left a head whose disposition is unknown,
|
|
1883
|
+
// and starting the next queued input would submit against a poisoned FIFO.
|
|
1884
|
+
// The signed fatal already tore the generation down; stop draining.
|
|
1885
|
+
if (generationFenced)
|
|
1886
|
+
break;
|
|
1887
|
+
// Goal gate (B3): while an autonomous Goal continuation is native-busy,
|
|
1888
|
+
// only a daemon-authorized steerable head may enter it (as a Goal-steer
|
|
1889
|
+
// root). A missing/false-flag head must stay serial — leave it in the
|
|
1890
|
+
// queue and wait for the Goal to complete, at which point turn/completed's
|
|
1891
|
+
// no-active-turn branch re-kicks drainQueue so the head starts its own
|
|
1892
|
+
// turn. Without this a special-sink / non-interactive input would be
|
|
1893
|
+
// silently merged into the Goal turn and its output mis-delivered.
|
|
1894
|
+
if (nativeActiveTurnId !== undefined && queue[0].codexAppSteerable !== true) {
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
const next = queue.shift();
|
|
1898
|
+
try {
|
|
1899
|
+
await runTurn(next);
|
|
1900
|
+
}
|
|
1901
|
+
catch (err) {
|
|
1902
|
+
// A fenced generation already emitted the signed unknown_outcome+fatal;
|
|
1903
|
+
// never synthesize a failure final on top (it would advance the worker
|
|
1904
|
+
// FIFO past a turn whose true disposition is unknown).
|
|
1905
|
+
if (generationFenced) {
|
|
1906
|
+
activeTurn = null;
|
|
1907
|
+
break;
|
|
1908
|
+
}
|
|
1909
|
+
const message = `Codex App runner error: ${err?.message ?? err}`;
|
|
1910
|
+
const completedAtMs = Date.now();
|
|
1911
|
+
const replyTurnId = next.replyTurnId;
|
|
1912
|
+
const nativeTurnId = activeTurn?.nativeTurnId;
|
|
1913
|
+
writeLine(message);
|
|
1914
|
+
emitFinalMarker({
|
|
1915
|
+
...(replyTurnId ? { turnId: replyTurnId } : {}),
|
|
1916
|
+
...(nativeTurnId ? { nativeTurnId } : {}),
|
|
1917
|
+
content: message,
|
|
1918
|
+
startedAtMs: activeTurn?.startedAtMs ?? completedAtMs,
|
|
1919
|
+
completedAtMs,
|
|
1920
|
+
});
|
|
1921
|
+
activeTurn = null;
|
|
1922
|
+
}
|
|
1923
|
+
// Do not publish a transient idle boundary between inputs already queued
|
|
1924
|
+
// in the serial runner. Once the queue is truly empty, append signed
|
|
1925
|
+
// busy:false only AFTER completed + every final fragment. The worker can
|
|
1926
|
+
// therefore become ready even if the terminal prompt is lost, while its
|
|
1927
|
+
// IPC order remains final_output before prompt_ready.
|
|
1928
|
+
//
|
|
1929
|
+
// A queue that is non-empty only because its head is a false-flag input
|
|
1930
|
+
// parked behind an active Goal (the B3 gate above) is NOT drainable now:
|
|
1931
|
+
// treat it like an empty queue so the runner still advertises native-busy
|
|
1932
|
+
// and never wedges without an idle/ready edge.
|
|
1933
|
+
const parkedBehindGoal = queue.length > 0
|
|
1934
|
+
&& nativeActiveTurnId !== undefined
|
|
1935
|
+
&& queue[0].codexAppSteerable !== true;
|
|
1936
|
+
if (queue.length === 0 || parkedBehindGoal) {
|
|
1937
|
+
const nativeBusy = nativeActiveTurnId !== undefined;
|
|
1938
|
+
emitRunnerState(nativeBusy, !nativeBusy);
|
|
1939
|
+
if (!nativeBusy)
|
|
1940
|
+
prompt();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
finally {
|
|
1945
|
+
processing = false;
|
|
1946
|
+
}
|
|
470
1947
|
}
|
|
471
|
-
controller = new CodexAppTurnController({
|
|
472
|
-
cwd: args.cwd,
|
|
473
|
-
ensureThread,
|
|
474
|
-
request: (method, params) => client.request(method, params),
|
|
475
|
-
prepareInput: prepareControllerInput,
|
|
476
|
-
isStartCapabilityError: isCleanInputCapabilityError,
|
|
477
|
-
onTurnInput(_input, prepared) {
|
|
478
|
-
writeLine();
|
|
479
|
-
writeLine('[user]');
|
|
480
|
-
writeLine(prepared.visibleText);
|
|
481
|
-
writeLine();
|
|
482
|
-
},
|
|
483
|
-
onOutput: text => output.display(text),
|
|
484
|
-
onDiagnostic: writeLine,
|
|
485
|
-
onLifecycle: event => emitMarker('lifecycle', event),
|
|
486
|
-
onFinal: marker => {
|
|
487
|
-
// Attach this turn's token usage (if the accumulator saw coherent totals)
|
|
488
|
-
// and drain its accumulator. Omitted when no usage was observed — never zeros.
|
|
489
|
-
const acc = marker.appTurnId ? usageAccumulators.get(marker.appTurnId) : undefined;
|
|
490
|
-
const usage = acc?.result() ?? undefined;
|
|
491
|
-
// Surface a protocol anomaly rather than silently omitting usage — a
|
|
492
|
-
// regression/negative-baseline should be visible in the runner log.
|
|
493
|
-
if (acc?.warning && !usage) {
|
|
494
|
-
writeLine(`[codex-app] token usage dropped for turn ${marker.appTurnId ?? '?'}: ${acc.warning}`);
|
|
495
|
-
}
|
|
496
|
-
if (marker.appTurnId)
|
|
497
|
-
usageAccumulators.delete(marker.appTurnId);
|
|
498
|
-
emitMarker('final', usage ? { ...marker, usage } : marker);
|
|
499
|
-
writeLine();
|
|
500
|
-
},
|
|
501
|
-
onPrompt: prompt,
|
|
502
|
-
});
|
|
503
1948
|
function enqueueLine(line) {
|
|
504
1949
|
const trimmed = line.trim();
|
|
505
1950
|
if (!trimmed)
|
|
506
1951
|
return;
|
|
507
|
-
if (trimmed.startsWith(
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1952
|
+
if (trimmed.startsWith('::botmux-codex-app:')) {
|
|
1953
|
+
const encoded = trimmed.slice('::botmux-codex-app:'.length);
|
|
1954
|
+
try {
|
|
1955
|
+
const decoded = JSON.parse(Buffer.from(encoded, 'base64').toString('utf8'));
|
|
1956
|
+
if (decoded?.type === 'message' && typeof decoded.content === 'string') {
|
|
1957
|
+
const codexAppInput = isCodexAppTurnInput(decoded.codexAppInput)
|
|
1958
|
+
? decoded.codexAppInput
|
|
1959
|
+
: undefined;
|
|
1960
|
+
if (decoded.codexAppInput !== undefined && !codexAppInput) {
|
|
1961
|
+
writeLine('[codex-app] ignored invalid structured input sidecar');
|
|
1962
|
+
}
|
|
1963
|
+
// Reply routing identity: prefer the top-level replyTurnId, else fall
|
|
1964
|
+
// back to the structured sidecar's clientUserMessageId (mirrors the
|
|
1965
|
+
// shared decodeCodexAppRunnerInput contract).
|
|
1966
|
+
const replyTurnId = typeof decoded.replyTurnId === 'string' && decoded.replyTurnId.length > 0
|
|
1967
|
+
? decoded.replyTurnId
|
|
1968
|
+
: codexAppInput?.clientUserMessageId;
|
|
1969
|
+
// Explicit positive only: the daemon admission gate (decision A) sets
|
|
1970
|
+
// `true` solely for a plain-human-interactive turn. Any other value is
|
|
1971
|
+
// treated as absent → forced serial, never a silent steer authorization.
|
|
1972
|
+
const codexAppSteerable = decoded.codexAppSteerable === true;
|
|
1973
|
+
queue.push({
|
|
1974
|
+
content: decoded.content,
|
|
1975
|
+
codexAppInput,
|
|
1976
|
+
...(replyTurnId ? { replyTurnId } : {}),
|
|
1977
|
+
...(codexAppSteerable ? { codexAppSteerable: true } : {}),
|
|
1978
|
+
receivedAtMs: Date.now(),
|
|
1979
|
+
});
|
|
1980
|
+
// If a steerable group is active, opportunistically steer this head into
|
|
1981
|
+
// it (pre-final). Otherwise fall through to the serial drain, which
|
|
1982
|
+
// starts it as its own turn once the runner is idle. drainQueue's
|
|
1983
|
+
// `await runTurn(root)` keeps the root active until its group settles, so
|
|
1984
|
+
// a head consumed by tryAdmitSteer is never double-processed by drain.
|
|
1985
|
+
void tryAdmitSteer();
|
|
1986
|
+
void drainQueue();
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
catch (err) {
|
|
1990
|
+
writeLine(`[codex-app] bad botmux input: ${err?.message ?? err}`);
|
|
1991
|
+
}
|
|
513
1992
|
return;
|
|
514
1993
|
}
|
|
515
|
-
|
|
1994
|
+
queue.push({ content: line });
|
|
1995
|
+
void drainQueue();
|
|
516
1996
|
}
|
|
517
1997
|
function handleInput(data) {
|
|
518
1998
|
const text = data.toString('utf8');
|
|
@@ -534,16 +2014,31 @@ function handleInput(data) {
|
|
|
534
2014
|
}
|
|
535
2015
|
}
|
|
536
2016
|
async function main() {
|
|
2017
|
+
const testTimeout = process.env.NODE_ENV === 'test'
|
|
2018
|
+
? Number(process.env.BOTMUX_TEST_CODEX_APP_STARTUP_TIMEOUT_MS)
|
|
2019
|
+
: Number.NaN;
|
|
2020
|
+
const startupTimeoutMs = Number.isFinite(testTimeout) && testTimeout > 0
|
|
2021
|
+
? testTimeout
|
|
2022
|
+
: 90_000;
|
|
2023
|
+
const startupDeadlineAtMs = Date.now() + startupTimeoutMs;
|
|
2024
|
+
const authTimeout = armCodexAppControlStartupTimeout(() => {
|
|
2025
|
+
console.error('Codex App startup timed out before the first signed runner state');
|
|
2026
|
+
process.exit(2);
|
|
2027
|
+
}, startupTimeoutMs);
|
|
2028
|
+
await controlReady;
|
|
2029
|
+
client = new AppServerClient(args.codexBin, args.cwd);
|
|
537
2030
|
client.onRequest(handleServerRequest);
|
|
538
2031
|
client.onNotification(handleNotification);
|
|
539
|
-
client.
|
|
540
|
-
|
|
541
|
-
process.exitCode = 1;
|
|
542
|
-
process.stdout.write('', () => process.exit(1));
|
|
543
|
-
});
|
|
544
|
-
await client.initialize();
|
|
545
|
-
await ensureThread();
|
|
2032
|
+
await client.initialize(startupRequestTimeout(startupDeadlineAtMs, 'initialize'));
|
|
2033
|
+
await ensureThread(startupDeadlineAtMs);
|
|
546
2034
|
writeLine('Codex App connected.');
|
|
2035
|
+
runnerReady = true;
|
|
2036
|
+
// Initial readiness is signed too; never rely on terminal rendering as the
|
|
2037
|
+
// only path that releases the worker's first-prompt gate.
|
|
2038
|
+
emitRunnerState(false);
|
|
2039
|
+
// Authentication is deliberately insufficient. Keep the absolute startup
|
|
2040
|
+
// timer armed through initialize + resume/start and the first signed state.
|
|
2041
|
+
clearTimeout(authTimeout);
|
|
547
2042
|
if (process.stdin.isTTY)
|
|
548
2043
|
process.stdin.setRawMode(true);
|
|
549
2044
|
process.stdin.resume();
|
|
@@ -551,14 +2046,24 @@ async function main() {
|
|
|
551
2046
|
prompt();
|
|
552
2047
|
}
|
|
553
2048
|
process.on('SIGTERM', () => {
|
|
554
|
-
|
|
2049
|
+
if (controlReconnectTimer)
|
|
2050
|
+
clearTimeout(controlReconnectTimer);
|
|
2051
|
+
controlSocket?.destroy();
|
|
2052
|
+
client?.close();
|
|
555
2053
|
process.exit(0);
|
|
556
2054
|
});
|
|
557
2055
|
process.on('SIGINT', () => {
|
|
558
|
-
|
|
2056
|
+
if (controlReconnectTimer)
|
|
2057
|
+
clearTimeout(controlReconnectTimer);
|
|
2058
|
+
controlSocket?.destroy();
|
|
2059
|
+
client?.close();
|
|
559
2060
|
process.exit(130);
|
|
560
2061
|
});
|
|
561
2062
|
main().catch(err => {
|
|
2063
|
+
if (!runnerReady && err instanceof AppServerRequestTimeoutError) {
|
|
2064
|
+
output.error('Codex App startup timed out before the first signed runner state\n');
|
|
2065
|
+
process.exit(2);
|
|
2066
|
+
}
|
|
562
2067
|
output.error(`${err?.stack ?? err?.message ?? err}\n`);
|
|
563
2068
|
process.exit(1);
|
|
564
2069
|
});
|