@songsid/agend 2.3.4-beta.6 → 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 +15 -12
- package/dist/backend/claude-code.js +137 -133
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-package.d.ts +5 -2
- package/dist/backend/codex-package.js +1 -5
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex.d.ts +136 -113
- package/dist/backend/codex.js +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 +749 -267
- 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();
|
|
@@ -9624,38 +9896,84 @@ export class Daemon extends EventEmitter {
|
|
|
9624
9896
|
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
|
|
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
|
-
// M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
|
|
9628
|
-
// tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
|
|
9629
|
-
// the CLI this paste went to is never sent to its successor.
|
|
9630
9899
|
const tmux = this.tmux;
|
|
9900
|
+
const backend = this.backend;
|
|
9901
|
+
const native = !!this.nativePaneLifecycle;
|
|
9631
9902
|
const generation = this.spawnGeneration;
|
|
9632
9903
|
const fence = this.launchFenceEpoch;
|
|
9633
|
-
const
|
|
9634
|
-
|
|
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));
|
|
9635
9909
|
if (!current())
|
|
9636
9910
|
return false;
|
|
9637
9911
|
// The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
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");
|
|
9641
9923
|
return false;
|
|
9642
9924
|
};
|
|
9643
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
|
|
9644
9926
|
// end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
|
|
9645
|
-
// past the bound.
|
|
9646
|
-
//
|
|
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.
|
|
9647
9930
|
const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
|
|
9931
|
+
let pane;
|
|
9932
|
+
let positiveRefusals = 0, enterMarked = false, positiveRetryDeadline = Infinity;
|
|
9648
9933
|
for (;;) {
|
|
9649
|
-
const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
|
|
9934
|
+
const mode = await this.waitWhilePaneInMode(phase, current, Math.min(modeDeadline, positiveRetryDeadline));
|
|
9650
9935
|
if (mode === "stale")
|
|
9651
9936
|
return false;
|
|
9652
9937
|
if (mode === "held")
|
|
9653
|
-
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
|
+
}
|
|
9654
9972
|
if (await this.paneInTmuxMode(phase)) {
|
|
9655
9973
|
if (!current())
|
|
9656
9974
|
return false;
|
|
9657
9975
|
if (performance.now() >= modeDeadline)
|
|
9658
|
-
return hold();
|
|
9976
|
+
return hold("pane_mode");
|
|
9659
9977
|
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9660
9978
|
if (!current())
|
|
9661
9979
|
return false;
|
|
@@ -9663,54 +9981,110 @@ export class Daemon extends EventEmitter {
|
|
|
9663
9981
|
}
|
|
9664
9982
|
if (!current())
|
|
9665
9983
|
return false;
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
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");
|
|
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");
|
|
9678
10059
|
try {
|
|
9679
|
-
|
|
10060
|
+
if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
|
|
10061
|
+
return hold("dialog");
|
|
9680
10062
|
}
|
|
9681
10063
|
catch {
|
|
9682
|
-
|
|
10064
|
+
return hold("check_failed");
|
|
9683
10065
|
}
|
|
9684
10066
|
if (!current())
|
|
9685
10067
|
return false;
|
|
9686
|
-
|
|
10068
|
+
if (performance.now() >= deadline)
|
|
10069
|
+
return hold("output_changed");
|
|
10070
|
+
let sent;
|
|
9687
10071
|
try {
|
|
9688
|
-
|
|
10072
|
+
sent = await pane.pasteBuffer(text, { guard: current, proof: { kind: "composer", screen } });
|
|
9689
10073
|
}
|
|
9690
10074
|
catch {
|
|
9691
|
-
|
|
10075
|
+
return hold("unconfirmed");
|
|
9692
10076
|
}
|
|
9693
|
-
if (
|
|
9694
|
-
|
|
9695
|
-
|
|
10077
|
+
if (sent)
|
|
10078
|
+
return true; // Exactly one successful paste ends the loop, before any Enter.
|
|
10079
|
+
if (!current())
|
|
9696
10080
|
return false;
|
|
9697
|
-
|
|
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));
|
|
9698
10086
|
}
|
|
9699
|
-
|
|
9700
|
-
// the key. A crash after this commit is never auto-replayed without a
|
|
9701
|
-
// positive transcript marker, even if the stale pane still shows composer.
|
|
9702
|
-
if (durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
|
|
9703
|
-
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
9704
|
-
return false;
|
|
9705
|
-
}
|
|
9706
|
-
const sent = await tmux.sendSpecialKey("Enter");
|
|
9707
|
-
if (!sent) {
|
|
9708
|
-
this.logger.error({
|
|
9709
|
-
phase,
|
|
9710
|
-
tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
|
|
9711
|
-
}, "tmux send-keys Enter failed during message delivery");
|
|
9712
|
-
}
|
|
9713
|
-
return sent;
|
|
10087
|
+
return hold("output_changed");
|
|
9714
10088
|
}
|
|
9715
10089
|
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
|
|
9716
10090
|
// Last and defaulted so the positional callers that ignore the outcome stay
|
|
@@ -9748,6 +10122,9 @@ export class Daemon extends EventEmitter {
|
|
|
9748
10122
|
const tmuxAtWrite = this.tmux;
|
|
9749
10123
|
const steerCurrent = () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping
|
|
9750
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;
|
|
9751
10128
|
// Bug A: paste with backoff. Transient failures are usually a stale window id
|
|
9752
10129
|
// after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
|
|
9753
10130
|
const maxAttempts = 3;
|
|
@@ -9815,8 +10192,8 @@ export class Daemon extends EventEmitter {
|
|
|
9815
10192
|
}
|
|
9816
10193
|
verdict.durableBeginCommitted = true;
|
|
9817
10194
|
verdict.submissionMode = attemptEvidence.submissionMode;
|
|
9818
|
-
if (attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
|
|
9819
|
-
verdict.transcriptCheckpoint = {
|
|
10195
|
+
if (attemptEvidence.runtimeTranscript || attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
|
|
10196
|
+
verdict.transcriptCheckpoint = attemptEvidence.runtimeTranscript ? { backend: attemptEvidence.backend, runtime: attemptEvidence.runtimeTranscript } : {
|
|
9820
10197
|
backend: attemptEvidence.backend,
|
|
9821
10198
|
path: attemptEvidence.transcriptPath,
|
|
9822
10199
|
offset: attemptEvidence.transcriptOffset,
|
|
@@ -9881,7 +10258,15 @@ export class Daemon extends EventEmitter {
|
|
|
9881
10258
|
}
|
|
9882
10259
|
const pasteGeneration = this.spawnGeneration;
|
|
9883
10260
|
beforePaste?.();
|
|
9884
|
-
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
|
+
}
|
|
9885
10270
|
if (pasted)
|
|
9886
10271
|
this.rememberPaste(formatted, pasteGeneration);
|
|
9887
10272
|
if (!pasted) {
|
|
@@ -9967,16 +10352,15 @@ export class Daemon extends EventEmitter {
|
|
|
9967
10352
|
let enterAt = Date.now();
|
|
9968
10353
|
const enterMono = performance.now();
|
|
9969
10354
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
9970
|
-
let inputHeld =
|
|
9971
|
-
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; }))) {
|
|
9972
10357
|
if (inputHeld) {
|
|
9973
|
-
// #2971: held in a tmux mode past the bound
|
|
9974
|
-
// the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
|
|
10358
|
+
// #2971: held in a tmux mode past the bound. The paste may still be in the CLI: keep it uncertain, never re-paste.
|
|
9975
10359
|
verdict.phase = "submit-enter";
|
|
9976
|
-
verdict.proof = inputHeld === "
|
|
10360
|
+
verdict.proof = inputHeld === "pane_mode" ? "pane-in-mode-held" : `native-input-${inputHeld}`;
|
|
9977
10361
|
return false;
|
|
9978
10362
|
}
|
|
9979
|
-
if (steerComposerProof && !steerCurrent()) {
|
|
10363
|
+
if ((steerComposerProof || nativeOwner) && !steerCurrent()) {
|
|
9980
10364
|
verdict.phase = "submit-enter";
|
|
9981
10365
|
verdict.proof = "fenced-before-enter";
|
|
9982
10366
|
return false;
|
|
@@ -10005,7 +10389,7 @@ export class Daemon extends EventEmitter {
|
|
|
10005
10389
|
if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
|
|
10006
10390
|
await new Promise(r => setTimeout(r, 1_000));
|
|
10007
10391
|
const retryAt = Date.now();
|
|
10008
|
-
if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
|
|
10392
|
+
if (await this.sendDeliveryEnter("queue-less-defensive-retry", nativeOwner)) {
|
|
10009
10393
|
enterAt = retryAt;
|
|
10010
10394
|
enterRetry = true;
|
|
10011
10395
|
this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
|
|
@@ -10136,7 +10520,7 @@ export class Daemon extends EventEmitter {
|
|
|
10136
10520
|
// again. The prompt is back now, so an Enter can land.
|
|
10137
10521
|
this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
|
|
10138
10522
|
const strandedAt = Date.now();
|
|
10139
|
-
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
|
|
10523
|
+
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", nativeOwner))) {
|
|
10140
10524
|
return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
|
|
10141
10525
|
}
|
|
10142
10526
|
// The recovery Enter can be accepted before Codex paints its new
|
|
@@ -10189,7 +10573,7 @@ export class Daemon extends EventEmitter {
|
|
|
10189
10573
|
}
|
|
10190
10574
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
10191
10575
|
const retryAt = Date.now();
|
|
10192
|
-
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
|
|
10576
|
+
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", nativeOwner))) {
|
|
10193
10577
|
return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
|
|
10194
10578
|
}
|
|
10195
10579
|
if (windowId && this.controlClient) {
|
|
@@ -10241,7 +10625,7 @@ export class Daemon extends EventEmitter {
|
|
|
10241
10625
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
10242
10626
|
const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
|
|
10243
10627
|
const retryAt = Date.now();
|
|
10244
|
-
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
|
|
10628
|
+
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", nativeOwner))) {
|
|
10245
10629
|
return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
|
|
10246
10630
|
}
|
|
10247
10631
|
submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
|
|
@@ -10329,7 +10713,7 @@ export class Daemon extends EventEmitter {
|
|
|
10329
10713
|
}
|
|
10330
10714
|
// No control client to observe output: fall back to the legacy double-Enter.
|
|
10331
10715
|
await new Promise(r => setTimeout(r, 1000));
|
|
10332
|
-
await this.sendDeliveryEnter("unobserved-defensive-retry");
|
|
10716
|
+
await this.sendDeliveryEnter("unobserved-defensive-retry", nativeOwner);
|
|
10333
10717
|
if (status)
|
|
10334
10718
|
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10335
10719
|
}
|
|
@@ -10549,14 +10933,22 @@ export class Daemon extends EventEmitter {
|
|
|
10549
10933
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
10550
10934
|
*/
|
|
10551
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");
|
|
10552
10938
|
const launchAtWrite = this.launchFenceEpoch;
|
|
10553
10939
|
return {
|
|
10554
|
-
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),
|
|
10555
10941
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
10556
|
-
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
|
|
10942
|
+
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
|
|
10943
|
+
&& (!checkpoint.runtime || this.nativePaneLifecycle?.current(checkpoint.runtime) === true),
|
|
10557
10944
|
onProof,
|
|
10558
10945
|
};
|
|
10559
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
|
+
}
|
|
10560
10952
|
/** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
|
|
10561
10953
|
cliLabel() {
|
|
10562
10954
|
return this.config.backend ?? this.backend?.binaryName ?? "CLI";
|
|
@@ -10800,7 +11192,14 @@ export class Daemon extends EventEmitter {
|
|
|
10800
11192
|
* (the retries included). Without a guard it is exactly the old unconditional path.
|
|
10801
11193
|
*/
|
|
10802
11194
|
async submitSystemPaste(text, label, guard, admission) {
|
|
10803
|
-
|
|
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;
|
|
10804
11203
|
if (current && !current())
|
|
10805
11204
|
return false;
|
|
10806
11205
|
if (!this.tmux)
|
|
@@ -10920,13 +11319,13 @@ export class Daemon extends EventEmitter {
|
|
|
10920
11319
|
&& sameInteractionOwner(recoveryOwner, this.interactionOwner());
|
|
10921
11320
|
const previousWindowId = this.tmux?.getWindowId();
|
|
10922
11321
|
try {
|
|
10923
|
-
const windows = await
|
|
11322
|
+
const windows = await this.paneHost.listWindows(this.tmuxSessionName, this.controlClient);
|
|
10924
11323
|
if (!recoveryCurrent() || this.tmux !== previousTmux)
|
|
10925
11324
|
return undefined;
|
|
10926
11325
|
const match = windows.find(w => w.name === this.name);
|
|
10927
11326
|
if (!match)
|
|
10928
11327
|
return undefined;
|
|
10929
|
-
this.tmux =
|
|
11328
|
+
this.tmux = this.paneHost.bind(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
10930
11329
|
const recoveredTmux = this.tmux;
|
|
10931
11330
|
writeFileSync(join(this.instanceDir, "window-id"), match.id);
|
|
10932
11331
|
// The window we were talking to is gone; leaving it registered means the
|
|
@@ -10989,6 +11388,8 @@ export class Daemon extends EventEmitter {
|
|
|
10989
11388
|
return false;
|
|
10990
11389
|
}
|
|
10991
11390
|
getWindowId() {
|
|
11391
|
+
if (this.nativePaneLifecycle)
|
|
11392
|
+
return this.nativePaneLifecycle.current() ? this.tmux?.getWindowId() : undefined;
|
|
10992
11393
|
try {
|
|
10993
11394
|
return readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() || undefined;
|
|
10994
11395
|
}
|
|
@@ -11020,7 +11421,51 @@ export class Daemon extends EventEmitter {
|
|
|
11020
11421
|
* Handle a tool call from the MCP server (forwarded by Claude).
|
|
11021
11422
|
* Routes to the channel adapter via MessageBus.
|
|
11022
11423
|
*/
|
|
11023
|
-
|
|
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) {
|
|
11024
11469
|
const tool = msg.tool;
|
|
11025
11470
|
const args = (msg.args ?? {});
|
|
11026
11471
|
const requestId = msg.requestId;
|
|
@@ -11028,15 +11473,15 @@ export class Daemon extends EventEmitter {
|
|
|
11028
11473
|
this.logger.debug({ tool, requestId }, "Tool call from MCP server");
|
|
11029
11474
|
// A tool call can only come from a live MCP server of ours — proof of life
|
|
11030
11475
|
// that outranks whatever the pid slot says.
|
|
11031
|
-
if (this.socketSessionNames.get(socket) === this.name) {
|
|
11032
|
-
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);
|
|
11033
11478
|
}
|
|
11034
11479
|
// A tool invocation is not delivery evidence. Capture which obligation it
|
|
11035
11480
|
// belongs to now, then settle it only after the adapter/fleet responds.
|
|
11036
11481
|
// Explicitly mapped sibling sessions must not satisfy this instance's turn;
|
|
11037
11482
|
// an unmapped socket is retained for legacy/tests where mcp_ready was not
|
|
11038
11483
|
// observed before the first tool call.
|
|
11039
|
-
const sourceSession = this.socketSessionNames.get(socket);
|
|
11484
|
+
const sourceSession = peer ? this.name : socket ? this.socketSessionNames.get(socket) : undefined;
|
|
11040
11485
|
const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
|
|
11041
11486
|
? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
|
|
11042
11487
|
: null;
|
|
@@ -11082,14 +11527,21 @@ export class Daemon extends EventEmitter {
|
|
|
11082
11527
|
// failed" and report unknown immediately rather than starting recovery.
|
|
11083
11528
|
this.turnReplyGuard.settleToolAttempt(replyAttempt, false);
|
|
11084
11529
|
}
|
|
11085
|
-
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;
|
|
11086
11532
|
if (!sent) {
|
|
11087
|
-
|
|
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");
|
|
11088
11537
|
}
|
|
11089
11538
|
else if (operationId) {
|
|
11090
11539
|
this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
|
|
11091
11540
|
}
|
|
11092
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;
|
|
11093
11545
|
// Repo checkout — handled locally in daemon (no fleet-manager)
|
|
11094
11546
|
if (tool === "checkout_repo") {
|
|
11095
11547
|
this.handleCheckoutRepo(args, respond);
|
|
@@ -11118,7 +11570,7 @@ export class Daemon extends EventEmitter {
|
|
|
11118
11570
|
clearTimeout(timeout);
|
|
11119
11571
|
respond(respMsg.result, respMsg.error);
|
|
11120
11572
|
});
|
|
11121
|
-
this.
|
|
11573
|
+
this.broadcastFleetMessage({
|
|
11122
11574
|
type,
|
|
11123
11575
|
payload: args,
|
|
11124
11576
|
meta: { instance_name: this.name },
|
|
@@ -11144,7 +11596,7 @@ export class Daemon extends EventEmitter {
|
|
|
11144
11596
|
clearTimeout(timeout);
|
|
11145
11597
|
resolve({ result: respMsg.result, error: respMsg.error });
|
|
11146
11598
|
});
|
|
11147
|
-
this.
|
|
11599
|
+
this.broadcastFleetMessage({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
|
|
11148
11600
|
});
|
|
11149
11601
|
void (async () => {
|
|
11150
11602
|
if (tool === "preview_web_appearance") {
|
|
@@ -11221,7 +11673,7 @@ export class Daemon extends EventEmitter {
|
|
|
11221
11673
|
clearTimeout(timeout);
|
|
11222
11674
|
respond(respMsg.result, respMsg.error);
|
|
11223
11675
|
});
|
|
11224
|
-
this.
|
|
11676
|
+
this.broadcastFleetMessage({
|
|
11225
11677
|
type: "fleet_task",
|
|
11226
11678
|
payload: args,
|
|
11227
11679
|
meta: { instance_name: this.name },
|
|
@@ -11244,7 +11696,7 @@ export class Daemon extends EventEmitter {
|
|
|
11244
11696
|
clearTimeout(timeout);
|
|
11245
11697
|
respond(respMsg.result, respMsg.error);
|
|
11246
11698
|
});
|
|
11247
|
-
this.
|
|
11699
|
+
this.broadcastFleetMessage({
|
|
11248
11700
|
type: typeMap[tool],
|
|
11249
11701
|
payload: args,
|
|
11250
11702
|
meta: { instance_name: this.name, working_directory: this.config.working_directory },
|
|
@@ -11271,7 +11723,7 @@ export class Daemon extends EventEmitter {
|
|
|
11271
11723
|
clearTimeout(timeout);
|
|
11272
11724
|
respond(respMsg.result, respMsg.error);
|
|
11273
11725
|
});
|
|
11274
|
-
this.
|
|
11726
|
+
this.broadcastFleetMessage({
|
|
11275
11727
|
type: typeMap[tool],
|
|
11276
11728
|
payload: args,
|
|
11277
11729
|
meta: {
|
|
@@ -11286,11 +11738,11 @@ export class Daemon extends EventEmitter {
|
|
|
11286
11738
|
}
|
|
11287
11739
|
if (CROSS_INSTANCE_TOOLS.has(tool)) {
|
|
11288
11740
|
// Route to fleet manager via IPC (topic mode only)
|
|
11289
|
-
if (this.topicMode && this.
|
|
11741
|
+
if (this.topicMode && this.fleetTransportAvailable()) {
|
|
11290
11742
|
// Use fleetRequestId (not requestId) to avoid MCP server resolving the
|
|
11291
11743
|
// pending tool call prematurely when it receives the broadcast.
|
|
11292
11744
|
const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
|
|
11293
|
-
const senderSessionName =
|
|
11745
|
+
const senderSessionName = sourceSession;
|
|
11294
11746
|
// Shared table, so delete_instance and the deployment tools get the same
|
|
11295
11747
|
// budget here as the MCP client expects (the ad-hoc list omitted them).
|
|
11296
11748
|
const crossTimeoutMs = daemonBudgetMs(tool);
|
|
@@ -11302,7 +11754,7 @@ export class Daemon extends EventEmitter {
|
|
|
11302
11754
|
clearTimeout(timeout);
|
|
11303
11755
|
respond(respMsg.result, respMsg.error);
|
|
11304
11756
|
});
|
|
11305
|
-
this.
|
|
11757
|
+
this.broadcastFleetMessage({
|
|
11306
11758
|
type: "fleet_outbound",
|
|
11307
11759
|
tool,
|
|
11308
11760
|
args,
|
|
@@ -11361,7 +11813,7 @@ export class Daemon extends EventEmitter {
|
|
|
11361
11813
|
// Register the waiter before broadcasting. A fast/mock adapter can settle
|
|
11362
11814
|
// immediately; broadcasting first left a response-before-registration
|
|
11363
11815
|
// window that degraded into a timeout despite a completed POST.
|
|
11364
|
-
this.
|
|
11816
|
+
this.broadcastFleetMessage({
|
|
11365
11817
|
type: "fleet_outbound",
|
|
11366
11818
|
tool,
|
|
11367
11819
|
args,
|
|
@@ -11868,6 +12320,8 @@ export class Daemon extends EventEmitter {
|
|
|
11868
12320
|
catch { /* best effort */ }
|
|
11869
12321
|
}
|
|
11870
12322
|
async spawnClaudeWindow() {
|
|
12323
|
+
if (this.nativePaneLifecycle)
|
|
12324
|
+
throw Error("Native panes never enter local resume/fresh fallback");
|
|
11871
12325
|
this.assertSessionOomAdmission();
|
|
11872
12326
|
this.beginSpawn();
|
|
11873
12327
|
let resumedSuccessfully = false;
|
|
@@ -12454,6 +12908,14 @@ export class Daemon extends EventEmitter {
|
|
|
12454
12908
|
await this.museUsageRelay.stop().catch(() => { });
|
|
12455
12909
|
this.museUsageRelay = null;
|
|
12456
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
|
+
}
|
|
12457
12919
|
try {
|
|
12458
12920
|
await this.killProcessTree("SIGTERM", "startup aborted");
|
|
12459
12921
|
}
|
|
@@ -12483,13 +12945,15 @@ export class Daemon extends EventEmitter {
|
|
|
12483
12945
|
}
|
|
12484
12946
|
/** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
|
|
12485
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");
|
|
12486
12950
|
const windowId = this.tmux?.getWindowId();
|
|
12487
12951
|
// Before createWindow succeeds there is no pane owned by this daemon.
|
|
12488
12952
|
// tmux interprets an empty selector as the session's active window.
|
|
12489
12953
|
if (!windowId?.trim())
|
|
12490
12954
|
return;
|
|
12491
12955
|
try {
|
|
12492
|
-
const pid = await
|
|
12956
|
+
const pid = await this.paneHost.getPanePid(this.tmuxSessionName, windowId);
|
|
12493
12957
|
if (pid) {
|
|
12494
12958
|
this.noteStopRequest(signal, reason);
|
|
12495
12959
|
process.kill(-pid, signal);
|
|
@@ -12730,6 +13194,8 @@ export class Daemon extends EventEmitter {
|
|
|
12730
13194
|
}
|
|
12731
13195
|
}
|
|
12732
13196
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
13197
|
+
if (this.nativePaneLifecycle)
|
|
13198
|
+
throw Error("Native relaunch requires its owned exit and a new lifecycle reservation");
|
|
12733
13199
|
this.assertSessionOomAdmission();
|
|
12734
13200
|
this.startupAdmission?.();
|
|
12735
13201
|
const launchFence = this.launchFenceEpoch;
|
|
@@ -12834,8 +13300,8 @@ export class Daemon extends EventEmitter {
|
|
|
12834
13300
|
this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
|
|
12835
13301
|
}
|
|
12836
13302
|
// Ensure tmux session exists (may have been destroyed if all windows died)
|
|
12837
|
-
await
|
|
12838
|
-
if (this.stormWindow?.observeServerAlive(await
|
|
13303
|
+
await this.paneHost.ensureSession(this.tmuxSessionName);
|
|
13304
|
+
if (this.stormWindow?.observeServerAlive(await this.paneHost.getServerPid(this.tmuxSessionName))) {
|
|
12839
13305
|
this.emit("tmux_server_crash", this.name);
|
|
12840
13306
|
}
|
|
12841
13307
|
this.startupAdmission?.();
|
|
@@ -13109,12 +13575,15 @@ export class Daemon extends EventEmitter {
|
|
|
13109
13575
|
// each have seen a respawn, pause or stop.
|
|
13110
13576
|
if (!owned())
|
|
13111
13577
|
return false;
|
|
13578
|
+
const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : answeredPane;
|
|
13579
|
+
if (!owned() || !Daemon.dialogMatches(dialog, keyScreen))
|
|
13580
|
+
return false;
|
|
13112
13581
|
if (key === "Up" || key === "Down" || key === "Enter" || key === "Escape") {
|
|
13113
|
-
if (!await this.tmux.sendSpecialKey(key))
|
|
13582
|
+
if (!await (this.nativePaneLifecycle ? this.tmux.sendSpecialKey(key, undefined, { kind: "dialog", screen: keyScreen }) : this.tmux.sendSpecialKey(key)))
|
|
13114
13583
|
return false;
|
|
13115
13584
|
}
|
|
13116
13585
|
else {
|
|
13117
|
-
if (!await this.tmux.sendKeys(key))
|
|
13586
|
+
if (!await (this.nativePaneLifecycle ? this.tmux.sendKeys(key, { kind: "dialog", screen: keyScreen }) : this.tmux.sendKeys(key)))
|
|
13118
13587
|
return false;
|
|
13119
13588
|
}
|
|
13120
13589
|
await new Promise(r => setTimeout(r, 200));
|
|
@@ -13128,7 +13597,8 @@ export class Daemon extends EventEmitter {
|
|
|
13128
13597
|
// Wait for next screen to render — bounded by what is left of the budget.
|
|
13129
13598
|
const renderWait = Math.max(0, Math.min(10_000, remaining()));
|
|
13130
13599
|
if (this.controlClient) {
|
|
13131
|
-
const wid =
|
|
13600
|
+
const wid = this.nativePaneLifecycle ? ownerTmux.getWindowId()
|
|
13601
|
+
: readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
|
|
13132
13602
|
await this.controlClient.waitForIdle(wid, renderWait);
|
|
13133
13603
|
}
|
|
13134
13604
|
else {
|
|
@@ -13164,7 +13634,9 @@ export class Daemon extends EventEmitter {
|
|
|
13164
13634
|
// scan parked on the old limit text.
|
|
13165
13635
|
const codexReady = this.backend.binaryName !== "codex"
|
|
13166
13636
|
|| this.isCodexLivePaneSnapshot(pane);
|
|
13167
|
-
|
|
13637
|
+
const nativeComposerReady = !this.nativePaneLifecycle || (this.backend?.readInputRow
|
|
13638
|
+
? this.backend.readInputRow(pane) !== null : this.backend?.isDeliveryInputReadyPane?.(pane) === true);
|
|
13639
|
+
if (readyPatternMatches && codexReady && nativeComposerReady) {
|
|
13168
13640
|
cleanReadyPolls++;
|
|
13169
13641
|
if (cleanReadyPolls >= 2 || remaining() <= 0) {
|
|
13170
13642
|
// A real ready prompt (with no fatal dialog on screen — those are
|
|
@@ -13289,6 +13761,16 @@ export class Daemon extends EventEmitter {
|
|
|
13289
13761
|
void refresh.call(this.backend).then(() => this.saveSessionId(), () => { });
|
|
13290
13762
|
}
|
|
13291
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
|
+
}
|
|
13292
13774
|
// When a resume failure has forced a fresh start, don't persist the stale id
|
|
13293
13775
|
// back from statusline.json — that would re-arm --continue and re-loop.
|
|
13294
13776
|
if (this.skipResume)
|