botmux 3.16.0 → 3.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/riff-backend.d.ts +84 -1
- package/dist/adapters/backend/riff-backend.d.ts.map +1 -1
- package/dist/adapters/backend/riff-backend.js +202 -19
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/sandbox.d.ts +8 -0
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +15 -0
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/tmux-backend.d.ts +61 -7
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +80 -9
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +62 -4
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +240 -27
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +4 -2
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +27 -0
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +28 -0
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +51 -23
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/dsh-tui.d.ts +24 -0
- package/dist/adapters/cli/dsh-tui.d.ts.map +1 -0
- package/dist/adapters/cli/dsh-tui.js +99 -0
- package/dist/adapters/cli/dsh-tui.js.map +1 -0
- package/dist/adapters/cli/dsh.d.ts.map +1 -1
- package/dist/adapters/cli/dsh.js +15 -9
- package/dist/adapters/cli/dsh.js.map +1 -1
- package/dist/adapters/cli/fs-policy.d.ts +98 -1
- package/dist/adapters/cli/fs-policy.d.ts.map +1 -1
- package/dist/adapters/cli/fs-policy.js +182 -7
- package/dist/adapters/cli/fs-policy.js.map +1 -1
- package/dist/adapters/cli/grok.js +16 -18
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/model-catalog-json.d.ts +17 -0
- package/dist/adapters/cli/model-catalog-json.d.ts.map +1 -0
- package/dist/adapters/cli/model-catalog-json.js +43 -0
- package/dist/adapters/cli/model-catalog-json.js.map +1 -0
- package/dist/adapters/cli/oh-my-pi.d.ts +2 -0
- package/dist/adapters/cli/oh-my-pi.d.ts.map +1 -1
- package/dist/adapters/cli/oh-my-pi.js +45 -13
- package/dist/adapters/cli/oh-my-pi.js.map +1 -1
- package/dist/adapters/cli/opencode.js +1 -1
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +2 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +6 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/relay.d.ts.map +1 -1
- package/dist/adapters/cli/relay.js +7 -0
- package/dist/adapters/cli/relay.js.map +1 -1
- package/dist/adapters/cli/seed.d.ts.map +1 -1
- package/dist/adapters/cli/seed.js +6 -0
- package/dist/adapters/cli/seed.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +20 -14
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts +4 -0
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +153 -0
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +42 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/autostart.d.ts +6 -0
- package/dist/autostart.d.ts.map +1 -1
- package/dist/autostart.js +21 -1
- package/dist/autostart.js.map +1 -1
- package/dist/bot-registry.d.ts +27 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +62 -2
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/vc-agent.d.ts.map +1 -1
- package/dist/cli/vc-agent.js +21 -3
- package/dist/cli/vc-agent.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +92 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +28 -0
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/core/bot-steer-directive.d.ts +25 -0
- package/dist/core/bot-steer-directive.d.ts.map +1 -0
- package/dist/core/bot-steer-directive.js +47 -0
- package/dist/core/bot-steer-directive.js.map +1 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +102 -18
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +53 -13
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +3 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +26 -5
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dashboard-url.d.ts +10 -1
- package/dist/core/dashboard-url.d.ts.map +1 -1
- package/dist/core/dashboard-url.js +12 -2
- package/dist/core/dashboard-url.js.map +1 -1
- package/dist/core/dispatch.d.ts.map +1 -1
- package/dist/core/dispatch.js +5 -4
- package/dist/core/dispatch.js.map +1 -1
- package/dist/core/existing-app-server.d.ts +22 -0
- package/dist/core/existing-app-server.d.ts.map +1 -0
- package/dist/core/existing-app-server.js +85 -0
- package/dist/core/existing-app-server.js.map +1 -0
- package/dist/core/native-topic-id.d.ts +9 -0
- package/dist/core/native-topic-id.d.ts.map +1 -0
- package/dist/core/native-topic-id.js +14 -0
- package/dist/core/native-topic-id.js.map +1 -0
- package/dist/core/per-bot-env.d.ts.map +1 -1
- package/dist/core/per-bot-env.js +10 -0
- package/dist/core/per-bot-env.js.map +1 -1
- package/dist/core/plugins/mcp/gateway.d.ts +27 -3
- package/dist/core/plugins/mcp/gateway.d.ts.map +1 -1
- package/dist/core/plugins/mcp/gateway.js +272 -26
- package/dist/core/plugins/mcp/gateway.js.map +1 -1
- package/dist/core/plugins/mcp/host.d.ts +18 -0
- package/dist/core/plugins/mcp/host.d.ts.map +1 -1
- package/dist/core/plugins/mcp/host.js +80 -16
- package/dist/core/plugins/mcp/host.js.map +1 -1
- package/dist/core/plugins/mcp/launch-record.d.ts +14 -0
- package/dist/core/plugins/mcp/launch-record.d.ts.map +1 -0
- package/dist/core/plugins/mcp/launch-record.js +63 -0
- package/dist/core/plugins/mcp/launch-record.js.map +1 -0
- package/dist/core/session-activity.d.ts +11 -0
- package/dist/core/session-activity.d.ts.map +1 -1
- package/dist/core/session-activity.js +23 -0
- package/dist/core/session-activity.js.map +1 -1
- package/dist/core/session-group-birth.d.ts.map +1 -1
- package/dist/core/session-group-birth.js +23 -8
- package/dist/core/session-group-birth.js.map +1 -1
- package/dist/core/session-manager.d.ts +1 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +16 -20
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/shared-adopt.d.ts +20 -0
- package/dist/core/shared-adopt.d.ts.map +1 -0
- package/dist/core/shared-adopt.js +26 -0
- package/dist/core/shared-adopt.js.map +1 -0
- package/dist/core/skills/customize-command.d.ts +7 -0
- package/dist/core/skills/customize-command.d.ts.map +1 -0
- package/dist/core/skills/customize-command.js +303 -0
- package/dist/core/skills/customize-command.js.map +1 -0
- 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 +15 -0
- package/dist/core/terminal-write-auth.js.map +1 -1
- package/dist/core/terminal-write-frame.d.ts +30 -0
- package/dist/core/terminal-write-frame.d.ts.map +1 -0
- package/dist/core/terminal-write-frame.js +44 -0
- package/dist/core/terminal-write-frame.js.map +1 -0
- package/dist/core/types.d.ts +14 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +6 -8
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +31 -6
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +444 -102
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/core/worker-startup-retry.d.ts +52 -0
- package/dist/core/worker-startup-retry.d.ts.map +1 -0
- package/dist/core/worker-startup-retry.js +68 -0
- package/dist/core/worker-startup-retry.js.map +1 -0
- package/dist/daemon.d.ts +9 -8
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +467 -171
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/autostart-api.d.ts +26 -0
- package/dist/dashboard/autostart-api.d.ts.map +1 -0
- package/dist/dashboard/autostart-api.js +88 -0
- package/dist/dashboard/autostart-api.js.map +1 -0
- package/dist/dashboard/bot-payload.d.ts +5 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +2 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/control-csrf.d.ts +29 -1
- package/dist/dashboard/control-csrf.d.ts.map +1 -1
- package/dist/dashboard/control-csrf.js +49 -10
- package/dist/dashboard/control-csrf.js.map +1 -1
- package/dist/dashboard/customization-api.d.ts +23 -0
- package/dist/dashboard/customization-api.d.ts.map +1 -0
- package/dist/dashboard/customization-api.js +198 -0
- package/dist/dashboard/customization-api.js.map +1 -0
- package/dist/dashboard/debug-terminal.d.ts +13 -1
- package/dist/dashboard/debug-terminal.d.ts.map +1 -1
- package/dist/dashboard/debug-terminal.js +18 -4
- package/dist/dashboard/debug-terminal.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +0 -23
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +7 -28
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/terminal-control-route.d.ts +32 -0
- package/dist/dashboard/terminal-control-route.d.ts.map +1 -0
- package/dist/dashboard/terminal-control-route.js +70 -0
- package/dist/dashboard/terminal-control-route.js.map +1 -0
- package/dist/dashboard/terminal-control.d.ts +64 -13
- package/dist/dashboard/terminal-control.d.ts.map +1 -1
- package/dist/dashboard/terminal-control.js +77 -14
- package/dist/dashboard/terminal-control.js.map +1 -1
- package/dist/dashboard/terminal-front-proxy.d.ts.map +1 -1
- package/dist/dashboard/terminal-front-proxy.js +41 -9
- package/dist/dashboard/terminal-front-proxy.js.map +1 -1
- package/dist/dashboard/vc-consumer-profiles-api.d.ts +69 -18
- package/dist/dashboard/vc-consumer-profiles-api.d.ts.map +1 -1
- package/dist/dashboard/vc-consumer-profiles-api.js +149 -29
- package/dist/dashboard/vc-consumer-profiles-api.js.map +1 -1
- package/dist/dashboard/web/agent-workbench-api.d.ts +34 -2
- package/dist/dashboard/web/agent-workbench-api.d.ts.map +1 -1
- package/dist/dashboard/web/agent-workbench-api.js +39 -4
- package/dist/dashboard/web/agent-workbench-api.js.map +1 -1
- package/dist/dashboard/web/agent-workbench-appearance.d.ts +6 -3
- package/dist/dashboard/web/agent-workbench-appearance.d.ts.map +1 -1
- package/dist/dashboard/web/agent-workbench-appearance.js +7 -4
- package/dist/dashboard/web/agent-workbench-appearance.js.map +1 -1
- package/dist/dashboard/web/agent-workbench-model.d.ts +2 -0
- package/dist/dashboard/web/agent-workbench-model.d.ts.map +1 -1
- package/dist/dashboard/web/agent-workbench-model.js.map +1 -1
- package/dist/dashboard/web/agent-workbench-page.js +2 -2
- package/dist/dashboard/web/agent-workbench-page.js.map +1 -1
- package/dist/dashboard/web/agent-workbench-panes.d.ts +59 -4
- package/dist/dashboard/web/agent-workbench-panes.d.ts.map +1 -1
- package/dist/dashboard/web/agent-workbench-panes.js +493 -85
- package/dist/dashboard/web/agent-workbench-panes.js.map +1 -1
- package/dist/dashboard/web/agent-workbench-session-list.d.ts +11 -7
- package/dist/dashboard/web/agent-workbench-session-list.d.ts.map +1 -1
- package/dist/dashboard/web/agent-workbench-session-list.js +24 -31
- package/dist/dashboard/web/agent-workbench-session-list.js.map +1 -1
- package/dist/dashboard/web/agent-workbench-terminal-control.d.ts +198 -0
- package/dist/dashboard/web/agent-workbench-terminal-control.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-terminal-control.js +292 -0
- package/dist/dashboard/web/agent-workbench-terminal-control.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-view.d.ts +3 -2
- package/dist/dashboard/web/agent-workbench-view.d.ts.map +1 -1
- package/dist/dashboard/web/agent-workbench-view.js +69 -50
- package/dist/dashboard/web/agent-workbench-view.js.map +1 -1
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +58 -7
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +24 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +191 -16
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +17 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +51 -2
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +32 -8
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/confirm-modal.d.ts +35 -0
- package/dist/dashboard/web/confirm-modal.d.ts.map +1 -0
- package/dist/dashboard/web/confirm-modal.js +111 -0
- package/dist/dashboard/web/confirm-modal.js.map +1 -0
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +2 -1
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/customization-page.d.ts +3 -0
- package/dist/dashboard/web/customization-page.d.ts.map +1 -0
- package/dist/dashboard/web/customization-page.js +315 -0
- package/dist/dashboard/web/customization-page.js.map +1 -0
- package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-routes.js +1 -0
- package/dist/dashboard/web/dashboard-routes.js.map +1 -1
- package/dist/dashboard/web/groups-page.d.ts.map +1 -1
- package/dist/dashboard/web/groups-page.js +11 -9
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +226 -42
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/monitor-room.d.ts.map +1 -1
- package/dist/dashboard/web/monitor-room.js +3 -2
- package/dist/dashboard/web/monitor-room.js.map +1 -1
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js +98 -63
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/roles-page.d.ts.map +1 -1
- package/dist/dashboard/web/roles-page.js +4 -3
- package/dist/dashboard/web/roles-page.js.map +1 -1
- package/dist/dashboard/web/schedules-page.d.ts.map +1 -1
- package/dist/dashboard/web/schedules-page.js +165 -18
- package/dist/dashboard/web/schedules-page.js.map +1 -1
- package/dist/dashboard/web/sessions-kanban.d.ts +2 -0
- package/dist/dashboard/web/sessions-kanban.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-kanban.js +142 -24
- 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 +177 -94
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/sessions.d.ts.map +1 -1
- package/dist/dashboard/web/sessions.js +5 -4
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +0 -8
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +185 -65
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/skills/skill-library-tab.d.ts +2 -0
- package/dist/dashboard/web/skills/skill-library-tab.d.ts.map +1 -1
- package/dist/dashboard/web/skills/skill-library-tab.js +15 -1
- package/dist/dashboard/web/skills/skill-library-tab.js.map +1 -1
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +5 -4
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/web/store.d.ts +5 -0
- package/dist/dashboard/web/store.d.ts.map +1 -1
- package/dist/dashboard/web/store.js +4 -0
- package/dist/dashboard/web/store.js.map +1 -1
- package/dist/dashboard/web/team-federation-page.d.ts.map +1 -1
- package/dist/dashboard/web/team-federation-page.js +3 -2
- package/dist/dashboard/web/team-federation-page.js.map +1 -1
- package/dist/dashboard/web/toast.d.ts +15 -0
- package/dist/dashboard/web/toast.d.ts.map +1 -0
- package/dist/dashboard/web/toast.js +70 -0
- package/dist/dashboard/web/toast.js.map +1 -0
- package/dist/dashboard/web/v3-components.d.ts.map +1 -1
- package/dist/dashboard/web/v3-components.js +2 -1
- package/dist/dashboard/web/v3-components.js.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts +10 -12
- package/dist/dashboard/web/vc-consumer-profiles-section.d.ts.map +1 -1
- package/dist/dashboard/web/vc-consumer-profiles-section.js +219 -200
- package/dist/dashboard/web/vc-consumer-profiles-section.js.map +1 -1
- package/dist/dashboard/web/whiteboards-page.d.ts.map +1 -1
- package/dist/dashboard/web/whiteboards-page.js +2 -1
- package/dist/dashboard/web/whiteboards-page.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/agent-workbench-dock-page-LZSUOCEQ.js +1 -0
- package/dist/dashboard-web/chunks/agent-workbench-page-YONDRJU7.js +1 -0
- package/dist/dashboard-web/chunks/bot-defaults-page-X7LV6373.js +1 -0
- package/dist/dashboard-web/chunks/chunk-2PVRDXEL.js +1 -0
- package/dist/dashboard-web/chunks/chunk-53JUKBU2.js +10 -0
- package/dist/dashboard-web/chunks/{chunk-5CJMHTT4.js → chunk-5QX4OYOR.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-NACP7BMR.js → chunk-5VRVJFQR.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-FDZ7WOBW.js → chunk-6ROOZWYV.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-F6KNPF4R.js → chunk-CK22743G.js} +15 -19
- package/dist/dashboard-web/chunks/chunk-CVNIMUPY.js +25 -0
- package/dist/dashboard-web/chunks/chunk-GJC2TMWS.js +2 -0
- package/dist/dashboard-web/chunks/chunk-OT6L6G7R.js +1 -0
- package/dist/dashboard-web/chunks/chunk-QITAMHW6.js +1 -0
- package/dist/dashboard-web/chunks/chunk-RHVFZC5Z.js +1 -0
- package/dist/dashboard-web/chunks/chunk-SOBMN2VY.js +1 -0
- package/dist/dashboard-web/chunks/chunk-SZOB6VK2.js +1 -0
- package/dist/dashboard-web/chunks/chunk-XVIXNQGC.js +44 -0
- package/dist/dashboard-web/chunks/chunk-YUJF34ME.js +1 -0
- package/dist/dashboard-web/chunks/connectors-page-LB2HEJ7F.js +1 -0
- package/dist/dashboard-web/chunks/customization-page-4GXBYLG6.js +105 -0
- package/dist/dashboard-web/chunks/feedback-page-IZQ7VLDG.js +1 -0
- package/dist/dashboard-web/chunks/groups-page-4WCP5OEV.js +8 -0
- package/dist/dashboard-web/chunks/{insights-page-HJFDYR5N.js → insights-page-TXQHIPQL.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-V7PCP47V.js → monitor-room-VBRVWXCM.js} +8 -8
- package/dist/dashboard-web/chunks/monitoring-page-QDDZWERE.js +2 -0
- package/dist/dashboard-web/chunks/office-page-PENFNYPG.js +1 -0
- package/dist/dashboard-web/chunks/overview-page-BZQDP6MP.js +1 -0
- package/dist/dashboard-web/chunks/plugin-page-JX4GSWZS.js +1 -0
- package/dist/dashboard-web/chunks/roles-page-VPNINZNK.js +1 -0
- package/dist/dashboard-web/chunks/schedules-page-BITO6HEA.js +1 -0
- package/dist/dashboard-web/chunks/sessions-page-ZKCOVTD2.js +8 -0
- package/dist/dashboard-web/chunks/settings-page-UT6JDTS6.js +2 -0
- package/dist/dashboard-web/chunks/skills-page-GULXVAJY.js +4 -0
- package/dist/dashboard-web/chunks/team-federation-page-CUUJWGEG.js +1 -0
- package/dist/dashboard-web/chunks/v3-page-5WVJ5AUR.js +94 -0
- package/dist/dashboard-web/chunks/whiteboards-page-FZ2CME4H.js +1 -0
- package/dist/dashboard-web/design-tokens.css +236 -0
- package/dist/dashboard-web/index.html +1 -0
- package/dist/dashboard-web/style.css +1930 -114
- package/dist/dashboard.js +360 -279
- package/dist/dashboard.js.map +1 -1
- package/dist/desktop/renderer/app.js +6 -0
- package/dist/desktop/renderer/app.js.map +1 -1
- package/dist/dsh-runner.js +139 -17
- package/dist/dsh-runner.js.map +1 -1
- package/dist/global-config.d.ts +33 -2
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +62 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +13 -4
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +27 -3
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +33 -4
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +13 -4
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +2 -0
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +7 -4
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +135 -20
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +38 -3
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +163 -13
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/v3-blocked-card-handler.d.ts.map +1 -1
- package/dist/im/lark/v3-blocked-card-handler.js +2 -1
- package/dist/im/lark/v3-blocked-card-handler.js.map +1 -1
- package/dist/im/lark/v3-blocked-card.d.ts +1 -1
- package/dist/im/lark/v3-blocked-card.d.ts.map +1 -1
- package/dist/im/lark/v3-blocked-card.js +14 -0
- package/dist/im/lark/v3-blocked-card.js.map +1 -1
- package/dist/index-core-only.js +2 -0
- package/dist/index-core-only.js.map +1 -1
- package/dist/index-daemon.js +3 -0
- package/dist/index-daemon.js.map +1 -1
- package/dist/platform/binding.d.ts +37 -0
- package/dist/platform/binding.d.ts.map +1 -1
- package/dist/platform/binding.js +15 -0
- package/dist/platform/binding.js.map +1 -1
- package/dist/services/bridge-turn-journal.d.ts +63 -0
- package/dist/services/bridge-turn-journal.d.ts.map +1 -0
- package/dist/services/bridge-turn-journal.js +149 -0
- package/dist/services/bridge-turn-journal.js.map +1 -0
- package/dist/services/bridge-turn-queue.d.ts +8 -1
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +2 -1
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/codex-app-runner-protocol.d.ts +5 -0
- package/dist/services/codex-app-runner-protocol.d.ts.map +1 -1
- package/dist/services/codex-app-runner-protocol.js +5 -0
- package/dist/services/codex-app-runner-protocol.js.map +1 -1
- package/dist/services/customization-store.d.ts +93 -0
- package/dist/services/customization-store.d.ts.map +1 -0
- package/dist/services/customization-store.js +542 -0
- package/dist/services/customization-store.js.map +1 -0
- package/dist/services/file-bridge-path.d.ts.map +1 -1
- package/dist/services/file-bridge-path.js +3 -0
- package/dist/services/file-bridge-path.js.map +1 -1
- package/dist/services/grok-transcript.d.ts.map +1 -1
- package/dist/services/grok-transcript.js +44 -9
- package/dist/services/grok-transcript.js.map +1 -1
- package/dist/services/local-cli-opener.d.ts.map +1 -1
- package/dist/services/local-cli-opener.js +12 -3
- package/dist/services/local-cli-opener.js.map +1 -1
- package/dist/services/model-catalog.d.ts +54 -0
- package/dist/services/model-catalog.d.ts.map +1 -0
- package/dist/services/model-catalog.js +147 -0
- package/dist/services/model-catalog.js.map +1 -0
- package/dist/services/omp-transcript.d.ts +27 -0
- package/dist/services/omp-transcript.d.ts.map +1 -0
- package/dist/services/omp-transcript.js +214 -0
- package/dist/services/omp-transcript.js.map +1 -0
- package/dist/services/platform-team-store.d.ts +16 -8
- package/dist/services/platform-team-store.d.ts.map +1 -1
- package/dist/services/platform-team-store.js +17 -10
- package/dist/services/platform-team-store.js.map +1 -1
- package/dist/services/project-scanner.d.ts.map +1 -1
- package/dist/services/project-scanner.js +13 -8
- package/dist/services/project-scanner.js.map +1 -1
- package/dist/services/resumable-session-discovery.d.ts.map +1 -1
- package/dist/services/resumable-session-discovery.js +117 -35
- package/dist/services/resumable-session-discovery.js.map +1 -1
- package/dist/services/structured-bridge-clis.d.ts +5 -3
- package/dist/services/structured-bridge-clis.d.ts.map +1 -1
- package/dist/services/structured-bridge-clis.js +5 -1
- package/dist/services/structured-bridge-clis.js.map +1 -1
- package/dist/services/vc-meeting-action-gate.d.ts.map +1 -1
- package/dist/services/vc-meeting-action-gate.js +15 -4
- package/dist/services/vc-meeting-action-gate.js.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts +46 -66
- package/dist/services/vc-meeting-consumer-profile-bootstrap.d.ts.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-bootstrap.js +33 -292
- package/dist/services/vc-meeting-consumer-profile-bootstrap.js.map +1 -1
- package/dist/services/vc-meeting-consumer-profile-templates.js +3 -3
- package/dist/services/vc-meeting-consumer-profile-templates.js.map +1 -1
- package/dist/services/vc-meeting-delivery-store.d.ts +18 -0
- package/dist/services/vc-meeting-delivery-store.d.ts.map +1 -1
- package/dist/services/vc-meeting-delivery-store.js +55 -0
- package/dist/services/vc-meeting-delivery-store.js.map +1 -1
- package/dist/services/vc-meeting-send-policy.d.ts +21 -1
- package/dist/services/vc-meeting-send-policy.d.ts.map +1 -1
- package/dist/services/vc-meeting-send-policy.js +30 -2
- package/dist/services/vc-meeting-send-policy.js.map +1 -1
- package/dist/services/vc-meeting-shared-consumer-catalog-store.d.ts +78 -0
- package/dist/services/vc-meeting-shared-consumer-catalog-store.d.ts.map +1 -0
- package/dist/services/vc-meeting-shared-consumer-catalog-store.js +227 -0
- package/dist/services/vc-meeting-shared-consumer-catalog-store.js.map +1 -0
- package/dist/services/vc-meeting-shared-consumer-catalog.d.ts +53 -0
- package/dist/services/vc-meeting-shared-consumer-catalog.d.ts.map +1 -0
- package/dist/services/vc-meeting-shared-consumer-catalog.js +161 -0
- package/dist/services/vc-meeting-shared-consumer-catalog.js.map +1 -0
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +1 -0
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +20 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +27 -0
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/skills/customization-bundle.d.ts +83 -0
- package/dist/skills/customization-bundle.d.ts.map +1 -0
- package/dist/skills/customization-bundle.js +214 -0
- package/dist/skills/customization-bundle.js.map +1 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +10 -3
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/effective-builtins.d.ts +38 -0
- package/dist/skills/effective-builtins.d.ts.map +1 -0
- package/dist/skills/effective-builtins.js +96 -0
- package/dist/skills/effective-builtins.js.map +1 -0
- package/dist/skills/injection-mode.d.ts +4 -1
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +11 -3
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +24 -1
- package/dist/skills/installer.js.map +1 -1
- package/dist/skills/prompt-fragments.d.ts +58 -0
- package/dist/skills/prompt-fragments.d.ts.map +1 -0
- package/dist/skills/prompt-fragments.js +88 -0
- package/dist/skills/prompt-fragments.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.d.ts +50 -0
- package/dist/utils/cli-usage-limit.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.js +43 -0
- package/dist/utils/cli-usage-limit.js.map +1 -1
- package/dist/utils/idle-detector.d.ts +12 -0
- package/dist/utils/idle-detector.d.ts.map +1 -1
- package/dist/utils/idle-detector.js +88 -2
- package/dist/utils/idle-detector.js.map +1 -1
- package/dist/utils/stuck-detector.d.ts +5 -0
- package/dist/utils/stuck-detector.d.ts.map +1 -1
- package/dist/utils/stuck-detector.js +41 -0
- package/dist/utils/stuck-detector.js.map +1 -1
- package/dist/utils/usage-limit-tracker.d.ts +46 -0
- package/dist/utils/usage-limit-tracker.d.ts.map +1 -0
- package/dist/utils/usage-limit-tracker.js +91 -0
- package/dist/utils/usage-limit-tracker.js.map +1 -0
- package/dist/utils/user-token.js +1 -1
- package/dist/utils/user-token.js.map +1 -1
- package/dist/worker.js +812 -125
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +14 -14
- package/dist/workflows/events/payloads.d.ts +12 -12
- package/dist/workflows/events/schema.d.ts +164 -164
- package/dist/workflows/migration/v2-to-v3.js +1 -1
- package/dist/workflows/migration/v2-to-v3.js.map +1 -1
- package/dist/workflows/v3/architect.d.ts.map +1 -1
- package/dist/workflows/v3/architect.js +11 -5
- package/dist/workflows/v3/architect.js.map +1 -1
- package/dist/workflows/v3/artifact-contract-declarations.d.ts +22 -0
- package/dist/workflows/v3/artifact-contract-declarations.d.ts.map +1 -0
- package/dist/workflows/v3/artifact-contract-declarations.js +95 -0
- package/dist/workflows/v3/artifact-contract-declarations.js.map +1 -0
- package/dist/workflows/v3/contract.d.ts +1 -0
- package/dist/workflows/v3/contract.d.ts.map +1 -1
- package/dist/workflows/v3/contract.js.map +1 -1
- package/dist/workflows/v3/daemon-run.d.ts +2 -2
- package/dist/workflows/v3/daemon-run.d.ts.map +1 -1
- package/dist/workflows/v3/daemon-run.js +4 -0
- package/dist/workflows/v3/daemon-run.js.map +1 -1
- package/dist/workflows/v3/dag.d.ts +7 -0
- package/dist/workflows/v3/dag.d.ts.map +1 -1
- package/dist/workflows/v3/dag.js +69 -8
- package/dist/workflows/v3/dag.js.map +1 -1
- package/dist/workflows/v3/ephemeral-pool.js +1 -1
- package/dist/workflows/v3/ephemeral-pool.js.map +1 -1
- package/dist/workflows/v3/event-contract.d.ts +3 -1
- package/dist/workflows/v3/event-contract.d.ts.map +1 -1
- package/dist/workflows/v3/host.d.ts.map +1 -1
- package/dist/workflows/v3/host.js +5 -1
- package/dist/workflows/v3/host.js.map +1 -1
- package/dist/workflows/v3/journal.d.ts +1 -1
- package/dist/workflows/v3/journal.d.ts.map +1 -1
- package/dist/workflows/v3/journal.js.map +1 -1
- package/dist/workflows/v3/library-materialize.d.ts.map +1 -1
- package/dist/workflows/v3/library-materialize.js +9 -1
- package/dist/workflows/v3/library-materialize.js.map +1 -1
- package/dist/workflows/v3/library-schema.d.ts +3 -1
- package/dist/workflows/v3/library-schema.d.ts.map +1 -1
- package/dist/workflows/v3/library-schema.js +20 -4
- package/dist/workflows/v3/library-schema.js.map +1 -1
- package/dist/workflows/v3/library-store.d.ts.map +1 -1
- package/dist/workflows/v3/library-store.js +15 -3
- package/dist/workflows/v3/library-store.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +16 -16
- package/dist/workflows/v3/shared-node-runtime.d.ts +2 -1
- package/dist/workflows/v3/shared-node-runtime.d.ts.map +1 -1
- package/dist/workflows/v3/shared-node-runtime.js +48 -7
- package/dist/workflows/v3/shared-node-runtime.js.map +1 -1
- package/package.json +2 -1
- package/dist/dashboard-web/chunks/agent-workbench-dock-page-IKAC5CFL.js +0 -1
- package/dist/dashboard-web/chunks/agent-workbench-page-M53UN5EB.js +0 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-YKE5SDZF.js +0 -10
- package/dist/dashboard-web/chunks/chunk-22WIADUT.js +0 -2
- package/dist/dashboard-web/chunks/chunk-24ONYQMR.js +0 -1
- package/dist/dashboard-web/chunks/chunk-2W6SCN3M.js +0 -1
- package/dist/dashboard-web/chunks/chunk-4XNJX6B3.js +0 -1
- package/dist/dashboard-web/chunks/chunk-5THX4DI2.js +0 -66
- package/dist/dashboard-web/chunks/chunk-7SZRL6FY.js +0 -1
- package/dist/dashboard-web/chunks/chunk-AGCPF6MT.js +0 -1
- package/dist/dashboard-web/chunks/chunk-FXW3V4JR.js +0 -1
- package/dist/dashboard-web/chunks/chunk-SYC6D5XU.js +0 -1
- package/dist/dashboard-web/chunks/connectors-page-VOISXXII.js +0 -1
- package/dist/dashboard-web/chunks/feedback-page-RN6HRZMS.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-UQT6DCB7.js +0 -8
- package/dist/dashboard-web/chunks/monitoring-page-NKEKVQAP.js +0 -2
- package/dist/dashboard-web/chunks/office-page-GVJF73XW.js +0 -1
- package/dist/dashboard-web/chunks/overview-page-KHLV6VTI.js +0 -1
- package/dist/dashboard-web/chunks/plugin-page-J3OF5IY4.js +0 -1
- package/dist/dashboard-web/chunks/roles-page-WQIRSOLZ.js +0 -1
- package/dist/dashboard-web/chunks/schedules-page-J2DEY2X6.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-7BQFEA5P.js +0 -8
- package/dist/dashboard-web/chunks/settings-page-BJEIOFJH.js +0 -2
- package/dist/dashboard-web/chunks/skills-page-HGIFR3IU.js +0 -4
- package/dist/dashboard-web/chunks/team-federation-page-MR5ZPL4B.js +0 -1
- package/dist/dashboard-web/chunks/v3-page-GBR5JWPE.js +0 -94
- package/dist/dashboard-web/chunks/whiteboards-page-QAZM4BN3.js +0 -1
- package/dist/services/vc-meeting-consumer-profile-store.d.ts +0 -39
- package/dist/services/vc-meeting-consumer-profile-store.d.ts.map +0 -1
- package/dist/services/vc-meeting-consumer-profile-store.js +0 -191
- package/dist/services/vc-meeting-consumer-profile-store.js.map +0 -1
package/dist/worker.js
CHANGED
|
@@ -15,15 +15,16 @@
|
|
|
15
15
|
import { createHash, randomBytes } from 'node:crypto';
|
|
16
16
|
import { accessSync, chmodSync, mkdirSync, writeFileSync, unlinkSync, rmdirSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, constants as fsConstants } from 'node:fs';
|
|
17
17
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
18
|
-
import { join, basename, dirname, delimiter } from 'node:path';
|
|
18
|
+
import { join, basename, dirname, delimiter, relative } from 'node:path';
|
|
19
19
|
import { resolveBotmuxWrapperBinDir, prependBotmuxBin } from './core/botmux-wrapper.js';
|
|
20
20
|
import { homedir, tmpdir, userInfo } from 'node:os';
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
22
|
import { evaluateCredentialOnlyIsolationGate, credentialIsolationRequired, deviceCredentialIsolationMarkerPath, isCredentialIsolationReservedBasename, buildCredentialIsolationRules, buildSeatbeltProfile, isolatedPaneOriginChannel, isolatedPaneReattachSafe, evaluatePersistentPaneMigration, executePersistentPaneMigration, persistentTeardownKillKind, isolationPaneMarkerPath, policyOffTombstonePath, policyOffTombstoneContent, policyOffTombstoneValid, provenancePendingContent, provenancePendingNonce, sendCredFilePath, botHomePath, buildCliExecutableReadCarveOuts, isolationPaneMarkerContent, isolationPanePolicyDigest, } from './adapters/cli/read-isolation.js';
|
|
23
|
-
import { buildFsPolicy, compileToSeatbelt, migrateLegacySandboxFields, resolveRedirectedAdapterAuthPaths, FsPolicyConfigError } from './adapters/cli/fs-policy.js';
|
|
23
|
+
import { buildFsPolicy, compileToSeatbelt, migrateLegacySandboxFields, resolveRedirectedAdapterAuthPaths, resolveLarkCliLinuxStoreDir, larkCliChildDataRoot, FsPolicyConfigError } from './adapters/cli/fs-policy.js';
|
|
24
24
|
import { buildCloseResultMessage, normalizeDestroyResult, mayRestoreWriteAdmission, interpretAbortOutcome, classifyRestartTeardown } from './adapters/backend/destroy-result.js';
|
|
25
25
|
import { isRemoteBackendType, killPersistentBackendTarget, killPersistentSession, probePersistentBackendTarget, probePersistentSession, shouldRejectPersistentPostKillProbe } from './core/persistent-backend.js';
|
|
26
26
|
import { finalizeRawCommandDelivery, writeRawCommandLine } from './core/raw-command-writer.js';
|
|
27
|
+
import { decodeTerminalWriteFrameSource, terminalWriteFrame, } from './core/terminal-write-frame.js';
|
|
27
28
|
import { rawCommandWriteOptionsFor } from './core/raw-command-write-options.js';
|
|
28
29
|
import { publishCliSessionIdToDaemon } from './core/cli-session-id-publisher.js';
|
|
29
30
|
import { readProcessStartIdentity } from './core/session-marker.js';
|
|
@@ -46,20 +47,24 @@ import { remoteWorkerShutdownInputBlocker } from './core/remote-worker-shutdown-
|
|
|
46
47
|
import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
|
|
47
48
|
import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
|
|
48
49
|
import { sanitizePerBotEnv } from './core/per-bot-env.js';
|
|
50
|
+
import { normalizeExistingAppServerEndpoint } from './core/existing-app-server.js';
|
|
49
51
|
import { resolveChildBotsConfig } from './core/config-dir.js';
|
|
50
52
|
import { evaluateVcMeetingManagedSend, } from './services/vc-meeting-send-policy.js';
|
|
51
53
|
import { TurnTerminalDeduper } from './services/turn-terminal-deduper.js';
|
|
54
|
+
import { appendBridgeTurnJournalEntry, BridgeRestoreGate, clearBridgeTurnJournal, readBridgeTurnJournal, removeBridgeTurnJournalEntry, selectRestorableBridgeTurns, writeBridgeTurnJournal, } from './services/bridge-turn-journal.js';
|
|
52
55
|
import { defaultGatewayEntry, ensureGatewayEntry } from './core/plugins/mcp/gateway-installer.js';
|
|
53
|
-
import { sessionMcpGatewayPathRegex, startSessionMcpGatewayHost, } from './core/plugins/mcp/host.js';
|
|
56
|
+
import { sessionMcpGatewayPathRegex, sessionMcpGatewaySocketPath, startSessionMcpGatewayHost, } from './core/plugins/mcp/host.js';
|
|
57
|
+
import { clearMcpGatewayLaunchRecord, mcpGatewayPaneReattachSafe, readMcpGatewayLaunchRecord, writeMcpGatewayLaunchRecord, } from './core/plugins/mcp/launch-record.js';
|
|
54
58
|
import { MCP_GATEWAY_REQUIRED_ENV, MCP_GATEWAY_SOCKET_ENV, } from './core/plugins/mcp/environment.js';
|
|
55
59
|
import { readSessionMcpRuntimeManifest, sessionMcpRuntimeHostOnlyPaths, } from './core/plugins/mcp/session-runtime.js';
|
|
56
60
|
import { prepareCliPluginGeneration } from './core/plugins/cli-generation.js';
|
|
57
61
|
import { loadBotConfigs, resolveBrandLabel, resolveUsageDisplay, } from './bot-registry.js';
|
|
58
62
|
import { readGlobalConfig, isWorkflowFeatureEnabled } from './global-config.js';
|
|
59
|
-
import { deriveTerminalWriteToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, } from './core/terminal-write-auth.js';
|
|
63
|
+
import { deriveTerminalWriteToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, TERMINAL_PLATFORM_READONLY_HINT_HEADER, } from './core/terminal-write-auth.js';
|
|
60
64
|
import { deriveWorkerViewGeneration, looksLikeTerminalControlGrant, TERMINAL_VIEW_FORWARD_HEADER, verifyTerminalControlGrant, verifyTerminalViewForward, } from './core/terminal-control-grant.js';
|
|
61
65
|
import { appendControlAudit, controlAuditRecord } from './dashboard/control-audit.js';
|
|
62
66
|
import { readPlatformBinding } from './platform/binding.js';
|
|
67
|
+
import { buildPlatformDashboardLoginUrl } from './core/dashboard-url.js';
|
|
63
68
|
import { loadDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
|
|
64
69
|
import { InflightInputTracker } from './core/inflight-input-tracker.js';
|
|
65
70
|
import { InputTurnDeduper } from './core/input-turn-dedupe.js';
|
|
@@ -78,6 +83,7 @@ import { currentHermesStateOffset, drainHermesStateDb, resolveHermesStateDbPath
|
|
|
78
83
|
import { filterHermesEventsForBotmuxSession } from './services/hermes-session-filter.js';
|
|
79
84
|
import { currentMtrSessionOffset, drainMtrSession, findLatestMtrSessionByDirectory, findMtrSessionById } from './services/mtr-transcript.js';
|
|
80
85
|
import { drainPiTranscript } from './services/pi-transcript.js';
|
|
86
|
+
import { drainOmpTranscript } from './services/omp-transcript.js';
|
|
81
87
|
import { drainGrokUpdates, findGrokSessionByPid, grokSessionIdFromPath, } from './services/grok-transcript.js';
|
|
82
88
|
import { resolveFileBridgePath } from './services/file-bridge-path.js';
|
|
83
89
|
import { isStructuredBridgeAdoptIdleCli, isStructuredBridgeAdoptInputCli, isStructuredBridgeFallbackActive, isStructuredBridgeLifecycleBlockingCli, } from './services/structured-bridge-clis.js';
|
|
@@ -93,6 +99,7 @@ import { listenWebTerminalWithFallback } from './utils/web-terminal-listen.js';
|
|
|
93
99
|
import { HerdrWebTerminalBinding } from './utils/herdr-web-terminal-binding.js';
|
|
94
100
|
import { TERMINAL_FAVICON_DATA_URI } from './utils/terminal-favicon.js';
|
|
95
101
|
import { t, setDefaultLocale } from './i18n/index.js';
|
|
102
|
+
import { registerPromptOverrideResolver } from './skills/effective-builtins.js';
|
|
96
103
|
import { TerminalRenderer } from './utils/terminal-renderer.js';
|
|
97
104
|
import { DEFAULT_RENDER_COLS, DEFAULT_RENDER_ROWS, MAX_RENDER_COLS, MAX_RENDER_ROWS, MIN_RENDER_COLS, MIN_RENDER_ROWS, clamp, resolveRenderDimensions, } from './utils/render-dimensions.js';
|
|
98
105
|
import { createCliAdapterSync, locateOnPath } from './adapters/cli/registry.js';
|
|
@@ -104,6 +111,7 @@ import { delay } from './utils/timing.js';
|
|
|
104
111
|
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, syncClaudeResumeTargetToCwd, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
|
|
105
112
|
import { sessionReadyHookCommand } from './adapters/hook-command.js';
|
|
106
113
|
import { mtrSessionIdForBotmuxSession } from './adapters/cli/mtr.js';
|
|
114
|
+
import { ompSessionDir } from './adapters/cli/oh-my-pi.js';
|
|
107
115
|
import { strictInputHandle } from './adapters/cli/strict-input-handle.js';
|
|
108
116
|
import { HerdrBackend } from './adapters/backend/herdr-backend.js';
|
|
109
117
|
import { TmuxBackend } from './adapters/backend/tmux-backend.js';
|
|
@@ -127,7 +135,7 @@ import { tmuxEnv, probeTmuxFunctionalWithRetry } from './setup/ensure-tmux.js';
|
|
|
127
135
|
import { probeZmxVersion } from './setup/ensure-zmx.js';
|
|
128
136
|
import { tmuxRestartJitterMs } from './core/tmux-recovery.js';
|
|
129
137
|
import { IdleDetector } from './utils/idle-detector.js';
|
|
130
|
-
import { StuckDetector, matchHookReviewScreen } from './utils/stuck-detector.js';
|
|
138
|
+
import { StuckDetector, matchHookReviewScreen, shouldHoldInputForHookReview, } from './utils/stuck-detector.js';
|
|
131
139
|
import { processStuckWarningTuiKeys, shouldRearmStuckDetector } from './utils/stuck-key-guard.js';
|
|
132
140
|
import { sendTuiKeySequence, submitTuiTextInput } from './utils/tui-input-delivery.js';
|
|
133
141
|
import { captureToPng } from './utils/screenshot-renderer.js';
|
|
@@ -135,7 +143,8 @@ import { snapshotToPng, snapshotToText, shouldCaptureScreen, isScreenSelfDriven
|
|
|
135
143
|
import { chooseWebTerminalSeed } from './utils/web-terminal-seed.js';
|
|
136
144
|
import { mergeHerdrWebSnapshot, renderHerdrWebHistory, } from './utils/herdr-web-history.js';
|
|
137
145
|
import { parseWorkerRequestUrl } from './utils/worker-http.js';
|
|
138
|
-
import {
|
|
146
|
+
import { structuredRateLimitState, isStructuredRateLimitAuthoritative } from './utils/cli-usage-limit.js';
|
|
147
|
+
import { createUsageLimitTracker } from './utils/usage-limit-tracker.js';
|
|
139
148
|
import { uploadImageBuffer } from './utils/lark-upload.js';
|
|
140
149
|
import { applySessionOwnerEnv, redactChildEnv, scrubClaudeSessionMarkerEnv, scrubSessionCliHomeEnv } from './utils/child-env.js';
|
|
141
150
|
import { decideSubmitConfirmationAction, settleDeferredSubmitConfirmation, settleStaleWriteContinuation, } from './services/submit-confirmation.js';
|
|
@@ -150,7 +159,7 @@ import { fetchDaemonIpc } from './core/daemon-ipc-auth.js';
|
|
|
150
159
|
import { withCodexAppContext } from './utils/codex-app-context.js';
|
|
151
160
|
import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.js';
|
|
152
161
|
import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
|
|
153
|
-
import { normalizeCodexAppLifecycleEvent, normalizeFinalUsage, } from './services/codex-app-runner-protocol.js';
|
|
162
|
+
import { CODEX_APP_ACTIVE_WRITER_EXIT_CODE, normalizeCodexAppLifecycleEvent, normalizeFinalUsage, } from './services/codex-app-runner-protocol.js';
|
|
154
163
|
import { hasMatchingManagedOriginCapability, ensureManagedOriginAttestationDirectory, ensureManagedOriginCapabilityLeafSafe, ensureManagedOriginDataRootProbe, ensureManagedOriginIsolationSentinel, ensureManagedOriginRootLocator, managedOriginLegacyIsolationProbeAccess, managedOriginDataRootProbeAccess, managedOriginIsolationSentinelAccess, managedOriginAttestationDirectory, managedOriginCapabilityDirectory, managedOriginCapabilityPath, managedOriginRootLocatorPath, readManagedOriginAuthorityFile, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, sweepManagedOriginAttestationProofs, } from './core/managed-origin-capability.js';
|
|
155
164
|
import { CodexRpcEngine, } from './codex-rpc-engine.js';
|
|
156
165
|
import { beginRuntimeWriteCycle, isCliBackendGenerationCurrent, PtyOutputGeneration, projectRuntimeScreenStatus, } from './utils/runtime-screen-status.js';
|
|
@@ -1172,16 +1181,38 @@ async function prepareCliPluginGenerationAndGateway(cfg, adapter) {
|
|
|
1172
1181
|
const manifest = readSessionMcpRuntimeManifest(cfg.sessionId, config.session.dataDir);
|
|
1173
1182
|
stopSessionMcpGatewayHost();
|
|
1174
1183
|
if (adapter.mcpGateway && manifest?.entries.length) {
|
|
1175
|
-
|
|
1176
|
-
sessionId: cfg.sessionId,
|
|
1177
|
-
dataDir: config.session.dataDir,
|
|
1178
|
-
trustedTurnIdentity: currentGatewayTrustedTurnIdentity,
|
|
1179
|
-
onError: error => log(`[mcp-gateway] host error: ${error.message}`),
|
|
1180
|
-
});
|
|
1184
|
+
await startAndRecordSessionMcpGatewayHost(cfg.sessionId);
|
|
1181
1185
|
log(`[mcp-gateway] trusted host listening for ${manifest.entries.length} plugin server(s)`);
|
|
1182
1186
|
}
|
|
1187
|
+
else {
|
|
1188
|
+
clearMcpGatewayLaunchRecord(config.session.dataDir, cfg.sessionId);
|
|
1189
|
+
}
|
|
1183
1190
|
return manifest;
|
|
1184
1191
|
}
|
|
1192
|
+
/** Bind the trusted MCP Gateway host at the session's DETERMINISTIC socket path
|
|
1193
|
+
* and persist its launch record. Deliberately does NOT refresh the plugin/Skill
|
|
1194
|
+
* generation: prepareCliPluginGenerationAndGateway does that for fresh/resumed
|
|
1195
|
+
* spawns, whereas a warm reattach to a surviving pane keeps the CLI's existing
|
|
1196
|
+
* catalog untouched — but the host itself died with the previous worker, so the
|
|
1197
|
+
* reattach path must still re-serve it here so the pane's surviving relay can
|
|
1198
|
+
* reconnect to the same path (see spawnCli's paneRelayReattachSafe branch).
|
|
1199
|
+
* The caller owns stopping any prior host first (prepare*) or gating on absence
|
|
1200
|
+
* (reattach). Token rotation + stale-socket removal are handled inside
|
|
1201
|
+
* startSessionMcpGatewayHost. The trusted-turn-identity provider (#917) is
|
|
1202
|
+
* wired here so BOTH the fresh/resumed host and the re-served reattach host
|
|
1203
|
+
* inject the per-turn host-signed caller — the reattach path must not lose it. */
|
|
1204
|
+
async function startAndRecordSessionMcpGatewayHost(sessionId) {
|
|
1205
|
+
sessionMcpGatewayHost = await startSessionMcpGatewayHost({
|
|
1206
|
+
sessionId,
|
|
1207
|
+
dataDir: config.session.dataDir,
|
|
1208
|
+
trustedTurnIdentity: currentGatewayTrustedTurnIdentity,
|
|
1209
|
+
onError: error => log(`[mcp-gateway] host error: ${error.message}`),
|
|
1210
|
+
});
|
|
1211
|
+
// The NEXT worker generation reads this record to decide whether a surviving
|
|
1212
|
+
// persistent pane can be reattached (relay reconnects to the same path)
|
|
1213
|
+
// instead of killed for a cold-resume.
|
|
1214
|
+
writeMcpGatewayLaunchRecord(config.session.dataDir, sessionId, sessionMcpGatewayHost.socketPath);
|
|
1215
|
+
}
|
|
1185
1216
|
/** v2 read isolation — provision a bot's PER-BOT config dir under its BOT_HOME so the
|
|
1186
1217
|
* CLI (redirected there via CLAUDE_CONFIG_DIR/CODEX_HOME) starts fully set up despite
|
|
1187
1218
|
* the global ~/.claude|~/.codex being Seatbelt-denied. Idempotent (guards on
|
|
@@ -1385,6 +1416,7 @@ const adoptWriteQueue = new AsyncSerialQueue();
|
|
|
1385
1416
|
* adapter's hermesBridgeAttach reads the correct mode. */
|
|
1386
1417
|
let lastSpawnEffectiveResume = false;
|
|
1387
1418
|
let lastSpawnEffectiveCliSessionId;
|
|
1419
|
+
let lastSpawnEffectiveAdapterSessionId;
|
|
1388
1420
|
let lastSpawnDeferInitialPrompt = false;
|
|
1389
1421
|
let lastSpawnQueuedInitialPrompt;
|
|
1390
1422
|
let lastSpawnQueuedInitialPromptLogicalContent;
|
|
@@ -1534,6 +1566,18 @@ function refreshTerminalWriteToken() {
|
|
|
1534
1566
|
if (secret && sessionId)
|
|
1535
1567
|
writeToken = deriveTerminalWriteToken(secret, sessionId);
|
|
1536
1568
|
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Owner-login URL for THIS session's read-only web terminal, or '' when the
|
|
1571
|
+
* machine is unbound / remote access is off (banner then shows non-link text).
|
|
1572
|
+
* `next=/s/<sessionId>` is what makes the round-trip land the owner back on a
|
|
1573
|
+
* writable terminal: the platform routes a `/s/`-prefixed next to the terminal
|
|
1574
|
+
* subdomain surface and mints the host-only proxy-session cookie there, which
|
|
1575
|
+
* is the credential the owner write-check reads (the cross-subdomain SSO cookie
|
|
1576
|
+
* alone does not). See buildPlatformDashboardLoginUrl.
|
|
1577
|
+
*/
|
|
1578
|
+
function deriveTerminalLoginUrl(sid) {
|
|
1579
|
+
return buildPlatformDashboardLoginUrl(`/s/${encodeURIComponent(sid)}`) ?? '';
|
|
1580
|
+
}
|
|
1537
1581
|
function resolveTerminalAccessForReq(req, url) {
|
|
1538
1582
|
// P1-6 invariant: an explicit, matching write-link `?token=` is an
|
|
1539
1583
|
// independent capability with the HIGHEST priority. It wins outright — before
|
|
@@ -1641,7 +1685,7 @@ let closeRequested = false;
|
|
|
1641
1685
|
let capturedSpawnCommand = null;
|
|
1642
1686
|
let deferredTopicOutputTail = '';
|
|
1643
1687
|
const reportedDeferredTopicRoots = new Set();
|
|
1644
|
-
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', opencode2: 'OpenCode 2', 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', reasonix: 'Reasonix', dsh: 'DeepSeek Harness', mojo: 'Mojo' };
|
|
1688
|
+
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', opencode2: 'OpenCode 2', 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', reasonix: 'Reasonix', dsh: 'DeepSeek Harness', 'dsh-tui': 'DeepSeek Harness TUI', mojo: 'Mojo' };
|
|
1645
1689
|
function cliName() {
|
|
1646
1690
|
return (lastInitConfig?.cliRuntime?.source === 'configured'
|
|
1647
1691
|
? (lastInitConfig.cliRuntime.displayName?.trim() || lastInitConfig.cliRuntime.id)
|
|
@@ -1756,6 +1800,38 @@ let readyPatternSeenDuringHold = false;
|
|
|
1756
1800
|
let awaitingPostSessionStartPromptEvidence = false;
|
|
1757
1801
|
/** Scoped marker set only by IdleDetector's screen-driven callback. */
|
|
1758
1802
|
let postSessionStartPromptEvidenceInFlight = false;
|
|
1803
|
+
/** A visible Codex Hook-review screen owns Enter/arrow keys. Keep queued IM
|
|
1804
|
+
* input out of that menu so an automated submit cannot accidentally select
|
|
1805
|
+
* “Trust all” or get consumed as menu navigation. */
|
|
1806
|
+
let hookReviewInputHold = false;
|
|
1807
|
+
let hookReviewInputHoldNotified = false;
|
|
1808
|
+
function refreshHookReviewInputHold(snapshot) {
|
|
1809
|
+
if (!snapshot.trim())
|
|
1810
|
+
return;
|
|
1811
|
+
const blocked = shouldHoldInputForHookReview(snapshot);
|
|
1812
|
+
if (blocked === hookReviewInputHold)
|
|
1813
|
+
return;
|
|
1814
|
+
hookReviewInputHold = blocked;
|
|
1815
|
+
if (blocked) {
|
|
1816
|
+
log('Hook review menu visible — holding queued input until the menu is resolved');
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
hookReviewInputHoldNotified = false;
|
|
1820
|
+
log('Hook review menu cleared — releasing queued input');
|
|
1821
|
+
if (hasPendingInputForFlush())
|
|
1822
|
+
queueMicrotask(() => { void flushPending(); });
|
|
1823
|
+
}
|
|
1824
|
+
function notifyHookReviewInputHold() {
|
|
1825
|
+
if (hookReviewInputHoldNotified)
|
|
1826
|
+
return;
|
|
1827
|
+
hookReviewInputHoldNotified = true;
|
|
1828
|
+
send({
|
|
1829
|
+
type: 'user_notify',
|
|
1830
|
+
turnId: currentBotmuxTurnId,
|
|
1831
|
+
message: '⚠️ Codex 正在显示 Hook 审核菜单。为避免消息误触“Trust all”或被菜单吞掉,'
|
|
1832
|
+
+ 'BotMux 已暂存后续消息;请先在终端完成 Hook 选择,恢复普通输入框后会自动投递。',
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1759
1835
|
/** Wait until the PTY has been quiet for READY_FLUSH_SETTLE_MS (Ink render
|
|
1760
1836
|
* drained), capped at READY_FLUSH_SETTLE_CAP_MS, then flush the held prompt.
|
|
1761
1837
|
* An authoritative direct ready command (Hermes) can mark prompt readiness;
|
|
@@ -1991,6 +2067,15 @@ let pendingSessionRename = null;
|
|
|
1991
2067
|
let nativeSessionTitleSyncInFlight;
|
|
1992
2068
|
let nativeSessionTitleAppliedThreadId;
|
|
1993
2069
|
let nativeSessionTitleRevision = 0;
|
|
2070
|
+
/** Every user-controlled input class that flushPending can deliver. Keep this
|
|
2071
|
+
* shared with review-menu release handling so a queued raw command, adopt
|
|
2072
|
+
* message, or native rename is not stranded after the menu disappears. */
|
|
2073
|
+
function hasPendingInputForFlush() {
|
|
2074
|
+
return pendingMessages.length > 0
|
|
2075
|
+
|| pendingAdoptMessages.length > 0
|
|
2076
|
+
|| pendingRawInputs.length > 0
|
|
2077
|
+
|| pendingSessionRename !== null;
|
|
2078
|
+
}
|
|
1994
2079
|
let nativeSessionTitleResumeUpdatedAt;
|
|
1995
2080
|
let nativeSessionTitleCurrentGenerationResume = false;
|
|
1996
2081
|
const nativeSessionTitleSyncAbortControllers = new Set();
|
|
@@ -3379,59 +3464,27 @@ function codexAppLivenessStatus(base, nowMs = Date.now()) {
|
|
|
3379
3464
|
function structuredRateLimitAuthoritative() {
|
|
3380
3465
|
return isStructuredRateLimitAuthoritative(cliAdapter);
|
|
3381
3466
|
}
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
: undefined;
|
|
3404
|
-
return turnSeq;
|
|
3405
|
-
},
|
|
3406
|
-
// Map a runtime status to a usage-limit-aware status, recording whether this
|
|
3407
|
-
// turn hit a limit (read back via detectedThisTurn).
|
|
3408
|
-
classify(content, status) {
|
|
3409
|
-
const detected = detectCliUsageLimit(content, undefined, { suppressRateKind: structuredRateLimitAuthoritative() });
|
|
3410
|
-
if (!detected.limited)
|
|
3411
|
-
return { status };
|
|
3412
|
-
const key = usageLimitStateKey(detected);
|
|
3413
|
-
if (detected.retryReady && key === suppressedRetryReadyKey) {
|
|
3414
|
-
return { status };
|
|
3415
|
-
}
|
|
3416
|
-
suppressedRetryReadyKey = undefined;
|
|
3417
|
-
detectedTurn = turnSeq;
|
|
3418
|
-
return { status: 'limited', usageLimit: detected };
|
|
3419
|
-
},
|
|
3420
|
-
detectedThisTurn(seq) {
|
|
3421
|
-
return detectedTurn === seq;
|
|
3422
|
-
},
|
|
3423
|
-
// Record a limit that came from a STRUCTURED signal (transcript error
|
|
3424
|
-
// record) rather than screen text. Mirrors classify()'s state writes so
|
|
3425
|
-
// the tracker stays coherent: mark this turn as having hit a limit (read
|
|
3426
|
-
// by detectedThisTurn for the submit-confirmation recheck) and clear any
|
|
3427
|
-
// stale retry-ready suppression. The actual emit is done by the caller.
|
|
3428
|
-
noteStructuredLimit() {
|
|
3429
|
-
suppressedRetryReadyKey = undefined;
|
|
3430
|
-
detectedTurn = turnSeq;
|
|
3431
|
-
},
|
|
3432
|
-
};
|
|
3433
|
-
}
|
|
3434
|
-
const usageLimitTracker = createUsageLimitTracker();
|
|
3467
|
+
/**
|
|
3468
|
+
* PTY-quiescence window for the usage-limit active-work gate. A `working`
|
|
3469
|
+
* status alone does not prove output is progressing (it is the default
|
|
3470
|
+
* projection whenever promptReady === false), so the screen-scan suppression
|
|
3471
|
+
* only applies while PTY output is this fresh. A non-structured CLI blocked at
|
|
3472
|
+
* a rate/quota error screen that never renders its ready prompt goes silent
|
|
3473
|
+
* past this window and is correctly detected instead of being suppressed
|
|
3474
|
+
* forever. Aligns with the StuckDetector's PTY-quiescence threshold.
|
|
3475
|
+
*/
|
|
3476
|
+
const USAGE_LIMIT_OUTPUT_ACTIVE_WINDOW_MS = 15_000;
|
|
3477
|
+
function isOutputActivelyProgressing() {
|
|
3478
|
+
return Date.now() - lastPtyActivityAtMs < USAGE_LIMIT_OUTPUT_ACTIVE_WINDOW_MS;
|
|
3479
|
+
}
|
|
3480
|
+
// Per-turn usage-limit state machine (extracted to utils/usage-limit-tracker
|
|
3481
|
+
// for unit testing). The structured-limit stickiness it owns is load-bearing:
|
|
3482
|
+
// a one-shot transcript emit must survive the daemon's working-frame self-heal
|
|
3483
|
+
// while the CLI stays genuinely blocked.
|
|
3484
|
+
const usageLimitTracker = createUsageLimitTracker({
|
|
3485
|
+
isRateKindSuppressed: structuredRateLimitAuthoritative,
|
|
3486
|
+
isOutputActive: isOutputActivelyProgressing,
|
|
3487
|
+
});
|
|
3435
3488
|
function currentUsageLimitSnapshot() {
|
|
3436
3489
|
if (!backendScreenEvidenceIsAuthoritativeForMutation())
|
|
3437
3490
|
return '';
|
|
@@ -3505,6 +3558,19 @@ const bridgeSecondaryPaths = new Map(); // path → offset
|
|
|
3505
3558
|
let bridgeOffset = 0;
|
|
3506
3559
|
let bridgePendingTail = '';
|
|
3507
3560
|
const bridgeQueue = new BridgeTurnQueue();
|
|
3561
|
+
/** Journal-restore of interrupted Lark turns is allowed AT MOST ONCE per worker
|
|
3562
|
+
* process, and only on the FIRST baseline this process runs. Rationale: the
|
|
3563
|
+
* journal exists to recover a turn that a *cross-process* death (daemon
|
|
3564
|
+
* restart / worker crash) orphaned — there the whole worker is new and no
|
|
3565
|
+
* other recovery owner survives. An *in-worker* CLI restart (crash-respawn,
|
|
3566
|
+
* cwd-move, durable lease expiry) keeps this process alive, so its
|
|
3567
|
+
* InflightInputTracker carryover already re-delivers the interrupted plain-IM
|
|
3568
|
+
* input (a full fresh answer) — letting the journal ALSO restore that turn
|
|
3569
|
+
* would post the recovered partial AND the fresh answer (same turnId, different
|
|
3570
|
+
* lastUuid → daemon dedupe can't collapse them). Armed on every watcher
|
|
3571
|
+
* teardown so restore is confined to the one baseline with no competing
|
|
3572
|
+
* recovery owner. */
|
|
3573
|
+
const bridgeRestoreGate = new BridgeRestoreGate();
|
|
3508
3574
|
/** uuids of structured transcript rate-limit records already turned into a
|
|
3509
3575
|
* `limited` emit. Readers may re-read from offset 0 after rotation, so the
|
|
3510
3576
|
* stable record uuid keeps the notification idempotent. */
|
|
@@ -3538,6 +3604,10 @@ let activeRuntimePublished = false;
|
|
|
3538
3604
|
const codexBridgeQueue = new CodexBridgeQueue();
|
|
3539
3605
|
let codexBridgeWatcher = null;
|
|
3540
3606
|
let codexBridgeTimer = null;
|
|
3607
|
+
let ompBridgeState = {};
|
|
3608
|
+
let ompQuietCandidateKey;
|
|
3609
|
+
let ompQuietCandidateCompleteOffset;
|
|
3610
|
+
const ompRetiredTranscriptPaths = new Set();
|
|
3541
3611
|
/** Settings are observed on the same append-only cursor as bridge output.
|
|
3542
3612
|
* The tracker owns rollout-generation clear/update semantics and publishes a
|
|
3543
3613
|
* dedicated IPC event, independent of PTY redraw frequency. */
|
|
@@ -3666,6 +3736,46 @@ function bridgeMarkerPath() {
|
|
|
3666
3736
|
return undefined;
|
|
3667
3737
|
return join(process.env.SESSION_DATA_DIR, 'turn-sends', `${sessionId}.jsonl`);
|
|
3668
3738
|
}
|
|
3739
|
+
/** Durable journal of pending Lark bridge turns (services/bridge-turn-journal).
|
|
3740
|
+
* Written at mark time, cleared at every turn-terminal outcome; consulted by
|
|
3741
|
+
* bridgeAbsorbBaseline after a restart so an interrupted turn's output can
|
|
3742
|
+
* still reach Lark. Sibling of the turn-sends marker file. */
|
|
3743
|
+
function bridgeTurnJournalFilePath() {
|
|
3744
|
+
if (!process.env.SESSION_DATA_DIR || !sessionId)
|
|
3745
|
+
return undefined;
|
|
3746
|
+
return join(process.env.SESSION_DATA_DIR, 'turn-marks', `${sessionId}.json`);
|
|
3747
|
+
}
|
|
3748
|
+
function journalBridgeTurnMark(entry) {
|
|
3749
|
+
const path = bridgeTurnJournalFilePath();
|
|
3750
|
+
if (!path)
|
|
3751
|
+
return;
|
|
3752
|
+
try {
|
|
3753
|
+
appendBridgeTurnJournalEntry(path, entry);
|
|
3754
|
+
}
|
|
3755
|
+
catch (err) {
|
|
3756
|
+
log(`Bridge turn journal write failed (${err.message}) — restart recovery unavailable for turn ${entry.turnId.substring(0, 8)}`);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
function journalBridgeTurnClear(turnId, dispatchAttempt) {
|
|
3760
|
+
const path = bridgeTurnJournalFilePath();
|
|
3761
|
+
if (!path)
|
|
3762
|
+
return;
|
|
3763
|
+
try {
|
|
3764
|
+
removeBridgeTurnJournalEntry(path, turnId, dispatchAttempt);
|
|
3765
|
+
}
|
|
3766
|
+
catch (err) {
|
|
3767
|
+
log(`Bridge turn journal clear failed (${err.message}) for turn ${turnId.substring(0, 8)}`);
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
function clearBridgeTurnJournalFile() {
|
|
3771
|
+
const path = bridgeTurnJournalFilePath();
|
|
3772
|
+
if (!path)
|
|
3773
|
+
return;
|
|
3774
|
+
try {
|
|
3775
|
+
clearBridgeTurnJournal(path);
|
|
3776
|
+
}
|
|
3777
|
+
catch { /* best-effort — session is closing */ }
|
|
3778
|
+
}
|
|
3669
3779
|
function readSendMarkers() {
|
|
3670
3780
|
if (closeRequested)
|
|
3671
3781
|
return [];
|
|
@@ -3916,6 +4026,13 @@ function bridgeAbsorbBaseline() {
|
|
|
3916
4026
|
if (!bridgeJsonlPath)
|
|
3917
4027
|
return;
|
|
3918
4028
|
if (!lastInitConfig?.adoptMode) {
|
|
4029
|
+
// Restart recovery: if the previous generation left pending Lark turns in
|
|
4030
|
+
// the durable journal (worker/daemon died mid-turn), re-mark them and
|
|
4031
|
+
// baseline BEHIND their recorded offsets instead of skipping to EOF — the
|
|
4032
|
+
// interrupted turn's user line and partial assistant output are then
|
|
4033
|
+
// re-attributed and delivered through the normal fallback gate.
|
|
4034
|
+
if (tryRestoreInterruptedBridgeTurns())
|
|
4035
|
+
return;
|
|
3919
4036
|
const cursor = baselineJsonlCursor(bridgeJsonlPath);
|
|
3920
4037
|
bridgeOffset = cursor.newOffset;
|
|
3921
4038
|
bridgePendingTail = cursor.pendingTail;
|
|
@@ -3935,6 +4052,72 @@ function bridgeAbsorbBaseline() {
|
|
|
3935
4052
|
if (lastInitConfig?.adoptMode)
|
|
3936
4053
|
maybeEmitAdoptPreamble(result.events);
|
|
3937
4054
|
}
|
|
4055
|
+
/** Restore journal-persisted pending turns after a restart interrupted them.
|
|
4056
|
+
*
|
|
4057
|
+
* Returns true when it established the bridge cursor itself (restore mode);
|
|
4058
|
+
* false when there is nothing to restore and the caller should run the
|
|
4059
|
+
* normal baseline-to-EOF.
|
|
4060
|
+
*
|
|
4061
|
+
* Scope guards (all deliberate):
|
|
4062
|
+
* - non-adopt only (caller gates);
|
|
4063
|
+
* - same transcript file only — a sessionId rotation between generations
|
|
4064
|
+
* invalidates offsets and risks cross-file mis-attribution;
|
|
4065
|
+
* - bounded age (BRIDGE_TURN_RESTORE_MAX_AGE_MS) — a partial answer from
|
|
4066
|
+
* days ago is noise, not recovery.
|
|
4067
|
+
*
|
|
4068
|
+
* Safety properties:
|
|
4069
|
+
* - events before the earliest mark (minus the same 5s guard the
|
|
4070
|
+
* fingerprint switch uses) are ABSORBED as history, so old turns are
|
|
4071
|
+
* never re-emitted;
|
|
4072
|
+
* - a stray prior-turn assistant tail after the cutoff can only synthesize
|
|
4073
|
+
* a local turn, which the non-adopt emit gate always suppresses;
|
|
4074
|
+
* - a restored mark whose user line never landed in the jsonl simply
|
|
4075
|
+
* never starts, and pruneExpired clears it (journal entry included) on
|
|
4076
|
+
* the first idle tick;
|
|
4077
|
+
* - the send-marker gate still applies at emit — turn-sends markers
|
|
4078
|
+
* persist across restarts, so an answer the model already `botmux
|
|
4079
|
+
* send`-ed before the kill stays suppressed. */
|
|
4080
|
+
function tryRestoreInterruptedBridgeTurns() {
|
|
4081
|
+
// At-most-once, and only on this worker process's FIRST bridge baseline. Any
|
|
4082
|
+
// baseline that runs after a watcher teardown is an in-worker CLI restart
|
|
4083
|
+
// whose InflightInputTracker carryover already re-delivers the interrupted
|
|
4084
|
+
// turn — restoring here too would double-deliver (see bridgeRestoreGate,
|
|
4085
|
+
// armed in stopBridgeWatcher).
|
|
4086
|
+
if (!bridgeRestoreGate.mayRestoreOnThisBaseline())
|
|
4087
|
+
return false;
|
|
4088
|
+
const journalPath = bridgeTurnJournalFilePath();
|
|
4089
|
+
if (!journalPath || !bridgeJsonlPath)
|
|
4090
|
+
return false;
|
|
4091
|
+
const entries = readBridgeTurnJournal(journalPath);
|
|
4092
|
+
if (entries.length === 0)
|
|
4093
|
+
return false;
|
|
4094
|
+
const restorable = selectRestorableBridgeTurns(entries, { currentJsonlPath: bridgeJsonlPath });
|
|
4095
|
+
if (restorable.length !== entries.length) {
|
|
4096
|
+
// Entries for rotated-away files or beyond the age cap can never restore —
|
|
4097
|
+
// drop them now so they don't linger across future generations.
|
|
4098
|
+
try {
|
|
4099
|
+
writeBridgeTurnJournal(journalPath, restorable);
|
|
4100
|
+
}
|
|
4101
|
+
catch { /* best-effort */ }
|
|
4102
|
+
}
|
|
4103
|
+
if (restorable.length === 0)
|
|
4104
|
+
return false;
|
|
4105
|
+
for (const entry of restorable) {
|
|
4106
|
+
bridgeQueue.mark(entry.turnId, entry.fingerprint, entry.markTimeMs, entry.contentNormalized, entry.dispatchAttempt, { restoredFromJournal: true });
|
|
4107
|
+
}
|
|
4108
|
+
const fromOffset = Math.min(...restorable.map(entry => entry.offsetAtMark));
|
|
4109
|
+
const drained = drainTranscript(bridgeJsonlPath, fromOffset);
|
|
4110
|
+
bridgeOffset = drained.newOffset;
|
|
4111
|
+
bridgePendingTail = drained.pendingTail;
|
|
4112
|
+
const cutoffMs = Math.min(...restorable.map(entry => entry.markTimeMs)) - 5_000;
|
|
4113
|
+
const { history, live } = splitTranscriptEventsByCutoff(drained.events, cutoffMs);
|
|
4114
|
+
bridgeQueue.absorb(history);
|
|
4115
|
+
if (live.length > 0)
|
|
4116
|
+
bridgeQueue.ingest(live, bridgeJsonlPath);
|
|
4117
|
+
bridgeBaselineDone = true;
|
|
4118
|
+
log(`Bridge restored ${restorable.length} interrupted turn(s) from journal (drain ${fromOffset}→${bridgeOffset}, absorbed=${history.length}, live=${live.length})`);
|
|
4119
|
+
return true;
|
|
4120
|
+
}
|
|
3938
4121
|
/** Record `bridgeStalePidStateSessionId` if the pid file's current sid
|
|
3939
4122
|
* disagrees with the just-accepted candidate's sid. Stops the next pid
|
|
3940
4123
|
* resolver tick from pulling the watcher back to the stale spawn-time
|
|
@@ -4501,6 +4684,7 @@ function bridgeIngest() {
|
|
|
4501
4684
|
for (const t of expired) {
|
|
4502
4685
|
if (t.contentFingerprint)
|
|
4503
4686
|
bridgeFingerprintScanLastMs.delete(t.contentFingerprint);
|
|
4687
|
+
journalBridgeTurnClear(t.turnId, t.dispatchAttempt);
|
|
4504
4688
|
log(`Bridge mark expired after ${Math.round(BRIDGE_PENDING_TURN_TTL_MS / 1000)}s without matching a jsonl user line (turnId=${t.turnId}) — dropped to prevent rotation-fallback scan loop.`);
|
|
4505
4689
|
}
|
|
4506
4690
|
// Drain secondary paths first so any trailing assistant text on an old
|
|
@@ -4602,7 +4786,7 @@ function maybeEmitStructuredRateLimit(events) {
|
|
|
4602
4786
|
// Prefer a clock parsed from the record's own text ("... resets 10:40pm");
|
|
4603
4787
|
// fall back to the shared bucketed cooldown when it carries none.
|
|
4604
4788
|
const usageLimit = structuredRateLimitState(apiErrorMessageText(ev));
|
|
4605
|
-
usageLimitTracker.noteStructuredLimit();
|
|
4789
|
+
usageLimitTracker.noteStructuredLimit(usageLimit);
|
|
4606
4790
|
send({
|
|
4607
4791
|
type: 'screen_update',
|
|
4608
4792
|
content: currentUsageLimitSnapshot(),
|
|
@@ -4755,6 +4939,12 @@ function stopBridgeWatcher() {
|
|
|
4755
4939
|
// A fresh backend must not inherit a terminal/fingerprint from the exited
|
|
4756
4940
|
// process; durable replay is owned by the receiver with a new attempt.
|
|
4757
4941
|
bridgeQueue.clearPending();
|
|
4942
|
+
// A watcher teardown means this worker process has now had at least one CLI
|
|
4943
|
+
// generation, so any FUTURE baseline is an in-worker restart — never the
|
|
4944
|
+
// first-baseline cross-process recovery the journal is for. Disarm journal
|
|
4945
|
+
// restore: the restart's InflightInputTracker carryover already re-delivers
|
|
4946
|
+
// the interrupted turn, and restoring on top would double-deliver.
|
|
4947
|
+
bridgeRestoreGate.markGenerationRetired();
|
|
4758
4948
|
bridgePreambleSent = false;
|
|
4759
4949
|
}
|
|
4760
4950
|
/**
|
|
@@ -4816,7 +5006,30 @@ function bridgeMarkPendingTurn(messageText, preferredTurnId, dispatchAttempt) {
|
|
|
4816
5006
|
const normalised = normaliseForFingerprint(messageText);
|
|
4817
5007
|
const contentNormalized = normalised.length > 0 ? normalised : undefined;
|
|
4818
5008
|
const turnId = preferredTurnId ?? randomBytes(8).toString('hex');
|
|
4819
|
-
|
|
5009
|
+
const markTimeMs = Date.now();
|
|
5010
|
+
bridgeQueue.mark(turnId, fingerprint, markTimeMs, contentNormalized, dispatchAttempt);
|
|
5011
|
+
// Journal the mark so a worker/daemon restart mid-turn can restore it
|
|
5012
|
+
// (bridgeAbsorbBaseline). Excluded:
|
|
5013
|
+
// - adopt mode: its baseline absorbs the whole transcript with different
|
|
5014
|
+
// semantics and never suppresses — restore marks would double-attribute;
|
|
5015
|
+
// - durable turns (dispatchAttempt !== undefined): a durable delivery has
|
|
5016
|
+
// its OWN cross-restart recovery owner (the daemon's persisted
|
|
5017
|
+
// queuedActivation* write-ahead + receipt auto-redispatch), so a journal
|
|
5018
|
+
// restore is always redundant for it and — because the recovered partial
|
|
5019
|
+
// and the re-dispatched attempt carry the same turnId but different
|
|
5020
|
+
// lastUuid — would double-deliver. Only PLAIN Lark IM turns (no other
|
|
5021
|
+
// recovery owner) actually need the journal.
|
|
5022
|
+
if (!lastInitConfig?.adoptMode && bridgeJsonlPath && dispatchAttempt === undefined) {
|
|
5023
|
+
journalBridgeTurnMark({
|
|
5024
|
+
turnId,
|
|
5025
|
+
dispatchAttempt,
|
|
5026
|
+
markTimeMs,
|
|
5027
|
+
fingerprint,
|
|
5028
|
+
contentNormalized,
|
|
5029
|
+
jsonlPath: bridgeJsonlPath,
|
|
5030
|
+
offsetAtMark: bridgeOffset,
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
4820
5033
|
return turnId;
|
|
4821
5034
|
}
|
|
4822
5035
|
function bridgeDrainAndMaybeEmit() {
|
|
@@ -4846,6 +5059,14 @@ function emitReadyTurns(opts = {}) {
|
|
|
4846
5059
|
});
|
|
4847
5060
|
if (ready.length === 0)
|
|
4848
5061
|
return;
|
|
5062
|
+
// Every popped turn is terminal (emitted or suppressed below) — retire its
|
|
5063
|
+
// durable journal entry NOW, before the final_output IPC. Clear-before-send:
|
|
5064
|
+
// if the worker dies in between, the answer is lost rather than duplicated
|
|
5065
|
+
// (the daemon-side dedupe key does not survive restarts).
|
|
5066
|
+
for (const turn of ready) {
|
|
5067
|
+
if (!turn.isLocal)
|
|
5068
|
+
journalBridgeTurnClear(turn.turnId, turn.dispatchAttempt);
|
|
5069
|
+
}
|
|
4849
5070
|
const adoptMode = lastInitConfig?.adoptMode === true;
|
|
4850
5071
|
// Send markers (`botmux send` landed in own thread) + the queue's first
|
|
4851
5072
|
// still-unready turn. The latter caps the LAST ready turn's window —
|
|
@@ -4931,6 +5152,11 @@ function emitReadyTurns(opts = {}) {
|
|
|
4931
5152
|
const fields = formatLocalTurnFields(rawUserText, postText);
|
|
4932
5153
|
if (!fields)
|
|
4933
5154
|
continue;
|
|
5155
|
+
// A harvested answer proves the CLI recovered from any structured
|
|
5156
|
+
// limit that parked it — mirror the daemon's final_output self-heal
|
|
5157
|
+
// (worker-pool clears ds.usageLimit there) by dropping the tracker's
|
|
5158
|
+
// re-emit latch so the next classify() does not re-pin the card.
|
|
5159
|
+
usageLimitTracker.noteTurnCompleted();
|
|
4934
5160
|
send({
|
|
4935
5161
|
type: 'final_output',
|
|
4936
5162
|
content: fields.content,
|
|
@@ -4946,6 +5172,7 @@ function emitReadyTurns(opts = {}) {
|
|
|
4946
5172
|
const headlessContent = formatHeadlessLocalTurnContent(postText);
|
|
4947
5173
|
if (!headlessContent)
|
|
4948
5174
|
continue;
|
|
5175
|
+
usageLimitTracker.noteTurnCompleted();
|
|
4949
5176
|
send({
|
|
4950
5177
|
type: 'final_output',
|
|
4951
5178
|
content: headlessContent,
|
|
@@ -4956,9 +5183,17 @@ function emitReadyTurns(opts = {}) {
|
|
|
4956
5183
|
});
|
|
4957
5184
|
continue;
|
|
4958
5185
|
}
|
|
5186
|
+
// A restored turn's fallback is a RECOVERED partial/final produced around
|
|
5187
|
+
// a worker restart — label it so the user can tell it from a live answer.
|
|
5188
|
+
// Prefix is applied AFTER the suppress gate so dedup compares the model's
|
|
5189
|
+
// own text, never the notice.
|
|
5190
|
+
const deliveredText = turn.restoredFromJournal
|
|
5191
|
+
? `${t('worker.bridge_restored_turn_notice')}\n\n${postText}`
|
|
5192
|
+
: postText;
|
|
5193
|
+
usageLimitTracker.noteTurnCompleted();
|
|
4959
5194
|
send({
|
|
4960
5195
|
type: 'final_output',
|
|
4961
|
-
content:
|
|
5196
|
+
content: deliveredText,
|
|
4962
5197
|
lastUuid,
|
|
4963
5198
|
turnId: turn.turnId,
|
|
4964
5199
|
...(turn.dispatchAttempt !== undefined ? { dispatchAttempt: turn.dispatchAttempt } : {}),
|
|
@@ -5001,6 +5236,19 @@ function codexBridgeFallbackActive() {
|
|
|
5001
5236
|
// services/structured-bridge-clis.ts (single source of the allowlist).
|
|
5002
5237
|
return isStructuredBridgeFallbackActive(lastInitConfig?.cliId, lastInitConfig?.adoptMode === true);
|
|
5003
5238
|
}
|
|
5239
|
+
/** A Codex App shared-adopt starts a SECOND official `codex --remote` client
|
|
5240
|
+
* against an external App Server/thread. It has the same transcript shape as
|
|
5241
|
+
* terminal `/adopt`, but is deliberately NOT `adoptMode`: BotMux still owns
|
|
5242
|
+
* the remote TUI and its normal Lark-input reliability rules. */
|
|
5243
|
+
function isExistingAppServerSharedBridge() {
|
|
5244
|
+
return !!lastInitConfig?.existingAppServerEndpoint;
|
|
5245
|
+
}
|
|
5246
|
+
/** `/adopt`'s split-live attach is also required for the App Server share:
|
|
5247
|
+
* existing history is absorbed while new, App-side turns after the attach
|
|
5248
|
+
* boundary can be synthesised back into the Lark conversation. */
|
|
5249
|
+
function codexBridgeUsesSplitLiveAttach() {
|
|
5250
|
+
return lastInitConfig?.adoptMode === true || isExistingAppServerSharedBridge();
|
|
5251
|
+
}
|
|
5004
5252
|
/** Only drivers with a complete normal-final + interrupted-terminal contract
|
|
5005
5253
|
* may let a transcript-started turn override the screen-ready heuristic. */
|
|
5006
5254
|
function hasStructuredLifecycleBlock() {
|
|
@@ -5033,6 +5281,9 @@ function structuredBridgeIsMtr() {
|
|
|
5033
5281
|
function structuredBridgeIsPi() {
|
|
5034
5282
|
return lastInitConfig?.cliId === 'pi';
|
|
5035
5283
|
}
|
|
5284
|
+
function structuredBridgeIsOmp() {
|
|
5285
|
+
return lastInitConfig?.cliId === 'oh-my-pi';
|
|
5286
|
+
}
|
|
5036
5287
|
function structuredBridgeIsGrok() {
|
|
5037
5288
|
return lastInitConfig?.cliId === 'grok';
|
|
5038
5289
|
}
|
|
@@ -5042,7 +5293,7 @@ function codexBridgeIsCursor() {
|
|
|
5042
5293
|
function currentHermesBridgeDbPath() {
|
|
5043
5294
|
return hermesBridgeDbPath ?? resolveHermesStateDbPath();
|
|
5044
5295
|
}
|
|
5045
|
-
function structuredBridgeIngestPath(path, offset) {
|
|
5296
|
+
function structuredBridgeIngestPath(path, offset, opts = {}) {
|
|
5046
5297
|
if (structuredBridgeIsCodex())
|
|
5047
5298
|
return drainCodexRollout(path, offset);
|
|
5048
5299
|
// adoptMode gates the drainer's bare-sentinel synthesis: adopt posts
|
|
@@ -5054,6 +5305,13 @@ function structuredBridgeIngestPath(path, offset) {
|
|
|
5054
5305
|
return drainCursorTranscript(path, offset);
|
|
5055
5306
|
if (structuredBridgeIsPi())
|
|
5056
5307
|
return drainPiTranscript(path, offset);
|
|
5308
|
+
if (structuredBridgeIsOmp()) {
|
|
5309
|
+
const result = drainOmpTranscript(path, offset, ompBridgeState, {
|
|
5310
|
+
flushTrailingFinal: opts.flushOmpTrailingFinal,
|
|
5311
|
+
});
|
|
5312
|
+
ompBridgeState = result.state;
|
|
5313
|
+
return result;
|
|
5314
|
+
}
|
|
5057
5315
|
if (structuredBridgeIsGrok())
|
|
5058
5316
|
return drainGrokUpdates(path, offset);
|
|
5059
5317
|
if (structuredBridgeIsHermes()) {
|
|
@@ -5145,12 +5403,14 @@ function codexBridgeStartTimer() {
|
|
|
5145
5403
|
// fd is process-scoped, so following that fd cannot select a sibling
|
|
5146
5404
|
// TRAE process merely because it shares the working directory.
|
|
5147
5405
|
maybeFollowTraexSessionRotationViaPid();
|
|
5406
|
+
maybeFollowOmpTranscriptRotation();
|
|
5148
5407
|
if (!codexBridgeRolloutPath) {
|
|
5149
5408
|
// Late-attach: cliSessionId (writeInput / daemon probe) then adopt
|
|
5150
5409
|
// pid. Path lookup is centralized in resolveFileBridgePath so
|
|
5151
5410
|
// adding a file-backed CLI does not grow this if-tree.
|
|
5152
|
-
//
|
|
5153
|
-
//
|
|
5411
|
+
// Terminal `/adopt` and an existing-App-Server share both need
|
|
5412
|
+
// split-live: absorb history before the attach boundary while keeping
|
|
5413
|
+
// new local/App-side turns after it available for forwarding.
|
|
5154
5414
|
const path = resolveFileBridgePath(lastInitConfig?.cliId, {
|
|
5155
5415
|
sessionId: codexBridgePendingSessionId,
|
|
5156
5416
|
cwd: lastInitConfig?.workingDir,
|
|
@@ -5179,11 +5439,12 @@ function codexBridgeStartTimer() {
|
|
|
5179
5439
|
}
|
|
5180
5440
|
codexBridgePendingSessionId = undefined;
|
|
5181
5441
|
codexAdoptPendingPid = undefined;
|
|
5182
|
-
const mode =
|
|
5442
|
+
const mode = codexBridgeUsesSplitLiveAttach() ? 'split-live' : 'fresh-empty';
|
|
5183
5443
|
codexBridgeAttach(path, mode);
|
|
5184
5444
|
}
|
|
5185
5445
|
}
|
|
5186
5446
|
codexBridgeIngest();
|
|
5447
|
+
maybeFlushOmpTrailingFinalOnQuietTick();
|
|
5187
5448
|
if (isPromptReady)
|
|
5188
5449
|
emitReadyCodexTurns();
|
|
5189
5450
|
}
|
|
@@ -5285,6 +5546,9 @@ function mtrBridgeIngest() {
|
|
|
5285
5546
|
}
|
|
5286
5547
|
}
|
|
5287
5548
|
function codexBridgeAttach(rolloutPath, mode) {
|
|
5549
|
+
ompBridgeState = {};
|
|
5550
|
+
ompQuietCandidateKey = undefined;
|
|
5551
|
+
ompQuietCandidateCompleteOffset = undefined;
|
|
5288
5552
|
codexBridgeRolloutPath = rolloutPath;
|
|
5289
5553
|
if (structuredBridgeIsCodex())
|
|
5290
5554
|
codexServiceTierTracker.bind(rolloutPath);
|
|
@@ -5468,6 +5732,9 @@ function codexBridgeDetachFile() {
|
|
|
5468
5732
|
codexBridgeOffset = 0;
|
|
5469
5733
|
codexBridgePendingTail = '';
|
|
5470
5734
|
codexBridgeBaselineDone = false;
|
|
5735
|
+
ompBridgeState = {};
|
|
5736
|
+
ompQuietCandidateKey = undefined;
|
|
5737
|
+
ompQuietCandidateCompleteOffset = undefined;
|
|
5471
5738
|
}
|
|
5472
5739
|
/** Resolve the pid of the Codex process this worker observes (spawned child or
|
|
5473
5740
|
* adopted pane), mirroring the grok/traex pid-follow resolution order. */
|
|
@@ -5588,7 +5855,7 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
5588
5855
|
const pid = currentCodexObservedPid();
|
|
5589
5856
|
const next = resolveFileBridgePath('codex', { sessionId: cliSessionId });
|
|
5590
5857
|
if (next && next !== codexBridgeRolloutPath) {
|
|
5591
|
-
const attachMode =
|
|
5858
|
+
const attachMode = codexBridgeUsesSplitLiveAttach() ? 'split-live' : 'fresh-empty';
|
|
5592
5859
|
log(`Codex session binding corrected ${currentSid ?? '?'} → ${cliSessionId} (pid ${pid} owns it); re-attaching bridge to ${next}`);
|
|
5593
5860
|
codexBridgeDetachFile();
|
|
5594
5861
|
codexBridgePendingSessionId = undefined;
|
|
@@ -5717,7 +5984,7 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
5717
5984
|
}
|
|
5718
5985
|
return;
|
|
5719
5986
|
}
|
|
5720
|
-
// Codex INITIAL attach (no prior rollout bound). The multi-fd adopt case
|
|
5987
|
+
// Codex INITIAL attach (no prior rollout bound). The multi-fd terminal-adopt case
|
|
5721
5988
|
// reaches here with codexBridgeRolloutPath still unset: findCodexRolloutByPid
|
|
5722
5989
|
// returned undefined (ambiguous parent+sibling), so the adopt block armed the
|
|
5723
5990
|
// poller instead of attaching. The cliSessionId is normally already
|
|
@@ -5755,7 +6022,7 @@ function codexBridgeNotifyCliSessionId(cliSessionId) {
|
|
|
5755
6022
|
});
|
|
5756
6023
|
if (path) {
|
|
5757
6024
|
codexBridgePendingSessionId = undefined;
|
|
5758
|
-
codexBridgeAttach(path, 'fresh-empty');
|
|
6025
|
+
codexBridgeAttach(path, codexBridgeUsesSplitLiveAttach() ? 'split-live' : 'fresh-empty');
|
|
5759
6026
|
}
|
|
5760
6027
|
else {
|
|
5761
6028
|
codexBridgePendingSessionId = cliSessionId;
|
|
@@ -5807,6 +6074,30 @@ function maybeFollowTraexSessionRotationViaPid() {
|
|
|
5807
6074
|
persistCliSessionId(observed.cliSessionId);
|
|
5808
6075
|
codexBridgeNotifyCliSessionId(observed.cliSessionId);
|
|
5809
6076
|
}
|
|
6077
|
+
/** OMP may create a new JSONL in the same isolated Botmux session directory.
|
|
6078
|
+
* Follow only that exact directory; no pid/global-session discovery is used. */
|
|
6079
|
+
function maybeFollowOmpTranscriptRotation() {
|
|
6080
|
+
if (!structuredBridgeIsOmp() || !codexBridgeRolloutPath
|
|
6081
|
+
|| !lastSpawnEffectiveAdapterSessionId)
|
|
6082
|
+
return;
|
|
6083
|
+
const next = resolveFileBridgePath('oh-my-pi', {
|
|
6084
|
+
sessionId: lastSpawnEffectiveAdapterSessionId,
|
|
6085
|
+
});
|
|
6086
|
+
if (!next || next === codexBridgeRolloutPath || ompRetiredTranscriptPaths.has(next))
|
|
6087
|
+
return;
|
|
6088
|
+
const retired = codexBridgeRolloutPath;
|
|
6089
|
+
try {
|
|
6090
|
+
codexBridgeIngest();
|
|
6091
|
+
emitReadyCodexTurns();
|
|
6092
|
+
}
|
|
6093
|
+
catch (err) {
|
|
6094
|
+
log(`OMP pre-rotation bridge drain failed: ${err.message}`);
|
|
6095
|
+
}
|
|
6096
|
+
log(`OMP transcript rotated: ${codexBridgeRolloutPath} → ${next}`);
|
|
6097
|
+
ompRetiredTranscriptPaths.add(retired);
|
|
6098
|
+
codexBridgeDetachFile();
|
|
6099
|
+
codexBridgeAttach(next, 'fresh-empty');
|
|
6100
|
+
}
|
|
5810
6101
|
function codexBridgeIngest(opts = {}) {
|
|
5811
6102
|
// Follow-up RPC turns install their exact bridge mark only after the
|
|
5812
6103
|
// turn/start ACK passes the generation fence. Ordinary ingest must not
|
|
@@ -5825,7 +6116,9 @@ function codexBridgeIngest(opts = {}) {
|
|
|
5825
6116
|
}
|
|
5826
6117
|
if (!codexBridgeRolloutPath || !codexBridgeBaselineDone)
|
|
5827
6118
|
return;
|
|
5828
|
-
const result = structuredBridgeIngestPath(codexBridgeRolloutPath, codexBridgeOffset
|
|
6119
|
+
const result = structuredBridgeIngestPath(codexBridgeRolloutPath, codexBridgeOffset, {
|
|
6120
|
+
flushOmpTrailingFinal: opts.flushOmpTrailingFinal,
|
|
6121
|
+
});
|
|
5829
6122
|
codexBridgeOffset = result.newOffset;
|
|
5830
6123
|
codexBridgePendingTail = result.pendingTail;
|
|
5831
6124
|
if (structuredBridgeIsTraex()) {
|
|
@@ -5861,6 +6154,42 @@ function codexBridgeIngest(opts = {}) {
|
|
|
5861
6154
|
idleDetector?.fireIdle();
|
|
5862
6155
|
}
|
|
5863
6156
|
}
|
|
6157
|
+
/** Confirm an OMP file-tail terminal only after one complete quiet bridge tick
|
|
6158
|
+
* and an authoritative current viewport with no busy marker. This deliberately
|
|
6159
|
+
* runs while lifecycle blocking is asserted; ingesting the final releases it. */
|
|
6160
|
+
function maybeFlushOmpTrailingFinalOnQuietTick() {
|
|
6161
|
+
if (!structuredBridgeIsOmp())
|
|
6162
|
+
return;
|
|
6163
|
+
const candidate = ompBridgeState.provisionalFinal;
|
|
6164
|
+
if (!candidate) {
|
|
6165
|
+
ompQuietCandidateKey = undefined;
|
|
6166
|
+
ompQuietCandidateCompleteOffset = undefined;
|
|
6167
|
+
return;
|
|
6168
|
+
}
|
|
6169
|
+
const key = candidate.event.uuid;
|
|
6170
|
+
if (ompQuietCandidateKey !== key
|
|
6171
|
+
|| ompQuietCandidateCompleteOffset !== codexBridgeOffset) {
|
|
6172
|
+
ompQuietCandidateKey = key;
|
|
6173
|
+
ompQuietCandidateCompleteOffset = codexBridgeOffset;
|
|
6174
|
+
return;
|
|
6175
|
+
}
|
|
6176
|
+
if (codexBridgePendingTail || !hasStructuredLifecycleBlock() || !backend
|
|
6177
|
+
|| !backendScreenEvidenceIsAuthoritativeForMutation()
|
|
6178
|
+
|| !cliAdapter?.busyPattern)
|
|
6179
|
+
return;
|
|
6180
|
+
try {
|
|
6181
|
+
const screen = captureBackendScreen(backend);
|
|
6182
|
+
if (!screen || cliAdapter.busyPattern.test(busyProbeRegion(screen)))
|
|
6183
|
+
return;
|
|
6184
|
+
}
|
|
6185
|
+
catch (err) {
|
|
6186
|
+
log(`OMP quiet-final viewport capture failed: ${err.message}`);
|
|
6187
|
+
return;
|
|
6188
|
+
}
|
|
6189
|
+
ompQuietCandidateKey = undefined;
|
|
6190
|
+
ompQuietCandidateCompleteOffset = undefined;
|
|
6191
|
+
codexBridgeIngest({ flushOmpTrailingFinal: true });
|
|
6192
|
+
}
|
|
5864
6193
|
/** 将 Codex 的结构化 429 终态同步为既有的限流状态。 */
|
|
5865
6194
|
function maybeEmitCodexStructuredRateLimit(events) {
|
|
5866
6195
|
for (const ev of events) {
|
|
@@ -5868,7 +6197,7 @@ function maybeEmitCodexStructuredRateLimit(events) {
|
|
|
5868
6197
|
continue;
|
|
5869
6198
|
emittedRateLimitUuids.add(ev.uuid);
|
|
5870
6199
|
const usageLimit = structuredRateLimitState(ev.terminalErrorSummary ?? '429 Too Many Requests');
|
|
5871
|
-
usageLimitTracker.noteStructuredLimit();
|
|
6200
|
+
usageLimitTracker.noteStructuredLimit(usageLimit);
|
|
5872
6201
|
send({
|
|
5873
6202
|
type: 'screen_update',
|
|
5874
6203
|
content: currentUsageLimitSnapshot(),
|
|
@@ -6245,11 +6574,12 @@ function emitReadyCodexTurns() {
|
|
|
6245
6574
|
// that flag to settle completed-with-empty; a bare `completed` terminal (e.g.
|
|
6246
6575
|
// the RPC-hydration timeout path) must never be read as silence.
|
|
6247
6576
|
const nothingToSendTurns = new Set();
|
|
6248
|
-
const
|
|
6577
|
+
const terminalAdoptMode = lastInitConfig?.adoptMode === true;
|
|
6578
|
+
const sharedAppServerBridge = isExistingAppServerSharedBridge();
|
|
6249
6579
|
// Adopt mode: model is the user's external Codex, no botmux send to
|
|
6250
6580
|
// gate against — every assistant turn (Lark-driven OR locally typed)
|
|
6251
6581
|
// should reach the thread. Skip marker IO entirely.
|
|
6252
|
-
const markers =
|
|
6582
|
+
const markers = terminalAdoptMode ? [] : readSendMarkers();
|
|
6253
6583
|
const remaining = codexBridgeQueue.peek();
|
|
6254
6584
|
// Only a STARTED pending turn can bound the last ready turn's send window.
|
|
6255
6585
|
// An unstarted turn hasn't been dequeued yet (its user event hasn't landed),
|
|
@@ -6264,6 +6594,13 @@ function emitReadyCodexTurns() {
|
|
|
6264
6594
|
: undefined;
|
|
6265
6595
|
for (let i = 0; i < ready.length; i++) {
|
|
6266
6596
|
const turn = ready[i];
|
|
6597
|
+
// A shared App Server session still owns the BotMux remote TUI, so its
|
|
6598
|
+
// Lark-originated turns retain normal send-marker deduplication. Only a
|
|
6599
|
+
// turn synthesized from Codex App input is external/local: that side has
|
|
6600
|
+
// no pending Lark fingerprint and must be forwarded like terminal
|
|
6601
|
+
// `/adopt`, including both the App prompt and its final reply.
|
|
6602
|
+
const adoptMode = terminalAdoptMode
|
|
6603
|
+
|| (sharedAppServerBridge && turn.isLocal === true);
|
|
6267
6604
|
const sourceHermesSessionId = structuredBridgeIsHermes() ? turn.sourceSessionId : undefined;
|
|
6268
6605
|
const nextBoundaryMs = (i + 1 < ready.length ? ready[i + 1].markTimeMs : nextPendingMarkTimeMs);
|
|
6269
6606
|
const gateInput = {
|
|
@@ -6319,6 +6656,9 @@ function emitReadyCodexTurns() {
|
|
|
6319
6656
|
const fields = formatLocalTurnFields(turn.userText ?? '', postContent);
|
|
6320
6657
|
if (!fields)
|
|
6321
6658
|
continue;
|
|
6659
|
+
// Harvested answer → drop the structured-limit re-emit latch (mirrors
|
|
6660
|
+
// the daemon's final_output self-heal; see emitReadyTurns).
|
|
6661
|
+
usageLimitTracker.noteTurnCompleted();
|
|
6322
6662
|
send({
|
|
6323
6663
|
type: 'final_output',
|
|
6324
6664
|
...(sourceHermesSessionId ? { sourceHermesSessionId } : {}),
|
|
@@ -6331,6 +6671,7 @@ function emitReadyCodexTurns() {
|
|
|
6331
6671
|
});
|
|
6332
6672
|
continue;
|
|
6333
6673
|
}
|
|
6674
|
+
usageLimitTracker.noteTurnCompleted();
|
|
6334
6675
|
send({
|
|
6335
6676
|
type: 'final_output',
|
|
6336
6677
|
...(sourceHermesSessionId ? { sourceHermesSessionId } : {}),
|
|
@@ -6369,6 +6710,10 @@ function stopCodexBridge() {
|
|
|
6369
6710
|
codexBridgeOffset = 0;
|
|
6370
6711
|
codexBridgePendingTail = '';
|
|
6371
6712
|
codexBridgeBaselineDone = false;
|
|
6713
|
+
ompBridgeState = {};
|
|
6714
|
+
ompQuietCandidateKey = undefined;
|
|
6715
|
+
ompQuietCandidateCompleteOffset = undefined;
|
|
6716
|
+
ompRetiredTranscriptPaths.clear();
|
|
6372
6717
|
hermesBridgeOffset = 0;
|
|
6373
6718
|
hermesBridgeBaselineDone = false;
|
|
6374
6719
|
hermesBridgeSourceSessionId = undefined;
|
|
@@ -7803,6 +8148,13 @@ function handleVisibleStartupInteraction(data) {
|
|
|
7803
8148
|
// row; never let its selection marker reach the first-prompt idle detector.
|
|
7804
8149
|
if (dismissAidenCodexUpdateDialog(data))
|
|
7805
8150
|
return true;
|
|
8151
|
+
// An existing App Server viewer shares the user's Codex App trust context.
|
|
8152
|
+
// Never auto-accept an interactive trust prompt here: it can include
|
|
8153
|
+
// project/plugin hooks owned outside BotMux. The operator can safely choose
|
|
8154
|
+
// "Continue without trusting" through the Web Terminal when Codex presents
|
|
8155
|
+
// its Hooks review menu.
|
|
8156
|
+
if (lastInitConfig?.existingAppServerEndpoint)
|
|
8157
|
+
return false;
|
|
7806
8158
|
if (trustHandled)
|
|
7807
8159
|
return false;
|
|
7808
8160
|
const stripped = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
@@ -8612,6 +8964,7 @@ function onPtyData(data) {
|
|
|
8612
8964
|
}
|
|
8613
8965
|
}
|
|
8614
8966
|
renderer?.write(data);
|
|
8967
|
+
refreshHookReviewInputHold(`${renderer?.rawSnapshot() ?? ''}\n${data}`);
|
|
8615
8968
|
// In tmux-attach mode, each web client has its own tmux attach PTY —
|
|
8616
8969
|
// no relay needed. In non-tmux mode AND in pipe mode (adopt-bridge),
|
|
8617
8970
|
// broadcast through the shared scrollback so all connected web clients
|
|
@@ -8721,6 +9074,7 @@ async function onBackendScreenResync(snapshot) {
|
|
|
8721
9074
|
// current viewport: a prior local attach may have changed the real geometry.
|
|
8722
9075
|
const visibleSnapshot = nextRenderer?.rawSnapshot() ?? '';
|
|
8723
9076
|
lastAnalyzerSnapshot = visibleSnapshot;
|
|
9077
|
+
refreshHookReviewInputHold(visibleSnapshot);
|
|
8724
9078
|
// ZMX history does not carry the authoritative current PTY dimensions. A
|
|
8725
9079
|
// local `zmx attach` can resize the session below our default 120x24 and that
|
|
8726
9080
|
// size persists after detach, so even the rendered tail may include rows just
|
|
@@ -9329,6 +9683,7 @@ function dropFailedBridgeMark(bridgeTurnId, dispatchAttempt) {
|
|
|
9329
9683
|
if (dropped) {
|
|
9330
9684
|
if (dropped.contentFingerprint)
|
|
9331
9685
|
bridgeFingerprintScanLastMs.delete(dropped.contentFingerprint);
|
|
9686
|
+
journalBridgeTurnClear(bridgeTurnId, dispatchAttempt);
|
|
9332
9687
|
log(`Bridge mark dropped after submit failure (turnId=${bridgeTurnId}) — rotation-fallback scan will stop spinning on this fingerprint.`);
|
|
9333
9688
|
}
|
|
9334
9689
|
if (droppedStructured) {
|
|
@@ -9429,12 +9784,9 @@ function scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId
|
|
|
9429
9784
|
case 'suppress-active':
|
|
9430
9785
|
redriveRejectedStructuredReady();
|
|
9431
9786
|
log(`Deferred recheck missing but later ${action.evidence} shows ${cliName()} is active — suppressing submit warning. preview="${preview}"`);
|
|
9432
|
-
//
|
|
9433
|
-
//
|
|
9434
|
-
|
|
9435
|
-
if (turnIdentity?.dispatchAttempt !== undefined) {
|
|
9436
|
-
scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, undefined, turnSeq, turnIdentity, durableTerminalStatus);
|
|
9437
|
-
}
|
|
9787
|
+
// activity 证据只能说明 CLI 仍在动,不能证明本次输入已提交
|
|
9788
|
+
// 普通 IM 和 durable turn 都继续重查,直到 transcript 命中或安静窗口落到 submit_unconfirmed
|
|
9789
|
+
scheduleSubmitFailureNotify(msg, recheck, transcriptLabel, bridgeTurnId, undefined, turnSeq, turnIdentity, durableTerminalStatus, structuredTarget);
|
|
9438
9790
|
return;
|
|
9439
9791
|
case 'notify-hard-failure':
|
|
9440
9792
|
// failureReason is handled synchronously above.
|
|
@@ -9608,7 +9960,7 @@ async function flushPending() {
|
|
|
9608
9960
|
}
|
|
9609
9961
|
if (ambiguousSubmissionRecoveryHold?.backend === backend)
|
|
9610
9962
|
return;
|
|
9611
|
-
if (
|
|
9963
|
+
if (!hasPendingInputForFlush())
|
|
9612
9964
|
return; // nothing to flush — keep isPromptReady
|
|
9613
9965
|
if (sessionRenameInFlight())
|
|
9614
9966
|
return; // wait for /rename to finish before any user input
|
|
@@ -9627,6 +9979,14 @@ async function flushPending() {
|
|
|
9627
9979
|
return;
|
|
9628
9980
|
if (bareShellLaunchBlocked)
|
|
9629
9981
|
return; // launch is held at a bare shell — don't type prompts into it
|
|
9982
|
+
// A Hook-review page has an active selection and consumes Enter/arrow keys.
|
|
9983
|
+
// Re-check the rendered viewport right before a literal write; PTY chunks
|
|
9984
|
+
// can arrive between idle detection and this queue drain.
|
|
9985
|
+
refreshHookReviewInputHold(lastAnalyzerSnapshot || renderer?.rawSnapshot() || '');
|
|
9986
|
+
if (hookReviewInputHold) {
|
|
9987
|
+
notifyHookReviewInputHold();
|
|
9988
|
+
return;
|
|
9989
|
+
}
|
|
9630
9990
|
// Screen-idle is not a durable receipt. A permission/AskUser prompt can look
|
|
9631
9991
|
// idle while the logical delivery is unresolved, so no following IM or
|
|
9632
9992
|
// meeting turn may cross this boundary until an explicit terminal releases
|
|
@@ -10952,6 +11312,27 @@ function scheduleBusyPatternIdleProbe(source) {
|
|
|
10952
11312
|
}
|
|
10953
11313
|
async function spawnCli(cfg, opts = {}) {
|
|
10954
11314
|
const spawnGeneration = ++cliSpawnGeneration;
|
|
11315
|
+
// Experimental external App Server attachment: BotMux owns only this TUI
|
|
11316
|
+
// client, never the server or its JSON-RPC input stream. Re-establish the
|
|
11317
|
+
// remote argv state on EVERY spawn because killCli() deliberately clears the
|
|
11318
|
+
// generic RPC remote variables before a restart.
|
|
11319
|
+
if (cfg.existingAppServerEndpoint !== undefined) {
|
|
11320
|
+
if (cfg.cliId !== 'codex') {
|
|
11321
|
+
throw new Error('existing App Server attachment requires cliId "codex"');
|
|
11322
|
+
}
|
|
11323
|
+
if (!cfg.cliSessionId) {
|
|
11324
|
+
throw new Error('existing App Server attachment requires an already-selected Codex thread id');
|
|
11325
|
+
}
|
|
11326
|
+
if (cfg.codexRpcInput === true) {
|
|
11327
|
+
throw new Error('existing App Server attachment cannot enable BotMux codexRpcInput');
|
|
11328
|
+
}
|
|
11329
|
+
if (codexRpcEngine) {
|
|
11330
|
+
throw new Error('existing App Server attachment cannot share a BotMux-owned Codex RPC engine');
|
|
11331
|
+
}
|
|
11332
|
+
remoteWsUrl = normalizeExistingAppServerEndpoint(cfg.existingAppServerEndpoint, `worker existingAppServerEndpoint for session ${cfg.sessionId}`);
|
|
11333
|
+
remoteThreadId = cfg.cliSessionId;
|
|
11334
|
+
log(`External Codex App Server attach prepared for thread ${cfg.cliSessionId.substring(0, 12)}`);
|
|
11335
|
+
}
|
|
10955
11336
|
// Prefer force-clear so a half-finished rename cannot block the new generation.
|
|
10956
11337
|
forceClearSessionRenameInFlight();
|
|
10957
11338
|
currentCliCredentialIsolated = false;
|
|
@@ -11121,7 +11502,16 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11121
11502
|
log(`Adopt mode (${effectiveBackendType}): observing ${target} (${cols}x${rows})`);
|
|
11122
11503
|
return;
|
|
11123
11504
|
}
|
|
11124
|
-
|
|
11505
|
+
// dsh runtime variant: when the bot selected dsh-tui (dshRuntime='tui'),
|
|
11506
|
+
// resolve the PTY-driven dsh-tui adapter instead of the headless JSON-RPC
|
|
11507
|
+
// runner. The bot's cliId stays 'dsh' (the toggle is a per-bot config field),
|
|
11508
|
+
// so every other dsh-specific branch (OSC decode, turn timeout, etc.) is
|
|
11509
|
+
// unaffected — dsh-tui simply doesn't emit OSC frames, making the decoder a
|
|
11510
|
+
// no-op for it.
|
|
11511
|
+
const effectiveCliId = cfg.cliId === 'dsh' && cfg.dshRuntime === 'tui'
|
|
11512
|
+
? 'dsh-tui'
|
|
11513
|
+
: cfg.cliId;
|
|
11514
|
+
cliAdapter = createCliAdapterSync(effectiveCliId, cfg.cliPathOverride);
|
|
11125
11515
|
// backendType trust-but-verify + HARD GATE (PTY 退役): an explicit per-bot
|
|
11126
11516
|
// config (or BACKEND_TYPE env override) bypasses config.ts's default, so the
|
|
11127
11517
|
// worker re-probes the requested persistent backend here. A requested
|
|
@@ -11927,11 +12317,35 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11927
12317
|
if (effectiveBackendType === 'zmx') {
|
|
11928
12318
|
resolvedZmxSessionProbe = paneProbe;
|
|
11929
12319
|
}
|
|
11930
|
-
|
|
11931
|
-
|
|
11932
|
-
|
|
11933
|
-
//
|
|
11934
|
-
|
|
12320
|
+
const paneRelayReattachSafe = paneProbe === 'exists'
|
|
12321
|
+
&& mcpGatewayPaneReattachSafe(readMcpGatewayLaunchRecord(config.session.dataDir, cfg.sessionId), sessionMcpGatewaySocketPath(cfg.sessionId, config.session.dataDir));
|
|
12322
|
+
if (paneRelayReattachSafe) {
|
|
12323
|
+
// The pane's CLI was launched against the deterministic Gateway socket
|
|
12324
|
+
// path by a reconnect-capable relay. Reattaching preserves whatever turn
|
|
12325
|
+
// the CLI is executing — the whole point of the persistent backend — but
|
|
12326
|
+
// the trusted host died with the previous worker, so we must RE-SERVE it
|
|
12327
|
+
// here at the same deterministic path for the pane's surviving relay to
|
|
12328
|
+
// reconnect to. This reattach path skips prepareCliPluginGenerationAndGateway
|
|
12329
|
+
// (it must not refresh the catalog on a warm reattach), which is the only
|
|
12330
|
+
// other host starter; without this the relay would reconnect forever to a
|
|
12331
|
+
// socket nothing binds. Start only when this fresh worker has no live host
|
|
12332
|
+
// yet — never stomp a host an in-worker restart already brought up.
|
|
12333
|
+
if (!sessionMcpGatewayHost) {
|
|
12334
|
+
await startAndRecordSessionMcpGatewayHost(cfg.sessionId);
|
|
12335
|
+
if (spawnGeneration !== cliSpawnGeneration)
|
|
12336
|
+
throw new CliSpawnSupersededError();
|
|
12337
|
+
log(`[mcp-gateway] persistent pane ${cfg.sessionId} keeps its relay socket path — re-served trusted host at the same path; relay reconnects on its own`);
|
|
12338
|
+
}
|
|
12339
|
+
else {
|
|
12340
|
+
log(`[mcp-gateway] persistent pane ${cfg.sessionId} keeps its relay socket path — reattaching; live host already bound, relay stays connected`);
|
|
12341
|
+
}
|
|
12342
|
+
}
|
|
12343
|
+
else if (paneProbe === 'exists') {
|
|
12344
|
+
// Legacy pane (mkdtemp-random socket path, or a relay predating
|
|
12345
|
+
// reconnect support): its MCP client can never reach the replacement
|
|
12346
|
+
// host. Cold-resume the CLI so it gets a fresh relay socket instead of
|
|
12347
|
+
// reattaching to a dead connection.
|
|
12348
|
+
log(`[mcp-gateway] persistent pane ${cfg.sessionId} has plugin MCP state without a reconnect-capable relay — cold-resuming with a fresh host`);
|
|
11935
12349
|
const persistentBackendType = effectiveBackendType;
|
|
11936
12350
|
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
11937
12351
|
if (effectiveBackendType === 'zmx') {
|
|
@@ -12050,6 +12464,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12050
12464
|
bareShellLaunchBlocked = false;
|
|
12051
12465
|
bareShellChecked = false;
|
|
12052
12466
|
bareShellCheckInProgress = false;
|
|
12467
|
+
hookReviewInputHold = false;
|
|
12468
|
+
hookReviewInputHoldNotified = false;
|
|
12053
12469
|
// ── Resume pre-flight check + two-tier fallback ──────────────────────────
|
|
12054
12470
|
// Tier 0 (adapter capability): adapters whose buildArgs can only resume a
|
|
12055
12471
|
// PRECISE cliSessionId (cursor/copilot/kimi — no --continue/latest fallback,
|
|
@@ -12171,6 +12587,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12171
12587
|
// the fresh session's first turn.
|
|
12172
12588
|
lastSpawnEffectiveResume = effectiveResume;
|
|
12173
12589
|
lastSpawnEffectiveCliSessionId = effectiveCliSessionId;
|
|
12590
|
+
lastSpawnEffectiveAdapterSessionId = effectiveAdapterSessionId;
|
|
12591
|
+
ompRetiredTranscriptPaths.clear();
|
|
12174
12592
|
// ttadk 网关:模型走 ttadk 自己的 `-m`(启动期注入到 ttadk 前缀,见下方 wrapperCli
|
|
12175
12593
|
// 分支),不能再把 cfg.model 透给底层适配器,否则真实 CLI 会再吃一个 --model 重复。
|
|
12176
12594
|
const ttadkGateway = isTtadkWrapper(cfg.wrapperCli);
|
|
@@ -12667,6 +13085,47 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12667
13085
|
}
|
|
12668
13086
|
return out;
|
|
12669
13087
|
};
|
|
13088
|
+
// Canonicalize a path whose LEAF may not exist yet (e.g. a lark-cli keystore dir
|
|
13089
|
+
// lark-cli will create on first write): realpath the DEEPEST EXISTING ancestor
|
|
13090
|
+
// (resolving any symlinked prefix — symlinked $HOME, a symlinked data root) and
|
|
13091
|
+
// re-append the non-existent tail. A plain full-path realpath THROWS on the
|
|
13092
|
+
// missing leaf and returns the raw lexical value, so a symlinked ancestor would
|
|
13093
|
+
// stay unresolved and the policy could anchor a DIFFERENT namespace than the one
|
|
13094
|
+
// lark-cli opens after IT canonicalizes (a canonicalization TOCTOU). Input must already be
|
|
13095
|
+
// lexically clean (no `..`); resolveLarkCliLinuxStoreDir Cleans it first.
|
|
13096
|
+
const canonicalNearestAncestor = (p) => {
|
|
13097
|
+
if (!p.startsWith('/'))
|
|
13098
|
+
return p;
|
|
13099
|
+
const segs = p.split('/').filter(Boolean);
|
|
13100
|
+
for (let i = segs.length; i >= 0; i--) {
|
|
13101
|
+
const prefix = '/' + segs.slice(0, i).join('/');
|
|
13102
|
+
try {
|
|
13103
|
+
const real = realpathSync(prefix);
|
|
13104
|
+
return i === segs.length ? real : `${real}/${segs.slice(i).join('/')}`;
|
|
13105
|
+
}
|
|
13106
|
+
catch { /* ancestor missing → try a shallower prefix */ }
|
|
13107
|
+
}
|
|
13108
|
+
return p; // not even '/' resolved (impossible) → lexical fallback
|
|
13109
|
+
};
|
|
13110
|
+
// OMP keeps all transcripts under one shared state root. Materialize this
|
|
13111
|
+
// Botmux session's exact directory before allow-path existence filtering;
|
|
13112
|
+
// the policy below masks the shared sessions parent and re-opens only this
|
|
13113
|
+
// deeper directory read-write.
|
|
13114
|
+
const ompCurrentSessionDir = cliAdapter.id === 'oh-my-pi'
|
|
13115
|
+
? ompSessionDir(effectiveAdapterSessionId)
|
|
13116
|
+
: undefined;
|
|
13117
|
+
if (ompCurrentSessionDir)
|
|
13118
|
+
mkdirSync(ompCurrentSessionDir, { recursive: true, mode: 0o700 });
|
|
13119
|
+
const canonicalOmpSessionsRoot = ompCurrentSessionDir
|
|
13120
|
+
? canonical(dirname(dirname(ompCurrentSessionDir)))
|
|
13121
|
+
: undefined;
|
|
13122
|
+
const canonicalOmpSessionDir = ompCurrentSessionDir
|
|
13123
|
+
? canonical(ompCurrentSessionDir)
|
|
13124
|
+
: undefined;
|
|
13125
|
+
if (canonicalOmpSessionsRoot && canonicalOmpSessionDir
|
|
13126
|
+
&& relative(canonicalOmpSessionsRoot, canonicalOmpSessionDir) !== join('botmux', effectiveAdapterSessionId)) {
|
|
13127
|
+
throw new Error('[sandbox] OMP session directory escapes its managed sessions root');
|
|
13128
|
+
}
|
|
12670
13129
|
// User three-tier lists: the new sandboxPaths field, or a pre-migration
|
|
12671
13130
|
// session/config's legacy fields mapped through the SAME lossless mapping
|
|
12672
13131
|
// the startup migration uses.
|
|
@@ -12786,6 +13245,9 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12786
13245
|
if (process.platform === 'linux') {
|
|
12787
13246
|
mandatoryDenyPaths.push(join(canonical(dataDir), 'sandboxes', cfg.sessionId));
|
|
12788
13247
|
}
|
|
13248
|
+
if (canonicalOmpSessionsRoot) {
|
|
13249
|
+
mandatoryDenyPaths.push(canonicalOmpSessionsRoot);
|
|
13250
|
+
}
|
|
12789
13251
|
if (process.platform === 'darwin') {
|
|
12790
13252
|
const osUserHomeDir = userInfo().homedir;
|
|
12791
13253
|
if (!osUserHomeDir) {
|
|
@@ -12917,6 +13379,23 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12917
13379
|
// arbitrary parent dir (`/tmp`, `/etc`, a project root) and bricking the
|
|
12918
13380
|
// core CLI (codex P1). Canonicalized so it shares the roots' namespace.
|
|
12919
13381
|
loadedBotsConfigPath: cfg.loadedBotsConfigPath ? canonical(cfg.loadedBotsConfigPath) : undefined,
|
|
13382
|
+
// Linux lark-cli keystore (holds every bot's appsecret ciphertext + the shared
|
|
13383
|
+
// master key). Resolve it EXACTLY as the in-sandbox lark-cli will:
|
|
13384
|
+
// `<clean(LARKSUITE_CLI_DATA_DIR)>/lark-cli` when that env var is a valid ABSOLUTE
|
|
13385
|
+
// path (lexically Cleaned — `..`/`.`/`//` resolved, control chars rejected), else
|
|
13386
|
+
// `$HOME/.local/share/lark-cli` — lark-cli does NOT read XDG_DATA_HOME (verified by
|
|
13387
|
+
// strace on v1.0.76 + its keychain_other.go::StorageDir / SafeEnvDirPath). The
|
|
13388
|
+
// Clean happens INSIDE resolveLarkCliLinuxStoreDir (a `..`-bearing value
|
|
13389
|
+
// must be normalized here, or realpath throws on the nonexistent leaf, the raw `..`
|
|
13390
|
+
// survives, normalizeFsPath rejects it, and the policy anchors the DEFAULT store
|
|
13391
|
+
// while lark-cli opens the Clean'd real one → leak). canonicalNearestAncestor then
|
|
13392
|
+
// resolves any SYMLINKED ancestor even when the store leaf does not exist yet (a
|
|
13393
|
+
// full-path realpath would throw and skip symlink resolution → policy/CLI namespace
|
|
13394
|
+
// divergence). bwrap has no --clearenv, so the child inherits this bot's
|
|
13395
|
+
// LARKSUITE_CLI_DATA_DIR from the worker's own (frozen, non-agent-controllable)
|
|
13396
|
+
// process.env, and sandbox.ts pins the child to the SAME cleaned resolution
|
|
13397
|
+
// (--setenv/--unsetenv) so policy == CLI by construction. Ignored on darwin.
|
|
13398
|
+
larkCliLinuxStore: canonicalNearestAncestor(resolveLarkCliLinuxStoreDir(process.env.LARKSUITE_CLI_DATA_DIR, lexicalHome)),
|
|
12920
13399
|
redirectedCliData: willRedirectCliData,
|
|
12921
13400
|
cliDataPaths: willRedirectCliData ? undefined : keepExisting([
|
|
12922
13401
|
cliAdapter.claudeDataDir,
|
|
@@ -12968,7 +13447,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12968
13447
|
]),
|
|
12969
13448
|
botmuxInstallRoot,
|
|
12970
13449
|
outbox,
|
|
12971
|
-
extraWritePaths: keepExisting([process.env.TMPDIR]),
|
|
13450
|
+
extraWritePaths: keepExisting([process.env.TMPDIR, canonicalOmpSessionDir]),
|
|
12972
13451
|
userPaths,
|
|
12973
13452
|
mandatoryDenyPaths,
|
|
12974
13453
|
mandatoryDenyRegexes,
|
|
@@ -13055,6 +13534,25 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13055
13534
|
}
|
|
13056
13535
|
}
|
|
13057
13536
|
else {
|
|
13537
|
+
// Canonical lark-cli data root pinned into the child so the in-sandbox lark-cli
|
|
13538
|
+
// resolves the SAME physical keystore the policy denied/carved-out (avoiding a
|
|
13539
|
+
// symlinked-data-root namespace split). fsPolicyCtx.larkCliLinuxStore is the FULLY-canonical store dir (leaf
|
|
13540
|
+
// symlinks followed) — the physical location the policy protects. The child opens
|
|
13541
|
+
// `<pin>/lark-cli`, so pin = dirname(store) is same-source ONLY when the canonical
|
|
13542
|
+
// store's basename is still `lark-cli` (the common case, incl. a symlink that
|
|
13543
|
+
// resolves to another `lark-cli` dir). If the `lark-cli` leaf was a symlink to a
|
|
13544
|
+
// DIFFERENTLY-named dir (basename !== 'lark-cli'), dirname(store)+'/lark-cli' would
|
|
13545
|
+
// NOT equal the canonical store → child would open an unbound path (auth breaks) OR
|
|
13546
|
+
// a foreign one. There is no leak either way (the policy still denies the real dir,
|
|
13547
|
+
// and both candidate stores are locked), so degrade SAFELY: pin nothing → child
|
|
13548
|
+
// falls back to the default store (also locked), and log the exotic layout.
|
|
13549
|
+
const canonStore = fsPolicyCtx.larkCliLinuxStore;
|
|
13550
|
+
const childLarkDataRoot = larkCliChildDataRoot(canonStore);
|
|
13551
|
+
if (canonStore && childLarkDataRoot === null) {
|
|
13552
|
+
log(`[sandbox] lark-cli keystore ${canonStore} does not end in 'lark-cli' (symlinked leaf?) — `
|
|
13553
|
+
+ `not pinning a custom LARKSUITE_CLI_DATA_DIR into the sandbox; the in-sandbox lark-cli will `
|
|
13554
|
+
+ `use the default store. The real keystore is still denied/carve-out'd by policy (no leak).`);
|
|
13555
|
+
}
|
|
13058
13556
|
const sbx = prepareDirectSandbox({
|
|
13059
13557
|
sessionId: cfg.sessionId,
|
|
13060
13558
|
dataDir,
|
|
@@ -13065,6 +13563,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13065
13563
|
cliArgs: args,
|
|
13066
13564
|
trustedBotmuxCommandPaths: [defaultGatewayEntry().command],
|
|
13067
13565
|
mcpGatewaySocketPath: sessionMcpGatewayHost?.socketPath,
|
|
13566
|
+
larkCliDataDir: childLarkDataRoot,
|
|
13068
13567
|
});
|
|
13069
13568
|
if (!sbx) {
|
|
13070
13569
|
// FAIL-SAFE: never silently run unsandboxed.
|
|
@@ -13606,6 +14105,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13606
14105
|
// can verify they were spawned inside a botmux session by walking the
|
|
13607
14106
|
// process tree and looking for a matching pid file in this directory.
|
|
13608
14107
|
const cliPid = backend.getChildPid?.();
|
|
14108
|
+
if (cfg.existingAppServerEndpoint !== undefined) {
|
|
14109
|
+
// The local `codex --remote` client does not own the selected rollout fd;
|
|
14110
|
+
// writeInput must accept only the explicitly bound remote thread in the
|
|
14111
|
+
// shared history.jsonl instead of applying its normal local-PID ownership
|
|
14112
|
+
// filter (which would reject the correct App Server submission).
|
|
14113
|
+
backend.expectedCodexSessionId = cfg.cliSessionId;
|
|
14114
|
+
}
|
|
13609
14115
|
publishLocalProcessAttestation(cliPid ?? undefined);
|
|
13610
14116
|
if (cliPid && process.env.SESSION_DATA_DIR) {
|
|
13611
14117
|
const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
|
|
@@ -13809,10 +14315,21 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13809
14315
|
hermesBridgeAttach(effectiveResume ? 'baseline-existing' : 'fresh-empty');
|
|
13810
14316
|
}
|
|
13811
14317
|
else if (cfg.cliId === 'codex') {
|
|
14318
|
+
if (cfg.existingAppServerEndpoint) {
|
|
14319
|
+
// A shared Codex App thread has two legitimate writers: the App and
|
|
14320
|
+
// BotMux's official `codex --remote` client. Enable local-turn
|
|
14321
|
+
// synthesis only from this share's attach boundary, so older thread
|
|
14322
|
+
// history is absorbed while future App-side turns are mirrored to Lark.
|
|
14323
|
+
codexAdoptStartMs = Date.now();
|
|
14324
|
+
codexBridgeQueue.setLocalTurns(true, codexAdoptStartMs);
|
|
14325
|
+
log('Codex bridge enabled App Server shared-turn forwarding');
|
|
14326
|
+
}
|
|
13812
14327
|
if (effectiveCliSessionId) {
|
|
13813
14328
|
const rolloutPath = findCodexRolloutBySessionId(effectiveCliSessionId);
|
|
13814
14329
|
if (rolloutPath) {
|
|
13815
|
-
codexBridgeAttach(rolloutPath,
|
|
14330
|
+
codexBridgeAttach(rolloutPath, cfg.existingAppServerEndpoint
|
|
14331
|
+
? 'split-live'
|
|
14332
|
+
: effectiveResume ? 'baseline-existing' : 'fresh-empty');
|
|
13816
14333
|
}
|
|
13817
14334
|
else {
|
|
13818
14335
|
codexBridgePendingSessionId = effectiveCliSessionId;
|
|
@@ -13858,7 +14375,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13858
14375
|
codexBridgeStartTimer();
|
|
13859
14376
|
}
|
|
13860
14377
|
}
|
|
13861
|
-
else if (cfg.cliId === 'pi' || cfg.cliId === 'grok') {
|
|
14378
|
+
else if (cfg.cliId === 'pi' || cfg.cliId === 'grok' || cfg.cliId === 'oh-my-pi') {
|
|
13862
14379
|
// File-backed: pin path when known (pi session id / grok --session-id
|
|
13863
14380
|
// UUID), else arm the poller. Grok collision-fallback (dir already
|
|
13864
14381
|
// exists → no --session-id → grok mints id) is recovered via writeInput
|
|
@@ -14012,7 +14529,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
14012
14529
|
// itself fail-opens on non-authoritative screens (ZMX), so a stale
|
|
14013
14530
|
// `Working...` in ZMX history can never block a structured terminal.
|
|
14014
14531
|
const busyGuardedIdle = evidenceSource === 'screen'
|
|
14015
|
-
|| (evidenceSource === 'external'
|
|
14532
|
+
|| (evidenceSource === 'external'
|
|
14533
|
+
&& (structuredBridgeIsPi() || structuredBridgeIsOmp()));
|
|
14016
14534
|
if (busyGuardedIdle && idleBackend
|
|
14017
14535
|
&& deferPromptReadyWhileBusy(`${cliName()} ${evidenceSource}-idle`, idleBackend))
|
|
14018
14536
|
return;
|
|
@@ -14254,7 +14772,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
14254
14772
|
log('Suppressed claude_exit for intentional in-worker restart');
|
|
14255
14773
|
}
|
|
14256
14774
|
else {
|
|
14257
|
-
send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic, turnId: exitedTurnId, dispatchAttempt: exitedDispatchAttempt });
|
|
14775
|
+
send({ type: 'claude_exit', code, signal, logTail, canParkDiagnostic, turnId: exitedTurnId, dispatchAttempt: exitedDispatchAttempt, ...(lastInitConfig?.cliId === 'codex-app' && code === CODEX_APP_ACTIVE_WRITER_EXIT_CODE ? { codexAppActiveWriter: true } : {}) });
|
|
14258
14776
|
}
|
|
14259
14777
|
});
|
|
14260
14778
|
const isPersistentBackendReattach = actuallyReattachedPersistent;
|
|
@@ -14524,8 +15042,8 @@ function killCli(opts = {}) {
|
|
|
14524
15042
|
appRunnerControlDecoder.reset();
|
|
14525
15043
|
}
|
|
14526
15044
|
async function restartCliProcess(reason, opts = {}) {
|
|
14527
|
-
if (lastInitConfig?.adoptMode) {
|
|
14528
|
-
log(`Restart ignored in adopt mode (${reason})`);
|
|
15045
|
+
if (lastInitConfig?.adoptMode || lastInitConfig?.existingAppServerEndpoint) {
|
|
15046
|
+
log(`Restart ignored in shared-adopt mode (${reason})`);
|
|
14529
15047
|
return;
|
|
14530
15048
|
}
|
|
14531
15049
|
// Invalidate queued/deferred callbacks before async teardown begins. Waiting
|
|
@@ -14718,8 +15236,29 @@ function startWebServer(host, preferredPort) {
|
|
|
14718
15236
|
res.end('Forbidden');
|
|
14719
15237
|
return;
|
|
14720
15238
|
}
|
|
15239
|
+
// #933 回归修复:平台注入的 Cookie/Role 会被中央前门剥掉(P1-6 / 内部 grant),
|
|
15240
|
+
// 此时 platformReadonly 判不出来;前门改用这个展示层提示头把「平台认证过的只读
|
|
15241
|
+
// 访客」这一事实带过来。只影响只读页渲染哪条横幅(SSO 登录引导 vs 纯只读提示),
|
|
15242
|
+
// 不参与任何授权判定——hasRead/hasWrite 在它之前就已定死。
|
|
15243
|
+
const platformReadonlyHint = !hasWrite
|
|
15244
|
+
&& req.headers[TERMINAL_PLATFORM_READONLY_HINT_HEADER] !== undefined;
|
|
14721
15245
|
const loginHdr = req.headers['x-botmux-login-url'];
|
|
14722
|
-
|
|
15246
|
+
// The central front proxy only injects X-Botmux-Login-Url on the SPA 401
|
|
15247
|
+
// path, never on `/s/` terminal requests — so a read-only web terminal
|
|
15248
|
+
// showed an owner-login banner that was a dead <div> (no href). When the
|
|
15249
|
+
// header is absent, self-derive the platform owner-login URL for THIS
|
|
15250
|
+
// session's terminal path: `/open/<machineId>?next=/s/<sessionId>`. The
|
|
15251
|
+
// `/s/` next routes the platform to the terminal subdomain surface and,
|
|
15252
|
+
// after login, lands the browser back on this terminal WITH the host-only
|
|
15253
|
+
// proxy-session cookie the owner check requires (the cross-subdomain SSO
|
|
15254
|
+
// cookie alone does not make the request writable). Returns '' when this
|
|
15255
|
+
// machine is unbound or remote access is off, so the banner falls back to
|
|
15256
|
+
// its non-link text form rather than a broken link.
|
|
15257
|
+
const headerLoginUrl = typeof loginHdr === 'string' && /^https?:\/\/[^"'<>\s]+$/.test(loginHdr)
|
|
15258
|
+
? loginHdr
|
|
15259
|
+
: '';
|
|
15260
|
+
const loginUrl = headerLoginUrl
|
|
15261
|
+
|| (sessionId ? deriveTerminalLoginUrl(sessionId) : '');
|
|
14723
15262
|
// Herdr snapshots contain screen cells but not terminal mode state. On a
|
|
14724
15263
|
// refreshed page an alt-screen CLI would otherwise look like an empty
|
|
14725
15264
|
// normal buffer until resize causes a redraw. Preserve that mode so
|
|
@@ -14743,7 +15282,7 @@ function startWebServer(host, preferredPort) {
|
|
|
14743
15282
|
|| effectiveBackendType === 'tmux'
|
|
14744
15283
|
|| effectiveBackendType === 'zellij';
|
|
14745
15284
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
14746
|
-
res.end(getTerminalHtml(hasWrite, platformReadonly, loginUrl, forceRemoteScroll, localTerminalBackend, allowReadOnlyRemoteScroll));
|
|
15285
|
+
res.end(getTerminalHtml(hasWrite, platformReadonly || platformReadonlyHint, loginUrl, forceRemoteScroll, localTerminalBackend, allowReadOnlyRemoteScroll));
|
|
14747
15286
|
});
|
|
14748
15287
|
wss = new WebSocketServer({
|
|
14749
15288
|
server: httpServer,
|
|
@@ -14799,6 +15338,26 @@ function startWebServer(host, preferredPort) {
|
|
|
14799
15338
|
if (hasWrite)
|
|
14800
15339
|
authedClients.add(ws);
|
|
14801
15340
|
log(`WS client connected (total: ${wsClients.size}, write: ${hasWrite})`);
|
|
15341
|
+
// The page only knows the verdict of its own HTTP GET. This upgrade was a
|
|
15342
|
+
// SECOND, independent authorization — and an iOS WebView does not send
|
|
15343
|
+
// cookies on it, so the very same page can be judged writable over HTTP
|
|
15344
|
+
// and read-only here. Tell the page what THIS socket actually got: it
|
|
15345
|
+
// stops forwarding input it knows will be dropped, and reports the truth
|
|
15346
|
+
// up to whatever embeds it (the Workbench terminal pane reads it to label
|
|
15347
|
+
// the channel).
|
|
15348
|
+
//
|
|
15349
|
+
// It used to ride the PTY byte stream as an OSC sequence, which the page
|
|
15350
|
+
// then scanned for on EVERY frame — so any process running inside a
|
|
15351
|
+
// read-only terminal could print the same bytes and flip the page's own
|
|
15352
|
+
// write verdict (or DoS itself into read-only). It is now an out-of-band
|
|
15353
|
+
// control frame that MUST be the first message on this socket, sent in the
|
|
15354
|
+
// same synchronous tick that registered it: the page only ever decodes
|
|
15355
|
+
// frame #1 of a connection as control, so PTY output can never be mistaken
|
|
15356
|
+
// for it. Nothing awaits between `wsClients.add` above and this send.
|
|
15357
|
+
try {
|
|
15358
|
+
ws.send(terminalWriteFrame(hasWrite));
|
|
15359
|
+
}
|
|
15360
|
+
catch { /* already closing */ }
|
|
14802
15361
|
// A signed Dashboard grant is fixed-expiry — for READ scope as much as
|
|
14803
15362
|
// for write (P1-5). Even if the central proxy's socket invalidation is
|
|
14804
15363
|
// delayed or bypassed, the worker independently removes any granted
|
|
@@ -15254,6 +15813,18 @@ ${loginUrl ? `<a id="login-banner" href="${loginUrl}" target="_top" rel="noopene
|
|
|
15254
15813
|
var isTouch='ontouchstart'in window||navigator.maxTouchPoints>0;
|
|
15255
15814
|
if(isTouch){document.body.classList.add('touch');}
|
|
15256
15815
|
var hasToken=${hasWrite};
|
|
15816
|
+
// hasToken 是**这一次 HTTP GET** 的判定,只当初值。真正说了算的是下面这条:worker 在
|
|
15817
|
+
// WebSocket 握手完成时把「这条连接实际拿到什么权限」发下来(OSC 1989 write),落成
|
|
15818
|
+
// wsHasWrite。两者会不一致——iOS WebView 的 WS 升级不带 Cookie,页面 HTTP 判可写、
|
|
15819
|
+
// WS 却只读,照抄 hasToken 就会让人对着一个不收字的终端打字。null = 还没连上。
|
|
15820
|
+
var wsHasWrite=null;
|
|
15821
|
+
// 「这条连接拿到什么权限」的解码器 —— 与 worker 侧同一份实现,原样嵌进来(这段页面
|
|
15822
|
+
// 代码住在模板字符串里,import 不进来,所以只能一份源码两处跑)。
|
|
15823
|
+
// 规则:只在**本连接的第一帧**上尝试,且要求整帧精确匹配。PTY 里跑的进程打出一模
|
|
15824
|
+
// 一样的字节也翻不动权限——它永远不可能是第一帧。
|
|
15825
|
+
var _wbDecodeWriteFrame=${decodeTerminalWriteFrameSource};
|
|
15826
|
+
// 本连接还没消费过首帧。每次 connect() 重置:上一条连接的结论套不到新连接上。
|
|
15827
|
+
var _wbFirstFrame=true;
|
|
15257
15828
|
var platformReadonly=${platformReadonly};
|
|
15258
15829
|
var remoteScroll=${forceRemoteScroll};
|
|
15259
15830
|
var localTerminalBackend=${localTerminalBackend};
|
|
@@ -15331,6 +15902,39 @@ function _wbApplyAutoFontSize(){
|
|
|
15331
15902
|
return true;
|
|
15332
15903
|
}
|
|
15333
15904
|
fit.fit();
|
|
15905
|
+
// 这条通道到底能不能写,以**已经建立的 WS** 为准(见页面顶部 wsHasWrite 那段注释)。
|
|
15906
|
+
// 结论一到手就做两件事:① 页面自己收起输入,别把注定被丢掉的字发出去;② 上抛给嵌入
|
|
15907
|
+
// 方(工作台终端面板据此显示「可输入 / 只读」,而不是照抄 HTTP 的 hasToken)。
|
|
15908
|
+
// 上抛的目标 origin 绝不用星号:优先取 location.ancestorOrigins(WebKit / Blink 都有,
|
|
15909
|
+
// iOS WebView 正是 WebKit),取不到就退回「父页给我们发外观消息时带的那个 origin」;
|
|
15910
|
+
// 两者都没有就不发——宁可不上抛,也不广播出去。
|
|
15911
|
+
// 注意:这一整段在 worker.ts 的模板字符串里,注释和代码都不能出现反引号,
|
|
15912
|
+
// 也不能出现「美元号 + 左花括号」的插值起始序列,否则会把模板字符串截断。
|
|
15913
|
+
var _wbParentOrigin=null;
|
|
15914
|
+
try{
|
|
15915
|
+
var _wbAo=window.location.ancestorOrigins;
|
|
15916
|
+
if(_wbAo&&_wbAo.length&&_wbAo[0]&&_wbAo[0]!=='null')_wbParentOrigin=_wbAo[0];
|
|
15917
|
+
}catch(_e){}
|
|
15918
|
+
function _wbPostWrite(){
|
|
15919
|
+
if(window.parent===window||!_wbParentOrigin)return;
|
|
15920
|
+
// wsHasWrite===null 也要上抛:那是「这条连接退回未知」的一次明确通知(重连开始时
|
|
15921
|
+
// 发),嵌入方据此把上一条连接的结论清掉,而不是继续显示一个过期的判定。
|
|
15922
|
+
try{window.parent.postMessage({type:'botmux:wb-terminal-write',write:wsHasWrite},_wbParentOrigin)}catch(_e){}
|
|
15923
|
+
}
|
|
15924
|
+
function _wbSetWsWrite(v){
|
|
15925
|
+
var changed=wsHasWrite!==v;
|
|
15926
|
+
wsHasWrite=v;
|
|
15927
|
+
if(changed){
|
|
15928
|
+
var _wb=document.getElementById('readonly-banner');
|
|
15929
|
+
// 页面以为自己可写、这条连接其实只读:把只读横幅亮出来。反过来(连上后确认可写)
|
|
15930
|
+
// 就把它收掉,别留一条与事实相反的提示。null(未知)两边都不动:还没有依据。
|
|
15931
|
+
if(_wb&&!platformReadonly){
|
|
15932
|
+
if(v===false&&hasToken)_wb.classList.add('show');
|
|
15933
|
+
else if(v===true)_wb.classList.remove('show');
|
|
15934
|
+
}
|
|
15935
|
+
}
|
|
15936
|
+
_wbPostWrite();
|
|
15937
|
+
}
|
|
15334
15938
|
// 工作台下发的终端外观(消息类型 botmux:wb-appearance)。终端画布住在这个跨文档
|
|
15335
15939
|
// iframe 里,父页换 class / 换 CSS 变量都传不进来,配色只能靠 postMessage 递过来。
|
|
15336
15940
|
// 注意:这一整段在 worker.ts 的模板字符串里,注释和代码都不能出现反引号,
|
|
@@ -15354,17 +15958,22 @@ window.addEventListener('message',function(_ev){
|
|
|
15354
15958
|
if(_ev.source===window||_ev.source!==window.parent)return;
|
|
15355
15959
|
var _d=_ev.data;
|
|
15356
15960
|
if(!_d||_d.type!=='botmux:wb-appearance')return;
|
|
15961
|
+
// 这一条消息本身就证明了父页的 origin(来源已经校验过是 window.parent)。
|
|
15962
|
+
// ancestorOrigins 拿不到时(Firefox)靠它补上,并把攒着的写权限结论补发一次。
|
|
15963
|
+
if(!_wbParentOrigin&&_ev.origin&&_ev.origin!=='null'){_wbParentOrigin=_ev.origin;_wbPostWrite();}
|
|
15357
15964
|
var _theme=_wbSanitizeTheme(_d.theme);
|
|
15358
15965
|
if(!_theme)return;
|
|
15359
15966
|
try{
|
|
15360
15967
|
term.options.theme=_theme;
|
|
15361
|
-
//
|
|
15968
|
+
// 阅读风走 1.15 的行距;经典保持 xterm 默认的 1,「经典 = 原样」的一部分。
|
|
15362
15969
|
// 经典那套色值与上面写死的字面量逐色相同,所以没开阅读风的用户零变化。
|
|
15363
15970
|
// 这两个数字的唯一出处是 WORKBENCH_TERM_LINE_HEIGHTS(agent-workbench-appearance.ts),
|
|
15364
15971
|
// 接缝测试按那张表比对这一行的字面量:改一处必须两处一起改。
|
|
15365
15972
|
// 曾经是 1.55,单元格高约 24.5px,CLI 底部那块固定 8 行的 chrome(提示 + 输入框 +
|
|
15366
|
-
// 状态条)就要占掉约 196px —— 一屏四分之一全是 chrome
|
|
15367
|
-
|
|
15973
|
+
// 状态条)就要占掉约 196px —— 一屏四分之一全是 chrome,正文被挤走,所以先降到 1.3。
|
|
15974
|
+
// 1.3 对中文密集的 TUI 内容还是偏松:汉字撑满 em 框,行间空隙观感接近隔行,于是
|
|
15975
|
+
// 再收一档到 1.15(单元格高约 18.2px),比经典松一成半,读起来才连成一段。
|
|
15976
|
+
term.options.lineHeight=_d.termStyle==='reader'?1.15:1;
|
|
15368
15977
|
// 行距变了可视行数就变,必须复算一次。几何契约不动:只有画布内重绘。
|
|
15369
15978
|
fit.fit();
|
|
15370
15979
|
}catch(_e){}
|
|
@@ -15550,10 +16159,17 @@ function _sendInput(d){if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:
|
|
|
15550
16159
|
var _MOTION_RE=/^(?:\\x1b\\[<(?:35|39|43|47|51|55|59|63);\\d+;\\d+[Mm])+$/;
|
|
15551
16160
|
var _motionPend=null,_motionT=0;
|
|
15552
16161
|
term.onData(function(d){
|
|
15553
|
-
|
|
16162
|
+
// 只有已建立的 WS 明确回报「可写」(wsHasWrite===true) 才放行输入。null(还没确认,
|
|
16163
|
+
// 含重连中)和 false(这条连接判成只读)一律不发——桌面也要等首帧。照抄 hasToken
|
|
16164
|
+
// (这一次 HTTP GET 的判定)会在 WS 不带 Cookie 的 iOS WebView 上让人对着一个不收字
|
|
16165
|
+
// 的终端打字,按键石沉大海最难自查(第 17 点)。
|
|
16166
|
+
if(wsHasWrite!==true){
|
|
15554
16167
|
// Mouse escape sequences are input too: a TUI can bind clicks or wheel
|
|
15555
16168
|
// events to actions. View links never forward terminal bytes.
|
|
15556
|
-
|
|
16169
|
+
// 已经确定只读(这条连接判 false,或 HTTP GET 本来就没给写)才弹只读提示;null 是
|
|
16170
|
+
// 还没连上的短暂过渡,此时 ws 多半也没 OPEN,静默丢弃即可,别闪一条与事实相反的只读。
|
|
16171
|
+
if(!hasToken||wsHasWrite===false)_showReadonlyToast();
|
|
16172
|
+
return;
|
|
15557
16173
|
}
|
|
15558
16174
|
if(_MOTION_RE.test(d)){
|
|
15559
16175
|
// Trailing throttle: keep only the LATEST motion, flush every 90ms. Hover
|
|
@@ -15614,6 +16230,10 @@ if(typeof ResizeObserver!=='undefined'){
|
|
|
15614
16230
|
var proto=location.protocol==='https:'?'wss':'ws';
|
|
15615
16231
|
var ws=new WebSocket(proto+'://'+location.host+base+'/'+location.search);
|
|
15616
16232
|
ws_=ws;ws.binaryType='arraybuffer';
|
|
16233
|
+
// 新连接 = 新的一次鉴权。上一条连接拿到的权限说明不了这一条,先退回未知(并把这
|
|
16234
|
+
// 个「未知」上抛给嵌入方),等这条连接自己的首帧说话。
|
|
16235
|
+
_wbFirstFrame=true;
|
|
16236
|
+
_wbSetWsWrite(null);
|
|
15617
16237
|
// Force a resize on every (re)connect: clear the dedup memory first. On
|
|
15618
16238
|
// reconnect the browser grid is usually unchanged, so without this the
|
|
15619
16239
|
// dedup in sendResize() would suppress the resize — but a reconnect often
|
|
@@ -15623,6 +16243,14 @@ if(typeof ResizeObserver!=='undefined'){
|
|
|
15623
16243
|
// (status-line update bleeds into the line below). Always re-assert size.
|
|
15624
16244
|
ws.onopen=function(){el.textContent='connected';el.className='ok';_lastC=_lastR=0;sendResize()};
|
|
15625
16245
|
ws.onmessage=function(e){
|
|
16246
|
+
// 带外控制帧只可能是**本连接的第一帧**,而且必须整帧精确匹配(见页面顶部
|
|
16247
|
+
// _wbDecodeWriteFrame 那段注释)。首帧一旦消费掉,这条连接上之后的每一个字节
|
|
16248
|
+
// 就都只是 PTY 输出 —— 里面的进程再怎么打印控制帧的字面量也翻不动权限。
|
|
16249
|
+
if(_wbFirstFrame){
|
|
16250
|
+
_wbFirstFrame=false;
|
|
16251
|
+
var _ctl=_wbDecodeWriteFrame(e.data,true);
|
|
16252
|
+
if(_ctl!==null){_wbSetWsWrite(_ctl);return;}
|
|
16253
|
+
}
|
|
15626
16254
|
var data=typeof e.data==='string'?e.data:new TextDecoder().decode(e.data);
|
|
15627
16255
|
// Snapshot-aware Herdr history replaces the buffer instead of appending a
|
|
15628
16256
|
// mostly-overlapping full screen. Preserve the reader's anchor when older
|
|
@@ -15663,7 +16291,15 @@ if(typeof ResizeObserver!=='undefined'){
|
|
|
15663
16291
|
if(m){try{_clipBuf=new TextDecoder().decode(Uint8Array.from(atob(m[1]),function(c){return c.charCodeAt(0)}));_doCopy(_clipBuf);_showCopied()}catch(ex){}}
|
|
15664
16292
|
term.write(data,_settleInitialBottom);
|
|
15665
16293
|
};
|
|
15666
|
-
ws.onclose=function(){
|
|
16294
|
+
ws.onclose=function(){
|
|
16295
|
+
ws_=null;el.textContent='disconnected';el.className='err';
|
|
16296
|
+
// 关闭当下就把这条连接的写权限退回未知:先复位首帧标志(重连后要等新首帧才恢复
|
|
16297
|
+
// 结论),再 _wbSetWsWrite(null) —— 它同时收起输入(term.onData/toolbar/_fwdScroll
|
|
16298
|
+
// 的门禁都以 wsHasWrite===true 为准)并向嵌入方上抛 write:null。不这样做的话,断线
|
|
16299
|
+
// 到 2 秒后重连的空窗里,页面仍以上一条连接的旧判定放行输入、父页也还显示旧的可写。
|
|
16300
|
+
_wbFirstFrame=true;_wbSetWsWrite(null);
|
|
16301
|
+
setTimeout(connect,2000);
|
|
16302
|
+
};
|
|
15667
16303
|
ws.onerror=function(){ws.close()};
|
|
15668
16304
|
})();
|
|
15669
16305
|
|
|
@@ -15734,6 +16370,12 @@ function _cellAt(clientX,clientY){
|
|
|
15734
16370
|
}
|
|
15735
16371
|
function _fwdScroll(px,coord){
|
|
15736
16372
|
if((!hasToken&&!readOnlyRemoteScroll)||!ws_||ws_.readyState!==1||!px)return;
|
|
16373
|
+
// 写链路(hasToken)转发的是 type:'input',与 term.onData / toolbar 同一条判据:只有已
|
|
16374
|
+
// 建立的 WS 明确回报可写(wsHasWrite===true)才放行;null(未确认,含重连中)与 false
|
|
16375
|
+
// (这条连接只读)一律不发——照抄 hasToken 会在 WS 不带 Cookie 时把输入打进一个原地
|
|
16376
|
+
// 丢字的终端(第 17 点)。只读远程滚动(hasToken=false)发的是 type:'scroll',不是输入,
|
|
16377
|
+
// 不受这道门约束(服务端另有校验)。
|
|
16378
|
+
if(hasToken&&wsHasWrite!==true)return;
|
|
15737
16379
|
coord=coord||(((term.cols>>1)+1)+';'+((term.rows>>1)+1)); // never (1,1)
|
|
15738
16380
|
var dir=px<0?-1:1;
|
|
15739
16381
|
if(_scrollBurstDir&&dir!==_scrollBurstDir){_scrollAccum=0;_scrollBurstTicks=0;}
|
|
@@ -15978,6 +16620,9 @@ if(isTouch&&hasToken){
|
|
|
15978
16620
|
var press=null;
|
|
15979
16621
|
function fire(){
|
|
15980
16622
|
if(!ws_||ws_.readyState!==1)return;
|
|
16623
|
+
// 与 term.onData 同一条判据:只有 WS 明确回报可写(wsHasWrite===true)才发;null
|
|
16624
|
+
// (还没确认)与 false(只读)都不发,桌面也等首帧(第 17 点)。
|
|
16625
|
+
if(wsHasWrite!==true){if(wsHasWrite===false)_showReadonlyToast();return;}
|
|
15981
16626
|
var k=km[btn.getAttribute('data-k')];
|
|
15982
16627
|
if(k)ws_.send(JSON.stringify({type:'input',data:k}));
|
|
15983
16628
|
}
|
|
@@ -16350,6 +16995,10 @@ process.on('message', async (raw) => {
|
|
|
16350
16995
|
if (msg.locale === 'zh' || msg.locale === 'en') {
|
|
16351
16996
|
setDefaultLocale(msg.locale);
|
|
16352
16997
|
}
|
|
16998
|
+
// Wire user prompt-key overrides into t() so worker-side prompt building
|
|
16999
|
+
// (claude-family `--append-system-prompt` via buildBotmuxSystemPromptText,
|
|
17000
|
+
// mojo catalog, etc.) honours customizations. Idempotent per worker.
|
|
17001
|
+
registerPromptOverrideResolver();
|
|
16353
17002
|
// Scope session store to this bot's per-bot file.
|
|
16354
17003
|
// Slice C0: workflow-spawned workers (BOTMUX_WORKFLOW=1) skip this —
|
|
16355
17004
|
// their `sessionId` is synthetic (`wf-<runId>-<activityId>-...`) and
|
|
@@ -16800,12 +17449,23 @@ process.on('message', async (raw) => {
|
|
|
16800
17449
|
// shell。bareShellCheckInProgress 覆盖“检查进行中”、bareShellLaunchBlocked
|
|
16801
17450
|
// 覆盖“仍停在裸 shell 的安全 hold”两种状态,一并入队;若该进程随后出现
|
|
16802
17451
|
// 真实 PTY prompt 且 leaf 已变为非 shell,markPromptReady 会恢复排空。
|
|
17452
|
+
// Hook-review menus are another direct-write hazard: raw_input normally
|
|
17453
|
+
// preserves busy delivery, but its Enter/arrow sequence must never drive
|
|
17454
|
+
// the menu's current selection.
|
|
17455
|
+
refreshHookReviewInputHold(lastAnalyzerSnapshot || renderer?.rawSnapshot() || '');
|
|
16803
17456
|
if (cliRestartInProgress || rawInputRestartGate || sessionRenameInFlight()
|
|
16804
17457
|
|| shouldHoldCodexRunnerInput(codexRunnerFreshness)
|
|
16805
17458
|
|| injectionFlushing || shouldDeferUserFlush(pendingInjections)
|
|
16806
|
-
|| bareShellCheckInProgress || bareShellLaunchBlocked
|
|
17459
|
+
|| bareShellCheckInProgress || bareShellLaunchBlocked
|
|
17460
|
+
|| hookReviewInputHold) {
|
|
16807
17461
|
freshnessInputQueue.enqueueRaw(msg);
|
|
16808
|
-
|
|
17462
|
+
if (hookReviewInputHold) {
|
|
17463
|
+
notifyHookReviewInputHold();
|
|
17464
|
+
log(`Deferred passthrough slash command while Hook review is visible: ${msg.content}`);
|
|
17465
|
+
}
|
|
17466
|
+
else {
|
|
17467
|
+
log(`Deferred passthrough slash command until CLI input gate settles: ${msg.content}`);
|
|
17468
|
+
}
|
|
16809
17469
|
}
|
|
16810
17470
|
else {
|
|
16811
17471
|
await deliverRawInput(msg);
|
|
@@ -17198,6 +17858,15 @@ process.on('message', async (raw) => {
|
|
|
17198
17858
|
log('Late ready signal after timeout fallback — marking prompt ready now');
|
|
17199
17859
|
markPromptReady();
|
|
17200
17860
|
}
|
|
17861
|
+
// CLI 启动确证(SessionStart hook 已跑):复位连续重启计数——resume 目标
|
|
17862
|
+
// 显然存在且加载成功。这和 prompt 侧复位是同一语义,但更早:2026-08-20
|
|
17863
|
+
// 事故里 resume 后的 CLI 已 ready 二十多秒、还没等到 prompt 复位就被共享
|
|
17864
|
+
// server 抖动误杀,第二次重启因此被 tier-2 强制 FRESH 白丢上下文。到过
|
|
17865
|
+
// ready 的 spawn 崩溃不是「resume 目标不存在」问题,下一轮应有全新预算。
|
|
17866
|
+
if (consecutiveInWorkerRestarts > 0) {
|
|
17867
|
+
log(`SessionStart ready — resetting consecutive restart count (was ${consecutiveInWorkerRestarts})`);
|
|
17868
|
+
consecutiveInWorkerRestarts = 0;
|
|
17869
|
+
}
|
|
17201
17870
|
if (msg.requestId) {
|
|
17202
17871
|
send({ type: 'session_ready_ack', requestId: msg.requestId });
|
|
17203
17872
|
}
|
|
@@ -17359,11 +18028,13 @@ process.on('message', async (raw) => {
|
|
|
17359
18028
|
catch { /* logged by backend */ }
|
|
17360
18029
|
}
|
|
17361
18030
|
killCli();
|
|
17362
|
-
// Bridge marker
|
|
17363
|
-
// restarts so a mid-flight send is still credited
|
|
17364
|
-
//
|
|
17365
|
-
// same sessionId starts
|
|
18031
|
+
// Bridge marker + turn-journal files outlive a single CLI process (kept
|
|
18032
|
+
// across restarts so a mid-flight send is still credited and an
|
|
18033
|
+
// interrupted turn can be restored), but a real close tears down the
|
|
18034
|
+
// session — purge both so a future re-use of the same sessionId starts
|
|
18035
|
+
// clean.
|
|
17366
18036
|
clearSendMarkers();
|
|
18037
|
+
clearBridgeTurnJournalFile();
|
|
17367
18038
|
cleanup();
|
|
17368
18039
|
process.exit(0);
|
|
17369
18040
|
}
|
|
@@ -17536,6 +18207,7 @@ process.on('message', async (raw) => {
|
|
|
17536
18207
|
stopCodexBridge();
|
|
17537
18208
|
killCli();
|
|
17538
18209
|
clearSendMarkers();
|
|
18210
|
+
clearBridgeTurnJournalFile();
|
|
17539
18211
|
cleanup();
|
|
17540
18212
|
process.exit(0);
|
|
17541
18213
|
}
|
|
@@ -17695,10 +18367,29 @@ function cleanup() {
|
|
|
17695
18367
|
// cleanup retires the lease. SIGKILL residue is reclaimed by the next worker.
|
|
17696
18368
|
releaseCodexAppPosixOwnerLease();
|
|
17697
18369
|
}
|
|
17698
|
-
|
|
17699
|
-
|
|
18370
|
+
/**
|
|
18371
|
+
* A shared existing-App-Server session owns this worker and its bmx-* remote
|
|
18372
|
+
* TUI, but never owns the App Server/thread behind that TUI. On daemon
|
|
18373
|
+
* restart, preserve the remote TUI so the replacement daemon can reattach it;
|
|
18374
|
+
* only retire this worker's HTTP/WebSocket observers. Ordinary managed sessions
|
|
18375
|
+
* retain the historical killCli shutdown path.
|
|
18376
|
+
*/
|
|
18377
|
+
function shutdownWorkerForParentExit(reason) {
|
|
18378
|
+
stopScreenshotLoop();
|
|
18379
|
+
if (lastInitConfig?.existingAppServerEndpoint) {
|
|
18380
|
+
log(`Preserving existing-App-Server remote TUI during ${reason}`);
|
|
18381
|
+
cleanup();
|
|
18382
|
+
process.exit(0);
|
|
18383
|
+
return;
|
|
18384
|
+
}
|
|
18385
|
+
killCli();
|
|
18386
|
+
cleanup();
|
|
18387
|
+
process.exit(0);
|
|
18388
|
+
}
|
|
18389
|
+
process.on('SIGTERM', () => shutdownWorkerForParentExit('SIGTERM'));
|
|
18390
|
+
process.on('SIGINT', () => shutdownWorkerForParentExit('SIGINT'));
|
|
17700
18391
|
// If parent daemon dies, IPC channel closes — clean up
|
|
17701
|
-
process.on('disconnect', () => { log('Daemon disconnected');
|
|
18392
|
+
process.on('disconnect', () => { log('Daemon disconnected'); shutdownWorkerForParentExit('IPC disconnect'); });
|
|
17702
18393
|
// Watchdog: belt-and-braces parent-death detection. SIGTERM and 'disconnect'
|
|
17703
18394
|
// should both reach us when the daemon dies, but if main thread is stuck in
|
|
17704
18395
|
// a sync path V8 silently buffers the signal and we end up as a ppid=1
|
|
@@ -17715,16 +18406,12 @@ setInterval(() => {
|
|
|
17715
18406
|
const currentPpid = process.ppid;
|
|
17716
18407
|
if (currentPpid !== ORIGINAL_PARENT_PID || currentPpid === 1) {
|
|
17717
18408
|
log(`Watchdog: parent pid changed (${ORIGINAL_PARENT_PID} → ${currentPpid}) — daemon died, exiting`);
|
|
17718
|
-
stopScreenshotLoop();
|
|
17719
18409
|
try {
|
|
17720
|
-
|
|
18410
|
+
shutdownWorkerForParentExit('parent watchdog');
|
|
17721
18411
|
}
|
|
17722
|
-
catch {
|
|
17723
|
-
|
|
17724
|
-
cleanup();
|
|
18412
|
+
catch {
|
|
18413
|
+
process.exit(0); /* best-effort exit if teardown itself faults */
|
|
17725
18414
|
}
|
|
17726
|
-
catch { /* best-effort */ }
|
|
17727
|
-
process.exit(0);
|
|
17728
18415
|
}
|
|
17729
18416
|
}, 30_000).unref();
|
|
17730
18417
|
// ─── Sandbox crash-time teardown ─────────────────────────────────────────────
|