@songsid/agend 2.3.4-beta.7 → 2.3.5-alpha.3
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.md +2 -0
- package/README.zh-TW.md +2 -0
- package/THIRD_PARTY_NOTICES.md +53 -0
- package/dist/agent-endpoint.d.ts +5 -1
- package/dist/agent-endpoint.js +12 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.d.ts +21 -0
- package/dist/auth-api.js +89 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/web-authority.d.ts +12 -2
- package/dist/authority/web-authority.js +25 -4
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/claude-code.d.ts +15 -12
- package/dist/backend/claude-code.js +152 -133
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-package.d.ts +5 -2
- package/dist/backend/codex-package.js +1 -5
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex.d.ts +136 -113
- package/dist/backend/codex.js +1281 -1219
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok-model-cache.d.ts +7 -0
- package/dist/backend/grok-model-cache.js +65 -0
- package/dist/backend/grok-model-cache.js.map +1 -0
- package/dist/backend/grok.d.ts +2 -0
- package/dist/backend/grok.js +22 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/native-pane.d.ts +9 -0
- package/dist/backend/native-pane.js +59 -0
- package/dist/backend/native-pane.js.map +1 -0
- package/dist/backend/types.d.ts +7 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +2 -0
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +71 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +16 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +67 -5
- package/dist/daemon.js +823 -266
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +15 -3
- package/dist/delivery-consumed-watch.js +23 -1
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-outbox.d.ts +5 -0
- package/dist/delivery-outbox.js +32 -4
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +2 -35
- package/dist/delivery-reconciliation.js +2 -156
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -2
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +2 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +238 -3
- package/dist/fleet-manager.js +1826 -104
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-windows-runtime.d.ts +92 -0
- package/dist/fleet-windows-runtime.js +380 -0
- package/dist/fleet-windows-runtime.js.map +1 -0
- package/dist/general-binding.d.ts +85 -0
- package/dist/general-binding.js +298 -0
- package/dist/general-binding.js.map +1 -0
- package/dist/general-host-owner.d.ts +8 -0
- package/dist/general-host-owner.js +26 -0
- package/dist/general-host-owner.js.map +1 -0
- package/dist/general-instance.d.ts +7 -0
- package/dist/general-instance.js +20 -3
- package/dist/general-instance.js.map +1 -1
- package/dist/general-role.d.ts +5 -0
- package/dist/general-role.js +26 -0
- package/dist/general-role.js.map +1 -0
- package/dist/instance-config-impact.js +4 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +21 -3
- package/dist/instance-lifecycle.js +378 -162
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +6 -0
- package/dist/instructions.js.map +1 -1
- package/dist/local-pane-endpoint.d.ts +46 -0
- package/dist/local-pane-endpoint.js +189 -0
- package/dist/local-pane-endpoint.js.map +1 -0
- package/dist/local-pane-launcher.d.ts +2 -0
- package/dist/local-pane-launcher.js +38 -0
- package/dist/local-pane-launcher.js.map +1 -0
- package/dist/local-pane-stage.ps1 +101 -0
- package/dist/locale.js +85 -0
- package/dist/locale.js.map +1 -1
- package/dist/model-catalog-defaults.d.ts +14 -0
- package/dist/model-catalog-defaults.js +20 -0
- package/dist/model-catalog-defaults.js.map +1 -0
- package/dist/native-agent-admission.d.ts +7 -0
- package/dist/native-agent-admission.js +26 -0
- package/dist/native-agent-admission.js.map +1 -0
- package/dist/native-daemon-client.d.ts +13 -0
- package/dist/native-daemon-client.js +28 -0
- package/dist/native-daemon-client.js.map +1 -0
- package/dist/native-instance-api.d.ts +14 -0
- package/dist/native-instance-api.js +205 -0
- package/dist/native-instance-api.js.map +1 -0
- package/dist/native-instance-input.d.ts +84 -0
- package/dist/native-instance-input.js +54 -0
- package/dist/native-instance-input.js.map +1 -0
- package/dist/native-instance-registry.d.ts +32 -0
- package/dist/native-instance-registry.js +105 -0
- package/dist/native-instance-registry.js.map +1 -0
- package/dist/native-mcp.d.ts +40 -0
- package/dist/native-mcp.js +278 -0
- package/dist/native-mcp.js.map +1 -0
- package/dist/native-model-catalog.d.ts +5 -0
- package/dist/native-model-catalog.js +76 -0
- package/dist/native-model-catalog.js.map +1 -0
- package/dist/native-pane-crash-budget.d.ts +20 -0
- package/dist/native-pane-crash-budget.js +60 -0
- package/dist/native-pane-crash-budget.js.map +1 -0
- package/dist/native-pane-create.d.ts +26 -0
- package/dist/native-pane-create.js +53 -0
- package/dist/native-pane-create.js.map +1 -0
- package/dist/native-pane-lifecycle.d.ts +65 -0
- package/dist/native-pane-lifecycle.js +230 -0
- package/dist/native-pane-lifecycle.js.map +1 -0
- package/dist/native-pane-manager.d.ts +59 -0
- package/dist/native-pane-manager.js +584 -0
- package/dist/native-pane-manager.js.map +1 -0
- package/dist/native-pane-output-log.d.ts +15 -0
- package/dist/native-pane-output-log.js +108 -0
- package/dist/native-pane-output-log.js.map +1 -0
- package/dist/native-pane-reconciliation.d.ts +4 -0
- package/dist/native-pane-reconciliation.js +40 -0
- package/dist/native-pane-reconciliation.js.map +1 -0
- package/dist/native-pane-recovery.d.ts +12 -0
- package/dist/native-pane-recovery.js +10 -0
- package/dist/native-pane-recovery.js.map +1 -0
- package/dist/native-pane-tools.d.ts +2 -0
- package/dist/native-pane-tools.js +14 -0
- package/dist/native-pane-tools.js.map +1 -0
- package/dist/needs-you-hub.d.ts +2 -0
- package/dist/needs-you-hub.js +2 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +19 -1
- package/dist/needs-you.js +12 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +16 -2
- package/dist/outbound-handlers.js +67 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +38 -0
- package/dist/outbound-schemas.js +12 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-port.d.ts +118 -0
- package/dist/pane-port.js +2 -0
- package/dist/pane-port.js.map +1 -0
- package/dist/public-web-gateway.js +9 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +5 -0
- package/dist/public-web-link.js +4 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.js +9 -5
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +4 -1
- package/dist/quickstart.js.map +1 -1
- package/dist/runtime-api.d.ts +8 -0
- package/dist/runtime-api.js +96 -0
- package/dist/runtime-api.js.map +1 -0
- package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +21 -0
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +2 -0
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +19 -0
- package/dist/runtime-assets/@xterm/addon-serialize/package.json +29 -0
- package/dist/runtime-assets/@xterm/headless/LICENSE +21 -0
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +2 -0
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
- package/dist/runtime-assets/@xterm/headless/package.json +29 -0
- package/dist/runtime-assets/node-pty/CONPTY-LICENSE +21 -0
- package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +503 -0
- package/dist/runtime-assets/node-pty/LICENSE +69 -0
- package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +16 -0
- package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/runtime-assets/node-pty/lib/index.js +52 -0
- package/dist/runtime-assets/node-pty/lib/interfaces.js +7 -0
- package/dist/runtime-assets/node-pty/lib/shared/conout.js +11 -0
- package/dist/runtime-assets/node-pty/lib/terminal.js +190 -0
- package/dist/runtime-assets/node-pty/lib/types.js +7 -0
- package/dist/runtime-assets/node-pty/lib/unixTerminal.js +346 -0
- package/dist/runtime-assets/node-pty/lib/utils.js +39 -0
- package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +125 -0
- package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +320 -0
- package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +199 -0
- package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/runtime-assets/node-pty/package.json +64 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/dist/runtime-bundle.d.ts +17 -0
- package/dist/runtime-bundle.js +150 -0
- package/dist/runtime-bundle.js.map +1 -0
- package/dist/runtime-cli.d.ts +2 -0
- package/dist/runtime-cli.js +117 -0
- package/dist/runtime-cli.js.map +1 -0
- package/dist/runtime-client-store.d.ts +49 -0
- package/dist/runtime-client-store.js +292 -0
- package/dist/runtime-client-store.js.map +1 -0
- package/dist/runtime-client.d.ts +100 -0
- package/dist/runtime-client.js +700 -0
- package/dist/runtime-client.js.map +1 -0
- package/dist/runtime-coordinator.d.ts +82 -0
- package/dist/runtime-coordinator.js +864 -0
- package/dist/runtime-coordinator.js.map +1 -0
- package/dist/runtime-general-protocol.d.ts +22 -0
- package/dist/runtime-general-protocol.js +57 -0
- package/dist/runtime-general-protocol.js.map +1 -0
- package/dist/runtime-general-workspace.d.ts +17 -0
- package/dist/runtime-general-workspace.js +213 -0
- package/dist/runtime-general-workspace.js.map +1 -0
- package/dist/runtime-pane-assets.d.ts +17 -0
- package/dist/runtime-pane-assets.js +23 -0
- package/dist/runtime-pane-assets.js.map +1 -0
- package/dist/runtime-pane-boot.d.ts +2 -0
- package/dist/runtime-pane-boot.js +24 -0
- package/dist/runtime-pane-boot.js.map +1 -0
- package/dist/runtime-pane-codex-index.d.ts +1 -0
- package/dist/runtime-pane-codex-index.js +47 -0
- package/dist/runtime-pane-codex-index.js.map +1 -0
- package/dist/runtime-pane-config.d.ts +16 -0
- package/dist/runtime-pane-config.js +54 -0
- package/dist/runtime-pane-config.js.map +1 -0
- package/dist/runtime-pane-host.d.ts +44 -0
- package/dist/runtime-pane-host.js +424 -0
- package/dist/runtime-pane-host.js.map +1 -0
- package/dist/runtime-pane-job-owner.d.ts +1 -0
- package/dist/runtime-pane-job-owner.js +29 -0
- package/dist/runtime-pane-job-owner.js.map +1 -0
- package/dist/runtime-pane-job.ps1 +116 -0
- package/dist/runtime-pane-link.d.ts +40 -0
- package/dist/runtime-pane-link.js +246 -0
- package/dist/runtime-pane-link.js.map +1 -0
- package/dist/runtime-pane-mcp-protocol.d.ts +32 -0
- package/dist/runtime-pane-mcp-protocol.js +39 -0
- package/dist/runtime-pane-mcp-protocol.js.map +1 -0
- package/dist/runtime-pane-mcp-relay.d.ts +17 -0
- package/dist/runtime-pane-mcp-relay.js +70 -0
- package/dist/runtime-pane-mcp-relay.js.map +1 -0
- package/dist/runtime-pane-mcp.d.ts +51 -0
- package/dist/runtime-pane-mcp.js +150 -0
- package/dist/runtime-pane-mcp.js.map +1 -0
- package/dist/runtime-pane-native.d.ts +45 -0
- package/dist/runtime-pane-native.js +65 -0
- package/dist/runtime-pane-native.js.map +1 -0
- package/dist/runtime-pane-output.d.ts +19 -0
- package/dist/runtime-pane-output.js +53 -0
- package/dist/runtime-pane-output.js.map +1 -0
- package/dist/runtime-pane-port.d.ts +139 -0
- package/dist/runtime-pane-port.js +558 -0
- package/dist/runtime-pane-port.js.map +1 -0
- package/dist/runtime-pane-private.d.ts +14 -0
- package/dist/runtime-pane-private.js +51 -0
- package/dist/runtime-pane-private.js.map +1 -0
- package/dist/runtime-pane-private.ps1 +14 -0
- package/dist/runtime-pane-process.d.ts +4 -0
- package/dist/runtime-pane-process.js +108 -0
- package/dist/runtime-pane-process.js.map +1 -0
- package/dist/runtime-pane-protocol.d.ts +172 -0
- package/dist/runtime-pane-protocol.js +195 -0
- package/dist/runtime-pane-protocol.js.map +1 -0
- package/dist/runtime-pane-screen.d.ts +19 -0
- package/dist/runtime-pane-screen.js +76 -0
- package/dist/runtime-pane-screen.js.map +1 -0
- package/dist/runtime-pane-service.d.ts +11 -0
- package/dist/runtime-pane-service.js +221 -0
- package/dist/runtime-pane-service.js.map +1 -0
- package/dist/runtime-pane-stdio-protocol.d.ts +54 -0
- package/dist/runtime-pane-stdio-protocol.js +94 -0
- package/dist/runtime-pane-stdio-protocol.js.map +1 -0
- package/dist/runtime-pane-stdio.d.ts +3 -0
- package/dist/runtime-pane-stdio.js +123 -0
- package/dist/runtime-pane-stdio.js.map +1 -0
- package/dist/runtime-pane-store.d.ts +33 -0
- package/dist/runtime-pane-store.js +164 -0
- package/dist/runtime-pane-store.js.map +1 -0
- package/dist/runtime-pane-transcript-protocol.d.ts +43 -0
- package/dist/runtime-pane-transcript-protocol.js +58 -0
- package/dist/runtime-pane-transcript-protocol.js.map +1 -0
- package/dist/runtime-pane-transcript.d.ts +45 -0
- package/dist/runtime-pane-transcript.js +605 -0
- package/dist/runtime-pane-transcript.js.map +1 -0
- package/dist/runtime-pane-worker.d.ts +1 -0
- package/dist/runtime-pane-worker.js +154 -0
- package/dist/runtime-pane-worker.js.map +1 -0
- package/dist/runtime-protocol.d.ts +149 -0
- package/dist/runtime-protocol.js +19 -0
- package/dist/runtime-protocol.js.map +1 -0
- package/dist/runtime-switches.d.ts +79 -0
- package/dist/runtime-switches.js +90 -0
- package/dist/runtime-switches.js.map +1 -0
- package/dist/runtime-transcript-delta-reader.d.ts +23 -0
- package/dist/runtime-transcript-delta-reader.js +74 -0
- package/dist/runtime-transcript-delta-reader.js.map +1 -0
- package/dist/runtime-transcript-source.d.ts +36 -0
- package/dist/runtime-transcript-source.js +189 -0
- package/dist/runtime-transcript-source.js.map +1 -0
- package/dist/runtime-windows-bridge.d.ts +41 -0
- package/dist/runtime-windows-bridge.js +157 -0
- package/dist/runtime-windows-bridge.js.map +1 -0
- package/dist/settings-api.d.ts +5 -0
- package/dist/settings-api.js +95 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +1 -0
- package/dist/settings-baseline.js +1 -1
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.js +28 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +16 -1
- package/dist/settings-confirmation.js +47 -9
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.d.ts +4 -0
- package/dist/settings-effect.js +58 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +1 -0
- package/dist/settings-http-confirmation.js +15 -2
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-request-capability.d.ts +2 -0
- package/dist/settings-request-capability.js +4 -0
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/settings-route-lifetime.d.ts +14 -0
- package/dist/settings-route-lifetime.js +48 -0
- package/dist/settings-route-lifetime.js.map +1 -0
- package/dist/settings-transaction.d.ts +4 -0
- package/dist/settings-transaction.js +9 -0
- package/dist/settings-transaction.js.map +1 -1
- package/dist/setup-form.js +6 -3
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +3 -0
- package/dist/setup-i18n.js.map +1 -1
- package/dist/tmux-control.d.ts +2 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +2 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-pane-host.d.ts +26 -0
- package/dist/tmux-pane-host.js +25 -0
- package/dist/tmux-pane-host.js.map +1 -0
- package/dist/tool-permissions.js +2 -1
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +3 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +1 -1
- package/dist/transcript-delta-reader.js +1 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-monitor.d.ts +3 -0
- package/dist/transcript-monitor.js +22 -46
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-needles.d.ts +35 -0
- package/dist/transcript-needles.js +158 -0
- package/dist/transcript-needles.js.map +1 -0
- package/dist/transcript-records.d.ts +21 -0
- package/dist/transcript-records.js +140 -0
- package/dist/transcript-records.js.map +1 -0
- package/dist/transcript-sources.d.ts +5 -0
- package/dist/transcript-sources.js +8 -79
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/ui/agent-proposals.js +90 -0
- package/dist/ui/agent-runtime.js +106 -0
- package/dist/ui/chat-store.js +13 -2
- package/dist/ui/chat-strings.js +48 -0
- package/dist/ui/cli-picker.js +42 -0
- package/dist/ui/general-location.js +71 -0
- package/dist/ui/native-instance-settings.js +47 -0
- package/dist/ui/native-pane-recovery.js +45 -0
- package/dist/ui/panel-chat.js +63 -32
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +99 -18
- package/dist/ui/panel-needs.js +154 -3
- package/dist/ui/panel-runtimes.js +231 -0
- package/dist/ui/panel-settings.js +19 -9
- package/dist/ui/panel-windows-agent.js +86 -0
- package/dist/ui/panel-windows-cli.js +143 -0
- package/dist/ui/provider-marks.js +83 -0
- package/dist/ui/runtime-catalog.js +55 -0
- package/dist/ui/settings-catalog.js +37 -10
- package/dist/ui/settings-confirm.js +3 -1
- package/dist/ui/settings-dialogs.js +22 -7
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-outside.js +176 -0
- package/dist/ui/settings-registry.js +6 -0
- package/dist/ui/settings-strings.js +16 -0
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-route.js +6 -2
- package/dist/ui/shared/app-shell.js +3 -2
- package/dist/ui/shared/app-store.js +1 -0
- package/dist/ui/shared/app-stream.js +3 -1
- package/dist/ui/shared/app.css +301 -0
- package/dist/ui/shared/app.js +4 -0
- package/dist/ui/shared/header-tools.js +6 -4
- package/dist/ui/shared/instance-nav.js +33 -7
- package/dist/ui/shared/instance-switch.js +4 -3
- package/dist/ui/shared/qrcode.module.js +2237 -0
- package/dist/ui/shared/signin.js +39 -5
- package/dist/ui/shared/ui-menu.js +28 -8
- package/dist/ui/shared/vendor/qrcode-generator.LICENSE +27 -0
- package/dist/ui/shared/vendor/vendor.json +19 -2
- package/dist/ui/shared/view-strings.js +8 -0
- package/dist/ui/windows-agents-nav.js +2 -0
- package/dist/ui/windows-cli-catalog.js +79 -0
- package/dist/web-api.d.ts +37 -0
- package/dist/web-api.js +248 -6
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +19 -2
- package/dist/web-chat-history.js +72 -3
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-only-fleet.d.ts +20 -0
- package/dist/web-only-fleet.js +21 -0
- package/dist/web-only-fleet.js.map +1 -1
- package/dist/web-remote-access.d.ts +152 -0
- package/dist/web-remote-access.js +368 -0
- package/dist/web-remote-access.js.map +1 -0
- package/dist/web-session.d.ts +7 -0
- package/dist/web-session.js +2 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -1
- package/dist/web-shell-routes.js +7 -2
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/windows-agents.d.ts +124 -0
- package/dist/windows-agents.js +487 -0
- package/dist/windows-agents.js.map +1 -0
- package/dist/windows-cli-api.d.ts +23 -0
- package/dist/windows-cli-api.js +158 -0
- package/dist/windows-cli-api.js.map +1 -0
- package/dist/windows-cli-providers.d.ts +115 -0
- package/dist/windows-cli-providers.js +429 -0
- package/dist/windows-cli-providers.js.map +1 -0
- package/dist/windows-cli-worker.d.ts +58 -0
- package/dist/windows-cli-worker.js +479 -0
- package/dist/windows-cli-worker.js.map +1 -0
- package/dist/windows-cli.d.ts +115 -0
- package/dist/windows-cli.js +402 -0
- package/dist/windows-cli.js.map +1 -0
- package/dist/windows-native-mcp.d.ts +19 -0
- package/dist/windows-native-mcp.js +143 -0
- package/dist/windows-native-mcp.js.map +1 -0
- package/dist/windows-pane-mcp.d.ts +26 -0
- package/dist/windows-pane-mcp.js +187 -0
- package/dist/windows-pane-mcp.js.map +1 -0
- package/package.json +9 -3
- package/templates/runtime-assets/conpty-LICENSE +21 -0
- package/templates/runtime-assets/conpty-NOTICE.md +503 -0
- package/templates/runtime-assets/local-pane-stage.ps1 +101 -0
- package/templates/runtime-assets/runtime-pane-job.ps1 +116 -0
- package/templates/runtime-assets/runtime-pane-private.ps1 +14 -0
- package/templates/runtime-assets/xterm-LICENSE +21 -0
package/dist/daemon.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { nativePaneToolRefusal } from "./native-pane-tools.js";
|
|
2
|
+
import { generalByName } from "./general-instance.js";
|
|
1
3
|
import { dialogScreenText } from "./dialog-screen-text.js";
|
|
2
4
|
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
3
5
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
@@ -20,7 +22,7 @@ import { assertNoSessionOomStop, readSessionOomStop, writeSessionOomStop, Sessio
|
|
|
20
22
|
import { mcpServerState } from "./mcp-liveness.js";
|
|
21
23
|
import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
22
24
|
import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
23
|
-
import {
|
|
25
|
+
import { resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
24
26
|
import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
25
27
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
26
28
|
import { TranscriptTurnLedger } from "./transcript-turns.js";
|
|
@@ -48,6 +50,7 @@ import { HangDetector } from "./hang-detector.js";
|
|
|
48
50
|
import { writeSecretFile } from "./secret-file.js";
|
|
49
51
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
50
52
|
import { answerRemoteCard } from "./remote-card-answer.js";
|
|
53
|
+
import { tmuxPaneHost } from "./tmux-pane-host.js";
|
|
51
54
|
import { buildFleetInstructions } from "./instructions.js";
|
|
52
55
|
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
53
56
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
@@ -57,6 +60,7 @@ import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT
|
|
|
57
60
|
import { t } from "./locale.js";
|
|
58
61
|
import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
|
|
59
62
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
63
|
+
import { reconcileNativePaneTarget } from "./native-pane-reconciliation.js";
|
|
60
64
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
61
65
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
62
66
|
import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
|
|
@@ -1112,6 +1116,7 @@ export class Daemon extends EventEmitter {
|
|
|
1112
1116
|
spawnGate;
|
|
1113
1117
|
stormWindow;
|
|
1114
1118
|
backendOutage;
|
|
1119
|
+
paneTransport;
|
|
1115
1120
|
/** #1314: fleet-directory fallbacks already reported to the topic (once per daemon, per field and path). */
|
|
1116
1121
|
promptFileFallbackNoticed = new Set();
|
|
1117
1122
|
/** Identity of this live Daemon object; changes on object stop/restart. */
|
|
@@ -1943,9 +1948,87 @@ export class Daemon extends EventEmitter {
|
|
|
1943
1948
|
this.errorRecoveryDeadlineAt = 0;
|
|
1944
1949
|
this.activeErrorPatternKey = null;
|
|
1945
1950
|
}
|
|
1951
|
+
/** Lazy default also preserves bare-prototype diagnostic/test construction. */
|
|
1952
|
+
get paneHost() { return this.paneTransport ?? tmuxPaneHost; }
|
|
1953
|
+
nativePaneLifecycle;
|
|
1954
|
+
nativeFleetBridge;
|
|
1955
|
+
nativeTranscriptSource;
|
|
1956
|
+
usesNativePane() { return !!this.nativePaneLifecycle; }
|
|
1957
|
+
logNativeInputRefusal(details) {
|
|
1958
|
+
this.logger.warn({ component: "runtime-pane-host", reason: "expected_seq_changed", ...details }, "Native pane input refused before write");
|
|
1959
|
+
}
|
|
1960
|
+
logNativeToolFailure(details) {
|
|
1961
|
+
const turn = this.turnReplyGuard.snapshot();
|
|
1962
|
+
this.logger.warn({ ...details, currentMessageId: turn?.target.messageId ?? null,
|
|
1963
|
+
guardGeneration: turn?.generation ?? null, guardPhase: turn?.phase ?? null }, "Native pane tool operation failed");
|
|
1964
|
+
}
|
|
1965
|
+
nativePaneCurrent() { return this.nativePaneLifecycle?.current() ?? false; }
|
|
1966
|
+
nativeEnterHold = null;
|
|
1967
|
+
/** Per-launch attention for a native delivery whose bounded input attempts were refused or uncertain, including non-outbox input. */
|
|
1968
|
+
nativeDeliveryHold() {
|
|
1969
|
+
const hold = this.nativeEnterHold;
|
|
1970
|
+
return hold && this.nativePaneLifecycle && !this.deliveryWritesStopping && hold.spawn === this.spawnGeneration && hold.fence === this.launchFenceEpoch
|
|
1971
|
+
? { key: hold.key, since: hold.since } : null;
|
|
1972
|
+
}
|
|
1973
|
+
nativePaneHasExited() {
|
|
1974
|
+
const snapshot = this.nativePaneLifecycle?.snapshot?.();
|
|
1975
|
+
return snapshot?.state === "exited" && snapshot.instance === this.name && this.tmux?.getWindowId() === snapshot.paneId;
|
|
1976
|
+
}
|
|
1977
|
+
/** Positive native exit evidence only, fenced to this exact launch and Daemon owner. */
|
|
1978
|
+
nativePaneExitCurrent(snapshot, owner) {
|
|
1979
|
+
const actual = this.nativePaneLifecycle?.snapshot?.();
|
|
1980
|
+
return !!actual && actual.state === "exited" && snapshot.state === "exited"
|
|
1981
|
+
&& sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.startupAborted
|
|
1982
|
+
&& ["hostBootId", "paneId", "launchId", "agentId", "instance", "sessionId", "commandSequence", "exitCode"].every(key => actual[key] === snapshot[key])
|
|
1983
|
+
&& this.tmux?.getWindowId() === snapshot.paneId;
|
|
1984
|
+
}
|
|
1985
|
+
async reconcileNativeDeliveries(outbox, current) {
|
|
1986
|
+
if (!this.nativePaneLifecycle)
|
|
1987
|
+
return false;
|
|
1988
|
+
return reconcileNativePaneTarget(outbox, this.name, this.nativePaneLifecycle, current);
|
|
1989
|
+
}
|
|
1990
|
+
/** Same-process transport for the owned native daemon; never exposed as a runner RPC. */
|
|
1991
|
+
setNativeFleetBridge(bridge) {
|
|
1992
|
+
if (!this.nativePaneLifecycle || this.nativeFleetBridge?.current())
|
|
1993
|
+
throw Error("Native Fleet bridge requires one bound pane owner");
|
|
1994
|
+
this.nativeFleetBridge = bridge;
|
|
1995
|
+
}
|
|
1996
|
+
/** Fleet admission and generation checks happen before this synchronous handoff. */
|
|
1997
|
+
acceptNativeFleetMessage(message) {
|
|
1998
|
+
if (!this.nativePaneLifecycle || !this.nativeFleetBridge?.current() || this.deliveryWritesStopping || this.runtimeMonitorsFrozen)
|
|
1999
|
+
return false;
|
|
2000
|
+
if (typeof message.type === "string" && FLEET_RESPONSE_TYPES.has(message.type)) {
|
|
2001
|
+
this.routeFleetResponse(message);
|
|
2002
|
+
return true;
|
|
2003
|
+
}
|
|
2004
|
+
const bridge = this.nativeFleetBridge;
|
|
2005
|
+
const current = () => this.nativeFleetBridge === bridge && bridge.current() && !this.deliveryWritesStopping && !this.runtimeMonitorsFrozen;
|
|
2006
|
+
return this.handleFleetMessage(message, reply => { if (current())
|
|
2007
|
+
this.broadcastFleetMessage(reply); }, current);
|
|
2008
|
+
}
|
|
2009
|
+
broadcastFleetMessage(message) {
|
|
2010
|
+
if (this.nativePaneLifecycle) {
|
|
2011
|
+
if (this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen && !this.deliveryWritesStopping)
|
|
2012
|
+
this.nativeFleetBridge.send(message);
|
|
2013
|
+
}
|
|
2014
|
+
else
|
|
2015
|
+
this.ipcServer?.broadcast(message);
|
|
2016
|
+
}
|
|
2017
|
+
fleetTransportAvailable() {
|
|
2018
|
+
return this.nativePaneLifecycle ? !!this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen : !!this.ipcServer;
|
|
2019
|
+
}
|
|
2020
|
+
/** Internal lifecycle injection, before start. Native paths and provider files stay on their runner. */
|
|
2021
|
+
setNativePaneLifecycle(native) {
|
|
2022
|
+
if (this.tmux || this.ipcServer || this.nativePaneLifecycle)
|
|
2023
|
+
throw Error("Native pane adapter must be bound before daemon startup");
|
|
2024
|
+
this.nativePaneLifecycle = native;
|
|
2025
|
+
this.backend = native.backend;
|
|
2026
|
+
this.paneTransport = native.host;
|
|
2027
|
+
this.controlClient = native.observation;
|
|
2028
|
+
}
|
|
1946
2029
|
constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger, runtimeIdentity, spawnGate, stormWindow,
|
|
1947
2030
|
/** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
|
|
1948
|
-
backendOutage) {
|
|
2031
|
+
backendOutage, paneTransport) {
|
|
1949
2032
|
super();
|
|
1950
2033
|
this.name = name;
|
|
1951
2034
|
this.config = config;
|
|
@@ -1957,6 +2040,7 @@ export class Daemon extends EventEmitter {
|
|
|
1957
2040
|
this.spawnGate = spawnGate;
|
|
1958
2041
|
this.stormWindow = stormWindow;
|
|
1959
2042
|
this.backendOutage = backendOutage;
|
|
2043
|
+
this.paneTransport = paneTransport;
|
|
1960
2044
|
// The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
|
|
1961
2045
|
// sharing that object would activate cold changes before the normal restart/apply boundary.
|
|
1962
2046
|
this.config = structuredClone(config);
|
|
@@ -1979,7 +2063,8 @@ export class Daemon extends EventEmitter {
|
|
|
1979
2063
|
this.messageBus.setLogger(this.logger);
|
|
1980
2064
|
// General is the dispatcher — it must stay warm to route messages, so it is
|
|
1981
2065
|
// never auto-paused regardless of auto_pause_after.
|
|
1982
|
-
|
|
2066
|
+
// #3035 review: the one rule (an agent named general unticked — general_topic: false — is a worker).
|
|
2067
|
+
const isGeneral = config.general_topic === true || generalByName(name, config);
|
|
1983
2068
|
const autoPauseMinutes = isGeneral ? 0 : (typeof config.auto_pause_after === "number" ? config.auto_pause_after : 0); // default: disabled
|
|
1984
2069
|
this.autoPauseController = new AutoPauseController(Math.max(0, autoPauseMinutes) * 60_000, readLastInboundAt(instanceDir) ?? Date.now());
|
|
1985
2070
|
// #1209: every real completion clears the in-flight marker, so a
|
|
@@ -2038,12 +2123,19 @@ export class Daemon extends EventEmitter {
|
|
|
2038
2123
|
return outcome === "begun";
|
|
2039
2124
|
}
|
|
2040
2125
|
async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
|
|
2041
|
-
let checkpoint = null;
|
|
2042
|
-
|
|
2043
|
-
|
|
2126
|
+
let checkpoint = null, runtimeTranscript = null;
|
|
2127
|
+
if (this.nativePaneLifecycle) {
|
|
2128
|
+
runtimeTranscript = await this.transcriptMonitor?.reconciliationRuntimeCheckpoint() ?? null;
|
|
2129
|
+
if (!runtimeTranscript || !this.nativePaneLifecycle.current(runtimeTranscript))
|
|
2130
|
+
throw Error("Native transcript checkpoint is not current; no delivery was written");
|
|
2044
2131
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2132
|
+
else {
|
|
2133
|
+
try {
|
|
2134
|
+
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2135
|
+
}
|
|
2136
|
+
catch (err) {
|
|
2137
|
+
this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
|
|
2138
|
+
}
|
|
2047
2139
|
}
|
|
2048
2140
|
const submissionMode = rawPaste ? "raw_paste" : steer
|
|
2049
2141
|
? "steer"
|
|
@@ -2059,8 +2151,9 @@ export class Daemon extends EventEmitter {
|
|
|
2059
2151
|
backendVersion,
|
|
2060
2152
|
windowId: windowId ?? this.tmux?.getWindowId() ?? null,
|
|
2061
2153
|
transcriptPath: checkpoint?.path ?? null,
|
|
2062
|
-
transcriptOffset: checkpoint?.offset ?? null,
|
|
2063
|
-
transcriptSessionId: checkpoint?.sessionId ?? null,
|
|
2154
|
+
transcriptOffset: runtimeTranscript?.cursor.offset ?? checkpoint?.offset ?? null,
|
|
2155
|
+
transcriptSessionId: runtimeTranscript?.cursor.sessionId ?? checkpoint?.sessionId ?? null,
|
|
2156
|
+
...(runtimeTranscript ? { runtimeTranscript } : {}),
|
|
2064
2157
|
submissionMode,
|
|
2065
2158
|
queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
|
|
2066
2159
|
};
|
|
@@ -2103,17 +2196,25 @@ export class Daemon extends EventEmitter {
|
|
|
2103
2196
|
const outbox = this.deliveryOutbox;
|
|
2104
2197
|
if (!checkpoint || !outbox?.markConsumed)
|
|
2105
2198
|
return;
|
|
2199
|
+
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
2200
|
+
return;
|
|
2106
2201
|
if (verdict.submissionMode !== "steer" && verdict.submissionMode !== "native_queue_handoff")
|
|
2107
2202
|
return;
|
|
2108
2203
|
if (!["claude-code", "codex"].includes(checkpoint.backend))
|
|
2109
2204
|
return;
|
|
2110
2205
|
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, boot = this.bootId;
|
|
2206
|
+
let location;
|
|
2207
|
+
try {
|
|
2208
|
+
location = checkpoint.runtime ? { reader: this.nativeTranscriptReader(checkpoint.runtime, delivery.deliveryId) } : { path: checkpoint.path, offset: checkpoint.offset };
|
|
2209
|
+
}
|
|
2210
|
+
catch {
|
|
2211
|
+
return;
|
|
2212
|
+
} // The owner changed; an unavailable consumed watch proves nothing.
|
|
2111
2213
|
consumedWatches.start({
|
|
2112
2214
|
deliveryId: delivery.deliveryId,
|
|
2113
2215
|
attemptNo: delivery.attemptNo,
|
|
2114
2216
|
backend: checkpoint.backend,
|
|
2115
|
-
|
|
2116
|
-
offset: checkpoint.offset,
|
|
2217
|
+
...location,
|
|
2117
2218
|
current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch,
|
|
2118
2219
|
consumed: (via, evidence) => {
|
|
2119
2220
|
try {
|
|
@@ -2194,24 +2295,34 @@ export class Daemon extends EventEmitter {
|
|
|
2194
2295
|
const spawn = this.spawnGeneration;
|
|
2195
2296
|
const fence = this.launchFenceEpoch;
|
|
2196
2297
|
let last = "unavailable";
|
|
2197
|
-
|
|
2198
|
-
let
|
|
2298
|
+
const native = !!checkpoint.runtime || !!this.nativePaneLifecycle;
|
|
2299
|
+
let outcome = native ? "uncertain" : "delivered";
|
|
2300
|
+
let evidence = native ? "runtime-transcript-unconfirmed" : OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
2199
2301
|
// #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
|
|
2200
2302
|
// in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
|
|
2201
2303
|
const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
|
|
2202
2304
|
let found = false;
|
|
2203
2305
|
try {
|
|
2306
|
+
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
2307
|
+
throw Error("Native proof cannot read a coordinator-local transcript path");
|
|
2308
|
+
const nativeReader = checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : null;
|
|
2204
2309
|
for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
|
|
2205
2310
|
if (this.deliveryWritesStopping)
|
|
2206
2311
|
return; // reconciliation owns the row now
|
|
2207
|
-
const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
2312
|
+
const look = nativeReader ? await nativeReader.look() : await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
2313
|
+
if (this.deliveryWritesStopping)
|
|
2314
|
+
return;
|
|
2315
|
+
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
2316
|
+
last = "unavailable";
|
|
2317
|
+
break;
|
|
2318
|
+
}
|
|
2208
2319
|
if (look === "user" || look === "absorbed" || look === "queued") {
|
|
2209
2320
|
found = true;
|
|
2210
2321
|
outcome = "delivered";
|
|
2211
2322
|
evidence = transcriptMarkerEvidence(look, !!rawPaste);
|
|
2212
2323
|
break;
|
|
2213
2324
|
}
|
|
2214
|
-
last = look;
|
|
2325
|
+
last = look === "pending" ? "unavailable" : look;
|
|
2215
2326
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
2216
2327
|
last = "unavailable";
|
|
2217
2328
|
break;
|
|
@@ -2223,13 +2334,13 @@ export class Daemon extends EventEmitter {
|
|
|
2223
2334
|
// `uncertain`: re-check the same fixed epochs after the last await, before settling (#1207 P2).
|
|
2224
2335
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch)
|
|
2225
2336
|
last = "unavailable";
|
|
2226
|
-
if (last === "no-match" &&
|
|
2337
|
+
if (last === "no-match" && !found) {
|
|
2227
2338
|
outcome = "uncertain";
|
|
2228
2339
|
evidence = "unverifiable-no-transcript-marker";
|
|
2229
2340
|
}
|
|
2230
2341
|
}
|
|
2231
2342
|
catch (err) {
|
|
2232
|
-
this.logger.debug({ err }, "Transcript proof
|
|
2343
|
+
this.logger.debug({ err }, "Transcript proof unavailable — preserving the conservative outcome");
|
|
2233
2344
|
}
|
|
2234
2345
|
if (rawPaste && !found) {
|
|
2235
2346
|
outcome = "uncertain";
|
|
@@ -2416,128 +2527,130 @@ export class Daemon extends EventEmitter {
|
|
|
2416
2527
|
}
|
|
2417
2528
|
}
|
|
2418
2529
|
catch { /* corrupt/missing — ignore */ }
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2530
|
+
if (!this.nativePaneLifecycle) {
|
|
2531
|
+
// 1. IPC server — bridge between MCP server (Claude's child) and daemon
|
|
2532
|
+
const sockPath = join(this.instanceDir, "channel.sock");
|
|
2533
|
+
this.ipcServer = new IpcServer(sockPath, this.logger);
|
|
2534
|
+
// Forward IPC server errors as daemon events (prevents unhandled 'error' crash).
|
|
2535
|
+
// Guard: only forward post-listen errors — startup errors are handled by listen() rejection.
|
|
2536
|
+
let ipcListening = false;
|
|
2537
|
+
this.ipcServer.on("error", (err) => {
|
|
2538
|
+
if (!ipcListening)
|
|
2539
|
+
return; // startup errors handled by listen() rejection
|
|
2540
|
+
this.logger.error({ err, name: this.name }, "IPC server error");
|
|
2541
|
+
this.emit("error", err);
|
|
2542
|
+
});
|
|
2543
|
+
await this.ipcServer.listen();
|
|
2544
|
+
this.startupAdmission?.();
|
|
2545
|
+
ipcListening = true;
|
|
2546
|
+
// Permanent IPC dispatcher: routes responses to pending requests by type+id key
|
|
2547
|
+
this.ipcServer.on("message", (msg) => this.routeFleetResponse(msg));
|
|
2548
|
+
// IPC message relay: when daemon wants to push a channel message to Claude,
|
|
2549
|
+
// it broadcasts to all IPC clients (the MCP server is one of them).
|
|
2550
|
+
// When MCP server sends a tool_call, daemon handles it via the messageBus.
|
|
2551
|
+
this.ipcServer.on("message", (msg, socket) => {
|
|
2552
|
+
if (msg.type === "tool_call") {
|
|
2553
|
+
// MCP server forwarding a Claude tool call (reply, react, edit, download)
|
|
2554
|
+
this.handleToolCall(msg, socket);
|
|
2555
|
+
}
|
|
2556
|
+
else if (msg.type === "mcp_ready") {
|
|
2557
|
+
this.handleMcpReady(msg, socket);
|
|
2558
|
+
}
|
|
2559
|
+
else if (msg.type === "pong") {
|
|
2560
|
+
this.handleMcpPong(msg, socket);
|
|
2561
|
+
}
|
|
2562
|
+
else if (msg.type === "query_sessions") {
|
|
2563
|
+
// Fleet manager asks for all registered session names (catches sessions
|
|
2564
|
+
// that sent mcp_ready before fleet manager connected).
|
|
2565
|
+
const sessions = [];
|
|
2566
|
+
for (const [s, sessionName] of this.socketSessionNames) {
|
|
2567
|
+
if (!s.destroyed && sessionName !== this.name) {
|
|
2568
|
+
// Individual mcp_ready for initial registration path
|
|
2569
|
+
this.ipcServer?.send(socket, { type: "mcp_ready", sessionName });
|
|
2570
|
+
sessions.push(sessionName);
|
|
2571
|
+
}
|
|
2459
2572
|
}
|
|
2573
|
+
// Batch response for prune path
|
|
2574
|
+
this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
|
|
2460
2575
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
|
|
2471
|
-
}).catch(err => {
|
|
2472
|
-
this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
|
|
2473
|
-
this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
|
|
2474
|
-
});
|
|
2475
|
-
}
|
|
2476
|
-
else if (msg.type === "raw_paste") {
|
|
2477
|
-
this.handleRawPasteIpcMessage(msg);
|
|
2478
|
-
}
|
|
2479
|
-
else if (msg.type === "config_update") {
|
|
2480
|
-
this.applyConfigUpdate(msg.config);
|
|
2481
|
-
}
|
|
2482
|
-
else if (msg.type === "steer") {
|
|
2483
|
-
const meta = (msg.meta ?? {});
|
|
2484
|
-
this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
2485
|
-
}
|
|
2486
|
-
else if (msg.type === "btw") {
|
|
2487
|
-
const meta = (msg.meta ?? {});
|
|
2488
|
-
this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
2489
|
-
}
|
|
2490
|
-
else if (msg.type === "fleet_schedule_trigger") {
|
|
2491
|
-
const payload = msg.payload;
|
|
2492
|
-
const meta = msg.meta;
|
|
2493
|
-
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
2494
|
-
void this.wake().then(() => this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch)).catch(err => {
|
|
2495
|
-
this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
|
|
2496
|
-
});
|
|
2497
|
-
}
|
|
2498
|
-
else if (msg.type === "query_instance_state") {
|
|
2499
|
-
void this.respondToInstanceStateQuery(msg, socket);
|
|
2500
|
-
}
|
|
2501
|
-
});
|
|
2502
|
-
// 2. Tmux — ensure session, create window if not alive
|
|
2503
|
-
await TmuxManager.ensureSession(this.tmuxSessionName);
|
|
2504
|
-
this.startupAdmission?.();
|
|
2505
|
-
this.tmux = new TmuxManager(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
2506
|
-
// Strategy A: always start fresh Claude window (MCP server has no reconnection)
|
|
2507
|
-
// Kill any existing window from previous run
|
|
2508
|
-
const windowIdFile = join(this.instanceDir, "window-id");
|
|
2509
|
-
if (existsSync(windowIdFile)) {
|
|
2510
|
-
const savedId = readFileSync(windowIdFile, "utf-8").trim();
|
|
2511
|
-
if (savedId) {
|
|
2512
|
-
const oldTmux = new TmuxManager(this.tmuxSessionName, savedId);
|
|
2513
|
-
if (await oldTmux.isWindowAlive()) {
|
|
2514
|
-
this.startupAdmission?.();
|
|
2515
|
-
await this.checkpointSessionId();
|
|
2576
|
+
else
|
|
2577
|
+
this.handleFleetMessage(msg, reply => { this.ipcServer?.send(socket, reply); });
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
let nativeDisposition;
|
|
2581
|
+
if (this.nativePaneLifecycle) {
|
|
2582
|
+
this.beginSpawn();
|
|
2583
|
+
try {
|
|
2584
|
+
const acquired = await this.nativePaneLifecycle.acquire(() => {
|
|
2516
2585
|
this.startupAdmission?.();
|
|
2517
|
-
|
|
2586
|
+
if (this.startupAborted)
|
|
2587
|
+
throw Error("Native launch was superseded");
|
|
2588
|
+
});
|
|
2589
|
+
this.tmux = acquired.pane;
|
|
2590
|
+
this.nativeTranscriptSource = acquired.source;
|
|
2591
|
+
nativeDisposition = acquired.disposition;
|
|
2592
|
+
this.lastSpawnAt = Date.now();
|
|
2593
|
+
this.setProcessStatus("running");
|
|
2594
|
+
this.bindInstanceStateOutputListener(this.tmux.getWindowId());
|
|
2595
|
+
this.saveSessionId();
|
|
2596
|
+
// New/resumed native processes can still be painting their first frame. Keep deliveries behind the same
|
|
2597
|
+
// bounded dialog/readiness scan as local startup. An already-running reattachment may be busy: do not
|
|
2598
|
+
// wait for that work to finish or inject a startup key into it.
|
|
2599
|
+
if (acquired.disposition !== "reattached") {
|
|
2600
|
+
if (!await this.finishStartupScan(120_000))
|
|
2601
|
+
throw Error("Native CLI exited during startup; retained session was not replaced");
|
|
2518
2602
|
this.startupAdmission?.();
|
|
2519
|
-
this.
|
|
2603
|
+
if (this.startupAborted || this.deliveryWritesStopping || !this.nativePaneLifecycle.current())
|
|
2604
|
+
throw Error("Native launch was superseded");
|
|
2520
2605
|
}
|
|
2521
2606
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
this.isNewSession = !resumed;
|
|
2526
|
-
if (!resumed) {
|
|
2527
|
-
await this.injectSnapshotMessage(this.startupAdmission);
|
|
2528
|
-
this.startupAdmission?.();
|
|
2607
|
+
finally {
|
|
2608
|
+
this.endSpawn();
|
|
2609
|
+
}
|
|
2529
2610
|
}
|
|
2530
2611
|
else {
|
|
2531
|
-
//
|
|
2532
|
-
|
|
2533
|
-
|
|
2612
|
+
// 2. Tmux — ensure session, create window if not alive
|
|
2613
|
+
await this.paneHost.ensureSession(this.tmuxSessionName);
|
|
2614
|
+
this.startupAdmission?.();
|
|
2615
|
+
this.tmux = this.paneHost.bind(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
2616
|
+
// Strategy A: always start fresh Claude window (MCP server has no reconnection)
|
|
2617
|
+
// Kill any existing window from previous run
|
|
2618
|
+
const windowIdFile = join(this.instanceDir, "window-id");
|
|
2619
|
+
if (existsSync(windowIdFile)) {
|
|
2620
|
+
const savedId = readFileSync(windowIdFile, "utf-8").trim();
|
|
2621
|
+
if (savedId) {
|
|
2622
|
+
const oldTmux = this.paneHost.bind(this.tmuxSessionName, savedId);
|
|
2623
|
+
if (await oldTmux.isWindowAlive()) {
|
|
2624
|
+
this.startupAdmission?.();
|
|
2625
|
+
await this.checkpointSessionId();
|
|
2626
|
+
this.startupAdmission?.();
|
|
2627
|
+
await oldTmux.killWindow();
|
|
2628
|
+
this.startupAdmission?.();
|
|
2629
|
+
this.logger.info({ savedId }, "Killed old tmux window for fresh start");
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
const resumed = await this.spawnClaudeWindow();
|
|
2634
|
+
this.startupAdmission?.();
|
|
2635
|
+
this.isNewSession = !resumed;
|
|
2636
|
+
if (!resumed) {
|
|
2637
|
+
await this.injectSnapshotMessage(this.startupAdmission);
|
|
2638
|
+
this.startupAdmission?.();
|
|
2639
|
+
}
|
|
2640
|
+
else {
|
|
2641
|
+
// Clean up stale snapshot file — resume restored full context, snapshot not needed
|
|
2642
|
+
try {
|
|
2643
|
+
unlinkSync(join(this.instanceDir, "rotation-state.json"));
|
|
2644
|
+
}
|
|
2645
|
+
catch { /* may not exist */ }
|
|
2534
2646
|
}
|
|
2535
|
-
catch { /* may not exist */ }
|
|
2536
2647
|
}
|
|
2648
|
+
if (nativeDisposition)
|
|
2649
|
+
this.isNewSession = nativeDisposition === "fresh";
|
|
2537
2650
|
// #1209: conditional continuation for the turn interrupted by the last
|
|
2538
2651
|
// restart, if the gates allow. Detached; the gate
|
|
2539
2652
|
// consumes its one-shot marker first and never throws into boot.
|
|
2540
|
-
const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2653
|
+
const legacyResume = this.nativePaneLifecycle ? Promise.resolve("unknown") : this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2541
2654
|
const recoveryOwner = JSON.stringify(this.interactionOwner());
|
|
2542
2655
|
void Promise.allSettled([legacyResume]).then(() => {
|
|
2543
2656
|
if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
|
|
@@ -2556,7 +2669,7 @@ export class Daemon extends EventEmitter {
|
|
|
2556
2669
|
// (statusline transcript); codex/kiro/opencode read their CLI's own
|
|
2557
2670
|
// conversation store via a pluggable source. Backends with no known
|
|
2558
2671
|
// source stay inert exactly as before.
|
|
2559
|
-
this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
|
|
2672
|
+
this.transcriptMonitor = this.buildTranscriptMonitor(this.nativeTranscriptSource ?? createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
|
|
2560
2673
|
const transcriptMonitor = this.transcriptMonitor;
|
|
2561
2674
|
const transcriptFence = this.launchFenceEpoch;
|
|
2562
2675
|
// Baseline in the isolate before this daemon accepts new work. A
|
|
@@ -2612,13 +2725,15 @@ export class Daemon extends EventEmitter {
|
|
|
2612
2725
|
if (hangConfig?.enabled !== false) {
|
|
2613
2726
|
this.hangDetector = new HangDetector();
|
|
2614
2727
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
this.
|
|
2621
|
-
|
|
2728
|
+
if (!this.nativePaneLifecycle) {
|
|
2729
|
+
// 8. Context guardian
|
|
2730
|
+
const statusFile = join(this.instanceDir, "statusline.json");
|
|
2731
|
+
this.guardian = new ContextGuardian(this.config.context_guardian ?? {}, this.logger, statusFile);
|
|
2732
|
+
this.guardian.startWatching();
|
|
2733
|
+
this.guardian.on("status_update", () => {
|
|
2734
|
+
this.saveSessionId();
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2622
2737
|
// Context rotation removed: all CLI backends have built-in auto-compact.
|
|
2623
2738
|
// Crash recovery (health check + respawn with snapshot) is retained below.
|
|
2624
2739
|
}
|
|
@@ -2669,7 +2784,7 @@ export class Daemon extends EventEmitter {
|
|
|
2669
2784
|
this.socketPids.delete(socket);
|
|
2670
2785
|
// Notify fleet manager so it can clean up sessionRegistry
|
|
2671
2786
|
if (sessionName !== this.name) {
|
|
2672
|
-
this.
|
|
2787
|
+
this.broadcastFleetMessage({ type: "session_disconnected", sessionName });
|
|
2673
2788
|
}
|
|
2674
2789
|
});
|
|
2675
2790
|
}
|
|
@@ -2685,11 +2800,16 @@ export class Daemon extends EventEmitter {
|
|
|
2685
2800
|
// sub-agent's server may have left a dead pid in the slot).
|
|
2686
2801
|
this.noteMcpProofOfLife("mcp_ready", typeof msg.pid === "number" ? msg.pid : undefined);
|
|
2687
2802
|
}
|
|
2688
|
-
this.
|
|
2803
|
+
this.broadcastFleetMessage({ type: "mcp_ready", sessionName });
|
|
2689
2804
|
}
|
|
2690
2805
|
checkMcpServerAlive() {
|
|
2691
2806
|
if (this.isPaused)
|
|
2692
2807
|
return;
|
|
2808
|
+
if (this.nativePaneLifecycle) {
|
|
2809
|
+
if (this.nativePaneLifecycle.mcpProof() === "alive")
|
|
2810
|
+
this.noteMcpProofOfLife("connection");
|
|
2811
|
+
return; // No native PID/socket guessing or restart from an unknown helper observation.
|
|
2812
|
+
}
|
|
2693
2813
|
const status = mcpServerState(this.instanceDir);
|
|
2694
2814
|
if (status.state === "alive") {
|
|
2695
2815
|
this.mcpServedGeneration = this.spawnGeneration;
|
|
@@ -2773,6 +2893,8 @@ export class Daemon extends EventEmitter {
|
|
|
2773
2893
|
* server from a third-party one, and failed the whole launch for either.
|
|
2774
2894
|
*/
|
|
2775
2895
|
checkFleetMcpNeverConnected() {
|
|
2896
|
+
if (this.nativePaneLifecycle)
|
|
2897
|
+
return; // Native helper presence owns this observation.
|
|
2776
2898
|
if (this.config.agent_mode === "cli")
|
|
2777
2899
|
return; // no MCP server by design
|
|
2778
2900
|
if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
|
|
@@ -2983,6 +3105,8 @@ export class Daemon extends EventEmitter {
|
|
|
2983
3105
|
* proof that tools are being served, whatever channel.mcp.pid says.
|
|
2984
3106
|
*/
|
|
2985
3107
|
mcpServerAlive() {
|
|
3108
|
+
if (this.nativePaneLifecycle)
|
|
3109
|
+
return this.nativePaneLifecycle.mcpProof() === "alive" ? { alive: true, source: "connection" } : { alive: false, source: "none" };
|
|
2986
3110
|
const status = mcpServerState(this.instanceDir);
|
|
2987
3111
|
if (status.state === "alive")
|
|
2988
3112
|
return { alive: true, source: "pid", pid: status.pid };
|
|
@@ -3047,7 +3171,7 @@ export class Daemon extends EventEmitter {
|
|
|
3047
3171
|
this.mcpDeathNotifiedForPid = null;
|
|
3048
3172
|
this.mcpDeathDeferredForPid = null;
|
|
3049
3173
|
this.clearMcpRestartRequest();
|
|
3050
|
-
if (pid && mcpServerState(this.instanceDir).state !== "alive")
|
|
3174
|
+
if (!this.nativePaneLifecycle && pid && mcpServerState(this.instanceDir).state !== "alive")
|
|
3051
3175
|
this.repairMcpPidSlot(pid, source);
|
|
3052
3176
|
}
|
|
3053
3177
|
emitMcpRecovered(source, pid) {
|
|
@@ -3094,6 +3218,26 @@ export class Daemon extends EventEmitter {
|
|
|
3094
3218
|
// ended this instance's health loop because scheduleNext() sat after the
|
|
3095
3219
|
// throwing code. Catch, log, and keep checking.
|
|
3096
3220
|
try {
|
|
3221
|
+
if (this.nativePaneLifecycle) {
|
|
3222
|
+
if (this.healthCheckPaused) {
|
|
3223
|
+
scheduleNext();
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
const native = this.nativePaneLifecycle, pane = this.tmux;
|
|
3227
|
+
const status = await pane?.getPaneStatus();
|
|
3228
|
+
if (this.runtimeMonitorsFrozen || native !== this.nativePaneLifecycle || pane !== this.tmux)
|
|
3229
|
+
return;
|
|
3230
|
+
const snapshot = native.snapshot?.();
|
|
3231
|
+
if (status?.alive === false && snapshot?.state === "exited") {
|
|
3232
|
+
this.healthCheckPaused = true;
|
|
3233
|
+
this.setProcessStatus(snapshot.exitCode === 0 ? "stopped" : "crashed");
|
|
3234
|
+
this.emit("native_pane_exited", { name: this.name, snapshot, owner: this.interactionOwner() });
|
|
3235
|
+
}
|
|
3236
|
+
else if (status?.alive)
|
|
3237
|
+
this.checkMcpServerAlive();
|
|
3238
|
+
scheduleNext();
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3097
3241
|
// Instance directory removed externally (e.g. `rm -rf ~/.agend/instances/<name>`).
|
|
3098
3242
|
// Stop the loop permanently — otherwise every tick triggers a respawn, whose
|
|
3099
3243
|
// writeRotationSnapshot fails with ENOENT and gets caught as "Failed to respawn",
|
|
@@ -3205,13 +3349,13 @@ export class Daemon extends EventEmitter {
|
|
|
3205
3349
|
let inWindowLossBurst = false;
|
|
3206
3350
|
let nullReason;
|
|
3207
3351
|
if (!paneStatus) {
|
|
3208
|
-
const serverAlive = await
|
|
3352
|
+
const serverAlive = await this.paneHost.sessionExists(this.tmuxSessionName);
|
|
3209
3353
|
if (discardStaleHealth())
|
|
3210
3354
|
return;
|
|
3211
3355
|
// A server may have already restarted before this daemon's health
|
|
3212
3356
|
// tick. The new PID is the durable generation boundary: treat it as
|
|
3213
3357
|
// a fleet storm even though `has-session` is true again.
|
|
3214
|
-
const serverPid = serverAlive ? await
|
|
3358
|
+
const serverPid = serverAlive ? await this.paneHost.getServerPid(this.tmuxSessionName) : null;
|
|
3215
3359
|
if (discardStaleHealth())
|
|
3216
3360
|
return;
|
|
3217
3361
|
const generationChanged = serverAlive && this.stormWindow?.observeServerAlive(serverPid) === true;
|
|
@@ -3240,7 +3384,7 @@ export class Daemon extends EventEmitter {
|
|
|
3240
3384
|
// the window truly no longer exists vs a transient query glitch.
|
|
3241
3385
|
nullReason = "no_window";
|
|
3242
3386
|
try {
|
|
3243
|
-
const windows = await
|
|
3387
|
+
const windows = await this.paneHost.listWindowsStrict(this.tmuxSessionName, this.controlClient);
|
|
3244
3388
|
if (discardStaleHealth())
|
|
3245
3389
|
return;
|
|
3246
3390
|
this.windowQueryFailureTicks = 0;
|
|
@@ -3485,10 +3629,10 @@ export class Daemon extends EventEmitter {
|
|
|
3485
3629
|
// Wrapped in try-catch: if tmux server is dead, listWindows throws —
|
|
3486
3630
|
// must not block spawnClaudeWindow (which calls ensureSession).
|
|
3487
3631
|
try {
|
|
3488
|
-
const windows = await
|
|
3632
|
+
const windows = await this.paneHost.listWindows(this.tmuxSessionName);
|
|
3489
3633
|
for (const w of windows) {
|
|
3490
3634
|
if (w.name === this.name) {
|
|
3491
|
-
const tm =
|
|
3635
|
+
const tm = this.paneHost.bind(this.tmuxSessionName, w.id);
|
|
3492
3636
|
await tm.killWindow();
|
|
3493
3637
|
}
|
|
3494
3638
|
}
|
|
@@ -3631,7 +3775,7 @@ export class Daemon extends EventEmitter {
|
|
|
3631
3775
|
// Force the next ready capture to emit a fresh transition after respawn.
|
|
3632
3776
|
this.instanceState = "working";
|
|
3633
3777
|
}
|
|
3634
|
-
this.
|
|
3778
|
+
this.broadcastFleetMessage({
|
|
3635
3779
|
type: "instance_process_state",
|
|
3636
3780
|
instanceName: this.name,
|
|
3637
3781
|
status,
|
|
@@ -3827,12 +3971,15 @@ export class Daemon extends EventEmitter {
|
|
|
3827
3971
|
for (const key of dialog.keys) {
|
|
3828
3972
|
if (stale())
|
|
3829
3973
|
return;
|
|
3974
|
+
const keyScreen = this.nativePaneLifecycle ? await pollTmux.capturePane() : currentPane;
|
|
3975
|
+
if (stale() || !Daemon.dialogMatches(dialog, keyScreen))
|
|
3976
|
+
return;
|
|
3830
3977
|
let sent = false;
|
|
3831
3978
|
if (SPECIAL_KEYS.has(key)) {
|
|
3832
|
-
sent = await pollTmux.sendSpecialKey(key);
|
|
3979
|
+
sent = await (this.nativePaneLifecycle ? pollTmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : pollTmux.sendSpecialKey(key));
|
|
3833
3980
|
}
|
|
3834
3981
|
else if (dialog.literalKeys) {
|
|
3835
|
-
sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
|
|
3982
|
+
sent = await (this.nativePaneLifecycle ? pollTmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : pollTmux.sendKeys(key)); // typed, no Enter (#2872)
|
|
3836
3983
|
}
|
|
3837
3984
|
else {
|
|
3838
3985
|
sent = await pollTmux.pasteText(key, this.systemPasteOptions());
|
|
@@ -4195,7 +4342,20 @@ export class Daemon extends EventEmitter {
|
|
|
4195
4342
|
*/
|
|
4196
4343
|
async sendEscape() {
|
|
4197
4344
|
const cancelKey = this.backend?.getCancelKey() ?? "Escape";
|
|
4198
|
-
|
|
4345
|
+
const tmux = this.tmux;
|
|
4346
|
+
if (!tmux)
|
|
4347
|
+
return;
|
|
4348
|
+
if (this.nativePaneLifecycle) {
|
|
4349
|
+
// A native pane's composer proof cannot see a permission dialog, so an unproved key is withheld there. The cancel
|
|
4350
|
+
// is the person's own interrupt, keyed on the screen read just before (as the dialog answer path is): what Esc
|
|
4351
|
+
// does on Linux's tmux pane, on the same screen, with the same key.
|
|
4352
|
+
const screen = await tmux.capturePane().catch(() => null);
|
|
4353
|
+
if (screen === null)
|
|
4354
|
+
return;
|
|
4355
|
+
await tmux.sendSpecialKey(cancelKey, undefined, { kind: "dialog", screen });
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4358
|
+
await tmux.sendSpecialKey(cancelKey);
|
|
4199
4359
|
}
|
|
4200
4360
|
/**
|
|
4201
4361
|
* Drop every not-yet-started pane delivery which predates a user cancel.
|
|
@@ -4253,7 +4413,7 @@ export class Daemon extends EventEmitter {
|
|
|
4253
4413
|
return;
|
|
4254
4414
|
const outcome = follower.look(pane);
|
|
4255
4415
|
if (outcome) {
|
|
4256
|
-
this.
|
|
4416
|
+
this.broadcastFleetMessage({ type: "compact_pane_result", id, owner, outcome });
|
|
4257
4417
|
return;
|
|
4258
4418
|
}
|
|
4259
4419
|
}
|
|
@@ -4273,7 +4433,7 @@ export class Daemon extends EventEmitter {
|
|
|
4273
4433
|
const reportSwitch = (result) => {
|
|
4274
4434
|
if (!ownSwitch)
|
|
4275
4435
|
return;
|
|
4276
|
-
this.
|
|
4436
|
+
this.broadcastFleetMessage({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
|
|
4277
4437
|
};
|
|
4278
4438
|
// Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
|
|
4279
4439
|
// (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
|
|
@@ -4288,7 +4448,7 @@ export class Daemon extends EventEmitter {
|
|
|
4288
4448
|
if (!notifyWritten || told)
|
|
4289
4449
|
return;
|
|
4290
4450
|
told = true;
|
|
4291
|
-
this.
|
|
4451
|
+
this.broadcastFleetMessage({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
|
|
4292
4452
|
...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
|
|
4293
4453
|
if (written && paneFollower)
|
|
4294
4454
|
void this.followCompactPane(notifyWritten, owner, paneFollower);
|
|
@@ -4450,10 +4610,11 @@ export class Daemon extends EventEmitter {
|
|
|
4450
4610
|
const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
|
|
4451
4611
|
if (this.tmux !== tmux)
|
|
4452
4612
|
return false;
|
|
4453
|
-
const
|
|
4613
|
+
const screen = await read();
|
|
4614
|
+
const now = claudeSwitchDialog(screen);
|
|
4454
4615
|
if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
|
|
4455
4616
|
return false;
|
|
4456
|
-
return tmux.sendSpecialKey(key);
|
|
4617
|
+
return this.nativePaneLifecycle ? tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen }) : tmux.sendSpecialKey(key);
|
|
4457
4618
|
});
|
|
4458
4619
|
const gone = async () => {
|
|
4459
4620
|
const until = performance.now() + OWN_SWITCH_SETTLE_MS;
|
|
@@ -4542,9 +4703,12 @@ export class Daemon extends EventEmitter {
|
|
|
4542
4703
|
return false;
|
|
4543
4704
|
const specialKeys = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
|
|
4544
4705
|
for (const key of dialog.keys) {
|
|
4706
|
+
const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : currentPane;
|
|
4707
|
+
if (!matches(keyScreen))
|
|
4708
|
+
return false;
|
|
4545
4709
|
const ok = specialKeys.has(key)
|
|
4546
|
-
? await this.tmux.sendSpecialKey(key)
|
|
4547
|
-
: await this.tmux.sendKeys(key);
|
|
4710
|
+
? await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key))
|
|
4711
|
+
: await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key));
|
|
4548
4712
|
if (!ok)
|
|
4549
4713
|
return false;
|
|
4550
4714
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -4787,7 +4951,7 @@ export class Daemon extends EventEmitter {
|
|
|
4787
4951
|
await this.tmux.sendSpecialKey("Escape").catch(() => false);
|
|
4788
4952
|
return true;
|
|
4789
4953
|
}
|
|
4790
|
-
async stop() {
|
|
4954
|
+
async stop(options = {}) {
|
|
4791
4955
|
this.publishWorkingState("before_stop");
|
|
4792
4956
|
this.logger.info("Stopping daemon instance");
|
|
4793
4957
|
this.clearQuitRelaunchWatch();
|
|
@@ -4809,8 +4973,18 @@ export class Daemon extends EventEmitter {
|
|
|
4809
4973
|
}
|
|
4810
4974
|
if (this.adapter)
|
|
4811
4975
|
await this.adapter.stop();
|
|
4976
|
+
if (this.nativePaneLifecycle) {
|
|
4977
|
+
this.healthCheckPaused = true;
|
|
4978
|
+
this.saveSessionId();
|
|
4979
|
+
await this.nativePaneLifecycle.release(options.nativePane ?? "detach");
|
|
4980
|
+
try {
|
|
4981
|
+
unlinkSync(join(this.instanceDir, "daemon.pid"));
|
|
4982
|
+
}
|
|
4983
|
+
catch { /* metadata only */ }
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4812
4986
|
// Notify MCP servers of graceful shutdown (prevents reconnect attempts)
|
|
4813
|
-
this.
|
|
4987
|
+
this.broadcastFleetMessage({ type: "shutdown" });
|
|
4814
4988
|
// Quit CLI FIRST — this kills MCP server child processes cleanly.
|
|
4815
4989
|
// IPC must stay open during quit so MCP servers receive the shutdown message.
|
|
4816
4990
|
if (this.tmux) {
|
|
@@ -4956,6 +5130,24 @@ export class Daemon extends EventEmitter {
|
|
|
4956
5130
|
freshChoice: this.recoveryFreshChoice || this.isNewSession && !this.bootSkippedResume, crashLoopFresh: this.recoveryCrashLoopFresh,
|
|
4957
5131
|
cancelled: this.recoveryCancelled, background: this.workingBackgroundObservation };
|
|
4958
5132
|
}
|
|
5133
|
+
/** Observe this host General before its approved switch. No keys, process signals or provider writes. */
|
|
5134
|
+
async generalHostOwner() {
|
|
5135
|
+
const pane = this.tmux, owner = JSON.stringify(this.interactionOwner());
|
|
5136
|
+
if (!this.config.general_topic || this.nativePaneLifecycle || !pane || !/^@\d+$/.test(pane.getWindowId()))
|
|
5137
|
+
throw Error("General host owner is unavailable");
|
|
5138
|
+
const status = await pane.getPaneStatus();
|
|
5139
|
+
if (!status?.alive || !status.pid || status.pid <= 1)
|
|
5140
|
+
throw Error("General host process cannot be proven");
|
|
5141
|
+
const checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint();
|
|
5142
|
+
if (this.tmux !== pane || owner !== JSON.stringify(this.interactionOwner()))
|
|
5143
|
+
throw Error("General host owner changed during observation");
|
|
5144
|
+
const backend = this.config.backend ?? "claude-code";
|
|
5145
|
+
let sessionId = this.backend?.getSessionId() ?? undefined;
|
|
5146
|
+
if (backend === "codex" && checkpoint)
|
|
5147
|
+
sessionId = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(checkpoint.path)?.[1];
|
|
5148
|
+
return { kind: "tmux", windowId: pane.getWindowId(), pid: status.pid,
|
|
5149
|
+
...(typeof sessionId === "string" && sessionId.length > 0 && sessionId.length <= 256 && !/[\p{Cc}\p{Cf}]/u.test(sessionId) ? { pin: { backend, sessionId } } : {}) };
|
|
5150
|
+
}
|
|
4959
5151
|
/** Local system work: no user turn, channel reply target, wake or replay of an original request. */
|
|
4960
5152
|
async deliverWorkingRecovery(content, delivery, admitted) {
|
|
4961
5153
|
const tmux = this.tmux, owner = JSON.stringify(this.interactionOwner()), epoch = this.deliveryEpoch;
|
|
@@ -4976,7 +5168,11 @@ export class Daemon extends EventEmitter {
|
|
|
4976
5168
|
|| !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
|
|
4977
5169
|
|| this.backend?.backgroundWorkRunning?.(pane) === true)
|
|
4978
5170
|
return;
|
|
4979
|
-
if (
|
|
5171
|
+
if (this.nativePaneLifecycle ? !this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"
|
|
5172
|
+
|| !!this.backend?.isDeliveryInputReadyPane && !this.backend.isDeliveryInputReadyPane(pane)
|
|
5173
|
+
: await tmux.getPaneInputMode?.() !== "raw")
|
|
5174
|
+
return;
|
|
5175
|
+
if (!current())
|
|
4980
5176
|
return;
|
|
4981
5177
|
const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
|
|
4982
5178
|
if (!current())
|
|
@@ -5067,7 +5263,7 @@ export class Daemon extends EventEmitter {
|
|
|
5067
5263
|
this.logger.debug({ err }, "Interaction listener failed");
|
|
5068
5264
|
}
|
|
5069
5265
|
try {
|
|
5070
|
-
this.
|
|
5266
|
+
this.broadcastFleetMessage({ type: "instance_interaction", instanceName: this.name, interaction });
|
|
5071
5267
|
}
|
|
5072
5268
|
catch (err) {
|
|
5073
5269
|
this.logger.debug({ err }, "Interaction broadcast failed");
|
|
@@ -5155,6 +5351,56 @@ export class Daemon extends EventEmitter {
|
|
|
5155
5351
|
}
|
|
5156
5352
|
return true;
|
|
5157
5353
|
}
|
|
5354
|
+
/** Shared Fleet inputs: socket and native transports retain the same wake, epoch and raw-paste semantics. */
|
|
5355
|
+
handleFleetMessage(msg, send, current = () => true) {
|
|
5356
|
+
if (msg.type === "query_sessions") {
|
|
5357
|
+
send({ type: "query_sessions_response", sessions: [] });
|
|
5358
|
+
}
|
|
5359
|
+
else if (msg.type === "fleet_inbound") {
|
|
5360
|
+
const meta = msg.meta;
|
|
5361
|
+
const targetSession = msg.targetSession;
|
|
5362
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
5363
|
+
void this.wake().then(() => {
|
|
5364
|
+
if (!current()) {
|
|
5365
|
+
this.deferDurableDelivery(meta, "native channel owner changed before pane submission");
|
|
5366
|
+
return;
|
|
5367
|
+
}
|
|
5368
|
+
this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
|
|
5369
|
+
}).catch(err => {
|
|
5370
|
+
this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
|
|
5371
|
+
this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
|
|
5372
|
+
});
|
|
5373
|
+
}
|
|
5374
|
+
else if (msg.type === "raw_paste") {
|
|
5375
|
+
this.handleRawPasteIpcMessage(msg);
|
|
5376
|
+
}
|
|
5377
|
+
else if (msg.type === "config_update") {
|
|
5378
|
+
this.applyConfigUpdate(msg.config);
|
|
5379
|
+
}
|
|
5380
|
+
else if (msg.type === "steer") {
|
|
5381
|
+
const meta = (msg.meta ?? {});
|
|
5382
|
+
this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
5383
|
+
}
|
|
5384
|
+
else if (msg.type === "btw") {
|
|
5385
|
+
const meta = (msg.meta ?? {});
|
|
5386
|
+
this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
5387
|
+
}
|
|
5388
|
+
else if (msg.type === "fleet_schedule_trigger") {
|
|
5389
|
+
const payload = msg.payload;
|
|
5390
|
+
const meta = msg.meta;
|
|
5391
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
5392
|
+
void this.wake().then(() => { if (current())
|
|
5393
|
+
this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch); }).catch(err => {
|
|
5394
|
+
this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
else if (msg.type === "query_instance_state") {
|
|
5398
|
+
void this.respondToInstanceStateQuery(msg, undefined, send);
|
|
5399
|
+
}
|
|
5400
|
+
else
|
|
5401
|
+
return false;
|
|
5402
|
+
return true;
|
|
5403
|
+
}
|
|
5158
5404
|
/**
|
|
5159
5405
|
* Answer a FleetManager state query. Most callers only need the cached state,
|
|
5160
5406
|
* but lifecycle decisions such as post-reply Cancel retirement need an
|
|
@@ -5162,7 +5408,7 @@ export class Daemon extends EventEmitter {
|
|
|
5162
5408
|
* the last transition can stay "working" even though the CLI has since
|
|
5163
5409
|
* settled without emitting another control-mode output record.
|
|
5164
5410
|
*/
|
|
5165
|
-
async respondToInstanceStateQuery(msg, socket) {
|
|
5411
|
+
async respondToInstanceStateQuery(msg, socket, send) {
|
|
5166
5412
|
try {
|
|
5167
5413
|
if (msg.deliveryIdle === true) {
|
|
5168
5414
|
await this.captureAndEvaluateInstanceState("delivery_idle_gate");
|
|
@@ -5171,7 +5417,7 @@ export class Daemon extends EventEmitter {
|
|
|
5171
5417
|
await this.captureAndEvaluateInstanceState("state_query");
|
|
5172
5418
|
}
|
|
5173
5419
|
const snapshot = this.getInstanceStateSnapshot();
|
|
5174
|
-
|
|
5420
|
+
const reply = {
|
|
5175
5421
|
type: "instance_state_response",
|
|
5176
5422
|
requestId: msg.requestId,
|
|
5177
5423
|
instanceName: this.name,
|
|
@@ -5179,7 +5425,11 @@ export class Daemon extends EventEmitter {
|
|
|
5179
5425
|
state: this.isPaused ? "paused" : snapshot.state,
|
|
5180
5426
|
processStatus: this.processStatus,
|
|
5181
5427
|
pausedAt: this.lastPausedAt,
|
|
5182
|
-
}
|
|
5428
|
+
};
|
|
5429
|
+
if (send)
|
|
5430
|
+
send(reply);
|
|
5431
|
+
else if (socket)
|
|
5432
|
+
this.ipcServer?.send(socket, reply);
|
|
5183
5433
|
}
|
|
5184
5434
|
catch (err) {
|
|
5185
5435
|
this.logger.debug({ err: err.message }, "Instance state query failed");
|
|
@@ -5219,7 +5469,49 @@ export class Daemon extends EventEmitter {
|
|
|
5219
5469
|
static AUTH_RECONFIRM_ROWS = 15;
|
|
5220
5470
|
/** Why the deferred pause was requested; what its marker records (Phase 2a). */
|
|
5221
5471
|
pendingPauseReason = "auth";
|
|
5472
|
+
nativePauseProof = null;
|
|
5473
|
+
/** Consume the exact idle proof synchronously after the lifecycle's await, before fencing and closing. */
|
|
5474
|
+
nativePauseStillReady() {
|
|
5475
|
+
const proof = this.nativePauseProof;
|
|
5476
|
+
this.nativePauseProof = null;
|
|
5477
|
+
if (proof?.())
|
|
5478
|
+
return true;
|
|
5479
|
+
this.pauseRequested = false;
|
|
5480
|
+
return false;
|
|
5481
|
+
}
|
|
5482
|
+
/** Native lifecycle pause uses the ordinary idle/background/admission proof before closing its owned Job. */
|
|
5483
|
+
async nativePauseReady(admission) {
|
|
5484
|
+
this.nativePauseProof = null;
|
|
5485
|
+
const native = this.nativePaneLifecycle, pane = this.tmux, owner = this.interactionOwner();
|
|
5486
|
+
const allowStuck = this.pauseAllowStuck;
|
|
5487
|
+
this.pauseAllowStuck = false;
|
|
5488
|
+
const ready = () => !!native && native === this.nativePaneLifecycle && native.current() && this.tmux === pane
|
|
5489
|
+
&& sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.spawning
|
|
5490
|
+
&& this.processStatus === "running" && this.pasteQueueDepth === 0
|
|
5491
|
+
&& (this.instanceState === "idle" || allowStuck && this.instanceState === "stuck") && (!admission || admission.current());
|
|
5492
|
+
if (!pane || !ready()) {
|
|
5493
|
+
this.pauseRequested = false;
|
|
5494
|
+
return false;
|
|
5495
|
+
}
|
|
5496
|
+
let text;
|
|
5497
|
+
try {
|
|
5498
|
+
text = await pane.capturePane();
|
|
5499
|
+
}
|
|
5500
|
+
catch {
|
|
5501
|
+
this.pauseRequested = false;
|
|
5502
|
+
return false;
|
|
5503
|
+
}
|
|
5504
|
+
if (!ready() || admission && !admission.accepts(text) || this.backend?.backgroundWorkRunning?.(text)
|
|
5505
|
+
|| !allowStuck && !this.paneAuthoritativelyIdle(text)) {
|
|
5506
|
+
this.pauseRequested = false;
|
|
5507
|
+
return false;
|
|
5508
|
+
}
|
|
5509
|
+
this.nativePauseProof = ready;
|
|
5510
|
+
return true;
|
|
5511
|
+
}
|
|
5222
5512
|
async pause(reason = "operator", admission) {
|
|
5513
|
+
if (this.nativePaneLifecycle)
|
|
5514
|
+
throw Error("Native pause is owned by its Fleet lifecycle transition");
|
|
5223
5515
|
if (admission && !admission.current())
|
|
5224
5516
|
return;
|
|
5225
5517
|
if (this.pauseWakeState === "paused")
|
|
@@ -5315,7 +5607,7 @@ export class Daemon extends EventEmitter {
|
|
|
5315
5607
|
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
|
|
5316
5608
|
this.publishWorkingState("execution");
|
|
5317
5609
|
this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
|
|
5318
|
-
this.
|
|
5610
|
+
this.broadcastFleetMessage({
|
|
5319
5611
|
type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
|
|
5320
5612
|
});
|
|
5321
5613
|
this.emit("auto_paused", { name: this.name, pausedAt: this.lastPausedAt });
|
|
@@ -5410,7 +5702,7 @@ export class Daemon extends EventEmitter {
|
|
|
5410
5702
|
this.resumeRuntimeMonitors();
|
|
5411
5703
|
this.publishWorkingState("execution");
|
|
5412
5704
|
this.logger.info("Instance auto-woke");
|
|
5413
|
-
this.
|
|
5705
|
+
this.broadcastFleetMessage({
|
|
5414
5706
|
type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
|
|
5415
5707
|
});
|
|
5416
5708
|
this.emit("auto_woke", { name: this.name });
|
|
@@ -5570,7 +5862,7 @@ export class Daemon extends EventEmitter {
|
|
|
5570
5862
|
this.logStateEdge(previous, snapshot);
|
|
5571
5863
|
this.publishWorkingState("execution");
|
|
5572
5864
|
this.emit("instance_state", { name: this.name, ...snapshot });
|
|
5573
|
-
this.
|
|
5865
|
+
this.broadcastFleetMessage({ type: "instance_state", instanceName: this.name, ...snapshot });
|
|
5574
5866
|
if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
|
|
5575
5867
|
this.handleStuckTransition(pane, snapshot, this.instanceStateReadyPattern);
|
|
5576
5868
|
}
|
|
@@ -5670,7 +5962,7 @@ export class Daemon extends EventEmitter {
|
|
|
5670
5962
|
// during the continuation boots clean instead of chaining resumes.
|
|
5671
5963
|
// #2544: a reply that settled before this arm already answered the person —
|
|
5672
5964
|
// writing a marker would re-inject a continuation for a finished turn.
|
|
5673
|
-
if (!meta.resumedContinuationOf && !armed.completionDelivered) {
|
|
5965
|
+
if (!this.nativePaneLifecycle && !meta.resumedContinuationOf && !armed.completionDelivered) {
|
|
5674
5966
|
const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
|
|
5675
5967
|
const resumeCwd = this.config.working_directory ?? "";
|
|
5676
5968
|
writeInFlightTurnMarker(this.instanceDir, {
|
|
@@ -5902,7 +6194,7 @@ export class Daemon extends EventEmitter {
|
|
|
5902
6194
|
// outbound action suppresses a duplicate proxy; only a delivered
|
|
5903
6195
|
// human-facing channel action satisfies the completion guard below.
|
|
5904
6196
|
if (!turn.outboundDelivered && this.config.mcp_proxy_reply === true
|
|
5905
|
-
&& mcpServerState(this.instanceDir).state === "dead") {
|
|
6197
|
+
&& (this.nativePaneLifecycle ? this.nativePaneLifecycle.mcpProof() === "dead" : mcpServerState(this.instanceDir).state === "dead")) {
|
|
5906
6198
|
this.turnReplyGuard.complete(turn.generation);
|
|
5907
6199
|
void this.sendProxyReply(pane, turn.target);
|
|
5908
6200
|
return;
|
|
@@ -6279,7 +6571,7 @@ export class Daemon extends EventEmitter {
|
|
|
6279
6571
|
}
|
|
6280
6572
|
});
|
|
6281
6573
|
}
|
|
6282
|
-
if (!this.
|
|
6574
|
+
if (!this.fleetTransportAvailable()) {
|
|
6283
6575
|
this.logger.error(`${logLabel} failed — no adapter or fleet IPC route`);
|
|
6284
6576
|
return Promise.resolve(false);
|
|
6285
6577
|
}
|
|
@@ -6303,7 +6595,7 @@ export class Daemon extends EventEmitter {
|
|
|
6303
6595
|
resolve(delivered);
|
|
6304
6596
|
});
|
|
6305
6597
|
try {
|
|
6306
|
-
this.
|
|
6598
|
+
this.broadcastFleetMessage({
|
|
6307
6599
|
type: "fleet_outbound",
|
|
6308
6600
|
tool: "reply",
|
|
6309
6601
|
args,
|
|
@@ -6874,7 +7166,7 @@ export class Daemon extends EventEmitter {
|
|
|
6874
7166
|
if (next === this.currentActivity)
|
|
6875
7167
|
return;
|
|
6876
7168
|
this.currentActivity = next;
|
|
6877
|
-
this.
|
|
7169
|
+
this.broadcastFleetMessage({
|
|
6878
7170
|
type: "instance_activity",
|
|
6879
7171
|
instanceName: this.name,
|
|
6880
7172
|
activity: next,
|
|
@@ -6951,7 +7243,7 @@ export class Daemon extends EventEmitter {
|
|
|
6951
7243
|
else
|
|
6952
7244
|
this.config.auto_pause_after = nextAutoPause;
|
|
6953
7245
|
if (nextAutoPause !== previousAutoPause) {
|
|
6954
|
-
const isGeneral = this.config.general_topic === true || this.name
|
|
7246
|
+
const isGeneral = this.config.general_topic === true || generalByName(this.name, this.config);
|
|
6955
7247
|
this.autoPauseController.reconfigure((isGeneral ? 0 : nextAutoPause) * 60_000);
|
|
6956
7248
|
}
|
|
6957
7249
|
}
|
|
@@ -7031,7 +7323,7 @@ export class Daemon extends EventEmitter {
|
|
|
7031
7323
|
return;
|
|
7032
7324
|
this.lastProgressBroadcast = rendered;
|
|
7033
7325
|
this.lastProgressBroadcastAt = Date.now();
|
|
7034
|
-
this.
|
|
7326
|
+
this.broadcastFleetMessage({
|
|
7035
7327
|
type: "instance_progress",
|
|
7036
7328
|
instanceName: this.name,
|
|
7037
7329
|
progress: rendered,
|
|
@@ -7047,7 +7339,7 @@ export class Daemon extends EventEmitter {
|
|
|
7047
7339
|
if (this.lastProgressBroadcast !== "") {
|
|
7048
7340
|
this.lastProgressBroadcast = "";
|
|
7049
7341
|
this.lastProgressBroadcastAt = Date.now();
|
|
7050
|
-
this.
|
|
7342
|
+
this.broadcastFleetMessage({ type: "instance_progress", instanceName: this.name, progress: "" });
|
|
7051
7343
|
}
|
|
7052
7344
|
}
|
|
7053
7345
|
/**
|
|
@@ -7212,7 +7504,7 @@ export class Daemon extends EventEmitter {
|
|
|
7212
7504
|
reportCrossInstanceDeliveryFailure(meta, verdict, error) {
|
|
7213
7505
|
if (!meta.from_instance)
|
|
7214
7506
|
return;
|
|
7215
|
-
this.
|
|
7507
|
+
this.broadcastFleetMessage({
|
|
7216
7508
|
type: "cross_instance_delivery_failed",
|
|
7217
7509
|
senderSession: meta.from_instance,
|
|
7218
7510
|
targetInstance: this.name,
|
|
@@ -8572,13 +8864,14 @@ export class Daemon extends EventEmitter {
|
|
|
8572
8864
|
if (this.remoteCardAnswering)
|
|
8573
8865
|
return { outcome: "refused", reason: "another answer to this card is being keyed in" };
|
|
8574
8866
|
this.remoteCardAnswering = true;
|
|
8867
|
+
let answerScreen = null;
|
|
8575
8868
|
try {
|
|
8576
8869
|
const outcome = await answerRemoteCard({
|
|
8577
8870
|
locked: fn => this.paneWriteLock.run(fn),
|
|
8578
8871
|
// An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
|
|
8579
|
-
read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) :
|
|
8580
|
-
key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
|
|
8581
|
-
paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
|
|
8872
|
+
read: async () => answerScreen = this.tmux === tmux ? await tmux.capturePane().catch(() => null) : null,
|
|
8873
|
+
key: key => this.tmux !== tmux || answerScreen === null ? Promise.resolve(false) : key === "Space" ? (this.nativePaneLifecycle ? tmux.sendKeys(" ", { kind: "dialog", screen: answerScreen }) : tmux.sendKeys(" ")) : (this.nativePaneLifecycle ? tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: answerScreen }) : tmux.sendSpecialKey(key)),
|
|
8874
|
+
paste: text => this.tmux !== tmux || answerScreen === null ? Promise.resolve(false) : this.nativePaneLifecycle ? tmux.pasteBuffer(text, { proof: { kind: "dialog", screen: answerScreen } }) : tmux.pasteBuffer(text),
|
|
8582
8875
|
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
8583
8876
|
now: () => performance.now(),
|
|
8584
8877
|
}, pane => {
|
|
@@ -8849,12 +9142,19 @@ export class Daemon extends EventEmitter {
|
|
|
8849
9142
|
if (!check || !this.tmux)
|
|
8850
9143
|
return !check;
|
|
8851
9144
|
try {
|
|
8852
|
-
const
|
|
8853
|
-
|
|
9145
|
+
const inputActive = this.nativePaneLifecycle
|
|
9146
|
+
? this.nativePaneLifecycle.current() && this.nativePaneLifecycle.mcpProof() === "alive"
|
|
9147
|
+
: await this.tmux.getPaneInputMode?.() === "raw";
|
|
9148
|
+
// ConPTY has no POSIX termios mode. Its closed launch plus current helper handshake is the input barrier;
|
|
9149
|
+
// the same strict composer detector below is still required. Never grant native unknown-layout fallback.
|
|
9150
|
+
if (!inputActive) {
|
|
8854
9151
|
this.resetFooterFallback();
|
|
8855
9152
|
return false;
|
|
8856
9153
|
}
|
|
8857
|
-
|
|
9154
|
+
const pane = await this.tmux.capturePane();
|
|
9155
|
+
if (this.nativePaneLifecycle && (!this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"))
|
|
9156
|
+
return false;
|
|
9157
|
+
return this.deliveryInputReadyPane(pane);
|
|
8858
9158
|
}
|
|
8859
9159
|
catch {
|
|
8860
9160
|
this.resetFooterFallback();
|
|
@@ -9628,34 +9928,86 @@ export class Daemon extends EventEmitter {
|
|
|
9628
9928
|
// tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
|
|
9629
9929
|
// the CLI this paste went to is never sent to its successor.
|
|
9630
9930
|
const tmux = this.tmux;
|
|
9931
|
+
const backend = this.backend;
|
|
9932
|
+
const native = !!this.nativePaneLifecycle;
|
|
9631
9933
|
const generation = this.spawnGeneration;
|
|
9632
9934
|
const fence = this.launchFenceEpoch;
|
|
9935
|
+
const windowId = native ? tmux?.getWindowId() : undefined;
|
|
9936
|
+
// M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
|
|
9937
|
+
// tmux object and window — so the spawn generation and launch fence are part of every pane's fence: an Enter meant
|
|
9938
|
+
// for the CLI this paste went to is never sent to its successor. A native Windows pane keeps #2958's full fence too
|
|
9939
|
+
// (its launch can be replaced under the same handle).
|
|
9633
9940
|
const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
|
|
9634
|
-
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent())
|
|
9941
|
+
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent())
|
|
9942
|
+
&& (!native || (!!windowId && !!backend && this.backend === backend && tmux.getWindowId() === windowId));
|
|
9635
9943
|
if (!current())
|
|
9636
9944
|
return false;
|
|
9637
9945
|
// The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9946
|
+
// S-35: say which check withheld Enter.
|
|
9947
|
+
const hold = (reason) => {
|
|
9948
|
+
onInputHeld?.(reason);
|
|
9949
|
+
if (reason === "output_changed" && current() && !this.nativeDeliveryHold())
|
|
9950
|
+
this.nativeEnterHold = {
|
|
9951
|
+
key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence,
|
|
9952
|
+
};
|
|
9953
|
+
if (reason === "pane-in-mode")
|
|
9954
|
+
this.logger.warn({ phase, reason }, "The pane stayed in a tmux mode; Enter withheld");
|
|
9955
|
+
else
|
|
9956
|
+
this.logger.warn({ ...tmux?.getLastInputDiagnostic?.(), phase, reason, retryCount: positiveRefusals }, "Native delivery Enter withheld by its safety check");
|
|
9641
9957
|
return false;
|
|
9642
9958
|
};
|
|
9643
9959
|
// M-2267 (#2971): a pane in a tmux mode (a person left it in copy-mode) would take this Enter. Wait for the mode to
|
|
9644
9960
|
// end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
|
|
9645
|
-
// past the bound.
|
|
9646
|
-
//
|
|
9961
|
+
// past the bound.
|
|
9962
|
+
// Native delivery follows main #2990: only this backend's recognised runtime dialog vetoes Enter.
|
|
9963
|
+
// The fresh capture still binds the native host output sequence; ownership and positive-ACK-only retries remain.
|
|
9647
9964
|
const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
|
|
9965
|
+
let pane;
|
|
9966
|
+
let positiveRefusals = 0, enterMarked = false, positiveRetryDeadline = Infinity, linuxDialogRead = false;
|
|
9648
9967
|
for (;;) {
|
|
9649
|
-
const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
|
|
9968
|
+
const mode = await this.waitWhilePaneInMode(phase, current, Math.min(modeDeadline, positiveRetryDeadline));
|
|
9650
9969
|
if (mode === "stale")
|
|
9651
9970
|
return false;
|
|
9652
9971
|
if (mode === "held")
|
|
9653
|
-
return hold();
|
|
9972
|
+
return hold("pane-in-mode");
|
|
9973
|
+
if (native) {
|
|
9974
|
+
// S-35: a native pane's capture refuses while output is still arriving ("Pane changed during capture; observe it
|
|
9975
|
+
// again"). Right after a turn the CLI repaints for a moment; one refused read left the message unsent for ever.
|
|
9976
|
+
// Read again a few times — reading types nothing — and still hold if it never settles.
|
|
9977
|
+
let captured;
|
|
9978
|
+
for (let attempt = 0; captured === undefined; attempt++) {
|
|
9979
|
+
try {
|
|
9980
|
+
captured = await tmux.capturePane();
|
|
9981
|
+
}
|
|
9982
|
+
catch {
|
|
9983
|
+
if (!current())
|
|
9984
|
+
return false;
|
|
9985
|
+
if (attempt >= 4)
|
|
9986
|
+
return hold("capture_failed");
|
|
9987
|
+
await new Promise(done => setTimeout(done, 150 * (attempt + 1)));
|
|
9988
|
+
if (!current())
|
|
9989
|
+
return false;
|
|
9990
|
+
}
|
|
9991
|
+
}
|
|
9992
|
+
const screen = captured;
|
|
9993
|
+
pane = screen;
|
|
9994
|
+
if (!current())
|
|
9995
|
+
return false;
|
|
9996
|
+
try {
|
|
9997
|
+
if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
|
|
9998
|
+
return hold("dialog");
|
|
9999
|
+
}
|
|
10000
|
+
catch {
|
|
10001
|
+
return hold("check_failed");
|
|
10002
|
+
}
|
|
10003
|
+
if (!current())
|
|
10004
|
+
return false;
|
|
10005
|
+
}
|
|
9654
10006
|
if (await this.paneInTmuxMode(phase)) {
|
|
9655
10007
|
if (!current())
|
|
9656
10008
|
return false;
|
|
9657
10009
|
if (performance.now() >= modeDeadline)
|
|
9658
|
-
return hold();
|
|
10010
|
+
return hold("pane-in-mode");
|
|
9659
10011
|
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9660
10012
|
if (!current())
|
|
9661
10013
|
return false;
|
|
@@ -9663,54 +10015,143 @@ export class Daemon extends EventEmitter {
|
|
|
9663
10015
|
}
|
|
9664
10016
|
if (!current())
|
|
9665
10017
|
return false;
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
10018
|
+
// M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468; main): on a Linux pane, a dialog the
|
|
10019
|
+
// backend RECOGNISES (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the
|
|
10020
|
+
// paste and this Enter would take the key as its answer. Read the pane once more: such a dialog withholds the
|
|
10021
|
+
// Enter (the caller keeps the delivery uncertain → Needs-you; the dialog scanner parks it for a person). Nothing
|
|
10022
|
+
// else is judged — an unknown or unreadable screen sends: #2958's composer proof (decision f57474aa) is NOT back.
|
|
10023
|
+
// Only a caller that can hold asks (onInputHeld); a native pane read its own screen above, every round.
|
|
10024
|
+
if (!native && onInputHeld && !linuxDialogRead) {
|
|
10025
|
+
linuxDialogRead = true;
|
|
10026
|
+
let finalPane = null;
|
|
10027
|
+
try {
|
|
10028
|
+
finalPane = await tmux.capturePane();
|
|
10029
|
+
}
|
|
10030
|
+
catch {
|
|
10031
|
+
finalPane = null;
|
|
10032
|
+
}
|
|
10033
|
+
if (!current())
|
|
10034
|
+
return false;
|
|
10035
|
+
let dialog;
|
|
10036
|
+
try {
|
|
10037
|
+
dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
|
|
10038
|
+
}
|
|
10039
|
+
catch {
|
|
10040
|
+
dialog = undefined;
|
|
10041
|
+
}
|
|
10042
|
+
if (dialog !== undefined) {
|
|
10043
|
+
onInputHeld("dialog");
|
|
10044
|
+
this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
|
|
10045
|
+
return false;
|
|
10046
|
+
}
|
|
10047
|
+
}
|
|
10048
|
+
if (positiveRefusals && performance.now() >= positiveRetryDeadline)
|
|
10049
|
+
return hold("output_changed");
|
|
10050
|
+
// W1: persist the "Enter may have happened" boundary before tmux receives
|
|
10051
|
+
// the key. A crash after this commit is never auto-replayed without a
|
|
10052
|
+
// positive transcript marker, even if the stale pane still shows composer.
|
|
10053
|
+
if (!enterMarked && durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
|
|
10054
|
+
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
10055
|
+
return false;
|
|
10056
|
+
}
|
|
10057
|
+
enterMarked = true;
|
|
10058
|
+
const sent = await (native && pane !== undefined ? tmux.sendSpecialKey("Enter", undefined, { kind: "delivery", screen: pane, retryCount: positiveRefusals }) : tmux.sendSpecialKey("Enter"));
|
|
10059
|
+
// A busy renderer can change seq after our capture. Only a positive zero-input ACK admits a NEW proof/key;
|
|
10060
|
+
// An Enter retry never re-pastes, retries an uncertain write, reuses stale proof or re-commits the durable boundary.
|
|
10061
|
+
if (!sent && native && tmux.lastInputWasNotSent?.() === true
|
|
10062
|
+
&& tmux.getLastSendSpecialKeyError() === "Pane changed after observation; input was not sent"
|
|
10063
|
+
&& current()) {
|
|
10064
|
+
if (!positiveRefusals)
|
|
10065
|
+
positiveRetryDeadline = Math.min(modeDeadline, performance.now() + 2000);
|
|
10066
|
+
if (positiveRefusals >= 4 || performance.now() >= positiveRetryDeadline)
|
|
10067
|
+
return hold("output_changed");
|
|
10068
|
+
positiveRefusals++;
|
|
10069
|
+
await new Promise(done => setTimeout(done, 100));
|
|
10070
|
+
if (!current())
|
|
10071
|
+
return false;
|
|
10072
|
+
if (performance.now() >= positiveRetryDeadline)
|
|
10073
|
+
return hold("output_changed");
|
|
10074
|
+
continue;
|
|
10075
|
+
}
|
|
10076
|
+
if (!sent) {
|
|
10077
|
+
const details = { phase, tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure" };
|
|
10078
|
+
if (native)
|
|
10079
|
+
this.logger.warn({ ...tmux?.getLastInputDiagnostic?.(), ...details, retryCount: positiveRefusals }, "Native delivery Enter gave up");
|
|
10080
|
+
else
|
|
10081
|
+
this.logger.error(details, "tmux send-keys Enter failed during message delivery");
|
|
10082
|
+
}
|
|
10083
|
+
if (sent && native && current())
|
|
10084
|
+
this.nativeEnterHold = null;
|
|
10085
|
+
return sent;
|
|
10086
|
+
}
|
|
10087
|
+
}
|
|
10088
|
+
/** A pre-existing native busy-paste race: retry bytes only after an exact, owned zero-input acknowledgement. */
|
|
10089
|
+
async sendDeliveryPaste(text, stillCurrent) {
|
|
10090
|
+
const pane = this.tmux, native = this.nativePaneLifecycle;
|
|
10091
|
+
if (!native)
|
|
10092
|
+
return pane.pasteBuffer(text);
|
|
10093
|
+
const backend = this.backend, generation = this.spawnGeneration, fence = this.launchFenceEpoch, windowId = pane.getWindowId();
|
|
10094
|
+
const current = () => !!windowId && this.tmux === pane && this.nativePaneLifecycle === native && this.backend === backend
|
|
10095
|
+
&& this.spawnGeneration === generation && this.launchFenceEpoch === fence && pane.getWindowId() === windowId
|
|
10096
|
+
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
|
|
10097
|
+
let retryCount = 0;
|
|
10098
|
+
const hold = (reason) => {
|
|
10099
|
+
if (current() && !this.nativeDeliveryHold())
|
|
10100
|
+
this.nativeEnterHold = { key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence };
|
|
10101
|
+
this.logger.warn({ ...pane.getLastInputDiagnostic?.(), reason, retryCount }, "Native delivery paste held by its safety check");
|
|
10102
|
+
return false;
|
|
10103
|
+
};
|
|
10104
|
+
const deadline = performance.now() + 2000;
|
|
10105
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
10106
|
+
retryCount = attempt;
|
|
10107
|
+
if (!current())
|
|
10108
|
+
return false;
|
|
10109
|
+
if (performance.now() >= deadline)
|
|
10110
|
+
return hold("output_changed");
|
|
10111
|
+
let screen;
|
|
10112
|
+
for (let read = 0; read < 5 && screen === undefined; read++) {
|
|
10113
|
+
try {
|
|
10114
|
+
screen = await pane.capturePane();
|
|
10115
|
+
}
|
|
10116
|
+
catch { /* Reads do not construct an input operation. */ }
|
|
10117
|
+
if (!current())
|
|
10118
|
+
return false;
|
|
10119
|
+
if (performance.now() >= deadline)
|
|
10120
|
+
return hold("capture_failed");
|
|
10121
|
+
if (screen === undefined && read < 4)
|
|
10122
|
+
await new Promise(done => setTimeout(done, 100));
|
|
10123
|
+
}
|
|
10124
|
+
if (screen === undefined)
|
|
10125
|
+
return hold("capture_failed");
|
|
9678
10126
|
try {
|
|
9679
|
-
|
|
10127
|
+
if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
|
|
10128
|
+
return hold("dialog");
|
|
9680
10129
|
}
|
|
9681
10130
|
catch {
|
|
9682
|
-
|
|
10131
|
+
return hold("check_failed");
|
|
9683
10132
|
}
|
|
9684
10133
|
if (!current())
|
|
9685
10134
|
return false;
|
|
9686
|
-
|
|
10135
|
+
if (performance.now() >= deadline)
|
|
10136
|
+
return hold("output_changed");
|
|
10137
|
+
let sent;
|
|
9687
10138
|
try {
|
|
9688
|
-
|
|
10139
|
+
sent = await pane.pasteBuffer(text, { guard: current, proof: { kind: "composer", screen, retryCount: attempt } });
|
|
9689
10140
|
}
|
|
9690
10141
|
catch {
|
|
9691
|
-
|
|
10142
|
+
return hold("unconfirmed");
|
|
9692
10143
|
}
|
|
9693
|
-
if (
|
|
9694
|
-
|
|
9695
|
-
|
|
10144
|
+
if (sent)
|
|
10145
|
+
return true; // Exactly one successful paste ends the loop, before any Enter.
|
|
10146
|
+
if (!current())
|
|
9696
10147
|
return false;
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
if (durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
|
|
9703
|
-
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
9704
|
-
return false;
|
|
9705
|
-
}
|
|
9706
|
-
const sent = await tmux.sendSpecialKey("Enter");
|
|
9707
|
-
if (!sent) {
|
|
9708
|
-
this.logger.error({
|
|
9709
|
-
phase,
|
|
9710
|
-
tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
|
|
9711
|
-
}, "tmux send-keys Enter failed during message delivery");
|
|
10148
|
+
if (pane.lastInputWasNotSent?.() !== true || pane.getLastPasteError?.() !== "Pane changed after observation; input was not sent")
|
|
10149
|
+
return hold("unconfirmed");
|
|
10150
|
+
if (attempt >= 4 || performance.now() >= deadline)
|
|
10151
|
+
return hold("output_changed");
|
|
10152
|
+
await new Promise(done => setTimeout(done, 100));
|
|
9712
10153
|
}
|
|
9713
|
-
return
|
|
10154
|
+
return hold("output_changed");
|
|
9714
10155
|
}
|
|
9715
10156
|
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
|
|
9716
10157
|
// Last and defaulted so the positional callers that ignore the outcome stay
|
|
@@ -9748,6 +10189,9 @@ export class Daemon extends EventEmitter {
|
|
|
9748
10189
|
const tmuxAtWrite = this.tmux;
|
|
9749
10190
|
const steerCurrent = () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping
|
|
9750
10191
|
&& this.tmux === tmuxAtWrite && this.spawnGeneration === spawnAtWrite;
|
|
10192
|
+
// Alpha (S-36, Prism #2989 P2): a NATIVE pane keeps #2958's pre-paste owner fence through the initial Enter and every
|
|
10193
|
+
// retry Enter (a pane replaced while the paste awaits never takes this delivery's Enter); a tmux pane follows main.
|
|
10194
|
+
const nativeOwner = this.nativePaneLifecycle ? steerCurrent : undefined;
|
|
9751
10195
|
// Bug A: paste with backoff. Transient failures are usually a stale window id
|
|
9752
10196
|
// after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
|
|
9753
10197
|
const maxAttempts = 3;
|
|
@@ -9815,8 +10259,8 @@ export class Daemon extends EventEmitter {
|
|
|
9815
10259
|
}
|
|
9816
10260
|
verdict.durableBeginCommitted = true;
|
|
9817
10261
|
verdict.submissionMode = attemptEvidence.submissionMode;
|
|
9818
|
-
if (attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
|
|
9819
|
-
verdict.transcriptCheckpoint = {
|
|
10262
|
+
if (attemptEvidence.runtimeTranscript || attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
|
|
10263
|
+
verdict.transcriptCheckpoint = attemptEvidence.runtimeTranscript ? { backend: attemptEvidence.backend, runtime: attemptEvidence.runtimeTranscript } : {
|
|
9820
10264
|
backend: attemptEvidence.backend,
|
|
9821
10265
|
path: attemptEvidence.transcriptPath,
|
|
9822
10266
|
offset: attemptEvidence.transcriptOffset,
|
|
@@ -9881,7 +10325,15 @@ export class Daemon extends EventEmitter {
|
|
|
9881
10325
|
}
|
|
9882
10326
|
const pasteGeneration = this.spawnGeneration;
|
|
9883
10327
|
beforePaste?.();
|
|
9884
|
-
const pasted = await this.
|
|
10328
|
+
const pasted = await this.sendDeliveryPaste(formatted, nativeOwner);
|
|
10329
|
+
if (!pasted && this.nativePaneLifecycle) {
|
|
10330
|
+
// The native write attempt is retained as uncertain, including exhausted positive refusals. Never route an
|
|
10331
|
+
// unknown/partial write through legacy window recovery or abort it back to an automatically replayable row.
|
|
10332
|
+
verdict.paneWriteStarted = true;
|
|
10333
|
+
verdict.phase = "paste";
|
|
10334
|
+
verdict.proof = "native-paste-held";
|
|
10335
|
+
return false;
|
|
10336
|
+
}
|
|
9885
10337
|
if (pasted)
|
|
9886
10338
|
this.rememberPaste(formatted, pasteGeneration);
|
|
9887
10339
|
if (!pasted) {
|
|
@@ -9968,15 +10420,18 @@ export class Daemon extends EventEmitter {
|
|
|
9968
10420
|
const enterMono = performance.now();
|
|
9969
10421
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
9970
10422
|
let inputHeld = null;
|
|
9971
|
-
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
|
|
10423
|
+
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof || nativeOwner ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
|
|
9972
10424
|
if (inputHeld) {
|
|
9973
10425
|
// #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
|
|
9974
10426
|
// the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
|
|
9975
10427
|
verdict.phase = "submit-enter";
|
|
9976
|
-
|
|
10428
|
+
// The merge of #2957/M-2290 (main) and the alpha's native holds: a tmux mode, a recognised dialog on a Linux pane,
|
|
10429
|
+
// and each native safety check say which.
|
|
10430
|
+
verdict.proof = inputHeld === "pane-in-mode" ? "pane-in-mode-held" : nativeOwner ? `native-input-${inputHeld}`
|
|
10431
|
+
: inputHeld === "dialog" ? "dialog-on-pane" : `input-held-${inputHeld}`;
|
|
9977
10432
|
return false;
|
|
9978
10433
|
}
|
|
9979
|
-
if (steerComposerProof && !steerCurrent()) {
|
|
10434
|
+
if ((steerComposerProof || nativeOwner) && !steerCurrent()) {
|
|
9980
10435
|
verdict.phase = "submit-enter";
|
|
9981
10436
|
verdict.proof = "fenced-before-enter";
|
|
9982
10437
|
return false;
|
|
@@ -10005,7 +10460,7 @@ export class Daemon extends EventEmitter {
|
|
|
10005
10460
|
if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
|
|
10006
10461
|
await new Promise(r => setTimeout(r, 1_000));
|
|
10007
10462
|
const retryAt = Date.now();
|
|
10008
|
-
if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
|
|
10463
|
+
if (await this.sendDeliveryEnter("queue-less-defensive-retry", nativeOwner)) {
|
|
10009
10464
|
enterAt = retryAt;
|
|
10010
10465
|
enterRetry = true;
|
|
10011
10466
|
this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
|
|
@@ -10138,7 +10593,7 @@ export class Daemon extends EventEmitter {
|
|
|
10138
10593
|
// again. The prompt is back now, so an Enter can land.
|
|
10139
10594
|
this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
|
|
10140
10595
|
const strandedAt = Date.now();
|
|
10141
|
-
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
|
|
10596
|
+
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", nativeOwner))) {
|
|
10142
10597
|
return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
|
|
10143
10598
|
}
|
|
10144
10599
|
// The recovery Enter can be accepted before Codex paints its new
|
|
@@ -10191,7 +10646,7 @@ export class Daemon extends EventEmitter {
|
|
|
10191
10646
|
}
|
|
10192
10647
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
10193
10648
|
const retryAt = Date.now();
|
|
10194
|
-
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
|
|
10649
|
+
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", nativeOwner))) {
|
|
10195
10650
|
return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
|
|
10196
10651
|
}
|
|
10197
10652
|
if (windowId && this.controlClient) {
|
|
@@ -10243,7 +10698,7 @@ export class Daemon extends EventEmitter {
|
|
|
10243
10698
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
10244
10699
|
const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
|
|
10245
10700
|
const retryAt = Date.now();
|
|
10246
|
-
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
|
|
10701
|
+
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", nativeOwner))) {
|
|
10247
10702
|
return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
|
|
10248
10703
|
}
|
|
10249
10704
|
submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
|
|
@@ -10331,7 +10786,7 @@ export class Daemon extends EventEmitter {
|
|
|
10331
10786
|
}
|
|
10332
10787
|
// No control client to observe output: fall back to the legacy double-Enter.
|
|
10333
10788
|
await new Promise(r => setTimeout(r, 1000));
|
|
10334
|
-
await this.sendDeliveryEnter("unobserved-defensive-retry");
|
|
10789
|
+
await this.sendDeliveryEnter("unobserved-defensive-retry", nativeOwner);
|
|
10335
10790
|
if (status)
|
|
10336
10791
|
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10337
10792
|
}
|
|
@@ -10551,14 +11006,22 @@ export class Daemon extends EventEmitter {
|
|
|
10551
11006
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
10552
11007
|
*/
|
|
10553
11008
|
transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
|
|
11009
|
+
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
11010
|
+
throw Error("Native proof cannot read a coordinator-local transcript path");
|
|
10554
11011
|
const launchAtWrite = this.launchFenceEpoch;
|
|
10555
11012
|
return {
|
|
10556
|
-
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
11013
|
+
reader: checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
10557
11014
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
10558
|
-
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
|
|
11015
|
+
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
|
|
11016
|
+
&& (!checkpoint.runtime || this.nativePaneLifecycle?.current(checkpoint.runtime) === true),
|
|
10559
11017
|
onProof,
|
|
10560
11018
|
};
|
|
10561
11019
|
}
|
|
11020
|
+
nativeTranscriptReader(reference, needle) {
|
|
11021
|
+
if (!this.nativePaneLifecycle || !this.nativePaneLifecycle.current(reference))
|
|
11022
|
+
throw Error("Native delivery transcript owner is unavailable");
|
|
11023
|
+
return this.nativePaneLifecycle.proofReader(reference, needle);
|
|
11024
|
+
}
|
|
10562
11025
|
/** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
|
|
10563
11026
|
cliLabel() {
|
|
10564
11027
|
return this.config.backend ?? this.backend?.binaryName ?? "CLI";
|
|
@@ -10802,7 +11265,14 @@ export class Daemon extends EventEmitter {
|
|
|
10802
11265
|
* (the retries included). Without a guard it is exactly the old unconditional path.
|
|
10803
11266
|
*/
|
|
10804
11267
|
async submitSystemPaste(text, label, guard, admission) {
|
|
10805
|
-
|
|
11268
|
+
// Alpha (S-36, Prism #2989 P2): a NATIVE pane's system paste keeps the pane owner from before its first await
|
|
11269
|
+
// (#2958's fence, reverted on main for tmux panes) — a pane replaced while the paste awaits never takes its Enter.
|
|
11270
|
+
const native = !!this.nativePaneLifecycle;
|
|
11271
|
+
const ownerTmux = this.tmux, ownerGeneration = this.spawnGeneration, ownerFence = this.launchFenceEpoch;
|
|
11272
|
+
const ownerWindow = native ? ownerTmux?.getWindowId() : undefined;
|
|
11273
|
+
const owner = () => !native || (!!ownerTmux && !!ownerWindow && this.tmux === ownerTmux && ownerTmux.getWindowId() === ownerWindow
|
|
11274
|
+
&& this.spawnGeneration === ownerGeneration && this.launchFenceEpoch === ownerFence && !this.deliveryWritesStopping);
|
|
11275
|
+
const current = guard || admission || native ? () => { admission?.(); return owner() && (guard?.current() ?? true); } : undefined;
|
|
10806
11276
|
if (current && !current())
|
|
10807
11277
|
return false;
|
|
10808
11278
|
if (!this.tmux)
|
|
@@ -10922,13 +11392,13 @@ export class Daemon extends EventEmitter {
|
|
|
10922
11392
|
&& sameInteractionOwner(recoveryOwner, this.interactionOwner());
|
|
10923
11393
|
const previousWindowId = this.tmux?.getWindowId();
|
|
10924
11394
|
try {
|
|
10925
|
-
const windows = await
|
|
11395
|
+
const windows = await this.paneHost.listWindows(this.tmuxSessionName, this.controlClient);
|
|
10926
11396
|
if (!recoveryCurrent() || this.tmux !== previousTmux)
|
|
10927
11397
|
return undefined;
|
|
10928
11398
|
const match = windows.find(w => w.name === this.name);
|
|
10929
11399
|
if (!match)
|
|
10930
11400
|
return undefined;
|
|
10931
|
-
this.tmux =
|
|
11401
|
+
this.tmux = this.paneHost.bind(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
10932
11402
|
const recoveredTmux = this.tmux;
|
|
10933
11403
|
writeFileSync(join(this.instanceDir, "window-id"), match.id);
|
|
10934
11404
|
// The window we were talking to is gone; leaving it registered means the
|
|
@@ -10991,6 +11461,8 @@ export class Daemon extends EventEmitter {
|
|
|
10991
11461
|
return false;
|
|
10992
11462
|
}
|
|
10993
11463
|
getWindowId() {
|
|
11464
|
+
if (this.nativePaneLifecycle)
|
|
11465
|
+
return this.nativePaneLifecycle.current() ? this.tmux?.getWindowId() : undefined;
|
|
10994
11466
|
try {
|
|
10995
11467
|
return readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() || undefined;
|
|
10996
11468
|
}
|
|
@@ -11022,7 +11494,55 @@ export class Daemon extends EventEmitter {
|
|
|
11022
11494
|
* Handle a tool call from the MCP server (forwarded by Claude).
|
|
11023
11495
|
* Routes to the channel adapter via MessageBus.
|
|
11024
11496
|
*/
|
|
11025
|
-
|
|
11497
|
+
/** Internal native-MCP admission. The pane/machine/launch owner supplies a current, dynamically checked tool lease. */
|
|
11498
|
+
handlePaneToolCall(input, lease) {
|
|
11499
|
+
const refusal = nativePaneToolRefusal(input.tool, input.args);
|
|
11500
|
+
if (refusal)
|
|
11501
|
+
return Promise.resolve({ result: null, error: refusal });
|
|
11502
|
+
if (!lease.current() || !lease.allowed(input.tool, input.args) || this.deliveryWritesStopping)
|
|
11503
|
+
return Promise.resolve({ result: null, error: "Pane tool permission is no longer current" });
|
|
11504
|
+
const fence = this.launchFenceEpoch;
|
|
11505
|
+
const current = () => lease.current() && lease.allowed(input.tool, input.args) && !this.deliveryWritesStopping && fence === this.launchFenceEpoch;
|
|
11506
|
+
return new Promise(resolve => {
|
|
11507
|
+
let settled = false;
|
|
11508
|
+
const finish = (value) => { if (settled)
|
|
11509
|
+
return; settled = true; clearTimeout(timer); resolve(value); };
|
|
11510
|
+
const timer = setTimeout(() => finish({ result: null, error: "Pane tool result is unconfirmed; inspect its operation before retrying" }), daemonBudgetMs(input.tool) + 1000);
|
|
11511
|
+
timer.unref?.();
|
|
11512
|
+
try {
|
|
11513
|
+
this.handleToolCall({ ...input, requestId: ++this.fleetRequestSeq }, undefined, {
|
|
11514
|
+
current,
|
|
11515
|
+
dispatch: (tool, args, respond) => {
|
|
11516
|
+
if (!current()) {
|
|
11517
|
+
respond(null, "Pane tool permission was revoked before dispatch");
|
|
11518
|
+
return true;
|
|
11519
|
+
}
|
|
11520
|
+
const dispatched = lease.dispatch?.(tool, args);
|
|
11521
|
+
if (!dispatched)
|
|
11522
|
+
return false;
|
|
11523
|
+
void dispatched.then(result => respond(result), () => respond(null, "Pane tool operation was refused"));
|
|
11524
|
+
return true;
|
|
11525
|
+
},
|
|
11526
|
+
// Resolving the relay is not proof that its HTTP response reached the provider. No response-written receipt yet.
|
|
11527
|
+
send: value => {
|
|
11528
|
+
finish({ result: value.result, ...(typeof value.error === "string" ? { error: value.error } : {}),
|
|
11529
|
+
...(typeof value.errorCode === "string" || typeof value.errorCode === "number" ? { errorCode: value.errorCode } : {}) });
|
|
11530
|
+
return false;
|
|
11531
|
+
},
|
|
11532
|
+
});
|
|
11533
|
+
}
|
|
11534
|
+
catch {
|
|
11535
|
+
finish({ result: null, error: "Pane tool operation failed" });
|
|
11536
|
+
}
|
|
11537
|
+
});
|
|
11538
|
+
}
|
|
11539
|
+
/** Only the authority lease with a verified helper stdout ACK calls this internal barrier. */
|
|
11540
|
+
paneToolResponseWritten(operationId, current) {
|
|
11541
|
+
if (!current() || this.deliveryWritesStopping || !/^[a-f0-9-]{36}$/i.test(operationId))
|
|
11542
|
+
return;
|
|
11543
|
+
this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
|
|
11544
|
+
}
|
|
11545
|
+
handleToolCall(msg, socket, peer) {
|
|
11026
11546
|
const tool = msg.tool;
|
|
11027
11547
|
const args = (msg.args ?? {});
|
|
11028
11548
|
const requestId = msg.requestId;
|
|
@@ -11030,20 +11550,20 @@ export class Daemon extends EventEmitter {
|
|
|
11030
11550
|
this.logger.debug({ tool, requestId }, "Tool call from MCP server");
|
|
11031
11551
|
// A tool call can only come from a live MCP server of ours — proof of life
|
|
11032
11552
|
// that outranks whatever the pid slot says.
|
|
11033
|
-
if (this.socketSessionNames.get(socket) === this.name) {
|
|
11034
|
-
this.noteMcpProofOfLife("tool_call", this.socketPids.get(socket));
|
|
11553
|
+
if (peer?.current() || socket && this.socketSessionNames.get(socket) === this.name) {
|
|
11554
|
+
this.noteMcpProofOfLife("tool_call", socket ? this.socketPids.get(socket) : undefined);
|
|
11035
11555
|
}
|
|
11036
11556
|
// A tool invocation is not delivery evidence. Capture which obligation it
|
|
11037
11557
|
// belongs to now, then settle it only after the adapter/fleet responds.
|
|
11038
11558
|
// Explicitly mapped sibling sessions must not satisfy this instance's turn;
|
|
11039
11559
|
// an unmapped socket is retained for legacy/tests where mcp_ready was not
|
|
11040
11560
|
// observed before the first tool call.
|
|
11041
|
-
const sourceSession = this.socketSessionNames.get(socket);
|
|
11561
|
+
const sourceSession = peer ? this.name : socket ? this.socketSessionNames.get(socket) : undefined;
|
|
11042
11562
|
const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
|
|
11043
11563
|
? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
|
|
11044
11564
|
: null;
|
|
11045
11565
|
// For now, log and respond. Full adapter routing will be wired in fleet manager.
|
|
11046
|
-
const respond = (result, error) => {
|
|
11566
|
+
const respond = (result, error, errorCode) => {
|
|
11047
11567
|
// A message that verifiably went out stands down the dead-MCP proxy reply
|
|
11048
11568
|
// for this turn: the agent proved it can still speak for itself.
|
|
11049
11569
|
if (!error && result != null && TURN_OUTBOUND_TOOLS.has(tool)) {
|
|
@@ -11084,14 +11604,21 @@ export class Daemon extends EventEmitter {
|
|
|
11084
11604
|
// failed" and report unknown immediately rather than starting recovery.
|
|
11085
11605
|
this.turnReplyGuard.settleToolAttempt(replyAttempt, false);
|
|
11086
11606
|
}
|
|
11087
|
-
const
|
|
11607
|
+
const packet = { requestId, result, error, operationId, ...(errorCode !== undefined ? { errorCode } : {}) };
|
|
11608
|
+
const sent = peer ? peer.current() && peer.send(packet) : socket ? this.ipcServer?.send(socket, packet) ?? false : false;
|
|
11088
11609
|
if (!sent) {
|
|
11089
|
-
|
|
11610
|
+
if (peer)
|
|
11611
|
+
this.logger.debug({ tool, requestId }, "Native MCP result awaits transport-written confirmation");
|
|
11612
|
+
else
|
|
11613
|
+
this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
|
|
11090
11614
|
}
|
|
11091
11615
|
else if (operationId) {
|
|
11092
11616
|
this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
|
|
11093
11617
|
}
|
|
11094
11618
|
};
|
|
11619
|
+
// Native Fleet/task operations carry their lease to the existing dispatcher mutation admission points.
|
|
11620
|
+
if (peer?.dispatch?.(tool, args, respond))
|
|
11621
|
+
return;
|
|
11095
11622
|
// Repo checkout — handled locally in daemon (no fleet-manager)
|
|
11096
11623
|
if (tool === "checkout_repo") {
|
|
11097
11624
|
this.handleCheckoutRepo(args, respond);
|
|
@@ -11120,7 +11647,7 @@ export class Daemon extends EventEmitter {
|
|
|
11120
11647
|
clearTimeout(timeout);
|
|
11121
11648
|
respond(respMsg.result, respMsg.error);
|
|
11122
11649
|
});
|
|
11123
|
-
this.
|
|
11650
|
+
this.broadcastFleetMessage({
|
|
11124
11651
|
type,
|
|
11125
11652
|
payload: args,
|
|
11126
11653
|
meta: { instance_name: this.name },
|
|
@@ -11146,7 +11673,7 @@ export class Daemon extends EventEmitter {
|
|
|
11146
11673
|
clearTimeout(timeout);
|
|
11147
11674
|
resolve({ result: respMsg.result, error: respMsg.error });
|
|
11148
11675
|
});
|
|
11149
|
-
this.
|
|
11676
|
+
this.broadcastFleetMessage({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
|
|
11150
11677
|
});
|
|
11151
11678
|
void (async () => {
|
|
11152
11679
|
if (tool === "preview_web_appearance") {
|
|
@@ -11223,7 +11750,7 @@ export class Daemon extends EventEmitter {
|
|
|
11223
11750
|
clearTimeout(timeout);
|
|
11224
11751
|
respond(respMsg.result, respMsg.error);
|
|
11225
11752
|
});
|
|
11226
|
-
this.
|
|
11753
|
+
this.broadcastFleetMessage({
|
|
11227
11754
|
type: "fleet_task",
|
|
11228
11755
|
payload: args,
|
|
11229
11756
|
meta: { instance_name: this.name },
|
|
@@ -11246,7 +11773,7 @@ export class Daemon extends EventEmitter {
|
|
|
11246
11773
|
clearTimeout(timeout);
|
|
11247
11774
|
respond(respMsg.result, respMsg.error);
|
|
11248
11775
|
});
|
|
11249
|
-
this.
|
|
11776
|
+
this.broadcastFleetMessage({
|
|
11250
11777
|
type: typeMap[tool],
|
|
11251
11778
|
payload: args,
|
|
11252
11779
|
meta: { instance_name: this.name, working_directory: this.config.working_directory },
|
|
@@ -11273,7 +11800,7 @@ export class Daemon extends EventEmitter {
|
|
|
11273
11800
|
clearTimeout(timeout);
|
|
11274
11801
|
respond(respMsg.result, respMsg.error);
|
|
11275
11802
|
});
|
|
11276
|
-
this.
|
|
11803
|
+
this.broadcastFleetMessage({
|
|
11277
11804
|
type: typeMap[tool],
|
|
11278
11805
|
payload: args,
|
|
11279
11806
|
meta: {
|
|
@@ -11288,11 +11815,11 @@ export class Daemon extends EventEmitter {
|
|
|
11288
11815
|
}
|
|
11289
11816
|
if (CROSS_INSTANCE_TOOLS.has(tool)) {
|
|
11290
11817
|
// Route to fleet manager via IPC (topic mode only)
|
|
11291
|
-
if (this.topicMode && this.
|
|
11818
|
+
if (this.topicMode && this.fleetTransportAvailable()) {
|
|
11292
11819
|
// Use fleetRequestId (not requestId) to avoid MCP server resolving the
|
|
11293
11820
|
// pending tool call prematurely when it receives the broadcast.
|
|
11294
11821
|
const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
|
|
11295
|
-
const senderSessionName =
|
|
11822
|
+
const senderSessionName = sourceSession;
|
|
11296
11823
|
// Shared table, so delete_instance and the deployment tools get the same
|
|
11297
11824
|
// budget here as the MCP client expects (the ad-hoc list omitted them).
|
|
11298
11825
|
const crossTimeoutMs = daemonBudgetMs(tool);
|
|
@@ -11304,7 +11831,7 @@ export class Daemon extends EventEmitter {
|
|
|
11304
11831
|
clearTimeout(timeout);
|
|
11305
11832
|
respond(respMsg.result, respMsg.error);
|
|
11306
11833
|
});
|
|
11307
|
-
this.
|
|
11834
|
+
this.broadcastFleetMessage({
|
|
11308
11835
|
type: "fleet_outbound",
|
|
11309
11836
|
tool,
|
|
11310
11837
|
args,
|
|
@@ -11358,12 +11885,12 @@ export class Daemon extends EventEmitter {
|
|
|
11358
11885
|
}, daemonBudgetMs(tool));
|
|
11359
11886
|
this.pendingIpcRequests.set(outboundKey, (respMsg) => {
|
|
11360
11887
|
clearTimeout(timeout);
|
|
11361
|
-
respond(respMsg.result, respMsg.error);
|
|
11888
|
+
respond(respMsg.result, respMsg.error, respMsg.errorCode);
|
|
11362
11889
|
});
|
|
11363
11890
|
// Register the waiter before broadcasting. A fast/mock adapter can settle
|
|
11364
11891
|
// immediately; broadcasting first left a response-before-registration
|
|
11365
11892
|
// window that degraded into a timeout despite a completed POST.
|
|
11366
|
-
this.
|
|
11893
|
+
this.broadcastFleetMessage({
|
|
11367
11894
|
type: "fleet_outbound",
|
|
11368
11895
|
tool,
|
|
11369
11896
|
args,
|
|
@@ -11494,7 +12021,7 @@ export class Daemon extends EventEmitter {
|
|
|
11494
12021
|
// The daemon used to read `general_topic` alone while `isGeneralInstance`
|
|
11495
12022
|
// also accepted the name, so an instance called `general` without the flag
|
|
11496
12023
|
// was a worker here and a general everywhere else.
|
|
11497
|
-
const isGeneral = this.config.general_topic === true || this.name
|
|
12024
|
+
const isGeneral = this.config.general_topic === true || generalByName(this.name, this.config);
|
|
11498
12025
|
// No third argument for an ordinary instance: the default is `worker` now,
|
|
11499
12026
|
// and naming it here would be a second place to change it.
|
|
11500
12027
|
const toolSet = isGeneral
|
|
@@ -11870,6 +12397,8 @@ export class Daemon extends EventEmitter {
|
|
|
11870
12397
|
catch { /* best effort */ }
|
|
11871
12398
|
}
|
|
11872
12399
|
async spawnClaudeWindow() {
|
|
12400
|
+
if (this.nativePaneLifecycle)
|
|
12401
|
+
throw Error("Native panes never enter local resume/fresh fallback");
|
|
11873
12402
|
this.assertSessionOomAdmission();
|
|
11874
12403
|
this.beginSpawn();
|
|
11875
12404
|
let resumedSuccessfully = false;
|
|
@@ -12456,6 +12985,14 @@ export class Daemon extends EventEmitter {
|
|
|
12456
12985
|
await this.museUsageRelay.stop().catch(() => { });
|
|
12457
12986
|
this.museUsageRelay = null;
|
|
12458
12987
|
}
|
|
12988
|
+
if (this.nativePaneLifecycle) {
|
|
12989
|
+
await this.nativePaneLifecycle.release("detach");
|
|
12990
|
+
try {
|
|
12991
|
+
unlinkSync(join(this.instanceDir, "daemon.pid"));
|
|
12992
|
+
}
|
|
12993
|
+
catch { /* metadata only */ }
|
|
12994
|
+
return;
|
|
12995
|
+
}
|
|
12459
12996
|
try {
|
|
12460
12997
|
await this.killProcessTree("SIGTERM", "startup aborted");
|
|
12461
12998
|
}
|
|
@@ -12485,13 +13022,15 @@ export class Daemon extends EventEmitter {
|
|
|
12485
13022
|
}
|
|
12486
13023
|
/** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
|
|
12487
13024
|
async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
|
|
13025
|
+
if (this.nativePaneLifecycle)
|
|
13026
|
+
throw Error("Native processes can only be closed through their owned runtime pane");
|
|
12488
13027
|
const windowId = this.tmux?.getWindowId();
|
|
12489
13028
|
// Before createWindow succeeds there is no pane owned by this daemon.
|
|
12490
13029
|
// tmux interprets an empty selector as the session's active window.
|
|
12491
13030
|
if (!windowId?.trim())
|
|
12492
13031
|
return;
|
|
12493
13032
|
try {
|
|
12494
|
-
const pid = await
|
|
13033
|
+
const pid = await this.paneHost.getPanePid(this.tmuxSessionName, windowId);
|
|
12495
13034
|
if (pid) {
|
|
12496
13035
|
this.noteStopRequest(signal, reason);
|
|
12497
13036
|
process.kill(-pid, signal);
|
|
@@ -12705,7 +13244,7 @@ export class Daemon extends EventEmitter {
|
|
|
12705
13244
|
whatsNewRoles() {
|
|
12706
13245
|
if (this.runtimeIdentity?.kind === "classic")
|
|
12707
13246
|
return ["classic"];
|
|
12708
|
-
if (this.config.general_topic === true || this.name
|
|
13247
|
+
if (this.config.general_topic === true || generalByName(this.name, this.config))
|
|
12709
13248
|
return ["general"];
|
|
12710
13249
|
const set = resolveToolSet(this.config, this.name);
|
|
12711
13250
|
return set === "coordinator" || set === "full" ? ["worker", "coordinator"] : ["worker"];
|
|
@@ -12732,6 +13271,8 @@ export class Daemon extends EventEmitter {
|
|
|
12732
13271
|
}
|
|
12733
13272
|
}
|
|
12734
13273
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
13274
|
+
if (this.nativePaneLifecycle)
|
|
13275
|
+
throw Error("Native relaunch requires its owned exit and a new lifecycle reservation");
|
|
12735
13276
|
this.assertSessionOomAdmission();
|
|
12736
13277
|
this.startupAdmission?.();
|
|
12737
13278
|
const launchFence = this.launchFenceEpoch;
|
|
@@ -12836,8 +13377,8 @@ export class Daemon extends EventEmitter {
|
|
|
12836
13377
|
this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
|
|
12837
13378
|
}
|
|
12838
13379
|
// Ensure tmux session exists (may have been destroyed if all windows died)
|
|
12839
|
-
await
|
|
12840
|
-
if (this.stormWindow?.observeServerAlive(await
|
|
13380
|
+
await this.paneHost.ensureSession(this.tmuxSessionName);
|
|
13381
|
+
if (this.stormWindow?.observeServerAlive(await this.paneHost.getServerPid(this.tmuxSessionName))) {
|
|
12841
13382
|
this.emit("tmux_server_crash", this.name);
|
|
12842
13383
|
}
|
|
12843
13384
|
this.startupAdmission?.();
|
|
@@ -13111,12 +13652,15 @@ export class Daemon extends EventEmitter {
|
|
|
13111
13652
|
// each have seen a respawn, pause or stop.
|
|
13112
13653
|
if (!owned())
|
|
13113
13654
|
return false;
|
|
13655
|
+
const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : answeredPane;
|
|
13656
|
+
if (!owned() || !Daemon.dialogMatches(dialog, keyScreen))
|
|
13657
|
+
return false;
|
|
13114
13658
|
if (key === "Up" || key === "Down" || key === "Enter" || key === "Escape") {
|
|
13115
|
-
if (!await this.tmux.sendSpecialKey(key))
|
|
13659
|
+
if (!await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key)))
|
|
13116
13660
|
return false;
|
|
13117
13661
|
}
|
|
13118
13662
|
else {
|
|
13119
|
-
if (!await this.tmux.sendKeys(key))
|
|
13663
|
+
if (!await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key)))
|
|
13120
13664
|
return false;
|
|
13121
13665
|
}
|
|
13122
13666
|
await new Promise(r => setTimeout(r, 200));
|
|
@@ -13130,7 +13674,8 @@ export class Daemon extends EventEmitter {
|
|
|
13130
13674
|
// Wait for next screen to render — bounded by what is left of the budget.
|
|
13131
13675
|
const renderWait = Math.max(0, Math.min(10_000, remaining()));
|
|
13132
13676
|
if (this.controlClient) {
|
|
13133
|
-
const wid =
|
|
13677
|
+
const wid = this.nativePaneLifecycle ? ownerTmux.getWindowId()
|
|
13678
|
+
: readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
|
|
13134
13679
|
await this.controlClient.waitForIdle(wid, renderWait);
|
|
13135
13680
|
}
|
|
13136
13681
|
else {
|
|
@@ -13166,7 +13711,9 @@ export class Daemon extends EventEmitter {
|
|
|
13166
13711
|
// scan parked on the old limit text.
|
|
13167
13712
|
const codexReady = this.backend.binaryName !== "codex"
|
|
13168
13713
|
|| this.isCodexLivePaneSnapshot(pane);
|
|
13169
|
-
|
|
13714
|
+
const nativeComposerReady = !this.nativePaneLifecycle || (this.backend?.readInputRow
|
|
13715
|
+
? this.backend.readInputRow(pane) !== null : this.backend?.isDeliveryInputReadyPane?.(pane) === true);
|
|
13716
|
+
if (readyPatternMatches && codexReady && nativeComposerReady) {
|
|
13170
13717
|
cleanReadyPolls++;
|
|
13171
13718
|
if (cleanReadyPolls >= 2 || remaining() <= 0) {
|
|
13172
13719
|
// A real ready prompt (with no fatal dialog on screen — those are
|
|
@@ -13291,6 +13838,16 @@ export class Daemon extends EventEmitter {
|
|
|
13291
13838
|
void refresh.call(this.backend).then(() => this.saveSessionId(), () => { });
|
|
13292
13839
|
}
|
|
13293
13840
|
saveSessionId() {
|
|
13841
|
+
if (this.nativePaneLifecycle) {
|
|
13842
|
+
try {
|
|
13843
|
+
this.nativePaneLifecycle.checkpointSession();
|
|
13844
|
+
this.ownedWorkingSession = this.backend?.getSessionId() ?? null;
|
|
13845
|
+
}
|
|
13846
|
+
catch (err) {
|
|
13847
|
+
this.logger.warn({ err }, "Native conversation checkpoint is unconfirmed");
|
|
13848
|
+
}
|
|
13849
|
+
return;
|
|
13850
|
+
}
|
|
13294
13851
|
// When a resume failure has forced a fresh start, don't persist the stale id
|
|
13295
13852
|
// back from statusline.json — that would re-arm --continue and re-loop.
|
|
13296
13853
|
if (this.skipResume)
|