botmux 3.4.2 → 3.5.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 +10 -7
- package/README.md +11 -6
- package/dist/adapters/backend/herdr-backend.d.ts +43 -6
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +362 -28
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/reproduce-command.d.ts +39 -0
- package/dist/adapters/backend/reproduce-command.d.ts.map +1 -0
- package/dist/adapters/backend/reproduce-command.js +63 -0
- package/dist/adapters/backend/reproduce-command.js.map +1 -0
- package/dist/adapters/backend/session-backend-selector.d.ts +10 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +73 -4
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +16 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/cli/pi-initial-prompt-extension.d.ts +27 -0
- package/dist/adapters/cli/pi-initial-prompt-extension.d.ts.map +1 -0
- package/dist/adapters/cli/pi-initial-prompt-extension.js +38 -0
- package/dist/adapters/cli/pi-initial-prompt-extension.js.map +1 -0
- package/dist/adapters/cli/pi-initial-prompt.d.ts +21 -0
- package/dist/adapters/cli/pi-initial-prompt.d.ts.map +1 -0
- package/dist/adapters/cli/pi-initial-prompt.js +59 -0
- package/dist/adapters/cli/pi-initial-prompt.js.map +1 -0
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +15 -4
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +5 -0
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +8 -0
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +5 -0
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +23 -3
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/adapters/hook-installer.d.ts +7 -0
- package/dist/adapters/hook-installer.d.ts.map +1 -1
- package/dist/adapters/hook-installer.js +56 -0
- package/dist/adapters/hook-installer.js.map +1 -1
- package/dist/bot-registry.d.ts +13 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +118 -2
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/capabilities.d.ts +24 -0
- package/dist/cli/capabilities.d.ts.map +1 -0
- package/dist/cli/capabilities.js +25 -0
- package/dist/cli/capabilities.js.map +1 -0
- package/dist/cli/create-group-resolver.d.ts +24 -0
- package/dist/cli/create-group-resolver.d.ts.map +1 -1
- package/dist/cli/create-group-resolver.js +23 -0
- package/dist/cli/create-group-resolver.js.map +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts +5 -7
- package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
- package/dist/cli/daemon-lifecycle-env.js +13 -7
- package/dist/cli/daemon-lifecycle-env.js.map +1 -1
- package/dist/cli/exact-chat-grant-client.d.ts +48 -0
- package/dist/cli/exact-chat-grant-client.d.ts.map +1 -0
- package/dist/cli/exact-chat-grant-client.js +108 -0
- package/dist/cli/exact-chat-grant-client.js.map +1 -0
- package/dist/cli/exact-chat-grant.d.ts +18 -0
- package/dist/cli/exact-chat-grant.d.ts.map +1 -0
- package/dist/cli/exact-chat-grant.js +97 -0
- package/dist/cli/exact-chat-grant.js.map +1 -0
- package/dist/cli.d.ts +10 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +770 -126
- package/dist/cli.js.map +1 -1
- package/dist/codex-rpc-engine.d.ts +26 -0
- package/dist/codex-rpc-engine.d.ts.map +1 -1
- package/dist/codex-rpc-engine.js +113 -2
- package/dist/codex-rpc-engine.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -1
- package/dist/core/bot-live-control.d.ts +41 -0
- package/dist/core/bot-live-control.d.ts.map +1 -0
- package/dist/core/bot-live-control.js +135 -0
- package/dist/core/bot-live-control.js.map +1 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +28 -8
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/daemon-config-fence.d.ts +8 -0
- package/dist/core/daemon-config-fence.d.ts.map +1 -0
- package/dist/core/daemon-config-fence.js +13 -0
- package/dist/core/daemon-config-fence.js.map +1 -0
- package/dist/core/dashboard-events.d.ts +5 -0
- package/dist/core/dashboard-events.d.ts.map +1 -1
- package/dist/core/dashboard-events.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +21 -2
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +232 -5
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dispatch-registry.d.ts +12 -0
- package/dist/core/dispatch-registry.d.ts.map +1 -0
- package/dist/core/dispatch-registry.js +33 -0
- package/dist/core/dispatch-registry.js.map +1 -0
- package/dist/core/dispatch.d.ts +99 -0
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +136 -0
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/inflight-input-tracker.d.ts +1 -0
- package/dist/core/inflight-input-tracker.d.ts.map +1 -1
- package/dist/core/inflight-input-tracker.js.map +1 -1
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +10 -3
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/persistent-backend.d.ts +16 -2
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +40 -0
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/session-discovery.d.ts +8 -1
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +140 -8
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +12 -8
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-title.d.ts +8 -0
- package/dist/core/session-title.d.ts.map +1 -1
- package/dist/core/session-title.js +54 -0
- package/dist/core/session-title.js.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +15 -0
- package/dist/core/terminal-write-auth.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.js +20 -0
- package/dist/core/terminal-write-auth.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +3 -2
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +33 -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 +27 -8
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +389 -19
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +4 -2
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +397 -26
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts +127 -1
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +1104 -25
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/debug-terminal.d.ts +18 -0
- package/dist/dashboard/debug-terminal.d.ts.map +1 -0
- package/dist/dashboard/debug-terminal.js +338 -0
- package/dist/dashboard/debug-terminal.js.map +1 -0
- package/dist/dashboard/public-redact.d.ts +2 -0
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +12 -3
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/registry.d.ts +8 -0
- package/dist/dashboard/registry.d.ts.map +1 -1
- package/dist/dashboard/registry.js +13 -0
- package/dist/dashboard/registry.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +47 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +102 -1
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +2 -0
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +1 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +24 -5
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +15 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +19 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +76 -6
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/icon-tooltip.d.ts +3 -0
- package/dist/dashboard/web/icon-tooltip.d.ts.map +1 -0
- package/dist/dashboard/web/icon-tooltip.js +108 -0
- package/dist/dashboard/web/icon-tooltip.js.map +1 -0
- package/dist/dashboard/web/sessions-kanban.js +2 -2
- 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 +48 -9
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +46 -2
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +78 -0
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +88 -1
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/store.d.ts +2 -0
- package/dist/dashboard/web/store.d.ts.map +1 -1
- package/dist/dashboard/web/store.js +13 -1
- package/dist/dashboard/web/store.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-O376WSER.js +5 -0
- package/dist/dashboard-web/chunks/{chunk-JNRSPKLS.js → chunk-EJDYUINZ.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-CUV65BIQ.js → chunk-H7P5UV4W.js} +6 -6
- package/dist/dashboard-web/chunks/{chunk-P6GO5KT4.js → chunk-QCPPKCX3.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-UBOLHQCK.js → chunk-WXPMNEYV.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-page-KTC2QWNW.js → connectors-page-IR54AOAI.js} +1 -1
- package/dist/dashboard-web/chunks/{groups-page-B3EKGOLY.js → groups-page-3P4TLUMF.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-X6RXNCWX.js → insights-page-TDCZV56A.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-LLRA5LQW.js → monitor-room-QWCWL3SX.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-7MT5FEOS.js → monitoring-page-IOWFSNVC.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-KD46D5TD.js → office-page-5AUPEHZP.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-JUWNH32O.js → overview-page-QJMI6NLW.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-P6F7NLOI.js → roles-page-WJVB52ET.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-XIHWZST4.js → schedules-page-RVRWHP4V.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-KWLGBX6G.js +3 -0
- package/dist/dashboard-web/chunks/settings-page-TNZEPC7V.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-A46KVWBL.js → skills-page-7YT5TIX5.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-SDUC56DF.js → team-federation-page-NJZKTU7A.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-J5JOTUSQ.js → v3-page-NQ73UA2Q.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-U7LSOAWI.js → whiteboards-page-DSM3MDMN.js} +1 -1
- package/dist/dashboard-web/style.css +198 -7
- package/dist/dashboard.js +294 -7
- package/dist/dashboard.js.map +1 -1
- package/dist/features/codex-notifier/adoption.d.ts +11 -0
- package/dist/features/codex-notifier/adoption.d.ts.map +1 -0
- package/dist/features/codex-notifier/adoption.js +11 -0
- package/dist/features/codex-notifier/adoption.js.map +1 -0
- package/dist/features/codex-notifier/app-opener.d.ts +21 -0
- package/dist/features/codex-notifier/app-opener.d.ts.map +1 -0
- package/dist/features/codex-notifier/app-opener.js +33 -0
- package/dist/features/codex-notifier/app-opener.js.map +1 -0
- package/dist/features/codex-notifier/card-action.d.ts +27 -0
- package/dist/features/codex-notifier/card-action.d.ts.map +1 -0
- package/dist/features/codex-notifier/card-action.js +84 -0
- package/dist/features/codex-notifier/card-action.js.map +1 -0
- package/dist/features/codex-notifier/card.d.ts +12 -0
- package/dist/features/codex-notifier/card.d.ts.map +1 -0
- package/dist/features/codex-notifier/card.js +150 -0
- package/dist/features/codex-notifier/card.js.map +1 -0
- package/dist/features/codex-notifier/codex-context.d.ts +12 -0
- package/dist/features/codex-notifier/codex-context.d.ts.map +1 -0
- package/dist/features/codex-notifier/codex-context.js +161 -0
- package/dist/features/codex-notifier/codex-context.js.map +1 -0
- package/dist/features/codex-notifier/config.d.ts +9 -0
- package/dist/features/codex-notifier/config.d.ts.map +1 -0
- package/dist/features/codex-notifier/config.js +11 -0
- package/dist/features/codex-notifier/config.js.map +1 -0
- package/dist/features/codex-notifier/confirmed-turn.d.ts +20 -0
- package/dist/features/codex-notifier/confirmed-turn.d.ts.map +1 -0
- package/dist/features/codex-notifier/confirmed-turn.js +130 -0
- package/dist/features/codex-notifier/confirmed-turn.js.map +1 -0
- package/dist/features/codex-notifier/emitter.d.ts +13 -0
- package/dist/features/codex-notifier/emitter.d.ts.map +1 -0
- package/dist/features/codex-notifier/emitter.js +33 -0
- package/dist/features/codex-notifier/emitter.js.map +1 -0
- package/dist/features/codex-notifier/event-store.d.ts +29 -0
- package/dist/features/codex-notifier/event-store.d.ts.map +1 -0
- package/dist/features/codex-notifier/event-store.js +217 -0
- package/dist/features/codex-notifier/event-store.js.map +1 -0
- package/dist/features/codex-notifier/event.d.ts +46 -0
- package/dist/features/codex-notifier/event.d.ts.map +1 -0
- package/dist/features/codex-notifier/event.js +188 -0
- package/dist/features/codex-notifier/event.js.map +1 -0
- package/dist/features/codex-notifier/hook-cli.d.ts +23 -0
- package/dist/features/codex-notifier/hook-cli.d.ts.map +1 -0
- package/dist/features/codex-notifier/hook-cli.js +130 -0
- package/dist/features/codex-notifier/hook-cli.js.map +1 -0
- package/dist/features/codex-notifier/hook-installer.d.ts +19 -0
- package/dist/features/codex-notifier/hook-installer.d.ts.map +1 -0
- package/dist/features/codex-notifier/hook-installer.js +158 -0
- package/dist/features/codex-notifier/hook-installer.js.map +1 -0
- package/dist/features/codex-notifier/index.d.ts +21 -0
- package/dist/features/codex-notifier/index.d.ts.map +1 -0
- package/dist/features/codex-notifier/index.js +19 -0
- package/dist/features/codex-notifier/index.js.map +1 -0
- package/dist/features/codex-notifier/internal-turn.d.ts +5 -0
- package/dist/features/codex-notifier/internal-turn.d.ts.map +1 -0
- package/dist/features/codex-notifier/internal-turn.js +43 -0
- package/dist/features/codex-notifier/internal-turn.js.map +1 -0
- package/dist/features/codex-notifier/outbox-worker.d.ts +71 -0
- package/dist/features/codex-notifier/outbox-worker.d.ts.map +1 -0
- package/dist/features/codex-notifier/outbox-worker.js +290 -0
- package/dist/features/codex-notifier/outbox-worker.js.map +1 -0
- package/dist/features/codex-notifier/outbox.d.ts +25 -0
- package/dist/features/codex-notifier/outbox.d.ts.map +1 -0
- package/dist/features/codex-notifier/outbox.js +225 -0
- package/dist/features/codex-notifier/outbox.js.map +1 -0
- package/dist/features/codex-notifier/paths.d.ts +7 -0
- package/dist/features/codex-notifier/paths.d.ts.map +1 -0
- package/dist/features/codex-notifier/paths.js +20 -0
- package/dist/features/codex-notifier/paths.js.map +1 -0
- package/dist/features/codex-notifier/screen-lock.d.ts +12 -0
- package/dist/features/codex-notifier/screen-lock.d.ts.map +1 -0
- package/dist/features/codex-notifier/screen-lock.js +47 -0
- package/dist/features/codex-notifier/screen-lock.js.map +1 -0
- package/dist/features/codex-notifier/side-conversation-monitor.d.ts +102 -0
- package/dist/features/codex-notifier/side-conversation-monitor.d.ts.map +1 -0
- package/dist/features/codex-notifier/side-conversation-monitor.js +774 -0
- package/dist/features/codex-notifier/side-conversation-monitor.js.map +1 -0
- package/dist/features/codex-notifier/types.d.ts +42 -0
- package/dist/features/codex-notifier/types.d.ts.map +1 -0
- package/dist/features/codex-notifier/types.js +2 -0
- package/dist/features/codex-notifier/types.js.map +1 -0
- package/dist/features/codex-notifier/worker-lock.d.ts +8 -0
- package/dist/features/codex-notifier/worker-lock.d.ts.map +1 -0
- package/dist/features/codex-notifier/worker-lock.js +108 -0
- package/dist/features/codex-notifier/worker-lock.js.map +1 -0
- package/dist/global-config.d.ts +13 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +30 -0
- package/dist/global-config.js.map +1 -1
- package/dist/im/lark/card-builder.js +2 -2
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +2 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +165 -34
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +62 -4
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +237 -29
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +20 -30
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/platform/secure-host-file.d.ts.map +1 -1
- package/dist/platform/secure-host-file.js +173 -54
- package/dist/platform/secure-host-file.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +47 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -1
- package/dist/services/codex-app-threads.js +502 -10
- package/dist/services/codex-app-threads.js.map +1 -1
- package/dist/services/exact-chat-grant.d.ts +83 -0
- package/dist/services/exact-chat-grant.d.ts.map +1 -0
- package/dist/services/exact-chat-grant.js +249 -0
- package/dist/services/exact-chat-grant.js.map +1 -0
- package/dist/services/group-creator.d.ts +11 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +18 -9
- package/dist/services/group-creator.js.map +1 -1
- package/dist/services/local-cli-opener.d.ts.map +1 -1
- package/dist/services/local-cli-opener.js +10 -5
- package/dist/services/local-cli-opener.js.map +1 -1
- package/dist/services/traex-user-input.d.ts +19 -0
- package/dist/services/traex-user-input.d.ts.map +1 -0
- package/dist/services/traex-user-input.js +57 -0
- package/dist/services/traex-user-input.js.map +1 -0
- package/dist/setup/open-platform-automation.d.ts +24 -3
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +45 -8
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/setup/verify-permissions.d.ts +18 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -1
- package/dist/setup/verify-permissions.js +60 -0
- package/dist/setup/verify-permissions.js.map +1 -1
- package/dist/types.d.ts +70 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/child-env.d.ts +1 -1
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +2 -0
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/pending-input-queue.d.ts +19 -0
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js +21 -1
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/utils/stuck-detector.d.ts +45 -0
- package/dist/utils/stuck-detector.d.ts.map +1 -0
- package/dist/utils/stuck-detector.js +188 -0
- package/dist/utils/stuck-detector.js.map +1 -0
- package/dist/utils/stuck-key-guard.d.ts +75 -0
- package/dist/utils/stuck-key-guard.d.ts.map +1 -0
- package/dist/utils/stuck-key-guard.js +77 -0
- package/dist/utils/stuck-key-guard.js.map +1 -0
- package/dist/worker.js +807 -117
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-B5AY4B2J.js +0 -5
- package/dist/dashboard-web/chunks/sessions-page-2VECQ27T.js +0 -3
- package/dist/dashboard-web/chunks/settings-page-HMIUE2JA.js +0 -2
package/dist/worker.js
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
* 7. On 'restart', kills CLI and re-spawns with --resume
|
|
14
14
|
*/
|
|
15
15
|
import { randomBytes } from 'node:crypto';
|
|
16
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
|
|
16
|
+
import { mkdirSync, writeFileSync, unlinkSync, rmdirSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
|
|
17
17
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
18
|
-
import { join, basename, dirname } from 'node:path';
|
|
18
|
+
import { join, basename, dirname, delimiter } from 'node:path';
|
|
19
19
|
import { homedir, tmpdir } from 'node:os';
|
|
20
20
|
import { spawnSync } from 'node:child_process';
|
|
21
21
|
import { evaluateReadIsolationGate, evaluateCredentialOnlyIsolationGate, credentialIsolationRequired, deviceCredentialIsolationMarkerPath, isCredentialIsolationReservedBasename, buildCredentialIsolationRules, buildSeatbeltProfile, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildV2DenyPaths, buildV2DenyRegexes, buildV2CarveOuts, buildReadIsolationProtectedWriteRules, buildCliExecutableReadCarveOuts, buildWriteSandboxRules, buildLinuxReadIsolationMasks, isolationPaneMarkerContent, } from './adapters/cli/read-isolation.js';
|
|
22
|
-
import { killPersistentSession } from './core/persistent-backend.js';
|
|
22
|
+
import { killPersistentBackendTarget, killPersistentSession, probePersistentBackendTarget } from './core/persistent-backend.js';
|
|
23
23
|
import { readProcessStartIdentity } from './core/session-marker.js';
|
|
24
24
|
import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
|
|
25
25
|
import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
|
|
@@ -29,7 +29,7 @@ import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirs
|
|
|
29
29
|
import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
|
|
30
30
|
import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
|
|
31
31
|
import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
|
|
32
|
-
import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, shouldDeferArgsBakedDurablePrompt, shouldDeferInitialPromptForArgLimit, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
|
|
32
|
+
import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, resolveInitialPromptDelivery, shouldDeferArgsBakedDurablePrompt, shouldDeferInitialPromptForArgLimit, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
|
|
33
33
|
import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
|
|
34
34
|
import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
|
|
35
35
|
import { sanitizePerBotEnv } from './core/per-bot-env.js';
|
|
@@ -42,14 +42,17 @@ import { readSessionMcpRuntimeManifest, sessionMcpRuntimeHostOnlyPaths, } from '
|
|
|
42
42
|
import { prepareCliPluginGeneration } from './core/plugins/cli-generation.js';
|
|
43
43
|
import { loadBotConfigs } from './bot-registry.js';
|
|
44
44
|
import { readGlobalConfig } from './global-config.js';
|
|
45
|
-
import { deriveTerminalViewToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, } from './core/terminal-write-auth.js';
|
|
45
|
+
import { deriveTerminalViewToken, deriveTerminalWriteToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, } from './core/terminal-write-auth.js';
|
|
46
46
|
import { readPlatformBinding } from './platform/binding.js';
|
|
47
47
|
import { loadDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
|
|
48
48
|
import { InflightInputTracker } from './core/inflight-input-tracker.js';
|
|
49
49
|
import { shouldRunQuietRotation, evaluatePidResolverPullback, decideFingerprintSwitch, shouldHealAbsentBaseline, sessionIdFromJsonlPath, SESSION_ID_FILENAME_RE, } from './services/bridge-rotation-policy.js';
|
|
50
50
|
import { CodexBridgeQueue } from './services/codex-bridge-queue.js';
|
|
51
|
+
import { generateCodexAppThreadTitle, readCodexAppThreadMetadata, setCodexAppThreadName, } from './services/codex-app-threads.js';
|
|
52
|
+
import { buildBotmuxLarkNativeSessionTitle } from './core/session-title.js';
|
|
51
53
|
import { drainCodexRollout, findCodexRolloutBySessionId, findCodexRolloutByPid, splitCodexEventsByCutoff, extractLastCodexTurn, codexSessionIdFromRolloutPath } from './services/codex-transcript.js';
|
|
52
54
|
import { drainTraexRollout, findTraexRolloutBySessionId, findTraexRolloutByPid } from './services/traex-transcript.js';
|
|
55
|
+
import { parseTraexUserInputQuestions } from './services/traex-user-input.js';
|
|
53
56
|
import { cocoEventsPathForSession, drainCocoEvents, findCocoSessionByPid } from './services/coco-transcript.js';
|
|
54
57
|
import { currentHermesStateOffset, drainHermesStateDb, resolveHermesStateDbPath } from './services/hermes-transcript.js';
|
|
55
58
|
import { filterHermesEventsForBotmuxSession } from './services/hermes-session-filter.js';
|
|
@@ -87,6 +90,7 @@ import { ZellijObserveBackend } from './adapters/backend/zellij-observe-backend.
|
|
|
87
90
|
import { zellijEnv } from './setup/ensure-zellij.js';
|
|
88
91
|
import { isObserveBackend } from './adapters/backend/types.js';
|
|
89
92
|
import { selectSessionBackend, decideBackendGate, backendGateUserMessage } from './adapters/backend/session-backend-selector.js';
|
|
93
|
+
import { buildReproduceCommand, selectReproduceLaunch } from './adapters/backend/reproduce-command.js';
|
|
90
94
|
import { deriveRiffReposFromDirs, deriveRiffRepoFromWorkingDir, isValidRiffBaseUrl, isValidRiffSandboxCluster, } from './adapters/backend/riff-backend.js';
|
|
91
95
|
import { prepareSandbox, prepareCredentialOnlySandbox, credentialOnlySandboxAvailable, probeHostCredentialIsolationMechanism, attachSandboxOutbox, startOutboxWatcher, sandboxEnabled, sandboxedClaudeDataDir, localSandboxApplies, } from './adapters/backend/sandbox.js';
|
|
92
96
|
import { DEVICE_AUTHORITY_DIRECTORY, DEVICE_CREDENTIAL_FILE, } from './platform/device-paths.js';
|
|
@@ -94,6 +98,8 @@ import { tmuxEnv, probeTmuxFunctionalWithRetry } from './setup/ensure-tmux.js';
|
|
|
94
98
|
import { tmuxRestartJitterMs } from './core/tmux-recovery.js';
|
|
95
99
|
import { IdleDetector } from './utils/idle-detector.js';
|
|
96
100
|
import { ScreenAnalyzer } from './utils/screen-analyzer.js';
|
|
101
|
+
import { StuckDetector, matchHookReviewScreen } from './utils/stuck-detector.js';
|
|
102
|
+
import { processStuckWarningTuiKeys, shouldRearmStuckDetector } from './utils/stuck-key-guard.js';
|
|
97
103
|
import { captureToPng } from './utils/screenshot-renderer.js';
|
|
98
104
|
import { snapshotToPng, snapshotToText, shouldCaptureScreen, isScreenSelfDriven } from './utils/transient-snapshot.js';
|
|
99
105
|
import { chooseWebTerminalSeed } from './utils/web-terminal-seed.js';
|
|
@@ -109,7 +115,8 @@ import * as pty from 'node-pty';
|
|
|
109
115
|
import { createHash } from 'node:crypto';
|
|
110
116
|
import { hasInstalledSessionReadyHook, installHook, } from './adapters/hook-installer.js';
|
|
111
117
|
import { hookCommandFor } from './adapters/hook-command.js';
|
|
112
|
-
import { parseDaemonIpcPort } from './utils/daemon-discovery.js';
|
|
118
|
+
import { findOnlineDaemon, parseDaemonIpcPort } from './utils/daemon-discovery.js';
|
|
119
|
+
import { fetchDaemonIpc } from './core/daemon-ipc-auth.js';
|
|
113
120
|
import { withCodexAppContext } from './utils/codex-app-context.js';
|
|
114
121
|
import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.js';
|
|
115
122
|
import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
|
|
@@ -144,6 +151,36 @@ let remoteWsUrl;
|
|
|
144
151
|
let remoteThreadId;
|
|
145
152
|
let rpcDialogDismissTimer = null;
|
|
146
153
|
let rpcEnginePidMarker = null;
|
|
154
|
+
const piInitialPromptCleanupPaths = [];
|
|
155
|
+
const piInitialPromptCleanupDirs = [];
|
|
156
|
+
let piInitialPromptReadonlyRoots = [];
|
|
157
|
+
let piInitialPromptAdditionalArgs = [];
|
|
158
|
+
let piInitialPromptEnv = {};
|
|
159
|
+
function cleanupPiInitialPromptFiles() {
|
|
160
|
+
while (piInitialPromptCleanupPaths.length > 0) {
|
|
161
|
+
const p = piInitialPromptCleanupPaths.pop();
|
|
162
|
+
if (!p)
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
unlinkSync(p);
|
|
166
|
+
}
|
|
167
|
+
catch { /* best effort */ }
|
|
168
|
+
}
|
|
169
|
+
while (piInitialPromptCleanupDirs.length > 0) {
|
|
170
|
+
const dir = piInitialPromptCleanupDirs.pop();
|
|
171
|
+
if (!dir)
|
|
172
|
+
continue;
|
|
173
|
+
// Non-recursive on purpose: remove only an empty session-owned directory,
|
|
174
|
+
// never a shared root or a path that unexpectedly gained other content.
|
|
175
|
+
try {
|
|
176
|
+
rmdirSync(dir);
|
|
177
|
+
}
|
|
178
|
+
catch { /* best effort */ }
|
|
179
|
+
}
|
|
180
|
+
piInitialPromptReadonlyRoots = [];
|
|
181
|
+
piInitialPromptAdditionalArgs = [];
|
|
182
|
+
piInitialPromptEnv = {};
|
|
183
|
+
}
|
|
147
184
|
function stopCodexRpcEngine() {
|
|
148
185
|
const engine = codexRpcEngine;
|
|
149
186
|
codexRpcEngine = undefined;
|
|
@@ -220,6 +257,238 @@ async function codexRolloutProbe(cliId, threadId, promptText, timeoutMs) {
|
|
|
220
257
|
} while (Date.now() < deadline);
|
|
221
258
|
return false;
|
|
222
259
|
}
|
|
260
|
+
function codexNativeTitleEnv(cfg) {
|
|
261
|
+
const env = {
|
|
262
|
+
...redactChildEnv(process.env),
|
|
263
|
+
...sanitizePerBotEnv(cfg.env),
|
|
264
|
+
};
|
|
265
|
+
env.PATH = `${join(homedir(), '.botmux', 'bin')}${delimiter}${env.PATH ?? ''}`;
|
|
266
|
+
return env;
|
|
267
|
+
}
|
|
268
|
+
function registerNativeTitleForceClose(forceClose) {
|
|
269
|
+
nativeSessionTitleSyncForceClosers.add(forceClose);
|
|
270
|
+
return () => nativeSessionTitleSyncForceClosers.delete(forceClose);
|
|
271
|
+
}
|
|
272
|
+
async function applyCodexNativeSessionTitle(threadId, title, cfg, engine, wait, expectedRevision) {
|
|
273
|
+
if (expectedRevision !== nativeSessionTitleRevision)
|
|
274
|
+
return false;
|
|
275
|
+
if (engine?.activeThreadId === threadId) {
|
|
276
|
+
if (wait === 'resume') {
|
|
277
|
+
await engine.waitForThreadUpdatedAfter(nativeSessionTitleResumeUpdatedAt ?? Math.floor(Date.now() / 1000), 10_000);
|
|
278
|
+
}
|
|
279
|
+
else if (wait === 'preview') {
|
|
280
|
+
await engine.waitForThreadPreview(10_000);
|
|
281
|
+
}
|
|
282
|
+
if (expectedRevision !== nativeSessionTitleRevision)
|
|
283
|
+
return false;
|
|
284
|
+
await engine.setThreadName(title);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
const adapter = createCliAdapterSync('codex', cfg.cliPathOverride);
|
|
288
|
+
const abortController = new AbortController();
|
|
289
|
+
nativeSessionTitleSyncAbortControllers.add(abortController);
|
|
290
|
+
try {
|
|
291
|
+
await setCodexAppThreadName({
|
|
292
|
+
threadId,
|
|
293
|
+
name: title,
|
|
294
|
+
codexBin: adapter.resolvedBin,
|
|
295
|
+
cwd: cfg.workingDir,
|
|
296
|
+
env: codexNativeTitleEnv(cfg),
|
|
297
|
+
timeoutMs: 10_000,
|
|
298
|
+
signal: abortController.signal,
|
|
299
|
+
detached: true,
|
|
300
|
+
waitForExistingPreview: wait === 'preview',
|
|
301
|
+
...(wait === 'resume' ? {
|
|
302
|
+
waitForUpdatedAfter: nativeSessionTitleResumeUpdatedAt ?? Math.floor(Date.now() / 1000),
|
|
303
|
+
} : {}),
|
|
304
|
+
registerForceClose: registerNativeTitleForceClose,
|
|
305
|
+
});
|
|
306
|
+
return expectedRevision === nativeSessionTitleRevision;
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
nativeSessionTitleSyncAbortControllers.delete(abortController);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** 首条 UserMessage 落盘后先写 fallback,再用隔离的临时 turn 生成语义标题。 */
|
|
313
|
+
async function syncFreshCodexNativeSessionTitle(threadId, engine) {
|
|
314
|
+
const cfg = lastInitConfig;
|
|
315
|
+
const fallbackTitle = cfg?.nativeSessionTitle?.trim();
|
|
316
|
+
if (!cfg || cfg.cliId !== 'codex' || cfg.adoptMode || !fallbackTitle || !threadId)
|
|
317
|
+
return;
|
|
318
|
+
if (nativeSessionTitleAppliedThreadId === threadId || nativeSessionTitleSyncInFlight === threadId)
|
|
319
|
+
return;
|
|
320
|
+
const revision = nativeSessionTitleRevision;
|
|
321
|
+
const resumeGeneration = nativeSessionTitleCurrentGenerationResume;
|
|
322
|
+
let syncLatestTitle = false;
|
|
323
|
+
nativeSessionTitleSyncInFlight = threadId;
|
|
324
|
+
try {
|
|
325
|
+
const fallbackApplied = await applyCodexNativeSessionTitle(threadId, fallbackTitle, cfg, engine, resumeGeneration ? 'resume' : 'preview', revision);
|
|
326
|
+
if (!fallbackApplied || revision !== nativeSessionTitleRevision) {
|
|
327
|
+
syncLatestTitle = true;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
nativeSessionTitleAppliedThreadId = threadId;
|
|
331
|
+
log(`Applied native Codex session title: ${fallbackTitle}`);
|
|
332
|
+
const sourceText = cfg.nativeSessionTitlePrompt?.trim() ?? '';
|
|
333
|
+
cfg.nativeSessionTitlePrompt = undefined;
|
|
334
|
+
if (!sourceText)
|
|
335
|
+
return;
|
|
336
|
+
const adapter = createCliAdapterSync('codex', cfg.cliPathOverride);
|
|
337
|
+
const abortController = new AbortController();
|
|
338
|
+
nativeSessionTitleSyncAbortControllers.add(abortController);
|
|
339
|
+
let semanticCore;
|
|
340
|
+
try {
|
|
341
|
+
semanticCore = await generateCodexAppThreadTitle({
|
|
342
|
+
sourceText,
|
|
343
|
+
codexBin: adapter.resolvedBin,
|
|
344
|
+
env: codexNativeTitleEnv(cfg),
|
|
345
|
+
model: cfg.model,
|
|
346
|
+
timeoutMs: 30_000,
|
|
347
|
+
signal: abortController.signal,
|
|
348
|
+
detached: true,
|
|
349
|
+
registerForceClose: registerNativeTitleForceClose,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
nativeSessionTitleSyncAbortControllers.delete(abortController);
|
|
354
|
+
}
|
|
355
|
+
if (revision !== nativeSessionTitleRevision) {
|
|
356
|
+
syncLatestTitle = true;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!semanticCore) {
|
|
360
|
+
log('Native Codex semantic title generation unavailable; keeping fallback title');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const semanticTitle = buildBotmuxLarkNativeSessionTitle(semanticCore);
|
|
364
|
+
if (semanticTitle === fallbackTitle)
|
|
365
|
+
return;
|
|
366
|
+
const semanticApplied = await applyCodexNativeSessionTitle(threadId, semanticTitle, cfg, engine, 'none', revision);
|
|
367
|
+
if (!semanticApplied || revision !== nativeSessionTitleRevision) {
|
|
368
|
+
syncLatestTitle = true;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
cfg.nativeSessionTitle = semanticTitle;
|
|
372
|
+
nativeSessionTitleAppliedThreadId = threadId;
|
|
373
|
+
send({ type: 'native_session_title_generated', title: semanticTitle });
|
|
374
|
+
log(`Applied generated native Codex session title: ${semanticTitle}`);
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
if (revision !== nativeSessionTitleRevision)
|
|
378
|
+
syncLatestTitle = true;
|
|
379
|
+
else
|
|
380
|
+
log(`Native Codex session title sync failed: ${err?.message ?? err}`);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
if (nativeSessionTitleSyncInFlight === threadId)
|
|
384
|
+
nativeSessionTitleSyncInFlight = undefined;
|
|
385
|
+
if (syncLatestTitle) {
|
|
386
|
+
void syncFreshCodexNativeSessionTitle(threadId, engine);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** 在 resume 首条输入前记录 updatedAt,后续用其确认历史派生标题已完成回写。 */
|
|
391
|
+
async function captureCodexResumeTitleBaseline(threadId, engine) {
|
|
392
|
+
const cfg = lastInitConfig;
|
|
393
|
+
if (!cfg || cfg.cliId !== 'codex' || cfg.adoptMode || !cfg.nativeSessionTitle)
|
|
394
|
+
return;
|
|
395
|
+
nativeSessionTitleResumeUpdatedAt = Math.floor(Date.now() / 1000);
|
|
396
|
+
try {
|
|
397
|
+
if (engine?.activeThreadId === threadId) {
|
|
398
|
+
const metadata = await engine.readThreadMetadata(7000);
|
|
399
|
+
if (metadata.updatedAt !== undefined)
|
|
400
|
+
nativeSessionTitleResumeUpdatedAt = metadata.updatedAt;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const adapter = createCliAdapterSync('codex', cfg.cliPathOverride);
|
|
404
|
+
const env = {
|
|
405
|
+
...redactChildEnv(process.env),
|
|
406
|
+
...sanitizePerBotEnv(cfg.env),
|
|
407
|
+
};
|
|
408
|
+
env.PATH = `${join(homedir(), '.botmux', 'bin')}${delimiter}${env.PATH ?? ''}`;
|
|
409
|
+
const abortController = new AbortController();
|
|
410
|
+
nativeSessionTitleSyncAbortControllers.add(abortController);
|
|
411
|
+
try {
|
|
412
|
+
const metadata = await readCodexAppThreadMetadata({
|
|
413
|
+
threadId,
|
|
414
|
+
codexBin: adapter.resolvedBin,
|
|
415
|
+
cwd: cfg.workingDir,
|
|
416
|
+
env,
|
|
417
|
+
timeoutMs: 7000,
|
|
418
|
+
signal: abortController.signal,
|
|
419
|
+
detached: true,
|
|
420
|
+
registerForceClose: registerNativeTitleForceClose,
|
|
421
|
+
});
|
|
422
|
+
if (metadata.updatedAt !== undefined)
|
|
423
|
+
nativeSessionTitleResumeUpdatedAt = metadata.updatedAt;
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
nativeSessionTitleSyncAbortControllers.delete(abortController);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
log(`Could not capture Codex resume title baseline: ${err?.message ?? err}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** 每次 CLI spawn 后重建当前 generation 的标题同步状态。 */
|
|
434
|
+
async function prepareCodexNativeTitleGeneration(cfg, engine) {
|
|
435
|
+
if (cfg.cliId !== 'codex' || cfg.adoptMode || !cfg.nativeSessionTitle)
|
|
436
|
+
return;
|
|
437
|
+
nativeSessionTitleAppliedThreadId = undefined;
|
|
438
|
+
nativeSessionTitleResumeUpdatedAt = undefined;
|
|
439
|
+
nativeSessionTitleCurrentGenerationResume = lastSpawnEffectiveResume;
|
|
440
|
+
if (!lastSpawnEffectiveResume)
|
|
441
|
+
return;
|
|
442
|
+
const threadId = engine?.activeThreadId ?? lastSpawnEffectiveCliSessionId ?? cfg.cliSessionId;
|
|
443
|
+
if (threadId)
|
|
444
|
+
await captureCodexResumeTitleBaseline(threadId, engine);
|
|
445
|
+
}
|
|
446
|
+
/** Bridge TRAE app-server's native request_user_input request to botmux's
|
|
447
|
+
* existing Lark ask broker. The app-server owns tool execution in RPC mode, so
|
|
448
|
+
* returning this response resumes the same turn without terminal key driving. */
|
|
449
|
+
async function bridgeTraexUserInput(cfg, params) {
|
|
450
|
+
const parsed = parseTraexUserInputQuestions(params);
|
|
451
|
+
if (parsed.kind === 'unsupported') {
|
|
452
|
+
// Returning empty answers makes TraeX silently complete the tool as if no
|
|
453
|
+
// one answered, dropping the whole batch. Throw instead so the RPC engine
|
|
454
|
+
// replies with a JSON-RPC error and the failure is visible on the turn.
|
|
455
|
+
throw new Error(`requestUserInput cannot be represented as an ask card: ${parsed.reason}`);
|
|
456
|
+
}
|
|
457
|
+
const { questions } = parsed;
|
|
458
|
+
const daemon = findOnlineDaemon(cfg.larkAppId);
|
|
459
|
+
if (!daemon)
|
|
460
|
+
throw new Error(`daemon not found for larkAppId=${cfg.larkAppId}`);
|
|
461
|
+
const response = await fetchDaemonIpc(daemon.ipcPort, '/api/asks', {
|
|
462
|
+
method: 'POST',
|
|
463
|
+
headers: { 'content-type': 'application/json' },
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
sessionId: cfg.sessionId,
|
|
466
|
+
chatId: cfg.chatId,
|
|
467
|
+
larkAppId: cfg.larkAppId,
|
|
468
|
+
rootMessageId: cfg.rootMessageId || null,
|
|
469
|
+
questions: questions.map(entry => entry.question),
|
|
470
|
+
timeoutMs: 3_600_000,
|
|
471
|
+
}),
|
|
472
|
+
});
|
|
473
|
+
if (!response.ok) {
|
|
474
|
+
throw new Error(`ask broker HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`);
|
|
475
|
+
}
|
|
476
|
+
const result = await response.json();
|
|
477
|
+
// Timeout/cancel/invalidated — surface as an error rather than an empty answer
|
|
478
|
+
// that TraeX would treat as "no one answered" and silently skip.
|
|
479
|
+
if (result.kind !== 'answered') {
|
|
480
|
+
throw new Error(`ask not answered (${result.kind ?? 'unknown'})`);
|
|
481
|
+
}
|
|
482
|
+
const customText = result.comment?.trim() ?? '';
|
|
483
|
+
const answers = {};
|
|
484
|
+
questions.forEach((entry, index) => {
|
|
485
|
+
const selected = result.answers?.[index] ?? [];
|
|
486
|
+
const values = selected.length > 0 ? [...selected] : customText ? [customText] : [];
|
|
487
|
+
if (values.length > 0)
|
|
488
|
+
answers[entry.id] = { answers: values };
|
|
489
|
+
});
|
|
490
|
+
return { answers };
|
|
491
|
+
}
|
|
223
492
|
/** Stand up (or re-establish) the per-session codex app-server + botmux-owned
|
|
224
493
|
* thread and point remote{WsUrl,ThreadId} at it, so the next spawnCli launches
|
|
225
494
|
* `codex --remote <ws> resume <thread>` and input flows over JSON-RPC. Fully
|
|
@@ -246,7 +515,21 @@ async function engageCodexRpc(cfg) {
|
|
|
246
515
|
const engineEnv = { ...redactChildEnv(process.env) };
|
|
247
516
|
engineEnv.PATH = `${join(homedir(), '.botmux', 'bin')}:${engineEnv.PATH ?? ''}`;
|
|
248
517
|
engineEnv.BOTMUX_SESSION_ID = cfg.sessionId;
|
|
518
|
+
// In Codex/TraeX RPC mode the app-server, not the remote viewer TUI, runs
|
|
519
|
+
// model shell tools. Give that process the same non-secret route binding as
|
|
520
|
+
// spawnCli so `botmux ask` can post into the current Lark thread.
|
|
521
|
+
engineEnv.BOTMUX_CHAT_ID = cfg.chatId;
|
|
522
|
+
if (cfg.chatType)
|
|
523
|
+
engineEnv.BOTMUX_CHAT_TYPE = cfg.chatType;
|
|
524
|
+
else
|
|
525
|
+
delete engineEnv.BOTMUX_CHAT_TYPE;
|
|
249
526
|
engineEnv.BOTMUX_LARK_APP_ID = cfg.larkAppId;
|
|
527
|
+
engineEnv.BOTMUX_ROOT_MESSAGE_ID = cfg.rootMessageId;
|
|
528
|
+
engineEnv.BOTMUX_SESSION_SCOPE = cfg.rootMessageId?.startsWith('om_') ? 'thread' : 'chat';
|
|
529
|
+
if (cfg.ownerOpenId)
|
|
530
|
+
engineEnv.BOTMUX_OWNER_OPEN_ID = cfg.ownerOpenId;
|
|
531
|
+
else
|
|
532
|
+
delete engineEnv.BOTMUX_OWNER_OPEN_ID;
|
|
250
533
|
// The app-server owns model execution in RPC mode. Its MCP gateway child
|
|
251
534
|
// must inherit the trusted host socket just like a native CLI process does.
|
|
252
535
|
if (sessionMcpGatewayHost) {
|
|
@@ -265,6 +548,10 @@ async function engageCodexRpc(cfg) {
|
|
|
265
548
|
engine = new CodexRpcEngine({
|
|
266
549
|
cliBin, cwd: cfg.workingDir, env: engineEnv, sessionId: cfg.sessionId,
|
|
267
550
|
model: cfg.model, log: (m) => log(m),
|
|
551
|
+
appServerFeatures: cfg.cliId === 'traex' ? ['default_mode_request_user_input'] : undefined,
|
|
552
|
+
onRequestUserInput: cfg.cliId === 'traex'
|
|
553
|
+
? (params) => bridgeTraexUserInput(cfg, params)
|
|
554
|
+
: undefined,
|
|
268
555
|
onDead: () => {
|
|
269
556
|
if (codexRpcEngine === engine) {
|
|
270
557
|
log('Codex RPC app-server died; replacing the tmux session and re-engaging the thread');
|
|
@@ -318,10 +605,13 @@ async function engageCodexRpc(cfg) {
|
|
|
318
605
|
codexBridgeMarkPendingTurn(cfg.prompt, cfg.turnId);
|
|
319
606
|
outcome = first; // 'accepted' | 'ambiguous' — both stay engaged, prompt never re-queued
|
|
320
607
|
}
|
|
321
|
-
persistCliSessionId(threadId);
|
|
322
608
|
codexRpcEngine = engine;
|
|
323
609
|
remoteWsUrl = engine.wsUrl;
|
|
324
610
|
remoteThreadId = threadId;
|
|
611
|
+
persistCliSessionId(threadId);
|
|
612
|
+
if (!wantResume && cfg.prompt && outcome === 'accepted') {
|
|
613
|
+
void syncFreshCodexNativeSessionTitle(threadId, engine);
|
|
614
|
+
}
|
|
325
615
|
log(`Codex RPC input engaged (${outcome}${wantResume ? '/resume' : '/fresh'}): app-server ${engine.wsUrl} thread ${threadId}`);
|
|
326
616
|
return outcome;
|
|
327
617
|
}
|
|
@@ -672,6 +962,9 @@ let reattachIdleProbeTimer = null;
|
|
|
672
962
|
* adapter's hermesBridgeAttach reads the correct mode. */
|
|
673
963
|
let lastSpawnEffectiveResume = false;
|
|
674
964
|
let lastSpawnEffectiveCliSessionId;
|
|
965
|
+
let lastSpawnDeferInitialPrompt = false;
|
|
966
|
+
let lastSpawnQueuedInitialPrompt;
|
|
967
|
+
let lastSpawnQueuedInitialPromptLogicalContent;
|
|
675
968
|
let idleDetector = null;
|
|
676
969
|
let isTmuxMode = false;
|
|
677
970
|
/** True once a crash diagnostic tmux shell (bmx-diag-<sid>) is live. */
|
|
@@ -706,7 +999,12 @@ const authedClients = new WeakSet();
|
|
|
706
999
|
const clientPtys = new Map();
|
|
707
1000
|
/** Managed-Herdr viewers survive an in-worker /restart while backend changes. */
|
|
708
1001
|
const herdrWebBindings = new Map();
|
|
709
|
-
|
|
1002
|
+
// Standalone/test fallback. Production replaces this after init with a stable
|
|
1003
|
+
// per-session HMAC derived from the host-only dashboard secret, so an
|
|
1004
|
+
// already-issued 「操作链接」/write link survives a worker restart (a silent
|
|
1005
|
+
// daemon restart re-forks every worker — a per-process random token would 403
|
|
1006
|
+
// every previously-issued operate link).
|
|
1007
|
+
let writeToken = randomBytes(16).toString('hex');
|
|
710
1008
|
// Standalone/test fallback. Production replaces this after init with a stable
|
|
711
1009
|
// per-session HMAC derived from the host-only dashboard secret.
|
|
712
1010
|
let viewToken = randomBytes(32).toString('base64url');
|
|
@@ -721,6 +1019,15 @@ function refreshTerminalViewToken() {
|
|
|
721
1019
|
if (secret && sessionId)
|
|
722
1020
|
viewToken = deriveTerminalViewToken(secret, sessionId);
|
|
723
1021
|
}
|
|
1022
|
+
/** Re-derive the stable write (operate) token from the host-only dashboard
|
|
1023
|
+
* secret so a restarted worker mints the SAME token — keeping already-issued
|
|
1024
|
+
* 「操作链接」/write links valid across restarts. Falls back to the random
|
|
1025
|
+
* boot token when the secret is unavailable (standalone/test). */
|
|
1026
|
+
function refreshTerminalWriteToken() {
|
|
1027
|
+
const secret = loadDashboardSecret(DASHBOARD_SECRET_PATH);
|
|
1028
|
+
if (secret && sessionId)
|
|
1029
|
+
writeToken = deriveTerminalWriteToken(secret, sessionId);
|
|
1030
|
+
}
|
|
724
1031
|
/**
|
|
725
1032
|
* Resolve terminal write permission for one request. The platform-injected
|
|
726
1033
|
* `X-Botmux-Role` header is trusted only when this machine is bound to a central
|
|
@@ -757,6 +1064,10 @@ function ensureZellijAttachConfig() {
|
|
|
757
1064
|
}
|
|
758
1065
|
let sessionId = '';
|
|
759
1066
|
let lastInitConfig = null;
|
|
1067
|
+
/** Dashboard「复现命令」:session 冷启时最终交给 backend.spawn 的真实调用
|
|
1068
|
+
* (bin + argv + cwd + 关键 env)。原样保留,worker `ready` 时随消息上报给 daemon
|
|
1069
|
+
* 持久化。仅有写权限的 dashboard 视图可见。 */
|
|
1070
|
+
let capturedSpawnCommand = null;
|
|
760
1071
|
let deferredTopicOutputTail = '';
|
|
761
1072
|
const reportedDeferredTopicRoots = new Set();
|
|
762
1073
|
const CLI_DISPLAY_NAMES = { 'claude-code': 'Claude', seed: 'Seed', relay: 'Relay', aiden: 'Aiden', coco: 'CoCo', codex: 'Codex', 'codex-app': 'Codex App', cursor: 'Cursor', gemini: 'Gemini', genius: 'Genius', opencode: 'OpenCode', antigravity: 'Antigravity', mtr: 'MTR', hermes: 'Hermes', mira: 'Mira', mir: 'Mir CLI', traex: 'TRAE', pi: 'Pi', copilot: 'Copilot', 'oh-my-pi': 'Oh My Pi', kimi: 'Kimi', grok: 'Grok Build', 'kiro-cli': 'Kiro', riff: 'Riff' };
|
|
@@ -1040,6 +1351,21 @@ const pendingRawInputs = [];
|
|
|
1040
1351
|
* administrative TUI command: never type-ahead while the agent is busy, never
|
|
1041
1352
|
* open a model turn, and latest-wins if several renames arrive before idle. */
|
|
1042
1353
|
let pendingSessionRename = null;
|
|
1354
|
+
let nativeSessionTitleSyncInFlight;
|
|
1355
|
+
let nativeSessionTitleAppliedThreadId;
|
|
1356
|
+
let nativeSessionTitleRevision = 0;
|
|
1357
|
+
let nativeSessionTitleResumeUpdatedAt;
|
|
1358
|
+
let nativeSessionTitleCurrentGenerationResume = false;
|
|
1359
|
+
const nativeSessionTitleSyncAbortControllers = new Set();
|
|
1360
|
+
const nativeSessionTitleSyncForceClosers = new Set();
|
|
1361
|
+
function stopNativeSessionTitleSync() {
|
|
1362
|
+
for (const abortController of nativeSessionTitleSyncAbortControllers)
|
|
1363
|
+
abortController.abort();
|
|
1364
|
+
nativeSessionTitleSyncAbortControllers.clear();
|
|
1365
|
+
for (const forceClose of nativeSessionTitleSyncForceClosers)
|
|
1366
|
+
forceClose();
|
|
1367
|
+
nativeSessionTitleSyncForceClosers.clear();
|
|
1368
|
+
}
|
|
1043
1369
|
/** True after the rename command's Enter lands and until the TUI returns to its
|
|
1044
1370
|
* prompt. Blocks type-ahead user messages from racing into the command UI. */
|
|
1045
1371
|
let sessionRenameInFlight = false;
|
|
@@ -3701,6 +4027,7 @@ function parkCrashDiagnosticTerminal(code, signal) {
|
|
|
3701
4027
|
// both when the next message respawns the CLI.
|
|
3702
4028
|
stopScreenUpdates();
|
|
3703
4029
|
stopScreenAnalyzer();
|
|
4030
|
+
stopStuckDetector();
|
|
3704
4031
|
log(`Crash diagnostic tmux session parked at ${TmuxBackend.diagnosticSessionName(sessionId)}`);
|
|
3705
4032
|
return true;
|
|
3706
4033
|
}
|
|
@@ -3816,6 +4143,68 @@ function stopScreenAnalyzer() {
|
|
|
3816
4143
|
screenAnalyzer = null;
|
|
3817
4144
|
tuiPromptBlocking = false;
|
|
3818
4145
|
}
|
|
4146
|
+
// ─── Stuck Detector (AI-free fallback for blocked CLI states) ───────────────
|
|
4147
|
+
let stuckDetector = null;
|
|
4148
|
+
// Monotonic counter bumped on every CLI/backend (re)start within this worker.
|
|
4149
|
+
// Paired with the backend object identity, it lets the worker verify that a
|
|
4150
|
+
// stuck-warning card click still targets the same CLI instance that was stuck —
|
|
4151
|
+
// a restart within the same Node worker must invalidate outstanding cards.
|
|
4152
|
+
let cliLifetimeNonce = 0;
|
|
4153
|
+
function startStuckDetector() {
|
|
4154
|
+
const sd = config.stuckDetector;
|
|
4155
|
+
if (!sd.enabled)
|
|
4156
|
+
return;
|
|
4157
|
+
stopStuckDetector();
|
|
4158
|
+
stuckDetector = new StuckDetector(sd.timeoutMs, {
|
|
4159
|
+
isActuallyStuck: () => {
|
|
4160
|
+
// Scope gate: this PR only handles the Codex PreToolUse hook-review
|
|
4161
|
+
// screen. Other CLIs (Claude Code, Gemini, ...) must never see the
|
|
4162
|
+
// Codex-specific t/Enter/Esc card, even if their output happens to
|
|
4163
|
+
// contain the same strings.
|
|
4164
|
+
if (lastInitConfig?.cliId !== 'codex')
|
|
4165
|
+
return false;
|
|
4166
|
+
// Only warn if the CLI is not at its idle prompt AND no TUI prompt card
|
|
4167
|
+
// is already posted. A long legitimate turn (model thinking, tool calls)
|
|
4168
|
+
// must not trigger this.
|
|
4169
|
+
if (isPromptReady)
|
|
4170
|
+
return false;
|
|
4171
|
+
if (tuiPromptBlocking)
|
|
4172
|
+
return false;
|
|
4173
|
+
// Anti-false-positive: if the PTY produced output recently the CLI is
|
|
4174
|
+
// still actively working (model streaming, tool output, spinner) — not
|
|
4175
|
+
// stuck. Require quiescence before firing.
|
|
4176
|
+
const sincePty = Date.now() - lastPtyActivityAtMs;
|
|
4177
|
+
if (sincePty < 15_000)
|
|
4178
|
+
return false;
|
|
4179
|
+
// Do NOT gate on durableTurnInFlight: the original hook-review incident
|
|
4180
|
+
// ran on a non-durable (ordinary IM) turn, and a durable turn's 20s
|
|
4181
|
+
// submit-recheck failure clears durableTurnInFlight before the 45s
|
|
4182
|
+
// detector fires. The PTY-quiescence + !isPromptReady combination is
|
|
4183
|
+
// sufficient to detect a genuinely stalled turn.
|
|
4184
|
+
return true;
|
|
4185
|
+
},
|
|
4186
|
+
onStuck: (elapsedMs, matchedLabel) => {
|
|
4187
|
+
// Prefer lastAnalyzerSnapshot (the capture-pane authoritative current
|
|
4188
|
+
// screen) over the long-lived renderer, which can drift under tmux.
|
|
4189
|
+
const snapshot = lastAnalyzerSnapshot || renderer?.rawSnapshot() || '';
|
|
4190
|
+
log(`StuckDetector: turn unresolved for ${Math.round(elapsedMs / 1000)}s${matchedLabel ? ` (${matchedLabel})` : ''}`);
|
|
4191
|
+
send({
|
|
4192
|
+
type: 'stuck_warning',
|
|
4193
|
+
elapsedMs,
|
|
4194
|
+
snapshot: snapshot.slice(-3000),
|
|
4195
|
+
matchedPattern: matchedLabel,
|
|
4196
|
+
turnId: currentBotmuxTurnId,
|
|
4197
|
+
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
4198
|
+
cliLifetime: cliLifetimeNonce,
|
|
4199
|
+
});
|
|
4200
|
+
},
|
|
4201
|
+
getSnapshot: () => lastAnalyzerSnapshot || renderer?.rawSnapshot() || '',
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
function stopStuckDetector() {
|
|
4205
|
+
stuckDetector?.dispose();
|
|
4206
|
+
stuckDetector = null;
|
|
4207
|
+
}
|
|
3819
4208
|
// ─── Screenshot Capture (PNG → Feishu image_key) ────────────────────────────
|
|
3820
4209
|
const SCREENSHOT_INTERVAL_MS = 10_000;
|
|
3821
4210
|
const POST_ACTION_DELAY_MS = 1_000;
|
|
@@ -4063,24 +4452,32 @@ const KEY_TO_ANSI = {
|
|
|
4063
4452
|
* Execute an AI-provided key sequence with delays between each key.
|
|
4064
4453
|
* @param keys — key names like ["Down","Down","Space","Up","Up"]
|
|
4065
4454
|
* @param isFinal — if true, this action ends the prompt (clear blocking state)
|
|
4455
|
+
* @returns true if all keys were written successfully; false if backend is gone
|
|
4456
|
+
* or a write threw (caller must NOT send tui_keys_delivered on false).
|
|
4066
4457
|
*/
|
|
4067
4458
|
async function handleTuiKeys(keys, isFinal) {
|
|
4068
4459
|
if (!backend || keys.length === 0)
|
|
4069
|
-
return;
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4460
|
+
return false;
|
|
4461
|
+
try {
|
|
4462
|
+
if ('sendSpecialKeys' in backend) {
|
|
4463
|
+
const b = backend;
|
|
4464
|
+
// Send each key individually with 100ms delay for TUI state processing
|
|
4465
|
+
for (const key of keys) {
|
|
4466
|
+
b.sendSpecialKeys(key);
|
|
4467
|
+
await new Promise(r => setTimeout(r, 100));
|
|
4468
|
+
}
|
|
4076
4469
|
}
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4470
|
+
else {
|
|
4471
|
+
for (const key of keys) {
|
|
4472
|
+
backend.write(KEY_TO_ANSI[key] ?? key);
|
|
4473
|
+
await new Promise(r => setTimeout(r, 100));
|
|
4474
|
+
}
|
|
4082
4475
|
}
|
|
4083
4476
|
}
|
|
4477
|
+
catch (e) {
|
|
4478
|
+
logError(`handleTuiKeys write failed: ${e?.message ?? e}`);
|
|
4479
|
+
return false;
|
|
4480
|
+
}
|
|
4084
4481
|
if (isFinal) {
|
|
4085
4482
|
tuiPromptBlocking = false;
|
|
4086
4483
|
if (isPromptReady) {
|
|
@@ -4090,6 +4487,7 @@ async function handleTuiKeys(keys, isFinal) {
|
|
|
4090
4487
|
screenAnalyzer?.notifySelection('final');
|
|
4091
4488
|
}
|
|
4092
4489
|
log(`TUI keys: ${keys.join(' ')}${isFinal ? ' (final)' : ''}`);
|
|
4490
|
+
return true;
|
|
4093
4491
|
}
|
|
4094
4492
|
// 待注入的 TUI 命令队列。生命周期绑定当前 CLI 进程:killCli() 会清空它,
|
|
4095
4493
|
// 防止 restart 后残留命令重放进新 CLI——新增清理状态时记得同步那里。
|
|
@@ -4597,6 +4995,7 @@ function markPromptReady() {
|
|
|
4597
4995
|
// worker-local replay of the durable attempt.
|
|
4598
4996
|
if (!durableTurnInFlight)
|
|
4599
4997
|
inflightInputs.onTurnComplete();
|
|
4998
|
+
stuckDetector?.disarm();
|
|
4600
4999
|
maybeEmitWorkflowTranscriptOutput();
|
|
4601
5000
|
if (awaitingFirstPrompt) {
|
|
4602
5001
|
awaitingFirstPrompt = false;
|
|
@@ -4781,6 +5180,7 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
4781
5180
|
persistCliSessionId(cliSessionId);
|
|
4782
5181
|
if (codexBridgeFallbackActive())
|
|
4783
5182
|
codexBridgeNotifyCliSessionId(cliSessionId);
|
|
5183
|
+
void syncFreshCodexNativeSessionTitle(cliSessionId, codexRpcEngine);
|
|
4784
5184
|
}
|
|
4785
5185
|
log(`Deferred recheck found submit in ${transcriptLabel} — suppressing warning. preview="${preview}"`);
|
|
4786
5186
|
return;
|
|
@@ -5065,9 +5465,12 @@ async function flushPending() {
|
|
|
5065
5465
|
// auto-re-queued: a lost/late ack surfaces as a submit-failure the user can
|
|
5066
5466
|
// resend manually, and the stable clientUserMessageId lets codex dedupe a
|
|
5067
5467
|
// resend (Codex delta P1-1).
|
|
5068
|
-
if (!codexRpcEngine)
|
|
5468
|
+
if (!codexRpcEngine) {
|
|
5069
5469
|
inflightInputs.onWrite(item);
|
|
5470
|
+
stuckDetector?.arm();
|
|
5471
|
+
}
|
|
5070
5472
|
const msg = item.content;
|
|
5473
|
+
const logicalMsg = item.logicalContent ?? msg;
|
|
5071
5474
|
currentBotmuxTurnId = item.turnId;
|
|
5072
5475
|
currentBotmuxDispatchAttempt = item.dispatchAttempt;
|
|
5073
5476
|
currentVcMeetingImTurnOrigin = item.vcMeetingImTurnOrigin;
|
|
@@ -5087,14 +5490,14 @@ async function flushPending() {
|
|
|
5087
5490
|
bridgeIngest();
|
|
5088
5491
|
}
|
|
5089
5492
|
catch { /* best-effort */ }
|
|
5090
|
-
bridgeTurnId = bridgeMarkPendingTurn(
|
|
5493
|
+
bridgeTurnId = bridgeMarkPendingTurn(logicalMsg, item.turnId, item.dispatchAttempt);
|
|
5091
5494
|
}
|
|
5092
5495
|
else if (codexBridgeActive) {
|
|
5093
5496
|
// Codex mark works even before the rollout path is known: the
|
|
5094
5497
|
// queue is path-agnostic, and the late-attach below will start
|
|
5095
5498
|
// ingest from offset 0 so the user_message that lands shortly
|
|
5096
5499
|
// after still fingerprint-matches this turn.
|
|
5097
|
-
codexBridgeMarkPendingTurn(
|
|
5500
|
+
codexBridgeMarkPendingTurn(logicalMsg, item.turnId, item.dispatchAttempt);
|
|
5098
5501
|
}
|
|
5099
5502
|
if (durableWrite
|
|
5100
5503
|
&& cliAdapter.reliableTurnTerminal === true
|
|
@@ -5148,7 +5551,7 @@ async function flushPending() {
|
|
|
5148
5551
|
// do. Otherwise surface it as a submit failure so the message isn't
|
|
5149
5552
|
// silently lost.
|
|
5150
5553
|
if (backend)
|
|
5151
|
-
scheduleSubmitFailureNotify(
|
|
5554
|
+
scheduleSubmitFailureNotify(logicalMsg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq, item);
|
|
5152
5555
|
break;
|
|
5153
5556
|
}
|
|
5154
5557
|
// Persist any sessionId the adapter observed via authoritative sources
|
|
@@ -5163,11 +5566,18 @@ async function flushPending() {
|
|
|
5163
5566
|
if (codexBridgeActive)
|
|
5164
5567
|
codexBridgeNotifyCliSessionId(result.cliSessionId);
|
|
5165
5568
|
}
|
|
5569
|
+
if (lastInitConfig?.cliId === 'codex' && result?.submitted !== false) {
|
|
5570
|
+
const threadId = result?.cliSessionId
|
|
5571
|
+
?? codexRpcEngine?.activeThreadId
|
|
5572
|
+
?? lastInitConfig.cliSessionId;
|
|
5573
|
+
if (threadId)
|
|
5574
|
+
void syncFreshCodexNativeSessionTitle(threadId, codexRpcEngine);
|
|
5575
|
+
}
|
|
5166
5576
|
// `&& backend`: if the CLI exited during this write (pane gone → onExit
|
|
5167
5577
|
// nulled backend) the user already got a "CLI exited" notice; don't also
|
|
5168
5578
|
// nag that the submit wasn't confirmed.
|
|
5169
5579
|
if (result && result.submitted === false && backend) {
|
|
5170
|
-
scheduleSubmitFailureNotify(
|
|
5580
|
+
scheduleSubmitFailureNotify(logicalMsg, result.recheck, '会话 JSONL', bridgeTurnId, result.failureReason, turnSeq, item);
|
|
5171
5581
|
}
|
|
5172
5582
|
// All structured bridges now drain every pending message in one flush:
|
|
5173
5583
|
// Claude's BridgeTurnQueue handles `attachment(queued_command)` events
|
|
@@ -5188,7 +5598,7 @@ async function flushPending() {
|
|
|
5188
5598
|
}
|
|
5189
5599
|
function sendToPty(content, turnId, opts = {}) {
|
|
5190
5600
|
if (!cliAdapter)
|
|
5191
|
-
return;
|
|
5601
|
+
return false;
|
|
5192
5602
|
const next = {
|
|
5193
5603
|
content,
|
|
5194
5604
|
turnId,
|
|
@@ -5205,7 +5615,7 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5205
5615
|
if (cliRestartInProgress || !backend) {
|
|
5206
5616
|
pendingMessages.push(next);
|
|
5207
5617
|
log(`Queued message while CLI backend is restarting (${pendingMessages.length} pending)`);
|
|
5208
|
-
return;
|
|
5618
|
+
return true;
|
|
5209
5619
|
}
|
|
5210
5620
|
const supportsTypeAhead = pendingInputAllowsTypeAhead(cliAdapter.supportsTypeAhead === true, durableTurnInFlight, next);
|
|
5211
5621
|
const shouldMergeQueued = opts.dispatchAttempt === undefined && !durableTurnInFlight
|
|
@@ -5262,6 +5672,7 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5262
5672
|
log(`Queued message (${pendingMessages.length} pending): "${content.substring(0, 80)}" — ${cliName()} ${awaitingFirstPrompt ? 'still booting' : 'is busy'}`);
|
|
5263
5673
|
scheduleBusyPatternIdleProbe(`${cliName()} queued-message`);
|
|
5264
5674
|
}
|
|
5675
|
+
return true;
|
|
5265
5676
|
}
|
|
5266
5677
|
// ─── Screen Update Timer ─────────────────────────────────────────────────────
|
|
5267
5678
|
function startScreenUpdates() {
|
|
@@ -5699,6 +6110,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
5699
6110
|
});
|
|
5700
6111
|
effectiveBackendType = 'herdr';
|
|
5701
6112
|
backend = herdrBe;
|
|
6113
|
+
cliLifetimeNonce++;
|
|
5702
6114
|
// Same as tmux/zellij adopt: writeInput (grok preferSessionId via
|
|
5703
6115
|
// findGrokSessionByPid, claude pid-state) needs cliPid/cliCwd on the
|
|
5704
6116
|
// PtyHandle. spawn() overwrites cliCwd from opts.cwd — use adoptCwd when
|
|
@@ -5757,6 +6169,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
5757
6169
|
: new TmuxPipeBackend(cfg.adoptTmuxTarget, { cliPid: cfg.adoptCliPid });
|
|
5758
6170
|
effectiveBackendType = cfg.adoptZellijPaneId ? 'zellij' : 'tmux';
|
|
5759
6171
|
backend = observeBe;
|
|
6172
|
+
cliLifetimeNonce++;
|
|
5760
6173
|
// writeInput (grok concurrent prompt_history binding, claude pid-state)
|
|
5761
6174
|
// reads these fields off the PtyHandle — constructor only stores
|
|
5762
6175
|
// watchCliPid for liveness, so surface them explicitly for adopt.
|
|
@@ -5936,11 +6349,6 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
5936
6349
|
}
|
|
5937
6350
|
}
|
|
5938
6351
|
}
|
|
5939
|
-
const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, backendType: effectiveBackend, backendConfig: riffBackendConfig });
|
|
5940
|
-
isTmuxMode = selectedBackend.isTmuxMode;
|
|
5941
|
-
isPipeMode = selectedBackend.isPipeMode;
|
|
5942
|
-
isZellijMode = selectedBackend.isZellijMode;
|
|
5943
|
-
backend = selectedBackend.backend;
|
|
5944
6352
|
const adapterSessionId = cfg.resume
|
|
5945
6353
|
? (cfg.originalSessionId ?? cfg.sessionId)
|
|
5946
6354
|
: cfg.sessionId;
|
|
@@ -6076,6 +6484,31 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6076
6484
|
currentCliCredentialIsolated = appliedIsolationCapabilities.includes('credential');
|
|
6077
6485
|
const isolationRuntimeDataDir = process.env.SESSION_DATA_DIR
|
|
6078
6486
|
?? join(defaultBotmuxHome, 'data');
|
|
6487
|
+
let mcpRuntimeManifest = readSessionMcpRuntimeManifest(cfg.sessionId, config.session.dataDir);
|
|
6488
|
+
const hasMcpRuntimeEntries = !!cliAdapter.mcpGateway && !!mcpRuntimeManifest?.entries.length;
|
|
6489
|
+
const selectBackend = () => selectSessionBackend({
|
|
6490
|
+
sessionId: cfg.sessionId,
|
|
6491
|
+
backendType: effectiveBackend,
|
|
6492
|
+
backendConfig: riffBackendConfig,
|
|
6493
|
+
persistentBackendTarget: cfg.persistentBackendTarget,
|
|
6494
|
+
// Old builds could place managed agents in a user's shared Herdr session.
|
|
6495
|
+
// Preserve that recorded target for compatibility unless this incarnation
|
|
6496
|
+
// requires an isolation/MCP boundary that only a Botmux-owned session can
|
|
6497
|
+
// safely provide. Fresh tasks use distinct agents in one machine-wide host.
|
|
6498
|
+
reuseRecordedHerdrTarget: !willReadIsolate
|
|
6499
|
+
&& !willWriteSandbox
|
|
6500
|
+
&& !hasMcpRuntimeEntries,
|
|
6501
|
+
});
|
|
6502
|
+
let selectedBackend = selectBackend();
|
|
6503
|
+
isTmuxMode = selectedBackend.isTmuxMode;
|
|
6504
|
+
isPipeMode = selectedBackend.isPipeMode;
|
|
6505
|
+
isZellijMode = selectedBackend.isZellijMode;
|
|
6506
|
+
backend = selectedBackend.backend;
|
|
6507
|
+
// Bump the CLI-lifetime nonce: any stuck-warning card posted by a previous
|
|
6508
|
+
// backend instance (within this same worker) must not inject keys into the
|
|
6509
|
+
// new one. The worker echoes this nonce in stuck_warning and re-checks it on
|
|
6510
|
+
// tui_keys, alongside the backend object identity.
|
|
6511
|
+
cliLifetimeNonce++;
|
|
6079
6512
|
// Every bot — isolated OR not — gets its own BOT_HOME dir as a ready-made private-
|
|
6080
6513
|
// storage slot. An isolated sibling denies this path regardless of whether the owner
|
|
6081
6514
|
// is isolated (deny uses the full bots.json), so a non-isolated bot can drop private
|
|
@@ -6141,13 +6574,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6141
6574
|
// on EVERY restart, e.g. for a sandboxed session whose transcript lives in
|
|
6142
6575
|
// an ephemeral overlay upper that the probe can't see). Computed here (not at
|
|
6143
6576
|
// the spawn site below) so the pre-flight can short-circuit on it.
|
|
6144
|
-
|
|
6145
|
-
? TmuxBackend.sessionName(cfg.sessionId)
|
|
6146
|
-
: effectiveBackendType === 'herdr'
|
|
6147
|
-
? HerdrBackend.sessionName(cfg.sessionId)
|
|
6148
|
-
: effectiveBackendType === 'zellij'
|
|
6149
|
-
? ZellijBackend.sessionName(cfg.sessionId)
|
|
6150
|
-
: undefined;
|
|
6577
|
+
let persistentSessionName = selectedBackend.persistentSessionName;
|
|
6151
6578
|
// [read-isolation] Before we decide to reattach a persistent pane: a pane can
|
|
6152
6579
|
// survive a daemon restart still running a CLI that may NOT be isolated (e.g.
|
|
6153
6580
|
// spawned before isolation was enabled, or by an old build). Isolation is only
|
|
@@ -6158,11 +6585,10 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6158
6585
|
// policy pane survives daemon restarts and suspend→resume safely because the
|
|
6159
6586
|
// confinement remains attached to the live process.
|
|
6160
6587
|
if (appliedIsolationCapabilities.length > 0 && persistentSessionName && effectiveBackendType !== 'pty') {
|
|
6161
|
-
const
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
: HerdrBackend.hasSession(persistentSessionName);
|
|
6588
|
+
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
6589
|
+
const paneLive = persistentTarget
|
|
6590
|
+
? probePersistentBackendTarget(persistentTarget) === 'exists'
|
|
6591
|
+
: false;
|
|
6166
6592
|
if (paneLive) {
|
|
6167
6593
|
let marker = null;
|
|
6168
6594
|
marker = readRegularHostFileNoFollow(join(isolationRuntimeDataDir, 'read-isolation', `${cfg.sessionId}.boot`));
|
|
@@ -6177,7 +6603,18 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6177
6603
|
// obsolete permissions. Kill it before publishing any new capability.
|
|
6178
6604
|
log(`[read-isolation] legacy/unmarked persistent pane for ${cfg.sessionId} — killing + cold-spawning with current policy`);
|
|
6179
6605
|
try {
|
|
6180
|
-
|
|
6606
|
+
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
6607
|
+
if (persistentTarget)
|
|
6608
|
+
killPersistentBackendTarget(persistentTarget);
|
|
6609
|
+
else
|
|
6610
|
+
killPersistentSession(effectiveBackendType, persistentSessionName);
|
|
6611
|
+
selectedBackend = selectBackend();
|
|
6612
|
+
isTmuxMode = selectedBackend.isTmuxMode;
|
|
6613
|
+
isPipeMode = selectedBackend.isPipeMode;
|
|
6614
|
+
isZellijMode = selectedBackend.isZellijMode;
|
|
6615
|
+
backend = selectedBackend.backend;
|
|
6616
|
+
cliLifetimeNonce++;
|
|
6617
|
+
persistentSessionName = selectedBackend.persistentSessionName;
|
|
6181
6618
|
}
|
|
6182
6619
|
catch (e) {
|
|
6183
6620
|
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: could not kill stale persistent pane (${e.message})`);
|
|
@@ -6185,28 +6622,32 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6185
6622
|
}
|
|
6186
6623
|
}
|
|
6187
6624
|
}
|
|
6188
|
-
let
|
|
6625
|
+
let willReattachPersistent = selectedBackend.isReattach === true;
|
|
6189
6626
|
if (cliAdapter.mcpGateway && mcpRuntimeManifest?.entries.length && persistentSessionName && effectiveBackendType !== 'pty') {
|
|
6190
|
-
const
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
: HerdrBackend.hasSession(persistentSessionName);
|
|
6627
|
+
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
6628
|
+
const paneLive = persistentTarget
|
|
6629
|
+
? probePersistentBackendTarget(persistentTarget) === 'exists'
|
|
6630
|
+
: false;
|
|
6195
6631
|
if (paneLive) {
|
|
6196
6632
|
// The trusted Gateway host belongs to the worker and cannot survive a
|
|
6197
6633
|
// worker/daemon replacement. Cold-resume the CLI so its MCP client gets a
|
|
6198
6634
|
// fresh relay socket instead of reattaching to a dead connection.
|
|
6199
6635
|
log(`[mcp-gateway] persistent pane ${cfg.sessionId} has plugin MCP state — cold-resuming with a fresh host`);
|
|
6200
|
-
|
|
6636
|
+
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
6637
|
+
if (persistentTarget)
|
|
6638
|
+
killPersistentBackendTarget(persistentTarget);
|
|
6639
|
+
else
|
|
6640
|
+
killPersistentSession(effectiveBackendType, persistentSessionName);
|
|
6641
|
+
selectedBackend = selectBackend();
|
|
6642
|
+
isTmuxMode = selectedBackend.isTmuxMode;
|
|
6643
|
+
isPipeMode = selectedBackend.isPipeMode;
|
|
6644
|
+
isZellijMode = selectedBackend.isZellijMode;
|
|
6645
|
+
backend = selectedBackend.backend;
|
|
6646
|
+
cliLifetimeNonce++;
|
|
6647
|
+
persistentSessionName = selectedBackend.persistentSessionName;
|
|
6648
|
+
willReattachPersistent = selectedBackend.isReattach === true;
|
|
6201
6649
|
}
|
|
6202
6650
|
}
|
|
6203
|
-
const willReattachPersistent = persistentSessionName
|
|
6204
|
-
? effectiveBackendType === 'tmux'
|
|
6205
|
-
? TmuxBackend.hasSession(persistentSessionName)
|
|
6206
|
-
: effectiveBackendType === 'zellij'
|
|
6207
|
-
? ZellijBackend.hasSession(persistentSessionName)
|
|
6208
|
-
: HerdrBackend.hasSession(persistentSessionName)
|
|
6209
|
-
: false;
|
|
6210
6651
|
// The plugin set is stable only for the lifetime of one real CLI process.
|
|
6211
6652
|
// A warm worker reattach keeps the existing Gateway and catalog untouched;
|
|
6212
6653
|
// every fresh/resumed CLI spawn atomically refreshes both from current Bot config.
|
|
@@ -6340,6 +6781,30 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6340
6781
|
// baking it into args would drop the message that triggered the resume.
|
|
6341
6782
|
// Finally, defer adapter-declared over-limit prompts to avoid backend command
|
|
6342
6783
|
// string limits (tmux "command too long") while preserving short argv prompts.
|
|
6784
|
+
let preparedInitialPrompt;
|
|
6785
|
+
let promptArgPreparationChanged = false;
|
|
6786
|
+
let preparedDeferredInput;
|
|
6787
|
+
if (cfg.prompt) {
|
|
6788
|
+
const prepared = cliAdapter.prepareInitialPromptArg?.({
|
|
6789
|
+
initialPrompt: cfg.prompt,
|
|
6790
|
+
sessionId: effectiveAdapterSessionId,
|
|
6791
|
+
sessionDataDir: process.env.SESSION_DATA_DIR,
|
|
6792
|
+
});
|
|
6793
|
+
if (prepared?.readonlyRoots?.length) {
|
|
6794
|
+
piInitialPromptReadonlyRoots = [
|
|
6795
|
+
...new Set([...piInitialPromptReadonlyRoots, ...prepared.readonlyRoots]),
|
|
6796
|
+
];
|
|
6797
|
+
}
|
|
6798
|
+
if (prepared?.cleanupPaths?.length) {
|
|
6799
|
+
piInitialPromptCleanupPaths.push(...prepared.cleanupPaths);
|
|
6800
|
+
}
|
|
6801
|
+
if (prepared?.cleanupDirs?.length) {
|
|
6802
|
+
piInitialPromptCleanupDirs.push(...prepared.cleanupDirs);
|
|
6803
|
+
}
|
|
6804
|
+
preparedDeferredInput = prepared?.deferredInput;
|
|
6805
|
+
preparedInitialPrompt = prepared?.initialPrompt ?? cfg.prompt;
|
|
6806
|
+
promptArgPreparationChanged = preparedInitialPrompt !== cfg.prompt;
|
|
6807
|
+
}
|
|
6343
6808
|
const deferInitialPrompt = shouldDeferInitialPromptForStartup({
|
|
6344
6809
|
hasStartupCommands: !!cfg.startupCommands?.length,
|
|
6345
6810
|
adoptMode: cfg.adoptMode === true,
|
|
@@ -6349,11 +6814,25 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6349
6814
|
adoptMode: cfg.adoptMode === true,
|
|
6350
6815
|
dispatchAttempt: cfg.dispatchAttempt,
|
|
6351
6816
|
}) || (effectiveResume && cliAdapter.initialPromptArgsIgnoredOnResume === true)
|
|
6352
|
-
|| shouldDeferInitialPromptForArgLimit({
|
|
6817
|
+
|| (!promptArgPreparationChanged && shouldDeferInitialPromptForArgLimit({
|
|
6353
6818
|
passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
|
|
6354
6819
|
prompt: cfg.prompt,
|
|
6355
6820
|
maxInitialPromptArgBytes: cliAdapter.maxInitialPromptArgBytes,
|
|
6356
|
-
});
|
|
6821
|
+
}));
|
|
6822
|
+
const initialPromptDelivery = resolveInitialPromptDelivery({
|
|
6823
|
+
originalPrompt: cfg.prompt,
|
|
6824
|
+
preparedArg: preparedInitialPrompt,
|
|
6825
|
+
preparedDeferredContent: preparedDeferredInput?.content,
|
|
6826
|
+
defer: deferInitialPrompt,
|
|
6827
|
+
});
|
|
6828
|
+
preparedInitialPrompt = initialPromptDelivery.argvPrompt;
|
|
6829
|
+
lastSpawnQueuedInitialPrompt = initialPromptDelivery.queuedContent;
|
|
6830
|
+
lastSpawnQueuedInitialPromptLogicalContent = initialPromptDelivery.logicalContent;
|
|
6831
|
+
if (deferInitialPrompt && preparedDeferredInput) {
|
|
6832
|
+
piInitialPromptAdditionalArgs = [...(preparedDeferredInput.additionalArgs ?? [])];
|
|
6833
|
+
piInitialPromptEnv = { ...(preparedDeferredInput.env ?? {}) };
|
|
6834
|
+
}
|
|
6835
|
+
lastSpawnDeferInitialPrompt = deferInitialPrompt;
|
|
6357
6836
|
kiroSessionIdCaptureArmed = cfg.cliId === 'kiro-cli' && !effectiveCliSessionId && !willReattachPersistent;
|
|
6358
6837
|
kiroSessionIdCaptureBuffer = '';
|
|
6359
6838
|
// Per-bot local read isolation: assemble the Seatbelt profile context (the gate
|
|
@@ -6432,7 +6911,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6432
6911
|
resume: effectiveResume,
|
|
6433
6912
|
workingDir: cfg.workingDir,
|
|
6434
6913
|
resumeSessionId: effectiveCliSessionId,
|
|
6435
|
-
initialPrompt:
|
|
6914
|
+
initialPrompt: preparedInitialPrompt,
|
|
6436
6915
|
botName: cfg.botName,
|
|
6437
6916
|
botOpenId: cfg.botOpenId,
|
|
6438
6917
|
larkAppId: cfg.larkAppId,
|
|
@@ -6447,6 +6926,12 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6447
6926
|
remoteWsUrl,
|
|
6448
6927
|
remoteThreadId,
|
|
6449
6928
|
});
|
|
6929
|
+
// Pi's deferred long-first-prompt command is implemented by a session-scoped
|
|
6930
|
+
// extension. Keep its launch args across owned process restarts while the
|
|
6931
|
+
// queued/in-flight command may still need replay.
|
|
6932
|
+
if (piInitialPromptAdditionalArgs.length > 0) {
|
|
6933
|
+
args.unshift(...piInitialPromptAdditionalArgs);
|
|
6934
|
+
}
|
|
6450
6935
|
// Extra args from env (CLI_DISABLE_DEFAULT_ARGS is removed — adapters own their defaults)
|
|
6451
6936
|
const extra = (process.env.CLI_EXTRA_ARGS ?? '').trim();
|
|
6452
6937
|
if (extra)
|
|
@@ -6576,6 +7061,9 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6576
7061
|
// them past the server's global env.
|
|
6577
7062
|
if (cliAdapter.spawnEnv)
|
|
6578
7063
|
Object.assign(childEnv, cliAdapter.spawnEnv);
|
|
7064
|
+
if (Object.keys(piInitialPromptEnv).length > 0) {
|
|
7065
|
+
Object.assign(childEnv, piInitialPromptEnv);
|
|
7066
|
+
}
|
|
6579
7067
|
// v2 read isolation: point the CLI at its PER-BOT config dir (set AFTER spawnEnv
|
|
6580
7068
|
// so it overrides any adapter default). claude → CLAUDE_CONFIG_DIR, codex →
|
|
6581
7069
|
// CODEX_HOME. Both are in BOTMUX_INJECTED_ENV_KEYS so the tmux backend forwards
|
|
@@ -6613,6 +7101,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6613
7101
|
let spawnBin = cliAdapter.resolvedBin;
|
|
6614
7102
|
let spawnArgs = args;
|
|
6615
7103
|
let spawnCwd = cfg.workingDir;
|
|
7104
|
+
// Dashboard「复现命令」:在**任何** sandbox 包装(下方 macOS Seatbelt / Linux bwrap /
|
|
7105
|
+
// credential-only)之前,记下**基础 CLI** 的 bin/args(cliAdapter.resolvedBin +
|
|
7106
|
+
// buildArgs 产出)。独立维护、绝不从已被外层包装的 spawnBin/spawnArgs 回推。最终
|
|
7107
|
+
// 复现形态(是否套 wrapperCli)由 selectReproduceLaunch 在 spawn 时统一决策——见
|
|
7108
|
+
// reproduce-command.ts。这里只锁定"包装前的基础"这个事实。
|
|
7109
|
+
const reproduceBaseBin = spawnBin;
|
|
7110
|
+
const reproduceBaseArgs = [...spawnArgs];
|
|
6616
7111
|
// Read isolation (macOS): wrap the whole CLI process in a Seatbelt sandbox that
|
|
6617
7112
|
// denies reads of the sensitive paths (blocklist). The CLI bypasses its OWN
|
|
6618
7113
|
// sandbox (see adapter) so the outer wrapper is the sole enforcer. DARWIN ONLY —
|
|
@@ -6662,6 +7157,9 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6662
7157
|
cliId: cliAdapter.id,
|
|
6663
7158
|
resolvedBin: canonical(cliAdapter.resolvedBin),
|
|
6664
7159
|
}),
|
|
7160
|
+
// buildV2DenyPaths masks the shared pi-initial-prompts root. Re-open
|
|
7161
|
+
// only this session's private child directory for Pi's @file/extension.
|
|
7162
|
+
...piInitialPromptReadonlyRoots.map(canonical),
|
|
6665
7163
|
];
|
|
6666
7164
|
finalDenyPaths = carve.finalDenyPaths.map(canonical);
|
|
6667
7165
|
traverseDirs = carve.traverseDirs.map(canonical);
|
|
@@ -6908,6 +7406,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6908
7406
|
extraExecPaths: cliAdapter.sandboxExtraExecPaths?.(),
|
|
6909
7407
|
readonlyRoots: [
|
|
6910
7408
|
...(cfg.skillReadonlyRoots ?? []),
|
|
7409
|
+
...piInitialPromptReadonlyRoots,
|
|
6911
7410
|
...(readIsoLinuxMasks?.ownReadOnlyPaths ?? []),
|
|
6912
7411
|
],
|
|
6913
7412
|
mcpGatewaySocketPath: sessionMcpGatewayHost?.socketPath,
|
|
@@ -7002,6 +7501,19 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7002
7501
|
}
|
|
7003
7502
|
}
|
|
7004
7503
|
}
|
|
7504
|
+
// Publish the exact selected resource BEFORE spawn. This both restores host
|
|
7505
|
+
// affinity for later in-worker restarts and closes the crash window where a
|
|
7506
|
+
// shared Herdr agent could be created but the daemon still knew only bmx-*.
|
|
7507
|
+
// A failed spawn leaving an intent stamp is safe: lifecycle probes see the
|
|
7508
|
+
// missing agent and close/cold-resume instead of leaking an untracked pane.
|
|
7509
|
+
cfg.persistentBackendTarget = selectedBackend.persistentBackendTarget;
|
|
7510
|
+
if (lastInitConfig) {
|
|
7511
|
+
lastInitConfig.persistentBackendTarget = selectedBackend.persistentBackendTarget;
|
|
7512
|
+
}
|
|
7513
|
+
send({
|
|
7514
|
+
type: 'persistent_backend_target',
|
|
7515
|
+
target: selectedBackend.persistentBackendTarget,
|
|
7516
|
+
});
|
|
7005
7517
|
// Mandatory credential-only confinement is the OUTERMOST launch wrapper so
|
|
7006
7518
|
// wrapperCli and every descendant it starts inherit the boundary. Full
|
|
7007
7519
|
// Seatbelt/bwrap sessions were already wrapped above and never enter these
|
|
@@ -7139,6 +7651,32 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7139
7651
|
log(`[device-credential-isolation] wrapping ${cliAdapter.id} in credential-only bwrap `
|
|
7140
7652
|
+ `(${hideDirectories.size} authority dir(s), ${hideFiles.size} exact file(s))`);
|
|
7141
7653
|
}
|
|
7654
|
+
// Dashboard「复现命令」:算出本次冷启的**近似**可复现命令(bin + argv + cwd +
|
|
7655
|
+
// 权威注入 env),随 ready 上报、只驻 daemon 内存(含凭证,绝不落盘)。基础 CLI
|
|
7656
|
+
// bin/args 取 sandbox 包装前的快照,最终形态(含/不含 wrapperCli)由
|
|
7657
|
+
// selectReproduceLaunch 决策——绝不含 sandbox-exec/bwrap 外层。riff 返回 null。
|
|
7658
|
+
try {
|
|
7659
|
+
const reproduceLaunch = selectReproduceLaunch({
|
|
7660
|
+
baseBin: reproduceBaseBin,
|
|
7661
|
+
baseArgs: reproduceBaseArgs,
|
|
7662
|
+
wrapperCli: cfg.wrapperCli,
|
|
7663
|
+
sandboxOn,
|
|
7664
|
+
binResolver: (b) => locateOnPath(b) ?? b,
|
|
7665
|
+
ttadkModel: cfg.model,
|
|
7666
|
+
});
|
|
7667
|
+
capturedSpawnCommand = buildReproduceCommand({
|
|
7668
|
+
backendType: effectiveBackendType,
|
|
7669
|
+
bin: reproduceLaunch.bin,
|
|
7670
|
+
args: reproduceLaunch.args,
|
|
7671
|
+
cwd: spawnCwd,
|
|
7672
|
+
env: childEnv,
|
|
7673
|
+
injectEnv: perBotInjectKeys.length ? perBotInjectEnv : undefined,
|
|
7674
|
+
});
|
|
7675
|
+
}
|
|
7676
|
+
catch (err) {
|
|
7677
|
+
capturedSpawnCommand = null;
|
|
7678
|
+
log(`Failed to capture reproduce command: ${err?.message ?? err}`);
|
|
7679
|
+
}
|
|
7142
7680
|
backend.spawn(spawnBin, spawnArgs, {
|
|
7143
7681
|
cwd: spawnCwd,
|
|
7144
7682
|
cols: PTY_COLS,
|
|
@@ -7147,6 +7685,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7147
7685
|
injectEnv: perBotInjectKeys.length ? perBotInjectEnv : undefined,
|
|
7148
7686
|
launchShell: lastInitConfig?.launchShell,
|
|
7149
7687
|
});
|
|
7688
|
+
if (selectedBackend.createdHerdrSessionName) {
|
|
7689
|
+
send({
|
|
7690
|
+
type: 'user_notify',
|
|
7691
|
+
turnId: currentBotmuxTurnId,
|
|
7692
|
+
message: `已创建 Botmux 专属 Herdr 会话:\`${selectedBackend.createdHerdrSessionName}\``,
|
|
7693
|
+
});
|
|
7694
|
+
}
|
|
7150
7695
|
// Write CLI PID marker so agent-facing subcommands (`botmux send`, etc.)
|
|
7151
7696
|
// can verify they were spawned inside a botmux session by walking the
|
|
7152
7697
|
// process tree and looking for a matching pid file in this directory.
|
|
@@ -7434,44 +7979,59 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7434
7979
|
}, READY_SIGNAL_TIMEOUT_MS);
|
|
7435
7980
|
readySignalTimer.unref?.();
|
|
7436
7981
|
}
|
|
7982
|
+
// A settled signal is a turn boundary. Drain structured transcripts before
|
|
7983
|
+
// markPromptReady(): that call may synchronously flush a type-ahead turn and
|
|
7984
|
+
// advance bridge attribution. Both screen-idle and authoritative Herdr status
|
|
7985
|
+
// must preserve this ordering.
|
|
7986
|
+
const drainBridgesThenMarkReady = (evidenceSource) => {
|
|
7987
|
+
if (bridgeJsonlPath) {
|
|
7988
|
+
try {
|
|
7989
|
+
bridgeDrainAndMaybeEmit();
|
|
7990
|
+
}
|
|
7991
|
+
catch (err) {
|
|
7992
|
+
log(`Bridge emit error: ${err.message}`);
|
|
7993
|
+
}
|
|
7994
|
+
}
|
|
7995
|
+
if (codexBridgeFallbackActive()) {
|
|
7996
|
+
try {
|
|
7997
|
+
codexBridgeDrainAndMaybeEmit();
|
|
7998
|
+
}
|
|
7999
|
+
catch (err) {
|
|
8000
|
+
log(`Codex bridge emit error: ${err.message}`);
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
8003
|
+
if (evidenceSource === 'screen')
|
|
8004
|
+
markPromptReadyFromPty();
|
|
8005
|
+
else
|
|
8006
|
+
markPromptReady();
|
|
8007
|
+
};
|
|
7437
8008
|
// Set up idle detection. Riff (remote HTTP backend) has no PTY output and
|
|
7438
8009
|
// is marked ready immediately after spawn (see below), so the idle detector
|
|
7439
8010
|
// is unnecessary — and without a readyPattern it would fire on every
|
|
7440
8011
|
// quiescence, repeatedly triggering markPromptReady() and duplicate cards.
|
|
7441
8012
|
if (effectiveBackendType !== 'riff') {
|
|
7442
8013
|
idleDetector = new IdleDetector(cliAdapter);
|
|
7443
|
-
idleDetector.onIdle(
|
|
8014
|
+
idleDetector.onIdle((evidenceSource) => {
|
|
7444
8015
|
log('Prompt detected (idle)');
|
|
7445
|
-
|
|
7446
|
-
// flushPending() which can immediately fire the next queued message
|
|
7447
|
-
// (type-ahead adapters), shifting bridgeQueue's notion of "current
|
|
7448
|
-
// turn" before we've had a chance to emit the previous one.
|
|
7449
|
-
if (bridgeJsonlPath) {
|
|
7450
|
-
try {
|
|
7451
|
-
bridgeDrainAndMaybeEmit();
|
|
7452
|
-
}
|
|
7453
|
-
catch (err) {
|
|
7454
|
-
log(`Bridge emit error: ${err.message}`);
|
|
7455
|
-
}
|
|
7456
|
-
}
|
|
7457
|
-
if (codexBridgeFallbackActive()) {
|
|
7458
|
-
try {
|
|
7459
|
-
codexBridgeDrainAndMaybeEmit();
|
|
7460
|
-
}
|
|
7461
|
-
catch (err) {
|
|
7462
|
-
log(`Codex bridge emit error: ${err.message}`);
|
|
7463
|
-
}
|
|
7464
|
-
}
|
|
7465
|
-
if (evidenceSource === 'screen') {
|
|
7466
|
-
markPromptReadyFromPty();
|
|
7467
|
-
}
|
|
7468
|
-
else {
|
|
7469
|
-
markPromptReady();
|
|
7470
|
-
}
|
|
8016
|
+
drainBridgesThenMarkReady(evidenceSource);
|
|
7471
8017
|
});
|
|
7472
8018
|
}
|
|
7473
8019
|
backend.onData(onPtyData);
|
|
7474
8020
|
const observedBackend = backend;
|
|
8021
|
+
if (observedBackend instanceof HerdrBackend) {
|
|
8022
|
+
observedBackend.onAgentStatus((status) => {
|
|
8023
|
+
if (backend !== observedBackend)
|
|
8024
|
+
return;
|
|
8025
|
+
if (status === 'idle' || status === 'done') {
|
|
8026
|
+
log(`Herdr agent ${status} — draining bridges before marking prompt ready`);
|
|
8027
|
+
drainBridgesThenMarkReady('structured');
|
|
8028
|
+
}
|
|
8029
|
+
else if (status === 'working') {
|
|
8030
|
+
isPromptReady = false;
|
|
8031
|
+
idleDetector?.reset();
|
|
8032
|
+
}
|
|
8033
|
+
});
|
|
8034
|
+
}
|
|
7475
8035
|
backend.onAccessUrl?.((url) => {
|
|
7476
8036
|
send({
|
|
7477
8037
|
type: 'riff_access_url',
|
|
@@ -7659,8 +8219,11 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7659
8219
|
}
|
|
7660
8220
|
function killCli(opts = {}) {
|
|
7661
8221
|
currentCliCredentialIsolated = false;
|
|
8222
|
+
stopNativeSessionTitleSync();
|
|
7662
8223
|
stopSessionMcpGatewayHost();
|
|
7663
8224
|
stopCodexRpcEngine();
|
|
8225
|
+
if (!opts.preservePending)
|
|
8226
|
+
cleanupPiInitialPromptFiles();
|
|
7664
8227
|
destroyCrashDiagnosticTerminal('killCli');
|
|
7665
8228
|
idleDetector?.dispose();
|
|
7666
8229
|
idleDetector = null;
|
|
@@ -7680,6 +8243,7 @@ function killCli(opts = {}) {
|
|
|
7680
8243
|
readyPatternSeenDuringHold = false;
|
|
7681
8244
|
awaitingPostSessionStartPromptEvidence = false;
|
|
7682
8245
|
stopScreenAnalyzer();
|
|
8246
|
+
stopStuckDetector();
|
|
7683
8247
|
stopScreenUpdates();
|
|
7684
8248
|
backend?.kill();
|
|
7685
8249
|
backend = null;
|
|
@@ -7796,6 +8360,7 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
7796
8360
|
if (lastInitConfig) {
|
|
7797
8361
|
startScreenUpdates();
|
|
7798
8362
|
startScreenAnalyzer();
|
|
8363
|
+
startStuckDetector();
|
|
7799
8364
|
try {
|
|
7800
8365
|
const restartCfg = { ...lastInitConfig, resume: true, prompt: '', cliSessionId: rpcThreadId ?? lastInitConfig.cliSessionId };
|
|
7801
8366
|
// Re-engage RPC so the new --remote pane binds to the CURRENT app-server
|
|
@@ -7809,6 +8374,7 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
7809
8374
|
await engageCodexRpc(restartCfg);
|
|
7810
8375
|
}
|
|
7811
8376
|
await spawnCli(restartCfg, { pluginGenerationPrepared: rpcPluginGenerationPrepared });
|
|
8377
|
+
await prepareCodexNativeTitleGeneration(restartCfg, codexRpcEngine);
|
|
7812
8378
|
if (codexRpcEngine)
|
|
7813
8379
|
armRpcStartupDialogDismiss();
|
|
7814
8380
|
}
|
|
@@ -7873,8 +8439,24 @@ function startWebServer(host, preferredPort) {
|
|
|
7873
8439
|
// normal buffer until resize causes a redraw. Preserve that mode so
|
|
7874
8440
|
// scroll gestures continue to target the CLI's own transcript.
|
|
7875
8441
|
const forceRemoteScroll = effectiveBackendType === 'herdr' && cliAdapter?.altScreen === true;
|
|
8442
|
+
// The wheel burst cap throttles backends where a forwarded wheel is
|
|
8443
|
+
// EXPENSIVE or has no local terminal to drive:
|
|
8444
|
+
// • Herdr — each forwarded wheel → pane send-text + snapshot re-render.
|
|
8445
|
+
// • Riff — has NO drivable terminal; writes become remote task/follow-up
|
|
8446
|
+
// creations (handleTermAction even rejects ANSI to Riff), so an
|
|
8447
|
+
// uncapped spin would flood remote task creation.
|
|
8448
|
+
// This is a BACKEND property, independent of the adapter's declared
|
|
8449
|
+
// altScreen: an altScreen:false CLI (Claude/Codex) that enters the
|
|
8450
|
+
// alternate buffer at runtime (vim/less, or the CLI's own alt-screen) still
|
|
8451
|
+
// forwards via _fwdScroll and must inherit its backend's cap. So gate the
|
|
8452
|
+
// cap on a POSITIVE allowlist of cheap, locally-drivable terminal backends
|
|
8453
|
+
// (pty/tmux/zellij); every other backend — Herdr, Riff, and any future one
|
|
8454
|
+
// — stays safely capped by default.
|
|
8455
|
+
const localTerminalBackend = effectiveBackendType === 'pty'
|
|
8456
|
+
|| effectiveBackendType === 'tmux'
|
|
8457
|
+
|| effectiveBackendType === 'zellij';
|
|
7876
8458
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
7877
|
-
res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll));
|
|
8459
|
+
res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll, localTerminalBackend));
|
|
7878
8460
|
});
|
|
7879
8461
|
wss = new WebSocketServer({
|
|
7880
8462
|
server: httpServer,
|
|
@@ -8171,7 +8753,7 @@ function startWebServer(host, preferredPort) {
|
|
|
8171
8753
|
.then(resolve, reject);
|
|
8172
8754
|
});
|
|
8173
8755
|
}
|
|
8174
|
-
function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '', forceRemoteScroll = false) {
|
|
8756
|
+
function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '', forceRemoteScroll = false, localTerminalBackend = false) {
|
|
8175
8757
|
const label = sessionId.substring(0, 8);
|
|
8176
8758
|
return `<!DOCTYPE html>
|
|
8177
8759
|
<html>
|
|
@@ -8308,6 +8890,7 @@ if(isTouch){document.body.classList.add('touch');}
|
|
|
8308
8890
|
var hasToken=${hasWrite};
|
|
8309
8891
|
var platformReadonly=${platformReadonly};
|
|
8310
8892
|
var remoteScroll=${forceRemoteScroll};
|
|
8893
|
+
var localTerminalBackend=${localTerminalBackend};
|
|
8311
8894
|
if(!hasToken){
|
|
8312
8895
|
if(platformReadonly){var _lb=document.getElementById('login-banner');_lb.classList.add('show');}
|
|
8313
8896
|
else{var _rb=document.getElementById('readonly-banner');_rb.classList.add('show');_rb.addEventListener('click',function(){_rb.classList.remove('show')});}
|
|
@@ -8514,8 +9097,21 @@ window.addEventListener('resize',onViewportResize);
|
|
|
8514
9097
|
// A high-resolution trackpad emits dozens of wheel events for one gesture, so
|
|
8515
9098
|
// cap the whole continuous burst — not each browser event — then require an idle
|
|
8516
9099
|
// gap (or direction reversal) before loading the next history chunk.
|
|
9100
|
+
//
|
|
9101
|
+
// The burst ceiling throttles backends where a forwarded wheel is EXPENSIVE or
|
|
9102
|
+
// has no local terminal to drive: Herdr (each tick → pane send-text + snapshot
|
|
9103
|
+
// re-render) and Riff (no drivable terminal — writes become remote task/follow-up
|
|
9104
|
+
// creations, so an uncapped spin would flood remote task creation). That cost is
|
|
9105
|
+
// a BACKEND property, not the adapter's declared altScreen — an altScreen:false
|
|
9106
|
+
// CLI (Claude/Codex) that enters the alternate buffer at runtime (vim/less, the
|
|
9107
|
+
// CLI's own alt-screen) still hits this path and inherits its backend's cap. A
|
|
9108
|
+
// local PTY/tmux/zellij backend repaints its own alt-screen cheaply, so capping
|
|
9109
|
+
// it at 6 ticks is what froze a continuous wheel spin after ~2 notches — the
|
|
9110
|
+
// "not smooth / stuck" symptom. Gate the release on a POSITIVE allowlist of
|
|
9111
|
+
// cheap local terminal backends (localTerminalBackend); every other backend —
|
|
9112
|
+
// Herdr, Riff, and any future one — stays safely capped by default.
|
|
8517
9113
|
var _scrollAccum=0,_scrollBurstTicks=0,_scrollBurstDir=0,_scrollBurstT=0;
|
|
8518
|
-
var _SCROLL_STEP=33;var _SCROLL_BURST_MAX=6;var _SCROLL_BURST_IDLE_MS=250;
|
|
9114
|
+
var _SCROLL_STEP=33;var _SCROLL_BURST_MAX=localTerminalBackend?Infinity:6;var _SCROLL_BURST_IDLE_MS=250;
|
|
8519
9115
|
function _endScrollBurst(){
|
|
8520
9116
|
clearTimeout(_scrollBurstT);_scrollBurstT=0;
|
|
8521
9117
|
_scrollAccum=0;_scrollBurstTicks=0;_scrollBurstDir=0;
|
|
@@ -8971,6 +9567,10 @@ function send(msg) {
|
|
|
8971
9567
|
}
|
|
8972
9568
|
process.send?.(payload);
|
|
8973
9569
|
}
|
|
9570
|
+
function acknowledgeTurnInputCommitted(turnId) {
|
|
9571
|
+
if (turnId)
|
|
9572
|
+
send({ type: 'turn_input_committed', turnId });
|
|
9573
|
+
}
|
|
8974
9574
|
function publishLocalProcessAttestation(cliPid) {
|
|
8975
9575
|
const cliProcStart = cliPid ? readProcessStartIdentity(cliPid) : undefined;
|
|
8976
9576
|
send({
|
|
@@ -9028,11 +9628,13 @@ process.on('message', async (raw) => {
|
|
|
9028
9628
|
const msg = raw;
|
|
9029
9629
|
switch (msg.type) {
|
|
9030
9630
|
case 'init': {
|
|
9631
|
+
const initStartedAtMs = Date.now();
|
|
9031
9632
|
if (lastInitConfig)
|
|
9032
9633
|
return; // already initialized
|
|
9033
9634
|
lastInitConfig = msg;
|
|
9034
9635
|
sessionId = msg.sessionId;
|
|
9035
9636
|
refreshTerminalViewToken();
|
|
9637
|
+
refreshTerminalWriteToken();
|
|
9036
9638
|
if (msg.ownerOpenId)
|
|
9037
9639
|
process.env.__OWNER_OPEN_ID = msg.ownerOpenId;
|
|
9038
9640
|
// Pin this worker's i18n locale early so every t() call below resolves
|
|
@@ -9076,6 +9678,7 @@ process.on('message', async (raw) => {
|
|
|
9076
9678
|
port = await startWebServer(config.web.workerHost, msg.webPort);
|
|
9077
9679
|
startScreenUpdates();
|
|
9078
9680
|
startScreenAnalyzer();
|
|
9681
|
+
startStuckDetector();
|
|
9079
9682
|
}
|
|
9080
9683
|
else {
|
|
9081
9684
|
// Workflow attempts still expose a read-only web terminal so the
|
|
@@ -9119,6 +9722,7 @@ process.on('message', async (raw) => {
|
|
|
9119
9722
|
throw new Error('codex RPC resume: could not replace stale --remote pane; aborting init');
|
|
9120
9723
|
}
|
|
9121
9724
|
await spawnCli(msg, { pluginGenerationPrepared: rpcPluginGenerationPrepared });
|
|
9725
|
+
await prepareCodexNativeTitleGeneration(msg, codexRpcEngine);
|
|
9122
9726
|
if (codexRpcEngine)
|
|
9123
9727
|
armRpcStartupDialogDismiss(); // boundary #4: keep the --remote pane from freezing on a startup dialog
|
|
9124
9728
|
// Queue the initial prompt — flushed when CLI shows idle.
|
|
@@ -9133,20 +9737,7 @@ process.on('message', async (raw) => {
|
|
|
9133
9737
|
// Tier-1/Tier-2 fresh demotion, which clears the flag). Adopt spawns
|
|
9134
9738
|
// return from spawnCli before that write — exclude them explicitly so
|
|
9135
9739
|
// the stale module-level value can't leak in.
|
|
9136
|
-
const deferInitialPrompt =
|
|
9137
|
-
hasStartupCommands: !!msg.startupCommands?.length,
|
|
9138
|
-
adoptMode: msg.adoptMode === true,
|
|
9139
|
-
passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
|
|
9140
|
-
}) || shouldDeferArgsBakedDurablePrompt({
|
|
9141
|
-
passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
|
|
9142
|
-
adoptMode: msg.adoptMode === true,
|
|
9143
|
-
dispatchAttempt: msg.dispatchAttempt,
|
|
9144
|
-
}) || (msg.adoptMode !== true && lastSpawnEffectiveResume && cliAdapter?.initialPromptArgsIgnoredOnResume === true)
|
|
9145
|
-
|| shouldDeferInitialPromptForArgLimit({
|
|
9146
|
-
passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
|
|
9147
|
-
prompt: msg.prompt,
|
|
9148
|
-
maxInitialPromptArgBytes: cliAdapter?.maxInitialPromptArgBytes,
|
|
9149
|
-
});
|
|
9740
|
+
const deferInitialPrompt = lastSpawnDeferInitialPrompt;
|
|
9150
9741
|
if (msg.prompt && cliAdapter?.passesInitialPromptViaArgs && !deferInitialPrompt && codexBridgeFallbackActive()) {
|
|
9151
9742
|
// Args-baked first prompts (notably Pi) never pass through the normal
|
|
9152
9743
|
// 'message' IPC path, so the structured bridge would otherwise see the
|
|
@@ -9163,6 +9754,7 @@ process.on('message', async (raw) => {
|
|
|
9163
9754
|
// execution — exactly-once, Codex P1-1). Ambiguous never reaches here.
|
|
9164
9755
|
// - RPC RESUME: queuePrompt=true → queue for post-ready flush (bridge
|
|
9165
9756
|
// marked at flush time, P0-1).
|
|
9757
|
+
let initialInputCommitted = false;
|
|
9166
9758
|
if (shouldQueueInitialPrompt({
|
|
9167
9759
|
hasPrompt: !!msg.prompt,
|
|
9168
9760
|
rpcEngineActive: !!codexRpcEngine,
|
|
@@ -9171,19 +9763,31 @@ process.on('message', async (raw) => {
|
|
|
9171
9763
|
deferInitialPrompt,
|
|
9172
9764
|
})) {
|
|
9173
9765
|
pendingMessages.push({
|
|
9174
|
-
content: msg.prompt,
|
|
9766
|
+
content: lastSpawnQueuedInitialPrompt ?? msg.prompt,
|
|
9767
|
+
...(lastSpawnQueuedInitialPromptLogicalContent
|
|
9768
|
+
? { logicalContent: lastSpawnQueuedInitialPromptLogicalContent }
|
|
9769
|
+
: {}),
|
|
9175
9770
|
turnId: msg.turnId,
|
|
9176
9771
|
dispatchAttempt: msg.dispatchAttempt,
|
|
9177
9772
|
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
9178
9773
|
codexAppInput: msg.promptCodexAppInput,
|
|
9179
9774
|
});
|
|
9775
|
+
initialInputCommitted = true;
|
|
9180
9776
|
}
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
if (
|
|
9777
|
+
else if (msg.prompt) {
|
|
9778
|
+
// A successful spawn with a non-queued prompt means the adapter baked
|
|
9779
|
+
// it into argv or the RPC engine already accepted it.
|
|
9780
|
+
initialInputCommitted = true;
|
|
9781
|
+
}
|
|
9782
|
+
if (initialInputCommitted)
|
|
9783
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9784
|
+
// A backend may become prompt-ready before spawnCli() returns. The
|
|
9785
|
+
// initial prompt is queued only afterwards, so the earlier
|
|
9786
|
+
// markPromptReady() necessarily flushed an empty queue. This is normal
|
|
9787
|
+
// for riff (ready immediately) and can also happen when Herdr reports a
|
|
9788
|
+
// fast-starting TUI as idle during spawn. Flush again after enqueueing;
|
|
9789
|
+
// the ready flag keeps booting/busy backends gated.
|
|
9790
|
+
if (isPromptReady && pendingMessages.length > 0) {
|
|
9187
9791
|
flushPending();
|
|
9188
9792
|
}
|
|
9189
9793
|
send({
|
|
@@ -9191,6 +9795,11 @@ process.on('message', async (raw) => {
|
|
|
9191
9795
|
port,
|
|
9192
9796
|
token: writeToken,
|
|
9193
9797
|
viewToken,
|
|
9798
|
+
...(capturedSpawnCommand ? { spawnCommand: capturedSpawnCommand } : {}),
|
|
9799
|
+
// A fast initial turn can complete via `botmux send` before Herdr
|
|
9800
|
+
// reports idle and this ready IPC is emitted. Tell the daemon not to
|
|
9801
|
+
// post a stale Starting card after the final reply is already visible.
|
|
9802
|
+
replyAlreadySent: readSendMarkers().some(marker => marker.sentAtMs >= initStartedAtMs),
|
|
9194
9803
|
turnId: currentBotmuxTurnId,
|
|
9195
9804
|
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
9196
9805
|
});
|
|
@@ -9205,6 +9814,21 @@ process.on('message', async (raw) => {
|
|
|
9205
9814
|
// Mark new turn baseline so the streaming card only shows this turn's content
|
|
9206
9815
|
renderer?.markNewTurn();
|
|
9207
9816
|
const turnSeq = usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
|
|
9817
|
+
if (msg.nativeSessionTitlePrompt
|
|
9818
|
+
&& msg.nativeSessionTitle
|
|
9819
|
+
&& lastInitConfig?.cliId === 'codex'
|
|
9820
|
+
&& !lastInitConfig.adoptMode) {
|
|
9821
|
+
nativeSessionTitleRevision += 1;
|
|
9822
|
+
nativeSessionTitleAppliedThreadId = undefined;
|
|
9823
|
+
lastInitConfig.nativeSessionTitle = msg.nativeSessionTitle;
|
|
9824
|
+
lastInitConfig.nativeSessionTitlePrompt = msg.nativeSessionTitlePrompt;
|
|
9825
|
+
stopNativeSessionTitleSync();
|
|
9826
|
+
const threadId = codexRpcEngine?.activeThreadId
|
|
9827
|
+
?? lastSpawnEffectiveCliSessionId
|
|
9828
|
+
?? lastInitConfig.cliSessionId;
|
|
9829
|
+
if (threadId)
|
|
9830
|
+
void syncFreshCodexNativeSessionTitle(threadId, codexRpcEngine);
|
|
9831
|
+
}
|
|
9208
9832
|
// Cancel any active tmux copy-mode scroll so user input reaches the CLI.
|
|
9209
9833
|
if (tmuxScrolledHalfPages > 0)
|
|
9210
9834
|
exitTmuxScrollMode();
|
|
@@ -9222,12 +9846,16 @@ process.on('message', async (raw) => {
|
|
|
9222
9846
|
log('Message received after crash-loop stop; retrying CLI start');
|
|
9223
9847
|
destroyCrashDiagnosticTerminal('retry after message');
|
|
9224
9848
|
stopScreenAnalyzer();
|
|
9849
|
+
stopStuckDetector();
|
|
9225
9850
|
stopScreenUpdates();
|
|
9226
9851
|
awaitingFirstPrompt = true;
|
|
9227
9852
|
startScreenUpdates();
|
|
9228
9853
|
startScreenAnalyzer();
|
|
9854
|
+
startStuckDetector();
|
|
9229
9855
|
try {
|
|
9230
|
-
|
|
9856
|
+
const restartCfg = { ...lastInitConfig, resume: true, prompt: '' };
|
|
9857
|
+
await spawnCli(restartCfg);
|
|
9858
|
+
await prepareCodexNativeTitleGeneration(restartCfg, codexRpcEngine);
|
|
9231
9859
|
}
|
|
9232
9860
|
catch (err) {
|
|
9233
9861
|
// Pass the message's own attempt (not the stale currentBotmux* from a
|
|
@@ -9309,6 +9937,9 @@ process.on('message', async (raw) => {
|
|
|
9309
9937
|
if (result && result.submitted === false) {
|
|
9310
9938
|
scheduleSubmitFailureNotify(content, result.recheck, 'submit history', undefined, result.failureReason, turnSeq);
|
|
9311
9939
|
}
|
|
9940
|
+
else {
|
|
9941
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9942
|
+
}
|
|
9312
9943
|
}
|
|
9313
9944
|
catch (err) {
|
|
9314
9945
|
log(`Adopt writeInput error (${lastInitConfig?.cliId}): ${err.message}`);
|
|
@@ -9326,9 +9957,11 @@ process.on('message', async (raw) => {
|
|
|
9326
9957
|
// command (raw_input) fix.
|
|
9327
9958
|
await new Promise(r => setTimeout(r, 200));
|
|
9328
9959
|
backend.sendSpecialKeys('Enter');
|
|
9960
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9329
9961
|
}
|
|
9330
9962
|
else {
|
|
9331
9963
|
backend.write(content + '\r');
|
|
9964
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9332
9965
|
}
|
|
9333
9966
|
isPromptReady = false;
|
|
9334
9967
|
idleDetector?.reset();
|
|
@@ -9340,11 +9973,13 @@ process.on('message', async (raw) => {
|
|
|
9340
9973
|
// arrival. Marking now would race with a still-running previous
|
|
9341
9974
|
// turn whose `botmux send` could sneak its sentAtMs past this
|
|
9342
9975
|
// turn's markTimeMs and falsely suppress its fallback.
|
|
9343
|
-
sendToPty(content, msg.turnId, {
|
|
9976
|
+
const inputCommitted = sendToPty(content, msg.turnId, {
|
|
9344
9977
|
codexAppInput,
|
|
9345
9978
|
dispatchAttempt: msg.dispatchAttempt,
|
|
9346
9979
|
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
9347
9980
|
});
|
|
9981
|
+
if (inputCommitted)
|
|
9982
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9348
9983
|
}
|
|
9349
9984
|
break;
|
|
9350
9985
|
}
|
|
@@ -9372,6 +10007,13 @@ process.on('message', async (raw) => {
|
|
|
9372
10007
|
// IPC handlers are concurrent with async init, so queue first even when
|
|
9373
10008
|
// the adapter/backend has not finished initializing. flushPending will
|
|
9374
10009
|
// capability-check and deliver the latest title once a real prompt is idle.
|
|
10010
|
+
nativeSessionTitleRevision += 1;
|
|
10011
|
+
nativeSessionTitleAppliedThreadId = undefined;
|
|
10012
|
+
if (lastInitConfig) {
|
|
10013
|
+
lastInitConfig.nativeSessionTitle = msg.title;
|
|
10014
|
+
lastInitConfig.nativeSessionTitlePrompt = undefined;
|
|
10015
|
+
}
|
|
10016
|
+
stopNativeSessionTitleSync();
|
|
9375
10017
|
pendingSessionRename = msg.title;
|
|
9376
10018
|
log(`Queued native session rename: ${msg.title}`);
|
|
9377
10019
|
void flushPending();
|
|
@@ -9468,7 +10110,49 @@ process.on('message', async (raw) => {
|
|
|
9468
10110
|
break;
|
|
9469
10111
|
}
|
|
9470
10112
|
case 'tui_keys': {
|
|
9471
|
-
|
|
10113
|
+
// Stale-card guard: if this key press came from a stuck-warning card,
|
|
10114
|
+
// delegate to processStuckWarningTuiKeys which does a FRESH capture (not
|
|
10115
|
+
// the 2s-cached lastAnalyzerSnapshot) and re-verifies the current screen
|
|
10116
|
+
// still matches the page type the card was built for. Fail-closed: any
|
|
10117
|
+
// mismatch (lifetime, capture, backend, page type, write failure) sends
|
|
10118
|
+
// stuck_warning_expired and drops the keys. ScreenAnalyzer TUI cards
|
|
10119
|
+
// (no stuckNonce) bypass the guard and write keys directly.
|
|
10120
|
+
let wroteKeys = false;
|
|
10121
|
+
if (msg.stuckNonce !== undefined && msg.stuckPageType) {
|
|
10122
|
+
const result = await processStuckWarningTuiKeys({
|
|
10123
|
+
stuckNonce: msg.stuckNonce,
|
|
10124
|
+
stuckPageType: msg.stuckPageType,
|
|
10125
|
+
stuckCliLifetime: msg.stuckCliLifetime,
|
|
10126
|
+
keys: msg.keys,
|
|
10127
|
+
isFinal: msg.isFinal,
|
|
10128
|
+
}, {
|
|
10129
|
+
getBackend: () => backend,
|
|
10130
|
+
getCurrentLifetime: () => cliLifetimeNonce,
|
|
10131
|
+
renderCols,
|
|
10132
|
+
renderRows,
|
|
10133
|
+
turnId: currentBotmuxTurnId,
|
|
10134
|
+
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
10135
|
+
capture: snapshotToText,
|
|
10136
|
+
match: matchHookReviewScreen,
|
|
10137
|
+
writeKeys: handleTuiKeys,
|
|
10138
|
+
sendExpired: (nonce, turnId, dispatchAttempt) => send({ type: 'stuck_warning_expired', nonce, turnId, dispatchAttempt }),
|
|
10139
|
+
sendDelivered: (nonce, turnId, dispatchAttempt) => send({ type: 'tui_keys_delivered', nonce, turnId, dispatchAttempt }),
|
|
10140
|
+
log,
|
|
10141
|
+
});
|
|
10142
|
+
wroteKeys = result.wroteKeys;
|
|
10143
|
+
}
|
|
10144
|
+
else {
|
|
10145
|
+
await handleTuiKeys(msg.keys, msg.isFinal);
|
|
10146
|
+
wroteKeys = true;
|
|
10147
|
+
}
|
|
10148
|
+
// Re-arm the stuck detector ONLY when the card-handler explicitly flags
|
|
10149
|
+
// this as a stuck-warning card's Enter action (advances to the next
|
|
10150
|
+
// review layer) AND keys were actually written. An expired click (CLI
|
|
10151
|
+
// recovered, page changed) must NOT re-arm — the detector should stay
|
|
10152
|
+
// disarmed until the next real stall. t/Esc and all ScreenAnalyzer cards
|
|
10153
|
+
// never set this flag.
|
|
10154
|
+
if (shouldRearmStuckDetector(!!msg.rearmStuckDetector, wroteKeys))
|
|
10155
|
+
stuckDetector?.arm();
|
|
9472
10156
|
break;
|
|
9473
10157
|
}
|
|
9474
10158
|
case 'inject_command': {
|
|
@@ -9633,6 +10317,8 @@ process.on('message', async (raw) => {
|
|
|
9633
10317
|
});
|
|
9634
10318
|
// ─── Cleanup ─────────────────────────────────────────────────────────────────
|
|
9635
10319
|
function cleanup() {
|
|
10320
|
+
stopNativeSessionTitleSync();
|
|
10321
|
+
cleanupPiInitialPromptFiles();
|
|
9636
10322
|
stopSessionMcpGatewayHost();
|
|
9637
10323
|
if (tmuxRestartTimer) {
|
|
9638
10324
|
clearTimeout(tmuxRestartTimer);
|
|
@@ -9736,7 +10422,11 @@ function teardownSandboxBestEffort() {
|
|
|
9736
10422
|
// and process.exit(1), killing a live session over a dropped log write. Install
|
|
9737
10423
|
// the guard before any further stdout writes (log() writes to process.stdout).
|
|
9738
10424
|
installStdioEpipeGuard();
|
|
9739
|
-
process.on('exit', () => {
|
|
10425
|
+
process.on('exit', () => {
|
|
10426
|
+
stopNativeSessionTitleSync();
|
|
10427
|
+
teardownSandboxBestEffort();
|
|
10428
|
+
stopCodexRpcEngine();
|
|
10429
|
+
});
|
|
9740
10430
|
process.on('uncaughtException', (err) => {
|
|
9741
10431
|
// A broken pipe on stdout/stderr (or any socket) must not tear down a live
|
|
9742
10432
|
// session — the stdio guard handles those it can; this is the backstop.
|