botmux 3.14.0 → 3.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/.runtime-build-id +1 -1
- package/dist/adapters/backend/destroy-result.d.ts +159 -0
- package/dist/adapters/backend/destroy-result.d.ts.map +1 -0
- package/dist/adapters/backend/destroy-result.js +223 -0
- package/dist/adapters/backend/destroy-result.js.map +1 -0
- package/dist/adapters/backend/herdr-backend.d.ts +4 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
- package/dist/adapters/backend/herdr-backend.js +23 -1
- package/dist/adapters/backend/herdr-backend.js.map +1 -1
- package/dist/adapters/backend/mojo-backend.d.ts +577 -0
- package/dist/adapters/backend/mojo-backend.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-backend.js +2471 -0
- package/dist/adapters/backend/mojo-backend.js.map +1 -0
- package/dist/adapters/backend/mojo-budgets.d.ts +21 -0
- package/dist/adapters/backend/mojo-budgets.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-budgets.js +22 -0
- package/dist/adapters/backend/mojo-budgets.js.map +1 -0
- package/dist/adapters/backend/mojo-isolated-workspace.d.ts +69 -0
- package/dist/adapters/backend/mojo-isolated-workspace.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-isolated-workspace.js +213 -0
- package/dist/adapters/backend/mojo-isolated-workspace.js.map +1 -0
- package/dist/adapters/backend/mojo-process-tree.d.ts +292 -0
- package/dist/adapters/backend/mojo-process-tree.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-process-tree.js +486 -0
- package/dist/adapters/backend/mojo-process-tree.js.map +1 -0
- package/dist/adapters/backend/mojo-types.d.ts +542 -0
- package/dist/adapters/backend/mojo-types.d.ts.map +1 -0
- package/dist/adapters/backend/mojo-types.js +682 -0
- package/dist/adapters/backend/mojo-types.js.map +1 -0
- package/dist/adapters/backend/reproduce-command.js +3 -3
- package/dist/adapters/backend/reproduce-command.js.map +1 -1
- package/dist/adapters/backend/riff-backend.js +1 -1
- package/dist/adapters/backend/riff-backend.js.map +1 -1
- package/dist/adapters/backend/sandbox.d.ts +44 -7
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +30 -7
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/backend/session-backend-selector.d.ts +14 -1
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +82 -10
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +30 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +60 -0
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +108 -6
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/backend/zmx-backend.d.ts +11 -0
- package/dist/adapters/backend/zmx-backend.d.ts.map +1 -1
- package/dist/adapters/backend/zmx-backend.js +16 -1
- package/dist/adapters/backend/zmx-backend.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +2 -2
- 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 +7 -0
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/copilot.d.ts +3 -1
- package/dist/adapters/cli/copilot.d.ts.map +1 -1
- package/dist/adapters/cli/copilot.js +23 -5
- package/dist/adapters/cli/copilot.js.map +1 -1
- package/dist/adapters/cli/cursor.d.ts.map +1 -1
- package/dist/adapters/cli/cursor.js +26 -4
- package/dist/adapters/cli/cursor.js.map +1 -1
- package/dist/adapters/cli/dsh.d.ts.map +1 -1
- package/dist/adapters/cli/dsh.js +5 -1
- package/dist/adapters/cli/dsh.js.map +1 -1
- package/dist/adapters/cli/grok.d.ts.map +1 -1
- package/dist/adapters/cli/grok.js +24 -5
- package/dist/adapters/cli/grok.js.map +1 -1
- package/dist/adapters/cli/kimi.d.ts.map +1 -1
- package/dist/adapters/cli/kimi.js +19 -1
- package/dist/adapters/cli/kimi.js.map +1 -1
- package/dist/adapters/cli/mojo.d.ts +28 -0
- package/dist/adapters/cli/mojo.d.ts.map +1 -0
- package/dist/adapters/cli/mojo.js +58 -0
- package/dist/adapters/cli/mojo.js.map +1 -0
- package/dist/adapters/cli/pi.d.ts +20 -5
- package/dist/adapters/cli/pi.d.ts.map +1 -1
- package/dist/adapters/cli/pi.js +27 -5
- package/dist/adapters/cli/pi.js.map +1 -1
- package/dist/adapters/cli/read-isolation.d.ts +251 -1
- package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
- package/dist/adapters/cli/read-isolation.js +254 -1
- package/dist/adapters/cli/read-isolation.js.map +1 -1
- package/dist/adapters/cli/registry.d.ts +13 -1
- package/dist/adapters/cli/registry.d.ts.map +1 -1
- package/dist/adapters/cli/registry.js +19 -1
- package/dist/adapters/cli/registry.js.map +1 -1
- package/dist/adapters/cli/runner-input.d.ts +3 -3
- package/dist/adapters/cli/runner-input.d.ts.map +1 -1
- package/dist/adapters/cli/runner-input.js +4 -3
- package/dist/adapters/cli/runner-input.js.map +1 -1
- package/dist/adapters/cli/shared-hints.d.ts +3 -1
- package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
- package/dist/adapters/cli/shared-hints.js +51 -30
- package/dist/adapters/cli/shared-hints.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +33 -4
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +42 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +54 -4
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/arg-utils.d.ts +5 -0
- package/dist/cli/arg-utils.d.ts.map +1 -1
- package/dist/cli/arg-utils.js +7 -0
- package/dist/cli/arg-utils.js.map +1 -1
- package/dist/cli/bots-list-output.d.ts +130 -2
- package/dist/cli/bots-list-output.d.ts.map +1 -1
- package/dist/cli/bots-list-output.js +173 -27
- package/dist/cli/bots-list-output.js.map +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts +10 -3
- package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -1
- package/dist/cli/daemon-lifecycle-env.js +38 -11
- package/dist/cli/daemon-lifecycle-env.js.map +1 -1
- package/dist/cli/dashboard-command.d.ts +15 -0
- package/dist/cli/dashboard-command.d.ts.map +1 -1
- package/dist/cli/dashboard-command.js +22 -0
- package/dist/cli/dashboard-command.js.map +1 -1
- package/dist/cli/log-tail.d.ts +54 -0
- package/dist/cli/log-tail.d.ts.map +1 -0
- package/dist/cli/log-tail.js +167 -0
- package/dist/cli/log-tail.js.map +1 -0
- package/dist/cli/pm2-env.d.ts +16 -0
- package/dist/cli/pm2-env.d.ts.map +1 -0
- package/dist/cli/pm2-env.js +63 -0
- package/dist/cli/pm2-env.js.map +1 -0
- package/dist/cli/pm2-fleet-lock.d.ts +26 -0
- package/dist/cli/pm2-fleet-lock.d.ts.map +1 -0
- package/dist/cli/pm2-fleet-lock.js +50 -0
- package/dist/cli/pm2-fleet-lock.js.map +1 -0
- package/dist/cli/pm2-god-retirement.d.ts +59 -0
- package/dist/cli/pm2-god-retirement.d.ts.map +1 -0
- package/dist/cli/pm2-god-retirement.js +95 -0
- package/dist/cli/pm2-god-retirement.js.map +1 -0
- package/dist/cli/session-list-wake.d.ts +41 -0
- package/dist/cli/session-list-wake.d.ts.map +1 -0
- package/dist/cli/session-list-wake.js +102 -0
- package/dist/cli/session-list-wake.js.map +1 -0
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +26 -0
- package/dist/cli/workflow.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +941 -118
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-runner.js +261 -49
- package/dist/codex-app-runner.js.map +1 -1
- package/dist/core/cli-runtime-update.d.ts +8 -0
- package/dist/core/cli-runtime-update.d.ts.map +1 -1
- package/dist/core/cli-runtime-update.js +127 -8
- package/dist/core/cli-runtime-update.js.map +1 -1
- package/dist/core/close-residual.d.ts +64 -0
- package/dist/core/close-residual.d.ts.map +1 -0
- package/dist/core/close-residual.js +93 -0
- package/dist/core/close-residual.js.map +1 -0
- package/dist/core/closed-session-card.d.ts.map +1 -1
- package/dist/core/closed-session-card.js +11 -3
- package/dist/core/closed-session-card.js.map +1 -1
- package/dist/core/command-discovery.d.ts.map +1 -1
- package/dist/core/command-discovery.js +4 -1
- package/dist/core/command-discovery.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +129 -27
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts +3 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +230 -1
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/daemon-background-close.d.ts +19 -0
- package/dist/core/daemon-background-close.d.ts.map +1 -0
- package/dist/core/daemon-background-close.js +39 -0
- package/dist/core/daemon-background-close.js.map +1 -0
- package/dist/core/dashboard-command/overview.d.ts +4 -0
- package/dist/core/dashboard-command/overview.d.ts.map +1 -1
- package/dist/core/dashboard-command/overview.js +8 -1
- package/dist/core/dashboard-command/overview.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +415 -42
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/dashboard-rows.d.ts +4 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -1
- package/dist/core/dashboard-rows.js +3 -0
- package/dist/core/dashboard-rows.js.map +1 -1
- package/dist/core/dashboard-url.d.ts +37 -0
- package/dist/core/dashboard-url.d.ts.map +1 -1
- package/dist/core/dashboard-url.js +71 -0
- package/dist/core/dashboard-url.js.map +1 -1
- package/dist/core/deferred-schedule-settlement.d.ts +15 -1
- package/dist/core/deferred-schedule-settlement.d.ts.map +1 -1
- package/dist/core/deferred-schedule-settlement.js +4 -1
- package/dist/core/deferred-schedule-settlement.js.map +1 -1
- package/dist/core/device-isolation-daemon.d.ts +91 -1
- package/dist/core/device-isolation-daemon.d.ts.map +1 -1
- package/dist/core/device-isolation-daemon.js +346 -2
- package/dist/core/device-isolation-daemon.js.map +1 -1
- package/dist/core/local-terminal-opener.d.ts.map +1 -1
- package/dist/core/local-terminal-opener.js +5 -1
- package/dist/core/local-terminal-opener.js.map +1 -1
- package/dist/core/loopback-target.d.ts +14 -0
- package/dist/core/loopback-target.d.ts.map +1 -0
- package/dist/core/loopback-target.js +20 -0
- package/dist/core/loopback-target.js.map +1 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +16 -2
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/mojo-containment-command.d.ts +20 -0
- package/dist/core/mojo-containment-command.d.ts.map +1 -0
- package/dist/core/mojo-containment-command.js +266 -0
- package/dist/core/mojo-containment-command.js.map +1 -0
- package/dist/core/mojo-containment.d.ts +575 -0
- package/dist/core/mojo-containment.d.ts.map +1 -0
- package/dist/core/mojo-containment.js +1158 -0
- package/dist/core/mojo-containment.js.map +1 -0
- package/dist/core/mojo-launcher-env-quarantine.d.ts +30 -0
- package/dist/core/mojo-launcher-env-quarantine.d.ts.map +1 -0
- package/dist/core/mojo-launcher-env-quarantine.js +175 -0
- package/dist/core/mojo-launcher-env-quarantine.js.map +1 -0
- package/dist/core/mojo-session-identity.d.ts +3 -0
- package/dist/core/mojo-session-identity.d.ts.map +1 -0
- package/dist/core/mojo-session-identity.js +126 -0
- package/dist/core/mojo-session-identity.js.map +1 -0
- package/dist/core/persistent-backend.d.ts +32 -8
- package/dist/core/persistent-backend.d.ts.map +1 -1
- package/dist/core/persistent-backend.js +55 -17
- package/dist/core/persistent-backend.js.map +1 -1
- package/dist/core/plugins/mcp/gateway.d.ts +14 -1
- package/dist/core/plugins/mcp/gateway.d.ts.map +1 -1
- package/dist/core/plugins/mcp/gateway.js +70 -3
- package/dist/core/plugins/mcp/gateway.js.map +1 -1
- package/dist/core/plugins/mcp/host.d.ts +2 -0
- package/dist/core/plugins/mcp/host.d.ts.map +1 -1
- package/dist/core/plugins/mcp/host.js +2 -0
- package/dist/core/plugins/mcp/host.js.map +1 -1
- package/dist/core/plugins/pm2.d.ts.map +1 -1
- package/dist/core/plugins/pm2.js +16 -3
- package/dist/core/plugins/pm2.js.map +1 -1
- package/dist/core/plugins/service-manager.d.ts +9 -0
- package/dist/core/plugins/service-manager.d.ts.map +1 -1
- package/dist/core/plugins/service-manager.js +33 -16
- package/dist/core/plugins/service-manager.js.map +1 -1
- package/dist/core/preview-port-owner.d.ts +109 -0
- package/dist/core/preview-port-owner.d.ts.map +1 -0
- package/dist/core/preview-port-owner.js +320 -0
- package/dist/core/preview-port-owner.js.map +1 -0
- package/dist/core/remote-cli-ids.d.ts +5 -0
- package/dist/core/remote-cli-ids.d.ts.map +1 -0
- package/dist/core/remote-cli-ids.js +24 -0
- package/dist/core/remote-cli-ids.js.map +1 -0
- package/dist/core/{riff-shutdown-detach.d.ts → remote-shutdown-detach.d.ts} +40 -39
- package/dist/core/remote-shutdown-detach.d.ts.map +1 -0
- package/dist/core/{riff-shutdown-detach.js → remote-shutdown-detach.js} +85 -83
- package/dist/core/remote-shutdown-detach.js.map +1 -0
- package/dist/core/{riff-worker-shutdown-readiness.d.ts → remote-worker-shutdown-readiness.d.ts} +5 -4
- package/dist/core/remote-worker-shutdown-readiness.d.ts.map +1 -0
- package/dist/core/{riff-worker-shutdown-readiness.js → remote-worker-shutdown-readiness.js} +4 -3
- package/dist/core/remote-worker-shutdown-readiness.js.map +1 -0
- package/dist/core/reply-target.d.ts +16 -1
- package/dist/core/reply-target.d.ts.map +1 -1
- package/dist/core/reply-target.js +61 -0
- package/dist/core/reply-target.js.map +1 -1
- package/dist/core/session-group-birth.d.ts +26 -0
- package/dist/core/session-group-birth.d.ts.map +1 -1
- package/dist/core/session-group-birth.js +81 -4
- package/dist/core/session-group-birth.js.map +1 -1
- package/dist/core/session-manager.d.ts +15 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +259 -5
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-model.d.ts +46 -0
- package/dist/core/session-model.d.ts.map +1 -0
- package/dist/core/session-model.js +42 -0
- package/dist/core/session-model.js.map +1 -0
- package/dist/core/session-owner-reminder.d.ts +63 -0
- package/dist/core/session-owner-reminder.d.ts.map +1 -0
- package/dist/core/session-owner-reminder.js +196 -0
- package/dist/core/session-owner-reminder.js.map +1 -0
- package/dist/core/session-preview-registry.d.ts +53 -0
- package/dist/core/session-preview-registry.d.ts.map +1 -0
- package/dist/core/session-preview-registry.js +82 -0
- package/dist/core/session-preview-registry.js.map +1 -0
- package/dist/core/session-preview.d.ts +114 -0
- package/dist/core/session-preview.d.ts.map +1 -0
- package/dist/core/session-preview.js +184 -0
- package/dist/core/session-preview.js.map +1 -0
- package/dist/core/session-rename.d.ts.map +1 -1
- package/dist/core/session-rename.js +5 -3
- package/dist/core/session-rename.js.map +1 -1
- package/dist/core/session-wake-deadline.d.ts +5 -0
- package/dist/core/session-wake-deadline.d.ts.map +1 -0
- package/dist/core/session-wake-deadline.js +13 -0
- package/dist/core/session-wake-deadline.js.map +1 -0
- package/dist/core/shutdown-budgets.d.ts +7 -6
- package/dist/core/shutdown-budgets.d.ts.map +1 -1
- package/dist/core/shutdown-budgets.js +11 -10
- package/dist/core/shutdown-budgets.js.map +1 -1
- package/dist/core/skills/frontmatter.d.ts.map +1 -1
- package/dist/core/skills/frontmatter.js +6 -1
- package/dist/core/skills/frontmatter.js.map +1 -1
- package/dist/core/terminal-control-grant.d.ts +74 -0
- package/dist/core/terminal-control-grant.d.ts.map +1 -0
- package/dist/core/terminal-control-grant.js +183 -0
- package/dist/core/terminal-control-grant.js.map +1 -0
- package/dist/core/terminal-proxy.d.ts.map +1 -1
- package/dist/core/terminal-proxy.js +4 -5
- package/dist/core/terminal-proxy.js.map +1 -1
- package/dist/core/terminal-write-auth.d.ts +4 -11
- package/dist/core/terminal-write-auth.d.ts.map +1 -1
- package/dist/core/terminal-write-auth.js +19 -16
- package/dist/core/terminal-write-auth.js.map +1 -1
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +36 -29
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/types.d.ts +88 -10
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +9 -7
- package/dist/core/types.js.map +1 -1
- package/dist/core/workbench-link.d.ts +81 -0
- package/dist/core/workbench-link.d.ts.map +1 -0
- package/dist/core/workbench-link.js +109 -0
- package/dist/core/workbench-link.js.map +1 -0
- package/dist/core/worker-pool.d.ts +312 -11
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2515 -217
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +447 -116
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +33 -3
- package/dist/dashboard/aggregator.d.ts.map +1 -1
- package/dist/dashboard/aggregator.js +93 -17
- package/dist/dashboard/aggregator.js.map +1 -1
- package/dist/dashboard/auth-session-connections.d.ts +59 -0
- package/dist/dashboard/auth-session-connections.d.ts.map +1 -0
- package/dist/dashboard/auth-session-connections.js +109 -0
- package/dist/dashboard/auth-session-connections.js.map +1 -0
- package/dist/dashboard/auth.d.ts +99 -1
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +176 -64
- package/dist/dashboard/auth.js.map +1 -1
- 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 +4 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/control-audit.d.ts +112 -0
- package/dist/dashboard/control-audit.d.ts.map +1 -0
- package/dist/dashboard/control-audit.js +195 -0
- package/dist/dashboard/control-audit.js.map +1 -0
- package/dist/dashboard/control-csrf.d.ts +123 -0
- package/dist/dashboard/control-csrf.d.ts.map +1 -0
- package/dist/dashboard/control-csrf.js +392 -0
- package/dist/dashboard/control-csrf.js.map +1 -0
- package/dist/dashboard/daemon-reconcile.d.ts +26 -0
- package/dist/dashboard/daemon-reconcile.d.ts.map +1 -0
- package/dist/dashboard/daemon-reconcile.js +46 -0
- package/dist/dashboard/daemon-reconcile.js.map +1 -0
- 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/events-sse.d.ts +60 -0
- package/dist/dashboard/events-sse.d.ts.map +1 -0
- package/dist/dashboard/events-sse.js +63 -0
- package/dist/dashboard/events-sse.js.map +1 -0
- package/dist/dashboard/h5-auth.d.ts +256 -0
- package/dist/dashboard/h5-auth.d.ts.map +1 -0
- package/dist/dashboard/h5-auth.js +825 -0
- package/dist/dashboard/h5-auth.js.map +1 -0
- package/dist/dashboard/managed-spawn.d.ts +36 -0
- package/dist/dashboard/managed-spawn.d.ts.map +1 -0
- package/dist/dashboard/managed-spawn.js +169 -0
- package/dist/dashboard/managed-spawn.js.map +1 -0
- package/dist/dashboard/preview-content-capability.d.ts +41 -0
- package/dist/dashboard/preview-content-capability.d.ts.map +1 -0
- package/dist/dashboard/preview-content-capability.js +127 -0
- package/dist/dashboard/preview-content-capability.js.map +1 -0
- package/dist/dashboard/preview-contract.d.ts +85 -0
- package/dist/dashboard/preview-contract.d.ts.map +1 -0
- package/dist/dashboard/preview-contract.js +198 -0
- package/dist/dashboard/preview-contract.js.map +1 -0
- package/dist/dashboard/preview-guard-page.d.ts +52 -0
- package/dist/dashboard/preview-guard-page.d.ts.map +1 -0
- package/dist/dashboard/preview-guard-page.js +162 -0
- package/dist/dashboard/preview-guard-page.js.map +1 -0
- package/dist/dashboard/preview-interaction.d.ts +57 -0
- package/dist/dashboard/preview-interaction.d.ts.map +1 -0
- package/dist/dashboard/preview-interaction.js +198 -0
- package/dist/dashboard/preview-interaction.js.map +1 -0
- package/dist/dashboard/preview-proxy.d.ts +113 -0
- package/dist/dashboard/preview-proxy.d.ts.map +1 -0
- package/dist/dashboard/preview-proxy.js +543 -0
- package/dist/dashboard/preview-proxy.js.map +1 -0
- package/dist/dashboard/public-redact.d.ts +23 -0
- package/dist/dashboard/public-redact.d.ts.map +1 -1
- package/dist/dashboard/public-redact.js +90 -21
- package/dist/dashboard/public-redact.js.map +1 -1
- package/dist/dashboard/request-identity.d.ts +77 -0
- package/dist/dashboard/request-identity.d.ts.map +1 -0
- package/dist/dashboard/request-identity.js +72 -0
- package/dist/dashboard/request-identity.js.map +1 -0
- package/dist/dashboard/session-cleanup.d.ts +5 -0
- package/dist/dashboard/session-cleanup.d.ts.map +1 -1
- package/dist/dashboard/session-cleanup.js +4 -0
- package/dist/dashboard/session-cleanup.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +5 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +14 -0
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/standing-link.d.ts +76 -0
- package/dist/dashboard/standing-link.d.ts.map +1 -0
- package/dist/dashboard/standing-link.js +111 -0
- package/dist/dashboard/standing-link.js.map +1 -0
- package/dist/dashboard/terminal-control.d.ts +105 -0
- package/dist/dashboard/terminal-control.d.ts.map +1 -0
- package/dist/dashboard/terminal-control.js +296 -0
- package/dist/dashboard/terminal-control.js.map +1 -0
- package/dist/dashboard/terminal-front-proxy.d.ts +55 -0
- package/dist/dashboard/terminal-front-proxy.d.ts.map +1 -0
- package/dist/dashboard/terminal-front-proxy.js +397 -0
- package/dist/dashboard/terminal-front-proxy.js.map +1 -0
- package/dist/dashboard/terminal-view-capability.d.ts +97 -0
- package/dist/dashboard/terminal-view-capability.d.ts.map +1 -0
- package/dist/dashboard/terminal-view-capability.js +145 -0
- package/dist/dashboard/terminal-view-capability.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-api.d.ts +59 -0
- package/dist/dashboard/web/agent-workbench-api.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-api.js +227 -0
- package/dist/dashboard/web/agent-workbench-api.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts +59 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.js +195 -0
- package/dist/dashboard/web/agent-workbench-appearance-menu.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance.d.ts +222 -0
- package/dist/dashboard/web/agent-workbench-appearance.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-appearance.js +499 -0
- package/dist/dashboard/web/agent-workbench-appearance.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-capabilities.d.ts +13 -0
- package/dist/dashboard/web/agent-workbench-capabilities.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-capabilities.js +24 -0
- package/dist/dashboard/web/agent-workbench-capabilities.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-chat.d.ts +90 -0
- package/dist/dashboard/web/agent-workbench-chat.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-chat.js +168 -0
- package/dist/dashboard/web/agent-workbench-chat.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-page.d.ts +3 -0
- package/dist/dashboard/web/agent-workbench-dock-page.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-page.js +23 -0
- package/dist/dashboard/web/agent-workbench-dock-page.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-view.d.ts +22 -0
- package/dist/dashboard/web/agent-workbench-dock-view.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-dock-view.js +76 -0
- package/dist/dashboard/web/agent-workbench-dock-view.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-model.d.ts +196 -0
- package/dist/dashboard/web/agent-workbench-model.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-model.js +529 -0
- package/dist/dashboard/web/agent-workbench-model.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-page.d.ts +3 -0
- package/dist/dashboard/web/agent-workbench-page.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-page.js +27 -0
- package/dist/dashboard/web/agent-workbench-page.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-panes.d.ts +49 -0
- package/dist/dashboard/web/agent-workbench-panes.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-panes.js +468 -0
- package/dist/dashboard/web/agent-workbench-panes.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-session-list.d.ts +42 -0
- package/dist/dashboard/web/agent-workbench-session-list.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-session-list.js +353 -0
- package/dist/dashboard/web/agent-workbench-session-list.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-storage.d.ts +49 -0
- package/dist/dashboard/web/agent-workbench-storage.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-storage.js +197 -0
- package/dist/dashboard/web/agent-workbench-storage.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-touch.d.ts +46 -0
- package/dist/dashboard/web/agent-workbench-touch.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-touch.js +96 -0
- package/dist/dashboard/web/agent-workbench-touch.js.map +1 -0
- package/dist/dashboard/web/agent-workbench-view.d.ts +34 -0
- package/dist/dashboard/web/agent-workbench-view.d.ts.map +1 -0
- package/dist/dashboard/web/agent-workbench-view.js +439 -0
- package/dist/dashboard/web/agent-workbench-view.js.map +1 -0
- package/dist/dashboard/web/app.d.ts.map +1 -1
- package/dist/dashboard/web/app.js +63 -2
- package/dist/dashboard/web/app.js.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.d.ts +18 -0
- package/dist/dashboard/web/bot-defaults-page.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults-page.js +362 -36
- package/dist/dashboard/web/bot-defaults-page.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts +8 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/control-csrf.d.ts +4 -0
- package/dist/dashboard/web/control-csrf.d.ts.map +1 -0
- package/dist/dashboard/web/control-csrf.js +26 -0
- package/dist/dashboard/web/control-csrf.js.map +1 -0
- package/dist/dashboard/web/dashboard-routes.d.ts.map +1 -1
- package/dist/dashboard/web/dashboard-routes.js +2 -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 -2
- package/dist/dashboard/web/groups-page.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +140 -6
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/overview-page.d.ts.map +1 -1
- package/dist/dashboard/web/overview-page.js +3 -2
- package/dist/dashboard/web/overview-page.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +41 -1
- 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 -1
- package/dist/dashboard/web/sessions.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts +3 -0
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +56 -10
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +4 -2
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/web/store.d.ts +27 -2
- package/dist/dashboard/web/store.d.ts.map +1 -1
- package/dist/dashboard/web/store.js +43 -4
- package/dist/dashboard/web/store.js.map +1 -1
- package/dist/dashboard/web/ui.d.ts +3 -0
- package/dist/dashboard/web/ui.d.ts.map +1 -1
- package/dist/dashboard/web/ui.js +11 -0
- package/dist/dashboard/web/ui.js.map +1 -1
- package/dist/dashboard/web/update-action.d.ts +12 -0
- package/dist/dashboard/web/update-action.d.ts.map +1 -1
- package/dist/dashboard/web/update-action.js +11 -0
- package/dist/dashboard/web/update-action.js.map +1 -1
- package/dist/dashboard/workbench-doctor.d.ts +114 -0
- package/dist/dashboard/workbench-doctor.d.ts.map +1 -0
- package/dist/dashboard/workbench-doctor.js +620 -0
- package/dist/dashboard/workbench-doctor.js.map +1 -0
- package/dist/dashboard/workbench-ticket.d.ts +165 -0
- package/dist/dashboard/workbench-ticket.d.ts.map +1 -0
- package/dist/dashboard/workbench-ticket.js +495 -0
- package/dist/dashboard/workbench-ticket.js.map +1 -0
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/agent-workbench-dock-page-IKAC5CFL.js +1 -0
- package/dist/dashboard-web/chunks/agent-workbench-page-M53UN5EB.js +1 -0
- package/dist/dashboard-web/chunks/bot-defaults-page-YKE5SDZF.js +10 -0
- package/dist/dashboard-web/chunks/chunk-22WIADUT.js +2 -0
- package/dist/dashboard-web/chunks/chunk-24ONYQMR.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-6O7KXFD2.js → chunk-2W6SCN3M.js} +1 -1
- package/dist/dashboard-web/chunks/chunk-4XNJX6B3.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-Q2MT4VWH.js → chunk-F6KNPF4R.js} +11 -9
- package/dist/dashboard-web/chunks/chunk-FXW3V4JR.js +1 -0
- package/dist/dashboard-web/chunks/chunk-G5ZS2YUS.js +1 -0
- package/dist/dashboard-web/chunks/chunk-KTQ7B6ZR.js +1 -0
- package/dist/dashboard-web/chunks/{chunk-U3ZFSO7E.js → chunk-NACP7BMR.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-page-2KZO5KSJ.js → connectors-page-VOISXXII.js} +1 -1
- package/dist/dashboard-web/chunks/{feedback-page-CLVTOLLO.js → feedback-page-RN6HRZMS.js} +1 -1
- package/dist/dashboard-web/chunks/groups-page-UQT6DCB7.js +8 -0
- package/dist/dashboard-web/chunks/{insights-page-4RPU2CRU.js → insights-page-HJFDYR5N.js} +3 -3
- package/dist/dashboard-web/chunks/{monitor-room-OTKQ2CXK.js → monitor-room-V7PCP47V.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-IJPUB6JT.js → monitoring-page-NKEKVQAP.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-QSR3J44Y.js → office-page-GVJF73XW.js} +1 -1
- package/dist/dashboard-web/chunks/overview-page-KHLV6VTI.js +1 -0
- package/dist/dashboard-web/chunks/{plugin-page-VEGTCEAS.js → plugin-page-J3OF5IY4.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-6NWJMOB5.js → roles-page-WQIRSOLZ.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-6I3LNXD4.js → schedules-page-J2DEY2X6.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-7BQFEA5P.js +8 -0
- package/dist/dashboard-web/chunks/settings-page-BJEIOFJH.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-M2Q6L5CB.js → skills-page-HGIFR3IU.js} +2 -2
- package/dist/dashboard-web/chunks/{team-federation-page-7I7FDE3B.js → team-federation-page-MR5ZPL4B.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-HQXRL65L.js → v3-page-GBR5JWPE.js} +2 -2
- package/dist/dashboard-web/chunks/{whiteboards-page-LQKC4TAZ.js → whiteboards-page-QAZM4BN3.js} +1 -1
- package/dist/dashboard-web/index.html +8 -1
- package/dist/dashboard-web/style.css +2006 -10
- package/dist/dashboard.js +1017 -264
- package/dist/dashboard.js.map +1 -1
- package/dist/dsh-runner.js +8 -1
- package/dist/dsh-runner.js.map +1 -1
- package/dist/global-config.d.ts +30 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +33 -0
- package/dist/global-config.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +46 -27
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +43 -27
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +1 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +22 -6
- 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 +118 -24
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts +15 -0
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +30 -0
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +48 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +138 -41
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/identity-cache.d.ts +14 -6
- package/dist/im/lark/identity-cache.d.ts.map +1 -1
- package/dist/im/lark/identity-cache.js +69 -26
- package/dist/im/lark/identity-cache.js.map +1 -1
- package/dist/im/lark/lark-hosts.d.ts +13 -0
- package/dist/im/lark/lark-hosts.d.ts.map +1 -1
- package/dist/im/lark/lark-hosts.js +16 -0
- package/dist/im/lark/lark-hosts.js.map +1 -1
- package/dist/im/lark/overview-card.d.ts +22 -0
- package/dist/im/lark/overview-card.d.ts.map +1 -1
- package/dist/im/lark/overview-card.js +53 -3
- package/dist/im/lark/overview-card.js.map +1 -1
- package/dist/im/lark/sessions-card.d.ts.map +1 -1
- package/dist/im/lark/sessions-card.js +26 -3
- package/dist/im/lark/sessions-card.js.map +1 -1
- package/dist/im/lark/workflow-slash-command.d.ts +6 -0
- package/dist/im/lark/workflow-slash-command.d.ts.map +1 -1
- package/dist/im/lark/workflow-slash-command.js +7 -0
- package/dist/im/lark/workflow-slash-command.js.map +1 -1
- package/dist/index-daemon.js +27 -4
- package/dist/index-daemon.js.map +1 -1
- package/dist/index-dashboard.d.ts +3 -0
- package/dist/index-dashboard.d.ts.map +1 -0
- package/dist/index-dashboard.js +94 -0
- package/dist/index-dashboard.js.map +1 -0
- package/dist/platform/team-agents-client.d.ts +199 -0
- package/dist/platform/team-agents-client.d.ts.map +1 -0
- package/dist/platform/team-agents-client.js +263 -0
- package/dist/platform/team-agents-client.js.map +1 -0
- package/dist/platform/tunnel-client.d.ts +6 -0
- package/dist/platform/tunnel-client.d.ts.map +1 -1
- package/dist/platform/tunnel-client.js.map +1 -1
- package/dist/services/async-trigger-state.d.ts +7 -3
- package/dist/services/async-trigger-state.d.ts.map +1 -1
- package/dist/services/async-trigger-state.js +28 -0
- package/dist/services/async-trigger-state.js.map +1 -1
- package/dist/services/async-trigger-store.d.ts +13 -4
- package/dist/services/async-trigger-store.d.ts.map +1 -1
- package/dist/services/async-trigger-store.js +55 -8
- package/dist/services/async-trigger-store.js.map +1 -1
- package/dist/services/bot-config-store.d.ts +1 -11
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +27 -3
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bot-profile-store.d.ts +17 -2
- package/dist/services/bot-profile-store.d.ts.map +1 -1
- package/dist/services/bot-profile-store.js +93 -12
- package/dist/services/bot-profile-store.js.map +1 -1
- package/dist/services/bridge-fallback-gate.d.ts +15 -7
- package/dist/services/bridge-fallback-gate.d.ts.map +1 -1
- package/dist/services/bridge-fallback-gate.js +43 -16
- package/dist/services/bridge-fallback-gate.js.map +1 -1
- package/dist/services/bridge-turn-queue.d.ts +10 -1
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +29 -9
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/claude-transcript.d.ts +19 -0
- package/dist/services/claude-transcript.d.ts.map +1 -1
- package/dist/services/claude-transcript.js +57 -5
- package/dist/services/claude-transcript.js.map +1 -1
- package/dist/services/codex-app-threads.d.ts +2 -0
- package/dist/services/codex-app-threads.d.ts.map +1 -1
- package/dist/services/codex-app-threads.js +1 -1
- package/dist/services/codex-app-threads.js.map +1 -1
- package/dist/services/codex-reasoning-effort.d.ts +8 -0
- package/dist/services/codex-reasoning-effort.d.ts.map +1 -1
- package/dist/services/codex-reasoning-effort.js +24 -0
- package/dist/services/codex-reasoning-effort.js.map +1 -1
- package/dist/services/cursor-resume-policy.d.ts.map +1 -1
- package/dist/services/cursor-resume-policy.js +15 -3
- package/dist/services/cursor-resume-policy.js.map +1 -1
- package/dist/services/open-platform-rename.d.ts.map +1 -1
- package/dist/services/open-platform-rename.js +3 -2
- package/dist/services/open-platform-rename.js.map +1 -1
- package/dist/services/ordinary-turn-recovery.d.ts +76 -0
- package/dist/services/ordinary-turn-recovery.d.ts.map +1 -0
- package/dist/services/ordinary-turn-recovery.js +349 -0
- package/dist/services/ordinary-turn-recovery.js.map +1 -0
- package/dist/services/peer-cross-ref-store.d.ts +8 -0
- package/dist/services/peer-cross-ref-store.d.ts.map +1 -0
- package/dist/services/peer-cross-ref-store.js +95 -0
- package/dist/services/peer-cross-ref-store.js.map +1 -0
- package/dist/services/pi-transcript.d.ts.map +1 -1
- package/dist/services/pi-transcript.js +10 -6
- package/dist/services/pi-transcript.js.map +1 -1
- package/dist/services/resume-fresh-policy.d.ts +17 -0
- package/dist/services/resume-fresh-policy.d.ts.map +1 -0
- package/dist/services/resume-fresh-policy.js +24 -0
- package/dist/services/resume-fresh-policy.js.map +1 -0
- package/dist/services/session-group-title.d.ts +1 -0
- package/dist/services/session-group-title.d.ts.map +1 -1
- package/dist/services/session-group-title.js +16 -1
- package/dist/services/session-group-title.js.map +1 -1
- package/dist/services/session-groups-store.d.ts +30 -0
- package/dist/services/session-groups-store.d.ts.map +1 -1
- package/dist/services/session-groups-store.js +26 -4
- package/dist/services/session-groups-store.js.map +1 -1
- package/dist/services/session-owner-notification.d.ts +9 -0
- package/dist/services/session-owner-notification.d.ts.map +1 -0
- package/dist/services/session-owner-notification.js +10 -0
- package/dist/services/session-owner-notification.js.map +1 -0
- package/dist/services/session-owner-reminder-config-store.d.ts +9 -0
- package/dist/services/session-owner-reminder-config-store.d.ts.map +1 -0
- package/dist/services/session-owner-reminder-config-store.js +24 -0
- package/dist/services/session-owner-reminder-config-store.js.map +1 -0
- package/dist/services/session-owner-reminder-store.d.ts +5 -0
- package/dist/services/session-owner-reminder-store.d.ts.map +1 -0
- package/dist/services/session-owner-reminder-store.js +54 -0
- package/dist/services/session-owner-reminder-store.js.map +1 -0
- package/dist/services/session-store.d.ts +98 -21
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +406 -48
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/structured-bridge-clis.d.ts +15 -2
- package/dist/services/structured-bridge-clis.d.ts.map +1 -1
- package/dist/services/structured-bridge-clis.js +16 -1
- package/dist/services/structured-bridge-clis.js.map +1 -1
- package/dist/services/transcript-resolver.d.ts +1 -1
- package/dist/services/transcript-resolver.d.ts.map +1 -1
- package/dist/services/transcript-resolver.js +6 -1
- package/dist/services/transcript-resolver.js.map +1 -1
- package/dist/setup/bot-config-editor.d.ts.map +1 -1
- package/dist/setup/bot-config-editor.js +7 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-herdr-integrations.d.ts +13 -0
- package/dist/setup/ensure-herdr-integrations.d.ts.map +1 -1
- package/dist/setup/ensure-herdr-integrations.js +21 -3
- package/dist/setup/ensure-herdr-integrations.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +1 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +107 -7
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/skills/definitions.d.ts +17 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +78 -8
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/injection-mode.d.ts +24 -3
- package/dist/skills/injection-mode.d.ts.map +1 -1
- package/dist/skills/injection-mode.js +64 -15
- package/dist/skills/injection-mode.js.map +1 -1
- package/dist/skills/installer.d.ts +18 -0
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +45 -1
- package/dist/skills/installer.js.map +1 -1
- package/dist/types.d.ts +222 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/bot-routing.d.ts +1 -1
- package/dist/utils/bot-routing.d.ts.map +1 -1
- package/dist/utils/bot-routing.js.map +1 -1
- package/dist/utils/child-env.d.ts +92 -2
- package/dist/utils/child-env.d.ts.map +1 -1
- package/dist/utils/child-env.js +195 -1
- package/dist/utils/child-env.js.map +1 -1
- package/dist/utils/cli-usage-limit.d.ts +25 -14
- package/dist/utils/cli-usage-limit.d.ts.map +1 -1
- package/dist/utils/cli-usage-limit.js +25 -15
- package/dist/utils/cli-usage-limit.js.map +1 -1
- package/dist/utils/daemon-rejection-guard.d.ts +35 -0
- package/dist/utils/daemon-rejection-guard.d.ts.map +1 -0
- package/dist/utils/daemon-rejection-guard.js +101 -0
- package/dist/utils/daemon-rejection-guard.js.map +1 -0
- package/dist/utils/dashboard-env.d.ts +53 -0
- package/dist/utils/dashboard-env.d.ts.map +1 -0
- package/dist/utils/dashboard-env.js +116 -0
- package/dist/utils/dashboard-env.js.map +1 -0
- package/dist/utils/global-install.d.ts.map +1 -1
- package/dist/utils/global-install.js +61 -5
- package/dist/utils/global-install.js.map +1 -1
- package/dist/utils/install-diagnostics.d.ts +14 -7
- package/dist/utils/install-diagnostics.d.ts.map +1 -1
- package/dist/utils/install-diagnostics.js +20 -11
- package/dist/utils/install-diagnostics.js.map +1 -1
- package/dist/utils/local-dev-update.d.ts +132 -0
- package/dist/utils/local-dev-update.d.ts.map +1 -0
- package/dist/utils/local-dev-update.js +176 -0
- package/dist/utils/local-dev-update.js.map +1 -0
- package/dist/utils/pending-input-queue.d.ts +12 -1
- package/dist/utils/pending-input-queue.d.ts.map +1 -1
- package/dist/utils/pending-input-queue.js.map +1 -1
- package/dist/worker.js +1517 -236
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +68 -68
- package/dist/workflows/events/payloads.d.ts +14 -14
- package/dist/workflows/events/schema.d.ts +312 -312
- package/dist/workflows/migration/v2-run-archive-schema.d.ts +40 -40
- package/dist/workflows/v3/cli-run.d.ts.map +1 -1
- package/dist/workflows/v3/cli-run.js +7 -0
- package/dist/workflows/v3/cli-run.js.map +1 -1
- package/dist/workflows/v3/goal-cli.d.ts +3 -0
- package/dist/workflows/v3/goal-cli.d.ts.map +1 -1
- package/dist/workflows/v3/goal-cli.js +11 -0
- package/dist/workflows/v3/goal-cli.js.map +1 -1
- package/dist/workflows/v3/run-envelope.d.ts +144 -144
- package/package.json +7 -2
- package/dist/cli/pm2-god-admission.d.ts +0 -8
- package/dist/cli/pm2-god-admission.d.ts.map +0 -1
- package/dist/cli/pm2-god-admission.js +0 -25
- package/dist/cli/pm2-god-admission.js.map +0 -1
- package/dist/core/riff-shutdown-detach.d.ts.map +0 -1
- package/dist/core/riff-shutdown-detach.js.map +0 -1
- package/dist/core/riff-worker-shutdown-readiness.d.ts.map +0 -1
- package/dist/core/riff-worker-shutdown-readiness.js.map +0 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-4RENHE7J.js +0 -10
- package/dist/dashboard-web/chunks/chunk-2WQDS65D.js +0 -1
- package/dist/dashboard-web/chunks/chunk-G5XGBCM6.js +0 -1
- package/dist/dashboard-web/chunks/chunk-WLIVBPJC.js +0 -1
- package/dist/dashboard-web/chunks/groups-page-HLASRTSY.js +0 -8
- package/dist/dashboard-web/chunks/overview-page-6OK63V4V.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-B2TXGJDO.js +0 -8
- package/dist/dashboard-web/chunks/settings-page-6KJADSC6.js +0 -2
package/dist/worker.js
CHANGED
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
* 7. On 'restart', kills CLI and re-spawns with --resume
|
|
14
14
|
*/
|
|
15
15
|
import { createHash, randomBytes } from 'node:crypto';
|
|
16
|
-
import { chmodSync, mkdirSync, writeFileSync, unlinkSync, rmdirSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream } from 'node:fs';
|
|
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 } from 'node:path';
|
|
18
|
+
import { join, basename, dirname, delimiter } 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
|
-
import { evaluateCredentialOnlyIsolationGate, credentialIsolationRequired, deviceCredentialIsolationMarkerPath, isCredentialIsolationReservedBasename, buildCredentialIsolationRules, buildSeatbeltProfile, isolatedPaneOriginChannel, isolatedPaneReattachSafe, sendCredFilePath, botHomePath, buildCliExecutableReadCarveOuts, isolationPaneMarkerContent, isolationPanePolicyDigest, } from './adapters/cli/read-isolation.js';
|
|
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
23
|
import { buildFsPolicy, compileToSeatbelt, migrateLegacySandboxFields, resolveRedirectedAdapterAuthPaths, FsPolicyConfigError } from './adapters/cli/fs-policy.js';
|
|
24
|
-
import {
|
|
24
|
+
import { buildCloseResultMessage, normalizeDestroyResult, mayRestoreWriteAdmission, interpretAbortOutcome, classifyRestartTeardown } from './adapters/backend/destroy-result.js';
|
|
25
|
+
import { isRemoteBackendType, killPersistentBackendTarget, killPersistentSession, probePersistentBackendTarget, probePersistentSession, shouldRejectPersistentPostKillProbe } from './core/persistent-backend.js';
|
|
25
26
|
import { finalizeRawCommandDelivery, writeRawCommandLine } from './core/raw-command-writer.js';
|
|
26
27
|
import { rawCommandWriteOptionsFor } from './core/raw-command-write-options.js';
|
|
27
28
|
import { publishCliSessionIdToDaemon } from './core/cli-session-id-publisher.js';
|
|
@@ -29,7 +30,7 @@ import { readProcessStartIdentity } from './core/session-marker.js';
|
|
|
29
30
|
import { roleLibraryRoot, roleLibrarySubtree } from './core/role-library.js';
|
|
30
31
|
import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff, isTranscriptRateLimitEvent, apiErrorMessageText } from './services/claude-transcript.js';
|
|
31
32
|
import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
|
|
32
|
-
import { bridgePostText, isBridgeNothingToSendFinal, shouldSuppressBridgeEmit, structuredFallbackKind,
|
|
33
|
+
import { bridgePostText, isBridgeNothingToSendFinal, shouldSuppressBridgeEmit, structuredFallbackKind, stripTrailingOaiMemoryCitation } from './services/bridge-fallback-gate.js';
|
|
33
34
|
import { buildSubmitMessagePreview } from './services/submit-notification.js';
|
|
34
35
|
import { decideHardTimeoutAction, decidePostHookPromptEvidence, decideSettleMarkReady, shouldArmPostHookPromptEvidenceFallback, shouldReleaseFirstPromptTimeout, shouldWaitForPostSessionStartPromptEvidence, shouldWriteNow, POST_HOOK_EVIDENCE_FALLBACK_MS, POST_HOOK_EVIDENCE_RETRY_MS, } from './utils/input-gate.js';
|
|
35
36
|
import { decideCodexRunnerFreshness, CodexRunnerFreshnessInputQueue, shouldHoldCodexRunnerInput, } from './services/codex-runner-freshness.js';
|
|
@@ -41,7 +42,7 @@ import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
|
|
|
41
42
|
import { EffortConfirmDialogGuard } from './utils/effort-confirm-dialog.js';
|
|
42
43
|
import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
|
|
43
44
|
import { handoffQueuedDurableInputsOnBackendExit, mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, resolveInitialPromptDelivery, shouldArmSpawnArgvInitialPromptBusy, shouldTrackArgvBakedFirstPrompt, shouldDeferArgsBakedDurablePrompt, shouldDeferInitialPromptForArgLimit, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
|
|
44
|
-
import {
|
|
45
|
+
import { remoteWorkerShutdownInputBlocker } from './core/remote-worker-shutdown-readiness.js';
|
|
45
46
|
import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
|
|
46
47
|
import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
|
|
47
48
|
import { sanitizePerBotEnv } from './core/per-bot-env.js';
|
|
@@ -54,8 +55,10 @@ import { MCP_GATEWAY_REQUIRED_ENV, MCP_GATEWAY_SOCKET_ENV, } from './core/plugin
|
|
|
54
55
|
import { readSessionMcpRuntimeManifest, sessionMcpRuntimeHostOnlyPaths, } from './core/plugins/mcp/session-runtime.js';
|
|
55
56
|
import { prepareCliPluginGeneration } from './core/plugins/cli-generation.js';
|
|
56
57
|
import { loadBotConfigs, resolveBrandLabel, resolveUsageDisplay, } from './bot-registry.js';
|
|
57
|
-
import { readGlobalConfig } from './global-config.js';
|
|
58
|
-
import {
|
|
58
|
+
import { readGlobalConfig, isWorkflowFeatureEnabled } from './global-config.js';
|
|
59
|
+
import { deriveTerminalWriteToken, resolveTerminalAccessForRequest, safeTerminalTokenEqual, } from './core/terminal-write-auth.js';
|
|
60
|
+
import { deriveWorkerViewGeneration, looksLikeTerminalControlGrant, TERMINAL_VIEW_FORWARD_HEADER, verifyTerminalControlGrant, verifyTerminalViewForward, } from './core/terminal-control-grant.js';
|
|
61
|
+
import { appendControlAudit, controlAuditRecord } from './dashboard/control-audit.js';
|
|
59
62
|
import { readPlatformBinding } from './platform/binding.js';
|
|
60
63
|
import { loadDashboardSecret, loadPersistedToken } from './dashboard/auth.js';
|
|
61
64
|
import { InflightInputTracker } from './core/inflight-input-tracker.js';
|
|
@@ -115,6 +118,9 @@ import { AmbiguousSubmissionBlockedError, isObserveBackend, } from './adapters/b
|
|
|
115
118
|
import { backendGateUserMessage, backendSandboxCompatibilityError, backendSandboxCompatibilityUserMessage, decideBackendGate, retireSupersededRecordedHerdrTarget, selectSessionBackend, } from './adapters/backend/session-backend-selector.js';
|
|
116
119
|
import { buildReproduceCommand, selectReproduceLaunch } from './adapters/backend/reproduce-command.js';
|
|
117
120
|
import { deriveRiffReposFromDirs, deriveRiffRepoFromWorkingDir, isValidRiffBaseUrl, isValidRiffSandboxCluster, } from './adapters/backend/riff-backend.js';
|
|
121
|
+
import { buildEffectiveChildEnv, buildEffectiveMojoConfig, findReservedMojoCliFlags, normalizeMojoConfig, normalizeMojoLivePatch } from './adapters/backend/mojo-types.js';
|
|
122
|
+
import { builtinSkillBlockForInjectsSessionContext } from './skills/injection-mode.js';
|
|
123
|
+
import { whiteboardEnabled } from './services/whiteboard-store.js';
|
|
118
124
|
import { prepareDirectSandbox, prepareCredentialOnlySandbox, credentialOnlySandboxAvailable, probeHostCredentialIsolationMechanism, attachSandboxOutbox, startOutboxWatcher, sandboxEnabled, localSandboxApplies, } from './adapters/backend/sandbox.js';
|
|
119
125
|
import { DEVICE_AUTHORITY_DIRECTORY, DEVICE_CREDENTIAL_FILE, } from './platform/device-paths.js';
|
|
120
126
|
import { tmuxEnv, probeTmuxFunctionalWithRetry } from './setup/ensure-tmux.js';
|
|
@@ -1169,6 +1175,7 @@ async function prepareCliPluginGenerationAndGateway(cfg, adapter) {
|
|
|
1169
1175
|
sessionMcpGatewayHost = await startSessionMcpGatewayHost({
|
|
1170
1176
|
sessionId: cfg.sessionId,
|
|
1171
1177
|
dataDir: config.session.dataDir,
|
|
1178
|
+
trustedTurnIdentity: currentGatewayTrustedTurnIdentity,
|
|
1172
1179
|
onError: error => log(`[mcp-gateway] host error: ${error.message}`),
|
|
1173
1180
|
});
|
|
1174
1181
|
log(`[mcp-gateway] trusted host listening for ${manifest.entries.length} plugin server(s)`);
|
|
@@ -1399,6 +1406,25 @@ let spawnArgvInitialPromptBusy = false;
|
|
|
1399
1406
|
* arm above; quiescence argv adapters seed working then idle at first ready.
|
|
1400
1407
|
*/
|
|
1401
1408
|
let spawnArgvNeedsWorkingSeed = false;
|
|
1409
|
+
/**
|
|
1410
|
+
* Startup-window evidence for an argv-baked first prompt: true once the turn
|
|
1411
|
+
* has VISIBLY started — busyPattern seen in the raw PTY stream since spawn, or
|
|
1412
|
+
* a live structured-transcript user/final event ingested. Pi's TUI renders its
|
|
1413
|
+
* input box seconds before it begins consuming the argv prompt (extension /
|
|
1414
|
+
* model loading), and during that gap the screen shows no busy marker and the
|
|
1415
|
+
* transcript has no user record — quiescence then reports a FALSE first idle
|
|
1416
|
+
* ("not started yet", not "turn complete"). Until this latches,
|
|
1417
|
+
* markPromptReady holds ready + re-arms instead of seeding working→idle.
|
|
1418
|
+
*/
|
|
1419
|
+
let spawnArgvTurnStartEvidenceSeen = false;
|
|
1420
|
+
/** Rolling ANSI-stripped PTY tail scanned for busyPattern while the argv
|
|
1421
|
+
* first-turn evidence gate is armed; dropped once evidence latches. */
|
|
1422
|
+
let spawnArgvTurnStartBusyScanTail = '';
|
|
1423
|
+
/** Fail-open deadline for the evidence gate: past this instant the gate stops
|
|
1424
|
+
* holding, so an argv prompt the CLI never consumed (e.g. dropped on a
|
|
1425
|
+
* restart) cannot park the card at 工作中 forever. */
|
|
1426
|
+
let spawnArgvTurnStartEvidenceDeadlineMs = 0;
|
|
1427
|
+
let spawnArgvTurnStartFailOpenTimer = null;
|
|
1402
1428
|
let idleDetector = null;
|
|
1403
1429
|
let isTmuxMode = false;
|
|
1404
1430
|
/** True once a crash diagnostic tmux shell (bmx-diag-<sid>) is live. */
|
|
@@ -1430,7 +1456,7 @@ function backendScreenEvidenceIsAuthoritativeForMutation() {
|
|
|
1430
1456
|
* generation. The daemon receives this over private IPC; child-writable PID
|
|
1431
1457
|
* marker files remain diagnostics only. */
|
|
1432
1458
|
let currentCliCredentialIsolated = false;
|
|
1433
|
-
/** Successful
|
|
1459
|
+
/** Successful remote close prepare awaiting durable daemon commit. */
|
|
1434
1460
|
let preparedCloseRequestId = null;
|
|
1435
1461
|
let closeRequestInFlightId = null;
|
|
1436
1462
|
let lastAbortedCloseRequestId = null;
|
|
@@ -1459,8 +1485,14 @@ const readOnlyRemoteScrollLimiter = new ReadOnlyRemoteScrollLimiter({
|
|
|
1459
1485
|
// daemon restart re-forks every worker — a per-process random token would 403
|
|
1460
1486
|
// every previously-issued operate link).
|
|
1461
1487
|
let writeToken = randomBytes(16).toString('hex');
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1488
|
+
// Per-BOOT random read capability, reported to the daemon in `ready` and
|
|
1489
|
+
// embedded in Feishu card 「打开 Web 终端」 links. Deliberately NOT the stable
|
|
1490
|
+
// per-session HMAC any more (P1-5): a stable view token could never be revoked
|
|
1491
|
+
// — a viewer who fetched it once kept terminal read access forever, across
|
|
1492
|
+
// worker restarts included. Per-boot randomness bounds every card link to this
|
|
1493
|
+
// worker generation (restart ⇒ all previously issued view tokens die), and the
|
|
1494
|
+
// dashboard view-link API mints its own short-lived signed read grants instead
|
|
1495
|
+
// of ever handing this value out (see resolveTerminalAccessForReq).
|
|
1464
1496
|
let viewToken = randomBytes(32).toString('base64url');
|
|
1465
1497
|
// Active dashboard token, persisted by the dashboard process at this stable
|
|
1466
1498
|
// path (mirrors dashboard.ts TOKEN_PATH). The platform proxy injects it as the
|
|
@@ -1468,10 +1500,30 @@ let viewToken = randomBytes(32).toString('base64url');
|
|
|
1468
1500
|
// proves a request traversed the platform's authenticated front door.
|
|
1469
1501
|
const DASHBOARD_TOKEN_PATH = join(homedir(), '.botmux', '.dashboard-token');
|
|
1470
1502
|
const DASHBOARD_SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
|
|
1471
|
-
|
|
1503
|
+
// Test-only seam (inert in production): widen the SYNCHRONOUS `.dashboard-secret`
|
|
1504
|
+
// read that happens twice inside the terminal WS handshake — once at
|
|
1505
|
+
// `verifyClient`, once at the post-upgrade `connection` re-check — so the
|
|
1506
|
+
// integration test can land a capability's expiry inside that gap and prove the
|
|
1507
|
+
// second check still fail-closes it (see worker-terminal-read-auth P1-3). A real
|
|
1508
|
+
// slow HOME (NFS/slow disk) produces the same window in production. The old test
|
|
1509
|
+
// bloated the secret file with 32MB of whitespace to force this delay, which is
|
|
1510
|
+
// incompatible with #920's strict 0600 host-authority reader (its 256-byte cap
|
|
1511
|
+
// rejects a padded file); this env-gated busy-wait reproduces the timing without
|
|
1512
|
+
// an oversized or otherwise unsafe credential file. Only ever set by that test.
|
|
1513
|
+
const HANDSHAKE_SECRET_READ_DELAY_MS = Number.isFinite(Number(process.env.BOTMUX_TEST_TERMINAL_SECRET_READ_DELAY_MS))
|
|
1514
|
+
? Math.max(0, Number(process.env.BOTMUX_TEST_TERMINAL_SECRET_READ_DELAY_MS))
|
|
1515
|
+
: 0;
|
|
1516
|
+
/** Read the dashboard secret on the terminal WS-handshake path. Identical to
|
|
1517
|
+
* {@link loadDashboardSecret} in production; adds a bounded synchronous delay
|
|
1518
|
+
* ONLY when the test seam env var is set, to make the handshake read window
|
|
1519
|
+
* observable without an oversized secret file. */
|
|
1520
|
+
function loadHandshakeSecret() {
|
|
1472
1521
|
const secret = loadDashboardSecret(DASHBOARD_SECRET_PATH);
|
|
1473
|
-
if (
|
|
1474
|
-
|
|
1522
|
+
if (HANDSHAKE_SECRET_READ_DELAY_MS > 0) {
|
|
1523
|
+
const until = Date.now() + HANDSHAKE_SECRET_READ_DELAY_MS;
|
|
1524
|
+
while (Date.now() < until) { /* busy-wait: mimic a slow synchronous fs read */ }
|
|
1525
|
+
}
|
|
1526
|
+
return secret;
|
|
1475
1527
|
}
|
|
1476
1528
|
/** Re-derive the stable write (operate) token from the host-only dashboard
|
|
1477
1529
|
* secret so a restarted worker mints the SAME token — keeping already-issued
|
|
@@ -1482,22 +1534,86 @@ function refreshTerminalWriteToken() {
|
|
|
1482
1534
|
if (secret && sessionId)
|
|
1483
1535
|
writeToken = deriveTerminalWriteToken(secret, sessionId);
|
|
1484
1536
|
}
|
|
1485
|
-
/**
|
|
1486
|
-
* Resolve terminal write permission for one request. The platform-injected
|
|
1487
|
-
* `X-Botmux-Role` header is trusted only when this machine is bound to a central
|
|
1488
|
-
* platform AND the request actually came through the platform proxy (proven by a
|
|
1489
|
-
* matching `botmux_dashboard_token` cookie — a secret a direct caller lacks).
|
|
1490
|
-
* A matching private write-link `?token=` is an independent capability that
|
|
1491
|
-
* grants write regardless of platform role (an explicitly issued write link
|
|
1492
|
-
* must work for a viewer the platform sees as guest). See
|
|
1493
|
-
* ./core/terminal-write-auth for the full rationale.
|
|
1494
|
-
*
|
|
1495
|
-
* Both the binding and the token are read PER REQUEST, never snapshotted:
|
|
1496
|
-
* `botmux bind`/unbind and dashboard token rotation are hot-reloaded without
|
|
1497
|
-
* restarting this worker, so a cached value would go stale.
|
|
1498
|
-
*/
|
|
1499
1537
|
function resolveTerminalAccessForReq(req, url) {
|
|
1500
|
-
|
|
1538
|
+
// P1-6 invariant: an explicit, matching write-link `?token=` is an
|
|
1539
|
+
// independent capability with the HIGHEST priority. It wins outright — before
|
|
1540
|
+
// the internal control-grant header is even looked at — so a central proxy
|
|
1541
|
+
// that attaches a read-scope grant can never silently downgrade an explicitly
|
|
1542
|
+
// issued 「操作链接」 to read-only for a viewer the platform authenticated as
|
|
1543
|
+
// teammate/guest. (The front proxy additionally refrains from injecting a
|
|
1544
|
+
// grant next to explicit query capabilities; this ordering makes the
|
|
1545
|
+
// guarantee hold regardless of proxy behavior, not by accident of it.)
|
|
1546
|
+
if (safeTerminalTokenEqual(url.searchParams.get('token'), writeToken)) {
|
|
1547
|
+
return { hasRead: true, hasWrite: true, platformReadonly: false };
|
|
1548
|
+
}
|
|
1549
|
+
// `?viewToken=` read capability, two accepted forms (P1-5):
|
|
1550
|
+
// • this worker's per-boot random token (Feishu card links) — plain
|
|
1551
|
+
// equality; dies with the worker generation;
|
|
1552
|
+
// • a short-lived signed read grant minted by the dashboard view-link API.
|
|
1553
|
+
// The retired stable per-session HMAC matches neither form, so every
|
|
1554
|
+
// previously issued stable view token fails closed on this worker.
|
|
1555
|
+
//
|
|
1556
|
+
// The signed form is accepted ONLY when all four hold, because this worker
|
|
1557
|
+
// has no view of dashboard logout state and a URL is trivially copied:
|
|
1558
|
+
// 1. signature + expiry + session binding verify (stateless base check);
|
|
1559
|
+
// 2. scope is `read` — a leaked write-scope loopback grant must never
|
|
1560
|
+
// double as a browser-shareable capability;
|
|
1561
|
+
// 3. audience is `central` AND the central front proxy countersigned THIS
|
|
1562
|
+
// capability (X-Botmux-Terminal-View). That header is computed from the
|
|
1563
|
+
// host-only dashboard secret and is only emitted after the proxy checked
|
|
1564
|
+
// the capability's auth session is still live, so a raw view URL replayed
|
|
1565
|
+
// against this port — or against the daemon's own network-bound `/s/`
|
|
1566
|
+
// reverse proxy — cannot produce it and dies here;
|
|
1567
|
+
// 4. the pinned worker generation matches this boot. A restart re-randomizes
|
|
1568
|
+
// `viewToken`, so grants minted for the previous boot fail closed even
|
|
1569
|
+
// though `.dashboard-secret` is unchanged.
|
|
1570
|
+
// The WebSocket is additionally closed at the grant's expiresAt.
|
|
1571
|
+
const viewParam = url.searchParams.get('viewToken');
|
|
1572
|
+
let viewTokenMatches = safeTerminalTokenEqual(viewParam, viewToken);
|
|
1573
|
+
let viewGrantUser;
|
|
1574
|
+
let viewGrantExpiresAt;
|
|
1575
|
+
if (!viewTokenMatches && looksLikeTerminalControlGrant(viewParam) && sessionId) {
|
|
1576
|
+
const secret = loadHandshakeSecret();
|
|
1577
|
+
if (secret && verifyTerminalViewForward(secret, viewParam, req.headers[TERMINAL_VIEW_FORWARD_HEADER])) {
|
|
1578
|
+
const viewGrant = verifyTerminalControlGrant(secret, viewParam, sessionId);
|
|
1579
|
+
if (viewGrant.ok
|
|
1580
|
+
&& viewGrant.claims.scope === 'read'
|
|
1581
|
+
&& viewGrant.claims.audience === 'central'
|
|
1582
|
+
&& viewGrant.claims.workerGeneration === deriveWorkerViewGeneration(secret, viewToken)) {
|
|
1583
|
+
viewTokenMatches = true;
|
|
1584
|
+
viewGrantUser = viewGrant.claims.userId;
|
|
1585
|
+
viewGrantExpiresAt = viewGrant.claims.expiresAt;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
const legacy = resolveTerminalAccessForRequest(req.headers, false, viewTokenMatches, () => readPlatformBinding() !== null, () => loadPersistedToken(DASHBOARD_TOKEN_PATH));
|
|
1590
|
+
if (viewGrantExpiresAt !== undefined && legacy.hasRead && !legacy.hasWrite) {
|
|
1591
|
+
legacy.auditUser = viewGrantUser;
|
|
1592
|
+
legacy.controlExpiresAt = viewGrantExpiresAt;
|
|
1593
|
+
}
|
|
1594
|
+
// Most terminal HTTP/WS requests use the query capabilities above. Avoid a
|
|
1595
|
+
// second synchronous secret-file read on that hot path; only the central
|
|
1596
|
+
// front proxy supplies this internal header.
|
|
1597
|
+
if (req.headers['x-botmux-terminal-control'] === undefined)
|
|
1598
|
+
return legacy;
|
|
1599
|
+
const secret = loadHandshakeSecret();
|
|
1600
|
+
if (!secret || !sessionId)
|
|
1601
|
+
return legacy;
|
|
1602
|
+
const grant = verifyTerminalControlGrant(secret, req.headers['x-botmux-terminal-control'], sessionId);
|
|
1603
|
+
if (!grant.ok)
|
|
1604
|
+
return legacy;
|
|
1605
|
+
return {
|
|
1606
|
+
hasRead: true,
|
|
1607
|
+
hasWrite: grant.claims.scope === 'write',
|
|
1608
|
+
platformReadonly: grant.claims.scope === 'read',
|
|
1609
|
+
auditUser: grant.claims.userId,
|
|
1610
|
+
controlExpiresAt: grant.claims.expiresAt,
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function auditTerminalInput(user, data) {
|
|
1614
|
+
appendControlAudit(controlAuditRecord(user ?? 'legacy-capability', sessionId, 'terminal.input', {
|
|
1615
|
+
bytes: Buffer.byteLength(data),
|
|
1616
|
+
}));
|
|
1501
1617
|
}
|
|
1502
1618
|
/** Lazily-written locked-mode zellij config for per-WS web-terminal attach
|
|
1503
1619
|
* clients: cleared keybinds + locked mode so every keystroke passes straight
|
|
@@ -1525,7 +1641,7 @@ let closeRequested = false;
|
|
|
1525
1641
|
let capturedSpawnCommand = null;
|
|
1526
1642
|
let deferredTopicOutputTail = '';
|
|
1527
1643
|
const reportedDeferredTopicRoots = new Set();
|
|
1528
|
-
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' };
|
|
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' };
|
|
1529
1645
|
function cliName() {
|
|
1530
1646
|
return (lastInitConfig?.cliRuntime?.source === 'configured'
|
|
1531
1647
|
? (lastInitConfig.cliRuntime.displayName?.trim() || lastInitConfig.cliRuntime.id)
|
|
@@ -1778,11 +1894,12 @@ async function runStartupCommands() {
|
|
|
1778
1894
|
return;
|
|
1779
1895
|
if (!backend)
|
|
1780
1896
|
return;
|
|
1781
|
-
//
|
|
1782
|
-
// 200ms 后 write 回车),对
|
|
1783
|
-
// riff 的初始化命令走自己的 riff.setupCommands
|
|
1784
|
-
|
|
1785
|
-
|
|
1897
|
+
// 远端后端:generic startupCommands 是 PTY 语义(sendRawCommandLine = write 文本 +
|
|
1898
|
+
// 200ms 后 write 回车),对 riff/mojo 每条会裂成两个独立远端 turn 并打乱血缘。
|
|
1899
|
+
// riff 的初始化命令走自己的 riff.setupCommands(沙箱内执行);mojo 无对应机制,
|
|
1900
|
+
// 其环境准备由 --cloud 沙箱镜像负责。两者这里都必须跳过。
|
|
1901
|
+
if (effectiveBackendType === 'riff' || effectiveBackendType === 'mojo') {
|
|
1902
|
+
log(`Skipping ${cmds.length} generic startup command(s) — ${effectiveBackendType} backend has no PTY to drive`);
|
|
1786
1903
|
return;
|
|
1787
1904
|
}
|
|
1788
1905
|
log(`Running ${cmds.length} startup command(s) before first prompt`);
|
|
@@ -1945,13 +2062,20 @@ async function deliverRawInput(msg) {
|
|
|
1945
2062
|
let recoveryFailureReason;
|
|
1946
2063
|
try {
|
|
1947
2064
|
await sendRawCommandLineWithRecoveryFence(targetBackend, msg.content, () => {
|
|
2065
|
+
// A passthrough (/compact, /model, ...) lands as a REAL mojo turn, so the
|
|
2066
|
+
// credential snapshot has to be applied here — at the write moment, which
|
|
2067
|
+
// is the same point flushPending applies it for a queued message. Without
|
|
2068
|
+
// this, a cleared or rotated JWT did not take effect on these turns.
|
|
2069
|
+
applyMojoLivePatch(msg.mojoLivePatch);
|
|
1948
2070
|
renderer?.markNewTurn();
|
|
1949
2071
|
usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
|
|
1950
2072
|
if (tmuxScrolledHalfPages > 0)
|
|
1951
2073
|
exitTmuxScrollMode();
|
|
1952
2074
|
currentBotmuxTurnId = msg.turnId;
|
|
1953
2075
|
currentBotmuxDispatchAttempt = undefined;
|
|
2076
|
+
currentGatewayTrustedCaller = undefined;
|
|
1954
2077
|
currentVcMeetingImTurnOrigin = undefined;
|
|
2078
|
+
stampMojoTurnMark(msg.turnId, undefined); // a passthrough IS a mojo turn
|
|
1955
2079
|
writeCliPidMarker();
|
|
1956
2080
|
publishSandboxRelayCapability();
|
|
1957
2081
|
});
|
|
@@ -2051,7 +2175,7 @@ async function deliverRawInput(msg) {
|
|
|
2051
2175
|
...(followUpContent
|
|
2052
2176
|
? {
|
|
2053
2177
|
writeFollowUp: async () => {
|
|
2054
|
-
const result = await writeAdoptMessage(followUpContent, msg.followUpTurnId, undefined, undefined, fence);
|
|
2178
|
+
const result = await writeAdoptMessage(followUpContent, msg.followUpTurnId, undefined, undefined, undefined, fence);
|
|
2055
2179
|
if (result === 'stale-before-write') {
|
|
2056
2180
|
send({
|
|
2057
2181
|
type: 'user_notify',
|
|
@@ -2094,8 +2218,16 @@ const inflightInputs = new InflightInputTracker();
|
|
|
2094
2218
|
let lastPtyActivityAtMs = 0;
|
|
2095
2219
|
let currentBotmuxTurnId;
|
|
2096
2220
|
let currentBotmuxDispatchAttempt;
|
|
2221
|
+
let currentGatewayTrustedCaller;
|
|
2097
2222
|
let currentVcMeetingImTurnOrigin;
|
|
2098
2223
|
let durableTurnInFlight = false;
|
|
2224
|
+
function currentGatewayTrustedTurnIdentity() {
|
|
2225
|
+
return {
|
|
2226
|
+
...(currentGatewayTrustedCaller ? { caller: currentGatewayTrustedCaller } : {}),
|
|
2227
|
+
...(currentBotmuxTurnId ? { turnId: currentBotmuxTurnId } : {}),
|
|
2228
|
+
...(currentBotmuxDispatchAttempt !== undefined ? { dispatchAttempt: currentBotmuxDispatchAttempt } : {}),
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2099
2231
|
function publishSandboxRelayCapability(opts = {}) {
|
|
2100
2232
|
const daemonIpcPort = parseDaemonIpcPort(process.env.BOTMUX_DAEMON_IPC_PORT);
|
|
2101
2233
|
const capability = {
|
|
@@ -3219,26 +3351,30 @@ function codexAppLivenessStatus(base, nowMs = Date.now()) {
|
|
|
3219
3351
|
}
|
|
3220
3352
|
/**
|
|
3221
3353
|
* True when this CLI has an authoritative STRUCTURED rate-limit signal that is
|
|
3222
|
-
* actually PUBLISHED as a `limited` screen_update
|
|
3223
|
-
* whose `bridgeIngest → maybeEmitStructuredRateLimit()` reads the
|
|
3224
|
-
* `error:"rate_limit"` record
|
|
3225
|
-
*
|
|
3226
|
-
*
|
|
3227
|
-
*
|
|
3228
|
-
* limit
|
|
3229
|
-
*
|
|
3230
|
-
*
|
|
3354
|
+
* actually PUBLISHED as a `limited` screen_update. Two families qualify: the
|
|
3355
|
+
* Claude family, whose `bridgeIngest → maybeEmitStructuredRateLimit()` reads the
|
|
3356
|
+
* transcript's `error:"rate_limit"` record; and codex, whose
|
|
3357
|
+
* `maybeEmitCodexStructuredRateLimit()` reads the rollout's `codex_rate_limited`
|
|
3358
|
+
* terminal (`isCodexRateLimitEvent`). For those CLIs the screen-text `rate`
|
|
3359
|
+
* heuristic is not just redundant but harmful: the model's own output or a dev
|
|
3360
|
+
* editing rate-limit code/tests puts phrases like "429 Too Many Requests" /
|
|
3361
|
+
* "exceeded retry limit" on screen, which the scraper cannot distinguish from a
|
|
3362
|
+
* real limit. So we suppress the screen-scan `rate` verdict and let the
|
|
3363
|
+
* structured path be the sole authority. `usage` (quota "hit your limit …") has
|
|
3364
|
+
* no structured equivalent yet, so it still comes from the screen.
|
|
3231
3365
|
*
|
|
3232
|
-
* Gate on `claudeDataDir`
|
|
3233
|
-
*
|
|
3234
|
-
* structured rate-limit EMIT only exists
|
|
3235
|
-
*
|
|
3236
|
-
*
|
|
3237
|
-
*
|
|
3238
|
-
*
|
|
3239
|
-
*
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3366
|
+
* Gate on the two capability fields (`claudeDataDir` for the Claude family;
|
|
3367
|
+
* `emitsStructuredRateLimit` for codex), NOT on `reliableTurnTerminal`. The
|
|
3368
|
+
* structured rate-limit EMIT only exists where those fields are set: codex's
|
|
3369
|
+
* emit runs under the `structuredBridgeIsCodex()` gate, so among the
|
|
3370
|
+
* codexBridgeQueue CLIs only codex carries the flag. The rest (grok / traex /
|
|
3371
|
+
* pi / hermes / mtr / cursor) map an `error` terminal to a failed/ambiguous
|
|
3372
|
+
* receipt but publish NO `limited` state — suppressing their screen `rate`
|
|
3373
|
+
* verdict would silently drop the Dashboard「需要你」signal + backoff on a real
|
|
3374
|
+
* 429. Most set `reliableTurnTerminal`, so gating on that flag would wrongly
|
|
3375
|
+
* suppress them; gating on the explicit capability fields keeps the split
|
|
3376
|
+
* exact. (A future structured rate-limit emit for another CLI just sets its
|
|
3377
|
+
* `emitsStructuredRateLimit`.)
|
|
3242
3378
|
*/
|
|
3243
3379
|
function structuredRateLimitAuthoritative() {
|
|
3244
3380
|
return isStructuredRateLimitAuthoritative(cliAdapter);
|
|
@@ -3505,7 +3641,8 @@ function failedBridgeFallbackContent(errorCode, summary, partialText) {
|
|
|
3505
3641
|
? 'worker.empty_final_failed_connection'
|
|
3506
3642
|
: 'worker.empty_final_failed';
|
|
3507
3643
|
const failure = t(key, { cliName: cliName(), reason });
|
|
3508
|
-
|
|
3644
|
+
const visiblePartialText = stripTrailingOaiMemoryCitation(partialText ?? '').trim();
|
|
3645
|
+
return visiblePartialText ? `${visiblePartialText}\n\n${failure}` : failure;
|
|
3509
3646
|
}
|
|
3510
3647
|
// ─── Bridge fallback marker (non-adopt) ────────────────────────────────────
|
|
3511
3648
|
//
|
|
@@ -3571,6 +3708,81 @@ function notifyExplicitReplyObserved(turnId, marker) {
|
|
|
3571
3708
|
...(marker.messageId ? { messageId: marker.messageId } : {}),
|
|
3572
3709
|
});
|
|
3573
3710
|
}
|
|
3711
|
+
// ─── Mojo final-answer bridge ───────────────────────────────────────────────
|
|
3712
|
+
// A headless backend has no terminal for the user to fall back on: if the agent
|
|
3713
|
+
// answers without calling `botmux send`, the answer exists only inside the
|
|
3714
|
+
// worker's synthesised screen (the streaming card) and the thread stays silent.
|
|
3715
|
+
// So mojo's per-turn `result` text is bridged into the thread through the SAME
|
|
3716
|
+
// gate the transcript-driven CLIs use — no second dedup policy: the sentinel and
|
|
3717
|
+
// the "already sent this turn" window rules live in shouldSuppressBridgeEmit.
|
|
3718
|
+
//
|
|
3719
|
+
// The window's lower bound is this record, stamped immediately before the turn's
|
|
3720
|
+
// literal write (see prepareNormalWrite) so a still-running previous turn's send
|
|
3721
|
+
// cannot land inside it. There is no upper bound to compute: mojo serialises its
|
|
3722
|
+
// turns (one child per turn; queued follow-ups wait for prompt-ready), so at the
|
|
3723
|
+
// moment the gate runs no later turn has started and no later send can exist.
|
|
3724
|
+
let mojoTurnMark = null;
|
|
3725
|
+
/** Stamp the send window at a mojo turn's literal write. Both write paths call
|
|
3726
|
+
* it (queued message flush + passthrough slash command), each right where it
|
|
3727
|
+
* rotates currentBotmuxTurnId — that pairing is what the staleness check in
|
|
3728
|
+
* deliverMojoTurnFinal relies on. */
|
|
3729
|
+
function stampMojoTurnMark(turnId, dispatchAttempt) {
|
|
3730
|
+
if (effectiveBackendType !== 'mojo')
|
|
3731
|
+
return;
|
|
3732
|
+
mojoTurnMark = { turnId, dispatchAttempt, markTimeMs: Date.now() };
|
|
3733
|
+
}
|
|
3734
|
+
function deliverMojoTurnFinal(text) {
|
|
3735
|
+
if (!text.trim())
|
|
3736
|
+
return;
|
|
3737
|
+
// A mark from an EARLIER turn must not gate this one: its window opened
|
|
3738
|
+
// before the previous turn's sends, which would suppress this answer for the
|
|
3739
|
+
// wrong reason. Falling back to an unmarked window degrades to "always
|
|
3740
|
+
// deliver" (shouldSuppressBridgeEmit needs markTimeMs to suppress on sends),
|
|
3741
|
+
// which is the safe direction for a bug whose symptom is silence.
|
|
3742
|
+
const mark = mojoTurnMark && mojoTurnMark.turnId === currentBotmuxTurnId ? mojoTurnMark : null;
|
|
3743
|
+
const turnId = mark?.turnId ?? currentBotmuxTurnId;
|
|
3744
|
+
// Without a turn id the daemon cannot resolve a reply target, and lastUuid
|
|
3745
|
+
// would not dedupe a retry. Dropping is right: this can only be output that
|
|
3746
|
+
// belongs to no Lark turn.
|
|
3747
|
+
if (!turnId) {
|
|
3748
|
+
log('Mojo final bridge skipped: no turn id for this answer');
|
|
3749
|
+
return;
|
|
3750
|
+
}
|
|
3751
|
+
const dispatchAttempt = mark?.dispatchAttempt ?? currentBotmuxDispatchAttempt;
|
|
3752
|
+
// adoptMode is structurally impossible for mojo (no pane to adopt); passing
|
|
3753
|
+
// the real flag keeps this call identical in shape to the other gate callers.
|
|
3754
|
+
const adoptMode = lastInitConfig?.adoptMode === true;
|
|
3755
|
+
const markers = adoptMode ? [] : readSendMarkers();
|
|
3756
|
+
const gateInput = {
|
|
3757
|
+
markTimeMs: mark?.markTimeMs,
|
|
3758
|
+
isLocal: false,
|
|
3759
|
+
finalText: text,
|
|
3760
|
+
};
|
|
3761
|
+
if (shouldSuppressBridgeEmit(gateInput, undefined, markers, adoptMode)) {
|
|
3762
|
+
log(`Mojo final bridge suppressed for turn ${turnId.substring(0, 12)} `
|
|
3763
|
+
+ `(${isBridgeNothingToSendFinal(text) ? 'nothing-to-send sentinel' : 'model already called botmux send'})`);
|
|
3764
|
+
// Same as the structured bridge: an explicit send IS this turn's reply, so
|
|
3765
|
+
// tell observers rather than leaving them waiting on a final that the gate
|
|
3766
|
+
// deliberately swallowed.
|
|
3767
|
+
notifyExplicitReplyObserved(turnId, explicitReplyMarkerForTurnWindow(gateInput, undefined, markers, adoptMode));
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3770
|
+
// Strip a trailing sentinel line: "prose + sentinel" with no send is the
|
|
3771
|
+
// "did the work, forgot to send" shape — post the prose, never the token.
|
|
3772
|
+
const postContent = bridgePostText(text, adoptMode);
|
|
3773
|
+
if (!postContent.trim())
|
|
3774
|
+
return;
|
|
3775
|
+
send({
|
|
3776
|
+
type: 'final_output',
|
|
3777
|
+
content: postContent,
|
|
3778
|
+
// Stable per turn: the daemon derives both its dedupe key and the Lark
|
|
3779
|
+
// idempotency uuid from this, so a retry collapses into one message.
|
|
3780
|
+
lastUuid: turnId,
|
|
3781
|
+
turnId,
|
|
3782
|
+
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
3783
|
+
});
|
|
3784
|
+
log(`Mojo final bridge delivered ${postContent.length} chars for turn ${turnId.substring(0, 12)}`);
|
|
3785
|
+
}
|
|
3574
3786
|
function submitActivityEvidenceSince(sinceMs) {
|
|
3575
3787
|
if (lastPtyActivityAtMs > sinceMs)
|
|
3576
3788
|
return 'pty-output';
|
|
@@ -4378,7 +4590,8 @@ function bridgeIngest() {
|
|
|
4378
4590
|
* session surfaces in Dashboard「需要你」with a retry countdown — identical
|
|
4379
4591
|
* wire shape to the screen-text detector's classify() output, so the daemon /
|
|
4380
4592
|
* card / persistence paths need no change. Claude-only (bridgeQueue is the
|
|
4381
|
-
* Claude bridge; Codex
|
|
4593
|
+
* Claude bridge; Codex has its own structured 429 via
|
|
4594
|
+
* maybeEmitCodexStructuredRateLimit on the codexBridgeQueue path). */
|
|
4382
4595
|
function maybeEmitStructuredRateLimit(events) {
|
|
4383
4596
|
for (const ev of events) {
|
|
4384
4597
|
if (!ev.uuid || emittedRateLimitUuids.has(ev.uuid))
|
|
@@ -4648,6 +4861,11 @@ function emitReadyTurns(opts = {}) {
|
|
|
4648
4861
|
const nothingToSendTurns = new Set();
|
|
4649
4862
|
for (let i = 0; i < ready.length; i++) {
|
|
4650
4863
|
const turn = ready[i];
|
|
4864
|
+
// Claude API-error text is execution metadata, not a model answer. The
|
|
4865
|
+
// structured terminal below owns retry/attention; never leak the raw
|
|
4866
|
+
// provider error through transcript fallback (regardless of send markers).
|
|
4867
|
+
if (turn.terminalOutcome && turn.terminalOutcome.status !== 'completed')
|
|
4868
|
+
continue;
|
|
4651
4869
|
const nextBoundaryMs = (i + 1 < ready.length ? ready[i + 1].markTimeMs : nextPendingMarkTimeMs);
|
|
4652
4870
|
if (turn.isLocal && shouldSuppressBridgeEmit({ markTimeMs: turn.markTimeMs, isLocal: turn.isLocal }, nextBoundaryMs, markers, adoptMode)) {
|
|
4653
4871
|
const reason = turn.isLocal ? 'local-typed' : 'model called botmux send within window';
|
|
@@ -4750,8 +4968,12 @@ function emitReadyTurns(opts = {}) {
|
|
|
4750
4968
|
// after the model used `botmux send`. Completion is not optional. Emit it
|
|
4751
4969
|
// only after all corresponding final_output IPC messages have been queued so
|
|
4752
4970
|
// the daemon observes a stable per-turn ordering.
|
|
4753
|
-
for (const turn of ready)
|
|
4754
|
-
|
|
4971
|
+
for (const turn of ready) {
|
|
4972
|
+
if (turn.rateLimited)
|
|
4973
|
+
continue;
|
|
4974
|
+
const outcome = turn.terminalOutcome;
|
|
4975
|
+
emitTurnTerminal(turn.turnId, outcome?.status ?? 'completed', outcome && outcome.status !== 'completed' ? outcome.errorCode : undefined, turn.dispatchAttempt, nothingToSendTurns.has(turn) ? 'nothing_to_send' : undefined, outcome && outcome.status !== 'completed' ? outcome.retryable : undefined);
|
|
4976
|
+
}
|
|
4755
4977
|
}
|
|
4756
4978
|
/** Drain `path` from `fromOffset` and feed the events to the bridge queue
|
|
4757
4979
|
* with that path as the source stamp. Pure side-effects on bridgeQueue +
|
|
@@ -5625,6 +5847,9 @@ function codexBridgeIngest(opts = {}) {
|
|
|
5625
5847
|
if (result.events.length > 0)
|
|
5626
5848
|
lastStructuredBridgeActivityAtMs = Date.now();
|
|
5627
5849
|
codexBridgeQueue.ingest(result.events);
|
|
5850
|
+
// After ingest so the latch's delivery re-kick observes the started turn —
|
|
5851
|
+
// the flush's own bridge mark must queue behind it, not ahead of it.
|
|
5852
|
+
noteSpawnArgvTurnStartTranscriptEvidence(result.events);
|
|
5628
5853
|
pruneExpiredStructuredHeadsAndEmit('structured ingest');
|
|
5629
5854
|
// Transcript-driven idle: a normal `assistant_final` or no-output
|
|
5630
5855
|
// `turn_aborted` is Codex declaring end-of-turn, far more reliable than the screen-pattern heuristic
|
|
@@ -6517,7 +6742,7 @@ function beginCliWriteCycle() {
|
|
|
6517
6742
|
resetIdleDetector: () => { idleDetector?.reset(); },
|
|
6518
6743
|
});
|
|
6519
6744
|
}
|
|
6520
|
-
async function writeAdoptMessage(content, turnId, dispatchAttempt, vcMeetingImTurnOrigin, fence) {
|
|
6745
|
+
async function writeAdoptMessage(content, turnId, dispatchAttempt, vcMeetingImTurnOrigin, trustedCaller, fence) {
|
|
6521
6746
|
const executionFence = fence ?? captureAdoptWriteFence();
|
|
6522
6747
|
if (!executionFence || !adoptWriteFenceIsCurrent(executionFence)) {
|
|
6523
6748
|
return 'stale-before-write';
|
|
@@ -6527,6 +6752,7 @@ async function writeAdoptMessage(content, turnId, dispatchAttempt, vcMeetingImTu
|
|
|
6527
6752
|
const turnSeq = usageLimitTracker.beginTurn(currentUsageLimitSnapshot());
|
|
6528
6753
|
currentBotmuxTurnId = turnId;
|
|
6529
6754
|
currentBotmuxDispatchAttempt = dispatchAttempt;
|
|
6755
|
+
currentGatewayTrustedCaller = trustedCaller;
|
|
6530
6756
|
currentVcMeetingImTurnOrigin = vcMeetingImTurnOrigin;
|
|
6531
6757
|
if (dispatchAttempt !== undefined)
|
|
6532
6758
|
durableTurnInFlight = true;
|
|
@@ -6741,7 +6967,7 @@ async function runAdoptMessageForCapturedGeneration(item, requeue) {
|
|
|
6741
6967
|
queue: adoptWriteQueue,
|
|
6742
6968
|
isCurrent: () => adoptWriteFenceIsCurrent(fence),
|
|
6743
6969
|
onStale: requeueOnce,
|
|
6744
|
-
write: () => writeAdoptMessage(item.content, item.turnId, item.dispatchAttempt, item.vcMeetingImTurnOrigin, fence),
|
|
6970
|
+
write: () => writeAdoptMessage(item.content, item.turnId, item.dispatchAttempt, item.vcMeetingImTurnOrigin, item.trustedCaller, fence),
|
|
6745
6971
|
});
|
|
6746
6972
|
if (queued.status === 'stale-before-write')
|
|
6747
6973
|
return 'stale-before-write';
|
|
@@ -6887,6 +7113,8 @@ let displayMode = 'hidden';
|
|
|
6887
7113
|
let screenshotTimer = null;
|
|
6888
7114
|
let pendingShotTimer = null;
|
|
6889
7115
|
let lastShotHash = '';
|
|
7116
|
+
// Throttle for the happy-path upload log in captureAndUpload (one line/min).
|
|
7117
|
+
let lastUploadLogAtMs = 0;
|
|
6890
7118
|
let larkAppIdForUpload = '';
|
|
6891
7119
|
let larkAppSecretForUpload = '';
|
|
6892
7120
|
let larkBrandForUpload = 'feishu';
|
|
@@ -7015,6 +7243,14 @@ async function captureAndUpload() {
|
|
|
7015
7243
|
return;
|
|
7016
7244
|
}
|
|
7017
7245
|
const status = projectedRuntimeScreenStatus();
|
|
7246
|
+
// Success is otherwise completely silent (skips and failures log above), which
|
|
7247
|
+
// made "loop alive and uploading" indistinguishable from "loop never started"
|
|
7248
|
+
// in the daemon log. One throttled line keeps the happy path observable.
|
|
7249
|
+
const nowMs = Date.now();
|
|
7250
|
+
if (nowMs - lastUploadLogAtMs >= 60_000) {
|
|
7251
|
+
lastUploadLogAtMs = nowMs;
|
|
7252
|
+
log(`Screenshot uploaded (${png.length}B, key=${imageKey.slice(0, 12)}…)`);
|
|
7253
|
+
}
|
|
7018
7254
|
send({
|
|
7019
7255
|
type: 'screenshot_uploaded',
|
|
7020
7256
|
imageKey,
|
|
@@ -7069,10 +7305,10 @@ function sendTermActionOnce(target, key) {
|
|
|
7069
7305
|
return false;
|
|
7070
7306
|
}
|
|
7071
7307
|
async function handleTermAction(key) {
|
|
7072
|
-
//
|
|
7073
|
-
// ANSI 序列的 follow-up
|
|
7074
|
-
if (effectiveBackendType === 'riff') {
|
|
7075
|
-
log(`term_action '${key}' ignored —
|
|
7308
|
+
// 远端后端:没有终端可驱动——把控制字符 write 进 riff/mojo 会变成一个内容为
|
|
7309
|
+
// ANSI 序列的 follow-up turn(^C 也不会 cancel 远端执行),必须整体拒绝。
|
|
7310
|
+
if (effectiveBackendType === 'riff' || effectiveBackendType === 'mojo') {
|
|
7311
|
+
log(`term_action '${key}' ignored — ${effectiveBackendType} backend has no local terminal to drive`);
|
|
7076
7312
|
return;
|
|
7077
7313
|
}
|
|
7078
7314
|
if (!backend)
|
|
@@ -7942,12 +8178,12 @@ async function handleTrustedCodexAppMarker(kind, payload, control) {
|
|
|
7942
8178
|
log(`${cliName()} native turn ${nativeTurnId.substring(0, 12)} mapped to botmux turn ${turnId.substring(0, 12)}`);
|
|
7943
8179
|
}
|
|
7944
8180
|
let suppressDelivery = false;
|
|
7945
|
-
// What actually reaches Lark: strip
|
|
7946
|
-
//
|
|
7947
|
-
// validator asserts finalContent==='' and
|
|
7948
|
-
// If nothing remains
|
|
7949
|
-
//
|
|
7950
|
-
const deliverableContent =
|
|
8181
|
+
// What actually reaches Lark: strip internal memory attribution and a
|
|
8182
|
+
// trailing sentinel line. `finalContent` stays RAW above (the
|
|
8183
|
+
// steer_superseded validator asserts finalContent==='' and the fallback gate
|
|
8184
|
+
// must see the unstripped payload). If nothing remains, this was metadata or
|
|
8185
|
+
// a pure-silence final → persist the FIFO advance without delivering.
|
|
8186
|
+
const deliverableContent = bridgePostText(finalContent, false);
|
|
7951
8187
|
if (deliverableContent.trim().length === 0 && finalContent.trim().length > 0) {
|
|
7952
8188
|
suppressDelivery = true;
|
|
7953
8189
|
}
|
|
@@ -8361,6 +8597,20 @@ function onPtyData(data) {
|
|
|
8361
8597
|
maybeReportDeferredTopicMaterialization(data);
|
|
8362
8598
|
maybeCaptureKiroSessionId(data);
|
|
8363
8599
|
captureWorkflowTranscript(data);
|
|
8600
|
+
// Argv first-turn start evidence (Pi): latch the busy marker from the raw
|
|
8601
|
+
// PTY stream. The viewport-based defer cannot see a marker that has not
|
|
8602
|
+
// rendered yet, and a fast turn can paint and clear `Working...` between
|
|
8603
|
+
// screen samples — the stream sees every byte exactly once.
|
|
8604
|
+
if (spawnArgvNeedsWorkingSeed
|
|
8605
|
+
&& !spawnArgvTurnStartEvidenceSeen
|
|
8606
|
+
&& structuredBridgeIsPi()
|
|
8607
|
+
&& cliAdapter?.busyPattern) {
|
|
8608
|
+
spawnArgvTurnStartBusyScanTail = tailChars(spawnArgvTurnStartBusyScanTail + stripAnsiForLog(data), 500);
|
|
8609
|
+
if (cliAdapter.busyPattern.test(spawnArgvTurnStartBusyScanTail)) {
|
|
8610
|
+
spawnArgvTurnStartEvidenceSeen = true;
|
|
8611
|
+
spawnArgvTurnStartBusyScanTail = '';
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8364
8614
|
renderer?.write(data);
|
|
8365
8615
|
// In tmux-attach mode, each web client has its own tmux attach PTY —
|
|
8366
8616
|
// no relay needed. In non-tmux mode AND in pipe mode (adopt-bridge),
|
|
@@ -8603,6 +8853,125 @@ function scheduleStructuredStartGraceRecheck(remainingMs) {
|
|
|
8603
8853
|
}, Math.max(1, remainingMs + 1));
|
|
8604
8854
|
structuredStartGraceRecheckTimer.unref?.();
|
|
8605
8855
|
}
|
|
8856
|
+
/** How long the argv first-turn evidence gate may hold ready with no start
|
|
8857
|
+
* evidence at all. Pi's startup gap (extension/model loading before it
|
|
8858
|
+
* consumes the argv prompt) is typically 3–9s; 90s covers slow hosts while
|
|
8859
|
+
* keeping a never-consumed argv prompt from parking the card at 工作中
|
|
8860
|
+
* forever. */
|
|
8861
|
+
const SPAWN_ARGV_TURN_START_EVIDENCE_GRACE_MS = 90_000;
|
|
8862
|
+
/** True while markPromptReady must hold the argv-baked first prompt's ready:
|
|
8863
|
+
* no "turn started" evidence yet and the bounded grace has not expired.
|
|
8864
|
+
* Scoped to Pi — the only quiescence-argv adapter with BOTH evidence channels
|
|
8865
|
+
* (busyPattern + always-on structured transcript); Gemini/OpenCode/MTR keep
|
|
8866
|
+
* their historical first-idle behavior. Never applies to the Grok-class busy
|
|
8867
|
+
* arm, whose first ready is handled by spawnArgvInitialPromptBusy. */
|
|
8868
|
+
function spawnArgvTurnStartGateHolds() {
|
|
8869
|
+
if (!spawnArgvNeedsWorkingSeed || spawnArgvInitialPromptBusy)
|
|
8870
|
+
return false;
|
|
8871
|
+
if (!structuredBridgeIsPi())
|
|
8872
|
+
return false;
|
|
8873
|
+
if (spawnArgvTurnStartEvidenceSeen)
|
|
8874
|
+
return false;
|
|
8875
|
+
return Date.now() < spawnArgvTurnStartEvidenceDeadlineMs;
|
|
8876
|
+
}
|
|
8877
|
+
/** Latch "the argv first turn actually began" from live structured-transcript
|
|
8878
|
+
* events. The latch must happen at INGEST time, not lazily at gate time: a
|
|
8879
|
+
* fast first turn can be started AND drained/emitted before markPromptReady
|
|
8880
|
+
* runs, leaving no started turn to observe in the queue. A user record is the
|
|
8881
|
+
* authoritative start signal; a final implies start too (covers a drain that
|
|
8882
|
+
* sees both lines at once).
|
|
8883
|
+
*
|
|
8884
|
+
* The latch is also a DELIVERY DRIVER, not just a boolean: when evidence
|
|
8885
|
+
* lands later than the 15s first-prompt timeout, every markPromptReady-based
|
|
8886
|
+
* driver has already fired rejected and stopped (probe one-shot, timeout
|
|
8887
|
+
* short-circuit) and the 90s fail-open stands down on evidenceSeen — with a
|
|
8888
|
+
* silent PTY nothing else would ever deliver a startup-queued message. The
|
|
8889
|
+
* user record hitting disk is itself the proof the TUI accepts input, so
|
|
8890
|
+
* re-kick immediately. (The PTY busyPattern latch needs no re-kick: busy
|
|
8891
|
+
* output implies a later quiescence edge that re-drives markPromptReady.) */
|
|
8892
|
+
function noteSpawnArgvTurnStartTranscriptEvidence(events) {
|
|
8893
|
+
// The whole evidence mechanism is Pi-scoped (matching the onPtyData busy
|
|
8894
|
+
// scan and spawnArgvTurnStartGateHolds); this generic ingest path also
|
|
8895
|
+
// serves Grok — another argv-baked + structured-bridge + type-ahead CLI —
|
|
8896
|
+
// so without this guard the flush side effect below would add a new
|
|
8897
|
+
// startup-window write for Grok (whose first ready is owned by the
|
|
8898
|
+
// SessionStart busy arm, not by this gate).
|
|
8899
|
+
if (!structuredBridgeIsPi())
|
|
8900
|
+
return;
|
|
8901
|
+
if (spawnArgvTurnStartEvidenceSeen || !spawnArgvNeedsWorkingSeed)
|
|
8902
|
+
return;
|
|
8903
|
+
if (!events.some(ev => ev.kind === 'user' || ev.kind === 'assistant_final'))
|
|
8904
|
+
return;
|
|
8905
|
+
spawnArgvTurnStartEvidenceSeen = true;
|
|
8906
|
+
spawnArgvTurnStartBusyScanTail = '';
|
|
8907
|
+
if (!isPromptReady && pendingMessages.length > 0) {
|
|
8908
|
+
log('Argv first-turn start evidence landed via transcript — releasing startup-queued input');
|
|
8909
|
+
flushQueuedInputAfterTurnStartEvidence();
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
function stopSpawnArgvTurnStartFailOpen() {
|
|
8913
|
+
if (!spawnArgvTurnStartFailOpenTimer)
|
|
8914
|
+
return;
|
|
8915
|
+
clearTimeout(spawnArgvTurnStartFailOpenTimer);
|
|
8916
|
+
spawnArgvTurnStartFailOpenTimer = null;
|
|
8917
|
+
}
|
|
8918
|
+
/** Turn-start evidence in hand ⇒ startup-queued input becomes deliverable.
|
|
8919
|
+
*
|
|
8920
|
+
* Messages that arrive during startup (awaitingFirstPrompt) skip
|
|
8921
|
+
* handleMessage's type-ahead write (shouldWriteNow holds them, "no input box
|
|
8922
|
+
* yet") and historically relied on markPromptReady()'s tail flush for
|
|
8923
|
+
* delivery. When a Pi argv first turn never lands its terminal record
|
|
8924
|
+
* (terminate:true gap), every ready driver dies rejected: the structured
|
|
8925
|
+
* lifecycle block refuses quiescence idles, the queued-message busy probe
|
|
8926
|
+
* and the 15s first-prompt timeout both short-circuit through the same
|
|
8927
|
+
* rejected markPromptReady(), and the 90s fail-open stands down once
|
|
8928
|
+
* evidence is latched. Without an explicit re-kick the message sits in
|
|
8929
|
+
* pendingMessages forever (card pinned at 工作中, the next transcript user
|
|
8930
|
+
* event never appears, HOL-drop never fires).
|
|
8931
|
+
*
|
|
8932
|
+
* Two call sites, one safety argument — input may only be re-kicked once the
|
|
8933
|
+
* TUI provably booted far enough to accept it:
|
|
8934
|
+
* 1. markPromptReady()'s lifecycle-block rejection (a started turn /
|
|
8935
|
+
* confirmed submit exists);
|
|
8936
|
+
* 2. the transcript-evidence latch in codexBridgeIngest (the user record
|
|
8937
|
+
* just hit disk — the ONLY driver left when evidence lands later than
|
|
8938
|
+
* every probe/timeout and the PTY stays silent).
|
|
8939
|
+
* The argv turn-start evidence gate must NOT re-kick — before any evidence
|
|
8940
|
+
* the TUI may still be loading extensions/models and a type-ahead paste
|
|
8941
|
+
* could be dropped or land in a half-drawn screen (flushPending() has no
|
|
8942
|
+
* awaitingFirstPrompt gate of its own; shouldWriteNow()'s hold is the only
|
|
8943
|
+
* startup input protection).
|
|
8944
|
+
*
|
|
8945
|
+
* flushPending() still re-checks its own admission gates (type-ahead
|
|
8946
|
+
* allowance, durable HOL, injection serialization, restart fences,
|
|
8947
|
+
* ready-gate settle), so a non-type-ahead adapter keeps holding until a real
|
|
8948
|
+
* ready edge; this is a re-kick, never a bypass of those gates. */
|
|
8949
|
+
function flushQueuedInputAfterTurnStartEvidence() {
|
|
8950
|
+
if (pendingMessages.length === 0)
|
|
8951
|
+
return;
|
|
8952
|
+
flushPending();
|
|
8953
|
+
}
|
|
8954
|
+
/** Re-drive the held first idle if no start evidence ever appears. Without
|
|
8955
|
+
* this, a swallowed quiescence idle followed by a fully silent PTY (argv
|
|
8956
|
+
* prompt never consumed) would leave no later signal to re-check the gate.
|
|
8957
|
+
* The re-driven idle still passes deferPromptReadyWhileBusy, so a turn whose
|
|
8958
|
+
* evidence was merely missed keeps deferring on a busy viewport. */
|
|
8959
|
+
function scheduleSpawnArgvTurnStartFailOpen() {
|
|
8960
|
+
if (spawnArgvTurnStartFailOpenTimer)
|
|
8961
|
+
return;
|
|
8962
|
+
const backendAtSchedule = backend;
|
|
8963
|
+
const cliGenerationAtSchedule = cliSpawnGeneration;
|
|
8964
|
+
spawnArgvTurnStartFailOpenTimer = setTimeout(() => {
|
|
8965
|
+
spawnArgvTurnStartFailOpenTimer = null;
|
|
8966
|
+
if (backend !== backendAtSchedule || cliSpawnGeneration !== cliGenerationAtSchedule)
|
|
8967
|
+
return;
|
|
8968
|
+
if (isPromptReady || !spawnArgvNeedsWorkingSeed || spawnArgvTurnStartEvidenceSeen)
|
|
8969
|
+
return;
|
|
8970
|
+
log('Argv-baked first prompt showed no start evidence within grace — failing open to quiescence idle');
|
|
8971
|
+
idleDetector?.fireIdle();
|
|
8972
|
+
}, Math.max(1, spawnArgvTurnStartEvidenceDeadlineMs - Date.now()));
|
|
8973
|
+
spawnArgvTurnStartFailOpenTimer.unref?.();
|
|
8974
|
+
}
|
|
8606
8975
|
function markPromptReady() {
|
|
8607
8976
|
if (bareShellLaunchBlocked) {
|
|
8608
8977
|
log('Ignoring non-PTY prompt-ready while bare-shell launch block is active');
|
|
@@ -8678,6 +9047,29 @@ function markPromptReady() {
|
|
|
8678
9047
|
}
|
|
8679
9048
|
if (freshnessAction === 'ignore')
|
|
8680
9049
|
return;
|
|
9050
|
+
// Argv-baked first prompt start gate (Pi): the TUI paints its input box
|
|
9051
|
+
// seconds before the CLI begins consuming the argv prompt, and that startup
|
|
9052
|
+
// window has no busyPattern on screen and no transcript user record —
|
|
9053
|
+
// quiescence then produces a FALSE first idle ("not started yet", not "turn
|
|
9054
|
+
// complete"). Letting it through would set isPromptReady for the whole turn
|
|
9055
|
+
// and seed working→idle mid-turn (premature ✅ DONE + card flip to 等待输入).
|
|
9056
|
+
// Hold ready and re-arm until the turn visibly starts; bounded fail-open via
|
|
9057
|
+
// SPAWN_ARGV_TURN_START_EVIDENCE_GRACE_MS restores the historical quiescence
|
|
9058
|
+
// behavior if evidence never appears.
|
|
9059
|
+
// Deliberately NO flush re-kick here (unlike the lifecycle block below):
|
|
9060
|
+
// with zero turn-start evidence the TUI may still be booting, and a
|
|
9061
|
+
// type-ahead paste could be dropped or land in a half-drawn screen —
|
|
9062
|
+
// shouldWriteNow()'s awaitingFirstPrompt hold is the only startup input
|
|
9063
|
+
// protection and flushPending() does not re-check it. Queued messages are
|
|
9064
|
+
// delivered the moment evidence appears (the transcript latch re-kicks, a
|
|
9065
|
+
// busy-marker latch implies a later quiescence edge) or by the 90s
|
|
9066
|
+
// fail-open.
|
|
9067
|
+
if (spawnArgvTurnStartGateHolds()) {
|
|
9068
|
+
log('Argv-baked first prompt has not visibly started (no busy marker or transcript user event since spawn) — re-arming idle detector');
|
|
9069
|
+
idleDetector?.reset();
|
|
9070
|
+
scheduleSpawnArgvTurnStartFailOpen();
|
|
9071
|
+
return;
|
|
9072
|
+
}
|
|
8681
9073
|
// Screen prompt/quiescence is only a UI heuristic. Structured transcript
|
|
8682
9074
|
// bridges have the stronger lifecycle signal: a transcript-started turn
|
|
8683
9075
|
// without assistant_final is still running even if the TUI redraw exposes a
|
|
@@ -8687,13 +9079,19 @@ function markPromptReady() {
|
|
|
8687
9079
|
// so a dropped Enter cannot create permanent false-busy.
|
|
8688
9080
|
// Reject the heuristic and re-arm IdleDetector so the later transcript-final
|
|
8689
9081
|
// fireIdle() can drive the real ready edge.
|
|
8690
|
-
if (hasStructuredLifecycleBlock()) {
|
|
9082
|
+
if (hasStructuredLifecycleBlock() && !spawnArgvInitialPromptBusy) {
|
|
9083
|
+
// Grok is lifecycle-blocking, but its FIRST ready is still a pre-execution
|
|
9084
|
+
// SessionStart / idle-detector edge while the argv-baked prompt is running.
|
|
9085
|
+
// The spawnArgvInitialPromptBusy arm below must publish working and consume
|
|
9086
|
+
// itself on that first edge. After the arm is gone, later TUI redraws stay
|
|
9087
|
+
// blocked until turn_completed so they cannot publish idle mid-turn.
|
|
8691
9088
|
const remainingMs = codexBridgeQueue.preStartLeaseRemainingMs();
|
|
8692
9089
|
structuredRejectedReadyEvidenceGeneration = ptyOutputGeneration.snapshot();
|
|
8693
9090
|
log('Ignoring prompt-ready heuristic while a structured turn is unfinished or submit verification/start is pending');
|
|
8694
9091
|
idleDetector?.reset();
|
|
8695
9092
|
if (remainingMs !== undefined)
|
|
8696
9093
|
scheduleStructuredStartGraceRecheck(remainingMs);
|
|
9094
|
+
flushQueuedInputAfterTurnStartEvidence();
|
|
8697
9095
|
return;
|
|
8698
9096
|
}
|
|
8699
9097
|
structuredRejectedReadyEvidenceGeneration = undefined;
|
|
@@ -9297,10 +9695,11 @@ async function flushPending() {
|
|
|
9297
9695
|
const rawInputReady = isPromptReady && pendingRawInputs.length > 0;
|
|
9298
9696
|
const adoptInputReady = isPromptReady && lastInitConfig?.adoptMode === true && pendingAdoptMessages.length > 0;
|
|
9299
9697
|
let supportedSessionRenameReady = sessionRenameReady;
|
|
9300
|
-
|
|
9698
|
+
const renameOnRemoteBackend = effectiveBackendType === 'riff' || effectiveBackendType === 'mojo';
|
|
9699
|
+
if (sessionRenameReady && (!cliAdapter.buildSessionRenameCommand || renameOnRemoteBackend)) {
|
|
9301
9700
|
pendingSessionRename = null;
|
|
9302
9701
|
supportedSessionRenameReady = false;
|
|
9303
|
-
log(`Ignoring native session rename — unsupported by ${cliName()}${
|
|
9702
|
+
log(`Ignoring native session rename — unsupported by ${cliName()}${renameOnRemoteBackend ? ` on ${effectiveBackendType} backend` : ''}`);
|
|
9304
9703
|
if (pendingMessages.length === 0 && pendingAdoptMessages.length === 0 && pendingRawInputs.length === 0)
|
|
9305
9704
|
return;
|
|
9306
9705
|
}
|
|
@@ -9443,6 +9842,10 @@ async function flushPending() {
|
|
|
9443
9842
|
const item = freshnessInputQueue.takeNormal();
|
|
9444
9843
|
if (!item)
|
|
9445
9844
|
break;
|
|
9845
|
+
// Apply the credential snapshot that arrived WITH this turn, immediately
|
|
9846
|
+
// before the turn is written. Doing it at IPC-receive time collapsed two
|
|
9847
|
+
// queued credential turns (A/B/C ran as A/C/C).
|
|
9848
|
+
applyMojoLivePatch(item.mojoLivePatch);
|
|
9446
9849
|
// Type-ahead can drain several items in one flush. Each logical submit
|
|
9447
9850
|
// starts its own readiness generation before transcript marking/writing.
|
|
9448
9851
|
beginCliWriteCycle();
|
|
@@ -9469,6 +9872,7 @@ async function flushPending() {
|
|
|
9469
9872
|
renderer?.markNewTurn();
|
|
9470
9873
|
currentBotmuxTurnId = item.turnId;
|
|
9471
9874
|
currentBotmuxDispatchAttempt = item.dispatchAttempt;
|
|
9875
|
+
currentGatewayTrustedCaller = item.trustedCaller;
|
|
9472
9876
|
currentVcMeetingImTurnOrigin = item.vcMeetingImTurnOrigin;
|
|
9473
9877
|
// Acquire durable HOL ownership only after this turn owns the backend
|
|
9474
9878
|
// submission mutex. If an older ZMX recovery debt rejects capture,
|
|
@@ -9503,6 +9907,13 @@ async function flushPending() {
|
|
|
9503
9907
|
codexBridgeQueue.beginSubmitVerification(bridgeTurnId, undefined, item.dispatchAttempt);
|
|
9504
9908
|
}
|
|
9505
9909
|
}
|
|
9910
|
+
else {
|
|
9911
|
+
// Same anchoring rule as the transcript bridges above: stamp the send
|
|
9912
|
+
// window's lower bound at the literal write, not on IPC arrival, so a
|
|
9913
|
+
// previous turn's `botmux send` cannot fall inside this turn's window
|
|
9914
|
+
// and falsely suppress its final. No-op for non-mojo backends.
|
|
9915
|
+
stampMojoTurnMark(item.turnId, item.dispatchAttempt);
|
|
9916
|
+
}
|
|
9506
9917
|
if (durableWrite
|
|
9507
9918
|
&& cliAdapter.reliableTurnTerminal === true
|
|
9508
9919
|
&& lastInitConfig?.cliId === 'claude-code'
|
|
@@ -9634,13 +10045,21 @@ async function flushPending() {
|
|
|
9634
10045
|
}
|
|
9635
10046
|
else if (item.codexAppInput && writeAdapter.writeStructuredInput) {
|
|
9636
10047
|
submissionBackend = writeBackend;
|
|
9637
|
-
const transaction = await runAmbiguousSubmissionTransaction(writeBackend, () => writeAdapter.writeStructuredInput(adapterInputHandle(writeBackend), msg, item.codexAppInput, {
|
|
10048
|
+
const transaction = await runAmbiguousSubmissionTransaction(writeBackend, () => writeAdapter.writeStructuredInput(adapterInputHandle(writeBackend), msg, item.codexAppInput, {
|
|
10049
|
+
turnId: item.turnId,
|
|
10050
|
+
...(item.trustedCaller ? { trustedCaller: item.trustedCaller } : {}),
|
|
10051
|
+
...(item.codexAppSteerable ? { codexAppSteerable: true } : {}),
|
|
10052
|
+
}), settleVerifiableSubmissionForJournal, prepareNormalWrite);
|
|
9638
10053
|
result = transaction.result;
|
|
9639
10054
|
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
9640
10055
|
}
|
|
9641
10056
|
else {
|
|
9642
10057
|
submissionBackend = writeBackend;
|
|
9643
|
-
const transaction = await runAmbiguousSubmissionTransaction(writeBackend, () => writeAdapter.writeInput(adapterInputHandle(writeBackend), msg, {
|
|
10058
|
+
const transaction = await runAmbiguousSubmissionTransaction(writeBackend, () => writeAdapter.writeInput(adapterInputHandle(writeBackend), msg, {
|
|
10059
|
+
turnId: item.turnId,
|
|
10060
|
+
...(item.trustedCaller ? { trustedCaller: item.trustedCaller } : {}),
|
|
10061
|
+
...(item.codexAppSteerable ? { codexAppSteerable: true } : {}),
|
|
10062
|
+
}), settleVerifiableSubmissionForJournal, prepareNormalWrite);
|
|
9644
10063
|
result = transaction.result;
|
|
9645
10064
|
recoveryFailureReason = transaction.recoveryFailureReason;
|
|
9646
10065
|
}
|
|
@@ -9901,6 +10320,8 @@ async function flushPending() {
|
|
|
9901
10320
|
// HOL-dropped or steered into the other.
|
|
9902
10321
|
if (rpcLifecycleFailClosedOwners.size > 0)
|
|
9903
10322
|
break;
|
|
10323
|
+
if (item.trustedCaller && lastInitConfig?.cliId === 'codex')
|
|
10324
|
+
break;
|
|
9904
10325
|
if (shouldStopPendingBatch(item, pendingMessages[0]))
|
|
9905
10326
|
break;
|
|
9906
10327
|
}
|
|
@@ -9917,6 +10338,30 @@ async function flushPending() {
|
|
|
9917
10338
|
}
|
|
9918
10339
|
}
|
|
9919
10340
|
}
|
|
10341
|
+
/**
|
|
10342
|
+
* Find an executable using the PATH the CHILD will actually see.
|
|
10343
|
+
*
|
|
10344
|
+
* The layered env (worker env → per-bot `env`) is authoritative: when it names a
|
|
10345
|
+
* PATH, that is the ONLY place searched, because falling back to the daemon's own
|
|
10346
|
+
* PATH is exactly how an ambient install shadowed a per-bot one.
|
|
10347
|
+
*/
|
|
10348
|
+
function locateOnEffectiveChildPath(cmd, childEnvForPath) {
|
|
10349
|
+
const childPath = childEnvForPath.PATH;
|
|
10350
|
+
if (!childPath)
|
|
10351
|
+
return locateOnPath(cmd);
|
|
10352
|
+
for (const dir of childPath.split(delimiter)) {
|
|
10353
|
+
if (!dir)
|
|
10354
|
+
continue;
|
|
10355
|
+
const candidate = join(dir, cmd);
|
|
10356
|
+
try {
|
|
10357
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
10358
|
+
return candidate;
|
|
10359
|
+
}
|
|
10360
|
+
catch { /* not here */ }
|
|
10361
|
+
}
|
|
10362
|
+
// Explicit child PATH is authoritative — do NOT fall back to the ambient one.
|
|
10363
|
+
return null;
|
|
10364
|
+
}
|
|
9920
10365
|
function sendToPty(content, turnId, opts = {}) {
|
|
9921
10366
|
const next = {
|
|
9922
10367
|
content,
|
|
@@ -9925,7 +10370,9 @@ function sendToPty(content, turnId, opts = {}) {
|
|
|
9925
10370
|
...(opts.codexAppDispatchId ? { codexAppDispatchId: opts.codexAppDispatchId } : {}),
|
|
9926
10371
|
...(opts.codexAppSteerable ? { codexAppSteerable: true } : {}),
|
|
9927
10372
|
...(opts.queuedActivationToken ? { queuedActivationToken: opts.queuedActivationToken } : {}),
|
|
10373
|
+
...(opts.mojoLivePatch ? { mojoLivePatch: opts.mojoLivePatch } : {}),
|
|
9928
10374
|
...(opts.codexAppInput ? { codexAppInput: opts.codexAppInput } : {}),
|
|
10375
|
+
...(opts.trustedCaller ? { trustedCaller: opts.trustedCaller } : {}),
|
|
9929
10376
|
...(opts.dispatchAttempt !== undefined ? { dispatchAttempt: opts.dispatchAttempt } : {}),
|
|
9930
10377
|
...(opts.atMostOnce ? { noReplay: true } : {}),
|
|
9931
10378
|
...(opts.vcMeetingImTurnOrigin
|
|
@@ -10771,18 +11218,82 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
10771
11218
|
// PTY/tmux backends inject these into the child process env directly; riff
|
|
10772
11219
|
// has no local process, so they go via config.env → the riff API's config.env.
|
|
10773
11220
|
let riffBackendConfig = cfg.backendConfig;
|
|
11221
|
+
// mojo: combine the user's `mojo` block with the platform-owned launch identity
|
|
11222
|
+
// into the ONE EffectiveMojoConfig the backend runs on. Without this the backend
|
|
11223
|
+
// silently ignores everything botmux already resolved — repo selection
|
|
11224
|
+
// (workingDir), the dashboard/setup `model`, `disableCliBypass`,
|
|
11225
|
+
// `cliPathOverride` and the persisted session lineage.
|
|
11226
|
+
//
|
|
11227
|
+
// There is NO precedence question: the launch identity has exactly one source
|
|
11228
|
+
// (the top-level bot fields, frozen onto the session). The `mojo` block is not
|
|
11229
|
+
// a fallback for those keys — both config entry points reject them, and the
|
|
11230
|
+
// builder strips any that slip through. Do not reintroduce a `block ?? generic`
|
|
11231
|
+
// merge here; that was the double-entry-point bug.
|
|
11232
|
+
if (effectiveBackendType === 'mojo') {
|
|
11233
|
+
// Defensive third gate. Both config doors already validate, but the IPC
|
|
11234
|
+
// payload could be stale (written by an older daemon) or malformed, and the
|
|
11235
|
+
// failure mode is a security one: `localDaemon: "false"` satisfies the
|
|
11236
|
+
// sandbox check's `!== true` while being truthy when building the child env,
|
|
11237
|
+
// i.e. isolation skipped AND host execution enabled. Refuse to launch rather
|
|
11238
|
+
// than run in that state.
|
|
11239
|
+
const validated = normalizeMojoConfig(cfg.backendConfig);
|
|
11240
|
+
if (!validated.ok) {
|
|
11241
|
+
throw new Error(`mojo config is invalid: ${validated.errors.join('; ')}`);
|
|
11242
|
+
}
|
|
11243
|
+
// Shared with the daemon's workerless `/close` path (see
|
|
11244
|
+
// destroyOrphanedBackingSession) so a session that runs on a custom binary /
|
|
11245
|
+
// per-bot JWT can still be cancelled after its worker is gone.
|
|
11246
|
+
riffBackendConfig = buildEffectiveMojoConfig(validated.value, {
|
|
11247
|
+
cliPathOverride: cfg.cliPathOverride,
|
|
11248
|
+
workingDir: cfg.workingDir,
|
|
11249
|
+
model: cfg.model,
|
|
11250
|
+
disableCliBypass: cfg.disableCliBypass,
|
|
11251
|
+
env: cfg.env ? sanitizePerBotEnv(cfg.env) : undefined,
|
|
11252
|
+
// Resume the persisted lineage: daemon restart, /relay and worker rebuilds
|
|
11253
|
+
// all arrive with riffParentTaskId set (the shared remote-lineage channel —
|
|
11254
|
+
// see the riff_task_id IPC message, emitted by the generic
|
|
11255
|
+
// backend.onTaskId hook and therefore already carrying mojo ids).
|
|
11256
|
+
resumeCliSessionId: cfg.riffParentTaskId,
|
|
11257
|
+
wrapperCli: cfg.wrapperCli,
|
|
11258
|
+
// mojo is injectsSessionContext + global skillsDir (same shape as
|
|
11259
|
+
// genius/grok), so session-manager does NOT wrap the per-message skill
|
|
11260
|
+
// envelope for it — the catalog for `prompt` mode, and the help pointer for
|
|
11261
|
+
// `off`, must ride on the prompt MojoBackend builds. Resolved here because
|
|
11262
|
+
// the mode lookup needs larkAppId and the backend has no bot context.
|
|
11263
|
+
// `global` resolves to '' (files already installed under ~/.mojo/skills).
|
|
11264
|
+
builtinSkillBlock: builtinSkillBlockForInjectsSessionContext(cfg.larkAppId, cfg.locale, {
|
|
11265
|
+
asksViaHook: false,
|
|
11266
|
+
whiteboardEnabled: whiteboardEnabled(),
|
|
11267
|
+
// mojo emits NO <botmux_routing> block (unlike genius/grok, which build one
|
|
11268
|
+
// via buildBotmuxSystemPromptText). So the catalog must keep the
|
|
11269
|
+
// routing-covered skills (history/quoted/bots) — otherwise they are
|
|
11270
|
+
// documented nowhere — and must not cite a routing block that is absent.
|
|
11271
|
+
// Full routing/identity injection for mojo is deliberately out of scope
|
|
11272
|
+
// here: a sandboxed remote session cannot reuse the normal CLI's routing
|
|
11273
|
+
// semantics verbatim (see the --mention-back note in adapters/cli/mojo.ts).
|
|
11274
|
+
hasRoutingBlock: false,
|
|
11275
|
+
}),
|
|
11276
|
+
});
|
|
11277
|
+
const resumed = riffBackendConfig.resumeCliSessionId;
|
|
11278
|
+
if (resumed)
|
|
11279
|
+
log(`mojo resuming session lineage ${resumed}`);
|
|
11280
|
+
}
|
|
10774
11281
|
if (effectiveBackendType === 'riff') {
|
|
10775
11282
|
if (!cfg.backendConfig) {
|
|
10776
11283
|
throw new Error('riff backend requires backendConfig (baseUrl, etc.)');
|
|
10777
11284
|
}
|
|
11285
|
+
// backendConfig is a union shared with the mojo backend; inside this
|
|
11286
|
+
// riff-only branch the shape is known, so narrow once instead of casting at
|
|
11287
|
+
// each riff-specific field access below.
|
|
11288
|
+
const riffCfg = cfg.backendConfig;
|
|
10778
11289
|
// Fail fast on a missing/invalid baseUrl — every config entry point funnels
|
|
10779
11290
|
// through this spawn gate, and a late `fetch("undefined/api/…")` error is
|
|
10780
11291
|
// far harder to diagnose than an explicit spawn refusal.
|
|
10781
|
-
if (!isValidRiffBaseUrl(
|
|
10782
|
-
throw new Error(`riff baseUrl 未配置或非法(需 http(s) URL,当前: ${JSON.stringify(
|
|
11292
|
+
if (!isValidRiffBaseUrl(riffCfg.baseUrl)) {
|
|
11293
|
+
throw new Error(`riff baseUrl 未配置或非法(需 http(s) URL,当前: ${JSON.stringify(riffCfg.baseUrl ?? null)})——请在 dashboard 的 Riff 配置中填写`);
|
|
10783
11294
|
}
|
|
10784
|
-
if (
|
|
10785
|
-
throw new Error(`riff sandboxCluster 非法(仅支持 boe/cn,当前: ${JSON.stringify(
|
|
11295
|
+
if (riffCfg.sandboxCluster !== undefined && !isValidRiffSandboxCluster(riffCfg.sandboxCluster)) {
|
|
11296
|
+
throw new Error(`riff sandboxCluster 非法(仅支持 boe/cn,当前: ${JSON.stringify(riffCfg.sandboxCluster)})——请在 dashboard 的 Riff 配置中重新选择`);
|
|
10786
11297
|
}
|
|
10787
11298
|
const sessionEnv = {
|
|
10788
11299
|
BOTMUX_SESSION_ID: cfg.sessionId,
|
|
@@ -10832,14 +11343,24 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
10832
11343
|
sessionEnv.BOTMUX_LARK_LIST_BOTS_API_ENABLED = chatBotDiscovery.listBotsApiEnabled ? 'true' : 'false';
|
|
10833
11344
|
sessionEnv.BOTMUX_LARK_LIST_BOTS_API_TIMEOUT_MS = String(chatBotDiscovery.listBotsApiTimeoutMs);
|
|
10834
11345
|
// Per-bot env (bots.json `env`) takes precedence over session context;
|
|
10835
|
-
// explicit riff config.env takes precedence over both.
|
|
10836
|
-
|
|
11346
|
+
// explicit riff config.env takes precedence over both. The workflow
|
|
11347
|
+
// kill-switch is a host-resolved snapshot and is re-frozen AFTER this merge
|
|
11348
|
+
// (see below), so it is intentionally NOT set in sessionEnv here.
|
|
11349
|
+
const mergedEnv = { ...sessionEnv, ...sanitizePerBotEnv(cfg.env), ...riffCfg.env };
|
|
10837
11350
|
// The effective policy is a host-resolved snapshot, not a user-overridable
|
|
10838
11351
|
// backend env knob. Re-freeze it after config.env/per-bot env merge.
|
|
10839
11352
|
if (cfg.feedback)
|
|
10840
11353
|
mergedEnv.BOTMUX_FEEDBACK_POLICY = JSON.stringify(cfg.feedback);
|
|
10841
11354
|
else
|
|
10842
11355
|
delete mergedEnv.BOTMUX_FEEDBACK_POLICY;
|
|
11356
|
+
// The workflow kill-switch is likewise a host-resolved snapshot. Re-freeze it
|
|
11357
|
+
// AFTER the merge: unlike per-bot `env` (sanitizePerBotEnv strips the BOTMUX*
|
|
11358
|
+
// prefix), `riffCfg.env` merges LAST and is NOT sanitized, so a stale or
|
|
11359
|
+
// user-shaped backendConfig.env could otherwise flip the pane's
|
|
11360
|
+
// BOTMUX_WORKFLOW_ENABLED and desync the remote CLI-side gate from the
|
|
11361
|
+
// daemon's authoritative decision. The daemon-side gate is unaffected either
|
|
11362
|
+
// way; this keeps the in-pane defense-in-depth honest on the riff path too.
|
|
11363
|
+
mergedEnv.BOTMUX_WORKFLOW_ENABLED = isWorkflowFeatureEnabled() ? 'true' : 'false';
|
|
10843
11364
|
// Re-freeze the no-transport capability keys AFTER the merge: a stale or
|
|
10844
11365
|
// attacker-shaped backendConfig.env / per-bot env merges LAST and would
|
|
10845
11366
|
// otherwise override the frozen values, restoring send capability for a
|
|
@@ -10857,11 +11378,14 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
10857
11378
|
// merge so a stale or user-supplied backend env cannot impersonate another
|
|
10858
11379
|
// owner (or resurrect an owner on an ownerless session).
|
|
10859
11380
|
applySessionOwnerEnv(mergedEnv, cfg.ownerOpenId);
|
|
10860
|
-
|
|
11381
|
+
// `riffCfg` is `cfg.backendConfig` already narrowed once at the top of this
|
|
11382
|
+
// riff-only branch — same object, so master's owner re-freeze above is
|
|
11383
|
+
// unaffected; keeping the typed alias avoids re-widening to the shared union.
|
|
11384
|
+
riffBackendConfig = Object.assign({}, riffCfg, { env: mergedEnv, resumeParentTaskId: cfg.riffParentTaskId });
|
|
10861
11385
|
// 复用本地仓库+分支:多仓只认会话上的显式 stamp(仓库选择卡多选流按用户
|
|
10862
11386
|
// 顺序写入 cfg.riffRepoDirs,首仓=primary);否则仅对 workingDir 本身做单仓
|
|
10863
11387
|
// 推导——绝不扫描任意非 git 目录的子目录(home/仓库集合目录会乱带仓库)。
|
|
10864
|
-
if (!
|
|
11388
|
+
if (!riffCfg.repos || riffCfg.repos.length === 0) {
|
|
10865
11389
|
const derived = cfg.riffRepoDirs && cfg.riffRepoDirs.length > 0
|
|
10866
11390
|
? deriveRiffReposFromDirs(cfg.riffRepoDirs)
|
|
10867
11391
|
: (() => { const one = deriveRiffRepoFromWorkingDir(cfg.workingDir); return one ? { repos: [one.repo], warnings: one.warnings } : null; })();
|
|
@@ -10897,15 +11421,29 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
10897
11421
|
// BOTS_CONFIG. riff runs in its own REMOTE sandbox with no local CLI process —
|
|
10898
11422
|
// local confinement is meaningless there and must be bypassed on ALL
|
|
10899
11423
|
// platforms, or a sandbox-enabled bot bricks the moment it switches to riff.
|
|
10900
|
-
|
|
11424
|
+
// mojo only counts as remote when it provably runs nothing locally
|
|
11425
|
+
// (cloud on, localDaemon off) — otherwise the local sandbox must stay engaged.
|
|
11426
|
+
const remoteBackendFullyOffBox = !localSandboxApplies(effectiveBackendType, effectiveBackendType === 'mojo'
|
|
11427
|
+
? riffBackendConfig
|
|
11428
|
+
: undefined);
|
|
11429
|
+
const riffRemoteBackend = remoteBackendFullyOffBox;
|
|
10901
11430
|
if (riffRemoteBackend && (cfg.sandbox === true || cfg.readIsolation === true)) {
|
|
10902
|
-
log(
|
|
11431
|
+
log(`Sandbox flag set but backend is ${effectiveBackendType} (remote sandbox, no local process) — local sandbox bypassed`);
|
|
11432
|
+
}
|
|
11433
|
+
if (effectiveBackendType === 'mojo' && !remoteBackendFullyOffBox
|
|
11434
|
+
&& (cfg.sandbox === true || cfg.readIsolation === true)) {
|
|
11435
|
+
// Loud on purpose: this is the combination where a mojo bot DOES execute
|
|
11436
|
+
// locally, so the local sandbox stays on rather than being skipped.
|
|
11437
|
+
log('mojo runs tools locally (cloud not enabled, or localDaemon set) — keeping the local sandbox engaged');
|
|
10903
11438
|
}
|
|
10904
11439
|
const sandboxRequested = !riffRemoteBackend
|
|
10905
11440
|
&& (cfg.sandbox === true || cfg.readIsolation === true || sandboxEnabled());
|
|
10906
11441
|
const backendIsolationGate = backendSandboxCompatibilityError({
|
|
10907
11442
|
backendType: effectiveBackendType,
|
|
10908
11443
|
fileSandboxRequested: sandboxRequested,
|
|
11444
|
+
...(effectiveBackendType === 'mojo'
|
|
11445
|
+
? { mojoConfig: (riffBackendConfig ?? {}) }
|
|
11446
|
+
: {}),
|
|
10909
11447
|
// The unified sandbox request above already includes legacy readIsolation.
|
|
10910
11448
|
effectiveReadIsolationRequested: false,
|
|
10911
11449
|
});
|
|
@@ -11134,17 +11672,65 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11134
11672
|
// so the probe below sees no pane and we cold-spawn fresh isolated. A pane from
|
|
11135
11673
|
// this lifetime (suspend→resume) keeps its marker → reattaches normally (it is
|
|
11136
11674
|
// still the isolated process). This lets isolated bots use tmux/zellij/herdr.
|
|
11675
|
+
//
|
|
11676
|
+
// The MIRROR case is just as load-bearing: policy is now OFF (no sandbox, not
|
|
11677
|
+
// enrolled → appliedIsolationCapabilities empty), but a pane spawned by an OLDER
|
|
11678
|
+
// build under the previous FORCED no-transport isolation is still alive AND still
|
|
11679
|
+
// stamped. That pane runs confined against a policy we no longer want; a bare
|
|
11680
|
+
// `capabilities.length > 0` gate would skip the check entirely and warm-reattach
|
|
11681
|
+
// the still-isolated process, silently contradicting "read scope follows local
|
|
11682
|
+
// config" on resume/restart (the 2026-08 no-transport放宽 upgrade path). So we
|
|
11683
|
+
// also enter when a boot marker is present on disk for THIS session — then the
|
|
11684
|
+
// policy-off arm below (no expected capabilities) demands the marker be truly
|
|
11685
|
+
// absent to reattach, else kills + cold-spawns unconfined. Presence is checked
|
|
11686
|
+
// by the no-follow existence probe (a planted/tampered leaf that reads as null
|
|
11687
|
+
// still counts as present, so it cannot be used to force a silent reattach).
|
|
11137
11688
|
let persistentPaneOriginChannelId;
|
|
11138
|
-
|
|
11689
|
+
const stalePaneMarkerPath = isolationPaneMarkerPath(isolationRuntimeDataDir, cfg.sessionId);
|
|
11690
|
+
const policyOffTombstoneFilePath = policyOffTombstonePath(isolationRuntimeDataDir, cfg.sessionId);
|
|
11691
|
+
// no-transport (apiOnly bot OR HTTP virtual chat) is the ONLY session shape the
|
|
11692
|
+
// removed force-isolation rule ever confined; the policy-off migration arm is
|
|
11693
|
+
// scoped to it so an ordinary transport-enabled chat is never subjected to the
|
|
11694
|
+
// tombstone requirement (no false kills). Computed locally — the merge-scoped
|
|
11695
|
+
// `noTransport` above is out of scope here.
|
|
11696
|
+
const noTransportSession = cfg.apiOnly === true
|
|
11697
|
+
|| cfg.chatId?.startsWith('http_async_') === true
|
|
11698
|
+
|| cfg.chatId?.startsWith('http_wait_') === true;
|
|
11699
|
+
const isolationCapableBackend = effectiveBackendType === 'tmux';
|
|
11700
|
+
// Existence via no-follow probes so a planted/tampered leaf still counts as
|
|
11701
|
+
// present (→ triggers cleanup / conservative kill) and can never be used to
|
|
11702
|
+
// force a silent reattach.
|
|
11703
|
+
const stalePaneMarkerPresent = hostEntryExistsNoFollow(stalePaneMarkerPath);
|
|
11704
|
+
const policyOffTombstonePresent = hostEntryExistsNoFollow(policyOffTombstoneFilePath);
|
|
11705
|
+
// The guard must ENTER the state machine whenever it could have anything to
|
|
11706
|
+
// decide, WITHOUT depending on provenance already being present for the live-
|
|
11707
|
+
// pane arms — else a no-transport pane whose best-effort isolation marker write
|
|
11708
|
+
// was lost would (NEITHER file) skip the guard and warm-reattach still confined.
|
|
11709
|
+
// Enter for:
|
|
11710
|
+
// · any policy-ON spawn (capability check runs on every persistent backend,
|
|
11711
|
+
// incl. credential-only zellij/herdr/zmx), OR
|
|
11712
|
+
// · a policy-OFF no-transport tmux session (the file-sandbox migration scope), OR
|
|
11713
|
+
// · ANY session (incl. transport-enabled, any backend) that has stale
|
|
11714
|
+
// provenance on disk — so a dead pane's leftover marker/tombstone is cleared
|
|
11715
|
+
// before cold-spawn on EVERY backend. Otherwise a transport chat that turned
|
|
11716
|
+
// sandbox OFF leaves a matching marker that would later warm-reattach a fresh
|
|
11717
|
+
// UNisolated pane as "isolated" when sandbox is re-enabled.
|
|
11718
|
+
const persistentPaneGuardApplies = appliedIsolationCapabilities.length > 0
|
|
11719
|
+
|| (noTransportSession && isolationCapableBackend)
|
|
11720
|
+
|| stalePaneMarkerPresent || policyOffTombstonePresent;
|
|
11721
|
+
if (persistentSessionName && effectiveBackendType !== 'pty' && persistentPaneGuardApplies) {
|
|
11139
11722
|
const persistentTarget = selectedBackend.persistentBackendTarget;
|
|
11140
11723
|
// ZMX ownership is verified against the frozen PID, not just the name — a
|
|
11141
11724
|
// same-named session may belong to the user or to a newer generation.
|
|
11142
11725
|
const zmxOwnedProbe = effectiveBackendType === 'zmx'
|
|
11143
11726
|
? probeOwnedZmxSession(persistentSessionName, cfg.sessionId, resolvedZmxSessionPid)
|
|
11144
11727
|
: undefined;
|
|
11145
|
-
// ZMX ownership is label/PID-sensitive, so an inconclusive ZMX probe
|
|
11146
|
-
//
|
|
11147
|
-
//
|
|
11728
|
+
// ZMX ownership is label/PID-sensitive, so an inconclusive ZMX probe is not
|
|
11729
|
+
// proof of anything. Other persistent backends: their target probe returning
|
|
11730
|
+
// unknown is likewise not proof that a pane exists. Liveness is passed TRI-STATE
|
|
11731
|
+
// (paneProbe) into the state machine, which fail-closes on `unknown` for EVERY
|
|
11732
|
+
// backend (refuse-inconclusive-probe) — no longer only ZMX, and no longer
|
|
11733
|
+
// collapsed into "dead" (which would clear a still-confined pane's provenance).
|
|
11148
11734
|
const paneProbe = zmxOwnedProbe?.probe
|
|
11149
11735
|
?? (persistentTarget ? probePersistentBackendTarget(persistentTarget) : 'missing');
|
|
11150
11736
|
if (effectiveBackendType === 'zmx'
|
|
@@ -11153,80 +11739,122 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11153
11739
|
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: ` +
|
|
11154
11740
|
'ZMX session appeared after the frozen launch probe');
|
|
11155
11741
|
}
|
|
11156
|
-
if (effectiveBackendType === 'zmx' && paneProbe === 'unknown') {
|
|
11157
|
-
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: ` +
|
|
11158
|
-
`could not verify existing ${effectiveBackendType} pane`);
|
|
11159
|
-
}
|
|
11160
11742
|
const paneLive = paneProbe === 'exists';
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11168
|
-
|
|
11169
|
-
|
|
11170
|
-
?
|
|
11171
|
-
|
|
11172
|
-
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
|
|
11181
|
-
|
|
11182
|
-
|
|
11183
|
-
|
|
11184
|
-
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11743
|
+
const markerPath = stalePaneMarkerPath;
|
|
11744
|
+
const marker = paneLive ? readManagedOriginAuthorityFile(markerPath) : null;
|
|
11745
|
+
const originChannelPolicyExpected = !!managedOriginChannelPolicyDigest;
|
|
11746
|
+
// isolatedPaneReattachSafe only means anything under a policy-ON spawn; the
|
|
11747
|
+
// state machine consults it only in that arm.
|
|
11748
|
+
const isolationMarkerReattachSafe = appliedIsolationCapabilities.length > 0
|
|
11749
|
+
&& isolatedPaneReattachSafe(marker, {
|
|
11750
|
+
requiredCapabilities: appliedIsolationCapabilities,
|
|
11751
|
+
exactCapabilities: true,
|
|
11752
|
+
...(originChannelPolicyExpected ? {
|
|
11753
|
+
readIsolation: willReadIsolate,
|
|
11754
|
+
writeSandbox: willWriteSandbox,
|
|
11755
|
+
requireOriginChannel: true,
|
|
11756
|
+
policyDigest: managedOriginChannelPolicyDigest,
|
|
11757
|
+
} : {}),
|
|
11758
|
+
});
|
|
11759
|
+
// Tombstone authorizes a policy-off warm reattach ONLY when it passes a SECURE
|
|
11760
|
+
// read (real 0600 regular file, right owner) + schema/version validation — a
|
|
11761
|
+
// bare lstat "present" (empty / dir / symlink / garbage) must NOT authorize.
|
|
11762
|
+
// Presence (above) still drives cleanup; validity drives authorization.
|
|
11763
|
+
const policyOffTombstoneIsValid = paneLive && policyOffTombstonePresent
|
|
11764
|
+
&& policyOffTombstoneValid(readManagedOriginAuthorityFile(policyOffTombstoneFilePath));
|
|
11765
|
+
// PENDING provenance: a present marker OR tombstone whose secure-read body is a
|
|
11766
|
+
// `state:'pending'` record. This is a generation whose fresh-attribution never
|
|
11767
|
+
// completed (crash between pending-write and commit, or an uncommitted
|
|
11768
|
+
// late-flip/collision). It DOMINATES the state machine (all backends, both
|
|
11769
|
+
// policy directions) — see evaluatePersistentPaneMigration. Secure-read (not
|
|
11770
|
+
// lstat) because only a real 0600 file we wrote can be a trusted pending
|
|
11771
|
+
// record; a planted/garbage leaf reads as null → not pending → falls through
|
|
11772
|
+
// to the normal presence-but-invalid handling (still conservative).
|
|
11773
|
+
const pendingProvenancePresent = (stalePaneMarkerPresent
|
|
11774
|
+
&& provenancePendingNonce(readManagedOriginAuthorityFile(stalePaneMarkerPath)) !== null)
|
|
11775
|
+
|| (policyOffTombstonePresent
|
|
11776
|
+
&& provenancePendingNonce(readManagedOriginAuthorityFile(policyOffTombstoneFilePath)) !== null);
|
|
11777
|
+
const migration = evaluatePersistentPaneMigration({
|
|
11778
|
+
appliedIsolationCapabilities,
|
|
11779
|
+
isolationCapableBackend,
|
|
11780
|
+
noTransport: noTransportSession,
|
|
11781
|
+
isolationMarkerPresent: stalePaneMarkerPresent,
|
|
11782
|
+
policyOffTombstonePresent,
|
|
11783
|
+
policyOffTombstoneValid: policyOffTombstoneIsValid,
|
|
11784
|
+
paneProbe,
|
|
11785
|
+
pendingProvenancePresent,
|
|
11786
|
+
isolationMarkerReattachSafe,
|
|
11787
|
+
});
|
|
11788
|
+
// Verified removal of a provenance file: unlink then confirm it is truly gone
|
|
11789
|
+
// (no-follow). A leaf we cannot remove (directory / planted / perm) must FAIL
|
|
11790
|
+
// CLOSED — never fall through to publish a new generation, or a later restart
|
|
11791
|
+
// re-reads the stale proof and mis-kills the fresh pane in a loop.
|
|
11792
|
+
const removeProvenanceOrThrow = (path, label) => {
|
|
11793
|
+
try {
|
|
11794
|
+
unlinkSync(path);
|
|
11189
11795
|
}
|
|
11190
|
-
|
|
11191
|
-
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11796
|
+
catch { /* may already be absent — verified below */ }
|
|
11797
|
+
if (hostEntryExistsNoFollow(path)) {
|
|
11798
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: `
|
|
11799
|
+
+ `could not remove stale ${label} at ${path}`);
|
|
11800
|
+
}
|
|
11801
|
+
};
|
|
11802
|
+
// Capture the stale name/target BEFORE any re-selection below (reselect
|
|
11803
|
+
// reassigns persistentSessionName and widens it back to string | undefined).
|
|
11804
|
+
const staleSessionName = persistentSessionName;
|
|
11805
|
+
const stalePersistentTarget = selectedBackend.persistentBackendTarget;
|
|
11806
|
+
const migrationEffects = {
|
|
11807
|
+
killStalePane: () => {
|
|
11200
11808
|
try {
|
|
11201
11809
|
// ZMX keeps its own call here rather than going through the target
|
|
11202
11810
|
// helper: only this path holds the frozen PID, which makes the
|
|
11203
11811
|
// ownership check stricter than the name+label check.
|
|
11204
11812
|
if (effectiveBackendType === 'zmx') {
|
|
11205
|
-
ZmxBackend.killManagedSession(
|
|
11813
|
+
ZmxBackend.killManagedSession(staleSessionName, cfg.sessionId, resolvedZmxSessionPid);
|
|
11814
|
+
}
|
|
11815
|
+
else if (stalePersistentTarget) {
|
|
11816
|
+
killPersistentBackendTarget(stalePersistentTarget, cfg.sessionId);
|
|
11206
11817
|
}
|
|
11207
11818
|
else {
|
|
11208
|
-
|
|
11209
|
-
killPersistentBackendTarget(stalePersistentTarget, cfg.sessionId);
|
|
11210
|
-
else
|
|
11211
|
-
killPersistentSession(effectiveBackendType, persistentSessionName, cfg.sessionId);
|
|
11819
|
+
killPersistentSession(effectiveBackendType, staleSessionName, cfg.sessionId);
|
|
11212
11820
|
}
|
|
11213
11821
|
}
|
|
11214
11822
|
catch (e) {
|
|
11215
11823
|
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: could not kill stale persistent pane (${e.message})`);
|
|
11216
11824
|
}
|
|
11825
|
+
},
|
|
11826
|
+
confirmPaneGone: () => {
|
|
11217
11827
|
const postKillProbe = effectiveBackendType === 'zmx'
|
|
11218
11828
|
? probeOwnedZmxSession(staleSessionName, cfg.sessionId).probe
|
|
11219
11829
|
: (stalePersistentTarget
|
|
11220
11830
|
? probePersistentBackendTarget(stalePersistentTarget)
|
|
11221
11831
|
: probePersistentSession(effectiveBackendType, staleSessionName));
|
|
11222
|
-
|
|
11223
|
-
|
|
11224
|
-
|
|
11832
|
+
// Migration teardown fail-closes on ANY non-`missing` post-kill probe for
|
|
11833
|
+
// EVERY backend — not just ZMX. `exists` (kill didn't take) and `unknown`
|
|
11834
|
+
// (kill unconfirmed: tmux swallows kill errors incl. timeout, zellij does
|
|
11835
|
+
// not check its spawnSync exit) both mean "the confined pane may still be
|
|
11836
|
+
// alive", so publishing a new generation around it would silently keep the
|
|
11837
|
+
// old confinement. Only an authoritative `missing` confirms termination.
|
|
11838
|
+
// (This is STRICTER than the shared shouldRejectPersistentPostKillProbe,
|
|
11839
|
+
// which the separate mcp-gateway gate still uses with its own semantics.)
|
|
11840
|
+
if (postKillProbe !== 'missing') {
|
|
11841
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: `
|
|
11842
|
+
+ `could not confirm stale ${effectiveBackendType} pane termination `
|
|
11843
|
+
+ `(post-kill probe: ${postKillProbe})`);
|
|
11225
11844
|
}
|
|
11226
11845
|
if (effectiveBackendType === 'zmx') {
|
|
11227
11846
|
resolvedZmxSessionProbe = postKillProbe;
|
|
11228
11847
|
resolvedZmxSessionPid = undefined;
|
|
11229
11848
|
}
|
|
11849
|
+
},
|
|
11850
|
+
clearProvenanceVerified: () => {
|
|
11851
|
+
// Clear BOTH files (verified). Order-independent — both must end absent.
|
|
11852
|
+
if (stalePaneMarkerPresent)
|
|
11853
|
+
removeProvenanceOrThrow(stalePaneMarkerPath, 'isolation marker');
|
|
11854
|
+
if (policyOffTombstonePresent)
|
|
11855
|
+
removeProvenanceOrThrow(policyOffTombstoneFilePath, 'policy-off tombstone');
|
|
11856
|
+
},
|
|
11857
|
+
reselectBackend: () => {
|
|
11230
11858
|
// ZMX backend selection consumes the frozen probe. Refresh it before
|
|
11231
11859
|
// re-selecting or the replacement keeps isReattach=true for the pane
|
|
11232
11860
|
// that this gate just proved was removed.
|
|
@@ -11237,8 +11865,40 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11237
11865
|
backend = selectedBackend.backend;
|
|
11238
11866
|
cliLifetimeNonce++;
|
|
11239
11867
|
persistentSessionName = selectedBackend.persistentSessionName;
|
|
11868
|
+
},
|
|
11869
|
+
refuseInconclusiveProbe: () => {
|
|
11870
|
+
// The liveness probe was `unknown` where acting would be unsafe (the pane
|
|
11871
|
+
// may still be alive AND still confined under an obsolete policy). No
|
|
11872
|
+
// provenance is touched, no reselect — fail closed and let the next launch
|
|
11873
|
+
// re-probe once the backend is answering again.
|
|
11874
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: `
|
|
11875
|
+
+ `could not verify existing ${effectiveBackendType} pane `
|
|
11876
|
+
+ `(liveness probe: ${paneProbe})`);
|
|
11877
|
+
},
|
|
11878
|
+
};
|
|
11879
|
+
if (migration.action === 'reattach') {
|
|
11880
|
+
if (originChannelPolicyExpected) {
|
|
11881
|
+
persistentPaneOriginChannelId = isolatedPaneOriginChannel(marker);
|
|
11240
11882
|
}
|
|
11883
|
+
// Pane matches the current policy (isolated pane stamped under it, or a
|
|
11884
|
+
// no-transport pane with a VALIDATED policy-off tombstone) → still valid on
|
|
11885
|
+
// the running process across daemon restarts; warm reattach preserves
|
|
11886
|
+
// resume/context + tmux idle-suspend.
|
|
11887
|
+
log(`[read-isolation] reattaching persistent pane under current policy (${cfg.sessionId})`);
|
|
11241
11888
|
}
|
|
11889
|
+
else if (migration.action === 'clear-stale-then-cold-spawn') {
|
|
11890
|
+
log(`[read-isolation] clearing stale provenance for dead pane before cold-spawn (${cfg.sessionId})`);
|
|
11891
|
+
}
|
|
11892
|
+
else if (migration.action === 'kill-then-cold-spawn') {
|
|
11893
|
+
log(`[read-isolation] persistent pane provenance mismatch for ${cfg.sessionId} — killing + cold-spawning with current policy`);
|
|
11894
|
+
}
|
|
11895
|
+
else if (migration.action === 'refuse-inconclusive-probe') {
|
|
11896
|
+
log(`[read-isolation] inconclusive liveness probe (${paneProbe}) for ${cfg.sessionId} — refusing to start rather than clear/cold-spawn around a possibly-live confined pane`);
|
|
11897
|
+
}
|
|
11898
|
+
// Ordered, fail-closed side effects (kill → confirm → clear → reselect) live
|
|
11899
|
+
// in executePersistentPaneMigration so the ordering + stop-on-failure
|
|
11900
|
+
// guarantees are unit-testable with injected mocks.
|
|
11901
|
+
executePersistentPaneMigration(migration, migrationEffects);
|
|
11242
11902
|
}
|
|
11243
11903
|
readIsolationOriginChannelId = managedOriginChannelRequired
|
|
11244
11904
|
? (persistentPaneOriginChannelId ?? randomBytes(32).toString('hex'))
|
|
@@ -11391,6 +12051,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11391
12051
|
bareShellChecked = false;
|
|
11392
12052
|
bareShellCheckInProgress = false;
|
|
11393
12053
|
// ── Resume pre-flight check + two-tier fallback ──────────────────────────
|
|
12054
|
+
// Tier 0 (adapter capability): adapters whose buildArgs can only resume a
|
|
12055
|
+
// PRECISE cliSessionId (cursor/copilot/kimi — no --continue/latest fallback,
|
|
12056
|
+
// which would risk resuming a SIBLING session's conversation) start FRESH
|
|
12057
|
+
// when no id was persisted. Demote here so the user-visible fresh notice
|
|
12058
|
+
// fires — leaving effectiveResume=true would have the adapter silently
|
|
12059
|
+
// launch a blank session while the closed card / resume receipt still claim
|
|
12060
|
+
// history was restored.
|
|
11394
12061
|
// Tier 1 (adapter probe): adapter.checkResumeTargetExists returns false
|
|
11395
12062
|
// → skip --resume, spawn FRESH.
|
|
11396
12063
|
// Tier 2 (restart count): 2nd consecutive in-worker restart → force FRESH,
|
|
@@ -11435,8 +12102,15 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11435
12102
|
? resolveHermesStateDbPath({ ...process.env, ...sanitizePerBotEnv(cfg.env), BOTMUX_SESSION_ID: cfg.sessionId }, { botmuxSessionProfile: basename(cfg.cliPathOverride ?? '') === 'hermes-botmux-session' })
|
|
11436
12103
|
: undefined;
|
|
11437
12104
|
const tier2ForceFresh = effectiveResume && consecutiveInWorkerRestarts >= 2;
|
|
12105
|
+
// Tier 0: see block comment above. The adapter itself drops --resume when
|
|
12106
|
+
// the id is missing (its buildArgs starts fresh); demoting HERE keeps
|
|
12107
|
+
// effectiveResume honest so the fresh-demotion notice below fires.
|
|
12108
|
+
const missingExactResumeId = effectiveResume
|
|
12109
|
+
&& !willReattachPersistent
|
|
12110
|
+
&& !effectiveCliSessionId
|
|
12111
|
+
&& cliAdapter.resumeRequiresCliSessionId === true;
|
|
11438
12112
|
let tier1ProbeFalse = false;
|
|
11439
|
-
if (effectiveResume && !tier2ForceFresh && !willReattachPersistent) {
|
|
12113
|
+
if (effectiveResume && !tier2ForceFresh && !willReattachPersistent && !missingExactResumeId) {
|
|
11440
12114
|
const probe = cliAdapter.checkResumeTargetExists?.({
|
|
11441
12115
|
sessionId: effectiveAdapterSessionId,
|
|
11442
12116
|
cliSessionId: effectiveCliSessionId,
|
|
@@ -11447,11 +12121,13 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11447
12121
|
if (probe === false)
|
|
11448
12122
|
tier1ProbeFalse = true;
|
|
11449
12123
|
}
|
|
11450
|
-
const fallBackToFresh = effectiveResume && !willReattachPersistent && (tier1ProbeFalse || tier2ForceFresh);
|
|
12124
|
+
const fallBackToFresh = effectiveResume && !willReattachPersistent && (tier1ProbeFalse || tier2ForceFresh || missingExactResumeId);
|
|
11451
12125
|
if (fallBackToFresh) {
|
|
11452
12126
|
const reason = tier2ForceFresh
|
|
11453
12127
|
? `consecutive restart x${consecutiveInWorkerRestarts} — 2nd failed resume attempt`
|
|
11454
|
-
:
|
|
12128
|
+
: missingExactResumeId
|
|
12129
|
+
? 'no persisted CLI session id — this CLI can only resume a precise session (no --continue fallback)'
|
|
12130
|
+
: 'adapter confirmed resume target does not exist on disk';
|
|
11455
12131
|
log(`Resume fallback: dropping --resume (${reason}) → fresh session ${cfg.sessionId}`);
|
|
11456
12132
|
effectiveResume = false;
|
|
11457
12133
|
effectiveCliSessionId = undefined;
|
|
@@ -11571,6 +12247,14 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11571
12247
|
injectsReadyHook: cliAdapter.injectsReadyHook === true,
|
|
11572
12248
|
reliableTurnTerminal: cliAdapter.reliableTurnTerminal === true,
|
|
11573
12249
|
});
|
|
12250
|
+
// Fresh evidence window for the argv first-turn start gate (Pi): the new
|
|
12251
|
+
// generation must not inherit a previous spawn's latch or fail-open timer.
|
|
12252
|
+
stopSpawnArgvTurnStartFailOpen();
|
|
12253
|
+
spawnArgvTurnStartEvidenceSeen = false;
|
|
12254
|
+
spawnArgvTurnStartBusyScanTail = '';
|
|
12255
|
+
spawnArgvTurnStartEvidenceDeadlineMs = spawnArgvNeedsWorkingSeed
|
|
12256
|
+
? Date.now() + SPAWN_ARGV_TURN_START_EVIDENCE_GRACE_MS
|
|
12257
|
+
: 0;
|
|
11574
12258
|
if (deferInitialPrompt && preparedDeferredInput) {
|
|
11575
12259
|
piInitialPromptAdditionalArgs = [...(preparedDeferredInput.additionalArgs ?? [])];
|
|
11576
12260
|
piInitialPromptEnv = { ...(preparedDeferredInput.env ?? {}) };
|
|
@@ -11624,6 +12308,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11624
12308
|
larkAppId: cfg.larkAppId,
|
|
11625
12309
|
locale: cfg.locale,
|
|
11626
12310
|
model: ttadkGateway ? undefined : cfg.model,
|
|
12311
|
+
// dsh runner only; other adapters ignore the field.
|
|
12312
|
+
turnTimeoutMs: cfg.turnTimeoutMs,
|
|
11627
12313
|
reasoningEffort: cfg.reasoningEffort,
|
|
11628
12314
|
disableCliBypass: cfg.disableCliBypass === true,
|
|
11629
12315
|
// Codex-family hook-trust bypass: global toggle (default ON) so a headless
|
|
@@ -11821,6 +12507,10 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
11821
12507
|
const chatBotDiscovery = resolveChatBotDiscoveryConfig();
|
|
11822
12508
|
childEnv.BOTMUX_LARK_LIST_BOTS_API_ENABLED = chatBotDiscovery.listBotsApiEnabled ? 'true' : 'false';
|
|
11823
12509
|
childEnv.BOTMUX_LARK_LIST_BOTS_API_TIMEOUT_MS = String(chatBotDiscovery.listBotsApiTimeoutMs);
|
|
12510
|
+
// Inject the resolved workflow kill-switch so a pane's `botmux workflow …`
|
|
12511
|
+
// subcommand agrees with the daemon that spawned it, independent of stale
|
|
12512
|
+
// rcfile/tmux env (mirrors the chatBotDiscovery injection above).
|
|
12513
|
+
childEnv.BOTMUX_WORKFLOW_ENABLED = isWorkflowFeatureEnabled() ? 'true' : 'false';
|
|
11824
12514
|
if (cliAdapter.injectsReadyHook)
|
|
11825
12515
|
childEnv.BOTMUX_READY_COMMAND = sessionReadyHookCommand();
|
|
11826
12516
|
// Initial value only; long-lived panes get the latest turn via the JSON pid marker.
|
|
@@ -12405,23 +13095,69 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12405
13095
|
log(`Sandbox ON (${cfg.cliId}, fs-policy ${policy.rules.length} rules): outbox=${sbx.outbox}`);
|
|
12406
13096
|
}
|
|
12407
13097
|
}
|
|
12408
|
-
|
|
12409
|
-
// daemon's boot id so a later reattach can be trusted (see the stale-pane
|
|
12410
|
-
// guard above). pty needs no marker (never reattached).
|
|
13098
|
+
let pendingProvenanceCommit = null;
|
|
12411
13099
|
if (appliedIsolationCapabilities.length > 0 && persistentSessionName && !willReattachPersistent) {
|
|
13100
|
+
// Policy-ON isolation marker. The PENDING write is a spawn-time ADMISSION
|
|
13101
|
+
// PRECONDITION, NOT best-effort: if we cannot durably record the pending proof
|
|
13102
|
+
// and then backend.spawn() dynamically reattaches a late-arriving same-named
|
|
13103
|
+
// pane (zellij/TmuxBackend flip), the whole commit/teardown block below —
|
|
13104
|
+
// gated on pendingProvenanceCommit — would be SKIPPED, so the late-flip
|
|
13105
|
+
// teardown never runs and this launch keeps running attached to an
|
|
13106
|
+
// unattributed generation. "No committed proof → next launch cold-spawns" only
|
|
13107
|
+
// protects the NEXT launch, not THIS one. So a write failure must FAIL CLOSED
|
|
13108
|
+
// here (throw before spawn), exactly like the policy-off arm.
|
|
12412
13109
|
try {
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
12416
|
-
|
|
12417
|
-
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
13110
|
+
mkdirSync(join(isolationRuntimeDataDir, 'read-isolation'), { recursive: true });
|
|
13111
|
+
const nonce = randomBytes(32).toString('hex');
|
|
13112
|
+
const markerPath = isolationPaneMarkerPath(isolationRuntimeDataDir, cfg.sessionId);
|
|
13113
|
+
replaceManagedOriginCapabilityFile(markerPath, provenancePendingContent(nonce));
|
|
13114
|
+
pendingProvenanceCommit = {
|
|
13115
|
+
path: markerPath,
|
|
13116
|
+
nonce,
|
|
13117
|
+
committedContent: isolationPaneMarkerContent(cfg.daemonBootId ?? '', appliedIsolationCapabilities, managedOriginChannelPolicyDigest
|
|
13118
|
+
? {
|
|
13119
|
+
originChannelId: persistentPaneOriginChannelId,
|
|
13120
|
+
readIsolation: willReadIsolate,
|
|
13121
|
+
writeSandbox: willWriteSandbox,
|
|
13122
|
+
policyDigest: managedOriginChannelPolicyDigest,
|
|
13123
|
+
}
|
|
13124
|
+
: undefined),
|
|
13125
|
+
// A committed policy-ON generation must not carry a stale policy-off
|
|
13126
|
+
// tombstone (a later flip to policy-off could read it as a no-sandbox gen).
|
|
13127
|
+
clearSiblingPath: policyOffTombstonePath(isolationRuntimeDataDir, cfg.sessionId),
|
|
13128
|
+
label: 'isolation marker',
|
|
13129
|
+
};
|
|
13130
|
+
}
|
|
13131
|
+
catch (e) {
|
|
13132
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: `
|
|
13133
|
+
+ `could not record pending isolation-marker generation proof (${e.message})`);
|
|
13134
|
+
}
|
|
13135
|
+
}
|
|
13136
|
+
else if (appliedIsolationCapabilities.length === 0 && persistentSessionName
|
|
13137
|
+
&& !willReattachPersistent && noTransportSession && isolationCapableBackend) {
|
|
13138
|
+
// Policy-OFF generation proof (tmux-only: isolationCapableBackend === tmux).
|
|
13139
|
+
// NOT best-effort: if we cannot durably record the PENDING proof we must fail
|
|
13140
|
+
// closed rather than spawn a pane we can never prove. The committed tombstone
|
|
13141
|
+
// is written post-spawn only on a confirmed fresh generation.
|
|
13142
|
+
try {
|
|
13143
|
+
mkdirSync(join(isolationRuntimeDataDir, 'read-isolation'), { recursive: true });
|
|
13144
|
+
const nonce = randomBytes(32).toString('hex');
|
|
13145
|
+
const tombstonePath = policyOffTombstonePath(isolationRuntimeDataDir, cfg.sessionId);
|
|
13146
|
+
replaceManagedOriginCapabilityFile(tombstonePath, provenancePendingContent(nonce));
|
|
13147
|
+
pendingProvenanceCommit = {
|
|
13148
|
+
path: tombstonePath,
|
|
13149
|
+
nonce,
|
|
13150
|
+
committedContent: policyOffTombstoneContent(cfg.daemonBootId ?? ''),
|
|
13151
|
+
// A committed policy-off generation must not be shadowed by a stale
|
|
13152
|
+
// isolation marker (which DOMINATES the tombstone in the guard).
|
|
13153
|
+
clearSiblingPath: isolationPaneMarkerPath(isolationRuntimeDataDir, cfg.sessionId),
|
|
13154
|
+
label: 'policy-off tombstone',
|
|
13155
|
+
};
|
|
13156
|
+
}
|
|
13157
|
+
catch (e) {
|
|
13158
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: `
|
|
13159
|
+
+ `could not record pending policy-off generation proof (${e.message})`);
|
|
12423
13160
|
}
|
|
12424
|
-
catch { /* non-fatal: worst case a same-lifetime reattach cold-spawns instead */ }
|
|
12425
13161
|
}
|
|
12426
13162
|
// 通用启动前缀(wrapperCli):把启动命令重写成 `<wrapperCli> <CLI 参数>`(首 token 当
|
|
12427
13163
|
// bin 走 PATH 解析),无需 wrapper 脚本、跨系统。aiden x claude 形态会剥掉 aiden 拒收的
|
|
@@ -12434,12 +13170,53 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12434
13170
|
// ever (re)set it. Harmless for non-cjadk CLIs (they don't read it), but this
|
|
12435
13171
|
// keeps the behaviour intentional rather than ambient. (Codex review note.)
|
|
12436
13172
|
delete childEnv.CJADK_INTERACTIVE;
|
|
13173
|
+
// mojo: hand the generic extra args (CLI_EXTRA_ARGS) to the backend through the
|
|
13174
|
+
// config instead of through spawn args, and keep them OUT of the wrapper prefix.
|
|
13175
|
+
//
|
|
13176
|
+
// buildWrappedLaunch folds whatever it is given into the prefix, i.e. BEFORE the
|
|
13177
|
+
// per-turn flags the backend appends. With last-value-wins parsing that
|
|
13178
|
+
// silently inverted precedence: `CLI_EXTRA_ARGS=--idle-timeout 77` plus
|
|
13179
|
+
// `mojo.idleTimeoutSec=12` produced `--idle-timeout 77 … --idle-timeout 12` and
|
|
13180
|
+
// 12 won, contradicting the documented "extra args can override built-ins".
|
|
13181
|
+
// Carrying them separately lets the backend append them AFTER its own flags on
|
|
13182
|
+
// every turn, which is the same order the no-wrapper path already produced.
|
|
13183
|
+
if (effectiveBackendType === 'mojo' && spawnArgs.length > 0) {
|
|
13184
|
+
// Refuse platform-owned flags. Extra args are appended AFTER the backend's
|
|
13185
|
+
// own flags so an operator can override behaviour knobs — but with
|
|
13186
|
+
// last-value-wins that also let CLI_EXTRA_ARGS override the control-plane
|
|
13187
|
+
// identity frozen on the session (`--workspace-id`, `--agent-id`, `--cloud`),
|
|
13188
|
+
// the approval bypass (`--yolo`) or the resume lineage (`-r`). Fail closed
|
|
13189
|
+
// rather than silently dropping them: a dropped flag reads as applied.
|
|
13190
|
+
const reserved = findReservedMojoCliFlags(spawnArgs);
|
|
13191
|
+
if (reserved.length > 0) {
|
|
13192
|
+
throw new Error(`CLI_EXTRA_ARGS may not set ${reserved.join(' / ')} for a mojo bot — `
|
|
13193
|
+
+ 'these are platform-owned (control plane frozen per session, approval '
|
|
13194
|
+
+ 'bypass, resume lineage). Configure them through the bot settings instead.');
|
|
13195
|
+
}
|
|
13196
|
+
riffBackendConfig.extraCliArgs = [...spawnArgs];
|
|
13197
|
+
log(`mojo extra CLI args deferred to per-turn append: ${spawnArgs.join(' ')}`);
|
|
13198
|
+
spawnArgs = [];
|
|
13199
|
+
}
|
|
12437
13200
|
if (cfg.wrapperCli && cfg.wrapperCli.trim()) {
|
|
12438
13201
|
if (sandboxRequested) {
|
|
12439
13202
|
log(`wrapperCli="${cfg.wrapperCli}" ignored: file sandbox enabled and takes precedence (cannot combine launch prefix with the sandbox wrapper)`);
|
|
12440
13203
|
}
|
|
12441
13204
|
else {
|
|
12442
|
-
|
|
13205
|
+
// Resolve wrapper binaries against the EFFECTIVE child PATH — the exact env
|
|
13206
|
+
// the child will really run with — not the daemon's own. `locateOnPath`
|
|
13207
|
+
// reads this process's env, so an ambient install shadowed a per-bot one.
|
|
13208
|
+
// The layering MUST come from the shared buildEffectiveChildEnv: this site
|
|
13209
|
+
// previously merged only childEnv + bot env and omitted the
|
|
13210
|
+
// highest-precedence `mojo.env`, so a wrapper pinned via `mojo.env.PATH`
|
|
13211
|
+
// lost to a same-named binary on the bot-level PATH.
|
|
13212
|
+
const effectiveChildEnv = buildEffectiveChildEnv({
|
|
13213
|
+
base: childEnv,
|
|
13214
|
+
botEnv: perBotInjectEnv,
|
|
13215
|
+
mojoEnv: effectiveBackendType === 'mojo'
|
|
13216
|
+
? riffBackendConfig?.env
|
|
13217
|
+
: undefined,
|
|
13218
|
+
});
|
|
13219
|
+
const launch = buildWrappedLaunch(cfg.wrapperCli, spawnArgs, (b) => locateOnEffectiveChildPath(b, effectiveChildEnv) ?? b, {
|
|
12443
13220
|
ttadkModel: cfg.model,
|
|
12444
13221
|
});
|
|
12445
13222
|
if (launch.bin) {
|
|
@@ -12682,6 +13459,123 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
12682
13459
|
}
|
|
12683
13460
|
const actuallyReattachedPersistent = 'isReattach' in backend
|
|
12684
13461
|
&& backend.isReattach === true;
|
|
13462
|
+
// ── Generational-race commit/teardown for the read-isolation provenance proof ──
|
|
13463
|
+
// We wrote a PENDING proof before spawn (pendingProvenanceCommit). Now that spawn
|
|
13464
|
+
// has returned we know whether a FRESH generation was actually established.
|
|
13465
|
+
if (pendingProvenanceCommit) {
|
|
13466
|
+
const commit = pendingProvenanceCommit;
|
|
13467
|
+
// Verified removal of a proof file: unlink then confirm truly gone (no-follow).
|
|
13468
|
+
// A leaf we cannot remove must FAIL CLOSED — never leave an ambiguous proof.
|
|
13469
|
+
const removeProofOrThrow = (path, label) => {
|
|
13470
|
+
try {
|
|
13471
|
+
unlinkSync(path);
|
|
13472
|
+
}
|
|
13473
|
+
catch { /* may already be absent — verified below */ }
|
|
13474
|
+
if (hostEntryExistsNoFollow(path)) {
|
|
13475
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: `
|
|
13476
|
+
+ `could not remove ${label} at ${path}`);
|
|
13477
|
+
}
|
|
13478
|
+
};
|
|
13479
|
+
// Teardown the exact just-launched target, confirm authoritative `missing`,
|
|
13480
|
+
// and only THEN keep/clear the pending proof per the tri-state result. On a
|
|
13481
|
+
// non-missing (exists/unknown) post-kill probe we KEEP the pending proof and
|
|
13482
|
+
// refuse — never erase evidence of a possibly-live pane.
|
|
13483
|
+
//
|
|
13484
|
+
// CRITICAL: kill the EXACT backend target, NOT the session name. An isolated /
|
|
13485
|
+
// MCP herdr task lives as an agent on the SHARED host session `botmux`
|
|
13486
|
+
// (target = {sessionName:'botmux', agentName:<this topic>}); a name-only
|
|
13487
|
+
// killPersistentSession('herdr','botmux') would tear down the whole shared host
|
|
13488
|
+
// (every bot/topic agent). Mirror the migration effects' killStalePane /
|
|
13489
|
+
// confirmPaneGone: target helper for herdr's agent scope, frozen-PID path for
|
|
13490
|
+
// ZMX identity, name only as the last-resort fallback when no target exists.
|
|
13491
|
+
const teardownTarget = selectedBackend.persistentBackendTarget;
|
|
13492
|
+
const tearDownAndRefuse = (why) => {
|
|
13493
|
+
if (persistentSessionName) {
|
|
13494
|
+
// Pure policy (behaviorally tested): herdr's shared-host agent MUST be
|
|
13495
|
+
// killed via its target, never by the 'botmux' session name.
|
|
13496
|
+
const killKind = persistentTeardownKillKind({
|
|
13497
|
+
backendType: effectiveBackendType,
|
|
13498
|
+
hasBackendTarget: !!teardownTarget,
|
|
13499
|
+
});
|
|
13500
|
+
try {
|
|
13501
|
+
if (killKind === 'zmx') {
|
|
13502
|
+
ZmxBackend.killManagedSession(persistentSessionName, cfg.sessionId, resolvedZmxSessionPid);
|
|
13503
|
+
}
|
|
13504
|
+
else if (killKind === 'target') {
|
|
13505
|
+
killPersistentBackendTarget(teardownTarget, cfg.sessionId);
|
|
13506
|
+
}
|
|
13507
|
+
else {
|
|
13508
|
+
killPersistentSession(effectiveBackendType, persistentSessionName, cfg.sessionId);
|
|
13509
|
+
}
|
|
13510
|
+
}
|
|
13511
|
+
catch (killErr) {
|
|
13512
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: ${why}; `
|
|
13513
|
+
+ `could not kill the exact target (${killErr?.message ?? killErr}) — pending proof retained`);
|
|
13514
|
+
}
|
|
13515
|
+
const postKill = killKind === 'zmx'
|
|
13516
|
+
? probeOwnedZmxSession(persistentSessionName, cfg.sessionId).probe
|
|
13517
|
+
: (killKind === 'target'
|
|
13518
|
+
? probePersistentBackendTarget(teardownTarget)
|
|
13519
|
+
: probePersistentSession(effectiveBackendType, persistentSessionName));
|
|
13520
|
+
if (postKill !== 'missing') {
|
|
13521
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: ${why}; `
|
|
13522
|
+
+ `post-kill probe ${postKill} (not missing) — pending proof retained`);
|
|
13523
|
+
}
|
|
13524
|
+
}
|
|
13525
|
+
throw new Error(`[read-isolation] refusing to start session ${cfg.sessionId}: ${why}`);
|
|
13526
|
+
};
|
|
13527
|
+
// Condition #2: a predicted-fresh launch that backend.spawn() dynamically
|
|
13528
|
+
// flipped into a reattach (a same-named pane arrived before the in-spawn probe)
|
|
13529
|
+
// must NOT keep running — the live pane is a foreign/unknown generation. Tear
|
|
13530
|
+
// it down and refuse rather than "don't commit but keep running".
|
|
13531
|
+
if (actuallyReattachedPersistent) {
|
|
13532
|
+
tearDownAndRefuse('predicted-fresh persistent launch dynamically reattached a late-arriving pane');
|
|
13533
|
+
}
|
|
13534
|
+
else if (effectiveBackendType === 'zellij') {
|
|
13535
|
+
// Option B: zellij's session is created ASYNCHRONOUSLY inside the pty child,
|
|
13536
|
+
// so a synchronous `!isReattach` here does NOT prove a fresh generation
|
|
13537
|
+
// attributable to THIS launch (a late collision can still occur after the
|
|
13538
|
+
// last in-spawn hasSession probe). We have no synchronous attributable
|
|
13539
|
+
// signal, so we DELIBERATELY do not commit: the pending proof is left on
|
|
13540
|
+
// disk, and this isolation-capable zellij pane will always cold-spawn on the
|
|
13541
|
+
// next daemon restart/suspend-resume (no warm-reattach). The attributable-ack
|
|
13542
|
+
// protocol that would restore zellij warm-reattach is a separate follow-up.
|
|
13543
|
+
log(`[read-isolation] zellij persistent pane ${cfg.sessionId}: leaving PENDING proof (isolation-capable zellij does not warm-reattach; cold-spawn on next launch)`);
|
|
13544
|
+
}
|
|
13545
|
+
else {
|
|
13546
|
+
// tmux(-pipe) / herdr / zmx: attributable-fresh at the synchronous spawn
|
|
13547
|
+
// return — TmuxPipe throws on new-session collision, herdr/zmx throw on a
|
|
13548
|
+
// name collision / verify their own bootstrap+launchPid handshake. So a
|
|
13549
|
+
// returned spawn + !isReattach here IS a fresh generation created by THIS
|
|
13550
|
+
// launch. COMMIT with compare-before-replace + generation fence.
|
|
13551
|
+
try {
|
|
13552
|
+
// F1 generation fence: a superseded spawn must not let this commit run.
|
|
13553
|
+
if (spawnGeneration !== cliSpawnGeneration)
|
|
13554
|
+
throw new CliSpawnSupersededError();
|
|
13555
|
+
// F2 compare-before-replace: the pending file on disk must STILL be our
|
|
13556
|
+
// nonce (a same-worker restart / backend replacement between the pending
|
|
13557
|
+
// write and here could have replaced it with a newer generation's pending).
|
|
13558
|
+
const current = provenancePendingNonce(readManagedOriginAuthorityFile(commit.path));
|
|
13559
|
+
if (current !== commit.nonce) {
|
|
13560
|
+
throw new Error(`pending proof nonce mismatch (superseded generation) at ${commit.path}`);
|
|
13561
|
+
}
|
|
13562
|
+
// Mutual exclusivity: clear the sibling proof (verified) BEFORE committing,
|
|
13563
|
+
// so the committed generation is never shadowed by a stale sibling.
|
|
13564
|
+
if (commit.clearSiblingPath)
|
|
13565
|
+
removeProofOrThrow(commit.clearSiblingPath, `stale ${commit.label} sibling`);
|
|
13566
|
+
// Atomic replace pending → committed.
|
|
13567
|
+
replaceManagedOriginCapabilityFile(commit.path, commit.committedContent);
|
|
13568
|
+
}
|
|
13569
|
+
catch (err) {
|
|
13570
|
+
if (err instanceof CliSpawnSupersededError)
|
|
13571
|
+
throw err;
|
|
13572
|
+
// Condition #3: a commit-write failure must not leave an ambiguous started
|
|
13573
|
+
// pane running as if successful — tear down the exact target and refuse.
|
|
13574
|
+
// (The pending proof, if it survived, keeps the next launch fail-closed.)
|
|
13575
|
+
tearDownAndRefuse(`could not commit ${commit.label} generation proof (${err.message})`);
|
|
13576
|
+
}
|
|
13577
|
+
}
|
|
13578
|
+
}
|
|
12685
13579
|
try {
|
|
12686
13580
|
finalizeCodexAppControlGeneration(cfg, actuallyReattachedPersistent, !!persistentSessionName);
|
|
12687
13581
|
}
|
|
@@ -13082,11 +13976,12 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13082
13976
|
else
|
|
13083
13977
|
markPromptReady();
|
|
13084
13978
|
};
|
|
13085
|
-
// Set up idle detection.
|
|
13086
|
-
//
|
|
13979
|
+
// Set up idle detection. Remote backends (riff / mojo) have no PTY output and
|
|
13980
|
+
// are marked ready immediately after spawn (see below), so the idle detector
|
|
13087
13981
|
// is unnecessary — and without a readyPattern it would fire on every
|
|
13088
13982
|
// quiescence, repeatedly triggering markPromptReady() and duplicate cards.
|
|
13089
|
-
|
|
13983
|
+
// (For mojo it is also actively harmful — see MojoBackend.settleTurn.)
|
|
13984
|
+
if (!isRemoteBackendType(effectiveBackendType)) {
|
|
13090
13985
|
idleDetector = new IdleDetector(cliAdapter);
|
|
13091
13986
|
wireIdleDetectorBusyTransition(idleDetector, `${cliName()} PTY`);
|
|
13092
13987
|
idleDetector.onIdle(async (evidenceSource) => {
|
|
@@ -13177,6 +14072,22 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13177
14072
|
log(`${cliName()} task finished — re-arming prompt-ready for queued follow-ups`);
|
|
13178
14073
|
markPromptReady();
|
|
13179
14074
|
});
|
|
14075
|
+
// Headless final-answer bridge (mojo): deliver the turn's answer to the
|
|
14076
|
+
// thread when the agent produced one but never called `botmux send`. Same
|
|
14077
|
+
// generation fence as onTaskDone above — a late callback from a backend the
|
|
14078
|
+
// worker has already replaced must not post into the current turn.
|
|
14079
|
+
backend.onTurnFinal?.((text) => {
|
|
14080
|
+
if (fatalWorkerErrorPending)
|
|
14081
|
+
return;
|
|
14082
|
+
if (backend !== observedBackend)
|
|
14083
|
+
return;
|
|
14084
|
+
try {
|
|
14085
|
+
deliverMojoTurnFinal(text);
|
|
14086
|
+
}
|
|
14087
|
+
catch (err) {
|
|
14088
|
+
log(`Mojo final bridge failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
14089
|
+
}
|
|
14090
|
+
});
|
|
13180
14091
|
// riff:任务 id 变更同步给 daemon 持久化,daemon 重启后 follow-up 血缘不断。
|
|
13181
14092
|
backend.onTaskId?.((taskId) => {
|
|
13182
14093
|
send({ type: 'riff_task_id', taskId });
|
|
@@ -13327,6 +14238,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13327
14238
|
isPromptReady = false;
|
|
13328
14239
|
currentBotmuxTurnId = undefined;
|
|
13329
14240
|
currentBotmuxDispatchAttempt = undefined;
|
|
14241
|
+
currentGatewayTrustedCaller = undefined;
|
|
13330
14242
|
if (!intentionalRestart && activeRestartAttemptId) {
|
|
13331
14243
|
send({
|
|
13332
14244
|
type: 'restart_result',
|
|
@@ -13416,15 +14328,79 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
13416
14328
|
markPromptReady();
|
|
13417
14329
|
};
|
|
13418
14330
|
setTimeout(() => releaseFirstPromptTimeout(FIRST_PROMPT_TIMEOUT_MS, false), FIRST_PROMPT_TIMEOUT_MS);
|
|
13419
|
-
//
|
|
13420
|
-
//
|
|
13421
|
-
//
|
|
13422
|
-
//
|
|
13423
|
-
// and
|
|
13424
|
-
if (effectiveBackendType
|
|
14331
|
+
// Remote backends (riff / mojo) have no local boot process — the backend is
|
|
14332
|
+
// ready immediately after spawn(). The idle detector never fires for them (no
|
|
14333
|
+
// PTY output), and the first-prompt timeout only flushes for type-ahead
|
|
14334
|
+
// adapters, so isPromptReady would otherwise stay false until the ~15s
|
|
14335
|
+
// fallback and every first message would be needlessly delayed.
|
|
14336
|
+
if (isRemoteBackendType(effectiveBackendType)) {
|
|
14337
|
+
// BEFORE markPromptReady, which can flush a queued turn: a replacement
|
|
14338
|
+
// backend is built from the original init config, so a rotated — or cleared —
|
|
14339
|
+
// credential must be re-applied first or the queued turn runs against the
|
|
14340
|
+
// revived token.
|
|
14341
|
+
restoreMojoLivePatchAfterRespawn();
|
|
13425
14342
|
markPromptReady();
|
|
13426
14343
|
}
|
|
13427
14344
|
}
|
|
14345
|
+
/**
|
|
14346
|
+
* Hand a credential/behaviour patch to a live MojoBackend.
|
|
14347
|
+
*
|
|
14348
|
+
* Validated first: the daemon is trusted, but this arrives over IPC and a stale
|
|
14349
|
+
* or malformed payload must not reach the backend. Non-mojo backends ignore it.
|
|
14350
|
+
*/
|
|
14351
|
+
/**
|
|
14352
|
+
* Last VALIDATED credential snapshot applied to a backend in this worker.
|
|
14353
|
+
*
|
|
14354
|
+
* Survives a backend replacement (`/restart`, crash respawn, lease-fenced
|
|
14355
|
+
* restart), which is the only place it matters: a replacement backend is
|
|
14356
|
+
* constructed from the ORIGINAL init config, so a credential that had since been
|
|
14357
|
+
* rotated — or, worse, cleared — silently reverted. A queued clear turn then ran
|
|
14358
|
+
* against the revived token. Restart IPC deliberately does NOT need to carry the
|
|
14359
|
+
* snapshot; the worker already knows it.
|
|
14360
|
+
*/
|
|
14361
|
+
let lastAppliedMojoLivePatch;
|
|
14362
|
+
function applyMojoLivePatch(patch) {
|
|
14363
|
+
if (!patch || effectiveBackendType !== 'mojo')
|
|
14364
|
+
return;
|
|
14365
|
+
// Patch-specific validator. The CONFIG validator was wrong here: it requires a
|
|
14366
|
+
// non-empty `jwt` string, so it rejected every `null` tombstone and the backend
|
|
14367
|
+
// never received a clear request — the credential stayed live for the worker's
|
|
14368
|
+
// whole lifetime. This one accepts exactly `{ jwt: string | null }`.
|
|
14369
|
+
const validated = normalizeMojoLivePatch(patch);
|
|
14370
|
+
if (!validated.ok) {
|
|
14371
|
+
log(`Ignoring invalid mojo live patch: ${validated.errors.join('; ')}`);
|
|
14372
|
+
return;
|
|
14373
|
+
}
|
|
14374
|
+
if (validated.value.jwt !== undefined)
|
|
14375
|
+
lastAppliedMojoLivePatch = validated.value;
|
|
14376
|
+
pushMojoLivePatchToBackend(validated.value);
|
|
14377
|
+
}
|
|
14378
|
+
/** Hand an already-validated snapshot to the live backend, if it can take one. */
|
|
14379
|
+
function pushMojoLivePatchToBackend(patch) {
|
|
14380
|
+
// Duck-typed rather than `instanceof`: importing MojoBackend as a value here
|
|
14381
|
+
// would pull the backend module into the worker's eager import graph for every
|
|
14382
|
+
// CLI, and the capability check is what actually matters.
|
|
14383
|
+
const current = backend;
|
|
14384
|
+
if (typeof current?.applyLivePatch !== 'function')
|
|
14385
|
+
return;
|
|
14386
|
+
current.applyLivePatch(patch);
|
|
14387
|
+
}
|
|
14388
|
+
/**
|
|
14389
|
+
* Re-apply the remembered snapshot onto a freshly installed backend.
|
|
14390
|
+
*
|
|
14391
|
+
* Must run AFTER the replacement backend is installed and BEFORE any pending turn
|
|
14392
|
+
* is flushed into it, or a queued clear would execute against the reverted
|
|
14393
|
+
* credential.
|
|
14394
|
+
*/
|
|
14395
|
+
function restoreMojoLivePatchAfterRespawn() {
|
|
14396
|
+
if (effectiveBackendType !== 'mojo')
|
|
14397
|
+
return;
|
|
14398
|
+
const remembered = lastAppliedMojoLivePatch;
|
|
14399
|
+
if (!remembered || remembered.jwt === undefined)
|
|
14400
|
+
return;
|
|
14401
|
+
pushMojoLivePatchToBackend(remembered);
|
|
14402
|
+
log(`Restored live mojo JWT state after backend replacement (${remembered.jwt === null ? 'cleared' : 'rotated'})`);
|
|
14403
|
+
}
|
|
13428
14404
|
function killCli(opts = {}) {
|
|
13429
14405
|
// Invalidate an in-flight async spawn before any other teardown hook runs. The
|
|
13430
14406
|
// remaining cleanup is synchronous today, but generation ownership must not
|
|
@@ -13449,6 +14425,8 @@ function killCli(opts = {}) {
|
|
|
13449
14425
|
stopReattachIdleProbe();
|
|
13450
14426
|
stopBusyPatternIdleProbe();
|
|
13451
14427
|
stopStructuredStartGraceRecheck();
|
|
14428
|
+
stopSpawnArgvTurnStartFailOpen();
|
|
14429
|
+
spawnArgvTurnStartBusyScanTail = '';
|
|
13452
14430
|
structuredRejectedReadyEvidenceGeneration = undefined;
|
|
13453
14431
|
ptyOutputGeneration.reset();
|
|
13454
14432
|
// Cancel any pending ready-gate fallback / settle timers; spawnCli re-arms on respawn.
|
|
@@ -13500,6 +14478,7 @@ function killCli(opts = {}) {
|
|
|
13500
14478
|
readIsolationOriginChannelId = null;
|
|
13501
14479
|
currentBotmuxTurnId = undefined;
|
|
13502
14480
|
currentBotmuxDispatchAttempt = undefined;
|
|
14481
|
+
currentGatewayTrustedCaller = undefined;
|
|
13503
14482
|
currentVcMeetingImTurnOrigin = undefined;
|
|
13504
14483
|
submittedCodexAppReplyTurnIds.clear();
|
|
13505
14484
|
pendingCodexAppSteerAckIds.clear();
|
|
@@ -13586,14 +14565,39 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
13586
14565
|
if (restartingBackend)
|
|
13587
14566
|
intentionalRestartBackend = restartingBackend;
|
|
13588
14567
|
const teardown = restartingBackend?.destroySession?.();
|
|
14568
|
+
let teardownOutcome = { kind: 'absent' };
|
|
13589
14569
|
if (teardown && typeof teardown.then === 'function') {
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
|
|
13595
|
-
|
|
13596
|
-
|
|
14570
|
+
// Every branch is CAPTURED rather than discarded. The old code raced the
|
|
14571
|
+
// promise against a 22s sleep inside `try { } catch {}`, so a resolved
|
|
14572
|
+
// ok:false, a won timeout and a rejection were all indistinguishable from a
|
|
14573
|
+
// clean teardown — and all three fell through to killCli() + respawn.
|
|
14574
|
+
teardownOutcome = await Promise.race([
|
|
14575
|
+
Promise.resolve(teardown).then((raw) => ({ kind: 'settled', raw }), (error) => ({ kind: 'rejected', error })),
|
|
14576
|
+
new Promise(resolve => setTimeout(() => resolve({ kind: 'timeout' }), 22_000)),
|
|
14577
|
+
]);
|
|
14578
|
+
}
|
|
14579
|
+
const teardownVerdict = classifyRestartTeardown(teardownOutcome, {
|
|
14580
|
+
remote: isRemoteBackendType(effectiveBackendType),
|
|
14581
|
+
});
|
|
14582
|
+
if (!teardownVerdict.mayRespawn) {
|
|
14583
|
+
// Fail closed instead of respawning. The old subtree may still hold the
|
|
14584
|
+
// injected credential, so a replacement lineage must not be layered on top
|
|
14585
|
+
// of it: that is exactly how a second credentialed tree used to appear
|
|
14586
|
+
// while the first became unenumerable under a fresh nonce.
|
|
14587
|
+
//
|
|
14588
|
+
// Deliberately NOT calling abortDestroySession(): rollback is not the
|
|
14589
|
+
// legitimate exit from an uncertain teardown (see destroy-result.ts), and
|
|
14590
|
+
// for a latched fence the backend would refuse anyway. Admission therefore
|
|
14591
|
+
// stays fenced, and the persisted containment handle keeps the
|
|
14592
|
+
// device-isolation blocker in place for the next generation to resolve.
|
|
14593
|
+
log(`Restart ABORTED (${reason}): teardown not proven `
|
|
14594
|
+
+ `(${teardownVerdict.reason ?? 'unknown'}, `
|
|
14595
|
+
+ `${teardownVerdict.recovery ?? 'retryable'}); refusing to respawn a second `
|
|
14596
|
+
+ 'lineage over a possibly-live credentialed subtree');
|
|
14597
|
+
// cliRestartInProgress / rawInputRestartGate deliberately stay armed so no
|
|
14598
|
+
// input is accepted while the outcome is unknown.
|
|
14599
|
+
await sendFatalWorkerErrorAndExit(new Error(`restart teardown not proven: ${teardownVerdict.reason ?? 'unknown'}`));
|
|
14600
|
+
return;
|
|
13597
14601
|
}
|
|
13598
14602
|
killCli({ preservePending: opts.preservePending });
|
|
13599
14603
|
awaitingFirstPrompt = true;
|
|
@@ -13661,10 +14665,11 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
13661
14665
|
void restartCliProcess(reason, { preservePending: true, skipRestartBudget: followup.skipRestartBudget });
|
|
13662
14666
|
return;
|
|
13663
14667
|
}
|
|
13664
|
-
//
|
|
13665
|
-
// intentionally held by the restart gate above. Release
|
|
13666
|
-
// fence now;
|
|
13667
|
-
|
|
14668
|
+
// Remote backends mark themselves prompt-ready inside spawnCli(); that
|
|
14669
|
+
// early flush is intentionally held by the restart gate above. Release
|
|
14670
|
+
// the raw-input fence now; local backends keep it until their later
|
|
14671
|
+
// markPromptReady().
|
|
14672
|
+
if (isRemoteBackendType(effectiveBackendType) && isPromptReady)
|
|
13668
14673
|
releaseRawInputRestartGate();
|
|
13669
14674
|
// A local replacement process can exist before its TUI input box does.
|
|
13670
14675
|
// Only re-kick a prompt that became ready while the restart fence was
|
|
@@ -13759,21 +14764,62 @@ function startWebServer(host, preferredPort) {
|
|
|
13759
14764
|
},
|
|
13760
14765
|
});
|
|
13761
14766
|
wss.on('connection', (ws, req) => {
|
|
13762
|
-
|
|
13763
|
-
//
|
|
13764
|
-
//
|
|
14767
|
+
// P1-3 — the pre-handshake verdict is a SNAPSHOT, and this callback runs
|
|
14768
|
+
// strictly after it: ws only completes the upgrade once verifyClient
|
|
14769
|
+
// called back, and re-resolving access here re-reads `.dashboard-secret`
|
|
14770
|
+
// synchronously. A capability whose expiry — or the worker generation it
|
|
14771
|
+
// is pinned to, or the host secret it is signed under — turns over inside
|
|
14772
|
+
// that window was authorized by the FIRST check and is already dead by
|
|
14773
|
+
// this one. This callback used to recompute `access` but consume only
|
|
14774
|
+
// `hasWrite`, so such a socket was registered in `wsClients`, seeded with
|
|
14775
|
+
// the terminal scrollback, and — the second resolution carrying no
|
|
14776
|
+
// controlExpiresAt — never given an expiry timer at all: an unbounded
|
|
14777
|
+
// read-only stream outliving its own credential.
|
|
14778
|
+
//
|
|
14779
|
+
// FAIL CLOSED on the SECOND result instead. A socket that no longer
|
|
14780
|
+
// resolves to read authority is closed before any registration, so it
|
|
14781
|
+
// never joins a broadcast set and never receives the history seed; and
|
|
14782
|
+
// the expiry timer below is armed from THIS resolution's expiresAt —
|
|
14783
|
+
// never inherited from the handshake and never skipped.
|
|
13765
14784
|
const url = parseWorkerRequestUrl(req);
|
|
13766
14785
|
if (!url) {
|
|
13767
14786
|
log(`Bad worker WS URL rejected: ${JSON.stringify(req.url ?? '')}`);
|
|
13768
|
-
wsClients.delete(ws);
|
|
13769
14787
|
ws.close(1008, 'Bad Request');
|
|
13770
14788
|
return;
|
|
13771
14789
|
}
|
|
13772
|
-
const
|
|
14790
|
+
const access = resolveTerminalAccessForReq(req, url);
|
|
14791
|
+
const { hasRead, hasWrite, auditUser, controlExpiresAt } = access;
|
|
14792
|
+
if (!hasRead || (controlExpiresAt !== undefined && controlExpiresAt <= Date.now())) {
|
|
14793
|
+
log('WS client refused at the connection re-check: capability no longer valid');
|
|
14794
|
+
ws.close(4003, 'authorization expired');
|
|
14795
|
+
return;
|
|
14796
|
+
}
|
|
14797
|
+
wsClients.add(ws);
|
|
13773
14798
|
const allowReadOnlyRemoteScroll = canHandleReadOnlyRemoteScroll();
|
|
13774
14799
|
if (hasWrite)
|
|
13775
14800
|
authedClients.add(ws);
|
|
13776
14801
|
log(`WS client connected (total: ${wsClients.size}, write: ${hasWrite})`);
|
|
14802
|
+
// A signed Dashboard grant is fixed-expiry — for READ scope as much as
|
|
14803
|
+
// for write (P1-5). Even if the central proxy's socket invalidation is
|
|
14804
|
+
// delayed or bypassed, the worker independently removes any granted
|
|
14805
|
+
// authority and closes this connection at the signed boundary; a
|
|
14806
|
+
// reconnect must then present a still-valid credential. Read-only
|
|
14807
|
+
// sockets used to outlive their authentication here; now they cannot.
|
|
14808
|
+
let controlExpiryTimer;
|
|
14809
|
+
if (controlExpiresAt !== undefined) {
|
|
14810
|
+
const expiredReason = hasWrite ? 'control expired' : 'view expired';
|
|
14811
|
+
controlExpiryTimer = setTimeout(() => {
|
|
14812
|
+
authedClients.delete(ws);
|
|
14813
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
14814
|
+
ws.close(4003, expiredReason);
|
|
14815
|
+
}, Math.max(0, controlExpiresAt - Date.now()));
|
|
14816
|
+
controlExpiryTimer.unref();
|
|
14817
|
+
}
|
|
14818
|
+
ws.once('close', () => {
|
|
14819
|
+
if (controlExpiryTimer)
|
|
14820
|
+
clearTimeout(controlExpiryTimer);
|
|
14821
|
+
authedClients.delete(ws);
|
|
14822
|
+
});
|
|
13777
14823
|
if (isTmuxMode && !isPipeMode && sessionId) {
|
|
13778
14824
|
// ── Tmux-attach mode: per-client attach ──
|
|
13779
14825
|
// Each WS client gets its own `tmux attach-session` PTY.
|
|
@@ -13855,6 +14901,7 @@ function startWebServer(host, preferredPort) {
|
|
|
13855
14901
|
// A view capability therefore forwards no input bytes at all.
|
|
13856
14902
|
if (!authedClients.has(ws))
|
|
13857
14903
|
return;
|
|
14904
|
+
auditTerminalInput(auditUser, msg.data);
|
|
13858
14905
|
if (cp)
|
|
13859
14906
|
cp.write(msg.data);
|
|
13860
14907
|
else
|
|
@@ -13905,7 +14952,12 @@ function startWebServer(host, preferredPort) {
|
|
|
13905
14952
|
name: 'xterm-256color',
|
|
13906
14953
|
cols,
|
|
13907
14954
|
rows,
|
|
13908
|
-
|
|
14955
|
+
// redactChildEnv first, matching the tmux viewer above (tmuxEnv()
|
|
14956
|
+
// folds REDACTED_CHILD_ENV_KEYS into its own strip set, zellijEnv()
|
|
14957
|
+
// only drops ZELLIJ*). A viewer client needs none of the daemon's
|
|
14958
|
+
// secrets — bare IM-app creds, GitHub tokens, or the Dashboard H5
|
|
14959
|
+
// credentials — so it must not carry them into a pty it owns.
|
|
14960
|
+
env: zellijEnv(redactChildEnv(process.env)),
|
|
13909
14961
|
});
|
|
13910
14962
|
attachStarted = true;
|
|
13911
14963
|
observeBe?.setLiveAttach(true);
|
|
@@ -13938,6 +14990,7 @@ function startWebServer(host, preferredPort) {
|
|
|
13938
14990
|
else if (msg.type === 'input' && typeof msg.data === 'string') {
|
|
13939
14991
|
if (!authedClients.has(ws))
|
|
13940
14992
|
return;
|
|
14993
|
+
auditTerminalInput(auditUser, msg.data);
|
|
13941
14994
|
if (cp)
|
|
13942
14995
|
cp.write(msg.data);
|
|
13943
14996
|
else
|
|
@@ -13996,8 +15049,17 @@ function startWebServer(host, preferredPort) {
|
|
|
13996
15049
|
scrollback,
|
|
13997
15050
|
onError: log,
|
|
13998
15051
|
});
|
|
13999
|
-
|
|
14000
|
-
|
|
15052
|
+
// A capture-pane seed carries screen cells but no DECSET state: a fresh
|
|
15053
|
+
// client xterm never learns the CLI enabled mouse tracking (grok build:
|
|
15054
|
+
// 1003+1006), so clicks/double-clicks are silently swallowed instead of
|
|
15055
|
+
// reported. Re-assert the pane's live input modes after the seed —
|
|
15056
|
+
// write clients only: a read-only viewer forwards no input anyway, and
|
|
15057
|
+
// mouse-mode xterm would break its plain select-to-copy. Raw-scrollback
|
|
15058
|
+
// seeds already contain the original DECSET bytes; backends that can't
|
|
15059
|
+
// be queried (herdr/zmx/pty) simply don't implement the hook.
|
|
15060
|
+
const modeSeed = hasWrite ? (backend?.capturePaneInputModes?.() ?? '') : '';
|
|
15061
|
+
if (seed.length > 0 || modeSeed.length > 0) {
|
|
15062
|
+
ws.send(seed + modeSeed + herdrWebCursorSequence());
|
|
14001
15063
|
}
|
|
14002
15064
|
ws.on('message', (raw) => {
|
|
14003
15065
|
try {
|
|
@@ -14014,6 +15076,7 @@ function startWebServer(host, preferredPort) {
|
|
|
14014
15076
|
// A read-only view capability must never forward bytes to the backend.
|
|
14015
15077
|
if (!authedClients.has(ws))
|
|
14016
15078
|
return;
|
|
15079
|
+
auditTerminalInput(auditUser, msg.data);
|
|
14017
15080
|
if (usesHerdrSnapshotWebHistory()) {
|
|
14018
15081
|
if (msg.data.includes('\x1b[<64;'))
|
|
14019
15082
|
herdrWebScrollDirection = 'up';
|
|
@@ -14200,12 +15263,46 @@ if(!hasToken){
|
|
|
14200
15263
|
else{var _rb=document.getElementById('readonly-banner');_rb.classList.add('show');_rb.addEventListener('click',function(){_rb.classList.remove('show')});}
|
|
14201
15264
|
}
|
|
14202
15265
|
|
|
15266
|
+
// ── 终端字号按容器宽度自适应 ─────────────────────────────────────────────────
|
|
15267
|
+
// xterm 的 fontSize 是这一页唯一的几何输入:字号写死,列数就只能被容器宽度被动
|
|
15268
|
+
// 决定。14px 是照桌面宽度调的,手机把这一页直嵌进 390px 宽的 iframe 之后一行只
|
|
15269
|
+
// 剩约 44 列 —— 字大得离谱,CLI 的 TUI 还被迫在 44 列上硬折行,逻辑行换行稀碎。
|
|
15270
|
+
// 这里把因果反过来:先定一个可读的目标列数,再由容器实际宽度反推字号。
|
|
15271
|
+
//
|
|
15272
|
+
// _WB_FONT_ADVANCE 是等宽字体的标称「字宽 / 字号」比(JetBrains Mono、Fira Code、
|
|
15273
|
+
// Menlo、DejaVu Sans Mono 都是 0.6),只用于估算;真实列数仍由 xterm 量完字体后
|
|
15274
|
+
// 自己 fit() 出来,估算偏一点最多让列数在目标附近浮动,不会错位。
|
|
15275
|
+
// 只缩不放:算出来比 _WB_FONT_BASE 大一律按 base 走,所以宽度够(约 521px 以上)
|
|
15276
|
+
// 的桌面一律还是 14px,存量渲染零变化;窄容器才往下缩,最低到 _WB_FONT_MIN。
|
|
15277
|
+
//
|
|
15278
|
+
// 注意:这一整段在 worker.ts 的模板字符串里,注释和代码都不能出现反引号,
|
|
15279
|
+
// 也不能出现「美元号 + 左花括号」的插值起始序列,否则会把模板字符串截断。
|
|
15280
|
+
var _WB_FONT_MIN=9,_WB_FONT_MAX=15,_WB_FONT_BASE=14,_WB_FONT_ADVANCE=.6,_WB_TARGET_COLS=62;
|
|
15281
|
+
function _wbAutoFontSize(width){
|
|
15282
|
+
if(!(width>0))return _WB_FONT_BASE;
|
|
15283
|
+
var ideal=width/(_WB_TARGET_COLS*_WB_FONT_ADVANCE);
|
|
15284
|
+
var capped=ideal>_WB_FONT_BASE?_WB_FONT_BASE:ideal;
|
|
15285
|
+
// 半档取整:比整数细一档,又不会停在容易糊掉的亚像素字号上。
|
|
15286
|
+
var stepped=Math.round(capped*2)/2;
|
|
15287
|
+
// 硬边界:字号是这一页唯一的几何输入,任何异常宽度都不许把它推到读不了的档位。
|
|
15288
|
+
if(stepped<_WB_FONT_MIN)return _WB_FONT_MIN;
|
|
15289
|
+
if(stepped>_WB_FONT_MAX)return _WB_FONT_MAX;
|
|
15290
|
+
return stepped;
|
|
15291
|
+
}
|
|
15292
|
+
function _wbTerminalWidth(){
|
|
15293
|
+
var host=document.getElementById('terminal');
|
|
15294
|
+
var w=host?host.clientWidth:0;
|
|
15295
|
+
if(w>0)return w;
|
|
15296
|
+
// 容器还没布局出宽度(隐藏面板、刚插进 DOM 的 iframe)时退回视口宽;再量不到就
|
|
15297
|
+
// 返回 0,_wbAutoFontSize 保持基准字号,等尺寸事件到了再纠正。
|
|
15298
|
+
return window.innerWidth||0;
|
|
15299
|
+
}
|
|
14203
15300
|
var term=new Terminal({
|
|
14204
15301
|
theme:{background:'#1a1b26',foreground:'#a9b1d6',cursor:'#c0caf5',
|
|
14205
15302
|
selectionBackground:'#33467c',black:'#15161e',red:'#f7768e',
|
|
14206
15303
|
green:'#9ece6a',yellow:'#e0af68',blue:'#7aa2f7',magenta:'#bb9af7',
|
|
14207
15304
|
cyan:'#7dcfff',white:'#a9b1d6'},
|
|
14208
|
-
fontSize:
|
|
15305
|
+
fontSize:_wbAutoFontSize(_wbTerminalWidth()),fontFamily:"'JetBrains Mono','Fira Code',monospace",
|
|
14209
15306
|
cursorBlink:!isTouch,scrollback:50000,allowProposedApi:true
|
|
14210
15307
|
});
|
|
14211
15308
|
var fit=new FitAddon.FitAddon();
|
|
@@ -14225,7 +15322,53 @@ try{
|
|
|
14225
15322
|
}catch(_e){
|
|
14226
15323
|
try{term.loadAddon(new CanvasAddon.CanvasAddon())}catch(_e2){}
|
|
14227
15324
|
}
|
|
15325
|
+
// 首帧字号已经在 new Terminal 里按容器宽度定好了;这个函数负责后续尺寸变化
|
|
15326
|
+
// (转屏、iframe 改宽、分屏、地址栏收放)时复算。返回 true 表示字号真的换了档。
|
|
15327
|
+
function _wbApplyAutoFontSize(){
|
|
15328
|
+
var next=_wbAutoFontSize(_wbTerminalWidth());
|
|
15329
|
+
if(term.options.fontSize===next)return false;
|
|
15330
|
+
term.options.fontSize=next;
|
|
15331
|
+
return true;
|
|
15332
|
+
}
|
|
14228
15333
|
fit.fit();
|
|
15334
|
+
// 工作台下发的终端外观(消息类型 botmux:wb-appearance)。终端画布住在这个跨文档
|
|
15335
|
+
// iframe 里,父页换 class / 换 CSS 变量都传不进来,配色只能靠 postMessage 递过来。
|
|
15336
|
+
// 注意:这一整段在 worker.ts 的模板字符串里,注释和代码都不能出现反引号,
|
|
15337
|
+
// 也不能出现「美元号 + 左花括号」的插值起始序列,否则会把模板字符串截断。
|
|
15338
|
+
// 安全边界:这个页面会被跨 origin 嵌入,因此只认 window.parent 发来的、结构完全
|
|
15339
|
+
// 对得上的消息,且每个颜色都必须是 #rgb / #rrggbb 十六进制字面量;少一个键、多一个
|
|
15340
|
+
// 非法值,整条消息丢弃(宁可保持现状,也不要把半套主题刷到画布上)。
|
|
15341
|
+
var _WB_THEME_KEYS=['background','foreground','cursor','selectionBackground',
|
|
15342
|
+
'black','red','green','yellow','blue','magenta','cyan','white'];
|
|
15343
|
+
function _wbSanitizeTheme(raw){
|
|
15344
|
+
if(!raw||typeof raw!=='object')return null;
|
|
15345
|
+
var out={};
|
|
15346
|
+
for(var _i=0;_i<_WB_THEME_KEYS.length;_i++){
|
|
15347
|
+
var _k=_WB_THEME_KEYS[_i],_v=raw[_k];
|
|
15348
|
+
if(typeof _v!=='string'||!/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(_v))return null;
|
|
15349
|
+
out[_k]=_v;
|
|
15350
|
+
}
|
|
15351
|
+
return out;
|
|
15352
|
+
}
|
|
15353
|
+
window.addEventListener('message',function(_ev){
|
|
15354
|
+
if(_ev.source===window||_ev.source!==window.parent)return;
|
|
15355
|
+
var _d=_ev.data;
|
|
15356
|
+
if(!_d||_d.type!=='botmux:wb-appearance')return;
|
|
15357
|
+
var _theme=_wbSanitizeTheme(_d.theme);
|
|
15358
|
+
if(!_theme)return;
|
|
15359
|
+
try{
|
|
15360
|
+
term.options.theme=_theme;
|
|
15361
|
+
// 阅读风是大行距 1.3;经典保持 xterm 默认的 1,「经典 = 原样」的一部分。
|
|
15362
|
+
// 经典那套色值与上面写死的字面量逐色相同,所以没开阅读风的用户零变化。
|
|
15363
|
+
// 这两个数字的唯一出处是 WORKBENCH_TERM_LINE_HEIGHTS(agent-workbench-appearance.ts),
|
|
15364
|
+
// 接缝测试按那张表比对这一行的字面量:改一处必须两处一起改。
|
|
15365
|
+
// 曾经是 1.55,单元格高约 24.5px,CLI 底部那块固定 8 行的 chrome(提示 + 输入框 +
|
|
15366
|
+
// 状态条)就要占掉约 196px —— 一屏四分之一全是 chrome,正文被挤走。
|
|
15367
|
+
term.options.lineHeight=_d.termStyle==='reader'?1.3:1;
|
|
15368
|
+
// 行距变了可视行数就变,必须复算一次。几何契约不动:只有画布内重绘。
|
|
15369
|
+
fit.fit();
|
|
15370
|
+
}catch(_e){}
|
|
15371
|
+
});
|
|
14229
15372
|
// xterm parses writes asynchronously. On a brand-new page the first tmux /
|
|
14230
15373
|
// zellij frame (or relay history seed) can therefore finish after the browser
|
|
14231
15374
|
// has initialised the viewport scrollbar, leaving that viewport at scrollTop=0
|
|
@@ -14398,13 +15541,34 @@ if(hasToken && !/[?&]imefix=0\\b/.test(location.search)){(function(){
|
|
|
14398
15541
|
|
|
14399
15542
|
// ── WebSocket ──
|
|
14400
15543
|
var ws_=null,el=document.getElementById('status');
|
|
15544
|
+
function _sendInput(d){if(ws_&&ws_.readyState===1)ws_.send(JSON.stringify({type:'input',data:d}))}
|
|
15545
|
+
// Pure pointer-motion reports (SGR button code 35 + shift/alt/ctrl modifier
|
|
15546
|
+
// bits). Emitted by xterm once the seed re-asserts DECSET 1003 (grok build) —
|
|
15547
|
+
// one per cell crossed. Each forwarded report costs a synchronous tmux
|
|
15548
|
+
// send-keys exec in the worker, so an unthrottled sweep across a 120-col pane
|
|
15549
|
+
// would stall the relay for every viewer. Clicks/drags/wheel stay immediate.
|
|
15550
|
+
var _MOTION_RE=/^(?:\\x1b\\[<(?:35|39|43|47|51|55|59|63);\\d+;\\d+[Mm])+$/;
|
|
15551
|
+
var _motionPend=null,_motionT=0;
|
|
14401
15552
|
term.onData(function(d){
|
|
14402
15553
|
if(!hasToken){
|
|
14403
15554
|
// Mouse escape sequences are input too: a TUI can bind clicks or wheel
|
|
14404
15555
|
// events to actions. View links never forward terminal bytes.
|
|
14405
15556
|
_showReadonlyToast();return;
|
|
14406
15557
|
}
|
|
14407
|
-
if(
|
|
15558
|
+
if(_MOTION_RE.test(d)){
|
|
15559
|
+
// Trailing throttle: keep only the LATEST motion, flush every 90ms. Hover
|
|
15560
|
+
// feedback survives; the send-keys exec rate is bounded (~11/s).
|
|
15561
|
+
_motionPend=d;
|
|
15562
|
+
if(!_motionT)_motionT=setTimeout(function(){
|
|
15563
|
+
_motionT=0;if(_motionPend){_sendInput(_motionPend);_motionPend=null}
|
|
15564
|
+
},90);
|
|
15565
|
+
return;
|
|
15566
|
+
}
|
|
15567
|
+
// A press/release/drag/key supersedes any pending motion (it carries its own
|
|
15568
|
+
// coordinates); dropping it preserves event ordering for the TUI.
|
|
15569
|
+
if(_motionT){clearTimeout(_motionT);_motionT=0}
|
|
15570
|
+
_motionPend=null;
|
|
15571
|
+
_sendInput(d);
|
|
14408
15572
|
});
|
|
14409
15573
|
var fixedSize=false,_lastC=0,_lastR=0,_rzT=0;
|
|
14410
15574
|
function sendResize(){
|
|
@@ -14421,9 +15585,25 @@ function sendResize(){
|
|
|
14421
15585
|
// text re-wraps, i.e. the reported flicker. Coalesce to the settled size.
|
|
14422
15586
|
function onViewportResize(){
|
|
14423
15587
|
clearTimeout(_rzT);
|
|
14424
|
-
_rzT=setTimeout(function(){
|
|
15588
|
+
_rzT=setTimeout(function(){
|
|
15589
|
+
// 先复算字号再 fit:字号换档字宽就变,反过来 fit 出来的是旧字号下的列数。
|
|
15590
|
+
// 只动画布内的渲染几何,与后端的 resize 语义一如既往由下面的 sendResize 决定。
|
|
15591
|
+
try{_wbApplyAutoFontSize()}catch(e){}
|
|
15592
|
+
if(!fixedSize){try{fit.fit()}catch(e){}}
|
|
15593
|
+
sendResize();
|
|
15594
|
+
},250);
|
|
14425
15595
|
}
|
|
14426
15596
|
window.addEventListener('resize',onViewportResize);
|
|
15597
|
+
// 转屏:部分 iOS 版本只发 orientationchange 或先于 resize 发,重复触发被防抖吃掉。
|
|
15598
|
+
window.addEventListener('orientationchange',onViewportResize);
|
|
15599
|
+
// 嵌入方(工作台终端面板、会话坞)单独改 iframe 宽度时,窗口 resize 未必可靠地传
|
|
15600
|
+
// 进来。直接盯容器自己的尺寸,这一页才真正自包含 —— 任何嵌入方都跟着受益。
|
|
15601
|
+
if(typeof ResizeObserver!=='undefined'){
|
|
15602
|
+
try{
|
|
15603
|
+
var _wbHost=document.getElementById('terminal');
|
|
15604
|
+
if(_wbHost)new ResizeObserver(onViewportResize).observe(_wbHost);
|
|
15605
|
+
}catch(_e){}
|
|
15606
|
+
}
|
|
14427
15607
|
(function connect(){
|
|
14428
15608
|
// Derive base from the current path so the WS connects to the same prefix the
|
|
14429
15609
|
// page was served under — works both directly (path '/') and behind the
|
|
@@ -14927,7 +16107,7 @@ const emittedTurnTerminals = new TurnTerminalDeduper();
|
|
|
14927
16107
|
/** Report CLI processing completion independently from user-visible output.
|
|
14928
16108
|
* Keep a bounded worker-local dedup set because transcript watchers and app
|
|
14929
16109
|
* runners may surface the same final boundary more than once. */
|
|
14930
|
-
function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt, outputDisposition) {
|
|
16110
|
+
function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt, outputDisposition, retryable) {
|
|
14931
16111
|
if (!sessionId || !turnId)
|
|
14932
16112
|
return;
|
|
14933
16113
|
if (!emittedTurnTerminals.claim(sessionId, turnId, dispatchAttempt))
|
|
@@ -14949,6 +16129,7 @@ function emitTurnTerminal(turnId, status, errorCode, dispatchAttempt, outputDisp
|
|
|
14949
16129
|
...(dispatchAttempt !== undefined ? { dispatchAttempt } : {}),
|
|
14950
16130
|
...(errorCode ? { errorCode } : {}),
|
|
14951
16131
|
...(outputDisposition ? { outputDisposition } : {}),
|
|
16132
|
+
...(retryable !== undefined ? { retryable } : {}),
|
|
14952
16133
|
});
|
|
14953
16134
|
if (terminalReleasesDurableTurn({ turnId: currentBotmuxTurnId, dispatchAttempt: currentBotmuxDispatchAttempt }, { turnId, dispatchAttempt })) {
|
|
14954
16135
|
// Durable replay ownership moves back to the receiver/hub at terminal.
|
|
@@ -15122,7 +16303,7 @@ process.on('message', async (raw) => {
|
|
|
15122
16303
|
const ordinaryImTurnId = !msg.adoptMode
|
|
15123
16304
|
&& msg.dispatchAttempt === undefined
|
|
15124
16305
|
&& !!msg.prompt
|
|
15125
|
-
&& msg.turnId?.startsWith('om_')
|
|
16306
|
+
&& (msg.turnId?.startsWith('om_') || msg.turnId?.startsWith('bmx-recovery-'))
|
|
15126
16307
|
? msg.turnId
|
|
15127
16308
|
: undefined;
|
|
15128
16309
|
if (lastInitConfig) {
|
|
@@ -15159,7 +16340,8 @@ process.on('message', async (raw) => {
|
|
|
15159
16340
|
initialInputOwnershipPending = !!msg.prompt;
|
|
15160
16341
|
activeRestartAttemptId = msg.restartAttemptId;
|
|
15161
16342
|
sessionId = msg.sessionId;
|
|
15162
|
-
|
|
16343
|
+
// The view token intentionally stays per-boot random (no refresh): a
|
|
16344
|
+
// worker restart must invalidate every previously issued read link.
|
|
15163
16345
|
refreshTerminalWriteToken();
|
|
15164
16346
|
applySessionOwnerEnv(process.env, msg.ownerOpenId);
|
|
15165
16347
|
// Pin this worker's i18n locale early so every t() call below resolves
|
|
@@ -15229,6 +16411,7 @@ process.on('message', async (raw) => {
|
|
|
15229
16411
|
if (msg.turnId) {
|
|
15230
16412
|
currentBotmuxTurnId = msg.turnId;
|
|
15231
16413
|
currentBotmuxDispatchAttempt = msg.dispatchAttempt;
|
|
16414
|
+
currentGatewayTrustedCaller = msg.trustedCaller;
|
|
15232
16415
|
currentVcMeetingImTurnOrigin = msg.vcMeetingImTurnOrigin;
|
|
15233
16416
|
writeCliPidMarker();
|
|
15234
16417
|
publishSandboxRelayCapability();
|
|
@@ -15348,6 +16531,7 @@ process.on('message', async (raw) => {
|
|
|
15348
16531
|
: undefined),
|
|
15349
16532
|
codexAppInput: entry.codexAppInput,
|
|
15350
16533
|
vcMeetingImTurnOrigin: entry.vcMeetingImTurnOrigin,
|
|
16534
|
+
trustedCaller: msg.trustedCaller,
|
|
15351
16535
|
}));
|
|
15352
16536
|
let initialInputCommitted = false;
|
|
15353
16537
|
if (shouldQueueInitialPrompt({
|
|
@@ -15375,6 +16559,7 @@ process.on('message', async (raw) => {
|
|
|
15375
16559
|
codexAppDispatchId: msg.codexAppDispatchId,
|
|
15376
16560
|
queuedActivationToken: msg.queuedActivationToken,
|
|
15377
16561
|
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
16562
|
+
trustedCaller: msg.trustedCaller,
|
|
15378
16563
|
codexAppInput: msg.promptCodexAppInput,
|
|
15379
16564
|
// Thread the root's steer authorization so ordered pre-final steer can
|
|
15380
16565
|
// fold follow-ups into THIS turn (the runner's canSteer requires the
|
|
@@ -15449,10 +16634,22 @@ process.on('message', async (raw) => {
|
|
|
15449
16634
|
break;
|
|
15450
16635
|
}
|
|
15451
16636
|
case 'message': {
|
|
16637
|
+
// 每轮消息都捎带 daemon 侧当前解析出的模型,覆盖 respawn 快照——理由与
|
|
16638
|
+
// restart 那条通道相同,但这里管的是**崩溃 park 后由消息触发的恢复重启**:
|
|
16639
|
+
// 那条路在 worker 内部直接 `{...lastInitConfig, resume:true}` 起 CLI,没有
|
|
16640
|
+
// restart IPC 可携带新值。三分态同 restart:undefined = 不携带(旧 daemon)
|
|
16641
|
+
// 保持快照;null = 明确不传模型。对已经在跑的 CLI 无影响(只在下次 spawn 用)。
|
|
16642
|
+
if (msg.model !== undefined && lastInitConfig) {
|
|
16643
|
+
lastInitConfig.model = msg.model === null ? undefined : msg.model;
|
|
16644
|
+
}
|
|
16645
|
+
// NOTE: the mojo credential snapshot is deliberately NOT applied here. It
|
|
16646
|
+
// rides on the queue item and is applied when THIS turn is actually written
|
|
16647
|
+
// to the backend (see flushPending) — applying at receive time made two
|
|
16648
|
+
// queued credential turns collapse to the newest value for both.
|
|
15452
16649
|
const messageAdoptMode = lastInitConfig?.adoptMode === true;
|
|
15453
16650
|
const ordinaryImTurnId = !messageAdoptMode
|
|
15454
16651
|
&& msg.dispatchAttempt === undefined
|
|
15455
|
-
&& msg.turnId?.startsWith('om_')
|
|
16652
|
+
&& (msg.turnId?.startsWith('om_') || msg.turnId?.startsWith('bmx-recovery-'))
|
|
15456
16653
|
? msg.turnId
|
|
15457
16654
|
: undefined;
|
|
15458
16655
|
if (ordinaryImTurnId) {
|
|
@@ -15543,6 +16740,7 @@ process.on('message', async (raw) => {
|
|
|
15543
16740
|
codexAppDispatchId: msg.codexAppDispatchId,
|
|
15544
16741
|
queuedActivationToken: msg.queuedActivationToken,
|
|
15545
16742
|
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
16743
|
+
trustedCaller: msg.trustedCaller,
|
|
15546
16744
|
codexAppInput,
|
|
15547
16745
|
};
|
|
15548
16746
|
// process.on('message') does not serialize async listeners. Hold the
|
|
@@ -15574,6 +16772,9 @@ process.on('message', async (raw) => {
|
|
|
15574
16772
|
queuedActivationToken: msg.queuedActivationToken,
|
|
15575
16773
|
replyTurnId: msg.replyTurnId,
|
|
15576
16774
|
vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
|
|
16775
|
+
trustedCaller: msg.trustedCaller,
|
|
16776
|
+
// Applied when THIS item is written, not on receipt.
|
|
16777
|
+
...(msg.mojoLivePatch ? { mojoLivePatch: msg.mojoLivePatch } : {}),
|
|
15577
16778
|
});
|
|
15578
16779
|
if (inputCommitted)
|
|
15579
16780
|
acknowledgeTurnInputCommitted(msg.turnId);
|
|
@@ -15642,6 +16843,14 @@ process.on('message', async (raw) => {
|
|
|
15642
16843
|
log('Refused Riff generation restart; the existing lineage-owning worker is retained');
|
|
15643
16844
|
break;
|
|
15644
16845
|
}
|
|
16846
|
+
// Mojo is deliberately NOT refused here (unlike riff): the mojo restart
|
|
16847
|
+
// teardown cancels the remote session and cold-boots a replacement, and
|
|
16848
|
+
// internal machinery (per-bot env hot updates, backend-replacement tests)
|
|
16849
|
+
// relies on that. USER-facing restart for every remote backend is instead
|
|
16850
|
+
// rejected at the daemon boundary — /restart in command-handler and the
|
|
16851
|
+
// dashboard restart route both gate on isRemoteBackendSession — so a
|
|
16852
|
+
// restart IPC that reaches a mojo worker is a daemon-internal decision,
|
|
16853
|
+
// not a user command that would silently destroy remote context.
|
|
15645
16854
|
// 角色切换的 cwd-move respawn:respawn 用 {...lastInitConfig, resume:true},
|
|
15646
16855
|
// 先收敛 workingDir 才能让 CLI 在新目录重启(新 cwd 的 CLAUDE.md/记忆索引
|
|
15647
16856
|
// 开场注入)。旧桶 transcript 由 resume 预检的 syncClaudeResumeTargetToCwd
|
|
@@ -15659,6 +16868,14 @@ process.on('message', async (raw) => {
|
|
|
15659
16868
|
if (msg.env !== undefined && lastInitConfig) {
|
|
15660
16869
|
lastInitConfig.env = msg.env === null ? undefined : msg.env;
|
|
15661
16870
|
}
|
|
16871
|
+
// model 热更,与 env 同一条通道、同一套三分态。model 不进冻结集合、每次
|
|
16872
|
+
// spawn 按当前 bot 配置解析(见 resolveSessionLaunchModel),但 live-worker
|
|
16873
|
+
// restart 不 refork,没有 init 重发这条通道——不覆盖的话,改完模型再重启
|
|
16874
|
+
// 起来的还是快照里的旧模型。undefined=不携带(旧 daemon / 取不到)保持快照;
|
|
16875
|
+
// null=当前不该传模型(bot 未配 / 已清空)→ 移除快照。同样放在合并守卫之前。
|
|
16876
|
+
if (msg.model !== undefined && lastInitConfig) {
|
|
16877
|
+
lastInitConfig.model = msg.model === null ? undefined : msg.model;
|
|
16878
|
+
}
|
|
15662
16879
|
// restart 合并:已有一轮 restart 在飞(teardown 进行中,或 tmux jitter
|
|
15663
16880
|
// 定时器未触发)时不叠加第二轮——叠加会 clearTimeout 吃掉首轮 teardown、
|
|
15664
16881
|
// 把重启预算无故烧到 tier-2 强制 FRESH(丢上下文),非 tmux 路径还会
|
|
@@ -16004,8 +17221,13 @@ process.on('message', async (raw) => {
|
|
|
16004
17221
|
break;
|
|
16005
17222
|
}
|
|
16006
17223
|
case 'refresh_screen': {
|
|
16007
|
-
|
|
17224
|
+
// The daemon only offers manual refresh on a screenshot-mode card, so
|
|
17225
|
+
// landing here in another mode means the display-mode re-sync was lost —
|
|
17226
|
+
// say so instead of eating the click silently.
|
|
17227
|
+
if (displayMode !== 'screenshot') {
|
|
17228
|
+
log(`Manual screenshot refresh ignored: displayMode=${displayMode}`);
|
|
16008
17229
|
break;
|
|
17230
|
+
}
|
|
16009
17231
|
lastShotHash = '';
|
|
16010
17232
|
if (screenshotTimer) {
|
|
16011
17233
|
clearInterval(screenshotTimer);
|
|
@@ -16018,11 +17240,17 @@ process.on('message', async (raw) => {
|
|
|
16018
17240
|
case 'close': {
|
|
16019
17241
|
log('Close requested');
|
|
16020
17242
|
// destroySession kills tmux session permanently; kill() only detaches.
|
|
16021
|
-
//
|
|
16022
|
-
// process.exit
|
|
16023
|
-
|
|
17243
|
+
// Remote destroySession is an asynchronous cancellation. Explicit close
|
|
17244
|
+
// must therefore use prepare/commit: an immediate process.exit would cut
|
|
17245
|
+
// off the request and leave the remote agent running. A request-less
|
|
17246
|
+
// close on ANY remote backend is refused outright: the legacy path below
|
|
17247
|
+
// calls destroySession() — for Mojo that is `mojo session cancel` — so
|
|
17248
|
+
// letting a generic lifecycle teardown fall through there irreversibly
|
|
17249
|
+
// cancelled the remote session with no close_result to report ok:false
|
|
17250
|
+
// or residuals. Remote retirement must always carry a requestId.
|
|
17251
|
+
if (isRemoteBackendType(effectiveBackendType)) {
|
|
16024
17252
|
if (!msg.requestId) {
|
|
16025
|
-
log(
|
|
17253
|
+
log(`Refused unsafe request-less ${effectiveBackendType} close; explicit close requires prepare/commit`);
|
|
16026
17254
|
break;
|
|
16027
17255
|
}
|
|
16028
17256
|
let result;
|
|
@@ -16050,33 +17278,57 @@ process.on('message', async (raw) => {
|
|
|
16050
17278
|
lastAbortedCloseRequestId = null;
|
|
16051
17279
|
closeRequestInFlightId = msg.requestId;
|
|
16052
17280
|
try {
|
|
16053
|
-
|
|
16054
|
-
|
|
16055
|
-
|
|
16056
|
-
|
|
17281
|
+
if (!backend?.destroySession) {
|
|
17282
|
+
result = { ok: false, error: 'remote_close_unsupported' };
|
|
17283
|
+
}
|
|
17284
|
+
else {
|
|
17285
|
+
result = normalizeDestroyResult(await backend.destroySession(), { remote: isRemoteBackendType(effectiveBackendType) });
|
|
17286
|
+
}
|
|
16057
17287
|
}
|
|
16058
17288
|
catch (err) {
|
|
16059
|
-
|
|
17289
|
+
// A THROWN destroySession leaves the outcome unknown: it may already
|
|
17290
|
+
// have cancelled the remote session or spawned side effects. Defaulting
|
|
17291
|
+
// this to retryable re-opened write admission on a possibly-dead
|
|
17292
|
+
// lineage, so an unknown outcome must fence instead.
|
|
17293
|
+
result = {
|
|
17294
|
+
ok: false,
|
|
17295
|
+
error: err instanceof Error ? err.message : String(err),
|
|
17296
|
+
recovery: 'uncertain',
|
|
17297
|
+
// Stated explicitly: an unknown outcome must fence writes even if a
|
|
17298
|
+
// later reader decides the close itself may be retried.
|
|
17299
|
+
admission: 'fenced',
|
|
17300
|
+
};
|
|
16060
17301
|
}
|
|
16061
17302
|
}
|
|
16062
17303
|
if (result.ok) {
|
|
16063
17304
|
preparedCloseRequestId = msg.requestId;
|
|
16064
17305
|
}
|
|
16065
17306
|
else if (attemptedPrepare) {
|
|
16066
|
-
|
|
16067
|
-
|
|
17307
|
+
// Two separate questions, deliberately not collapsed: `recovery` says
|
|
17308
|
+
// whether the CLOSE may be retried, `admission` says whether WRITES may
|
|
17309
|
+
// be admitted again — mayRestoreWriteAdmission answers only the second.
|
|
17310
|
+
// Keying this on `recovery` re-opened writes on an unproven-local-child
|
|
17311
|
+
// close (retryable, yet a credentialed process may still be alive), and
|
|
17312
|
+
// aborting on every ok:false additionally re-opened writes on a lineage
|
|
17313
|
+
// already cancelled remotely.
|
|
17314
|
+
if (mayRestoreWriteAdmission(result)) {
|
|
17315
|
+
await backend?.abortDestroySession?.();
|
|
17316
|
+
lastAbortedCloseRequestId = msg.requestId;
|
|
17317
|
+
}
|
|
17318
|
+
else {
|
|
17319
|
+
log(`${effectiveBackendType} close prepare failed as `
|
|
17320
|
+
+ `${result.recovery ?? 'retryable'}/`
|
|
17321
|
+
+ `${result.admission ?? 'derived'}; write admission stays fenced `
|
|
17322
|
+
+ '(session remains active for retry)');
|
|
17323
|
+
}
|
|
16068
17324
|
}
|
|
16069
17325
|
if (closeRequestInFlightId === msg.requestId)
|
|
16070
17326
|
closeRequestInFlightId = null;
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16074
|
-
ok: result.ok,
|
|
16075
|
-
...(result.taskId ? { taskId: result.taskId } : {}),
|
|
16076
|
-
...(result.error ? { error: result.error } : {}),
|
|
16077
|
-
});
|
|
17327
|
+
// Built by the shared helper so the payload (notably `recovery`, which the
|
|
17328
|
+
// daemon needs to decide whether a rollback is legal) is covered by tests.
|
|
17329
|
+
send(buildCloseResultMessage(msg.requestId, result));
|
|
16078
17330
|
if (!result.ok) {
|
|
16079
|
-
log(
|
|
17331
|
+
log(`${effectiveBackendType} close prepare failed (${result.error ?? 'cancel failed'}); session stays active for retry`);
|
|
16080
17332
|
}
|
|
16081
17333
|
break;
|
|
16082
17334
|
}
|
|
@@ -16095,8 +17347,10 @@ process.on('message', async (raw) => {
|
|
|
16095
17347
|
stopScreenshotLoop();
|
|
16096
17348
|
stopBridgeWatcher();
|
|
16097
17349
|
stopCodexBridge();
|
|
16098
|
-
// Local close
|
|
16099
|
-
//
|
|
17350
|
+
// Local close destroys persistent owned sessions. Remote backends never
|
|
17351
|
+
// reach here: the branch above fences them all (request-less remote close
|
|
17352
|
+
// is refused; with a requestId it goes through prepare/commit), so
|
|
17353
|
+
// destroySession() below only ever tears down local backing sessions.
|
|
16100
17354
|
const closeTeardown = backend?.destroySession?.();
|
|
16101
17355
|
if (closeTeardown && typeof closeTeardown.then === 'function') {
|
|
16102
17356
|
try {
|
|
@@ -16134,28 +17388,28 @@ process.on('message', async (raw) => {
|
|
|
16134
17388
|
// We still request a clean exit; the daemon SIGKILL is the hard backstop.
|
|
16135
17389
|
process.exit(0);
|
|
16136
17390
|
}
|
|
16137
|
-
case '
|
|
16138
|
-
if (effectiveBackendType
|
|
17391
|
+
case 'remote_shutdown_prepare': {
|
|
17392
|
+
if (!isRemoteBackendType(effectiveBackendType)) {
|
|
16139
17393
|
send({
|
|
16140
|
-
type: '
|
|
17394
|
+
type: 'remote_shutdown_result',
|
|
16141
17395
|
requestId: msg.requestId,
|
|
16142
17396
|
phase: 'prepare',
|
|
16143
17397
|
ok: false,
|
|
16144
17398
|
taskId: null,
|
|
16145
|
-
error: '
|
|
17399
|
+
error: 'not_remote_backend',
|
|
16146
17400
|
});
|
|
16147
17401
|
break;
|
|
16148
17402
|
}
|
|
16149
17403
|
let result;
|
|
16150
|
-
const inputBlocker =
|
|
17404
|
+
const inputBlocker = remoteWorkerShutdownInputBlocker({
|
|
16151
17405
|
initPromptMaterialized,
|
|
16152
17406
|
isFlushing,
|
|
16153
17407
|
pendingMessages: pendingMessages.length,
|
|
16154
17408
|
pendingRawInputs: pendingRawInputs.length,
|
|
16155
17409
|
pendingSessionRename: pendingSessionRename !== null,
|
|
16156
17410
|
// sessionRenameInFlight became a function in this branch's rename
|
|
16157
|
-
// lifecycle rework;
|
|
16158
|
-
// the
|
|
17411
|
+
// lifecycle rework; remoteWorkerShutdownInputBlocker (added on master by
|
|
17412
|
+
// the original Riff two-phase shutdown) still consumes it as a boolean field.
|
|
16159
17413
|
sessionRenameInFlight: sessionRenameInFlight(),
|
|
16160
17414
|
commandLineWritesPending,
|
|
16161
17415
|
});
|
|
@@ -16195,7 +17449,7 @@ process.on('message', async (raw) => {
|
|
|
16195
17449
|
}
|
|
16196
17450
|
}
|
|
16197
17451
|
send({
|
|
16198
|
-
type: '
|
|
17452
|
+
type: 'remote_shutdown_result',
|
|
16199
17453
|
requestId: msg.requestId,
|
|
16200
17454
|
phase: 'prepare',
|
|
16201
17455
|
ok: result.ok,
|
|
@@ -16204,14 +17458,14 @@ process.on('message', async (raw) => {
|
|
|
16204
17458
|
});
|
|
16205
17459
|
break;
|
|
16206
17460
|
}
|
|
16207
|
-
case '
|
|
16208
|
-
if (effectiveBackendType
|
|
17461
|
+
case 'remote_shutdown_commit': {
|
|
17462
|
+
if (!isRemoteBackendType(effectiveBackendType)
|
|
16209
17463
|
|| shutdownDetachRequestId !== msg.requestId
|
|
16210
17464
|
|| shutdownDetachPhase !== 'prepared') {
|
|
16211
|
-
log(`Ignoring stale
|
|
17465
|
+
log(`Ignoring stale remote shutdown commit ${msg.requestId}`);
|
|
16212
17466
|
break;
|
|
16213
17467
|
}
|
|
16214
|
-
log(`
|
|
17468
|
+
log(`Remote shutdown detach committed (${msg.requestId})`);
|
|
16215
17469
|
shutdownDetachRequestId = null;
|
|
16216
17470
|
shutdownDetachPhase = null;
|
|
16217
17471
|
backend?.commitShutdownDetach?.();
|
|
@@ -16221,11 +17475,11 @@ process.on('message', async (raw) => {
|
|
|
16221
17475
|
cleanup();
|
|
16222
17476
|
process.exit(0);
|
|
16223
17477
|
}
|
|
16224
|
-
case '
|
|
17478
|
+
case 'remote_shutdown_abort': {
|
|
16225
17479
|
if (shutdownDetachRequestId !== msg.requestId) {
|
|
16226
|
-
log(`Ignoring stale
|
|
17480
|
+
log(`Ignoring stale remote shutdown abort ${msg.requestId}`);
|
|
16227
17481
|
send({
|
|
16228
|
-
type: '
|
|
17482
|
+
type: 'remote_shutdown_result',
|
|
16229
17483
|
requestId: msg.requestId,
|
|
16230
17484
|
phase: 'abort',
|
|
16231
17485
|
ok: false,
|
|
@@ -16234,7 +17488,7 @@ process.on('message', async (raw) => {
|
|
|
16234
17488
|
});
|
|
16235
17489
|
break;
|
|
16236
17490
|
}
|
|
16237
|
-
log(`
|
|
17491
|
+
log(`Remote shutdown detach aborted (${msg.requestId})`);
|
|
16238
17492
|
let result;
|
|
16239
17493
|
try {
|
|
16240
17494
|
result = await backend?.abortShutdownDetach?.()
|
|
@@ -16252,7 +17506,7 @@ process.on('message', async (raw) => {
|
|
|
16252
17506
|
shutdownDetachPhase = null;
|
|
16253
17507
|
}
|
|
16254
17508
|
send({
|
|
16255
|
-
type: '
|
|
17509
|
+
type: 'remote_shutdown_result',
|
|
16256
17510
|
requestId: msg.requestId,
|
|
16257
17511
|
phase: 'abort',
|
|
16258
17512
|
ok: result.ok,
|
|
@@ -16299,17 +17553,33 @@ process.on('message', async (raw) => {
|
|
|
16299
17553
|
});
|
|
16300
17554
|
break;
|
|
16301
17555
|
}
|
|
16302
|
-
log(`Close aborted (${msg.requestId});
|
|
17556
|
+
log(`Close aborted (${msg.requestId}); remote admission restore requested`);
|
|
16303
17557
|
let abortError = null;
|
|
17558
|
+
// A REFUSED rollback is not an error, and it is not a success either. The
|
|
17559
|
+
// backend may be holding a latched fence (an unproven local subtree, an
|
|
17560
|
+
// unnamed remote session), and the daemon used to infer "admission restored"
|
|
17561
|
+
// from a call that merely did not throw — persisting a cleared fence while
|
|
17562
|
+
// write() kept returning false.
|
|
17563
|
+
let admissionRestored = true;
|
|
17564
|
+
let fenceReason;
|
|
16304
17565
|
if (!alreadyRestored) {
|
|
16305
17566
|
try {
|
|
16306
|
-
await backend?.abortDestroySession?.();
|
|
17567
|
+
const outcome = interpretAbortOutcome(await backend?.abortDestroySession?.());
|
|
17568
|
+
admissionRestored = outcome.admissionRestored;
|
|
17569
|
+
fenceReason = outcome.reason;
|
|
16307
17570
|
}
|
|
16308
17571
|
catch (err) {
|
|
16309
17572
|
abortError = err instanceof Error ? err.message : String(err);
|
|
16310
17573
|
}
|
|
16311
17574
|
}
|
|
16312
|
-
if (!abortError) {
|
|
17575
|
+
if (!abortError && !admissionRestored) {
|
|
17576
|
+
log(`Close abort ${msg.requestId} did NOT restore write admission `
|
|
17577
|
+
+ `(${fenceReason ?? 'still fenced'}); this session will not accept writes again \u2014 retry the close`);
|
|
17578
|
+
}
|
|
17579
|
+
// Only clear the close bookkeeping when admission really came back. Leaving
|
|
17580
|
+
// it set on a still-fenced session keeps a later close_abort from being
|
|
17581
|
+
// rejected as stale while the fence is what actually needs resolving.
|
|
17582
|
+
if (!abortError && admissionRestored) {
|
|
16313
17583
|
preparedCloseRequestId = null;
|
|
16314
17584
|
closeRequestInFlightId = null;
|
|
16315
17585
|
lastAbortedCloseRequestId = null;
|
|
@@ -16318,6 +17588,10 @@ process.on('message', async (raw) => {
|
|
|
16318
17588
|
type: 'close_abort_result',
|
|
16319
17589
|
requestId: msg.requestId,
|
|
16320
17590
|
ok: abortError === null,
|
|
17591
|
+
// Distinct from `ok`: the abort was handled, but writes may still be
|
|
17592
|
+
// fenced. The daemon must persist THIS, not `ok`.
|
|
17593
|
+
admissionRestored: abortError === null && admissionRestored,
|
|
17594
|
+
...(fenceReason ? { fenceReason } : {}),
|
|
16321
17595
|
...(abortError ? { error: abortError } : {}),
|
|
16322
17596
|
});
|
|
16323
17597
|
break;
|
|
@@ -16345,7 +17619,14 @@ process.on('message', async (raw) => {
|
|
|
16345
17619
|
// uses to recover sessions after a reboot kills the tmux server).
|
|
16346
17620
|
revokeManagedTurnOriginForRestart();
|
|
16347
17621
|
try {
|
|
16348
|
-
|
|
17622
|
+
// mojo:suspend 语义是「休眠待续」——绝不能 cancel 远端会话(血缘已持久化,
|
|
17623
|
+
// 恢复时 follow-up 续上);只断流 detach。mojo 本来就没有常驻本地进程,
|
|
17624
|
+
// destroySession 想省的那份 CLI 内存并不存在。
|
|
17625
|
+
// riff 已在本 case 开头被直接拒绝(需 prepare/commit),所以这里只剩 mojo。
|
|
17626
|
+
if (isRemoteBackendType(effectiveBackendType))
|
|
17627
|
+
backend?.kill();
|
|
17628
|
+
else
|
|
17629
|
+
(backend?.destroySession ?? backend?.kill)?.call(backend);
|
|
16349
17630
|
}
|
|
16350
17631
|
catch { /* best-effort */ }
|
|
16351
17632
|
backend = null;
|