botmux 3.4.3 → 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.d.ts.map +1 -1
- package/dist/adapters/cli/pi-initial-prompt.js +5 -1
- package/dist/adapters/cli/pi-initial-prompt.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 +3 -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/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/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/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 +705 -93
- 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
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
import { randomBytes } from 'node:crypto';
|
|
16
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';
|
|
@@ -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';
|
|
@@ -250,6 +257,238 @@ async function codexRolloutProbe(cliId, threadId, promptText, timeoutMs) {
|
|
|
250
257
|
} while (Date.now() < deadline);
|
|
251
258
|
return false;
|
|
252
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
|
+
}
|
|
253
492
|
/** Stand up (or re-establish) the per-session codex app-server + botmux-owned
|
|
254
493
|
* thread and point remote{WsUrl,ThreadId} at it, so the next spawnCli launches
|
|
255
494
|
* `codex --remote <ws> resume <thread>` and input flows over JSON-RPC. Fully
|
|
@@ -276,7 +515,21 @@ async function engageCodexRpc(cfg) {
|
|
|
276
515
|
const engineEnv = { ...redactChildEnv(process.env) };
|
|
277
516
|
engineEnv.PATH = `${join(homedir(), '.botmux', 'bin')}:${engineEnv.PATH ?? ''}`;
|
|
278
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;
|
|
279
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;
|
|
280
533
|
// The app-server owns model execution in RPC mode. Its MCP gateway child
|
|
281
534
|
// must inherit the trusted host socket just like a native CLI process does.
|
|
282
535
|
if (sessionMcpGatewayHost) {
|
|
@@ -295,6 +548,10 @@ async function engageCodexRpc(cfg) {
|
|
|
295
548
|
engine = new CodexRpcEngine({
|
|
296
549
|
cliBin, cwd: cfg.workingDir, env: engineEnv, sessionId: cfg.sessionId,
|
|
297
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,
|
|
298
555
|
onDead: () => {
|
|
299
556
|
if (codexRpcEngine === engine) {
|
|
300
557
|
log('Codex RPC app-server died; replacing the tmux session and re-engaging the thread');
|
|
@@ -348,10 +605,13 @@ async function engageCodexRpc(cfg) {
|
|
|
348
605
|
codexBridgeMarkPendingTurn(cfg.prompt, cfg.turnId);
|
|
349
606
|
outcome = first; // 'accepted' | 'ambiguous' — both stay engaged, prompt never re-queued
|
|
350
607
|
}
|
|
351
|
-
persistCliSessionId(threadId);
|
|
352
608
|
codexRpcEngine = engine;
|
|
353
609
|
remoteWsUrl = engine.wsUrl;
|
|
354
610
|
remoteThreadId = threadId;
|
|
611
|
+
persistCliSessionId(threadId);
|
|
612
|
+
if (!wantResume && cfg.prompt && outcome === 'accepted') {
|
|
613
|
+
void syncFreshCodexNativeSessionTitle(threadId, engine);
|
|
614
|
+
}
|
|
355
615
|
log(`Codex RPC input engaged (${outcome}${wantResume ? '/resume' : '/fresh'}): app-server ${engine.wsUrl} thread ${threadId}`);
|
|
356
616
|
return outcome;
|
|
357
617
|
}
|
|
@@ -739,7 +999,12 @@ const authedClients = new WeakSet();
|
|
|
739
999
|
const clientPtys = new Map();
|
|
740
1000
|
/** Managed-Herdr viewers survive an in-worker /restart while backend changes. */
|
|
741
1001
|
const herdrWebBindings = new Map();
|
|
742
|
-
|
|
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');
|
|
743
1008
|
// Standalone/test fallback. Production replaces this after init with a stable
|
|
744
1009
|
// per-session HMAC derived from the host-only dashboard secret.
|
|
745
1010
|
let viewToken = randomBytes(32).toString('base64url');
|
|
@@ -754,6 +1019,15 @@ function refreshTerminalViewToken() {
|
|
|
754
1019
|
if (secret && sessionId)
|
|
755
1020
|
viewToken = deriveTerminalViewToken(secret, sessionId);
|
|
756
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
|
+
}
|
|
757
1031
|
/**
|
|
758
1032
|
* Resolve terminal write permission for one request. The platform-injected
|
|
759
1033
|
* `X-Botmux-Role` header is trusted only when this machine is bound to a central
|
|
@@ -790,6 +1064,10 @@ function ensureZellijAttachConfig() {
|
|
|
790
1064
|
}
|
|
791
1065
|
let sessionId = '';
|
|
792
1066
|
let lastInitConfig = null;
|
|
1067
|
+
/** Dashboard「复现命令」:session 冷启时最终交给 backend.spawn 的真实调用
|
|
1068
|
+
* (bin + argv + cwd + 关键 env)。原样保留,worker `ready` 时随消息上报给 daemon
|
|
1069
|
+
* 持久化。仅有写权限的 dashboard 视图可见。 */
|
|
1070
|
+
let capturedSpawnCommand = null;
|
|
793
1071
|
let deferredTopicOutputTail = '';
|
|
794
1072
|
const reportedDeferredTopicRoots = new Set();
|
|
795
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' };
|
|
@@ -1073,6 +1351,21 @@ const pendingRawInputs = [];
|
|
|
1073
1351
|
* administrative TUI command: never type-ahead while the agent is busy, never
|
|
1074
1352
|
* open a model turn, and latest-wins if several renames arrive before idle. */
|
|
1075
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
|
+
}
|
|
1076
1369
|
/** True after the rename command's Enter lands and until the TUI returns to its
|
|
1077
1370
|
* prompt. Blocks type-ahead user messages from racing into the command UI. */
|
|
1078
1371
|
let sessionRenameInFlight = false;
|
|
@@ -3734,6 +4027,7 @@ function parkCrashDiagnosticTerminal(code, signal) {
|
|
|
3734
4027
|
// both when the next message respawns the CLI.
|
|
3735
4028
|
stopScreenUpdates();
|
|
3736
4029
|
stopScreenAnalyzer();
|
|
4030
|
+
stopStuckDetector();
|
|
3737
4031
|
log(`Crash diagnostic tmux session parked at ${TmuxBackend.diagnosticSessionName(sessionId)}`);
|
|
3738
4032
|
return true;
|
|
3739
4033
|
}
|
|
@@ -3849,6 +4143,68 @@ function stopScreenAnalyzer() {
|
|
|
3849
4143
|
screenAnalyzer = null;
|
|
3850
4144
|
tuiPromptBlocking = false;
|
|
3851
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
|
+
}
|
|
3852
4208
|
// ─── Screenshot Capture (PNG → Feishu image_key) ────────────────────────────
|
|
3853
4209
|
const SCREENSHOT_INTERVAL_MS = 10_000;
|
|
3854
4210
|
const POST_ACTION_DELAY_MS = 1_000;
|
|
@@ -4096,24 +4452,32 @@ const KEY_TO_ANSI = {
|
|
|
4096
4452
|
* Execute an AI-provided key sequence with delays between each key.
|
|
4097
4453
|
* @param keys — key names like ["Down","Down","Space","Up","Up"]
|
|
4098
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).
|
|
4099
4457
|
*/
|
|
4100
4458
|
async function handleTuiKeys(keys, isFinal) {
|
|
4101
4459
|
if (!backend || keys.length === 0)
|
|
4102
|
-
return;
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
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
|
+
}
|
|
4109
4469
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
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
|
+
}
|
|
4115
4475
|
}
|
|
4116
4476
|
}
|
|
4477
|
+
catch (e) {
|
|
4478
|
+
logError(`handleTuiKeys write failed: ${e?.message ?? e}`);
|
|
4479
|
+
return false;
|
|
4480
|
+
}
|
|
4117
4481
|
if (isFinal) {
|
|
4118
4482
|
tuiPromptBlocking = false;
|
|
4119
4483
|
if (isPromptReady) {
|
|
@@ -4123,6 +4487,7 @@ async function handleTuiKeys(keys, isFinal) {
|
|
|
4123
4487
|
screenAnalyzer?.notifySelection('final');
|
|
4124
4488
|
}
|
|
4125
4489
|
log(`TUI keys: ${keys.join(' ')}${isFinal ? ' (final)' : ''}`);
|
|
4490
|
+
return true;
|
|
4126
4491
|
}
|
|
4127
4492
|
// 待注入的 TUI 命令队列。生命周期绑定当前 CLI 进程:killCli() 会清空它,
|
|
4128
4493
|
// 防止 restart 后残留命令重放进新 CLI——新增清理状态时记得同步那里。
|
|
@@ -4630,6 +4995,7 @@ function markPromptReady() {
|
|
|
4630
4995
|
// worker-local replay of the durable attempt.
|
|
4631
4996
|
if (!durableTurnInFlight)
|
|
4632
4997
|
inflightInputs.onTurnComplete();
|
|
4998
|
+
stuckDetector?.disarm();
|
|
4633
4999
|
maybeEmitWorkflowTranscriptOutput();
|
|
4634
5000
|
if (awaitingFirstPrompt) {
|
|
4635
5001
|
awaitingFirstPrompt = false;
|
|
@@ -4814,6 +5180,7 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
4814
5180
|
persistCliSessionId(cliSessionId);
|
|
4815
5181
|
if (codexBridgeFallbackActive())
|
|
4816
5182
|
codexBridgeNotifyCliSessionId(cliSessionId);
|
|
5183
|
+
void syncFreshCodexNativeSessionTitle(cliSessionId, codexRpcEngine);
|
|
4817
5184
|
}
|
|
4818
5185
|
log(`Deferred recheck found submit in ${transcriptLabel} — suppressing warning. preview="${preview}"`);
|
|
4819
5186
|
return;
|
|
@@ -5098,8 +5465,10 @@ async function flushPending() {
|
|
|
5098
5465
|
// auto-re-queued: a lost/late ack surfaces as a submit-failure the user can
|
|
5099
5466
|
// resend manually, and the stable clientUserMessageId lets codex dedupe a
|
|
5100
5467
|
// resend (Codex delta P1-1).
|
|
5101
|
-
if (!codexRpcEngine)
|
|
5468
|
+
if (!codexRpcEngine) {
|
|
5102
5469
|
inflightInputs.onWrite(item);
|
|
5470
|
+
stuckDetector?.arm();
|
|
5471
|
+
}
|
|
5103
5472
|
const msg = item.content;
|
|
5104
5473
|
const logicalMsg = item.logicalContent ?? msg;
|
|
5105
5474
|
currentBotmuxTurnId = item.turnId;
|
|
@@ -5197,6 +5566,13 @@ async function flushPending() {
|
|
|
5197
5566
|
if (codexBridgeActive)
|
|
5198
5567
|
codexBridgeNotifyCliSessionId(result.cliSessionId);
|
|
5199
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
|
+
}
|
|
5200
5576
|
// `&& backend`: if the CLI exited during this write (pane gone → onExit
|
|
5201
5577
|
// nulled backend) the user already got a "CLI exited" notice; don't also
|
|
5202
5578
|
// nag that the submit wasn't confirmed.
|
|
@@ -5222,7 +5598,7 @@ async function flushPending() {
|
|
|
5222
5598
|
}
|
|
5223
5599
|
function sendToPty(content, turnId, opts = {}) {
|
|
5224
5600
|
if (!cliAdapter)
|
|
5225
|
-
return;
|
|
5601
|
+
return false;
|
|
5226
5602
|
const next = {
|
|
5227
5603
|
content,
|
|
5228
5604
|
turnId,
|
|
@@ -5239,7 +5615,7 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5239
5615
|
if (cliRestartInProgress || !backend) {
|
|
5240
5616
|
pendingMessages.push(next);
|
|
5241
5617
|
log(`Queued message while CLI backend is restarting (${pendingMessages.length} pending)`);
|
|
5242
|
-
return;
|
|
5618
|
+
return true;
|
|
5243
5619
|
}
|
|
5244
5620
|
const supportsTypeAhead = pendingInputAllowsTypeAhead(cliAdapter.supportsTypeAhead === true, durableTurnInFlight, next);
|
|
5245
5621
|
const shouldMergeQueued = opts.dispatchAttempt === undefined && !durableTurnInFlight
|
|
@@ -5296,6 +5672,7 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
5296
5672
|
log(`Queued message (${pendingMessages.length} pending): "${content.substring(0, 80)}" — ${cliName()} ${awaitingFirstPrompt ? 'still booting' : 'is busy'}`);
|
|
5297
5673
|
scheduleBusyPatternIdleProbe(`${cliName()} queued-message`);
|
|
5298
5674
|
}
|
|
5675
|
+
return true;
|
|
5299
5676
|
}
|
|
5300
5677
|
// ─── Screen Update Timer ─────────────────────────────────────────────────────
|
|
5301
5678
|
function startScreenUpdates() {
|
|
@@ -5733,6 +6110,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
5733
6110
|
});
|
|
5734
6111
|
effectiveBackendType = 'herdr';
|
|
5735
6112
|
backend = herdrBe;
|
|
6113
|
+
cliLifetimeNonce++;
|
|
5736
6114
|
// Same as tmux/zellij adopt: writeInput (grok preferSessionId via
|
|
5737
6115
|
// findGrokSessionByPid, claude pid-state) needs cliPid/cliCwd on the
|
|
5738
6116
|
// PtyHandle. spawn() overwrites cliCwd from opts.cwd — use adoptCwd when
|
|
@@ -5791,6 +6169,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
5791
6169
|
: new TmuxPipeBackend(cfg.adoptTmuxTarget, { cliPid: cfg.adoptCliPid });
|
|
5792
6170
|
effectiveBackendType = cfg.adoptZellijPaneId ? 'zellij' : 'tmux';
|
|
5793
6171
|
backend = observeBe;
|
|
6172
|
+
cliLifetimeNonce++;
|
|
5794
6173
|
// writeInput (grok concurrent prompt_history binding, claude pid-state)
|
|
5795
6174
|
// reads these fields off the PtyHandle — constructor only stores
|
|
5796
6175
|
// watchCliPid for liveness, so surface them explicitly for adopt.
|
|
@@ -5970,11 +6349,6 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
5970
6349
|
}
|
|
5971
6350
|
}
|
|
5972
6351
|
}
|
|
5973
|
-
const selectedBackend = selectSessionBackend({ sessionId: cfg.sessionId, backendType: effectiveBackend, backendConfig: riffBackendConfig });
|
|
5974
|
-
isTmuxMode = selectedBackend.isTmuxMode;
|
|
5975
|
-
isPipeMode = selectedBackend.isPipeMode;
|
|
5976
|
-
isZellijMode = selectedBackend.isZellijMode;
|
|
5977
|
-
backend = selectedBackend.backend;
|
|
5978
6352
|
const adapterSessionId = cfg.resume
|
|
5979
6353
|
? (cfg.originalSessionId ?? cfg.sessionId)
|
|
5980
6354
|
: cfg.sessionId;
|
|
@@ -6110,6 +6484,31 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6110
6484
|
currentCliCredentialIsolated = appliedIsolationCapabilities.includes('credential');
|
|
6111
6485
|
const isolationRuntimeDataDir = process.env.SESSION_DATA_DIR
|
|
6112
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++;
|
|
6113
6512
|
// Every bot — isolated OR not — gets its own BOT_HOME dir as a ready-made private-
|
|
6114
6513
|
// storage slot. An isolated sibling denies this path regardless of whether the owner
|
|
6115
6514
|
// is isolated (deny uses the full bots.json), so a non-isolated bot can drop private
|
|
@@ -6175,13 +6574,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6175
6574
|
// on EVERY restart, e.g. for a sandboxed session whose transcript lives in
|
|
6176
6575
|
// an ephemeral overlay upper that the probe can't see). Computed here (not at
|
|
6177
6576
|
// the spawn site below) so the pre-flight can short-circuit on it.
|
|
6178
|
-
|
|
6179
|
-
? TmuxBackend.sessionName(cfg.sessionId)
|
|
6180
|
-
: effectiveBackendType === 'herdr'
|
|
6181
|
-
? HerdrBackend.sessionName(cfg.sessionId)
|
|
6182
|
-
: effectiveBackendType === 'zellij'
|
|
6183
|
-
? ZellijBackend.sessionName(cfg.sessionId)
|
|
6184
|
-
: undefined;
|
|
6577
|
+
let persistentSessionName = selectedBackend.persistentSessionName;
|
|
6185
6578
|
// [read-isolation] Before we decide to reattach a persistent pane: a pane can
|
|
6186
6579
|
// survive a daemon restart still running a CLI that may NOT be isolated (e.g.
|
|
6187
6580
|
// spawned before isolation was enabled, or by an old build). Isolation is only
|
|
@@ -6192,11 +6585,10 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6192
6585
|
// policy pane survives daemon restarts and suspend→resume safely because the
|
|
6193
6586
|
// confinement remains attached to the live process.
|
|
6194
6587
|
if (appliedIsolationCapabilities.length > 0 && persistentSessionName && effectiveBackendType !== 'pty') {
|
|
6195
|
-
const
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
: HerdrBackend.hasSession(persistentSessionName);
|
|
6588
|
+
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
6589
|
+
const paneLive = persistentTarget
|
|
6590
|
+
? probePersistentBackendTarget(persistentTarget) === 'exists'
|
|
6591
|
+
: false;
|
|
6200
6592
|
if (paneLive) {
|
|
6201
6593
|
let marker = null;
|
|
6202
6594
|
marker = readRegularHostFileNoFollow(join(isolationRuntimeDataDir, 'read-isolation', `${cfg.sessionId}.boot`));
|
|
@@ -6211,7 +6603,18 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6211
6603
|
// obsolete permissions. Kill it before publishing any new capability.
|
|
6212
6604
|
log(`[read-isolation] legacy/unmarked persistent pane for ${cfg.sessionId} — killing + cold-spawning with current policy`);
|
|
6213
6605
|
try {
|
|
6214
|
-
|
|
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;
|
|
6215
6618
|
}
|
|
6216
6619
|
catch (e) {
|
|
6217
6620
|
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: could not kill stale persistent pane (${e.message})`);
|
|
@@ -6219,28 +6622,32 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6219
6622
|
}
|
|
6220
6623
|
}
|
|
6221
6624
|
}
|
|
6222
|
-
let
|
|
6625
|
+
let willReattachPersistent = selectedBackend.isReattach === true;
|
|
6223
6626
|
if (cliAdapter.mcpGateway && mcpRuntimeManifest?.entries.length && persistentSessionName && effectiveBackendType !== 'pty') {
|
|
6224
|
-
const
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
: HerdrBackend.hasSession(persistentSessionName);
|
|
6627
|
+
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
6628
|
+
const paneLive = persistentTarget
|
|
6629
|
+
? probePersistentBackendTarget(persistentTarget) === 'exists'
|
|
6630
|
+
: false;
|
|
6229
6631
|
if (paneLive) {
|
|
6230
6632
|
// The trusted Gateway host belongs to the worker and cannot survive a
|
|
6231
6633
|
// worker/daemon replacement. Cold-resume the CLI so its MCP client gets a
|
|
6232
6634
|
// fresh relay socket instead of reattaching to a dead connection.
|
|
6233
6635
|
log(`[mcp-gateway] persistent pane ${cfg.sessionId} has plugin MCP state — cold-resuming with a fresh host`);
|
|
6234
|
-
|
|
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;
|
|
6235
6649
|
}
|
|
6236
6650
|
}
|
|
6237
|
-
const willReattachPersistent = persistentSessionName
|
|
6238
|
-
? effectiveBackendType === 'tmux'
|
|
6239
|
-
? TmuxBackend.hasSession(persistentSessionName)
|
|
6240
|
-
: effectiveBackendType === 'zellij'
|
|
6241
|
-
? ZellijBackend.hasSession(persistentSessionName)
|
|
6242
|
-
: HerdrBackend.hasSession(persistentSessionName)
|
|
6243
|
-
: false;
|
|
6244
6651
|
// The plugin set is stable only for the lifetime of one real CLI process.
|
|
6245
6652
|
// A warm worker reattach keeps the existing Gateway and catalog untouched;
|
|
6246
6653
|
// every fresh/resumed CLI spawn atomically refreshes both from current Bot config.
|
|
@@ -6694,6 +7101,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
6694
7101
|
let spawnBin = cliAdapter.resolvedBin;
|
|
6695
7102
|
let spawnArgs = args;
|
|
6696
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];
|
|
6697
7111
|
// Read isolation (macOS): wrap the whole CLI process in a Seatbelt sandbox that
|
|
6698
7112
|
// denies reads of the sensitive paths (blocklist). The CLI bypasses its OWN
|
|
6699
7113
|
// sandbox (see adapter) so the outer wrapper is the sole enforcer. DARWIN ONLY —
|
|
@@ -7087,6 +7501,19 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7087
7501
|
}
|
|
7088
7502
|
}
|
|
7089
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
|
+
});
|
|
7090
7517
|
// Mandatory credential-only confinement is the OUTERMOST launch wrapper so
|
|
7091
7518
|
// wrapperCli and every descendant it starts inherit the boundary. Full
|
|
7092
7519
|
// Seatbelt/bwrap sessions were already wrapped above and never enter these
|
|
@@ -7224,6 +7651,32 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7224
7651
|
log(`[device-credential-isolation] wrapping ${cliAdapter.id} in credential-only bwrap `
|
|
7225
7652
|
+ `(${hideDirectories.size} authority dir(s), ${hideFiles.size} exact file(s))`);
|
|
7226
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
|
+
}
|
|
7227
7680
|
backend.spawn(spawnBin, spawnArgs, {
|
|
7228
7681
|
cwd: spawnCwd,
|
|
7229
7682
|
cols: PTY_COLS,
|
|
@@ -7232,6 +7685,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7232
7685
|
injectEnv: perBotInjectKeys.length ? perBotInjectEnv : undefined,
|
|
7233
7686
|
launchShell: lastInitConfig?.launchShell,
|
|
7234
7687
|
});
|
|
7688
|
+
if (selectedBackend.createdHerdrSessionName) {
|
|
7689
|
+
send({
|
|
7690
|
+
type: 'user_notify',
|
|
7691
|
+
turnId: currentBotmuxTurnId,
|
|
7692
|
+
message: `已创建 Botmux 专属 Herdr 会话:\`${selectedBackend.createdHerdrSessionName}\``,
|
|
7693
|
+
});
|
|
7694
|
+
}
|
|
7235
7695
|
// Write CLI PID marker so agent-facing subcommands (`botmux send`, etc.)
|
|
7236
7696
|
// can verify they were spawned inside a botmux session by walking the
|
|
7237
7697
|
// process tree and looking for a matching pid file in this directory.
|
|
@@ -7519,44 +7979,59 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7519
7979
|
}, READY_SIGNAL_TIMEOUT_MS);
|
|
7520
7980
|
readySignalTimer.unref?.();
|
|
7521
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
|
+
};
|
|
7522
8008
|
// Set up idle detection. Riff (remote HTTP backend) has no PTY output and
|
|
7523
8009
|
// is marked ready immediately after spawn (see below), so the idle detector
|
|
7524
8010
|
// is unnecessary — and without a readyPattern it would fire on every
|
|
7525
8011
|
// quiescence, repeatedly triggering markPromptReady() and duplicate cards.
|
|
7526
8012
|
if (effectiveBackendType !== 'riff') {
|
|
7527
8013
|
idleDetector = new IdleDetector(cliAdapter);
|
|
7528
|
-
idleDetector.onIdle(
|
|
8014
|
+
idleDetector.onIdle((evidenceSource) => {
|
|
7529
8015
|
log('Prompt detected (idle)');
|
|
7530
|
-
|
|
7531
|
-
// flushPending() which can immediately fire the next queued message
|
|
7532
|
-
// (type-ahead adapters), shifting bridgeQueue's notion of "current
|
|
7533
|
-
// turn" before we've had a chance to emit the previous one.
|
|
7534
|
-
if (bridgeJsonlPath) {
|
|
7535
|
-
try {
|
|
7536
|
-
bridgeDrainAndMaybeEmit();
|
|
7537
|
-
}
|
|
7538
|
-
catch (err) {
|
|
7539
|
-
log(`Bridge emit error: ${err.message}`);
|
|
7540
|
-
}
|
|
7541
|
-
}
|
|
7542
|
-
if (codexBridgeFallbackActive()) {
|
|
7543
|
-
try {
|
|
7544
|
-
codexBridgeDrainAndMaybeEmit();
|
|
7545
|
-
}
|
|
7546
|
-
catch (err) {
|
|
7547
|
-
log(`Codex bridge emit error: ${err.message}`);
|
|
7548
|
-
}
|
|
7549
|
-
}
|
|
7550
|
-
if (evidenceSource === 'screen') {
|
|
7551
|
-
markPromptReadyFromPty();
|
|
7552
|
-
}
|
|
7553
|
-
else {
|
|
7554
|
-
markPromptReady();
|
|
7555
|
-
}
|
|
8016
|
+
drainBridgesThenMarkReady(evidenceSource);
|
|
7556
8017
|
});
|
|
7557
8018
|
}
|
|
7558
8019
|
backend.onData(onPtyData);
|
|
7559
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
|
+
}
|
|
7560
8035
|
backend.onAccessUrl?.((url) => {
|
|
7561
8036
|
send({
|
|
7562
8037
|
type: 'riff_access_url',
|
|
@@ -7744,6 +8219,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7744
8219
|
}
|
|
7745
8220
|
function killCli(opts = {}) {
|
|
7746
8221
|
currentCliCredentialIsolated = false;
|
|
8222
|
+
stopNativeSessionTitleSync();
|
|
7747
8223
|
stopSessionMcpGatewayHost();
|
|
7748
8224
|
stopCodexRpcEngine();
|
|
7749
8225
|
if (!opts.preservePending)
|
|
@@ -7767,6 +8243,7 @@ function killCli(opts = {}) {
|
|
|
7767
8243
|
readyPatternSeenDuringHold = false;
|
|
7768
8244
|
awaitingPostSessionStartPromptEvidence = false;
|
|
7769
8245
|
stopScreenAnalyzer();
|
|
8246
|
+
stopStuckDetector();
|
|
7770
8247
|
stopScreenUpdates();
|
|
7771
8248
|
backend?.kill();
|
|
7772
8249
|
backend = null;
|
|
@@ -7883,6 +8360,7 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
7883
8360
|
if (lastInitConfig) {
|
|
7884
8361
|
startScreenUpdates();
|
|
7885
8362
|
startScreenAnalyzer();
|
|
8363
|
+
startStuckDetector();
|
|
7886
8364
|
try {
|
|
7887
8365
|
const restartCfg = { ...lastInitConfig, resume: true, prompt: '', cliSessionId: rpcThreadId ?? lastInitConfig.cliSessionId };
|
|
7888
8366
|
// Re-engage RPC so the new --remote pane binds to the CURRENT app-server
|
|
@@ -7896,6 +8374,7 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
7896
8374
|
await engageCodexRpc(restartCfg);
|
|
7897
8375
|
}
|
|
7898
8376
|
await spawnCli(restartCfg, { pluginGenerationPrepared: rpcPluginGenerationPrepared });
|
|
8377
|
+
await prepareCodexNativeTitleGeneration(restartCfg, codexRpcEngine);
|
|
7899
8378
|
if (codexRpcEngine)
|
|
7900
8379
|
armRpcStartupDialogDismiss();
|
|
7901
8380
|
}
|
|
@@ -7960,8 +8439,24 @@ function startWebServer(host, preferredPort) {
|
|
|
7960
8439
|
// normal buffer until resize causes a redraw. Preserve that mode so
|
|
7961
8440
|
// scroll gestures continue to target the CLI's own transcript.
|
|
7962
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';
|
|
7963
8458
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
7964
|
-
res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll));
|
|
8459
|
+
res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll, localTerminalBackend));
|
|
7965
8460
|
});
|
|
7966
8461
|
wss = new WebSocketServer({
|
|
7967
8462
|
server: httpServer,
|
|
@@ -8258,7 +8753,7 @@ function startWebServer(host, preferredPort) {
|
|
|
8258
8753
|
.then(resolve, reject);
|
|
8259
8754
|
});
|
|
8260
8755
|
}
|
|
8261
|
-
function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '', forceRemoteScroll = false) {
|
|
8756
|
+
function getTerminalHtml(hasWrite, platformReadonly = false, loginUrl = '', forceRemoteScroll = false, localTerminalBackend = false) {
|
|
8262
8757
|
const label = sessionId.substring(0, 8);
|
|
8263
8758
|
return `<!DOCTYPE html>
|
|
8264
8759
|
<html>
|
|
@@ -8395,6 +8890,7 @@ if(isTouch){document.body.classList.add('touch');}
|
|
|
8395
8890
|
var hasToken=${hasWrite};
|
|
8396
8891
|
var platformReadonly=${platformReadonly};
|
|
8397
8892
|
var remoteScroll=${forceRemoteScroll};
|
|
8893
|
+
var localTerminalBackend=${localTerminalBackend};
|
|
8398
8894
|
if(!hasToken){
|
|
8399
8895
|
if(platformReadonly){var _lb=document.getElementById('login-banner');_lb.classList.add('show');}
|
|
8400
8896
|
else{var _rb=document.getElementById('readonly-banner');_rb.classList.add('show');_rb.addEventListener('click',function(){_rb.classList.remove('show')});}
|
|
@@ -8601,8 +9097,21 @@ window.addEventListener('resize',onViewportResize);
|
|
|
8601
9097
|
// A high-resolution trackpad emits dozens of wheel events for one gesture, so
|
|
8602
9098
|
// cap the whole continuous burst — not each browser event — then require an idle
|
|
8603
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.
|
|
8604
9113
|
var _scrollAccum=0,_scrollBurstTicks=0,_scrollBurstDir=0,_scrollBurstT=0;
|
|
8605
|
-
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;
|
|
8606
9115
|
function _endScrollBurst(){
|
|
8607
9116
|
clearTimeout(_scrollBurstT);_scrollBurstT=0;
|
|
8608
9117
|
_scrollAccum=0;_scrollBurstTicks=0;_scrollBurstDir=0;
|
|
@@ -9058,6 +9567,10 @@ function send(msg) {
|
|
|
9058
9567
|
}
|
|
9059
9568
|
process.send?.(payload);
|
|
9060
9569
|
}
|
|
9570
|
+
function acknowledgeTurnInputCommitted(turnId) {
|
|
9571
|
+
if (turnId)
|
|
9572
|
+
send({ type: 'turn_input_committed', turnId });
|
|
9573
|
+
}
|
|
9061
9574
|
function publishLocalProcessAttestation(cliPid) {
|
|
9062
9575
|
const cliProcStart = cliPid ? readProcessStartIdentity(cliPid) : undefined;
|
|
9063
9576
|
send({
|
|
@@ -9115,11 +9628,13 @@ process.on('message', async (raw) => {
|
|
|
9115
9628
|
const msg = raw;
|
|
9116
9629
|
switch (msg.type) {
|
|
9117
9630
|
case 'init': {
|
|
9631
|
+
const initStartedAtMs = Date.now();
|
|
9118
9632
|
if (lastInitConfig)
|
|
9119
9633
|
return; // already initialized
|
|
9120
9634
|
lastInitConfig = msg;
|
|
9121
9635
|
sessionId = msg.sessionId;
|
|
9122
9636
|
refreshTerminalViewToken();
|
|
9637
|
+
refreshTerminalWriteToken();
|
|
9123
9638
|
if (msg.ownerOpenId)
|
|
9124
9639
|
process.env.__OWNER_OPEN_ID = msg.ownerOpenId;
|
|
9125
9640
|
// Pin this worker's i18n locale early so every t() call below resolves
|
|
@@ -9163,6 +9678,7 @@ process.on('message', async (raw) => {
|
|
|
9163
9678
|
port = await startWebServer(config.web.workerHost, msg.webPort);
|
|
9164
9679
|
startScreenUpdates();
|
|
9165
9680
|
startScreenAnalyzer();
|
|
9681
|
+
startStuckDetector();
|
|
9166
9682
|
}
|
|
9167
9683
|
else {
|
|
9168
9684
|
// Workflow attempts still expose a read-only web terminal so the
|
|
@@ -9206,6 +9722,7 @@ process.on('message', async (raw) => {
|
|
|
9206
9722
|
throw new Error('codex RPC resume: could not replace stale --remote pane; aborting init');
|
|
9207
9723
|
}
|
|
9208
9724
|
await spawnCli(msg, { pluginGenerationPrepared: rpcPluginGenerationPrepared });
|
|
9725
|
+
await prepareCodexNativeTitleGeneration(msg, codexRpcEngine);
|
|
9209
9726
|
if (codexRpcEngine)
|
|
9210
9727
|
armRpcStartupDialogDismiss(); // boundary #4: keep the --remote pane from freezing on a startup dialog
|
|
9211
9728
|
// Queue the initial prompt — flushed when CLI shows idle.
|
|
@@ -9237,6 +9754,7 @@ process.on('message', async (raw) => {
|
|
|
9237
9754
|
// execution — exactly-once, Codex P1-1). Ambiguous never reaches here.
|
|
9238
9755
|
// - RPC RESUME: queuePrompt=true → queue for post-ready flush (bridge
|
|
9239
9756
|
// marked at flush time, P0-1).
|
|
9757
|
+
let initialInputCommitted = false;
|
|
9240
9758
|
if (shouldQueueInitialPrompt({
|
|
9241
9759
|
hasPrompt: !!msg.prompt,
|
|
9242
9760
|
rpcEngineActive: !!codexRpcEngine,
|
|
@@ -9254,13 +9772,22 @@ process.on('message', async (raw) => {
|
|
|
9254
9772
|
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
9255
9773
|
codexAppInput: msg.promptCodexAppInput,
|
|
9256
9774
|
});
|
|
9775
|
+
initialInputCommitted = true;
|
|
9776
|
+
}
|
|
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;
|
|
9257
9781
|
}
|
|
9258
|
-
|
|
9259
|
-
|
|
9260
|
-
//
|
|
9261
|
-
//
|
|
9262
|
-
//
|
|
9263
|
-
|
|
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) {
|
|
9264
9791
|
flushPending();
|
|
9265
9792
|
}
|
|
9266
9793
|
send({
|
|
@@ -9268,6 +9795,11 @@ process.on('message', async (raw) => {
|
|
|
9268
9795
|
port,
|
|
9269
9796
|
token: writeToken,
|
|
9270
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),
|
|
9271
9803
|
turnId: currentBotmuxTurnId,
|
|
9272
9804
|
dispatchAttempt: currentBotmuxDispatchAttempt,
|
|
9273
9805
|
});
|
|
@@ -9282,6 +9814,21 @@ process.on('message', async (raw) => {
|
|
|
9282
9814
|
// Mark new turn baseline so the streaming card only shows this turn's content
|
|
9283
9815
|
renderer?.markNewTurn();
|
|
9284
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
|
+
}
|
|
9285
9832
|
// Cancel any active tmux copy-mode scroll so user input reaches the CLI.
|
|
9286
9833
|
if (tmuxScrolledHalfPages > 0)
|
|
9287
9834
|
exitTmuxScrollMode();
|
|
@@ -9299,12 +9846,16 @@ process.on('message', async (raw) => {
|
|
|
9299
9846
|
log('Message received after crash-loop stop; retrying CLI start');
|
|
9300
9847
|
destroyCrashDiagnosticTerminal('retry after message');
|
|
9301
9848
|
stopScreenAnalyzer();
|
|
9849
|
+
stopStuckDetector();
|
|
9302
9850
|
stopScreenUpdates();
|
|
9303
9851
|
awaitingFirstPrompt = true;
|
|
9304
9852
|
startScreenUpdates();
|
|
9305
9853
|
startScreenAnalyzer();
|
|
9854
|
+
startStuckDetector();
|
|
9306
9855
|
try {
|
|
9307
|
-
|
|
9856
|
+
const restartCfg = { ...lastInitConfig, resume: true, prompt: '' };
|
|
9857
|
+
await spawnCli(restartCfg);
|
|
9858
|
+
await prepareCodexNativeTitleGeneration(restartCfg, codexRpcEngine);
|
|
9308
9859
|
}
|
|
9309
9860
|
catch (err) {
|
|
9310
9861
|
// Pass the message's own attempt (not the stale currentBotmux* from a
|
|
@@ -9386,6 +9937,9 @@ process.on('message', async (raw) => {
|
|
|
9386
9937
|
if (result && result.submitted === false) {
|
|
9387
9938
|
scheduleSubmitFailureNotify(content, result.recheck, 'submit history', undefined, result.failureReason, turnSeq);
|
|
9388
9939
|
}
|
|
9940
|
+
else {
|
|
9941
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9942
|
+
}
|
|
9389
9943
|
}
|
|
9390
9944
|
catch (err) {
|
|
9391
9945
|
log(`Adopt writeInput error (${lastInitConfig?.cliId}): ${err.message}`);
|
|
@@ -9403,9 +9957,11 @@ process.on('message', async (raw) => {
|
|
|
9403
9957
|
// command (raw_input) fix.
|
|
9404
9958
|
await new Promise(r => setTimeout(r, 200));
|
|
9405
9959
|
backend.sendSpecialKeys('Enter');
|
|
9960
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9406
9961
|
}
|
|
9407
9962
|
else {
|
|
9408
9963
|
backend.write(content + '\r');
|
|
9964
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9409
9965
|
}
|
|
9410
9966
|
isPromptReady = false;
|
|
9411
9967
|
idleDetector?.reset();
|
|
@@ -9417,11 +9973,13 @@ process.on('message', async (raw) => {
|
|
|
9417
9973
|
// arrival. Marking now would race with a still-running previous
|
|
9418
9974
|
// turn whose `botmux send` could sneak its sentAtMs past this
|
|
9419
9975
|
// turn's markTimeMs and falsely suppress its fallback.
|
|
9420
|
-
sendToPty(content, msg.turnId, {
|
|
9976
|
+
const inputCommitted = sendToPty(content, msg.turnId, {
|
|
9421
9977
|
codexAppInput,
|
|
9422
9978
|
dispatchAttempt: msg.dispatchAttempt,
|
|
9423
9979
|
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
9424
9980
|
});
|
|
9981
|
+
if (inputCommitted)
|
|
9982
|
+
acknowledgeTurnInputCommitted(msg.turnId);
|
|
9425
9983
|
}
|
|
9426
9984
|
break;
|
|
9427
9985
|
}
|
|
@@ -9449,6 +10007,13 @@ process.on('message', async (raw) => {
|
|
|
9449
10007
|
// IPC handlers are concurrent with async init, so queue first even when
|
|
9450
10008
|
// the adapter/backend has not finished initializing. flushPending will
|
|
9451
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();
|
|
9452
10017
|
pendingSessionRename = msg.title;
|
|
9453
10018
|
log(`Queued native session rename: ${msg.title}`);
|
|
9454
10019
|
void flushPending();
|
|
@@ -9545,7 +10110,49 @@ process.on('message', async (raw) => {
|
|
|
9545
10110
|
break;
|
|
9546
10111
|
}
|
|
9547
10112
|
case 'tui_keys': {
|
|
9548
|
-
|
|
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();
|
|
9549
10156
|
break;
|
|
9550
10157
|
}
|
|
9551
10158
|
case 'inject_command': {
|
|
@@ -9710,6 +10317,7 @@ process.on('message', async (raw) => {
|
|
|
9710
10317
|
});
|
|
9711
10318
|
// ─── Cleanup ─────────────────────────────────────────────────────────────────
|
|
9712
10319
|
function cleanup() {
|
|
10320
|
+
stopNativeSessionTitleSync();
|
|
9713
10321
|
cleanupPiInitialPromptFiles();
|
|
9714
10322
|
stopSessionMcpGatewayHost();
|
|
9715
10323
|
if (tmuxRestartTimer) {
|
|
@@ -9814,7 +10422,11 @@ function teardownSandboxBestEffort() {
|
|
|
9814
10422
|
// and process.exit(1), killing a live session over a dropped log write. Install
|
|
9815
10423
|
// the guard before any further stdout writes (log() writes to process.stdout).
|
|
9816
10424
|
installStdioEpipeGuard();
|
|
9817
|
-
process.on('exit', () => {
|
|
10425
|
+
process.on('exit', () => {
|
|
10426
|
+
stopNativeSessionTitleSync();
|
|
10427
|
+
teardownSandboxBestEffort();
|
|
10428
|
+
stopCodexRpcEngine();
|
|
10429
|
+
});
|
|
9818
10430
|
process.on('uncaughtException', (err) => {
|
|
9819
10431
|
// A broken pipe on stdout/stderr (or any socket) must not tear down a live
|
|
9820
10432
|
// session — the stdio guard handles those it can; this is the backstop.
|