@songsid/agend 2.3.4-beta.5 → 2.3.5-alpha.2
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/backend/claude-code.d.ts +18 -12
- package/dist/backend/claude-code.js +225 -136
- 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 +1271 -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 +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +59 -0
- 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 +62 -5
- package/dist/daemon.js +760 -240
- 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 +125 -2
- package/dist/fleet-manager.js +1157 -27
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-windows-runtime.d.ts +92 -0
- package/dist/fleet-windows-runtime.js +373 -0
- package/dist/fleet-windows-runtime.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 +20 -3
- package/dist/instance-lifecycle.js +358 -156
- package/dist/instance-lifecycle.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 +34 -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 +227 -0
- package/dist/native-pane-lifecycle.js.map +1 -0
- package/dist/native-pane-manager.d.ts +58 -0
- package/dist/native-pane-manager.js +563 -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 +1 -0
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +12 -1
- package/dist/needs-you.js +8 -1
- 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 +107 -0
- package/dist/pane-port.js +2 -0
- package/dist/pane-port.js.map +1 -0
- 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 +147 -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-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 +43 -0
- package/dist/runtime-pane-host.js +403 -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 +43 -0
- package/dist/runtime-pane-mcp.js +137 -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 +134 -0
- package/dist/runtime-pane-port.js +537 -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 +169 -0
- package/dist/runtime-pane-protocol.js +189 -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.js +58 -0
- package/dist/settings-api.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 +15 -1
- package/dist/settings-confirmation.js +46 -9
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +23 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +11 -2
- package/dist/settings-http-confirmation.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/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 +21 -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/native-instance-settings.js +47 -0
- package/dist/ui/native-pane-recovery.js +45 -0
- package/dist/ui/panel-chat.js +61 -32
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +99 -18
- package/dist/ui/panel-needs.js +152 -3
- package/dist/ui/panel-runtimes.js +231 -0
- package/dist/ui/panel-settings.js +14 -8
- 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 +14 -5
- package/dist/ui/settings-registry.js +5 -0
- package/dist/ui/settings-strings.js +14 -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 +20 -4
- package/dist/ui/shared/instance-switch.js +4 -3
- package/dist/ui/shared/ui-menu.js +28 -8
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/ui/windows-agents-nav.js +2 -0
- package/dist/ui/windows-cli-catalog.js +79 -0
- package/dist/update-check.d.ts +8 -2
- package/dist/update-check.js +5 -2
- package/dist/update-check.js.map +1 -1
- package/dist/web-api.d.ts +25 -0
- package/dist/web-api.js +171 -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-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 +112 -0
- package/dist/windows-agents.js +463 -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,4 @@
|
|
|
1
|
+
import { nativePaneToolRefusal } from "./native-pane-tools.js";
|
|
1
2
|
import { dialogScreenText } from "./dialog-screen-text.js";
|
|
2
3
|
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
3
4
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
@@ -20,7 +21,7 @@ import { assertNoSessionOomStop, readSessionOomStop, writeSessionOomStop, Sessio
|
|
|
20
21
|
import { mcpServerState } from "./mcp-liveness.js";
|
|
21
22
|
import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
22
23
|
import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
23
|
-
import {
|
|
24
|
+
import { resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
24
25
|
import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
25
26
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
26
27
|
import { TranscriptTurnLedger } from "./transcript-turns.js";
|
|
@@ -48,6 +49,7 @@ import { HangDetector } from "./hang-detector.js";
|
|
|
48
49
|
import { writeSecretFile } from "./secret-file.js";
|
|
49
50
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
50
51
|
import { answerRemoteCard } from "./remote-card-answer.js";
|
|
52
|
+
import { tmuxPaneHost } from "./tmux-pane-host.js";
|
|
51
53
|
import { buildFleetInstructions } from "./instructions.js";
|
|
52
54
|
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
53
55
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
@@ -57,6 +59,7 @@ import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT
|
|
|
57
59
|
import { t } from "./locale.js";
|
|
58
60
|
import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
|
|
59
61
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
62
|
+
import { reconcileNativePaneTarget } from "./native-pane-reconciliation.js";
|
|
60
63
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
61
64
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
62
65
|
import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
|
|
@@ -1112,6 +1115,7 @@ export class Daemon extends EventEmitter {
|
|
|
1112
1115
|
spawnGate;
|
|
1113
1116
|
stormWindow;
|
|
1114
1117
|
backendOutage;
|
|
1118
|
+
paneTransport;
|
|
1115
1119
|
/** #1314: fleet-directory fallbacks already reported to the topic (once per daemon, per field and path). */
|
|
1116
1120
|
promptFileFallbackNoticed = new Set();
|
|
1117
1121
|
/** Identity of this live Daemon object; changes on object stop/restart. */
|
|
@@ -1943,9 +1947,79 @@ export class Daemon extends EventEmitter {
|
|
|
1943
1947
|
this.errorRecoveryDeadlineAt = 0;
|
|
1944
1948
|
this.activeErrorPatternKey = null;
|
|
1945
1949
|
}
|
|
1950
|
+
/** Lazy default also preserves bare-prototype diagnostic/test construction. */
|
|
1951
|
+
get paneHost() { return this.paneTransport ?? tmuxPaneHost; }
|
|
1952
|
+
nativePaneLifecycle;
|
|
1953
|
+
nativeFleetBridge;
|
|
1954
|
+
nativeTranscriptSource;
|
|
1955
|
+
usesNativePane() { return !!this.nativePaneLifecycle; }
|
|
1956
|
+
nativePaneCurrent() { return this.nativePaneLifecycle?.current() ?? false; }
|
|
1957
|
+
nativeEnterHold = null;
|
|
1958
|
+
/** Per-launch attention for a native delivery whose bounded input attempts were refused or uncertain, including non-outbox input. */
|
|
1959
|
+
nativeDeliveryHold() {
|
|
1960
|
+
const hold = this.nativeEnterHold;
|
|
1961
|
+
return hold && this.nativePaneLifecycle && !this.deliveryWritesStopping && hold.spawn === this.spawnGeneration && hold.fence === this.launchFenceEpoch
|
|
1962
|
+
? { key: hold.key, since: hold.since } : null;
|
|
1963
|
+
}
|
|
1964
|
+
nativePaneHasExited() {
|
|
1965
|
+
const snapshot = this.nativePaneLifecycle?.snapshot?.();
|
|
1966
|
+
return snapshot?.state === "exited" && snapshot.instance === this.name && this.tmux?.getWindowId() === snapshot.paneId;
|
|
1967
|
+
}
|
|
1968
|
+
/** Positive native exit evidence only, fenced to this exact launch and Daemon owner. */
|
|
1969
|
+
nativePaneExitCurrent(snapshot, owner) {
|
|
1970
|
+
const actual = this.nativePaneLifecycle?.snapshot?.();
|
|
1971
|
+
return !!actual && actual.state === "exited" && snapshot.state === "exited"
|
|
1972
|
+
&& sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.startupAborted
|
|
1973
|
+
&& ["hostBootId", "paneId", "launchId", "agentId", "instance", "sessionId", "commandSequence", "exitCode"].every(key => actual[key] === snapshot[key])
|
|
1974
|
+
&& this.tmux?.getWindowId() === snapshot.paneId;
|
|
1975
|
+
}
|
|
1976
|
+
async reconcileNativeDeliveries(outbox, current) {
|
|
1977
|
+
if (!this.nativePaneLifecycle)
|
|
1978
|
+
return false;
|
|
1979
|
+
return reconcileNativePaneTarget(outbox, this.name, this.nativePaneLifecycle, current);
|
|
1980
|
+
}
|
|
1981
|
+
/** Same-process transport for the owned native daemon; never exposed as a runner RPC. */
|
|
1982
|
+
setNativeFleetBridge(bridge) {
|
|
1983
|
+
if (!this.nativePaneLifecycle || this.nativeFleetBridge?.current())
|
|
1984
|
+
throw Error("Native Fleet bridge requires one bound pane owner");
|
|
1985
|
+
this.nativeFleetBridge = bridge;
|
|
1986
|
+
}
|
|
1987
|
+
/** Fleet admission and generation checks happen before this synchronous handoff. */
|
|
1988
|
+
acceptNativeFleetMessage(message) {
|
|
1989
|
+
if (!this.nativePaneLifecycle || !this.nativeFleetBridge?.current() || this.deliveryWritesStopping || this.runtimeMonitorsFrozen)
|
|
1990
|
+
return false;
|
|
1991
|
+
if (typeof message.type === "string" && FLEET_RESPONSE_TYPES.has(message.type)) {
|
|
1992
|
+
this.routeFleetResponse(message);
|
|
1993
|
+
return true;
|
|
1994
|
+
}
|
|
1995
|
+
const bridge = this.nativeFleetBridge;
|
|
1996
|
+
const current = () => this.nativeFleetBridge === bridge && bridge.current() && !this.deliveryWritesStopping && !this.runtimeMonitorsFrozen;
|
|
1997
|
+
return this.handleFleetMessage(message, reply => { if (current())
|
|
1998
|
+
this.broadcastFleetMessage(reply); }, current);
|
|
1999
|
+
}
|
|
2000
|
+
broadcastFleetMessage(message) {
|
|
2001
|
+
if (this.nativePaneLifecycle) {
|
|
2002
|
+
if (this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen && !this.deliveryWritesStopping)
|
|
2003
|
+
this.nativeFleetBridge.send(message);
|
|
2004
|
+
}
|
|
2005
|
+
else
|
|
2006
|
+
this.ipcServer?.broadcast(message);
|
|
2007
|
+
}
|
|
2008
|
+
fleetTransportAvailable() {
|
|
2009
|
+
return this.nativePaneLifecycle ? !!this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen : !!this.ipcServer;
|
|
2010
|
+
}
|
|
2011
|
+
/** Internal lifecycle injection, before start. Native paths and provider files stay on their runner. */
|
|
2012
|
+
setNativePaneLifecycle(native) {
|
|
2013
|
+
if (this.tmux || this.ipcServer || this.nativePaneLifecycle)
|
|
2014
|
+
throw Error("Native pane adapter must be bound before daemon startup");
|
|
2015
|
+
this.nativePaneLifecycle = native;
|
|
2016
|
+
this.backend = native.backend;
|
|
2017
|
+
this.paneTransport = native.host;
|
|
2018
|
+
this.controlClient = native.observation;
|
|
2019
|
+
}
|
|
1946
2020
|
constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger, runtimeIdentity, spawnGate, stormWindow,
|
|
1947
2021
|
/** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
|
|
1948
|
-
backendOutage) {
|
|
2022
|
+
backendOutage, paneTransport) {
|
|
1949
2023
|
super();
|
|
1950
2024
|
this.name = name;
|
|
1951
2025
|
this.config = config;
|
|
@@ -1957,6 +2031,7 @@ export class Daemon extends EventEmitter {
|
|
|
1957
2031
|
this.spawnGate = spawnGate;
|
|
1958
2032
|
this.stormWindow = stormWindow;
|
|
1959
2033
|
this.backendOutage = backendOutage;
|
|
2034
|
+
this.paneTransport = paneTransport;
|
|
1960
2035
|
// The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
|
|
1961
2036
|
// sharing that object would activate cold changes before the normal restart/apply boundary.
|
|
1962
2037
|
this.config = structuredClone(config);
|
|
@@ -2038,12 +2113,19 @@ export class Daemon extends EventEmitter {
|
|
|
2038
2113
|
return outcome === "begun";
|
|
2039
2114
|
}
|
|
2040
2115
|
async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
|
|
2041
|
-
let checkpoint = null;
|
|
2042
|
-
|
|
2043
|
-
|
|
2116
|
+
let checkpoint = null, runtimeTranscript = null;
|
|
2117
|
+
if (this.nativePaneLifecycle) {
|
|
2118
|
+
runtimeTranscript = await this.transcriptMonitor?.reconciliationRuntimeCheckpoint() ?? null;
|
|
2119
|
+
if (!runtimeTranscript || !this.nativePaneLifecycle.current(runtimeTranscript))
|
|
2120
|
+
throw Error("Native transcript checkpoint is not current; no delivery was written");
|
|
2044
2121
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2122
|
+
else {
|
|
2123
|
+
try {
|
|
2124
|
+
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2125
|
+
}
|
|
2126
|
+
catch (err) {
|
|
2127
|
+
this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
|
|
2128
|
+
}
|
|
2047
2129
|
}
|
|
2048
2130
|
const submissionMode = rawPaste ? "raw_paste" : steer
|
|
2049
2131
|
? "steer"
|
|
@@ -2059,8 +2141,9 @@ export class Daemon extends EventEmitter {
|
|
|
2059
2141
|
backendVersion,
|
|
2060
2142
|
windowId: windowId ?? this.tmux?.getWindowId() ?? null,
|
|
2061
2143
|
transcriptPath: checkpoint?.path ?? null,
|
|
2062
|
-
transcriptOffset: checkpoint?.offset ?? null,
|
|
2063
|
-
transcriptSessionId: checkpoint?.sessionId ?? null,
|
|
2144
|
+
transcriptOffset: runtimeTranscript?.cursor.offset ?? checkpoint?.offset ?? null,
|
|
2145
|
+
transcriptSessionId: runtimeTranscript?.cursor.sessionId ?? checkpoint?.sessionId ?? null,
|
|
2146
|
+
...(runtimeTranscript ? { runtimeTranscript } : {}),
|
|
2064
2147
|
submissionMode,
|
|
2065
2148
|
queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
|
|
2066
2149
|
};
|
|
@@ -2103,17 +2186,25 @@ export class Daemon extends EventEmitter {
|
|
|
2103
2186
|
const outbox = this.deliveryOutbox;
|
|
2104
2187
|
if (!checkpoint || !outbox?.markConsumed)
|
|
2105
2188
|
return;
|
|
2189
|
+
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
2190
|
+
return;
|
|
2106
2191
|
if (verdict.submissionMode !== "steer" && verdict.submissionMode !== "native_queue_handoff")
|
|
2107
2192
|
return;
|
|
2108
2193
|
if (!["claude-code", "codex"].includes(checkpoint.backend))
|
|
2109
2194
|
return;
|
|
2110
2195
|
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, boot = this.bootId;
|
|
2196
|
+
let location;
|
|
2197
|
+
try {
|
|
2198
|
+
location = checkpoint.runtime ? { reader: this.nativeTranscriptReader(checkpoint.runtime, delivery.deliveryId) } : { path: checkpoint.path, offset: checkpoint.offset };
|
|
2199
|
+
}
|
|
2200
|
+
catch {
|
|
2201
|
+
return;
|
|
2202
|
+
} // The owner changed; an unavailable consumed watch proves nothing.
|
|
2111
2203
|
consumedWatches.start({
|
|
2112
2204
|
deliveryId: delivery.deliveryId,
|
|
2113
2205
|
attemptNo: delivery.attemptNo,
|
|
2114
2206
|
backend: checkpoint.backend,
|
|
2115
|
-
|
|
2116
|
-
offset: checkpoint.offset,
|
|
2207
|
+
...location,
|
|
2117
2208
|
current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch,
|
|
2118
2209
|
consumed: (via, evidence) => {
|
|
2119
2210
|
try {
|
|
@@ -2194,24 +2285,34 @@ export class Daemon extends EventEmitter {
|
|
|
2194
2285
|
const spawn = this.spawnGeneration;
|
|
2195
2286
|
const fence = this.launchFenceEpoch;
|
|
2196
2287
|
let last = "unavailable";
|
|
2197
|
-
|
|
2198
|
-
let
|
|
2288
|
+
const native = !!checkpoint.runtime || !!this.nativePaneLifecycle;
|
|
2289
|
+
let outcome = native ? "uncertain" : "delivered";
|
|
2290
|
+
let evidence = native ? "runtime-transcript-unconfirmed" : OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
2199
2291
|
// #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
|
|
2200
2292
|
// in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
|
|
2201
2293
|
const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
|
|
2202
2294
|
let found = false;
|
|
2203
2295
|
try {
|
|
2296
|
+
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
2297
|
+
throw Error("Native proof cannot read a coordinator-local transcript path");
|
|
2298
|
+
const nativeReader = checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : null;
|
|
2204
2299
|
for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
|
|
2205
2300
|
if (this.deliveryWritesStopping)
|
|
2206
2301
|
return; // reconciliation owns the row now
|
|
2207
|
-
const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
2302
|
+
const look = nativeReader ? await nativeReader.look() : await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
2303
|
+
if (this.deliveryWritesStopping)
|
|
2304
|
+
return;
|
|
2305
|
+
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
2306
|
+
last = "unavailable";
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2208
2309
|
if (look === "user" || look === "absorbed" || look === "queued") {
|
|
2209
2310
|
found = true;
|
|
2210
2311
|
outcome = "delivered";
|
|
2211
2312
|
evidence = transcriptMarkerEvidence(look, !!rawPaste);
|
|
2212
2313
|
break;
|
|
2213
2314
|
}
|
|
2214
|
-
last = look;
|
|
2315
|
+
last = look === "pending" ? "unavailable" : look;
|
|
2215
2316
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
2216
2317
|
last = "unavailable";
|
|
2217
2318
|
break;
|
|
@@ -2223,13 +2324,13 @@ export class Daemon extends EventEmitter {
|
|
|
2223
2324
|
// `uncertain`: re-check the same fixed epochs after the last await, before settling (#1207 P2).
|
|
2224
2325
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch)
|
|
2225
2326
|
last = "unavailable";
|
|
2226
|
-
if (last === "no-match" &&
|
|
2327
|
+
if (last === "no-match" && !found) {
|
|
2227
2328
|
outcome = "uncertain";
|
|
2228
2329
|
evidence = "unverifiable-no-transcript-marker";
|
|
2229
2330
|
}
|
|
2230
2331
|
}
|
|
2231
2332
|
catch (err) {
|
|
2232
|
-
this.logger.debug({ err }, "Transcript proof
|
|
2333
|
+
this.logger.debug({ err }, "Transcript proof unavailable — preserving the conservative outcome");
|
|
2233
2334
|
}
|
|
2234
2335
|
if (rawPaste && !found) {
|
|
2235
2336
|
outcome = "uncertain";
|
|
@@ -2416,128 +2517,130 @@ export class Daemon extends EventEmitter {
|
|
|
2416
2517
|
}
|
|
2417
2518
|
}
|
|
2418
2519
|
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
|
-
|
|
2520
|
+
if (!this.nativePaneLifecycle) {
|
|
2521
|
+
// 1. IPC server — bridge between MCP server (Claude's child) and daemon
|
|
2522
|
+
const sockPath = join(this.instanceDir, "channel.sock");
|
|
2523
|
+
this.ipcServer = new IpcServer(sockPath, this.logger);
|
|
2524
|
+
// Forward IPC server errors as daemon events (prevents unhandled 'error' crash).
|
|
2525
|
+
// Guard: only forward post-listen errors — startup errors are handled by listen() rejection.
|
|
2526
|
+
let ipcListening = false;
|
|
2527
|
+
this.ipcServer.on("error", (err) => {
|
|
2528
|
+
if (!ipcListening)
|
|
2529
|
+
return; // startup errors handled by listen() rejection
|
|
2530
|
+
this.logger.error({ err, name: this.name }, "IPC server error");
|
|
2531
|
+
this.emit("error", err);
|
|
2532
|
+
});
|
|
2533
|
+
await this.ipcServer.listen();
|
|
2534
|
+
this.startupAdmission?.();
|
|
2535
|
+
ipcListening = true;
|
|
2536
|
+
// Permanent IPC dispatcher: routes responses to pending requests by type+id key
|
|
2537
|
+
this.ipcServer.on("message", (msg) => this.routeFleetResponse(msg));
|
|
2538
|
+
// IPC message relay: when daemon wants to push a channel message to Claude,
|
|
2539
|
+
// it broadcasts to all IPC clients (the MCP server is one of them).
|
|
2540
|
+
// When MCP server sends a tool_call, daemon handles it via the messageBus.
|
|
2541
|
+
this.ipcServer.on("message", (msg, socket) => {
|
|
2542
|
+
if (msg.type === "tool_call") {
|
|
2543
|
+
// MCP server forwarding a Claude tool call (reply, react, edit, download)
|
|
2544
|
+
this.handleToolCall(msg, socket);
|
|
2545
|
+
}
|
|
2546
|
+
else if (msg.type === "mcp_ready") {
|
|
2547
|
+
this.handleMcpReady(msg, socket);
|
|
2548
|
+
}
|
|
2549
|
+
else if (msg.type === "pong") {
|
|
2550
|
+
this.handleMcpPong(msg, socket);
|
|
2551
|
+
}
|
|
2552
|
+
else if (msg.type === "query_sessions") {
|
|
2553
|
+
// Fleet manager asks for all registered session names (catches sessions
|
|
2554
|
+
// that sent mcp_ready before fleet manager connected).
|
|
2555
|
+
const sessions = [];
|
|
2556
|
+
for (const [s, sessionName] of this.socketSessionNames) {
|
|
2557
|
+
if (!s.destroyed && sessionName !== this.name) {
|
|
2558
|
+
// Individual mcp_ready for initial registration path
|
|
2559
|
+
this.ipcServer?.send(socket, { type: "mcp_ready", sessionName });
|
|
2560
|
+
sessions.push(sessionName);
|
|
2561
|
+
}
|
|
2459
2562
|
}
|
|
2563
|
+
// Batch response for prune path
|
|
2564
|
+
this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
|
|
2460
2565
|
}
|
|
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();
|
|
2566
|
+
else
|
|
2567
|
+
this.handleFleetMessage(msg, reply => { this.ipcServer?.send(socket, reply); });
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
let nativeDisposition;
|
|
2571
|
+
if (this.nativePaneLifecycle) {
|
|
2572
|
+
this.beginSpawn();
|
|
2573
|
+
try {
|
|
2574
|
+
const acquired = await this.nativePaneLifecycle.acquire(() => {
|
|
2516
2575
|
this.startupAdmission?.();
|
|
2517
|
-
|
|
2576
|
+
if (this.startupAborted)
|
|
2577
|
+
throw Error("Native launch was superseded");
|
|
2578
|
+
});
|
|
2579
|
+
this.tmux = acquired.pane;
|
|
2580
|
+
this.nativeTranscriptSource = acquired.source;
|
|
2581
|
+
nativeDisposition = acquired.disposition;
|
|
2582
|
+
this.lastSpawnAt = Date.now();
|
|
2583
|
+
this.setProcessStatus("running");
|
|
2584
|
+
this.bindInstanceStateOutputListener(this.tmux.getWindowId());
|
|
2585
|
+
this.saveSessionId();
|
|
2586
|
+
// New/resumed native processes can still be painting their first frame. Keep deliveries behind the same
|
|
2587
|
+
// bounded dialog/readiness scan as local startup. An already-running reattachment may be busy: do not
|
|
2588
|
+
// wait for that work to finish or inject a startup key into it.
|
|
2589
|
+
if (acquired.disposition !== "reattached") {
|
|
2590
|
+
if (!await this.finishStartupScan(120_000))
|
|
2591
|
+
throw Error("Native CLI exited during startup; retained session was not replaced");
|
|
2518
2592
|
this.startupAdmission?.();
|
|
2519
|
-
this.
|
|
2593
|
+
if (this.startupAborted || this.deliveryWritesStopping || !this.nativePaneLifecycle.current())
|
|
2594
|
+
throw Error("Native launch was superseded");
|
|
2520
2595
|
}
|
|
2521
2596
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
this.isNewSession = !resumed;
|
|
2526
|
-
if (!resumed) {
|
|
2527
|
-
await this.injectSnapshotMessage(this.startupAdmission);
|
|
2528
|
-
this.startupAdmission?.();
|
|
2597
|
+
finally {
|
|
2598
|
+
this.endSpawn();
|
|
2599
|
+
}
|
|
2529
2600
|
}
|
|
2530
2601
|
else {
|
|
2531
|
-
//
|
|
2532
|
-
|
|
2533
|
-
|
|
2602
|
+
// 2. Tmux — ensure session, create window if not alive
|
|
2603
|
+
await this.paneHost.ensureSession(this.tmuxSessionName);
|
|
2604
|
+
this.startupAdmission?.();
|
|
2605
|
+
this.tmux = this.paneHost.bind(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
2606
|
+
// Strategy A: always start fresh Claude window (MCP server has no reconnection)
|
|
2607
|
+
// Kill any existing window from previous run
|
|
2608
|
+
const windowIdFile = join(this.instanceDir, "window-id");
|
|
2609
|
+
if (existsSync(windowIdFile)) {
|
|
2610
|
+
const savedId = readFileSync(windowIdFile, "utf-8").trim();
|
|
2611
|
+
if (savedId) {
|
|
2612
|
+
const oldTmux = this.paneHost.bind(this.tmuxSessionName, savedId);
|
|
2613
|
+
if (await oldTmux.isWindowAlive()) {
|
|
2614
|
+
this.startupAdmission?.();
|
|
2615
|
+
await this.checkpointSessionId();
|
|
2616
|
+
this.startupAdmission?.();
|
|
2617
|
+
await oldTmux.killWindow();
|
|
2618
|
+
this.startupAdmission?.();
|
|
2619
|
+
this.logger.info({ savedId }, "Killed old tmux window for fresh start");
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
const resumed = await this.spawnClaudeWindow();
|
|
2624
|
+
this.startupAdmission?.();
|
|
2625
|
+
this.isNewSession = !resumed;
|
|
2626
|
+
if (!resumed) {
|
|
2627
|
+
await this.injectSnapshotMessage(this.startupAdmission);
|
|
2628
|
+
this.startupAdmission?.();
|
|
2629
|
+
}
|
|
2630
|
+
else {
|
|
2631
|
+
// Clean up stale snapshot file — resume restored full context, snapshot not needed
|
|
2632
|
+
try {
|
|
2633
|
+
unlinkSync(join(this.instanceDir, "rotation-state.json"));
|
|
2634
|
+
}
|
|
2635
|
+
catch { /* may not exist */ }
|
|
2534
2636
|
}
|
|
2535
|
-
catch { /* may not exist */ }
|
|
2536
2637
|
}
|
|
2638
|
+
if (nativeDisposition)
|
|
2639
|
+
this.isNewSession = nativeDisposition === "fresh";
|
|
2537
2640
|
// #1209: conditional continuation for the turn interrupted by the last
|
|
2538
2641
|
// restart, if the gates allow. Detached; the gate
|
|
2539
2642
|
// consumes its one-shot marker first and never throws into boot.
|
|
2540
|
-
const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2643
|
+
const legacyResume = this.nativePaneLifecycle ? Promise.resolve("unknown") : this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2541
2644
|
const recoveryOwner = JSON.stringify(this.interactionOwner());
|
|
2542
2645
|
void Promise.allSettled([legacyResume]).then(() => {
|
|
2543
2646
|
if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
|
|
@@ -2556,7 +2659,7 @@ export class Daemon extends EventEmitter {
|
|
|
2556
2659
|
// (statusline transcript); codex/kiro/opencode read their CLI's own
|
|
2557
2660
|
// conversation store via a pluggable source. Backends with no known
|
|
2558
2661
|
// 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));
|
|
2662
|
+
this.transcriptMonitor = this.buildTranscriptMonitor(this.nativeTranscriptSource ?? createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
|
|
2560
2663
|
const transcriptMonitor = this.transcriptMonitor;
|
|
2561
2664
|
const transcriptFence = this.launchFenceEpoch;
|
|
2562
2665
|
// Baseline in the isolate before this daemon accepts new work. A
|
|
@@ -2612,13 +2715,15 @@ export class Daemon extends EventEmitter {
|
|
|
2612
2715
|
if (hangConfig?.enabled !== false) {
|
|
2613
2716
|
this.hangDetector = new HangDetector();
|
|
2614
2717
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
this.
|
|
2621
|
-
|
|
2718
|
+
if (!this.nativePaneLifecycle) {
|
|
2719
|
+
// 8. Context guardian
|
|
2720
|
+
const statusFile = join(this.instanceDir, "statusline.json");
|
|
2721
|
+
this.guardian = new ContextGuardian(this.config.context_guardian ?? {}, this.logger, statusFile);
|
|
2722
|
+
this.guardian.startWatching();
|
|
2723
|
+
this.guardian.on("status_update", () => {
|
|
2724
|
+
this.saveSessionId();
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2622
2727
|
// Context rotation removed: all CLI backends have built-in auto-compact.
|
|
2623
2728
|
// Crash recovery (health check + respawn with snapshot) is retained below.
|
|
2624
2729
|
}
|
|
@@ -2669,7 +2774,7 @@ export class Daemon extends EventEmitter {
|
|
|
2669
2774
|
this.socketPids.delete(socket);
|
|
2670
2775
|
// Notify fleet manager so it can clean up sessionRegistry
|
|
2671
2776
|
if (sessionName !== this.name) {
|
|
2672
|
-
this.
|
|
2777
|
+
this.broadcastFleetMessage({ type: "session_disconnected", sessionName });
|
|
2673
2778
|
}
|
|
2674
2779
|
});
|
|
2675
2780
|
}
|
|
@@ -2685,11 +2790,16 @@ export class Daemon extends EventEmitter {
|
|
|
2685
2790
|
// sub-agent's server may have left a dead pid in the slot).
|
|
2686
2791
|
this.noteMcpProofOfLife("mcp_ready", typeof msg.pid === "number" ? msg.pid : undefined);
|
|
2687
2792
|
}
|
|
2688
|
-
this.
|
|
2793
|
+
this.broadcastFleetMessage({ type: "mcp_ready", sessionName });
|
|
2689
2794
|
}
|
|
2690
2795
|
checkMcpServerAlive() {
|
|
2691
2796
|
if (this.isPaused)
|
|
2692
2797
|
return;
|
|
2798
|
+
if (this.nativePaneLifecycle) {
|
|
2799
|
+
if (this.nativePaneLifecycle.mcpProof() === "alive")
|
|
2800
|
+
this.noteMcpProofOfLife("connection");
|
|
2801
|
+
return; // No native PID/socket guessing or restart from an unknown helper observation.
|
|
2802
|
+
}
|
|
2693
2803
|
const status = mcpServerState(this.instanceDir);
|
|
2694
2804
|
if (status.state === "alive") {
|
|
2695
2805
|
this.mcpServedGeneration = this.spawnGeneration;
|
|
@@ -2773,6 +2883,8 @@ export class Daemon extends EventEmitter {
|
|
|
2773
2883
|
* server from a third-party one, and failed the whole launch for either.
|
|
2774
2884
|
*/
|
|
2775
2885
|
checkFleetMcpNeverConnected() {
|
|
2886
|
+
if (this.nativePaneLifecycle)
|
|
2887
|
+
return; // Native helper presence owns this observation.
|
|
2776
2888
|
if (this.config.agent_mode === "cli")
|
|
2777
2889
|
return; // no MCP server by design
|
|
2778
2890
|
if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
|
|
@@ -2983,6 +3095,8 @@ export class Daemon extends EventEmitter {
|
|
|
2983
3095
|
* proof that tools are being served, whatever channel.mcp.pid says.
|
|
2984
3096
|
*/
|
|
2985
3097
|
mcpServerAlive() {
|
|
3098
|
+
if (this.nativePaneLifecycle)
|
|
3099
|
+
return this.nativePaneLifecycle.mcpProof() === "alive" ? { alive: true, source: "connection" } : { alive: false, source: "none" };
|
|
2986
3100
|
const status = mcpServerState(this.instanceDir);
|
|
2987
3101
|
if (status.state === "alive")
|
|
2988
3102
|
return { alive: true, source: "pid", pid: status.pid };
|
|
@@ -3047,7 +3161,7 @@ export class Daemon extends EventEmitter {
|
|
|
3047
3161
|
this.mcpDeathNotifiedForPid = null;
|
|
3048
3162
|
this.mcpDeathDeferredForPid = null;
|
|
3049
3163
|
this.clearMcpRestartRequest();
|
|
3050
|
-
if (pid && mcpServerState(this.instanceDir).state !== "alive")
|
|
3164
|
+
if (!this.nativePaneLifecycle && pid && mcpServerState(this.instanceDir).state !== "alive")
|
|
3051
3165
|
this.repairMcpPidSlot(pid, source);
|
|
3052
3166
|
}
|
|
3053
3167
|
emitMcpRecovered(source, pid) {
|
|
@@ -3094,6 +3208,26 @@ export class Daemon extends EventEmitter {
|
|
|
3094
3208
|
// ended this instance's health loop because scheduleNext() sat after the
|
|
3095
3209
|
// throwing code. Catch, log, and keep checking.
|
|
3096
3210
|
try {
|
|
3211
|
+
if (this.nativePaneLifecycle) {
|
|
3212
|
+
if (this.healthCheckPaused) {
|
|
3213
|
+
scheduleNext();
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
const native = this.nativePaneLifecycle, pane = this.tmux;
|
|
3217
|
+
const status = await pane?.getPaneStatus();
|
|
3218
|
+
if (this.runtimeMonitorsFrozen || native !== this.nativePaneLifecycle || pane !== this.tmux)
|
|
3219
|
+
return;
|
|
3220
|
+
const snapshot = native.snapshot?.();
|
|
3221
|
+
if (status?.alive === false && snapshot?.state === "exited") {
|
|
3222
|
+
this.healthCheckPaused = true;
|
|
3223
|
+
this.setProcessStatus(snapshot.exitCode === 0 ? "stopped" : "crashed");
|
|
3224
|
+
this.emit("native_pane_exited", { name: this.name, snapshot, owner: this.interactionOwner() });
|
|
3225
|
+
}
|
|
3226
|
+
else if (status?.alive)
|
|
3227
|
+
this.checkMcpServerAlive();
|
|
3228
|
+
scheduleNext();
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3097
3231
|
// Instance directory removed externally (e.g. `rm -rf ~/.agend/instances/<name>`).
|
|
3098
3232
|
// Stop the loop permanently — otherwise every tick triggers a respawn, whose
|
|
3099
3233
|
// writeRotationSnapshot fails with ENOENT and gets caught as "Failed to respawn",
|
|
@@ -3205,13 +3339,13 @@ export class Daemon extends EventEmitter {
|
|
|
3205
3339
|
let inWindowLossBurst = false;
|
|
3206
3340
|
let nullReason;
|
|
3207
3341
|
if (!paneStatus) {
|
|
3208
|
-
const serverAlive = await
|
|
3342
|
+
const serverAlive = await this.paneHost.sessionExists(this.tmuxSessionName);
|
|
3209
3343
|
if (discardStaleHealth())
|
|
3210
3344
|
return;
|
|
3211
3345
|
// A server may have already restarted before this daemon's health
|
|
3212
3346
|
// tick. The new PID is the durable generation boundary: treat it as
|
|
3213
3347
|
// a fleet storm even though `has-session` is true again.
|
|
3214
|
-
const serverPid = serverAlive ? await
|
|
3348
|
+
const serverPid = serverAlive ? await this.paneHost.getServerPid(this.tmuxSessionName) : null;
|
|
3215
3349
|
if (discardStaleHealth())
|
|
3216
3350
|
return;
|
|
3217
3351
|
const generationChanged = serverAlive && this.stormWindow?.observeServerAlive(serverPid) === true;
|
|
@@ -3240,7 +3374,7 @@ export class Daemon extends EventEmitter {
|
|
|
3240
3374
|
// the window truly no longer exists vs a transient query glitch.
|
|
3241
3375
|
nullReason = "no_window";
|
|
3242
3376
|
try {
|
|
3243
|
-
const windows = await
|
|
3377
|
+
const windows = await this.paneHost.listWindowsStrict(this.tmuxSessionName, this.controlClient);
|
|
3244
3378
|
if (discardStaleHealth())
|
|
3245
3379
|
return;
|
|
3246
3380
|
this.windowQueryFailureTicks = 0;
|
|
@@ -3485,10 +3619,10 @@ export class Daemon extends EventEmitter {
|
|
|
3485
3619
|
// Wrapped in try-catch: if tmux server is dead, listWindows throws —
|
|
3486
3620
|
// must not block spawnClaudeWindow (which calls ensureSession).
|
|
3487
3621
|
try {
|
|
3488
|
-
const windows = await
|
|
3622
|
+
const windows = await this.paneHost.listWindows(this.tmuxSessionName);
|
|
3489
3623
|
for (const w of windows) {
|
|
3490
3624
|
if (w.name === this.name) {
|
|
3491
|
-
const tm =
|
|
3625
|
+
const tm = this.paneHost.bind(this.tmuxSessionName, w.id);
|
|
3492
3626
|
await tm.killWindow();
|
|
3493
3627
|
}
|
|
3494
3628
|
}
|
|
@@ -3631,7 +3765,7 @@ export class Daemon extends EventEmitter {
|
|
|
3631
3765
|
// Force the next ready capture to emit a fresh transition after respawn.
|
|
3632
3766
|
this.instanceState = "working";
|
|
3633
3767
|
}
|
|
3634
|
-
this.
|
|
3768
|
+
this.broadcastFleetMessage({
|
|
3635
3769
|
type: "instance_process_state",
|
|
3636
3770
|
instanceName: this.name,
|
|
3637
3771
|
status,
|
|
@@ -3827,12 +3961,15 @@ export class Daemon extends EventEmitter {
|
|
|
3827
3961
|
for (const key of dialog.keys) {
|
|
3828
3962
|
if (stale())
|
|
3829
3963
|
return;
|
|
3964
|
+
const keyScreen = this.nativePaneLifecycle ? await pollTmux.capturePane() : currentPane;
|
|
3965
|
+
if (stale() || !Daemon.dialogMatches(dialog, keyScreen))
|
|
3966
|
+
return;
|
|
3830
3967
|
let sent = false;
|
|
3831
3968
|
if (SPECIAL_KEYS.has(key)) {
|
|
3832
|
-
sent = await pollTmux.sendSpecialKey(key);
|
|
3969
|
+
sent = await (this.nativePaneLifecycle ? pollTmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : pollTmux.sendSpecialKey(key));
|
|
3833
3970
|
}
|
|
3834
3971
|
else if (dialog.literalKeys) {
|
|
3835
|
-
sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
|
|
3972
|
+
sent = await (this.nativePaneLifecycle ? pollTmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : pollTmux.sendKeys(key)); // typed, no Enter (#2872)
|
|
3836
3973
|
}
|
|
3837
3974
|
else {
|
|
3838
3975
|
sent = await pollTmux.pasteText(key, this.systemPasteOptions());
|
|
@@ -4195,7 +4332,20 @@ export class Daemon extends EventEmitter {
|
|
|
4195
4332
|
*/
|
|
4196
4333
|
async sendEscape() {
|
|
4197
4334
|
const cancelKey = this.backend?.getCancelKey() ?? "Escape";
|
|
4198
|
-
|
|
4335
|
+
const tmux = this.tmux;
|
|
4336
|
+
if (!tmux)
|
|
4337
|
+
return;
|
|
4338
|
+
if (this.nativePaneLifecycle) {
|
|
4339
|
+
// A native pane's composer proof cannot see a permission dialog, so an unproved key is withheld there. The cancel
|
|
4340
|
+
// is the person's own interrupt, keyed on the screen read just before (as the dialog answer path is): what Esc
|
|
4341
|
+
// does on Linux's tmux pane, on the same screen, with the same key.
|
|
4342
|
+
const screen = await tmux.capturePane().catch(() => null);
|
|
4343
|
+
if (screen === null)
|
|
4344
|
+
return;
|
|
4345
|
+
await tmux.sendSpecialKey(cancelKey, undefined, { kind: "dialog", screen });
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
await tmux.sendSpecialKey(cancelKey);
|
|
4199
4349
|
}
|
|
4200
4350
|
/**
|
|
4201
4351
|
* Drop every not-yet-started pane delivery which predates a user cancel.
|
|
@@ -4253,7 +4403,7 @@ export class Daemon extends EventEmitter {
|
|
|
4253
4403
|
return;
|
|
4254
4404
|
const outcome = follower.look(pane);
|
|
4255
4405
|
if (outcome) {
|
|
4256
|
-
this.
|
|
4406
|
+
this.broadcastFleetMessage({ type: "compact_pane_result", id, owner, outcome });
|
|
4257
4407
|
return;
|
|
4258
4408
|
}
|
|
4259
4409
|
}
|
|
@@ -4273,7 +4423,7 @@ export class Daemon extends EventEmitter {
|
|
|
4273
4423
|
const reportSwitch = (result) => {
|
|
4274
4424
|
if (!ownSwitch)
|
|
4275
4425
|
return;
|
|
4276
|
-
this.
|
|
4426
|
+
this.broadcastFleetMessage({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
|
|
4277
4427
|
};
|
|
4278
4428
|
// Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
|
|
4279
4429
|
// (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
|
|
@@ -4288,7 +4438,7 @@ export class Daemon extends EventEmitter {
|
|
|
4288
4438
|
if (!notifyWritten || told)
|
|
4289
4439
|
return;
|
|
4290
4440
|
told = true;
|
|
4291
|
-
this.
|
|
4441
|
+
this.broadcastFleetMessage({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
|
|
4292
4442
|
...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
|
|
4293
4443
|
if (written && paneFollower)
|
|
4294
4444
|
void this.followCompactPane(notifyWritten, owner, paneFollower);
|
|
@@ -4450,10 +4600,11 @@ export class Daemon extends EventEmitter {
|
|
|
4450
4600
|
const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
|
|
4451
4601
|
if (this.tmux !== tmux)
|
|
4452
4602
|
return false;
|
|
4453
|
-
const
|
|
4603
|
+
const screen = await read();
|
|
4604
|
+
const now = claudeSwitchDialog(screen);
|
|
4454
4605
|
if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
|
|
4455
4606
|
return false;
|
|
4456
|
-
return tmux.sendSpecialKey(key);
|
|
4607
|
+
return this.nativePaneLifecycle ? tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen }) : tmux.sendSpecialKey(key);
|
|
4457
4608
|
});
|
|
4458
4609
|
const gone = async () => {
|
|
4459
4610
|
const until = performance.now() + OWN_SWITCH_SETTLE_MS;
|
|
@@ -4542,9 +4693,12 @@ export class Daemon extends EventEmitter {
|
|
|
4542
4693
|
return false;
|
|
4543
4694
|
const specialKeys = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
|
|
4544
4695
|
for (const key of dialog.keys) {
|
|
4696
|
+
const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : currentPane;
|
|
4697
|
+
if (!matches(keyScreen))
|
|
4698
|
+
return false;
|
|
4545
4699
|
const ok = specialKeys.has(key)
|
|
4546
|
-
? await this.tmux.sendSpecialKey(key)
|
|
4547
|
-
: await this.tmux.sendKeys(key);
|
|
4700
|
+
? await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key))
|
|
4701
|
+
: await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key));
|
|
4548
4702
|
if (!ok)
|
|
4549
4703
|
return false;
|
|
4550
4704
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -4787,7 +4941,7 @@ export class Daemon extends EventEmitter {
|
|
|
4787
4941
|
await this.tmux.sendSpecialKey("Escape").catch(() => false);
|
|
4788
4942
|
return true;
|
|
4789
4943
|
}
|
|
4790
|
-
async stop() {
|
|
4944
|
+
async stop(options = {}) {
|
|
4791
4945
|
this.publishWorkingState("before_stop");
|
|
4792
4946
|
this.logger.info("Stopping daemon instance");
|
|
4793
4947
|
this.clearQuitRelaunchWatch();
|
|
@@ -4809,8 +4963,18 @@ export class Daemon extends EventEmitter {
|
|
|
4809
4963
|
}
|
|
4810
4964
|
if (this.adapter)
|
|
4811
4965
|
await this.adapter.stop();
|
|
4966
|
+
if (this.nativePaneLifecycle) {
|
|
4967
|
+
this.healthCheckPaused = true;
|
|
4968
|
+
this.saveSessionId();
|
|
4969
|
+
await this.nativePaneLifecycle.release(options.nativePane ?? "detach");
|
|
4970
|
+
try {
|
|
4971
|
+
unlinkSync(join(this.instanceDir, "daemon.pid"));
|
|
4972
|
+
}
|
|
4973
|
+
catch { /* metadata only */ }
|
|
4974
|
+
return;
|
|
4975
|
+
}
|
|
4812
4976
|
// Notify MCP servers of graceful shutdown (prevents reconnect attempts)
|
|
4813
|
-
this.
|
|
4977
|
+
this.broadcastFleetMessage({ type: "shutdown" });
|
|
4814
4978
|
// Quit CLI FIRST — this kills MCP server child processes cleanly.
|
|
4815
4979
|
// IPC must stay open during quit so MCP servers receive the shutdown message.
|
|
4816
4980
|
if (this.tmux) {
|
|
@@ -4976,7 +5140,11 @@ export class Daemon extends EventEmitter {
|
|
|
4976
5140
|
|| !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
|
|
4977
5141
|
|| this.backend?.backgroundWorkRunning?.(pane) === true)
|
|
4978
5142
|
return;
|
|
4979
|
-
if (
|
|
5143
|
+
if (this.nativePaneLifecycle ? !this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"
|
|
5144
|
+
|| !!this.backend?.isDeliveryInputReadyPane && !this.backend.isDeliveryInputReadyPane(pane)
|
|
5145
|
+
: await tmux.getPaneInputMode?.() !== "raw")
|
|
5146
|
+
return;
|
|
5147
|
+
if (!current())
|
|
4980
5148
|
return;
|
|
4981
5149
|
const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
|
|
4982
5150
|
if (!current())
|
|
@@ -5067,7 +5235,7 @@ export class Daemon extends EventEmitter {
|
|
|
5067
5235
|
this.logger.debug({ err }, "Interaction listener failed");
|
|
5068
5236
|
}
|
|
5069
5237
|
try {
|
|
5070
|
-
this.
|
|
5238
|
+
this.broadcastFleetMessage({ type: "instance_interaction", instanceName: this.name, interaction });
|
|
5071
5239
|
}
|
|
5072
5240
|
catch (err) {
|
|
5073
5241
|
this.logger.debug({ err }, "Interaction broadcast failed");
|
|
@@ -5155,6 +5323,56 @@ export class Daemon extends EventEmitter {
|
|
|
5155
5323
|
}
|
|
5156
5324
|
return true;
|
|
5157
5325
|
}
|
|
5326
|
+
/** Shared Fleet inputs: socket and native transports retain the same wake, epoch and raw-paste semantics. */
|
|
5327
|
+
handleFleetMessage(msg, send, current = () => true) {
|
|
5328
|
+
if (msg.type === "query_sessions") {
|
|
5329
|
+
send({ type: "query_sessions_response", sessions: [] });
|
|
5330
|
+
}
|
|
5331
|
+
else if (msg.type === "fleet_inbound") {
|
|
5332
|
+
const meta = msg.meta;
|
|
5333
|
+
const targetSession = msg.targetSession;
|
|
5334
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
5335
|
+
void this.wake().then(() => {
|
|
5336
|
+
if (!current()) {
|
|
5337
|
+
this.deferDurableDelivery(meta, "native channel owner changed before pane submission");
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
|
|
5341
|
+
}).catch(err => {
|
|
5342
|
+
this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
|
|
5343
|
+
this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
|
|
5344
|
+
});
|
|
5345
|
+
}
|
|
5346
|
+
else if (msg.type === "raw_paste") {
|
|
5347
|
+
this.handleRawPasteIpcMessage(msg);
|
|
5348
|
+
}
|
|
5349
|
+
else if (msg.type === "config_update") {
|
|
5350
|
+
this.applyConfigUpdate(msg.config);
|
|
5351
|
+
}
|
|
5352
|
+
else if (msg.type === "steer") {
|
|
5353
|
+
const meta = (msg.meta ?? {});
|
|
5354
|
+
this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
5355
|
+
}
|
|
5356
|
+
else if (msg.type === "btw") {
|
|
5357
|
+
const meta = (msg.meta ?? {});
|
|
5358
|
+
this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
5359
|
+
}
|
|
5360
|
+
else if (msg.type === "fleet_schedule_trigger") {
|
|
5361
|
+
const payload = msg.payload;
|
|
5362
|
+
const meta = msg.meta;
|
|
5363
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
5364
|
+
void this.wake().then(() => { if (current())
|
|
5365
|
+
this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch); }).catch(err => {
|
|
5366
|
+
this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
|
|
5367
|
+
});
|
|
5368
|
+
}
|
|
5369
|
+
else if (msg.type === "query_instance_state") {
|
|
5370
|
+
void this.respondToInstanceStateQuery(msg, undefined, send);
|
|
5371
|
+
}
|
|
5372
|
+
else
|
|
5373
|
+
return false;
|
|
5374
|
+
return true;
|
|
5375
|
+
}
|
|
5158
5376
|
/**
|
|
5159
5377
|
* Answer a FleetManager state query. Most callers only need the cached state,
|
|
5160
5378
|
* but lifecycle decisions such as post-reply Cancel retirement need an
|
|
@@ -5162,7 +5380,7 @@ export class Daemon extends EventEmitter {
|
|
|
5162
5380
|
* the last transition can stay "working" even though the CLI has since
|
|
5163
5381
|
* settled without emitting another control-mode output record.
|
|
5164
5382
|
*/
|
|
5165
|
-
async respondToInstanceStateQuery(msg, socket) {
|
|
5383
|
+
async respondToInstanceStateQuery(msg, socket, send) {
|
|
5166
5384
|
try {
|
|
5167
5385
|
if (msg.deliveryIdle === true) {
|
|
5168
5386
|
await this.captureAndEvaluateInstanceState("delivery_idle_gate");
|
|
@@ -5171,7 +5389,7 @@ export class Daemon extends EventEmitter {
|
|
|
5171
5389
|
await this.captureAndEvaluateInstanceState("state_query");
|
|
5172
5390
|
}
|
|
5173
5391
|
const snapshot = this.getInstanceStateSnapshot();
|
|
5174
|
-
|
|
5392
|
+
const reply = {
|
|
5175
5393
|
type: "instance_state_response",
|
|
5176
5394
|
requestId: msg.requestId,
|
|
5177
5395
|
instanceName: this.name,
|
|
@@ -5179,7 +5397,11 @@ export class Daemon extends EventEmitter {
|
|
|
5179
5397
|
state: this.isPaused ? "paused" : snapshot.state,
|
|
5180
5398
|
processStatus: this.processStatus,
|
|
5181
5399
|
pausedAt: this.lastPausedAt,
|
|
5182
|
-
}
|
|
5400
|
+
};
|
|
5401
|
+
if (send)
|
|
5402
|
+
send(reply);
|
|
5403
|
+
else if (socket)
|
|
5404
|
+
this.ipcServer?.send(socket, reply);
|
|
5183
5405
|
}
|
|
5184
5406
|
catch (err) {
|
|
5185
5407
|
this.logger.debug({ err: err.message }, "Instance state query failed");
|
|
@@ -5219,7 +5441,49 @@ export class Daemon extends EventEmitter {
|
|
|
5219
5441
|
static AUTH_RECONFIRM_ROWS = 15;
|
|
5220
5442
|
/** Why the deferred pause was requested; what its marker records (Phase 2a). */
|
|
5221
5443
|
pendingPauseReason = "auth";
|
|
5444
|
+
nativePauseProof = null;
|
|
5445
|
+
/** Consume the exact idle proof synchronously after the lifecycle's await, before fencing and closing. */
|
|
5446
|
+
nativePauseStillReady() {
|
|
5447
|
+
const proof = this.nativePauseProof;
|
|
5448
|
+
this.nativePauseProof = null;
|
|
5449
|
+
if (proof?.())
|
|
5450
|
+
return true;
|
|
5451
|
+
this.pauseRequested = false;
|
|
5452
|
+
return false;
|
|
5453
|
+
}
|
|
5454
|
+
/** Native lifecycle pause uses the ordinary idle/background/admission proof before closing its owned Job. */
|
|
5455
|
+
async nativePauseReady(admission) {
|
|
5456
|
+
this.nativePauseProof = null;
|
|
5457
|
+
const native = this.nativePaneLifecycle, pane = this.tmux, owner = this.interactionOwner();
|
|
5458
|
+
const allowStuck = this.pauseAllowStuck;
|
|
5459
|
+
this.pauseAllowStuck = false;
|
|
5460
|
+
const ready = () => !!native && native === this.nativePaneLifecycle && native.current() && this.tmux === pane
|
|
5461
|
+
&& sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.spawning
|
|
5462
|
+
&& this.processStatus === "running" && this.pasteQueueDepth === 0
|
|
5463
|
+
&& (this.instanceState === "idle" || allowStuck && this.instanceState === "stuck") && (!admission || admission.current());
|
|
5464
|
+
if (!pane || !ready()) {
|
|
5465
|
+
this.pauseRequested = false;
|
|
5466
|
+
return false;
|
|
5467
|
+
}
|
|
5468
|
+
let text;
|
|
5469
|
+
try {
|
|
5470
|
+
text = await pane.capturePane();
|
|
5471
|
+
}
|
|
5472
|
+
catch {
|
|
5473
|
+
this.pauseRequested = false;
|
|
5474
|
+
return false;
|
|
5475
|
+
}
|
|
5476
|
+
if (!ready() || admission && !admission.accepts(text) || this.backend?.backgroundWorkRunning?.(text)
|
|
5477
|
+
|| !allowStuck && !this.paneAuthoritativelyIdle(text)) {
|
|
5478
|
+
this.pauseRequested = false;
|
|
5479
|
+
return false;
|
|
5480
|
+
}
|
|
5481
|
+
this.nativePauseProof = ready;
|
|
5482
|
+
return true;
|
|
5483
|
+
}
|
|
5222
5484
|
async pause(reason = "operator", admission) {
|
|
5485
|
+
if (this.nativePaneLifecycle)
|
|
5486
|
+
throw Error("Native pause is owned by its Fleet lifecycle transition");
|
|
5223
5487
|
if (admission && !admission.current())
|
|
5224
5488
|
return;
|
|
5225
5489
|
if (this.pauseWakeState === "paused")
|
|
@@ -5315,7 +5579,7 @@ export class Daemon extends EventEmitter {
|
|
|
5315
5579
|
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
|
|
5316
5580
|
this.publishWorkingState("execution");
|
|
5317
5581
|
this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
|
|
5318
|
-
this.
|
|
5582
|
+
this.broadcastFleetMessage({
|
|
5319
5583
|
type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
|
|
5320
5584
|
});
|
|
5321
5585
|
this.emit("auto_paused", { name: this.name, pausedAt: this.lastPausedAt });
|
|
@@ -5410,7 +5674,7 @@ export class Daemon extends EventEmitter {
|
|
|
5410
5674
|
this.resumeRuntimeMonitors();
|
|
5411
5675
|
this.publishWorkingState("execution");
|
|
5412
5676
|
this.logger.info("Instance auto-woke");
|
|
5413
|
-
this.
|
|
5677
|
+
this.broadcastFleetMessage({
|
|
5414
5678
|
type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
|
|
5415
5679
|
});
|
|
5416
5680
|
this.emit("auto_woke", { name: this.name });
|
|
@@ -5570,7 +5834,7 @@ export class Daemon extends EventEmitter {
|
|
|
5570
5834
|
this.logStateEdge(previous, snapshot);
|
|
5571
5835
|
this.publishWorkingState("execution");
|
|
5572
5836
|
this.emit("instance_state", { name: this.name, ...snapshot });
|
|
5573
|
-
this.
|
|
5837
|
+
this.broadcastFleetMessage({ type: "instance_state", instanceName: this.name, ...snapshot });
|
|
5574
5838
|
if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
|
|
5575
5839
|
this.handleStuckTransition(pane, snapshot, this.instanceStateReadyPattern);
|
|
5576
5840
|
}
|
|
@@ -5670,7 +5934,7 @@ export class Daemon extends EventEmitter {
|
|
|
5670
5934
|
// during the continuation boots clean instead of chaining resumes.
|
|
5671
5935
|
// #2544: a reply that settled before this arm already answered the person —
|
|
5672
5936
|
// writing a marker would re-inject a continuation for a finished turn.
|
|
5673
|
-
if (!meta.resumedContinuationOf && !armed.completionDelivered) {
|
|
5937
|
+
if (!this.nativePaneLifecycle && !meta.resumedContinuationOf && !armed.completionDelivered) {
|
|
5674
5938
|
const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
|
|
5675
5939
|
const resumeCwd = this.config.working_directory ?? "";
|
|
5676
5940
|
writeInFlightTurnMarker(this.instanceDir, {
|
|
@@ -5902,7 +6166,7 @@ export class Daemon extends EventEmitter {
|
|
|
5902
6166
|
// outbound action suppresses a duplicate proxy; only a delivered
|
|
5903
6167
|
// human-facing channel action satisfies the completion guard below.
|
|
5904
6168
|
if (!turn.outboundDelivered && this.config.mcp_proxy_reply === true
|
|
5905
|
-
&& mcpServerState(this.instanceDir).state === "dead") {
|
|
6169
|
+
&& (this.nativePaneLifecycle ? this.nativePaneLifecycle.mcpProof() === "dead" : mcpServerState(this.instanceDir).state === "dead")) {
|
|
5906
6170
|
this.turnReplyGuard.complete(turn.generation);
|
|
5907
6171
|
void this.sendProxyReply(pane, turn.target);
|
|
5908
6172
|
return;
|
|
@@ -6279,7 +6543,7 @@ export class Daemon extends EventEmitter {
|
|
|
6279
6543
|
}
|
|
6280
6544
|
});
|
|
6281
6545
|
}
|
|
6282
|
-
if (!this.
|
|
6546
|
+
if (!this.fleetTransportAvailable()) {
|
|
6283
6547
|
this.logger.error(`${logLabel} failed — no adapter or fleet IPC route`);
|
|
6284
6548
|
return Promise.resolve(false);
|
|
6285
6549
|
}
|
|
@@ -6303,7 +6567,7 @@ export class Daemon extends EventEmitter {
|
|
|
6303
6567
|
resolve(delivered);
|
|
6304
6568
|
});
|
|
6305
6569
|
try {
|
|
6306
|
-
this.
|
|
6570
|
+
this.broadcastFleetMessage({
|
|
6307
6571
|
type: "fleet_outbound",
|
|
6308
6572
|
tool: "reply",
|
|
6309
6573
|
args,
|
|
@@ -6874,7 +7138,7 @@ export class Daemon extends EventEmitter {
|
|
|
6874
7138
|
if (next === this.currentActivity)
|
|
6875
7139
|
return;
|
|
6876
7140
|
this.currentActivity = next;
|
|
6877
|
-
this.
|
|
7141
|
+
this.broadcastFleetMessage({
|
|
6878
7142
|
type: "instance_activity",
|
|
6879
7143
|
instanceName: this.name,
|
|
6880
7144
|
activity: next,
|
|
@@ -7031,7 +7295,7 @@ export class Daemon extends EventEmitter {
|
|
|
7031
7295
|
return;
|
|
7032
7296
|
this.lastProgressBroadcast = rendered;
|
|
7033
7297
|
this.lastProgressBroadcastAt = Date.now();
|
|
7034
|
-
this.
|
|
7298
|
+
this.broadcastFleetMessage({
|
|
7035
7299
|
type: "instance_progress",
|
|
7036
7300
|
instanceName: this.name,
|
|
7037
7301
|
progress: rendered,
|
|
@@ -7047,7 +7311,7 @@ export class Daemon extends EventEmitter {
|
|
|
7047
7311
|
if (this.lastProgressBroadcast !== "") {
|
|
7048
7312
|
this.lastProgressBroadcast = "";
|
|
7049
7313
|
this.lastProgressBroadcastAt = Date.now();
|
|
7050
|
-
this.
|
|
7314
|
+
this.broadcastFleetMessage({ type: "instance_progress", instanceName: this.name, progress: "" });
|
|
7051
7315
|
}
|
|
7052
7316
|
}
|
|
7053
7317
|
/**
|
|
@@ -7212,7 +7476,7 @@ export class Daemon extends EventEmitter {
|
|
|
7212
7476
|
reportCrossInstanceDeliveryFailure(meta, verdict, error) {
|
|
7213
7477
|
if (!meta.from_instance)
|
|
7214
7478
|
return;
|
|
7215
|
-
this.
|
|
7479
|
+
this.broadcastFleetMessage({
|
|
7216
7480
|
type: "cross_instance_delivery_failed",
|
|
7217
7481
|
senderSession: meta.from_instance,
|
|
7218
7482
|
targetInstance: this.name,
|
|
@@ -8572,13 +8836,14 @@ export class Daemon extends EventEmitter {
|
|
|
8572
8836
|
if (this.remoteCardAnswering)
|
|
8573
8837
|
return { outcome: "refused", reason: "another answer to this card is being keyed in" };
|
|
8574
8838
|
this.remoteCardAnswering = true;
|
|
8839
|
+
let answerScreen = null;
|
|
8575
8840
|
try {
|
|
8576
8841
|
const outcome = await answerRemoteCard({
|
|
8577
8842
|
locked: fn => this.paneWriteLock.run(fn),
|
|
8578
8843
|
// 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),
|
|
8844
|
+
read: async () => answerScreen = this.tmux === tmux ? await tmux.capturePane().catch(() => null) : null,
|
|
8845
|
+
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)),
|
|
8846
|
+
paste: text => this.tmux !== tmux || answerScreen === null ? Promise.resolve(false) : this.nativePaneLifecycle ? tmux.pasteBuffer(text, { proof: { kind: "dialog", screen: answerScreen } }) : tmux.pasteBuffer(text),
|
|
8582
8847
|
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
8583
8848
|
now: () => performance.now(),
|
|
8584
8849
|
}, pane => {
|
|
@@ -8849,12 +9114,19 @@ export class Daemon extends EventEmitter {
|
|
|
8849
9114
|
if (!check || !this.tmux)
|
|
8850
9115
|
return !check;
|
|
8851
9116
|
try {
|
|
8852
|
-
const
|
|
8853
|
-
|
|
9117
|
+
const inputActive = this.nativePaneLifecycle
|
|
9118
|
+
? this.nativePaneLifecycle.current() && this.nativePaneLifecycle.mcpProof() === "alive"
|
|
9119
|
+
: await this.tmux.getPaneInputMode?.() === "raw";
|
|
9120
|
+
// ConPTY has no POSIX termios mode. Its closed launch plus current helper handshake is the input barrier;
|
|
9121
|
+
// the same strict composer detector below is still required. Never grant native unknown-layout fallback.
|
|
9122
|
+
if (!inputActive) {
|
|
8854
9123
|
this.resetFooterFallback();
|
|
8855
9124
|
return false;
|
|
8856
9125
|
}
|
|
8857
|
-
|
|
9126
|
+
const pane = await this.tmux.capturePane();
|
|
9127
|
+
if (this.nativePaneLifecycle && (!this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"))
|
|
9128
|
+
return false;
|
|
9129
|
+
return this.deliveryInputReadyPane(pane);
|
|
8858
9130
|
}
|
|
8859
9131
|
catch {
|
|
8860
9132
|
this.resetFooterFallback();
|
|
@@ -9625,31 +9897,83 @@ export class Daemon extends EventEmitter {
|
|
|
9625
9897
|
// A recovery Enter may have waited for a transient while cancel or spawn replaced the delivery: check here, and
|
|
9626
9898
|
// again after every wait below, at the last point before the tmux write.
|
|
9627
9899
|
const tmux = this.tmux;
|
|
9628
|
-
const
|
|
9900
|
+
const backend = this.backend;
|
|
9901
|
+
const native = !!this.nativePaneLifecycle;
|
|
9902
|
+
const generation = this.spawnGeneration;
|
|
9903
|
+
const fence = this.launchFenceEpoch;
|
|
9904
|
+
const windowId = native ? tmux?.getWindowId() : undefined;
|
|
9905
|
+
// A native Windows pane keeps #2958's full fence (its launch can be replaced under the same handle).
|
|
9906
|
+
const current = () => this.tmux === tmux && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent())
|
|
9907
|
+
&& (!native || (!!windowId && !!backend && this.backend === backend && this.spawnGeneration === generation
|
|
9908
|
+
&& this.launchFenceEpoch === fence && tmux.getWindowId() === windowId));
|
|
9629
9909
|
if (!current())
|
|
9630
9910
|
return false;
|
|
9631
9911
|
// The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9912
|
+
// S-35: say which check withheld Enter.
|
|
9913
|
+
const hold = (reason) => {
|
|
9914
|
+
onInputHeld?.(reason);
|
|
9915
|
+
if (reason === "output_changed" && current() && !this.nativeDeliveryHold())
|
|
9916
|
+
this.nativeEnterHold = {
|
|
9917
|
+
key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence,
|
|
9918
|
+
};
|
|
9919
|
+
if (reason === "pane_mode")
|
|
9920
|
+
this.logger.warn({ phase, reason }, "The pane stayed in a tmux mode; Enter withheld");
|
|
9921
|
+
else
|
|
9922
|
+
this.logger.warn({ phase, reason }, "Native delivery Enter withheld by its safety check");
|
|
9635
9923
|
return false;
|
|
9636
9924
|
};
|
|
9637
9925
|
// 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
|
|
9638
9926
|
// end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
|
|
9639
|
-
// past the bound.
|
|
9640
|
-
//
|
|
9927
|
+
// past the bound.
|
|
9928
|
+
// Native delivery follows main #2990: only this backend's recognised runtime dialog vetoes Enter.
|
|
9929
|
+
// The fresh capture still binds the native host output sequence; ownership and positive-ACK-only retries remain.
|
|
9641
9930
|
const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
|
|
9931
|
+
let pane;
|
|
9932
|
+
let positiveRefusals = 0, enterMarked = false, positiveRetryDeadline = Infinity;
|
|
9642
9933
|
for (;;) {
|
|
9643
|
-
const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
|
|
9934
|
+
const mode = await this.waitWhilePaneInMode(phase, current, Math.min(modeDeadline, positiveRetryDeadline));
|
|
9644
9935
|
if (mode === "stale")
|
|
9645
9936
|
return false;
|
|
9646
9937
|
if (mode === "held")
|
|
9647
|
-
return hold();
|
|
9938
|
+
return hold("pane_mode");
|
|
9939
|
+
if (native) {
|
|
9940
|
+
// S-35: a native pane's capture refuses while output is still arriving ("Pane changed during capture; observe it
|
|
9941
|
+
// again"). Right after a turn the CLI repaints for a moment; one refused read left the message unsent for ever.
|
|
9942
|
+
// Read again a few times — reading types nothing — and still hold if it never settles.
|
|
9943
|
+
let captured;
|
|
9944
|
+
for (let attempt = 0; captured === undefined; attempt++) {
|
|
9945
|
+
try {
|
|
9946
|
+
captured = await tmux.capturePane();
|
|
9947
|
+
}
|
|
9948
|
+
catch {
|
|
9949
|
+
if (!current())
|
|
9950
|
+
return false;
|
|
9951
|
+
if (attempt >= 4)
|
|
9952
|
+
return hold("capture_failed");
|
|
9953
|
+
await new Promise(done => setTimeout(done, 150 * (attempt + 1)));
|
|
9954
|
+
if (!current())
|
|
9955
|
+
return false;
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
const screen = captured;
|
|
9959
|
+
pane = screen;
|
|
9960
|
+
if (!current())
|
|
9961
|
+
return false;
|
|
9962
|
+
try {
|
|
9963
|
+
if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
|
|
9964
|
+
return hold("dialog");
|
|
9965
|
+
}
|
|
9966
|
+
catch {
|
|
9967
|
+
return hold("check_failed");
|
|
9968
|
+
}
|
|
9969
|
+
if (!current())
|
|
9970
|
+
return false;
|
|
9971
|
+
}
|
|
9648
9972
|
if (await this.paneInTmuxMode(phase)) {
|
|
9649
9973
|
if (!current())
|
|
9650
9974
|
return false;
|
|
9651
9975
|
if (performance.now() >= modeDeadline)
|
|
9652
|
-
return hold();
|
|
9976
|
+
return hold("pane_mode");
|
|
9653
9977
|
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9654
9978
|
if (!current())
|
|
9655
9979
|
return false;
|
|
@@ -9657,23 +9981,110 @@ export class Daemon extends EventEmitter {
|
|
|
9657
9981
|
}
|
|
9658
9982
|
if (!current())
|
|
9659
9983
|
return false;
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9984
|
+
if (positiveRefusals && performance.now() >= positiveRetryDeadline)
|
|
9985
|
+
return hold("output_changed");
|
|
9986
|
+
// W1: persist the "Enter may have happened" boundary before tmux receives
|
|
9987
|
+
// the key. A crash after this commit is never auto-replayed without a
|
|
9988
|
+
// positive transcript marker, even if the stale pane still shows composer.
|
|
9989
|
+
if (!enterMarked && durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
|
|
9990
|
+
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
9991
|
+
return false;
|
|
9992
|
+
}
|
|
9993
|
+
enterMarked = true;
|
|
9994
|
+
const sent = await (native && pane !== undefined ? tmux.sendSpecialKey("Enter", undefined, { kind: "delivery", screen: pane }) : tmux.sendSpecialKey("Enter"));
|
|
9995
|
+
// A busy renderer can change seq after our capture. Only a positive zero-input ACK admits a NEW proof/key;
|
|
9996
|
+
// An Enter retry never re-pastes, retries an uncertain write, reuses stale proof or re-commits the durable boundary.
|
|
9997
|
+
if (!sent && native && tmux.lastInputWasNotSent?.() === true
|
|
9998
|
+
&& tmux.getLastSendSpecialKeyError() === "Pane changed after observation; input was not sent"
|
|
9999
|
+
&& current()) {
|
|
10000
|
+
if (!positiveRefusals)
|
|
10001
|
+
positiveRetryDeadline = Math.min(modeDeadline, performance.now() + 2000);
|
|
10002
|
+
if (positiveRefusals >= 4 || performance.now() >= positiveRetryDeadline)
|
|
10003
|
+
return hold("output_changed");
|
|
10004
|
+
positiveRefusals++;
|
|
10005
|
+
await new Promise(done => setTimeout(done, 100));
|
|
10006
|
+
if (!current())
|
|
10007
|
+
return false;
|
|
10008
|
+
if (performance.now() >= positiveRetryDeadline)
|
|
10009
|
+
return hold("output_changed");
|
|
10010
|
+
continue;
|
|
10011
|
+
}
|
|
10012
|
+
if (!sent) {
|
|
10013
|
+
this.logger.error({
|
|
10014
|
+
phase,
|
|
10015
|
+
tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
|
|
10016
|
+
}, "tmux send-keys Enter failed during message delivery");
|
|
10017
|
+
}
|
|
10018
|
+
if (sent && native && current())
|
|
10019
|
+
this.nativeEnterHold = null;
|
|
10020
|
+
return sent;
|
|
10021
|
+
}
|
|
10022
|
+
}
|
|
10023
|
+
/** A pre-existing native busy-paste race: retry bytes only after an exact, owned zero-input acknowledgement. */
|
|
10024
|
+
async sendDeliveryPaste(text, stillCurrent) {
|
|
10025
|
+
const pane = this.tmux, native = this.nativePaneLifecycle;
|
|
10026
|
+
if (!native)
|
|
10027
|
+
return pane.pasteBuffer(text);
|
|
10028
|
+
const backend = this.backend, generation = this.spawnGeneration, fence = this.launchFenceEpoch, windowId = pane.getWindowId();
|
|
10029
|
+
const current = () => !!windowId && this.tmux === pane && this.nativePaneLifecycle === native && this.backend === backend
|
|
10030
|
+
&& this.spawnGeneration === generation && this.launchFenceEpoch === fence && pane.getWindowId() === windowId
|
|
10031
|
+
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
|
|
10032
|
+
const hold = (reason) => {
|
|
10033
|
+
if (current() && !this.nativeDeliveryHold())
|
|
10034
|
+
this.nativeEnterHold = { key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence };
|
|
10035
|
+
this.logger.warn({ reason }, "Native delivery paste held by its safety check");
|
|
9667
10036
|
return false;
|
|
10037
|
+
};
|
|
10038
|
+
const deadline = performance.now() + 2000;
|
|
10039
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
10040
|
+
if (!current())
|
|
10041
|
+
return false;
|
|
10042
|
+
if (performance.now() >= deadline)
|
|
10043
|
+
return hold("output_changed");
|
|
10044
|
+
let screen;
|
|
10045
|
+
for (let read = 0; read < 5 && screen === undefined; read++) {
|
|
10046
|
+
try {
|
|
10047
|
+
screen = await pane.capturePane();
|
|
10048
|
+
}
|
|
10049
|
+
catch { /* Reads do not construct an input operation. */ }
|
|
10050
|
+
if (!current())
|
|
10051
|
+
return false;
|
|
10052
|
+
if (performance.now() >= deadline)
|
|
10053
|
+
return hold("capture_failed");
|
|
10054
|
+
if (screen === undefined && read < 4)
|
|
10055
|
+
await new Promise(done => setTimeout(done, 100));
|
|
10056
|
+
}
|
|
10057
|
+
if (screen === undefined)
|
|
10058
|
+
return hold("capture_failed");
|
|
10059
|
+
try {
|
|
10060
|
+
if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
|
|
10061
|
+
return hold("dialog");
|
|
10062
|
+
}
|
|
10063
|
+
catch {
|
|
10064
|
+
return hold("check_failed");
|
|
10065
|
+
}
|
|
10066
|
+
if (!current())
|
|
10067
|
+
return false;
|
|
10068
|
+
if (performance.now() >= deadline)
|
|
10069
|
+
return hold("output_changed");
|
|
10070
|
+
let sent;
|
|
10071
|
+
try {
|
|
10072
|
+
sent = await pane.pasteBuffer(text, { guard: current, proof: { kind: "composer", screen } });
|
|
10073
|
+
}
|
|
10074
|
+
catch {
|
|
10075
|
+
return hold("unconfirmed");
|
|
10076
|
+
}
|
|
10077
|
+
if (sent)
|
|
10078
|
+
return true; // Exactly one successful paste ends the loop, before any Enter.
|
|
10079
|
+
if (!current())
|
|
10080
|
+
return false;
|
|
10081
|
+
if (pane.lastInputWasNotSent?.() !== true || pane.getLastPasteError?.() !== "Pane changed after observation; input was not sent")
|
|
10082
|
+
return hold("unconfirmed");
|
|
10083
|
+
if (attempt >= 4 || performance.now() >= deadline)
|
|
10084
|
+
return hold("output_changed");
|
|
10085
|
+
await new Promise(done => setTimeout(done, 100));
|
|
9668
10086
|
}
|
|
9669
|
-
|
|
9670
|
-
if (!sent) {
|
|
9671
|
-
this.logger.error({
|
|
9672
|
-
phase,
|
|
9673
|
-
tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
|
|
9674
|
-
}, "tmux send-keys Enter failed during message delivery");
|
|
9675
|
-
}
|
|
9676
|
-
return sent;
|
|
10087
|
+
return hold("output_changed");
|
|
9677
10088
|
}
|
|
9678
10089
|
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
|
|
9679
10090
|
// Last and defaulted so the positional callers that ignore the outcome stay
|
|
@@ -9711,6 +10122,9 @@ export class Daemon extends EventEmitter {
|
|
|
9711
10122
|
const tmuxAtWrite = this.tmux;
|
|
9712
10123
|
const steerCurrent = () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping
|
|
9713
10124
|
&& this.tmux === tmuxAtWrite && this.spawnGeneration === spawnAtWrite;
|
|
10125
|
+
// Alpha (S-36, Prism #2989 P2): a NATIVE pane keeps #2958's pre-paste owner fence through the initial Enter and every
|
|
10126
|
+
// retry Enter (a pane replaced while the paste awaits never takes this delivery's Enter); a tmux pane follows main.
|
|
10127
|
+
const nativeOwner = this.nativePaneLifecycle ? steerCurrent : undefined;
|
|
9714
10128
|
// Bug A: paste with backoff. Transient failures are usually a stale window id
|
|
9715
10129
|
// after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
|
|
9716
10130
|
const maxAttempts = 3;
|
|
@@ -9778,8 +10192,8 @@ export class Daemon extends EventEmitter {
|
|
|
9778
10192
|
}
|
|
9779
10193
|
verdict.durableBeginCommitted = true;
|
|
9780
10194
|
verdict.submissionMode = attemptEvidence.submissionMode;
|
|
9781
|
-
if (attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
|
|
9782
|
-
verdict.transcriptCheckpoint = {
|
|
10195
|
+
if (attemptEvidence.runtimeTranscript || attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
|
|
10196
|
+
verdict.transcriptCheckpoint = attemptEvidence.runtimeTranscript ? { backend: attemptEvidence.backend, runtime: attemptEvidence.runtimeTranscript } : {
|
|
9783
10197
|
backend: attemptEvidence.backend,
|
|
9784
10198
|
path: attemptEvidence.transcriptPath,
|
|
9785
10199
|
offset: attemptEvidence.transcriptOffset,
|
|
@@ -9844,7 +10258,15 @@ export class Daemon extends EventEmitter {
|
|
|
9844
10258
|
}
|
|
9845
10259
|
const pasteGeneration = this.spawnGeneration;
|
|
9846
10260
|
beforePaste?.();
|
|
9847
|
-
const pasted = await this.
|
|
10261
|
+
const pasted = await this.sendDeliveryPaste(formatted, nativeOwner);
|
|
10262
|
+
if (!pasted && this.nativePaneLifecycle) {
|
|
10263
|
+
// The native write attempt is retained as uncertain, including exhausted positive refusals. Never route an
|
|
10264
|
+
// unknown/partial write through legacy window recovery or abort it back to an automatically replayable row.
|
|
10265
|
+
verdict.paneWriteStarted = true;
|
|
10266
|
+
verdict.phase = "paste";
|
|
10267
|
+
verdict.proof = "native-paste-held";
|
|
10268
|
+
return false;
|
|
10269
|
+
}
|
|
9848
10270
|
if (pasted)
|
|
9849
10271
|
this.rememberPaste(formatted, pasteGeneration);
|
|
9850
10272
|
if (!pasted) {
|
|
@@ -9930,15 +10352,15 @@ export class Daemon extends EventEmitter {
|
|
|
9930
10352
|
let enterAt = Date.now();
|
|
9931
10353
|
const enterMono = performance.now();
|
|
9932
10354
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
9933
|
-
let inputHeld =
|
|
9934
|
-
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt,
|
|
10355
|
+
let inputHeld = "";
|
|
10356
|
+
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof || nativeOwner ? steerEnterCurrent : undefined, durableAttempt, reason => { inputHeld = reason; }))) {
|
|
9935
10357
|
if (inputHeld) {
|
|
9936
10358
|
// #2971: held in a tmux mode past the bound. The paste may still be in the CLI: keep it uncertain, never re-paste.
|
|
9937
10359
|
verdict.phase = "submit-enter";
|
|
9938
|
-
verdict.proof = "pane-in-mode-held"
|
|
10360
|
+
verdict.proof = inputHeld === "pane_mode" ? "pane-in-mode-held" : `native-input-${inputHeld}`;
|
|
9939
10361
|
return false;
|
|
9940
10362
|
}
|
|
9941
|
-
if (steerComposerProof && !steerCurrent()) {
|
|
10363
|
+
if ((steerComposerProof || nativeOwner) && !steerCurrent()) {
|
|
9942
10364
|
verdict.phase = "submit-enter";
|
|
9943
10365
|
verdict.proof = "fenced-before-enter";
|
|
9944
10366
|
return false;
|
|
@@ -9967,7 +10389,7 @@ export class Daemon extends EventEmitter {
|
|
|
9967
10389
|
if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
|
|
9968
10390
|
await new Promise(r => setTimeout(r, 1_000));
|
|
9969
10391
|
const retryAt = Date.now();
|
|
9970
|
-
if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
|
|
10392
|
+
if (await this.sendDeliveryEnter("queue-less-defensive-retry", nativeOwner)) {
|
|
9971
10393
|
enterAt = retryAt;
|
|
9972
10394
|
enterRetry = true;
|
|
9973
10395
|
this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
|
|
@@ -10098,7 +10520,7 @@ export class Daemon extends EventEmitter {
|
|
|
10098
10520
|
// again. The prompt is back now, so an Enter can land.
|
|
10099
10521
|
this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
|
|
10100
10522
|
const strandedAt = Date.now();
|
|
10101
|
-
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
|
|
10523
|
+
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", nativeOwner))) {
|
|
10102
10524
|
return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
|
|
10103
10525
|
}
|
|
10104
10526
|
// The recovery Enter can be accepted before Codex paints its new
|
|
@@ -10151,7 +10573,7 @@ export class Daemon extends EventEmitter {
|
|
|
10151
10573
|
}
|
|
10152
10574
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
10153
10575
|
const retryAt = Date.now();
|
|
10154
|
-
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
|
|
10576
|
+
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", nativeOwner))) {
|
|
10155
10577
|
return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
|
|
10156
10578
|
}
|
|
10157
10579
|
if (windowId && this.controlClient) {
|
|
@@ -10203,7 +10625,7 @@ export class Daemon extends EventEmitter {
|
|
|
10203
10625
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
10204
10626
|
const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
|
|
10205
10627
|
const retryAt = Date.now();
|
|
10206
|
-
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
|
|
10628
|
+
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", nativeOwner))) {
|
|
10207
10629
|
return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
|
|
10208
10630
|
}
|
|
10209
10631
|
submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
|
|
@@ -10291,7 +10713,7 @@ export class Daemon extends EventEmitter {
|
|
|
10291
10713
|
}
|
|
10292
10714
|
// No control client to observe output: fall back to the legacy double-Enter.
|
|
10293
10715
|
await new Promise(r => setTimeout(r, 1000));
|
|
10294
|
-
await this.sendDeliveryEnter("unobserved-defensive-retry");
|
|
10716
|
+
await this.sendDeliveryEnter("unobserved-defensive-retry", nativeOwner);
|
|
10295
10717
|
if (status)
|
|
10296
10718
|
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10297
10719
|
}
|
|
@@ -10511,14 +10933,22 @@ export class Daemon extends EventEmitter {
|
|
|
10511
10933
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
10512
10934
|
*/
|
|
10513
10935
|
transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
|
|
10936
|
+
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
10937
|
+
throw Error("Native proof cannot read a coordinator-local transcript path");
|
|
10514
10938
|
const launchAtWrite = this.launchFenceEpoch;
|
|
10515
10939
|
return {
|
|
10516
|
-
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
10940
|
+
reader: checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
10517
10941
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
10518
|
-
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
|
|
10942
|
+
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
|
|
10943
|
+
&& (!checkpoint.runtime || this.nativePaneLifecycle?.current(checkpoint.runtime) === true),
|
|
10519
10944
|
onProof,
|
|
10520
10945
|
};
|
|
10521
10946
|
}
|
|
10947
|
+
nativeTranscriptReader(reference, needle) {
|
|
10948
|
+
if (!this.nativePaneLifecycle || !this.nativePaneLifecycle.current(reference))
|
|
10949
|
+
throw Error("Native delivery transcript owner is unavailable");
|
|
10950
|
+
return this.nativePaneLifecycle.proofReader(reference, needle);
|
|
10951
|
+
}
|
|
10522
10952
|
/** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
|
|
10523
10953
|
cliLabel() {
|
|
10524
10954
|
return this.config.backend ?? this.backend?.binaryName ?? "CLI";
|
|
@@ -10762,7 +11192,14 @@ export class Daemon extends EventEmitter {
|
|
|
10762
11192
|
* (the retries included). Without a guard it is exactly the old unconditional path.
|
|
10763
11193
|
*/
|
|
10764
11194
|
async submitSystemPaste(text, label, guard, admission) {
|
|
10765
|
-
|
|
11195
|
+
// Alpha (S-36, Prism #2989 P2): a NATIVE pane's system paste keeps the pane owner from before its first await
|
|
11196
|
+
// (#2958's fence, reverted on main for tmux panes) — a pane replaced while the paste awaits never takes its Enter.
|
|
11197
|
+
const native = !!this.nativePaneLifecycle;
|
|
11198
|
+
const ownerTmux = this.tmux, ownerGeneration = this.spawnGeneration, ownerFence = this.launchFenceEpoch;
|
|
11199
|
+
const ownerWindow = native ? ownerTmux?.getWindowId() : undefined;
|
|
11200
|
+
const owner = () => !native || (!!ownerTmux && !!ownerWindow && this.tmux === ownerTmux && ownerTmux.getWindowId() === ownerWindow
|
|
11201
|
+
&& this.spawnGeneration === ownerGeneration && this.launchFenceEpoch === ownerFence && !this.deliveryWritesStopping);
|
|
11202
|
+
const current = guard || admission || native ? () => { admission?.(); return owner() && (guard?.current() ?? true); } : undefined;
|
|
10766
11203
|
if (current && !current())
|
|
10767
11204
|
return false;
|
|
10768
11205
|
if (!this.tmux)
|
|
@@ -10882,13 +11319,13 @@ export class Daemon extends EventEmitter {
|
|
|
10882
11319
|
&& sameInteractionOwner(recoveryOwner, this.interactionOwner());
|
|
10883
11320
|
const previousWindowId = this.tmux?.getWindowId();
|
|
10884
11321
|
try {
|
|
10885
|
-
const windows = await
|
|
11322
|
+
const windows = await this.paneHost.listWindows(this.tmuxSessionName, this.controlClient);
|
|
10886
11323
|
if (!recoveryCurrent() || this.tmux !== previousTmux)
|
|
10887
11324
|
return undefined;
|
|
10888
11325
|
const match = windows.find(w => w.name === this.name);
|
|
10889
11326
|
if (!match)
|
|
10890
11327
|
return undefined;
|
|
10891
|
-
this.tmux =
|
|
11328
|
+
this.tmux = this.paneHost.bind(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
10892
11329
|
const recoveredTmux = this.tmux;
|
|
10893
11330
|
writeFileSync(join(this.instanceDir, "window-id"), match.id);
|
|
10894
11331
|
// The window we were talking to is gone; leaving it registered means the
|
|
@@ -10951,6 +11388,8 @@ export class Daemon extends EventEmitter {
|
|
|
10951
11388
|
return false;
|
|
10952
11389
|
}
|
|
10953
11390
|
getWindowId() {
|
|
11391
|
+
if (this.nativePaneLifecycle)
|
|
11392
|
+
return this.nativePaneLifecycle.current() ? this.tmux?.getWindowId() : undefined;
|
|
10954
11393
|
try {
|
|
10955
11394
|
return readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() || undefined;
|
|
10956
11395
|
}
|
|
@@ -10982,7 +11421,51 @@ export class Daemon extends EventEmitter {
|
|
|
10982
11421
|
* Handle a tool call from the MCP server (forwarded by Claude).
|
|
10983
11422
|
* Routes to the channel adapter via MessageBus.
|
|
10984
11423
|
*/
|
|
10985
|
-
|
|
11424
|
+
/** Internal native-MCP admission. The pane/machine/launch owner supplies a current, dynamically checked tool lease. */
|
|
11425
|
+
handlePaneToolCall(input, lease) {
|
|
11426
|
+
const refusal = nativePaneToolRefusal(input.tool, input.args);
|
|
11427
|
+
if (refusal)
|
|
11428
|
+
return Promise.resolve({ result: null, error: refusal });
|
|
11429
|
+
if (!lease.current() || !lease.allowed(input.tool, input.args) || this.deliveryWritesStopping)
|
|
11430
|
+
return Promise.resolve({ result: null, error: "Pane tool permission is no longer current" });
|
|
11431
|
+
const fence = this.launchFenceEpoch;
|
|
11432
|
+
const current = () => lease.current() && lease.allowed(input.tool, input.args) && !this.deliveryWritesStopping && fence === this.launchFenceEpoch;
|
|
11433
|
+
return new Promise(resolve => {
|
|
11434
|
+
let settled = false;
|
|
11435
|
+
const finish = (value) => { if (settled)
|
|
11436
|
+
return; settled = true; clearTimeout(timer); resolve(value); };
|
|
11437
|
+
const timer = setTimeout(() => finish({ result: null, error: "Pane tool result is unconfirmed; inspect its operation before retrying" }), daemonBudgetMs(input.tool) + 1000);
|
|
11438
|
+
timer.unref?.();
|
|
11439
|
+
try {
|
|
11440
|
+
this.handleToolCall({ ...input, requestId: ++this.fleetRequestSeq }, undefined, {
|
|
11441
|
+
current,
|
|
11442
|
+
dispatch: (tool, args, respond) => {
|
|
11443
|
+
if (!current()) {
|
|
11444
|
+
respond(null, "Pane tool permission was revoked before dispatch");
|
|
11445
|
+
return true;
|
|
11446
|
+
}
|
|
11447
|
+
const dispatched = lease.dispatch?.(tool, args);
|
|
11448
|
+
if (!dispatched)
|
|
11449
|
+
return false;
|
|
11450
|
+
void dispatched.then(result => respond(result), () => respond(null, "Pane tool operation was refused"));
|
|
11451
|
+
return true;
|
|
11452
|
+
},
|
|
11453
|
+
// Resolving the relay is not proof that its HTTP response reached the provider. No response-written receipt yet.
|
|
11454
|
+
send: value => { finish({ result: value.result, ...(typeof value.error === "string" ? { error: value.error } : {}) }); return false; },
|
|
11455
|
+
});
|
|
11456
|
+
}
|
|
11457
|
+
catch {
|
|
11458
|
+
finish({ result: null, error: "Pane tool operation failed" });
|
|
11459
|
+
}
|
|
11460
|
+
});
|
|
11461
|
+
}
|
|
11462
|
+
/** Only the authority lease with a verified helper stdout ACK calls this internal barrier. */
|
|
11463
|
+
paneToolResponseWritten(operationId, current) {
|
|
11464
|
+
if (!current() || this.deliveryWritesStopping || !/^[a-f0-9-]{36}$/i.test(operationId))
|
|
11465
|
+
return;
|
|
11466
|
+
this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
|
|
11467
|
+
}
|
|
11468
|
+
handleToolCall(msg, socket, peer) {
|
|
10986
11469
|
const tool = msg.tool;
|
|
10987
11470
|
const args = (msg.args ?? {});
|
|
10988
11471
|
const requestId = msg.requestId;
|
|
@@ -10990,15 +11473,15 @@ export class Daemon extends EventEmitter {
|
|
|
10990
11473
|
this.logger.debug({ tool, requestId }, "Tool call from MCP server");
|
|
10991
11474
|
// A tool call can only come from a live MCP server of ours — proof of life
|
|
10992
11475
|
// that outranks whatever the pid slot says.
|
|
10993
|
-
if (this.socketSessionNames.get(socket) === this.name) {
|
|
10994
|
-
this.noteMcpProofOfLife("tool_call", this.socketPids.get(socket));
|
|
11476
|
+
if (peer?.current() || socket && this.socketSessionNames.get(socket) === this.name) {
|
|
11477
|
+
this.noteMcpProofOfLife("tool_call", socket ? this.socketPids.get(socket) : undefined);
|
|
10995
11478
|
}
|
|
10996
11479
|
// A tool invocation is not delivery evidence. Capture which obligation it
|
|
10997
11480
|
// belongs to now, then settle it only after the adapter/fleet responds.
|
|
10998
11481
|
// Explicitly mapped sibling sessions must not satisfy this instance's turn;
|
|
10999
11482
|
// an unmapped socket is retained for legacy/tests where mcp_ready was not
|
|
11000
11483
|
// observed before the first tool call.
|
|
11001
|
-
const sourceSession = this.socketSessionNames.get(socket);
|
|
11484
|
+
const sourceSession = peer ? this.name : socket ? this.socketSessionNames.get(socket) : undefined;
|
|
11002
11485
|
const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
|
|
11003
11486
|
? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
|
|
11004
11487
|
: null;
|
|
@@ -11044,14 +11527,21 @@ export class Daemon extends EventEmitter {
|
|
|
11044
11527
|
// failed" and report unknown immediately rather than starting recovery.
|
|
11045
11528
|
this.turnReplyGuard.settleToolAttempt(replyAttempt, false);
|
|
11046
11529
|
}
|
|
11047
|
-
const
|
|
11530
|
+
const packet = { requestId, result, error, operationId };
|
|
11531
|
+
const sent = peer ? peer.current() && peer.send(packet) : socket ? this.ipcServer?.send(socket, packet) ?? false : false;
|
|
11048
11532
|
if (!sent) {
|
|
11049
|
-
|
|
11533
|
+
if (peer)
|
|
11534
|
+
this.logger.debug({ tool, requestId }, "Native MCP result awaits transport-written confirmation");
|
|
11535
|
+
else
|
|
11536
|
+
this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
|
|
11050
11537
|
}
|
|
11051
11538
|
else if (operationId) {
|
|
11052
11539
|
this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
|
|
11053
11540
|
}
|
|
11054
11541
|
};
|
|
11542
|
+
// Native Fleet/task operations carry their lease to the existing dispatcher mutation admission points.
|
|
11543
|
+
if (peer?.dispatch?.(tool, args, respond))
|
|
11544
|
+
return;
|
|
11055
11545
|
// Repo checkout — handled locally in daemon (no fleet-manager)
|
|
11056
11546
|
if (tool === "checkout_repo") {
|
|
11057
11547
|
this.handleCheckoutRepo(args, respond);
|
|
@@ -11080,7 +11570,7 @@ export class Daemon extends EventEmitter {
|
|
|
11080
11570
|
clearTimeout(timeout);
|
|
11081
11571
|
respond(respMsg.result, respMsg.error);
|
|
11082
11572
|
});
|
|
11083
|
-
this.
|
|
11573
|
+
this.broadcastFleetMessage({
|
|
11084
11574
|
type,
|
|
11085
11575
|
payload: args,
|
|
11086
11576
|
meta: { instance_name: this.name },
|
|
@@ -11106,7 +11596,7 @@ export class Daemon extends EventEmitter {
|
|
|
11106
11596
|
clearTimeout(timeout);
|
|
11107
11597
|
resolve({ result: respMsg.result, error: respMsg.error });
|
|
11108
11598
|
});
|
|
11109
|
-
this.
|
|
11599
|
+
this.broadcastFleetMessage({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
|
|
11110
11600
|
});
|
|
11111
11601
|
void (async () => {
|
|
11112
11602
|
if (tool === "preview_web_appearance") {
|
|
@@ -11183,7 +11673,7 @@ export class Daemon extends EventEmitter {
|
|
|
11183
11673
|
clearTimeout(timeout);
|
|
11184
11674
|
respond(respMsg.result, respMsg.error);
|
|
11185
11675
|
});
|
|
11186
|
-
this.
|
|
11676
|
+
this.broadcastFleetMessage({
|
|
11187
11677
|
type: "fleet_task",
|
|
11188
11678
|
payload: args,
|
|
11189
11679
|
meta: { instance_name: this.name },
|
|
@@ -11206,7 +11696,7 @@ export class Daemon extends EventEmitter {
|
|
|
11206
11696
|
clearTimeout(timeout);
|
|
11207
11697
|
respond(respMsg.result, respMsg.error);
|
|
11208
11698
|
});
|
|
11209
|
-
this.
|
|
11699
|
+
this.broadcastFleetMessage({
|
|
11210
11700
|
type: typeMap[tool],
|
|
11211
11701
|
payload: args,
|
|
11212
11702
|
meta: { instance_name: this.name, working_directory: this.config.working_directory },
|
|
@@ -11233,7 +11723,7 @@ export class Daemon extends EventEmitter {
|
|
|
11233
11723
|
clearTimeout(timeout);
|
|
11234
11724
|
respond(respMsg.result, respMsg.error);
|
|
11235
11725
|
});
|
|
11236
|
-
this.
|
|
11726
|
+
this.broadcastFleetMessage({
|
|
11237
11727
|
type: typeMap[tool],
|
|
11238
11728
|
payload: args,
|
|
11239
11729
|
meta: {
|
|
@@ -11248,11 +11738,11 @@ export class Daemon extends EventEmitter {
|
|
|
11248
11738
|
}
|
|
11249
11739
|
if (CROSS_INSTANCE_TOOLS.has(tool)) {
|
|
11250
11740
|
// Route to fleet manager via IPC (topic mode only)
|
|
11251
|
-
if (this.topicMode && this.
|
|
11741
|
+
if (this.topicMode && this.fleetTransportAvailable()) {
|
|
11252
11742
|
// Use fleetRequestId (not requestId) to avoid MCP server resolving the
|
|
11253
11743
|
// pending tool call prematurely when it receives the broadcast.
|
|
11254
11744
|
const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
|
|
11255
|
-
const senderSessionName =
|
|
11745
|
+
const senderSessionName = sourceSession;
|
|
11256
11746
|
// Shared table, so delete_instance and the deployment tools get the same
|
|
11257
11747
|
// budget here as the MCP client expects (the ad-hoc list omitted them).
|
|
11258
11748
|
const crossTimeoutMs = daemonBudgetMs(tool);
|
|
@@ -11264,7 +11754,7 @@ export class Daemon extends EventEmitter {
|
|
|
11264
11754
|
clearTimeout(timeout);
|
|
11265
11755
|
respond(respMsg.result, respMsg.error);
|
|
11266
11756
|
});
|
|
11267
|
-
this.
|
|
11757
|
+
this.broadcastFleetMessage({
|
|
11268
11758
|
type: "fleet_outbound",
|
|
11269
11759
|
tool,
|
|
11270
11760
|
args,
|
|
@@ -11323,7 +11813,7 @@ export class Daemon extends EventEmitter {
|
|
|
11323
11813
|
// Register the waiter before broadcasting. A fast/mock adapter can settle
|
|
11324
11814
|
// immediately; broadcasting first left a response-before-registration
|
|
11325
11815
|
// window that degraded into a timeout despite a completed POST.
|
|
11326
|
-
this.
|
|
11816
|
+
this.broadcastFleetMessage({
|
|
11327
11817
|
type: "fleet_outbound",
|
|
11328
11818
|
tool,
|
|
11329
11819
|
args,
|
|
@@ -11830,6 +12320,8 @@ export class Daemon extends EventEmitter {
|
|
|
11830
12320
|
catch { /* best effort */ }
|
|
11831
12321
|
}
|
|
11832
12322
|
async spawnClaudeWindow() {
|
|
12323
|
+
if (this.nativePaneLifecycle)
|
|
12324
|
+
throw Error("Native panes never enter local resume/fresh fallback");
|
|
11833
12325
|
this.assertSessionOomAdmission();
|
|
11834
12326
|
this.beginSpawn();
|
|
11835
12327
|
let resumedSuccessfully = false;
|
|
@@ -12416,6 +12908,14 @@ export class Daemon extends EventEmitter {
|
|
|
12416
12908
|
await this.museUsageRelay.stop().catch(() => { });
|
|
12417
12909
|
this.museUsageRelay = null;
|
|
12418
12910
|
}
|
|
12911
|
+
if (this.nativePaneLifecycle) {
|
|
12912
|
+
await this.nativePaneLifecycle.release("detach");
|
|
12913
|
+
try {
|
|
12914
|
+
unlinkSync(join(this.instanceDir, "daemon.pid"));
|
|
12915
|
+
}
|
|
12916
|
+
catch { /* metadata only */ }
|
|
12917
|
+
return;
|
|
12918
|
+
}
|
|
12419
12919
|
try {
|
|
12420
12920
|
await this.killProcessTree("SIGTERM", "startup aborted");
|
|
12421
12921
|
}
|
|
@@ -12445,13 +12945,15 @@ export class Daemon extends EventEmitter {
|
|
|
12445
12945
|
}
|
|
12446
12946
|
/** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
|
|
12447
12947
|
async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
|
|
12948
|
+
if (this.nativePaneLifecycle)
|
|
12949
|
+
throw Error("Native processes can only be closed through their owned runtime pane");
|
|
12448
12950
|
const windowId = this.tmux?.getWindowId();
|
|
12449
12951
|
// Before createWindow succeeds there is no pane owned by this daemon.
|
|
12450
12952
|
// tmux interprets an empty selector as the session's active window.
|
|
12451
12953
|
if (!windowId?.trim())
|
|
12452
12954
|
return;
|
|
12453
12955
|
try {
|
|
12454
|
-
const pid = await
|
|
12956
|
+
const pid = await this.paneHost.getPanePid(this.tmuxSessionName, windowId);
|
|
12455
12957
|
if (pid) {
|
|
12456
12958
|
this.noteStopRequest(signal, reason);
|
|
12457
12959
|
process.kill(-pid, signal);
|
|
@@ -12692,6 +13194,8 @@ export class Daemon extends EventEmitter {
|
|
|
12692
13194
|
}
|
|
12693
13195
|
}
|
|
12694
13196
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
13197
|
+
if (this.nativePaneLifecycle)
|
|
13198
|
+
throw Error("Native relaunch requires its owned exit and a new lifecycle reservation");
|
|
12695
13199
|
this.assertSessionOomAdmission();
|
|
12696
13200
|
this.startupAdmission?.();
|
|
12697
13201
|
const launchFence = this.launchFenceEpoch;
|
|
@@ -12796,8 +13300,8 @@ export class Daemon extends EventEmitter {
|
|
|
12796
13300
|
this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
|
|
12797
13301
|
}
|
|
12798
13302
|
// Ensure tmux session exists (may have been destroyed if all windows died)
|
|
12799
|
-
await
|
|
12800
|
-
if (this.stormWindow?.observeServerAlive(await
|
|
13303
|
+
await this.paneHost.ensureSession(this.tmuxSessionName);
|
|
13304
|
+
if (this.stormWindow?.observeServerAlive(await this.paneHost.getServerPid(this.tmuxSessionName))) {
|
|
12801
13305
|
this.emit("tmux_server_crash", this.name);
|
|
12802
13306
|
}
|
|
12803
13307
|
this.startupAdmission?.();
|
|
@@ -13071,12 +13575,15 @@ export class Daemon extends EventEmitter {
|
|
|
13071
13575
|
// each have seen a respawn, pause or stop.
|
|
13072
13576
|
if (!owned())
|
|
13073
13577
|
return false;
|
|
13578
|
+
const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : answeredPane;
|
|
13579
|
+
if (!owned() || !Daemon.dialogMatches(dialog, keyScreen))
|
|
13580
|
+
return false;
|
|
13074
13581
|
if (key === "Up" || key === "Down" || key === "Enter" || key === "Escape") {
|
|
13075
|
-
if (!await this.tmux.sendSpecialKey(key))
|
|
13582
|
+
if (!await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key)))
|
|
13076
13583
|
return false;
|
|
13077
13584
|
}
|
|
13078
13585
|
else {
|
|
13079
|
-
if (!await this.tmux.sendKeys(key))
|
|
13586
|
+
if (!await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key)))
|
|
13080
13587
|
return false;
|
|
13081
13588
|
}
|
|
13082
13589
|
await new Promise(r => setTimeout(r, 200));
|
|
@@ -13090,7 +13597,8 @@ export class Daemon extends EventEmitter {
|
|
|
13090
13597
|
// Wait for next screen to render — bounded by what is left of the budget.
|
|
13091
13598
|
const renderWait = Math.max(0, Math.min(10_000, remaining()));
|
|
13092
13599
|
if (this.controlClient) {
|
|
13093
|
-
const wid =
|
|
13600
|
+
const wid = this.nativePaneLifecycle ? ownerTmux.getWindowId()
|
|
13601
|
+
: readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
|
|
13094
13602
|
await this.controlClient.waitForIdle(wid, renderWait);
|
|
13095
13603
|
}
|
|
13096
13604
|
else {
|
|
@@ -13126,7 +13634,9 @@ export class Daemon extends EventEmitter {
|
|
|
13126
13634
|
// scan parked on the old limit text.
|
|
13127
13635
|
const codexReady = this.backend.binaryName !== "codex"
|
|
13128
13636
|
|| this.isCodexLivePaneSnapshot(pane);
|
|
13129
|
-
|
|
13637
|
+
const nativeComposerReady = !this.nativePaneLifecycle || (this.backend?.readInputRow
|
|
13638
|
+
? this.backend.readInputRow(pane) !== null : this.backend?.isDeliveryInputReadyPane?.(pane) === true);
|
|
13639
|
+
if (readyPatternMatches && codexReady && nativeComposerReady) {
|
|
13130
13640
|
cleanReadyPolls++;
|
|
13131
13641
|
if (cleanReadyPolls >= 2 || remaining() <= 0) {
|
|
13132
13642
|
// A real ready prompt (with no fatal dialog on screen — those are
|
|
@@ -13251,6 +13761,16 @@ export class Daemon extends EventEmitter {
|
|
|
13251
13761
|
void refresh.call(this.backend).then(() => this.saveSessionId(), () => { });
|
|
13252
13762
|
}
|
|
13253
13763
|
saveSessionId() {
|
|
13764
|
+
if (this.nativePaneLifecycle) {
|
|
13765
|
+
try {
|
|
13766
|
+
this.nativePaneLifecycle.checkpointSession();
|
|
13767
|
+
this.ownedWorkingSession = this.backend?.getSessionId() ?? null;
|
|
13768
|
+
}
|
|
13769
|
+
catch (err) {
|
|
13770
|
+
this.logger.warn({ err }, "Native conversation checkpoint is unconfirmed");
|
|
13771
|
+
}
|
|
13772
|
+
return;
|
|
13773
|
+
}
|
|
13254
13774
|
// When a resume failure has forced a fresh start, don't persist the stale id
|
|
13255
13775
|
// back from statusline.json — that would re-arm --continue and re-loop.
|
|
13256
13776
|
if (this.skipResume)
|