botmux 3.5.2 → 3.6.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/README.en.md +56 -396
- package/README.md +55 -396
- package/dist/.runtime-build-id +1 -0
- package/dist/adapters/cli/claude-code.d.ts +6 -0
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +197 -11
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.js +4 -4
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +9 -6
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/codex.js +5 -5
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
- package/dist/adapters/cli/fs-policy.js +5 -7
- package/dist/adapters/cli/fs-policy.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts +2 -1
- package/dist/adapters/cli/grok.d.ts.map +1 -1
- package/dist/adapters/cli/grok.js +2 -1
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/hermes.d.ts.map +1 -1
- package/dist/adapters/cli/hermes.js +8 -4
- package/dist/adapters/cli/hermes.js.map +1 -1
- package/dist/adapters/cli/mtr.d.ts.map +1 -1
- package/dist/adapters/cli/mtr.js +3 -2
- package/dist/adapters/cli/mtr.js.map +1 -1
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +5 -4
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/relay.js +2 -2
- package/dist/adapters/cli/relay.js.map +1 -1
- package/dist/adapters/cli/runner-input.d.ts +2 -2
- package/dist/adapters/cli/runner-input.d.ts.map +1 -1
- package/dist/adapters/cli/runner-input.js +9 -6
- package/dist/adapters/cli/runner-input.js.map +1 -1
- package/dist/adapters/cli/seed.d.ts.map +1 -1
- package/dist/adapters/cli/seed.js +2 -1
- package/dist/adapters/cli/seed.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +3 -2
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +25 -18
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +28 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +75 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +272 -86
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +85 -192
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/config.d.ts +0 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -30
- package/dist/config.js.map +1 -1
- package/dist/core/ask-broker.d.ts +12 -0
- package/dist/core/ask-broker.d.ts.map +1 -1
- package/dist/core/ask-broker.js +41 -0
- package/dist/core/ask-broker.js.map +1 -1
- package/dist/core/bot-turn-mutation-gate.d.ts +24 -0
- package/dist/core/bot-turn-mutation-gate.d.ts.map +1 -0
- package/dist/core/bot-turn-mutation-gate.js +369 -0
- package/dist/core/bot-turn-mutation-gate.js.map +1 -0
- package/dist/core/command-handler.d.ts +5 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +38 -16
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +330 -57
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +8 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/host-overload-alert.d.ts +157 -0
- package/dist/core/host-overload-alert.d.ts.map +1 -0
- package/dist/core/host-overload-alert.js +251 -0
- package/dist/core/host-overload-alert.js.map +1 -0
- package/dist/core/passthrough-commands.d.ts +8 -0
- package/dist/core/passthrough-commands.d.ts.map +1 -1
- package/dist/core/passthrough-commands.js +19 -0
- package/dist/core/passthrough-commands.js.map +1 -1
- package/dist/core/restart-coordinator.d.ts +28 -0
- package/dist/core/restart-coordinator.d.ts.map +1 -0
- package/dist/core/restart-coordinator.js +82 -0
- package/dist/core/restart-coordinator.js.map +1 -0
- package/dist/core/restart-followup-policy.d.ts +112 -0
- package/dist/core/restart-followup-policy.d.ts.map +1 -0
- package/dist/core/restart-followup-policy.js +49 -0
- package/dist/core/restart-followup-policy.js.map +1 -0
- package/dist/core/role-library.d.ts +1 -1
- package/dist/core/role-library.js +1 -1
- package/dist/core/session-cwd.d.ts.map +1 -1
- package/dist/core/session-cwd.js +8 -0
- package/dist/core/session-cwd.js.map +1 -1
- package/dist/core/session-discovery.d.ts +14 -0
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +25 -0
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-liveness.d.ts +15 -0
- package/dist/core/session-liveness.d.ts.map +1 -0
- package/dist/core/session-liveness.js +76 -0
- package/dist/core/session-liveness.js.map +1 -0
- package/dist/core/session-row-enrichment.d.ts +15 -0
- package/dist/core/session-row-enrichment.d.ts.map +1 -0
- package/dist/core/session-row-enrichment.js +93 -0
- package/dist/core/session-row-enrichment.js.map +1 -0
- package/dist/core/slash-inject.js +1 -1
- package/dist/core/slash-inject.js.map +1 -1
- package/dist/core/trigger-final-suppression.d.ts +24 -0
- package/dist/core/trigger-final-suppression.d.ts.map +1 -0
- package/dist/core/trigger-final-suppression.js +97 -0
- package/dist/core/trigger-final-suppression.js.map +1 -0
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +64 -4
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +6 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +20 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +283 -32
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +5 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +520 -35
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +1 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +34 -6
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +3 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +3 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/compat.d.ts +102 -2
- package/dist/dashboard/compat.d.ts.map +1 -1
- package/dist/dashboard/compat.js +78 -14
- package/dist/dashboard/compat.js.map +1 -1
- package/dist/dashboard/connector-api.d.ts.map +1 -1
- package/dist/dashboard/connector-api.js +1 -0
- package/dist/dashboard/connector-api.js.map +1 -1
- package/dist/dashboard/desktop-asks.d.ts +21 -0
- package/dist/dashboard/desktop-asks.d.ts.map +1 -0
- package/dist/dashboard/desktop-asks.js +49 -0
- package/dist/dashboard/desktop-asks.js.map +1 -0
- package/dist/dashboard/public-redact.d.ts +7 -0
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +35 -3
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/session-presentation.d.ts +16 -0
- package/dist/dashboard/session-presentation.d.ts.map +1 -0
- package/dist/dashboard/session-presentation.js +62 -0
- package/dist/dashboard/session-presentation.js.map +1 -0
- package/dist/dashboard/settings-write-applier.d.ts +2 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +19 -1
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/trigger-api.d.ts.map +1 -1
- package/dist/dashboard/trigger-api.js +19 -1
- package/dist/dashboard/trigger-api.js.map +1 -1
- package/dist/dashboard/vc-consumer-profiles-api.d.ts +7 -2
- package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -1
- package/dist/dashboard/vc-consumer-profiles-api.js +13 -0
- package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +18 -5
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +97 -35
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +3 -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/client-shell.d.ts +29 -0
- package/dist/dashboard/web/client-shell.d.ts.map +1 -0
- package/dist/dashboard/web/client-shell.js +89 -0
- package/dist/dashboard/web/client-shell.js.map +1 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +5 -1
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +100 -8
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/preferences.d.ts +7 -0
- package/dist/dashboard/web/preferences.d.ts.map +1 -1
- package/dist/dashboard/web/preferences.js +32 -0
- package/dist/dashboard/web/preferences.js.map +1 -1
- package/dist/dashboard/web/sessions-kanban.d.ts +1 -1
- package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-kanban.js +1 -1
- 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 +77 -23
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +4 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +14 -1
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +6 -0
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +55 -30
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/store.d.ts +1 -2
- package/dist/dashboard/web/store.d.ts.map +1 -1
- package/dist/dashboard/web/store.js +69 -20
- package/dist/dashboard/web/store.js.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.js +128 -42
- package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
- package/dist/dashboard/webhook-routes.d.ts.map +1 -1
- package/dist/dashboard/webhook-routes.js +9 -2
- package/dist/dashboard/webhook-routes.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-VQMPUJ4Y.js +10 -0
- package/dist/dashboard-web/chunks/chunk-AGCPF6MT.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-KPQAUX7V.js → chunk-JMRGZQDB.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-R7IAKAYX.js → chunk-NP4JUBGS.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-NRL6CVAS.js → chunk-P327LXOT.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-X2L65IOM.js → chunk-SNKATZWK.js} +15 -15
- package/dist/dashboard-web/chunks/connectors-page-DOY4J3ZL.js +1 -0
- package/dist/dashboard-web/chunks/{groups-page-6DKXBYOM.js → groups-page-74TS2DIH.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-NWARTJ6J.js → insights-page-AYCO65JC.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-AGX3XRJX.js → monitor-room-YKYLTSAN.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-GGGIWN4C.js → monitoring-page-QBIUNRBR.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-6SIVKSK3.js → office-page-4PH2A26P.js} +1 -1
- package/dist/dashboard-web/chunks/overview-page-H73D5UT4.js +1 -0
- package/dist/dashboard-web/chunks/{roles-page-WE2YB7KN.js → roles-page-U4BTOQ7F.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-TB3I24XN.js → schedules-page-RHRHX7R7.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-VSC6AM5G.js +2 -0
- package/dist/dashboard-web/chunks/settings-page-LF3RKM5P.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-3QAT2IWD.js → skills-page-SYOIGGXH.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-K4VVAVAK.js → team-federation-page-RMBLASTW.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-V6IOJR2B.js → v3-page-XJXKKJRN.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-V6CGKRNX.js → whiteboards-page-K3YVSTHJ.js} +1 -1
- package/dist/dashboard-web/style.css +551 -0
- package/dist/dashboard.js +153 -9
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +11 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +23 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +5 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +5 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +1 -1
- package/dist/im/lark/card-builder.js +1 -1
- package/dist/im/lark/card-handler.d.ts +10 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +163 -17
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +11 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +47 -18
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +13 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +20 -0
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/identity-cache.d.ts +25 -1
- package/dist/im/lark/identity-cache.d.ts.map +1 -1
- package/dist/im/lark/identity-cache.js +47 -1
- package/dist/im/lark/identity-cache.js.map +1 -1
- package/dist/im/lark/overload-nonce.d.ts +19 -0
- package/dist/im/lark/overload-nonce.d.ts.map +1 -0
- package/dist/im/lark/overload-nonce.js +72 -0
- package/dist/im/lark/overload-nonce.js.map +1 -0
- package/dist/im/lark/v3-progress-card.d.ts.map +1 -1
- package/dist/im/lark/v3-progress-card.js +5 -0
- package/dist/im/lark/v3-progress-card.js.map +1 -1
- package/dist/services/async-trigger-state.d.ts +82 -0
- package/dist/services/async-trigger-state.d.ts.map +1 -0
- package/dist/services/async-trigger-state.js +95 -0
- package/dist/services/async-trigger-state.js.map +1 -0
- package/dist/services/async-trigger-store.d.ts +28 -0
- package/dist/services/async-trigger-store.d.ts.map +1 -0
- package/dist/services/async-trigger-store.js +120 -0
- package/dist/services/async-trigger-store.js.map +1 -0
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +34 -2
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bridge-fallback-gate.d.ts +32 -0
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +39 -0
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +15 -1
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/card-prefs-store.d.ts +4 -0
- package/dist/services/card-prefs-store.d.ts.map +1 -1
- package/dist/services/card-prefs-store.js +8 -0
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/claude-transcript.d.ts +23 -0
- package/dist/services/claude-transcript.d.ts.map +1 -1
- package/dist/services/claude-transcript.js +33 -0
- package/dist/services/claude-transcript.js.map +1 -1
- package/dist/services/codex-app-runner-protocol.d.ts +61 -0
- package/dist/services/codex-app-runner-protocol.d.ts.map +1 -0
- package/dist/services/codex-app-runner-protocol.js +170 -0
- package/dist/services/codex-app-runner-protocol.js.map +1 -0
- package/dist/services/codex-app-turn-controller.d.ts +72 -0
- package/dist/services/codex-app-turn-controller.d.ts.map +1 -0
- package/dist/services/codex-app-turn-controller.js +524 -0
- package/dist/services/codex-app-turn-controller.js.map +1 -0
- package/dist/services/codex-runner-freshness.d.ts +39 -0
- package/dist/services/codex-runner-freshness.d.ts.map +1 -0
- package/dist/services/codex-runner-freshness.js +78 -0
- package/dist/services/codex-runner-freshness.js.map +1 -0
- package/dist/services/connector-store.d.ts +6 -0
- package/dist/services/connector-store.d.ts.map +1 -1
- package/dist/services/connector-store.js.map +1 -1
- package/dist/services/grant-store.d.ts.map +1 -1
- package/dist/services/grant-store.js +16 -0
- package/dist/services/grant-store.js.map +1 -1
- package/dist/services/hermes-transcript.d.ts +1 -0
- package/dist/services/hermes-transcript.d.ts.map +1 -1
- package/dist/services/hermes-transcript.js +49 -0
- package/dist/services/hermes-transcript.js.map +1 -1
- package/dist/services/schedule-split-migration.d.ts +2 -0
- package/dist/services/schedule-split-migration.d.ts.map +1 -0
- package/dist/services/schedule-split-migration.js +159 -0
- package/dist/services/schedule-split-migration.js.map +1 -0
- package/dist/services/schedule-store.d.ts +28 -4
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +118 -33
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/services/trigger-types.d.ts +18 -1
- package/dist/services/trigger-types.d.ts.map +1 -1
- package/dist/services/trigger-types.js +3 -0
- package/dist/services/trigger-types.js.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-store.d.ts.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-store.js +1 -0
- package/dist/services/vc-meeting-consumer-profile-store.js.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-templates.d.ts +32 -0
- package/dist/services/vc-meeting-consumer-profile-templates.d.ts.map +1 -0
- package/dist/services/vc-meeting-consumer-profile-templates.js +106 -0
- package/dist/services/vc-meeting-consumer-profile-templates.js.map +1 -0
- package/dist/services/vc-meeting-delivery-feed.d.ts +1 -1
- package/dist/services/vc-meeting-delivery-feed.d.ts.map +1 -1
- package/dist/services/vc-meeting-delivery-feed.js +2 -1
- package/dist/services/vc-meeting-delivery-feed.js.map +1 -1
- package/dist/services/vc-meeting-delivery-hub-store.d.ts +3 -1
- package/dist/services/vc-meeting-delivery-hub-store.d.ts.map +1 -1
- package/dist/services/vc-meeting-delivery-hub-store.js +11 -0
- package/dist/services/vc-meeting-delivery-hub-store.js.map +1 -1
- package/dist/services/vc-meeting-delivery-protocol.d.ts +9 -2
- package/dist/services/vc-meeting-delivery-protocol.d.ts.map +1 -1
- package/dist/services/vc-meeting-delivery-protocol.js +10 -1
- package/dist/services/vc-meeting-delivery-protocol.js.map +1 -1
- package/dist/services/vc-meeting-delivery-receiver.d.ts.map +1 -1
- package/dist/services/vc-meeting-delivery-receiver.js +10 -1
- package/dist/services/vc-meeting-delivery-receiver.js.map +1 -1
- package/dist/services/vc-meeting-delivery-store.d.ts +12 -2
- package/dist/services/vc-meeting-delivery-store.d.ts.map +1 -1
- package/dist/services/vc-meeting-delivery-store.js +14 -2
- package/dist/services/vc-meeting-delivery-store.js.map +1 -1
- package/dist/services/vc-meeting-im-reply.d.ts +2 -1
- package/dist/services/vc-meeting-im-reply.d.ts.map +1 -1
- package/dist/services/vc-meeting-im-reply.js +19 -3
- package/dist/services/vc-meeting-im-reply.js.map +1 -1
- package/dist/services/vc-meeting-listener-output-protocol.d.ts +27 -0
- package/dist/services/vc-meeting-listener-output-protocol.d.ts.map +1 -0
- package/dist/services/vc-meeting-listener-output-protocol.js +58 -0
- package/dist/services/vc-meeting-listener-output-protocol.js.map +1 -0
- package/dist/services/vc-meeting-listener-topic-store.d.ts +31 -0
- package/dist/services/vc-meeting-listener-topic-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-listener-topic-store.js +120 -0
- package/dist/services/vc-meeting-listener-topic-store.js.map +1 -0
- package/dist/services/vc-meeting-runtime-store.d.ts +2 -1
- package/dist/services/vc-meeting-runtime-store.d.ts.map +1 -1
- package/dist/services/vc-meeting-runtime-store.js +8 -0
- package/dist/services/vc-meeting-runtime-store.js.map +1 -1
- package/dist/services/vc-meeting-send-policy.d.ts +8 -1
- package/dist/services/vc-meeting-send-policy.d.ts.map +1 -1
- package/dist/services/vc-meeting-send-policy.js +8 -0
- package/dist/services/vc-meeting-send-policy.js.map +1 -1
- package/dist/types.d.ts +40 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/allowed-users-apply.d.ts +76 -0
- package/dist/utils/allowed-users-apply.d.ts.map +1 -0
- package/dist/utils/allowed-users-apply.js +123 -0
- package/dist/utils/allowed-users-apply.js.map +1 -0
- package/dist/utils/allowed-users-cache.d.ts +29 -0
- package/dist/utils/allowed-users-cache.d.ts.map +1 -0
- package/dist/utils/allowed-users-cache.js +82 -0
- package/dist/utils/allowed-users-cache.js.map +1 -0
- package/dist/utils/child-env.d.ts +2 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +2 -1
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/cli-usage-limit.d.ts +27 -1
- package/dist/utils/cli-usage-limit.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.js +92 -20
- package/dist/utils/cli-usage-limit.js.map +1 -1
- package/dist/utils/input-gate.d.ts +2 -0
- package/dist/utils/input-gate.d.ts.map +1 -1
- package/dist/utils/input-gate.js +2 -0
- package/dist/utils/input-gate.js.map +1 -1
- package/dist/utils/runtime-build-id.d.ts +21 -0
- package/dist/utils/runtime-build-id.d.ts.map +1 -0
- package/dist/utils/runtime-build-id.js +83 -0
- package/dist/utils/runtime-build-id.js.map +1 -0
- package/dist/utils/terminal-renderer.d.ts +3 -3
- package/dist/utils/terminal-renderer.js +3 -3
- package/dist/vc-agent/cards.d.ts.map +1 -1
- package/dist/vc-agent/cards.js +21 -1
- package/dist/vc-agent/cards.js.map +1 -1
- package/dist/worker.js +601 -179
- package/dist/worker.js.map +1 -1
- package/dist/workflows/hostExecutors/botmux-schedule.d.ts.map +1 -1
- package/dist/workflows/hostExecutors/botmux-schedule.js +13 -1
- package/dist/workflows/hostExecutors/botmux-schedule.js.map +1 -1
- package/dist/workflows/migration/v2-run-archive-schema.d.ts +20 -20
- package/dist/workflows/migration/v2-to-v3.d.ts.map +1 -1
- package/dist/workflows/migration/v2-to-v3.js +5 -1
- package/dist/workflows/migration/v2-to-v3.js.map +1 -1
- package/dist/workflows/v3/architect.d.ts.map +1 -1
- package/dist/workflows/v3/architect.js +1 -0
- package/dist/workflows/v3/architect.js.map +1 -1
- package/dist/workflows/v3/cli-run.d.ts +9 -0
- package/dist/workflows/v3/cli-run.d.ts.map +1 -1
- package/dist/workflows/v3/cli-run.js +17 -2
- package/dist/workflows/v3/cli-run.js.map +1 -1
- package/dist/workflows/v3/goal-cli.d.ts +58 -0
- package/dist/workflows/v3/goal-cli.d.ts.map +1 -0
- package/dist/workflows/v3/goal-cli.js +430 -0
- package/dist/workflows/v3/goal-cli.js.map +1 -0
- package/dist/workflows/v3/library-materialize.d.ts.map +1 -1
- package/dist/workflows/v3/library-materialize.js +5 -1
- package/dist/workflows/v3/library-materialize.js.map +1 -1
- package/dist/workflows/v3/library-schema.d.ts +21 -0
- package/dist/workflows/v3/library-schema.d.ts.map +1 -1
- package/dist/workflows/v3/library-schema.js +95 -1
- package/dist/workflows/v3/library-schema.js.map +1 -1
- package/dist/workflows/v3/progress-projection.d.ts +3 -0
- package/dist/workflows/v3/progress-projection.d.ts.map +1 -1
- package/dist/workflows/v3/progress-projection.js +23 -1
- package/dist/workflows/v3/progress-projection.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +46 -1
- package/dist/workflows/v3/run-envelope.d.ts.map +1 -1
- package/dist/workflows/v3/run-envelope.js +32 -1
- package/dist/workflows/v3/run-envelope.js.map +1 -1
- package/package.json +2 -2
- package/dist/dashboard-web/chunks/bot-defaults-page-AUKV7P4L.js +0 -10
- package/dist/dashboard-web/chunks/chunk-SZOB6VK2.js +0 -1
- package/dist/dashboard-web/chunks/connectors-page-HDQFEXZI.js +0 -1
- package/dist/dashboard-web/chunks/overview-page-FVR3NKAF.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-KOOOASIL.js +0 -2
- package/dist/dashboard-web/chunks/settings-page-IEBR7TGN.js +0 -2
- package/dist/utils/screen-analyzer.d.ts +0 -75
- package/dist/utils/screen-analyzer.d.ts.map +0 -1
- package/dist/utils/screen-analyzer.js +0 -333
- package/dist/utils/screen-analyzer.js.map +0 -1
package/dist/worker.js
CHANGED
|
@@ -22,11 +22,13 @@ import { evaluateCredentialOnlyIsolationGate, credentialIsolationRequired, devic
|
|
|
22
22
|
import { buildFsPolicy, compileToSeatbelt, migrateLegacySandboxFields } from './adapters/cli/fs-policy.js';
|
|
23
23
|
import { killPersistentBackendTarget, killPersistentSession, probePersistentBackendTarget } from './core/persistent-backend.js';
|
|
24
24
|
import { readProcessStartIdentity } from './core/session-marker.js';
|
|
25
|
-
import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
|
|
25
|
+
import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff, isTranscriptRateLimitEvent, apiErrorMessageText } from './services/claude-transcript.js';
|
|
26
26
|
import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
|
|
27
|
-
import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
|
|
27
|
+
import { shouldEmitEmptyCompletedBridgeFallback, shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
|
|
28
28
|
import { decideHardTimeoutAction, decideSettleMarkReady, shouldReleaseFirstPromptTimeout, shouldWaitForPostSessionStartPromptEvidence, shouldWriteNow, } from './utils/input-gate.js';
|
|
29
|
+
import { decideCodexRunnerFreshness, CodexRunnerFreshnessInputQueue, shouldHoldCodexRunnerInput, } from './services/codex-runner-freshness.js';
|
|
29
30
|
import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirst } from './core/inject-queue-policy.js';
|
|
31
|
+
import { decideRestartFollowup, settleDurableTurnForRestart } from './core/restart-followup-policy.js';
|
|
30
32
|
import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
|
|
31
33
|
import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
|
|
32
34
|
import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
|
|
@@ -78,7 +80,7 @@ import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_R
|
|
|
78
80
|
import { createCliAdapterSync, locateOnPath } from './adapters/cli/registry.js';
|
|
79
81
|
import { buildWrappedLaunch, parseWrapperCli, isTtadkWrapper } from './setup/cli-selection.js';
|
|
80
82
|
import { cliUnavailableMessage } from './setup/cli-availability.js';
|
|
81
|
-
import { findLaunchedCliPid, scheduleWrapperRealCliPid, readComm, isBareShellComm, bareShellLaunchKind } from './core/session-discovery.js';
|
|
83
|
+
import { findLaunchedCliPid, scheduleWrapperRealCliPid, readComm, isBareShellComm, bareShellLaunchKind, settleLaunchComm, } from './core/session-discovery.js';
|
|
82
84
|
import { codexRpcEligible, paneRunsRemoteTui, orchestrateCodexRpcInit, rolloutUserTurnMatches, decideStartupDialogAction, shouldQueueInitialPrompt, shouldPreMarkFirstTurn, killAndVerifyPersistentPane } from './codex-rpc-lifecycle.js';
|
|
83
85
|
import { delay } from './utils/timing.js';
|
|
84
86
|
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, syncClaudeResumeTargetToCwd, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
|
|
@@ -99,7 +101,6 @@ import { DEVICE_AUTHORITY_DIRECTORY, DEVICE_CREDENTIAL_FILE, } from './platform/
|
|
|
99
101
|
import { tmuxEnv, probeTmuxFunctionalWithRetry } from './setup/ensure-tmux.js';
|
|
100
102
|
import { tmuxRestartJitterMs } from './core/tmux-recovery.js';
|
|
101
103
|
import { IdleDetector } from './utils/idle-detector.js';
|
|
102
|
-
import { ScreenAnalyzer } from './utils/screen-analyzer.js';
|
|
103
104
|
import { StuckDetector, matchHookReviewScreen } from './utils/stuck-detector.js';
|
|
104
105
|
import { processStuckWarningTuiKeys, shouldRearmStuckDetector } from './utils/stuck-key-guard.js';
|
|
105
106
|
import { captureToPng } from './utils/screenshot-renderer.js';
|
|
@@ -107,7 +108,7 @@ import { snapshotToPng, snapshotToText, shouldCaptureScreen, isScreenSelfDriven
|
|
|
107
108
|
import { chooseWebTerminalSeed } from './utils/web-terminal-seed.js';
|
|
108
109
|
import { mergeHerdrWebSnapshot, renderHerdrWebHistory, } from './utils/herdr-web-history.js';
|
|
109
110
|
import { parseWorkerRequestUrl } from './utils/worker-http.js';
|
|
110
|
-
import { detectCliUsageLimit, usageLimitStateKey } from './utils/cli-usage-limit.js';
|
|
111
|
+
import { detectCliUsageLimit, usageLimitStateKey, structuredRateLimitState } from './utils/cli-usage-limit.js';
|
|
111
112
|
import { uploadImageBuffer } from './utils/lark-upload.js';
|
|
112
113
|
import { redactChildEnv, scrubSessionCliHomeEnv } from './utils/child-env.js';
|
|
113
114
|
import { decideSubmitConfirmationAction } from './services/submit-confirmation.js';
|
|
@@ -122,6 +123,7 @@ import { fetchDaemonIpc } from './core/daemon-ipc-auth.js';
|
|
|
122
123
|
import { withCodexAppContext } from './utils/codex-app-context.js';
|
|
123
124
|
import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.js';
|
|
124
125
|
import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
|
|
126
|
+
import { normalizeAppRunnerFinalMarker, normalizeCodexAppLifecycleEvent, projectAppRunnerFinalIds, } from './services/codex-app-runner-protocol.js';
|
|
125
127
|
import { hasMatchingManagedOriginCapability, managedOriginCapabilityPath, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, } from './core/managed-origin-capability.js';
|
|
126
128
|
import { CodexRpcEngine } from './codex-rpc-engine.js';
|
|
127
129
|
// A worker must never trust an INHERITED session-level CLI home pointer
|
|
@@ -692,7 +694,7 @@ function armRpcStartupDialogDismiss() {
|
|
|
692
694
|
let cliPidMarker = null; // path to .botmux-cli-pids/<pid>
|
|
693
695
|
let seatbeltProfilePath = null; // per-session Seatbelt .sb profile to rm at exit (external-wrapper read isolation)
|
|
694
696
|
let sandboxStopWatcher = null; // stop fn for the sandbox outbox watcher
|
|
695
|
-
let sandboxCleanup = null; //
|
|
697
|
+
let sandboxCleanup = null; // reclaim deny-mask mountpoints + rm the per-session sandbox tree
|
|
696
698
|
let sandboxRelayOutbox = null;
|
|
697
699
|
let sandboxRelayCapability = null;
|
|
698
700
|
let readIsolationOriginCapabilityFile = null;
|
|
@@ -955,6 +957,12 @@ const IDLE_PROBE_INTERVAL_MS = 3_500;
|
|
|
955
957
|
const IDLE_PROBE_MAX_ATTEMPTS = 24;
|
|
956
958
|
let busyPatternIdleProbeTimer = null;
|
|
957
959
|
let reattachIdleProbeTimer = null;
|
|
960
|
+
let codexRunnerFreshness = 'current';
|
|
961
|
+
let persistCodexRunnerBuildOnReady = false;
|
|
962
|
+
let activeRestartAttemptId;
|
|
963
|
+
/** Distinguishes a replacement's synchronous ready signal (Riff) from a late
|
|
964
|
+
* idle callback emitted by the backend being torn down. */
|
|
965
|
+
let replacementSpawnInProgress = false;
|
|
958
966
|
/** The effectiveResume flag used by the most recent spawnCli call. Written
|
|
959
967
|
* immediately after the two-tier fallback check so late-attach timers
|
|
960
968
|
* (hermes, cursor, etc.) can read THE SAME semantics the spawn used,
|
|
@@ -1106,6 +1114,17 @@ let bareShellLaunchBlocked = false;
|
|
|
1106
1114
|
* one-shot so it also covers a reattach onto a pane that degraded to a bare
|
|
1107
1115
|
* shell. Reset per spawn in spawnCli. */
|
|
1108
1116
|
let bareShellChecked = false;
|
|
1117
|
+
/** True only while detectBareShellLaunch() is inside its async launch-settle
|
|
1118
|
+
* window (settleLaunchComm's bounded ≤2s poll for a wrapper's final `exec
|
|
1119
|
+
* <cli>`). The message/injection flush paths already hold the isFlushing /
|
|
1120
|
+
* injectionFlushing mutexes across that await, but raw_input (passthrough
|
|
1121
|
+
* slash commands: /compact, /model, /btw) deliberately bypasses those to
|
|
1122
|
+
* preserve busy-delivery — so it would otherwise type into a pane whose leaf
|
|
1123
|
+
* is still the transient shell (or a shell already about to be blocked). This
|
|
1124
|
+
* latch lets raw_input defer for exactly that window without borrowing the
|
|
1125
|
+
* general isFlushing mutex (which would wrongly also block busy-delivery when
|
|
1126
|
+
* no settle is in progress). Reset per spawn in spawnCli. */
|
|
1127
|
+
let bareShellCheckInProgress = false;
|
|
1109
1128
|
/** Ready-gate (Claude-family): holds the first prompt until the SessionStart
|
|
1110
1129
|
* hook proves a cjadk-style startup selector is behind us. Claude then needs
|
|
1111
1130
|
* fresh post-hook prompt evidence because sibling hooks may still be running.
|
|
@@ -1344,11 +1363,42 @@ async function runStartupCommands() {
|
|
|
1344
1363
|
isPromptReady = false;
|
|
1345
1364
|
idleDetector?.reset();
|
|
1346
1365
|
}
|
|
1347
|
-
const
|
|
1366
|
+
const freshnessInputQueue = new CodexRunnerFreshnessInputQueue(() => codexRunnerFreshness, state => { codexRunnerFreshness = state; });
|
|
1367
|
+
const pendingMessages = freshnessInputQueue.normal;
|
|
1368
|
+
/** Correlation ids that this worker actually wrote into the owned Codex App
|
|
1369
|
+
* runner. Runner lifecycle/final markers may route only to ids in this bounded
|
|
1370
|
+
* local set; model/user display bytes cannot add entries. */
|
|
1371
|
+
const submittedCodexAppReplyTurnIds = new Set();
|
|
1372
|
+
const pendingCodexAppSteerAckIds = new Map();
|
|
1373
|
+
const acknowledgedCodexAppSteers = new Set();
|
|
1374
|
+
const CODEX_APP_CORRELATION_LIMIT = 256;
|
|
1375
|
+
function rememberBounded(set, value) {
|
|
1376
|
+
set.delete(value);
|
|
1377
|
+
set.add(value);
|
|
1378
|
+
while (set.size > CODEX_APP_CORRELATION_LIMIT) {
|
|
1379
|
+
const oldest = set.values().next().value;
|
|
1380
|
+
if (typeof oldest !== 'string')
|
|
1381
|
+
break;
|
|
1382
|
+
set.delete(oldest);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
function rememberBoundedMap(map, key, value) {
|
|
1386
|
+
map.delete(key);
|
|
1387
|
+
map.set(key, value);
|
|
1388
|
+
while (map.size > CODEX_APP_CORRELATION_LIMIT) {
|
|
1389
|
+
const oldest = map.keys().next().value;
|
|
1390
|
+
if (typeof oldest !== 'string')
|
|
1391
|
+
break;
|
|
1392
|
+
map.delete(oldest);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
function shortCorrelationId(value) {
|
|
1396
|
+
return value?.slice(0, 12) ?? '-';
|
|
1397
|
+
}
|
|
1348
1398
|
/** Literal commands that arrived while native /rename owned the TUI or while
|
|
1349
1399
|
* an owned CLI restart was fenced. Normal raw_input commands are still
|
|
1350
1400
|
* delivered immediately (including while busy). */
|
|
1351
|
-
const pendingRawInputs =
|
|
1401
|
+
const pendingRawInputs = freshnessInputQueue.raw;
|
|
1352
1402
|
/** Latest requested canonical session title. Unlike a normal prompt this is an
|
|
1353
1403
|
* administrative TUI command: never type-ahead while the agent is busy, never
|
|
1354
1404
|
* open a model turn, and latest-wins if several renames arrive before idle. */
|
|
@@ -1692,6 +1742,24 @@ function writeCliPidMarker() {
|
|
|
1692
1742
|
}
|
|
1693
1743
|
}
|
|
1694
1744
|
let lastStructuredBridgeActivityAtMs = 0;
|
|
1745
|
+
/**
|
|
1746
|
+
* True when this CLI has an authoritative STRUCTURED rate-limit signal in its
|
|
1747
|
+
* transcript (Claude family — `error:"rate_limit"`, surfaced by
|
|
1748
|
+
* maybeEmitStructuredRateLimit). For those CLIs the screen-text `rate`
|
|
1749
|
+
* heuristic is not just redundant but harmful: the model's own output or a dev
|
|
1750
|
+
* editing rate-limit code/tests puts phrases like "429 Too Many Requests" /
|
|
1751
|
+
* "exceeded retry limit" on screen, which the scraper cannot distinguish from a
|
|
1752
|
+
* real limit. So we suppress the screen-scan `rate` verdict and let the
|
|
1753
|
+
* structured path be the sole authority. `usage` (quota "hit your limit …")
|
|
1754
|
+
* has no structured equivalent yet, so it still comes from the screen.
|
|
1755
|
+
*
|
|
1756
|
+
* reliableTurnTerminal is exactly the "transcript-backed" capability flag
|
|
1757
|
+
* (claude-code / seed set it); non-transcript CLIs (Codex, gemini, …) keep the
|
|
1758
|
+
* screen scanner as their only rate-limit signal.
|
|
1759
|
+
*/
|
|
1760
|
+
function structuredRateLimitAuthoritative() {
|
|
1761
|
+
return cliAdapter?.reliableTurnTerminal === true;
|
|
1762
|
+
}
|
|
1695
1763
|
// Per-turn usage-limit state machine. Owns the turn counter plus the
|
|
1696
1764
|
// "did this turn hit a limit" / "suppress a stale retry-ready banner" flags, so
|
|
1697
1765
|
// classify()'s state writes are explicit method calls rather than hidden
|
|
@@ -1710,7 +1778,7 @@ function createUsageLimitTracker() {
|
|
|
1710
1778
|
beginTurn(snapshot) {
|
|
1711
1779
|
turnSeq++;
|
|
1712
1780
|
detectedTurn = undefined;
|
|
1713
|
-
const current = detectCliUsageLimit(snapshot);
|
|
1781
|
+
const current = detectCliUsageLimit(snapshot, undefined, { suppressRateKind: structuredRateLimitAuthoritative() });
|
|
1714
1782
|
suppressedRetryReadyKey = current.limited && current.retryReady
|
|
1715
1783
|
? usageLimitStateKey(current)
|
|
1716
1784
|
: undefined;
|
|
@@ -1719,7 +1787,7 @@ function createUsageLimitTracker() {
|
|
|
1719
1787
|
// Map a runtime status to a usage-limit-aware status, recording whether this
|
|
1720
1788
|
// turn hit a limit (read back via detectedThisTurn).
|
|
1721
1789
|
classify(content, status) {
|
|
1722
|
-
const detected = detectCliUsageLimit(content);
|
|
1790
|
+
const detected = detectCliUsageLimit(content, undefined, { suppressRateKind: structuredRateLimitAuthoritative() });
|
|
1723
1791
|
if (!detected.limited)
|
|
1724
1792
|
return { status };
|
|
1725
1793
|
const key = usageLimitStateKey(detected);
|
|
@@ -1733,6 +1801,15 @@ function createUsageLimitTracker() {
|
|
|
1733
1801
|
detectedThisTurn(seq) {
|
|
1734
1802
|
return detectedTurn === seq;
|
|
1735
1803
|
},
|
|
1804
|
+
// Record a limit that came from a STRUCTURED signal (transcript error
|
|
1805
|
+
// record) rather than screen text. Mirrors classify()'s state writes so
|
|
1806
|
+
// the tracker stays coherent: mark this turn as having hit a limit (read
|
|
1807
|
+
// by detectedThisTurn for the submit-confirmation recheck) and clear any
|
|
1808
|
+
// stale retry-ready suppression. The actual emit is done by the caller.
|
|
1809
|
+
noteStructuredLimit() {
|
|
1810
|
+
suppressedRetryReadyKey = undefined;
|
|
1811
|
+
detectedTurn = turnSeq;
|
|
1812
|
+
},
|
|
1736
1813
|
};
|
|
1737
1814
|
}
|
|
1738
1815
|
const usageLimitTracker = createUsageLimitTracker();
|
|
@@ -1802,6 +1879,11 @@ const bridgeSecondaryPaths = new Map(); // path → offset
|
|
|
1802
1879
|
let bridgeOffset = 0;
|
|
1803
1880
|
let bridgePendingTail = '';
|
|
1804
1881
|
const bridgeQueue = new BridgeTurnQueue();
|
|
1882
|
+
/** uuids of Claude transcript rate-limit records we've already turned into a
|
|
1883
|
+
* `limited` emit. drainTranscript re-reads from offset 0 on truncation /
|
|
1884
|
+
* rotation and emitReadyTurns re-drains from 0, so the same rate_limit record
|
|
1885
|
+
* can resurface; keying on the record's stable uuid makes the emit idempotent. */
|
|
1886
|
+
const emittedRateLimitUuids = new Set();
|
|
1805
1887
|
let bridgeWatcher = null;
|
|
1806
1888
|
let bridgeFallbackTimer = null;
|
|
1807
1889
|
let herdrAdoptBridgeQuietTimer = null;
|
|
@@ -1895,6 +1977,9 @@ function formatHeadlessLocalTurnContent(assistantText) {
|
|
|
1895
1977
|
const a = truncatePreambleText(assistantText.trim(), LOCAL_TURN_ASSISTANT_MAX);
|
|
1896
1978
|
return a || null;
|
|
1897
1979
|
}
|
|
1980
|
+
function emptyCompletedBridgeFallbackContent() {
|
|
1981
|
+
return t('worker.empty_final_completed', { cliName: cliName() });
|
|
1982
|
+
}
|
|
1898
1983
|
// ─── Bridge fallback marker (non-adopt) ────────────────────────────────────
|
|
1899
1984
|
//
|
|
1900
1985
|
// `botmux send` (cli.ts cmdSend) appends a line
|
|
@@ -2732,11 +2817,45 @@ function bridgeIngest() {
|
|
|
2732
2817
|
if (result.events.length > 0)
|
|
2733
2818
|
lastStructuredBridgeActivityAtMs = Date.now();
|
|
2734
2819
|
bridgeQueue.ingest(result.events, bridgeJsonlPath);
|
|
2820
|
+
// Structured rate-limit: Claude Code writes an `error:"rate_limit"` record
|
|
2821
|
+
// at the turn's terminal boundary. This is the authoritative "limited"
|
|
2822
|
+
// signal — read it here (event-driven, once per record) instead of scraping
|
|
2823
|
+
// the TUI. The queue already skips it as an assistant reply.
|
|
2824
|
+
maybeEmitStructuredRateLimit(result.events);
|
|
2735
2825
|
// Transcript terminal markers are authoritative and may settle a durable
|
|
2736
2826
|
// turn immediately. Do not wait for the screen prompt: permission/AskUser
|
|
2737
2827
|
// surfaces can resemble idle, while an explicit JSONL boundary cannot.
|
|
2738
2828
|
emitReadyTurns({ explicitTerminalOnly: true });
|
|
2739
2829
|
}
|
|
2830
|
+
/** Scan newly-drained Claude transcript events for a structured rate-limit
|
|
2831
|
+
* record and, on the first unseen one, emit a `limited` screen_update so the
|
|
2832
|
+
* session surfaces in Dashboard「需要你」with a retry countdown — identical
|
|
2833
|
+
* wire shape to the screen-text detector's classify() output, so the daemon /
|
|
2834
|
+
* card / persistence paths need no change. Claude-only (bridgeQueue is the
|
|
2835
|
+
* Claude bridge; Codex uses codexBridgeQueue and has no structured 429). */
|
|
2836
|
+
function maybeEmitStructuredRateLimit(events) {
|
|
2837
|
+
for (const ev of events) {
|
|
2838
|
+
if (!ev.uuid || emittedRateLimitUuids.has(ev.uuid))
|
|
2839
|
+
continue;
|
|
2840
|
+
if (!isTranscriptRateLimitEvent(ev))
|
|
2841
|
+
continue;
|
|
2842
|
+
emittedRateLimitUuids.add(ev.uuid);
|
|
2843
|
+
// Prefer a clock parsed from the record's own text ("... resets 10:40pm");
|
|
2844
|
+
// fall back to the shared bucketed cooldown when it carries none.
|
|
2845
|
+
const usageLimit = structuredRateLimitState(apiErrorMessageText(ev));
|
|
2846
|
+
usageLimitTracker.noteStructuredLimit();
|
|
2847
|
+
send({
|
|
2848
|
+
type: 'screen_update',
|
|
2849
|
+
content: currentUsageLimitSnapshot(),
|
|
2850
|
+
status: 'limited',
|
|
2851
|
+
usageLimit,
|
|
2852
|
+
turnId: currentBotmuxTurnId,
|
|
2853
|
+
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
2854
|
+
});
|
|
2855
|
+
log(`Structured rate-limit detected in Claude transcript (uuid=${ev.uuid.substring(0, 8)}, retryLabel=${usageLimit.retryLabel}) → emitted limited state.`);
|
|
2856
|
+
return; // one limited emit per ingest is enough; state key is stable
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2740
2859
|
function performBridgeIngestAndScheduleQuietEmit() {
|
|
2741
2860
|
const beforePath = bridgeJsonlPath;
|
|
2742
2861
|
const beforeOffset = bridgeOffset;
|
|
@@ -3265,6 +3384,7 @@ function hermesBridgeIngest() {
|
|
|
3265
3384
|
// `/clear`-rotates mid-batch; the daemon accumulates them into its authorized
|
|
3266
3385
|
// set, so a completed turn from an earlier source is not dropped as foreign.
|
|
3267
3386
|
for (const boundSourceSessionId of filtered.newlyBoundSourceSessionIds) {
|
|
3387
|
+
persistCliSessionId(boundSourceSessionId);
|
|
3268
3388
|
send({ type: 'bridge_source_session', bridge: 'hermes', sourceSessionId: boundSourceSessionId });
|
|
3269
3389
|
log(`Hermes bridge bound sourceSessionId=${boundSourceSessionId}`);
|
|
3270
3390
|
}
|
|
@@ -3677,6 +3797,37 @@ function codexBridgeDrainAndMaybeEmit(opts = {}) {
|
|
|
3677
3797
|
}
|
|
3678
3798
|
emitReadyCodexTurns();
|
|
3679
3799
|
}
|
|
3800
|
+
/** Before we emit a fail-closed `ambiguous` terminal for a durable turn that a
|
|
3801
|
+
* CLI kill is about to interrupt, give an already-persisted `completed` record
|
|
3802
|
+
* a chance to win the worker-local terminal deduper. `reliableTurnTerminal`
|
|
3803
|
+
* CLIs (claude-code / codex / grok / traex) durably append their terminal line
|
|
3804
|
+
* to the transcript; if that line landed microseconds before the kill but the
|
|
3805
|
+
* fs.watch/1s poller hasn't consumed it yet, an unconditional `ambiguous` would
|
|
3806
|
+
* claim the deduper first and needlessly mark a turn that actually completed →
|
|
3807
|
+
* same durable key re-dispatchable → external side effect executed twice.
|
|
3808
|
+
* Draining here publishes that `completed` (claiming the deduper) so the later
|
|
3809
|
+
* `ambiguous` becomes a no-op. Shared by the CLI `onExit` path and the daemon
|
|
3810
|
+
* `restart` IPC path — both must drain identically before their ambiguous emit. */
|
|
3811
|
+
function drainReliableTerminalBeforeInterrupt() {
|
|
3812
|
+
if (cliAdapter?.reliableTurnTerminal !== true)
|
|
3813
|
+
return;
|
|
3814
|
+
if (bridgeJsonlPath) {
|
|
3815
|
+
try {
|
|
3816
|
+
bridgeDrainAndMaybeEmit();
|
|
3817
|
+
}
|
|
3818
|
+
catch (err) {
|
|
3819
|
+
log(`Bridge terminal drain failed: ${err.message}`);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
if (codexBridgeFallbackActive()) {
|
|
3823
|
+
try {
|
|
3824
|
+
codexBridgeDrainAndMaybeEmit({ signalIdle: false });
|
|
3825
|
+
}
|
|
3826
|
+
catch (err) {
|
|
3827
|
+
log(`Codex bridge terminal drain failed: ${err.message}`);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3680
3831
|
function emitReadyCodexTurns() {
|
|
3681
3832
|
const ready = codexBridgeQueue.drainEmittable();
|
|
3682
3833
|
if (ready.length === 0)
|
|
@@ -3700,11 +3851,22 @@ function emitReadyCodexTurns() {
|
|
|
3700
3851
|
: undefined;
|
|
3701
3852
|
for (let i = 0; i < ready.length; i++) {
|
|
3702
3853
|
const turn = ready[i];
|
|
3703
|
-
if (!turn.finalText)
|
|
3704
|
-
continue;
|
|
3705
3854
|
const sourceHermesSessionId = structuredBridgeIsHermes() ? turn.sourceSessionId : undefined;
|
|
3706
3855
|
const nextBoundaryMs = (i + 1 < ready.length ? ready[i + 1].markTimeMs : nextPendingMarkTimeMs);
|
|
3707
|
-
|
|
3856
|
+
const gateInput = {
|
|
3857
|
+
markTimeMs: turn.markTimeMs,
|
|
3858
|
+
isLocal: turn.isLocal,
|
|
3859
|
+
finalText: turn.finalText,
|
|
3860
|
+
terminalStatus: turn.terminalStatus,
|
|
3861
|
+
};
|
|
3862
|
+
const content = turn.finalText && turn.finalText.trim()
|
|
3863
|
+
? turn.finalText
|
|
3864
|
+
: shouldEmitEmptyCompletedBridgeFallback(gateInput, nextBoundaryMs, markers, adoptMode)
|
|
3865
|
+
? emptyCompletedBridgeFallbackContent()
|
|
3866
|
+
: '';
|
|
3867
|
+
if (!content)
|
|
3868
|
+
continue;
|
|
3869
|
+
if (shouldSuppressBridgeEmit(gateInput, nextBoundaryMs, markers, adoptMode)) {
|
|
3708
3870
|
log(`Codex bridge fallback suppressed for turn ${turn.turnId.substring(0, 8)} (gate)`);
|
|
3709
3871
|
continue;
|
|
3710
3872
|
}
|
|
@@ -3713,7 +3875,7 @@ function emitReadyCodexTurns() {
|
|
|
3713
3875
|
// so the Lark thread sees a complete exchange instead of an orphan
|
|
3714
3876
|
// reply. formatLocalTurnFields caps both texts to keep within
|
|
3715
3877
|
// Lark's per-message limit; daemon owns the card chrome.
|
|
3716
|
-
const fields = formatLocalTurnFields(turn.userText ?? '',
|
|
3878
|
+
const fields = formatLocalTurnFields(turn.userText ?? '', content);
|
|
3717
3879
|
if (!fields)
|
|
3718
3880
|
continue;
|
|
3719
3881
|
send({
|
|
@@ -3731,7 +3893,7 @@ function emitReadyCodexTurns() {
|
|
|
3731
3893
|
send({
|
|
3732
3894
|
type: 'final_output',
|
|
3733
3895
|
...(sourceHermesSessionId ? { sourceHermesSessionId } : {}),
|
|
3734
|
-
content
|
|
3896
|
+
content,
|
|
3735
3897
|
lastUuid: turn.turnId,
|
|
3736
3898
|
turnId: turn.turnId,
|
|
3737
3899
|
...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
|
|
@@ -3853,8 +4015,9 @@ let renderRows = PTY_ROWS;
|
|
|
3853
4015
|
// ─── Headless Terminal for Screen Capture ────────────────────────────────────
|
|
3854
4016
|
let renderer = null;
|
|
3855
4017
|
/** Most recent unfiltered viewport text — kept in sync by the screen_update
|
|
3856
|
-
* timer for pipe-pane backends so
|
|
3857
|
-
* a fresh snapshot to read without needing
|
|
4018
|
+
* timer for pipe-pane backends so usage-limit detection and the CoCo picker
|
|
4019
|
+
* have a fresh snapshot to read without needing their own tmux capture-pane
|
|
4020
|
+
* call. (Historically also fed the AI ScreenAnalyzer, now removed.) */
|
|
3858
4021
|
let lastAnalyzerSnapshot = '';
|
|
3859
4022
|
let screenUpdateTimer = null;
|
|
3860
4023
|
const SCREEN_UPDATE_INTERVAL_MS = 2_000;
|
|
@@ -4028,14 +4191,14 @@ function parkCrashDiagnosticTerminal(code, signal) {
|
|
|
4028
4191
|
// so the diagnostic shell stays visible. flushPending's retry path restarts
|
|
4029
4192
|
// both when the next message respawns the CLI.
|
|
4030
4193
|
stopScreenUpdates();
|
|
4031
|
-
stopScreenAnalyzer();
|
|
4032
4194
|
stopStuckDetector();
|
|
4033
4195
|
log(`Crash diagnostic tmux session parked at ${TmuxBackend.diagnosticSessionName(sessionId)}`);
|
|
4034
4196
|
return true;
|
|
4035
4197
|
}
|
|
4036
|
-
// ───
|
|
4037
|
-
|
|
4038
|
-
|
|
4198
|
+
// ─── TUI prompt blocking state ──────────────────────────────────────────────
|
|
4199
|
+
/** When true, user messages are queued because a TUI prompt is active. Set by
|
|
4200
|
+
* the ask-hook / CoCo picker paths (driveCocoPicker, handleTuiKeys); cleared
|
|
4201
|
+
* when the prompt resolves or a Lark/terminal input overrides it. */
|
|
4039
4202
|
let tuiPromptBlocking = false;
|
|
4040
4203
|
function isWorkflowWorker() {
|
|
4041
4204
|
return process.env.BOTMUX_WORKFLOW === '1';
|
|
@@ -4097,54 +4260,6 @@ function maybeEmitWorkflowTranscriptOutput() {
|
|
|
4097
4260
|
});
|
|
4098
4261
|
log('Workflow PTY transcript final_output emitted');
|
|
4099
4262
|
}
|
|
4100
|
-
function startScreenAnalyzer() {
|
|
4101
|
-
const sa = config.screenAnalyzer;
|
|
4102
|
-
log(`ScreenAnalyzer config: enabled=${sa.enabled}, baseUrl=${sa.baseUrl ? 'set' : 'empty'}, model=${sa.model || 'empty'}, extraHeaders=${JSON.stringify(sa.extraHeaders)}`);
|
|
4103
|
-
if (!sa.enabled || !sa.baseUrl || !sa.apiKey || !sa.model)
|
|
4104
|
-
return;
|
|
4105
|
-
screenAnalyzer = new ScreenAnalyzer({
|
|
4106
|
-
baseUrl: sa.baseUrl,
|
|
4107
|
-
apiKey: sa.apiKey,
|
|
4108
|
-
model: sa.model,
|
|
4109
|
-
intervalMs: sa.intervalMs,
|
|
4110
|
-
stableCount: sa.stableCount,
|
|
4111
|
-
snapshotMaxChars: sa.snapshotMaxChars,
|
|
4112
|
-
extraHeaders: sa.extraHeaders,
|
|
4113
|
-
extraBody: sa.extraBody,
|
|
4114
|
-
}, {
|
|
4115
|
-
getSnapshot: () => {
|
|
4116
|
-
// ScreenAnalyzer is called every ~5s for TUI-prompt detection. We
|
|
4117
|
-
// can't make this async without overhauling the analyzer, so cache
|
|
4118
|
-
// the last pipe-pane text snapshot here and refresh it eagerly.
|
|
4119
|
-
// For pipe-pane backends, the cache is repopulated by the screen
|
|
4120
|
-
// update timer; for others, fall through to the long-lived renderer.
|
|
4121
|
-
return lastAnalyzerSnapshot || renderer?.rawSnapshot() || '';
|
|
4122
|
-
},
|
|
4123
|
-
onAnalyzing: () => { },
|
|
4124
|
-
onTuiPrompt: (description, options, multiSelect) => {
|
|
4125
|
-
tuiPromptBlocking = true;
|
|
4126
|
-
send({ type: 'tui_prompt', description, options, multiSelect, turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
|
|
4127
|
-
},
|
|
4128
|
-
onTuiPromptResolved: (selectedText) => {
|
|
4129
|
-
tuiPromptBlocking = false;
|
|
4130
|
-
send({
|
|
4131
|
-
type: 'tui_prompt_resolved',
|
|
4132
|
-
selectedText,
|
|
4133
|
-
turnId: currentBotmuxTurnId,
|
|
4134
|
-
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
4135
|
-
});
|
|
4136
|
-
// Flush any messages that were queued during the prompt
|
|
4137
|
-
flushPending();
|
|
4138
|
-
},
|
|
4139
|
-
log,
|
|
4140
|
-
});
|
|
4141
|
-
screenAnalyzer.start();
|
|
4142
|
-
}
|
|
4143
|
-
function stopScreenAnalyzer() {
|
|
4144
|
-
screenAnalyzer?.dispose();
|
|
4145
|
-
screenAnalyzer = null;
|
|
4146
|
-
tuiPromptBlocking = false;
|
|
4147
|
-
}
|
|
4148
4263
|
// ─── Stuck Detector (AI-free fallback for blocked CLI states) ───────────────
|
|
4149
4264
|
let stuckDetector = null;
|
|
4150
4265
|
// Monotonic counter bumped on every CLI/backend (re)start within this worker.
|
|
@@ -4341,8 +4456,6 @@ async function captureAndUpload() {
|
|
|
4341
4456
|
return;
|
|
4342
4457
|
}
|
|
4343
4458
|
let status = isPromptReady ? 'idle' : 'working';
|
|
4344
|
-
if (screenAnalyzer?.isAnalyzing)
|
|
4345
|
-
status = 'analyzing';
|
|
4346
4459
|
send({
|
|
4347
4460
|
type: 'screenshot_uploaded',
|
|
4348
4461
|
imageKey,
|
|
@@ -4432,14 +4545,12 @@ function handleTermAction(key) {
|
|
|
4432
4545
|
else if (PTY_SEQ_MAP[key]) {
|
|
4433
4546
|
backend.write(PTY_SEQ_MAP[key]);
|
|
4434
4547
|
}
|
|
4435
|
-
// ESC/Ctrl-C/Enter likely ends an active TUI prompt.
|
|
4436
|
-
//
|
|
4437
|
-
//
|
|
4438
|
-
//
|
|
4439
|
-
// subsequent user message.
|
|
4548
|
+
// ESC/Ctrl-C/Enter likely ends an active TUI prompt. Un-wedge the blocking
|
|
4549
|
+
// flag here — without this, dismissing an AskUserQuestion dialog via the
|
|
4550
|
+
// quick-key button leaves tuiPromptBlocking=true forever and silently queues
|
|
4551
|
+
// every subsequent user message.
|
|
4440
4552
|
if (tuiPromptBlocking && (key === 'esc' || key === 'ctrlc' || key === 'enter')) {
|
|
4441
4553
|
tuiPromptBlocking = false;
|
|
4442
|
-
screenAnalyzer?.notifySelection(`term_action:${key}`);
|
|
4443
4554
|
void flushPending();
|
|
4444
4555
|
}
|
|
4445
4556
|
log(`Term action: ${key}`);
|
|
@@ -4486,18 +4597,18 @@ async function handleTuiKeys(keys, isFinal) {
|
|
|
4486
4597
|
isPromptReady = false;
|
|
4487
4598
|
idleDetector?.reset();
|
|
4488
4599
|
}
|
|
4489
|
-
screenAnalyzer?.notifySelection('final');
|
|
4490
4600
|
}
|
|
4491
4601
|
log(`TUI keys: ${keys.join(' ')}${isFinal ? ' (final)' : ''}`);
|
|
4492
4602
|
return true;
|
|
4493
4603
|
}
|
|
4494
4604
|
// 待注入的 TUI 命令队列。生命周期绑定当前 CLI 进程:killCli() 会清空它,
|
|
4495
4605
|
// 防止 restart 后残留命令重放进新 CLI——新增清理状态时记得同步那里。
|
|
4496
|
-
// barrier=true
|
|
4497
|
-
//
|
|
4498
|
-
//
|
|
4499
|
-
// core/inject-queue-policy.ts
|
|
4500
|
-
//
|
|
4606
|
+
// barrier 语义:barrier=true 的注入必须先于本次 pending 用户消息落地。历史上
|
|
4607
|
+
// 唯一的 barrier 来源是 cwd-move 的 /cd 注入;角色切换改为 restart respawn 后
|
|
4608
|
+
// 现存发送方(/slash 白名单注入)全部 barrier=false,机制保留供未来有顺序
|
|
4609
|
+
// 依赖的注入复用。排队策略判定收敛到 core/inject-queue-policy.ts
|
|
4610
|
+
// (shouldDeferUserFlush / shouldFlushInjectionsFirst)以获得可单测的纯函数
|
|
4611
|
+
// 单元——本文件只持有队列状态。
|
|
4501
4612
|
const pendingInjections = [];
|
|
4502
4613
|
let injectionFlushing = false;
|
|
4503
4614
|
/** 排队注入一行 TUI 命令:idle(isPromptReady)时经 sendRawCommandLineSerially 敲入。 */
|
|
@@ -4534,9 +4645,18 @@ async function flushPendingInjections() {
|
|
|
4534
4645
|
// path sends first runs the detection.
|
|
4535
4646
|
if (!bareShellChecked) {
|
|
4536
4647
|
bareShellChecked = true;
|
|
4537
|
-
if (detectBareShellLaunch())
|
|
4648
|
+
if (await detectBareShellLaunch())
|
|
4538
4649
|
return; // finally{} releases the mutex; queue stays
|
|
4539
4650
|
}
|
|
4651
|
+
// The detector's settle await can span a restart's tmux jitter window
|
|
4652
|
+
// (cliRestartInProgress true, old backend still alive). Re-check the fence
|
|
4653
|
+
// before shift()/write so a queued injection never lands in a CLI already
|
|
4654
|
+
// being torn down. The pending injections are then handled by killCli's
|
|
4655
|
+
// restart policy — which drops them (barrier /cd is already durable in
|
|
4656
|
+
// workingDir; non-barrier injects are best-effort and not replayed
|
|
4657
|
+
// cross-process), unlike pendingMessages which killCli preserves.
|
|
4658
|
+
if (cliRestartInProgress)
|
|
4659
|
+
return;
|
|
4540
4660
|
const item = pendingInjections.shift();
|
|
4541
4661
|
const cmd = item.command;
|
|
4542
4662
|
isPromptReady = false;
|
|
@@ -4596,7 +4716,6 @@ async function handleTuiTextInput(keys, text) {
|
|
|
4596
4716
|
isPromptReady = false;
|
|
4597
4717
|
idleDetector?.reset();
|
|
4598
4718
|
}
|
|
4599
|
-
screenAnalyzer?.notifySelection('text-input');
|
|
4600
4719
|
// Wait briefly so the cursor position is stable before pasting
|
|
4601
4720
|
await new Promise(r => setTimeout(r, 200));
|
|
4602
4721
|
// Step 3: write text via cliAdapter (auto-switches to text mode + submits with Enter)
|
|
@@ -4755,9 +4874,74 @@ function handleCodexAppMarker(body) {
|
|
|
4755
4874
|
persistCliSessionId(payload.threadId);
|
|
4756
4875
|
return;
|
|
4757
4876
|
}
|
|
4758
|
-
if (kind === '
|
|
4759
|
-
const
|
|
4760
|
-
|
|
4877
|
+
if (kind === 'lifecycle') {
|
|
4878
|
+
const event = normalizeCodexAppLifecycleEvent(payload);
|
|
4879
|
+
if (!event) {
|
|
4880
|
+
log(`${cliName()} rejected malformed lifecycle marker`);
|
|
4881
|
+
return;
|
|
4882
|
+
}
|
|
4883
|
+
log(`${cliName()} lifecycle kind=${event.kind}`
|
|
4884
|
+
+ ` appTurn=${shortCorrelationId(event.appTurnId)}`
|
|
4885
|
+
+ ` replyTurn=${shortCorrelationId(event.replyTurnId)}`
|
|
4886
|
+
+ `${event.queueLength !== undefined ? ` queue=${event.queueLength}` : ''}`);
|
|
4887
|
+
if (!event.replyTurnId || !submittedCodexAppReplyTurnIds.has(event.replyTurnId))
|
|
4888
|
+
return;
|
|
4889
|
+
if (event.kind === 'steer_attempt') {
|
|
4890
|
+
rememberBoundedMap(pendingCodexAppSteerAckIds, event.replyTurnId, event.appTurnId);
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4893
|
+
const steerKey = `${event.appTurnId}\0${event.replyTurnId}`;
|
|
4894
|
+
if (event.kind !== 'steer_accepted'
|
|
4895
|
+
|| pendingCodexAppSteerAckIds.get(event.replyTurnId) !== event.appTurnId
|
|
4896
|
+
|| acknowledgedCodexAppSteers.has(steerKey))
|
|
4897
|
+
return;
|
|
4898
|
+
pendingCodexAppSteerAckIds.delete(event.replyTurnId);
|
|
4899
|
+
rememberBounded(acknowledgedCodexAppSteers, steerKey);
|
|
4900
|
+
send({
|
|
4901
|
+
type: 'steer_accepted',
|
|
4902
|
+
appTurnId: event.appTurnId,
|
|
4903
|
+
turnId: event.replyTurnId,
|
|
4904
|
+
});
|
|
4905
|
+
return;
|
|
4906
|
+
}
|
|
4907
|
+
if (kind === 'final') {
|
|
4908
|
+
const marker = normalizeAppRunnerFinalMarker(payload);
|
|
4909
|
+
if (!marker) {
|
|
4910
|
+
log(`${cliName()} rejected malformed final marker`);
|
|
4911
|
+
return;
|
|
4912
|
+
}
|
|
4913
|
+
const startedAtMs = marker.startedAtMs;
|
|
4914
|
+
const completedAtMs = marker.completedAtMs ?? Date.now();
|
|
4915
|
+
if (marker.appTurnId) {
|
|
4916
|
+
const trustedReplyTurnId = marker.replyTurnId
|
|
4917
|
+
&& submittedCodexAppReplyTurnIds.has(marker.replyTurnId)
|
|
4918
|
+
? marker.replyTurnId
|
|
4919
|
+
: undefined;
|
|
4920
|
+
if (marker.replyTurnId && !trustedReplyTurnId) {
|
|
4921
|
+
log(`${cliName()} ignored unsubmitted final reply route `
|
|
4922
|
+
+ `(replyTurn=${shortCorrelationId(marker.replyTurnId)})`);
|
|
4923
|
+
}
|
|
4924
|
+
const identity = projectAppRunnerFinalIds({ ...marker, replyTurnId: trustedReplyTurnId }, currentBotmuxTurnId, `${lastInitConfig?.cliId ?? 'app'}-${Date.now()}`);
|
|
4925
|
+
if (marker.appTurnId !== identity.turnId) {
|
|
4926
|
+
log(`${cliName()} app turn ${shortCorrelationId(marker.appTurnId)} mapped `
|
|
4927
|
+
+ `to botmux turn ${shortCorrelationId(identity.turnId)}`);
|
|
4928
|
+
}
|
|
4929
|
+
const dispatchAttempt = currentBotmuxDispatchAttempt;
|
|
4930
|
+
if (startedAtMs !== undefined && shouldSuppressBridgeEmit({ markTimeMs: startedAtMs, isLocal: false, finalText: marker.content }, completedAtMs + 5_001, readSendMarkers(), false)) {
|
|
4931
|
+
log(`${cliName()} final_output suppressed (model already called botmux send)`);
|
|
4932
|
+
emitTurnTerminal(identity.turnId, 'completed', undefined, dispatchAttempt);
|
|
4933
|
+
return;
|
|
4934
|
+
}
|
|
4935
|
+
send({
|
|
4936
|
+
type: 'final_output',
|
|
4937
|
+
content: marker.content,
|
|
4938
|
+
lastUuid: identity.lastUuid,
|
|
4939
|
+
turnId: identity.turnId,
|
|
4940
|
+
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
4941
|
+
});
|
|
4942
|
+
emitTurnTerminal(identity.turnId, 'completed', undefined, dispatchAttempt);
|
|
4943
|
+
return;
|
|
4944
|
+
}
|
|
4761
4945
|
// Codex App keeps the app-server-generated id separately for diagnostics.
|
|
4762
4946
|
// Routing must use its stable clientUserMessageId marker; legacy envelopes
|
|
4763
4947
|
// intentionally omit it and fall back to the worker's frozen botmux turn.
|
|
@@ -4782,7 +4966,7 @@ function handleCodexAppMarker(body) {
|
|
|
4782
4966
|
// equality for compatibility, but can never select another attempt.
|
|
4783
4967
|
const dispatchAttempt = currentBotmuxDispatchAttempt;
|
|
4784
4968
|
if (startedAtMs !== undefined) {
|
|
4785
|
-
const sentByModel = shouldSuppressBridgeEmit({ markTimeMs: startedAtMs, isLocal: false, finalText:
|
|
4969
|
+
const sentByModel = shouldSuppressBridgeEmit({ markTimeMs: startedAtMs, isLocal: false, finalText: marker.content }, completedAtMs + 5_001, readSendMarkers(), false);
|
|
4786
4970
|
if (sentByModel) {
|
|
4787
4971
|
log(`${cliName()} final_output suppressed (model already called botmux send)`);
|
|
4788
4972
|
emitTurnTerminal(turnId, 'completed', undefined, dispatchAttempt);
|
|
@@ -4791,7 +4975,7 @@ function handleCodexAppMarker(body) {
|
|
|
4791
4975
|
}
|
|
4792
4976
|
send({
|
|
4793
4977
|
type: 'final_output',
|
|
4794
|
-
content:
|
|
4978
|
+
content: marker.content,
|
|
4795
4979
|
lastUuid: turnId,
|
|
4796
4980
|
turnId,
|
|
4797
4981
|
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
@@ -4945,6 +5129,10 @@ function markPromptReadyFromPty() {
|
|
|
4945
5129
|
function markPromptReady() {
|
|
4946
5130
|
if (isPromptReady)
|
|
4947
5131
|
return; // guard against duplicate calls
|
|
5132
|
+
if (cliRestartInProgress && !replacementSpawnInProgress) {
|
|
5133
|
+
log('Ignoring prompt-ready from backend generation being replaced');
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
4948
5136
|
stopBusyPatternIdleProbe();
|
|
4949
5137
|
// Ready-gate: a startup selector's ❯ (cjadk et al.) falsely matches
|
|
4950
5138
|
// readyPattern → the IdleDetector fires idle while the CLI is NOT actually at
|
|
@@ -4974,6 +5162,14 @@ function markPromptReady() {
|
|
|
4974
5162
|
log('Idle detected during ready-gate settle; deferring prompt-ready until settle completes');
|
|
4975
5163
|
return;
|
|
4976
5164
|
}
|
|
5165
|
+
const freshnessAction = freshnessInputQueue.onPromptReady();
|
|
5166
|
+
if (freshnessAction === 'reload') {
|
|
5167
|
+
log('Stale Codex App runner became idle; replacing it before releasing queued input');
|
|
5168
|
+
void restartCliProcess('stale runner reached idle', { immediate: true, preservePending: true });
|
|
5169
|
+
return;
|
|
5170
|
+
}
|
|
5171
|
+
if (freshnessAction === 'ignore')
|
|
5172
|
+
return;
|
|
4977
5173
|
isPromptReady = true;
|
|
4978
5174
|
clearSessionRenameInFlight();
|
|
4979
5175
|
// An old backend can still report idle while its async teardown is running.
|
|
@@ -5005,6 +5201,33 @@ function markPromptReady() {
|
|
|
5005
5201
|
renderer?.markNewTurn(); // exclude history replay from streaming card
|
|
5006
5202
|
}
|
|
5007
5203
|
send({ type: 'prompt_ready' });
|
|
5204
|
+
if (persistCodexRunnerBuildOnReady
|
|
5205
|
+
&& lastInitConfig?.cliId === 'codex-app'
|
|
5206
|
+
&& lastInitConfig.runnerBuildId) {
|
|
5207
|
+
send({ type: 'runner_build_ready', runnerBuildId: lastInitConfig.runnerBuildId });
|
|
5208
|
+
persistCodexRunnerBuildOnReady = false;
|
|
5209
|
+
}
|
|
5210
|
+
if (activeRestartAttemptId) {
|
|
5211
|
+
// Defense in depth: only report a successful restart when a replacement
|
|
5212
|
+
// backend is actually installed. Every legitimate ready path assigns
|
|
5213
|
+
// `backend` before firing (spawnCli sets it, then idle/PTY callbacks run);
|
|
5214
|
+
// a stray callback that reached here with no backend (e.g. a late stale
|
|
5215
|
+
// generation slipping through a future gate change) must NOT claim success
|
|
5216
|
+
// and consume the attempt id — leave it for the real replacement or the
|
|
5217
|
+
// coordinator timeout.
|
|
5218
|
+
if (backend) {
|
|
5219
|
+
send({
|
|
5220
|
+
type: 'restart_result',
|
|
5221
|
+
attemptId: activeRestartAttemptId,
|
|
5222
|
+
status: 'succeeded',
|
|
5223
|
+
category: 'prompt_ready',
|
|
5224
|
+
});
|
|
5225
|
+
activeRestartAttemptId = undefined;
|
|
5226
|
+
}
|
|
5227
|
+
else {
|
|
5228
|
+
log('prompt-ready with no backend installed — deferring restart success receipt');
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5008
5231
|
// Send immediate idle snapshot so Lark card reflects idle status.
|
|
5009
5232
|
// BUT: skip when messages are pending — flushPending() will immediately
|
|
5010
5233
|
// make the CLI busy, so the idle state is transient and shouldn't appear
|
|
@@ -5014,13 +5237,11 @@ function markPromptReady() {
|
|
|
5014
5237
|
const { content } = renderer.snapshot();
|
|
5015
5238
|
send({ type: 'screen_update', content, ...usageLimitTracker.classify(content, 'idle'), turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt });
|
|
5016
5239
|
}
|
|
5017
|
-
//
|
|
5018
|
-
//
|
|
5019
|
-
// flushPending
|
|
5020
|
-
//
|
|
5021
|
-
//
|
|
5022
|
-
// flushPendingInjections 的 finally 会在注入完成、CLI 重新 idle 后补踢一次
|
|
5023
|
-
// flushPending 排空 pendingMessages。
|
|
5240
|
+
// barrier 注入必须先于本次 pending 用户消息落地(现存发送方均 barrier=false,
|
|
5241
|
+
// 该分支目前不触发;机制保留见 pendingInjections 声明处注释)。跳过本次
|
|
5242
|
+
// flushPending 不会饿死用户消息:flushPending 自身的 injectionFlushing 守卫
|
|
5243
|
+
// 会挡住并发写入,flushPendingInjections 的 finally 会在注入完成、CLI 重新
|
|
5244
|
+
// idle 后补踢一次 flushPending 排空 pendingMessages。
|
|
5024
5245
|
if (shouldFlushInjectionsFirst(pendingInjections)) {
|
|
5025
5246
|
void flushPendingInjections();
|
|
5026
5247
|
}
|
|
@@ -5238,19 +5459,41 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
5238
5459
|
* and the pty/herdr backends (which `exec` the CLI directly — getChildPid is the
|
|
5239
5460
|
* CLI itself, never a shell).
|
|
5240
5461
|
*
|
|
5241
|
-
* Returns true when a bare-shell launch was detected (caller must
|
|
5462
|
+
* Returns true when a persistent bare-shell launch was detected (caller must
|
|
5463
|
+
* NOT flush). A transient wrapper shell gets a bounded chance to exec the CLI.
|
|
5242
5464
|
*/
|
|
5243
|
-
function detectBareShellLaunch() {
|
|
5465
|
+
async function detectBareShellLaunch() {
|
|
5244
5466
|
if (bareShellLaunchBlocked)
|
|
5245
5467
|
return true;
|
|
5246
5468
|
if (lastInitConfig?.adoptMode)
|
|
5247
5469
|
return false; // observing an existing pane, not launching
|
|
5248
5470
|
if (lastInitConfig?.wrapperCli)
|
|
5249
5471
|
return false; // launcher legitimately wraps the CLI (transient shell shim)
|
|
5250
|
-
|
|
5251
|
-
|
|
5472
|
+
// Hold the raw_input passthrough latch across the bounded settle poll: the
|
|
5473
|
+
// await below yields the event loop for up to 2s, and a concurrent raw_input
|
|
5474
|
+
// IPC handler (which bypasses isFlushing to preserve busy-delivery) must not
|
|
5475
|
+
// type into the pane while its leaf is still the unsettled shell.
|
|
5476
|
+
bareShellCheckInProgress = true;
|
|
5477
|
+
let comm;
|
|
5478
|
+
try {
|
|
5479
|
+
comm = await settleLaunchComm(() => {
|
|
5480
|
+
const pid = backend?.getChildPid?.();
|
|
5481
|
+
return pid ? readComm(pid) : undefined;
|
|
5482
|
+
});
|
|
5483
|
+
}
|
|
5484
|
+
finally {
|
|
5485
|
+
bareShellCheckInProgress = false;
|
|
5486
|
+
}
|
|
5487
|
+
// A restart can begin while we're suspended in the settle await: tmux staggers
|
|
5488
|
+
// teardown behind a 250–1999ms jitter, so cliRestartInProgress is already true
|
|
5489
|
+
// but the old backend is still alive. Do NOT classify that torn-down pane as a
|
|
5490
|
+
// failed launch — it would set the persistent bareShellLaunchBlocked and emit a
|
|
5491
|
+
// misdiagnosis card for a CLI that's about to be replaced. Return healthy; the
|
|
5492
|
+
// caller's own post-await restart fence keeps the queue intact for the
|
|
5493
|
+
// replacement generation (whose spawnCli resets bareShellChecked and re-runs
|
|
5494
|
+
// this check).
|
|
5495
|
+
if (cliRestartInProgress)
|
|
5252
5496
|
return false;
|
|
5253
|
-
const comm = readComm(pid);
|
|
5254
5497
|
if (!isBareShellComm(comm))
|
|
5255
5498
|
return false; // CLI (rust/go/node) is running — healthy launch
|
|
5256
5499
|
// Bare shell is the pane leaf → the CLI never launched. Tier the message on
|
|
@@ -5299,6 +5542,8 @@ async function flushPending() {
|
|
|
5299
5542
|
// backend's idle/task-done callback) write across that restart boundary.
|
|
5300
5543
|
if (cliRestartInProgress)
|
|
5301
5544
|
return;
|
|
5545
|
+
if (shouldHoldCodexRunnerInput(codexRunnerFreshness))
|
|
5546
|
+
return;
|
|
5302
5547
|
if (isFlushing)
|
|
5303
5548
|
return; // while loop in active flush will pick up new messages
|
|
5304
5549
|
if (!backend || !cliAdapter)
|
|
@@ -5407,10 +5652,19 @@ async function flushPending() {
|
|
|
5407
5652
|
// doesn't get them typed into the bare shell first.
|
|
5408
5653
|
if (!bareShellChecked) {
|
|
5409
5654
|
bareShellChecked = true;
|
|
5410
|
-
if (detectBareShellLaunch()) {
|
|
5655
|
+
if (await detectBareShellLaunch()) {
|
|
5411
5656
|
return; // finally{} releases the mutex; pendingMessages stay queued, untouched
|
|
5412
5657
|
}
|
|
5413
5658
|
}
|
|
5659
|
+
// detectBareShellLaunch() awaits a bounded settle poll; a restart can begin
|
|
5660
|
+
// during that yield (tmux staggers teardown behind a 250–1999ms jitter, so
|
|
5661
|
+
// cliRestartInProgress flips true while the old backend is still alive). Do
|
|
5662
|
+
// not write startup commands / rename / user prompts into a CLI already
|
|
5663
|
+
// promised to teardown — re-check the restart fence now, before any shift or
|
|
5664
|
+
// write. The queue is untouched; the replacement generation's markPromptReady
|
|
5665
|
+
// re-invokes flushPending.
|
|
5666
|
+
if (cliRestartInProgress)
|
|
5667
|
+
return; // finally{} releases the mutex; queue stays
|
|
5414
5668
|
// One-shot per spawn: type the bot's startup commands (e.g. `/effort
|
|
5415
5669
|
// ultracode`) into the CLI before the first user prompt drains. Both ready
|
|
5416
5670
|
// paths funnel through flushPending — the ready-gate settle for Claude-family
|
|
@@ -5425,7 +5679,9 @@ async function flushPending() {
|
|
|
5425
5679
|
// rename. Some passthroughs (/clear, /new) can rotate the native session;
|
|
5426
5680
|
// applying the canonical title last keeps the resume-picker label aligned.
|
|
5427
5681
|
if (rawInputReady && pendingRawInputs.length > 0 && backend) {
|
|
5428
|
-
const raw =
|
|
5682
|
+
const raw = freshnessInputQueue.takeRaw();
|
|
5683
|
+
if (!raw)
|
|
5684
|
+
return;
|
|
5429
5685
|
await deliverRawInput(raw);
|
|
5430
5686
|
return;
|
|
5431
5687
|
}
|
|
@@ -5454,7 +5710,9 @@ async function flushPending() {
|
|
|
5454
5710
|
return;
|
|
5455
5711
|
}
|
|
5456
5712
|
while (pendingMessages.length > 0 && backend && cliAdapter) {
|
|
5457
|
-
const item =
|
|
5713
|
+
const item = freshnessInputQueue.takeNormal();
|
|
5714
|
+
if (!item)
|
|
5715
|
+
break;
|
|
5458
5716
|
const durableWrite = item.dispatchAttempt !== undefined;
|
|
5459
5717
|
if (durableWrite)
|
|
5460
5718
|
durableTurnInFlight = true;
|
|
@@ -5514,7 +5772,13 @@ async function flushPending() {
|
|
|
5514
5772
|
log('Refused durable Claude submit: transcript terminal bridge is unavailable');
|
|
5515
5773
|
break;
|
|
5516
5774
|
}
|
|
5517
|
-
|
|
5775
|
+
if (lastInitConfig?.cliId === 'codex-app') {
|
|
5776
|
+
log(`Writing Codex App input to PTY (flush): `
|
|
5777
|
+
+ `replyTurn=${shortCorrelationId(item.turnId)} chars=${msg.length}`);
|
|
5778
|
+
}
|
|
5779
|
+
else {
|
|
5780
|
+
log(`Writing to PTY (flush): "${msg.substring(0, 80)}"`);
|
|
5781
|
+
}
|
|
5518
5782
|
// Defense in depth: TmuxPipeBackend's send methods no longer throw on a
|
|
5519
5783
|
// dead pane (they fire onExit instead), but writeInput can still throw
|
|
5520
5784
|
// for other reasons (fs errors while resolving the JSONL, a future
|
|
@@ -5533,10 +5797,10 @@ async function flushPending() {
|
|
|
5533
5797
|
result = { submitted: true };
|
|
5534
5798
|
}
|
|
5535
5799
|
else if (item.codexAppInput && cliAdapter.writeStructuredInput) {
|
|
5536
|
-
result = await cliAdapter.writeStructuredInput(backend, msg, item.codexAppInput);
|
|
5800
|
+
result = await cliAdapter.writeStructuredInput(backend, msg, item.codexAppInput, { turnId: item.turnId });
|
|
5537
5801
|
}
|
|
5538
5802
|
else {
|
|
5539
|
-
result = await cliAdapter.writeInput(backend, msg);
|
|
5803
|
+
result = await cliAdapter.writeInput(backend, msg, { turnId: item.turnId });
|
|
5540
5804
|
}
|
|
5541
5805
|
scheduleBusyPatternIdleProbe(`${cliName()} post-submit`);
|
|
5542
5806
|
}
|
|
@@ -5556,6 +5820,11 @@ async function flushPending() {
|
|
|
5556
5820
|
scheduleSubmitFailureNotify(logicalMsg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq, item);
|
|
5557
5821
|
break;
|
|
5558
5822
|
}
|
|
5823
|
+
if (lastInitConfig?.cliId === 'codex-app'
|
|
5824
|
+
&& item.turnId
|
|
5825
|
+
&& result?.submitted !== false) {
|
|
5826
|
+
rememberBounded(submittedCodexAppReplyTurnIds, item.turnId);
|
|
5827
|
+
}
|
|
5559
5828
|
// Persist any sessionId the adapter observed via authoritative sources
|
|
5560
5829
|
// (Claude's pid file, Codex's history). Done independently of submit
|
|
5561
5830
|
// outcome — the rotation is real even when the current Enter didn't
|
|
@@ -5615,7 +5884,7 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5615
5884
|
// old early-return silently dropped it after receiver had already persisted
|
|
5616
5885
|
// DISPATCHED.
|
|
5617
5886
|
if (cliRestartInProgress || !backend) {
|
|
5618
|
-
|
|
5887
|
+
freshnessInputQueue.enqueueNormal(next);
|
|
5619
5888
|
log(`Queued message while CLI backend is restarting (${pendingMessages.length} pending)`);
|
|
5620
5889
|
return true;
|
|
5621
5890
|
}
|
|
@@ -5626,13 +5895,14 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5626
5895
|
isFlushing,
|
|
5627
5896
|
supportsTypeAhead,
|
|
5628
5897
|
awaitingFirstPrompt,
|
|
5898
|
+
holdForRunnerReload: shouldHoldCodexRunnerInput(codexRunnerFreshness),
|
|
5629
5899
|
}) && cliAdapter.mergeQueuedInput === true;
|
|
5630
5900
|
const mergedQueued = shouldMergeQueued && mergeQueuedCliInput(pendingMessages, next);
|
|
5631
5901
|
if (mergedQueued) {
|
|
5632
5902
|
log(`Merged queued message (${pendingMessages.length} pending): "${content.substring(0, 80)}" — ${cliName()} ${awaitingFirstPrompt ? 'still booting' : 'is busy'}`);
|
|
5633
5903
|
}
|
|
5634
5904
|
else {
|
|
5635
|
-
|
|
5905
|
+
freshnessInputQueue.enqueueNormal(next);
|
|
5636
5906
|
}
|
|
5637
5907
|
// User-override semantics: a fresh Lark message while a TUI prompt is "active"
|
|
5638
5908
|
// takes precedence over the AI-detected prompt. The screen analyzer can be
|
|
@@ -5643,7 +5913,6 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5643
5913
|
if (tuiPromptBlocking) {
|
|
5644
5914
|
log(`User override: incoming Lark message clears tuiPromptBlocking — "${content.substring(0, 80)}"`);
|
|
5645
5915
|
tuiPromptBlocking = false;
|
|
5646
|
-
screenAnalyzer?.notifySelection('lark-input');
|
|
5647
5916
|
// Tear down the prompt card so the user doesn't see stale options.
|
|
5648
5917
|
send({
|
|
5649
5918
|
type: 'tui_prompt_resolved',
|
|
@@ -5663,7 +5932,11 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5663
5932
|
// delivers queued messages instead. See input-gate.ts; this fixes dispatch's
|
|
5664
5933
|
// brief reaching Codex before its first idle and never landing.
|
|
5665
5934
|
if (!sessionRenameInFlight && commandLineWritesPending === 0 && shouldWriteNow({
|
|
5666
|
-
isPromptReady,
|
|
5935
|
+
isPromptReady,
|
|
5936
|
+
isFlushing,
|
|
5937
|
+
supportsTypeAhead,
|
|
5938
|
+
awaitingFirstPrompt,
|
|
5939
|
+
holdForRunnerReload: shouldHoldCodexRunnerInput(codexRunnerFreshness),
|
|
5667
5940
|
})) {
|
|
5668
5941
|
if (!mergedQueued)
|
|
5669
5942
|
log(`Writing to PTY: "${content.substring(0, 80)}"`);
|
|
@@ -5699,8 +5972,6 @@ function startScreenUpdates() {
|
|
|
5699
5972
|
if (awaitingFirstPrompt)
|
|
5700
5973
|
return;
|
|
5701
5974
|
let status = isPromptReady ? 'idle' : 'working';
|
|
5702
|
-
if (screenAnalyzer?.isAnalyzing)
|
|
5703
|
-
status = 'analyzing';
|
|
5704
5975
|
void (async () => {
|
|
5705
5976
|
let content = lastContent;
|
|
5706
5977
|
let changed = false;
|
|
@@ -5727,8 +5998,9 @@ function startScreenUpdates() {
|
|
|
5727
5998
|
const hash = pipeText.ansi;
|
|
5728
5999
|
changed = hash !== lastTextSnapshotHash;
|
|
5729
6000
|
lastTextSnapshotHash = hash;
|
|
5730
|
-
// Refresh the unfiltered cache that
|
|
5731
|
-
//
|
|
6001
|
+
// Refresh the unfiltered snapshot cache (lastAnalyzerSnapshot) that
|
|
6002
|
+
// usage-limit detection and the CoCo picker read from. Same tmux
|
|
6003
|
+
// call would otherwise need to fire twice per tick.
|
|
5732
6004
|
if (changed) {
|
|
5733
6005
|
const rawSnap = await snapshotToText(backend, renderCols, renderRows, { filter: false });
|
|
5734
6006
|
if (rawSnap)
|
|
@@ -6518,8 +6790,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6518
6790
|
// the full in-memory conversation. In that case the resume-vs-fresh question
|
|
6519
6791
|
// is moot: we must NOT run the pre-flight fallback (which would drop --resume
|
|
6520
6792
|
// and post a misleading "started a fresh clean session — context lost" card
|
|
6521
|
-
// on EVERY restart, e.g. for a sandboxed session whose transcript lives
|
|
6522
|
-
//
|
|
6793
|
+
// on EVERY restart, e.g. for a sandboxed session whose transcript lives at a
|
|
6794
|
+
// real host path the probe may not be able to stat). Computed here (not at
|
|
6523
6795
|
// the spawn site below) so the pre-flight can short-circuit on it.
|
|
6524
6796
|
let persistentSessionName = selectedBackend.persistentSessionName;
|
|
6525
6797
|
// [read-isolation] Before we decide to reattach a persistent pane: a pane can
|
|
@@ -6595,6 +6867,27 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6595
6867
|
willReattachPersistent = selectedBackend.isReattach === true;
|
|
6596
6868
|
}
|
|
6597
6869
|
}
|
|
6870
|
+
const replacementExpectedFresh = codexRunnerFreshness === 'restarting_fresh';
|
|
6871
|
+
const freshness = decideCodexRunnerFreshness({
|
|
6872
|
+
cliId: cfg.cliId,
|
|
6873
|
+
adoptMode: cfg.adoptMode === true,
|
|
6874
|
+
persistentReattach: willReattachPersistent,
|
|
6875
|
+
replacementExpectedFresh,
|
|
6876
|
+
currentBuildId: cfg.runnerBuildId,
|
|
6877
|
+
persistedBuildId: cfg.persistedRunnerBuildId,
|
|
6878
|
+
});
|
|
6879
|
+
codexRunnerFreshness = freshness.state;
|
|
6880
|
+
persistCodexRunnerBuildOnReady = freshness.persistOnReady;
|
|
6881
|
+
log(`Codex runner freshness=${freshness.state} reason=${freshness.reason}`);
|
|
6882
|
+
if (freshness.reason === 'replacement_reattached' && activeRestartAttemptId) {
|
|
6883
|
+
send({
|
|
6884
|
+
type: 'restart_result',
|
|
6885
|
+
attemptId: activeRestartAttemptId,
|
|
6886
|
+
status: 'failed',
|
|
6887
|
+
category: 'spawn_failed',
|
|
6888
|
+
});
|
|
6889
|
+
activeRestartAttemptId = undefined;
|
|
6890
|
+
}
|
|
6598
6891
|
// The plugin set is stable only for the lifetime of one real CLI process.
|
|
6599
6892
|
// A warm worker reattach keeps the existing Gateway and catalog untouched;
|
|
6600
6893
|
// every fresh/resumed CLI spawn atomically refreshes both from current Bot config.
|
|
@@ -6618,6 +6911,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6618
6911
|
// diagnostic instead of having the prompt typed into it.
|
|
6619
6912
|
bareShellLaunchBlocked = false;
|
|
6620
6913
|
bareShellChecked = false;
|
|
6914
|
+
bareShellCheckInProgress = false;
|
|
6621
6915
|
// ── Resume pre-flight check + two-tier fallback ──────────────────────────
|
|
6622
6916
|
// Tier 1 (adapter probe): adapter.checkResumeTargetExists returns false
|
|
6623
6917
|
// → skip --resume, spawn FRESH.
|
|
@@ -6655,6 +6949,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6655
6949
|
log(`WARN Claude resume transcript sync failed: ${err.message}`);
|
|
6656
6950
|
}
|
|
6657
6951
|
}
|
|
6952
|
+
// Hermes stores sessions in a SQLite state.db (not cwd-scoped JSONL). Resolve
|
|
6953
|
+
// the same path the bridge uses — honoring per-bot env, the forced
|
|
6954
|
+
// BOTMUX_SESSION_ID, and the hermes-botmux-session wrapper profile — so the
|
|
6955
|
+
// adapter probe below queries the exact DB Hermes will `--resume` against.
|
|
6956
|
+
const hermesResumeStateDbPath = cfg.cliId === 'hermes'
|
|
6957
|
+
? resolveHermesStateDbPath({ ...process.env, ...sanitizePerBotEnv(cfg.env), BOTMUX_SESSION_ID: cfg.sessionId }, { botmuxSessionProfile: basename(cfg.cliPathOverride ?? '') === 'hermes-botmux-session' })
|
|
6958
|
+
: undefined;
|
|
6658
6959
|
const tier2ForceFresh = effectiveResume && consecutiveInWorkerRestarts >= 2;
|
|
6659
6960
|
let tier1ProbeFalse = false;
|
|
6660
6961
|
if (effectiveResume && !tier2ForceFresh && !willReattachPersistent) {
|
|
@@ -6663,6 +6964,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6663
6964
|
cliSessionId: effectiveCliSessionId,
|
|
6664
6965
|
workingDir: cfg.workingDir,
|
|
6665
6966
|
dataDir: claudeDataDir,
|
|
6967
|
+
stateDbPath: hermesResumeStateDbPath,
|
|
6666
6968
|
});
|
|
6667
6969
|
if (probe === false)
|
|
6668
6970
|
tier1ProbeFalse = true;
|
|
@@ -7165,15 +7467,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7165
7467
|
mkdirSync(join(dataDir, 'attachments', cfg.larkAppId), { recursive: true });
|
|
7166
7468
|
}
|
|
7167
7469
|
catch { /* */ }
|
|
7168
|
-
//
|
|
7169
|
-
//
|
|
7170
|
-
// install's first in-sandbox `schedule add` can bind+write it on bwrap too.
|
|
7171
|
-
try {
|
|
7172
|
-
const sf = join(dataDir, 'schedules.json');
|
|
7173
|
-
if (!existsSync(sf))
|
|
7174
|
-
writeFileSync(sf, '{}');
|
|
7175
|
-
}
|
|
7176
|
-
catch { /* */ }
|
|
7470
|
+
// (Schedules moved into each bot's BOT_HOME — the whole dir is already
|
|
7471
|
+
// bound readWrite for the owner, so no per-file pre-create is needed.)
|
|
7177
7472
|
const mandatoryDenyPaths = [];
|
|
7178
7473
|
const mandatoryDenyRegexes = [];
|
|
7179
7474
|
const mandatoryReadOnlyPaths = [];
|
|
@@ -7964,6 +8259,16 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7964
8259
|
// prompt-ready — without this hook a follow-up arriving mid-task would sit
|
|
7965
8260
|
// in pendingMessages forever once the task finishes.
|
|
7966
8261
|
backend.onTaskDone?.(() => {
|
|
8262
|
+
// Generation fence (matches the onAgentStatus above and the onExit below):
|
|
8263
|
+
// a stale RiffBackend's `fetchAndEmitOutput(...).finally(taskDoneCb)` can
|
|
8264
|
+
// resolve AFTER destroySession()/kill() during a restart (neither clears
|
|
8265
|
+
// taskDoneCb nor awaits the in-flight fetch). If that late callback reached
|
|
8266
|
+
// markPromptReady() while a replacement is spawning, it would ride through
|
|
8267
|
+
// the global restart gate and emit a premature `restart_result: succeeded`,
|
|
8268
|
+
// swallowing the replacement's true terminal outcome. Only the current
|
|
8269
|
+
// backend generation may re-arm prompt-ready.
|
|
8270
|
+
if (backend !== observedBackend)
|
|
8271
|
+
return;
|
|
7967
8272
|
log(`${cliName()} task finished — re-arming prompt-ready for queued follow-ups`);
|
|
7968
8273
|
markPromptReady();
|
|
7969
8274
|
});
|
|
@@ -8002,22 +8307,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
8002
8307
|
// before exiting while fs.watch/the 1s poller is still queued. Drain it
|
|
8003
8308
|
// synchronously before claiming `cli_exit`; otherwise ambiguous wins the
|
|
8004
8309
|
// deduper and needlessly replays a turn that actually completed.
|
|
8005
|
-
|
|
8006
|
-
try {
|
|
8007
|
-
bridgeDrainAndMaybeEmit();
|
|
8008
|
-
}
|
|
8009
|
-
catch (err) {
|
|
8010
|
-
log(`Bridge exit drain failed: ${err.message}`);
|
|
8011
|
-
}
|
|
8012
|
-
}
|
|
8013
|
-
if (codexBridgeFallbackActive()) {
|
|
8014
|
-
try {
|
|
8015
|
-
codexBridgeDrainAndMaybeEmit({ signalIdle: false });
|
|
8016
|
-
}
|
|
8017
|
-
catch (err) {
|
|
8018
|
-
log(`Codex bridge exit drain failed: ${err.message}`);
|
|
8019
|
-
}
|
|
8020
|
-
}
|
|
8310
|
+
drainReliableTerminalBeforeInterrupt();
|
|
8021
8311
|
// Race-safe with transcript final / submit-failure: the worker-local
|
|
8022
8312
|
// terminal deduper lets exactly one status win for this attempt.
|
|
8023
8313
|
emitTurnTerminal(exitedTurnId, 'ambiguous', 'cli_exit', exitedDispatchAttempt);
|
|
@@ -8059,6 +8349,17 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
8059
8349
|
isPromptReady = false;
|
|
8060
8350
|
currentBotmuxTurnId = undefined;
|
|
8061
8351
|
currentBotmuxDispatchAttempt = undefined;
|
|
8352
|
+
if (!intentionalRestart && activeRestartAttemptId) {
|
|
8353
|
+
send({
|
|
8354
|
+
type: 'restart_result',
|
|
8355
|
+
attemptId: activeRestartAttemptId,
|
|
8356
|
+
status: 'failed',
|
|
8357
|
+
category: 'runner_exited',
|
|
8358
|
+
});
|
|
8359
|
+
activeRestartAttemptId = undefined;
|
|
8360
|
+
}
|
|
8361
|
+
if (!intentionalRestart)
|
|
8362
|
+
freshnessInputQueue.onReplacementFailed();
|
|
8062
8363
|
if (intentionalRestart) {
|
|
8063
8364
|
log('Suppressed claude_exit for intentional in-worker restart');
|
|
8064
8365
|
}
|
|
@@ -8161,8 +8462,8 @@ function killCli(opts = {}) {
|
|
|
8161
8462
|
promptReadyDetectedDuringSettle = false;
|
|
8162
8463
|
readyPatternSeenDuringHold = false;
|
|
8163
8464
|
awaitingPostSessionStartPromptEvidence = false;
|
|
8164
|
-
stopScreenAnalyzer();
|
|
8165
8465
|
stopStuckDetector();
|
|
8466
|
+
tuiPromptBlocking = false;
|
|
8166
8467
|
stopScreenUpdates();
|
|
8167
8468
|
backend?.kill();
|
|
8168
8469
|
backend = null;
|
|
@@ -8180,10 +8481,10 @@ function killCli(opts = {}) {
|
|
|
8180
8481
|
cliPidMarker = null;
|
|
8181
8482
|
}
|
|
8182
8483
|
completeManagedTurnOriginRevocation(sandboxRelayCapability, currentBotmuxTurnId, currentBotmuxDispatchAttempt);
|
|
8183
|
-
// Stop the sandbox outbox watcher, then
|
|
8184
|
-
// per-session sandbox tree. In the
|
|
8185
|
-
//
|
|
8186
|
-
//
|
|
8484
|
+
// Stop the sandbox outbox watcher, then reclaim the deny-mask mountpoints +
|
|
8485
|
+
// remove the per-session sandbox tree. In the fs-policy model the CLI writes
|
|
8486
|
+
// the project DIRECTLY at real host paths (no upper changeset to land), so by
|
|
8487
|
+
// close time everything worth keeping is already on disk.
|
|
8187
8488
|
if (sandboxStopWatcher) {
|
|
8188
8489
|
try {
|
|
8189
8490
|
sandboxStopWatcher();
|
|
@@ -8196,6 +8497,9 @@ function killCli(opts = {}) {
|
|
|
8196
8497
|
currentBotmuxTurnId = undefined;
|
|
8197
8498
|
currentBotmuxDispatchAttempt = undefined;
|
|
8198
8499
|
currentVcMeetingImTurnOrigin = undefined;
|
|
8500
|
+
submittedCodexAppReplyTurnIds.clear();
|
|
8501
|
+
pendingCodexAppSteerAckIds.clear();
|
|
8502
|
+
acknowledgedCodexAppSteers.clear();
|
|
8199
8503
|
if (sandboxCleanup) {
|
|
8200
8504
|
try {
|
|
8201
8505
|
sandboxCleanup();
|
|
@@ -8235,6 +8539,7 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
8235
8539
|
// of firing idle/task-done callbacks. Inputs accepted in that interval must
|
|
8236
8540
|
// remain queued until a replacement backend has been installed.
|
|
8237
8541
|
cliRestartInProgress = true;
|
|
8542
|
+
replacementSpawnInProgress = false;
|
|
8238
8543
|
rawInputRestartGate = true;
|
|
8239
8544
|
// The Node worker stays alive through this restart, so the daemon will see
|
|
8240
8545
|
// neither claude_exit nor worker-exit. Explicitly revoke the old turn's
|
|
@@ -8244,9 +8549,13 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
8244
8549
|
revokeManagedTurnOriginForRestart();
|
|
8245
8550
|
log(`Restart requested (${reason})`);
|
|
8246
8551
|
// Tier-2 guard: 2nd consecutive in-worker restart forces FRESH. Tier-1
|
|
8247
|
-
// adapter probing is still re-run on every spawn.
|
|
8248
|
-
|
|
8249
|
-
|
|
8552
|
+
// adapter probing is still re-run on every spawn. skipRestartBudget(角色
|
|
8553
|
+
// 切换的 cwd-move respawn)不计入:那是用户主动迁移不是崩溃恢复,计进去
|
|
8554
|
+
// 会让「切角色 + 一次无关重启」无故触发强制 FRESH 丢上下文。
|
|
8555
|
+
if (!opts.skipRestartBudget) {
|
|
8556
|
+
consecutiveInWorkerRestarts++;
|
|
8557
|
+
log(`Restart count: ${consecutiveInWorkerRestarts} (>=2 forces FRESH)`);
|
|
8558
|
+
}
|
|
8250
8559
|
const restart = async () => {
|
|
8251
8560
|
try {
|
|
8252
8561
|
tmuxRestartTimer = null;
|
|
@@ -8276,12 +8585,13 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
8276
8585
|
killCli({ preservePending: opts.preservePending });
|
|
8277
8586
|
awaitingFirstPrompt = true;
|
|
8278
8587
|
setTimeout(async () => {
|
|
8588
|
+
let spawnedWorkingDir;
|
|
8279
8589
|
if (lastInitConfig) {
|
|
8280
8590
|
startScreenUpdates();
|
|
8281
|
-
startScreenAnalyzer();
|
|
8282
8591
|
startStuckDetector();
|
|
8283
8592
|
try {
|
|
8284
8593
|
const restartCfg = { ...lastInitConfig, resume: true, prompt: '', cliSessionId: rpcThreadId ?? lastInitConfig.cliSessionId };
|
|
8594
|
+
spawnedWorkingDir = restartCfg.workingDir;
|
|
8285
8595
|
// Re-engage RPC so the new --remote pane binds to the CURRENT app-server
|
|
8286
8596
|
// (a fresh port), not the dead prior one. engageCodexRpc only sets
|
|
8287
8597
|
// remote* on success, else spawnCli falls back to paste.
|
|
@@ -8292,27 +8602,64 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
8292
8602
|
rpcPluginGenerationPrepared = true;
|
|
8293
8603
|
await engageCodexRpc(restartCfg);
|
|
8294
8604
|
}
|
|
8605
|
+
replacementSpawnInProgress = true;
|
|
8295
8606
|
await spawnCli(restartCfg, { pluginGenerationPrepared: rpcPluginGenerationPrepared });
|
|
8296
8607
|
await prepareCodexNativeTitleGeneration(restartCfg, codexRpcEngine);
|
|
8608
|
+
replacementSpawnInProgress = false;
|
|
8297
8609
|
if (codexRpcEngine)
|
|
8298
8610
|
armRpcStartupDialogDismiss();
|
|
8299
8611
|
}
|
|
8300
8612
|
catch (err) {
|
|
8613
|
+
replacementSpawnInProgress = false;
|
|
8301
8614
|
cliRestartInProgress = false;
|
|
8302
8615
|
await sendFatalWorkerErrorAndExit(err);
|
|
8303
8616
|
return;
|
|
8304
8617
|
}
|
|
8305
8618
|
}
|
|
8306
8619
|
cliRestartInProgress = false;
|
|
8620
|
+
replacementSpawnInProgress = false;
|
|
8621
|
+
// Follow-up decision (pure, unit-tested in restart-followup-policy.ts):
|
|
8622
|
+
// - cwd-move: a role-switch restart landed after restartCfg was
|
|
8623
|
+
// snapshotted → CLI came up in the old cwd while daemon repinned to
|
|
8624
|
+
// the new one. Respawn to converge (budget skipped only if the
|
|
8625
|
+
// backend is still alive; a dead one keeps the crash evidence).
|
|
8626
|
+
// - replacement-recovery: the freshly-spawned CLI exited inside the
|
|
8627
|
+
// window cliRestartInProgress still covers (spawnCli +
|
|
8628
|
+
// prepareCodexNativeTitleGeneration). onExit nulled `backend`
|
|
8629
|
+
// synchronously, so `!backend` is the ground-truth recovery signal —
|
|
8630
|
+
// NOT a merged daemon auto-restart (a restart message carries no
|
|
8631
|
+
// trustworthy source, so a healthy duplicate /restart during the
|
|
8632
|
+
// window must NOT be misread as a crash and force a budget-burning
|
|
8633
|
+
// second restart). Recover now or the session strands at
|
|
8634
|
+
// backend=null needing a manual /restart. Genuine crash recovery →
|
|
8635
|
+
// COUNTS toward tier-2 FRESH.
|
|
8636
|
+
const followup = decideRestartFollowup({
|
|
8637
|
+
spawnedWorkingDir,
|
|
8638
|
+
currentWorkingDir: lastInitConfig?.workingDir,
|
|
8639
|
+
backendAlive: !!backend,
|
|
8640
|
+
});
|
|
8641
|
+
if (followup.kind !== 'none') {
|
|
8642
|
+
const reason = followup.kind === 'cwd-move'
|
|
8643
|
+
? `cwd-move follow-up respawn → ${lastInitConfig?.workingDir}`
|
|
8644
|
+
: 'replacement-exit follow-up restart (replacement exited during in-flight restart)';
|
|
8645
|
+
log(reason);
|
|
8646
|
+
void restartCliProcess(reason, { preservePending: true, skipRestartBudget: followup.skipRestartBudget });
|
|
8647
|
+
return;
|
|
8648
|
+
}
|
|
8307
8649
|
// Riff marks itself prompt-ready inside spawnCli(); that early flush is
|
|
8308
8650
|
// intentionally held by the restart gate above. Release its raw-input
|
|
8309
8651
|
// fence now; other backends keep it until their later markPromptReady().
|
|
8310
8652
|
if (effectiveBackendType === 'riff' && isPromptReady)
|
|
8311
8653
|
releaseRawInputRestartGate();
|
|
8312
|
-
|
|
8654
|
+
// A local replacement process can exist before its TUI input box does.
|
|
8655
|
+
// Only re-kick a prompt that became ready while the restart fence was
|
|
8656
|
+
// still armed; otherwise markPromptReady() owns the first flush.
|
|
8657
|
+
if (isPromptReady)
|
|
8658
|
+
void flushPending();
|
|
8313
8659
|
}, 500);
|
|
8314
8660
|
}
|
|
8315
8661
|
catch (err) {
|
|
8662
|
+
replacementSpawnInProgress = false;
|
|
8316
8663
|
cliRestartInProgress = false;
|
|
8317
8664
|
try {
|
|
8318
8665
|
await sendFatalWorkerErrorAndExit(err);
|
|
@@ -8435,6 +8782,14 @@ function startWebServer(host, preferredPort) {
|
|
|
8435
8782
|
const startAttach = (cols, rows) => {
|
|
8436
8783
|
if (cp)
|
|
8437
8784
|
return;
|
|
8785
|
+
// Why: a prior web resize may leave the shared tmux window in manual mode.
|
|
8786
|
+
// `largest` restores the shared default without letting a smaller/newer
|
|
8787
|
+
// viewer resize every other attached client.
|
|
8788
|
+
spawnSync('tmux', ['set-option', '-t', tmuxTarget, 'window-size', 'largest'], {
|
|
8789
|
+
stdio: 'ignore',
|
|
8790
|
+
env: tmuxEnv(),
|
|
8791
|
+
timeout: 3000,
|
|
8792
|
+
});
|
|
8438
8793
|
// Defense in depth: view-capability clients attach through tmux's
|
|
8439
8794
|
// own read-only mode as well as the WebSocket input gate below.
|
|
8440
8795
|
cp = pty.spawn('tmux', [
|
|
@@ -9528,6 +9883,15 @@ async function sendFatalWorkerErrorAndExit(err, turnId = currentBotmuxTurnId, di
|
|
|
9528
9883
|
if (fatalWorkerErrorPending)
|
|
9529
9884
|
return;
|
|
9530
9885
|
fatalWorkerErrorPending = true;
|
|
9886
|
+
if (activeRestartAttemptId) {
|
|
9887
|
+
await sendAndFlush({
|
|
9888
|
+
type: 'restart_result',
|
|
9889
|
+
attemptId: activeRestartAttemptId,
|
|
9890
|
+
status: 'failed',
|
|
9891
|
+
category: 'spawn_failed',
|
|
9892
|
+
});
|
|
9893
|
+
activeRestartAttemptId = undefined;
|
|
9894
|
+
}
|
|
9531
9895
|
await sendAndFlush({
|
|
9532
9896
|
type: 'error',
|
|
9533
9897
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -9551,6 +9915,7 @@ process.on('message', async (raw) => {
|
|
|
9551
9915
|
if (lastInitConfig)
|
|
9552
9916
|
return; // already initialized
|
|
9553
9917
|
lastInitConfig = msg;
|
|
9918
|
+
activeRestartAttemptId = msg.restartAttemptId;
|
|
9554
9919
|
sessionId = msg.sessionId;
|
|
9555
9920
|
refreshTerminalViewToken();
|
|
9556
9921
|
refreshTerminalWriteToken();
|
|
@@ -9596,7 +9961,6 @@ process.on('message', async (raw) => {
|
|
|
9596
9961
|
if (!isWorkflowWorker()) {
|
|
9597
9962
|
port = await startWebServer(config.web.workerHost, msg.webPort);
|
|
9598
9963
|
startScreenUpdates();
|
|
9599
|
-
startScreenAnalyzer();
|
|
9600
9964
|
startStuckDetector();
|
|
9601
9965
|
}
|
|
9602
9966
|
else {
|
|
@@ -9764,12 +10128,11 @@ process.on('message', async (raw) => {
|
|
|
9764
10128
|
if (!backend && crashDiagnosticStopped && lastInitConfig && !lastInitConfig.adoptMode) {
|
|
9765
10129
|
log('Message received after crash-loop stop; retrying CLI start');
|
|
9766
10130
|
destroyCrashDiagnosticTerminal('retry after message');
|
|
9767
|
-
stopScreenAnalyzer();
|
|
9768
10131
|
stopStuckDetector();
|
|
10132
|
+
tuiPromptBlocking = false;
|
|
9769
10133
|
stopScreenUpdates();
|
|
9770
10134
|
awaitingFirstPrompt = true;
|
|
9771
10135
|
startScreenUpdates();
|
|
9772
|
-
startScreenAnalyzer();
|
|
9773
10136
|
startStuckDetector();
|
|
9774
10137
|
try {
|
|
9775
10138
|
const restartCfg = { ...lastInitConfig, resume: true, prompt: '' };
|
|
@@ -9912,9 +10275,18 @@ process.on('message', async (raw) => {
|
|
|
9912
10275
|
// barrier(shouldDeferUserFlush——/cd 未落地前任何用户输入都不得写入,
|
|
9913
10276
|
// 否则 passthrough 会执行在旧 cwd 的 CLI 里)时入队。注入排空后由
|
|
9914
10277
|
// flushPendingInjections 的 finally 补踢 flushPending 送达。
|
|
10278
|
+
// 第四个例外是启动稳定窗口:detectBareShellLaunch() 采到裸 shell 时会
|
|
10279
|
+
// await settleLaunchComm() 最长 2s 等 wrapper 完成 `exec <cli>`——这段
|
|
10280
|
+
// await 让出事件循环,而 IPC handler 不串行(见 raw-input-followup-
|
|
10281
|
+
// atomicity.test.ts),若此刻放行 passthrough 就会打进尚未 exec 的临时
|
|
10282
|
+
// shell。bareShellCheckInProgress 覆盖"检查进行中"、bareShellLaunchBlocked
|
|
10283
|
+
// 覆盖"已确认裸 shell 启动失败"两种状态,一并入队。裸 shell 确认失败后
|
|
10284
|
+
// 这些 pending 不会再被 flush(与 pendingMessages 同款处理),符合预期。
|
|
9915
10285
|
if (cliRestartInProgress || rawInputRestartGate || sessionRenameInFlight
|
|
9916
|
-
||
|
|
9917
|
-
|
|
10286
|
+
|| shouldHoldCodexRunnerInput(codexRunnerFreshness)
|
|
10287
|
+
|| injectionFlushing || shouldDeferUserFlush(pendingInjections)
|
|
10288
|
+
|| bareShellCheckInProgress || bareShellLaunchBlocked) {
|
|
10289
|
+
freshnessInputQueue.enqueueRaw(msg);
|
|
9918
10290
|
log(`Deferred passthrough slash command until CLI input gate settles: ${msg.content}`);
|
|
9919
10291
|
}
|
|
9920
10292
|
else {
|
|
@@ -9949,7 +10321,58 @@ process.on('message', async (raw) => {
|
|
|
9949
10321
|
break;
|
|
9950
10322
|
}
|
|
9951
10323
|
case 'restart': {
|
|
9952
|
-
|
|
10324
|
+
// 角色切换的 cwd-move respawn:respawn 用 {...lastInitConfig, resume:true},
|
|
10325
|
+
// 先收敛 workingDir 才能让 CLI 在新目录重启(新 cwd 的 CLAUDE.md/记忆索引
|
|
10326
|
+
// 开场注入)。旧桶 transcript 由 resume 预检的 syncClaudeResumeTargetToCwd
|
|
10327
|
+
// (COPY 最新 <sid>.jsonl 进新 cwd 桶,已在 master)接住,上下文不丢。
|
|
10328
|
+
if (msg.updateWorkingDir && lastInitConfig) {
|
|
10329
|
+
lastInitConfig.workingDir = msg.updateWorkingDir;
|
|
10330
|
+
}
|
|
10331
|
+
// restart 合并:已有一轮 restart 在飞(teardown 进行中,或 tmux jitter
|
|
10332
|
+
// 定时器未触发)时不叠加第二轮——叠加会 clearTimeout 吃掉首轮 teardown、
|
|
10333
|
+
// 把重启预算无故烧到 tier-2 强制 FRESH(丢上下文),非 tmux 路径还会
|
|
10334
|
+
// 双 spawn。workingDir 已收敛进 lastInitConfig,pending 的 spawn 展开
|
|
10335
|
+
// {...lastInitConfig} 时自然拿到新目录。
|
|
10336
|
+
//
|
|
10337
|
+
// 这里**只 break、不记任何 flag**:restart 消息不带可信来源,无法区分
|
|
10338
|
+
// 「replacement 崩溃触发的 auto-restart」与「用户重复点了一次 restart」。
|
|
10339
|
+
// replacement 真退出时 onExit 已同步把 backend 置 null,续体用 !backend 即可
|
|
10340
|
+
// 补 recovery(见 decideRestartFollowup);健康的重复 restart 就该被合并掉,
|
|
10341
|
+
// 记 flag 反而会逼健康进程再重启一轮、烧预算丢 --resume(正是合并要防的)。
|
|
10342
|
+
if (cliRestartInProgress || tmuxRestartTimer) {
|
|
10343
|
+
log(`Restart request merged into in-flight restart${msg.updateWorkingDir ? ` (workingDir → ${msg.updateWorkingDir})` : ''}`);
|
|
10344
|
+
break;
|
|
10345
|
+
}
|
|
10346
|
+
activeRestartAttemptId = msg.attemptId;
|
|
10347
|
+
codexRunnerFreshness = 'restarting_fresh';
|
|
10348
|
+
// restart 杀死 CLI,在飞的 durable turn 随之死亡。对被杀的那次投递,主动发一个
|
|
10349
|
+
// 'ambiguous' 终端回执:CLI 被中途杀掉,副作用到底发没发是**真的无法证明**
|
|
10350
|
+
// (故不能报 'cancelled'),交由 daemon 的重试策略即时对账。不发的话 receipt 会
|
|
10351
|
+
// 悬着,等 daemon 租约到期走 expire_durable_turn 的「无法证明 → 扣 ACK → 超时
|
|
10352
|
+
// fencing teardown」慢路径(还会把刚 respawn 的新 CLI 二次 teardown)。
|
|
10353
|
+
// emitTurnTerminal 自带释放(复位 durableTurnInFlight、退休 inflight input、
|
|
10354
|
+
// 撤销 turn-origin relay、丢弃 bridge 尝试)并对后续 CLI-exit 终端去重;它排的
|
|
10355
|
+
// flushPending 微任务会因下面 restartCliProcess 同步置位 cliRestartInProgress 而空跑。
|
|
10356
|
+
// 结算被 restart 打断的在飞 durable turn(编排见 settleDurableTurnForRestart,
|
|
10357
|
+
// 已单测):drain 已落盘可靠终态让「刚好在 kill 前完成、watcher 未消费」的 turn
|
|
10358
|
+
// 以 completed 抢占 deduper → 复检仍在飞才补发 ambiguous(否则无条件 ambiguous
|
|
10359
|
+
// 会误标已完成投递 → 同 key 可重派 → 副作用两次)→ 兜底释放 latch 免卡 respawn。
|
|
10360
|
+
// emitTurnTerminal 自带释放(复位 durableTurnInFlight、退休 inflight input、撤销
|
|
10361
|
+
// turn-origin relay、丢弃 bridge 尝试);它排的 flushPending 微任务会因下面
|
|
10362
|
+
// restartCliProcess 同步置位 cliRestartInProgress 而空跑。
|
|
10363
|
+
settleDurableTurnForRestart({
|
|
10364
|
+
hasInFlightTurn: durableTurnInFlight,
|
|
10365
|
+
hasCurrentTurnId: !!currentBotmuxTurnId,
|
|
10366
|
+
drain: () => drainReliableTerminalBeforeInterrupt(),
|
|
10367
|
+
isStillInFlight: () => durableTurnInFlight,
|
|
10368
|
+
emitAmbiguous: () => emitTurnTerminal(currentBotmuxTurnId, 'ambiguous', undefined, currentBotmuxDispatchAttempt),
|
|
10369
|
+
release: () => { durableTurnInFlight = false; inflightInputs.onTurnComplete(); },
|
|
10370
|
+
});
|
|
10371
|
+
await restartCliProcess(msg.updateWorkingDir ? `cwd-move respawn → ${msg.updateWorkingDir}` : 'daemon request',
|
|
10372
|
+
// cwd-move 是用户主动的目录迁移、不是崩溃恢复,不计入 tier-2 强制
|
|
10373
|
+
// FRESH 的重启预算;respawn 真失败仍有 claude_exit → daemon
|
|
10374
|
+
// auto-restart 那条裸 restart 的计数兜底。
|
|
10375
|
+
{ preservePending: true, skipRestartBudget: !!msg.updateWorkingDir });
|
|
9953
10376
|
break;
|
|
9954
10377
|
}
|
|
9955
10378
|
case 'expire_durable_turn': {
|
|
@@ -10075,12 +10498,10 @@ process.on('message', async (raw) => {
|
|
|
10075
10498
|
break;
|
|
10076
10499
|
}
|
|
10077
10500
|
case 'inject_command': {
|
|
10078
|
-
//
|
|
10079
|
-
//
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
}
|
|
10083
|
-
pendingInjections.push({ command: msg.command, barrier: !!msg.updateWorkingDir });
|
|
10501
|
+
// 唯一发送方是 /slash 路由的白名单 TUI 命令注入。cwd 移动不再走注入
|
|
10502
|
+
// (角色切换已改为 restart+updateWorkingDir 的 respawn),这里不接受
|
|
10503
|
+
// 任何 workingDir 改写——那会绕过 cd 路由的角色库硬校验。
|
|
10504
|
+
pendingInjections.push({ command: msg.command, barrier: false });
|
|
10084
10505
|
void flushPendingInjections();
|
|
10085
10506
|
break;
|
|
10086
10507
|
}
|
|
@@ -10213,10 +10634,11 @@ process.on('message', async (raw) => {
|
|
|
10213
10634
|
catch { /* best-effort */ }
|
|
10214
10635
|
backend = null;
|
|
10215
10636
|
isPromptReady = false;
|
|
10216
|
-
// Suspend INTENDS to resume later:
|
|
10217
|
-
//
|
|
10218
|
-
// over the SAME
|
|
10219
|
-
//
|
|
10637
|
+
// Suspend INTENDS to resume later: keep the per-session sandbox tree (the
|
|
10638
|
+
// outbox + pre-created deny-mask mountpoints) intact across the suspension
|
|
10639
|
+
// (on resume, prepareDirectSandbox re-binds over the SAME tree). So we stop
|
|
10640
|
+
// the outbox watcher (no live CLI to serve) but DO NOT run sandboxCleanup
|
|
10641
|
+
// (which would reclaim the mask mountpoints + rm the tree). We
|
|
10220
10642
|
// also disarm the exit-time teardown so process.exit(0) below can't reclaim
|
|
10221
10643
|
// it. (Crash/SIGKILL of a suspended-but-active session is still backstopped
|
|
10222
10644
|
// by the daemon's periodic sandbox reconciler.)
|
|
@@ -10227,7 +10649,7 @@ process.on('message', async (raw) => {
|
|
|
10227
10649
|
catch { /* */ }
|
|
10228
10650
|
sandboxStopWatcher = null;
|
|
10229
10651
|
}
|
|
10230
|
-
sandboxCleanup = null; // drop the ref WITHOUT calling it (keep the
|
|
10652
|
+
sandboxCleanup = null; // drop the ref WITHOUT calling it (keep the tree)
|
|
10231
10653
|
sandboxTeardownDone = true; // make the process.on('exit') hook a no-op
|
|
10232
10654
|
cleanup();
|
|
10233
10655
|
process.exit(0);
|
|
@@ -10303,15 +10725,15 @@ setInterval(() => {
|
|
|
10303
10725
|
}
|
|
10304
10726
|
}, 30_000).unref();
|
|
10305
10727
|
// ─── Sandbox crash-time teardown ─────────────────────────────────────────────
|
|
10306
|
-
// killCli() (which
|
|
10307
|
-
// from the SIGTERM/SIGINT/disconnect/watchdog handlers and the
|
|
10308
|
-
// cases. An UNCAUGHT exception or unhandled rejection kills
|
|
10309
|
-
// any of those firing, so without this hook a crashed
|
|
10310
|
-
//
|
|
10311
|
-
// per crash. We run a minimal,
|
|
10312
|
-
//
|
|
10313
|
-
// can't be trapped — the daemon-side
|
|
10314
|
-
// the backstop for that.)
|
|
10728
|
+
// killCli() (which reclaims the deny-mask mountpoints + rm's the per-session
|
|
10729
|
+
// tree) only runs from the SIGTERM/SIGINT/disconnect/watchdog handlers and the
|
|
10730
|
+
// close/suspend IPC cases. An UNCAUGHT exception or unhandled rejection kills
|
|
10731
|
+
// the process WITHOUT any of those firing, so without this hook a crashed
|
|
10732
|
+
// sandboxed worker would leak its pre-created host mask mountpoints (empty dirs/
|
|
10733
|
+
// files) + the per-session tree (disk leak) per crash. We run a minimal,
|
|
10734
|
+
// synchronous, best-effort sandbox teardown here so that residue is reclaimed
|
|
10735
|
+
// even on an abnormal exit. (SIGKILL still can't be trapped — the daemon-side
|
|
10736
|
+
// sweep + the periodic reconciler below are the backstop for that.)
|
|
10315
10737
|
function teardownSandboxBestEffort() {
|
|
10316
10738
|
if (sandboxTeardownDone)
|
|
10317
10739
|
return;
|