@songsid/agend 2.3.5-alpha.4 → 2.3.5-beta.1
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 +0 -2
- package/README.zh-TW.md +0 -2
- package/THIRD_PARTY_NOTICES.md +12 -28
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +4 -6
- package/dist/agent-endpoint.js +15 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.d.ts +0 -21
- package/dist/auth-api.js +8 -93
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +30 -4
- package/dist/authority/session-authorizations.js +69 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +2 -12
- package/dist/authority/web-authority.js +4 -25
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +12 -15
- package/dist/backend/claude-code.js +133 -152
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +14 -0
- package/dist/backend/codex-metadata.js +45 -0
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex-package.d.ts +2 -5
- package/dist/backend/codex-package.js +5 -1
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex-quota-history.d.ts +7 -0
- package/dist/backend/codex-quota-history.js +72 -0
- package/dist/backend/codex-quota-history.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +6 -0
- package/dist/backend/codex-session-lookup.js +1 -1
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex-speed.d.ts +14 -0
- package/dist/backend/codex-speed.js +35 -0
- package/dist/backend/codex-speed.js.map +1 -0
- package/dist/backend/codex.d.ts +116 -136
- package/dist/backend/codex.js +912 -942
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok-weekly-reset.d.ts +24 -0
- package/dist/backend/grok-weekly-reset.js +95 -0
- package/dist/backend/grok-weekly-reset.js.map +1 -0
- package/dist/backend/grok.d.ts +30 -3
- package/dist/backend/grok.js +301 -76
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +50 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +15 -8
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-tmux-guard.d.ts +21 -0
- package/dist/bundled-tmux-guard.js +96 -0
- package/dist/bundled-tmux-guard.js.map +1 -0
- package/dist/cache-ledger.d.ts +95 -0
- package/dist/cache-ledger.js +276 -4
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +11 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +235 -17
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/ipc-bridge.d.ts +0 -2
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/classic-channel-manager.d.ts +2 -0
- package/dist/classic-channel-manager.js +5 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +14 -10
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +30 -71
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +3 -17
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +70 -68
- package/dist/daemon.js +774 -846
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +8 -0
- package/dist/dashboard-permissions.js +16 -0
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +26 -16
- package/dist/delivery-consumed-watch.js +31 -26
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-outbox.d.ts +0 -5
- package/dist/delivery-outbox.js +4 -32
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +42 -2
- package/dist/delivery-reconciliation.js +164 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +4 -2
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +0 -2
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +108 -245
- package/dist/fleet-manager.js +656 -2028
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-instance.d.ts +0 -7
- package/dist/general-instance.js +3 -20
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +9 -3
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/instance-config-impact.js +6 -7
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +19 -23
- package/dist/instance-lifecycle.js +216 -383
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -3
- package/dist/instructions.js +1 -7
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/locale.js +63 -86
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +28 -0
- package/dist/log-service.js +29 -0
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.d.ts +16 -2
- package/dist/logger.js +346 -66
- package/dist/logger.js.map +1 -1
- package/dist/login-flows.d.ts +7 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.js +25 -4
- package/dist/login-manager.js.map +1 -1
- package/dist/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/muse-usage-relay.d.ts +2 -0
- package/dist/muse-usage-relay.js +5 -3
- package/dist/muse-usage-relay.js.map +1 -1
- package/dist/needs-you-hub.d.ts +6 -3
- package/dist/needs-you-hub.js +31 -9
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +20 -18
- package/dist/needs-you.js +18 -14
- package/dist/needs-you.js.map +1 -1
- package/dist/office-share-page.js +1 -1
- package/dist/office-share-page.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -16
- package/dist/outbound-handlers.js +10 -72
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -38
- package/dist/outbound-schemas.js +5 -14
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-quota-history.d.ts +7 -0
- package/dist/pane-quota-history.js +47 -0
- package/dist/pane-quota-history.js.map +1 -0
- package/dist/platform-general.d.ts +46 -0
- package/dist/platform-general.js +91 -0
- package/dist/platform-general.js.map +1 -0
- package/dist/public-web-gateway.js +0 -9
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +0 -11
- package/dist/public-web-link.js +0 -13
- package/dist/public-web-link.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/quickstart-api.js +5 -9
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +8 -1
- package/dist/quickstart.js +42 -4
- package/dist/quickstart.js.map +1 -1
- package/dist/quota-notice-config.d.ts +22 -0
- package/dist/quota-notice-config.js +41 -0
- package/dist/quota-notice-config.js.map +1 -0
- package/dist/quota-notice.d.ts +7 -0
- package/dist/quota-notice.js +40 -0
- package/dist/quota-notice.js.map +1 -1
- package/dist/reply-buttons-controller.js +10 -6
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-polls.d.ts +2 -2
- package/dist/reply-polls.js +3 -3
- package/dist/reply-polls.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +9 -0
- package/dist/scheduler/task-board.js +19 -0
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/settings-api.d.ts +13 -5
- package/dist/settings-api.js +26 -96
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +0 -1
- package/dist/settings-baseline.js +1 -1
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.js +23 -28
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +1 -16
- package/dist/settings-confirmation.js +9 -47
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.d.ts +0 -4
- package/dist/settings-effect.js +1 -58
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +0 -4
- package/dist/settings-http-confirmation.js +3 -31
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-instance-patch.js +28 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/settings-request-capability.d.ts +0 -2
- package/dist/settings-request-capability.js +0 -4
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/settings-transaction.d.ts +0 -4
- package/dist/settings-transaction.js +0 -9
- package/dist/settings-transaction.js.map +1 -1
- package/dist/setup-form.js +3 -6
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +0 -3
- package/dist/setup-i18n.js.map +1 -1
- package/dist/task-board-tool.d.ts +11 -0
- package/dist/task-board-tool.js +44 -0
- package/dist/task-board-tool.js.map +1 -1
- package/dist/task-board-waker.d.ts +6 -0
- package/dist/task-board-waker.js +4 -0
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.d.ts +1 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -2
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +7 -7
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -3
- 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 +20 -3
- package/dist/transcript-monitor.js +92 -24
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +0 -5
- package/dist/transcript-sources.js +79 -8
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +3 -23
- package/dist/ui/app.html +8 -1
- package/dist/ui/backend-signin.js +21 -36
- package/dist/ui/chat-md-strings.js +9 -0
- package/dist/ui/chat-store.js +2 -13
- package/dist/ui/chat-strings.js +2 -50
- package/dist/ui/chat-thread.js +53 -3
- package/dist/ui/fleet-task-card.js +3 -3
- package/dist/ui/fleet-task-common.js +10 -0
- package/dist/ui/fleet-tasks.js +7 -5
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +37 -66
- package/dist/ui/panel-details.js +1 -2
- package/dist/ui/panel-fleet.js +88 -107
- package/dist/ui/panel-needs.js +21 -163
- package/dist/ui/panel-notices.js +4 -3
- package/dist/ui/panel-settings.js +149 -239
- package/dist/ui/preview-panel.js +84 -6
- package/dist/ui/search-dialog.js +3 -2
- package/dist/ui/settings-appearance.js +23 -122
- package/dist/ui/settings-catalog.js +10 -37
- package/dist/ui/settings-codex-copy.js +19 -0
- package/dist/ui/settings-confirm.js +1 -3
- package/dist/ui/settings-dialogs.js +20 -27
- package/dist/ui/settings-guests.js +14 -27
- package/dist/ui/settings-model.js +17 -12
- package/dist/ui/settings-quota-notice.js +100 -0
- package/dist/ui/settings-registry.js +4 -11
- package/dist/ui/settings-strings.js +4 -113
- package/dist/ui/settings-web-authority.js +13 -15
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app-route.js +2 -6
- package/dist/ui/shared/app-shell.js +18 -23
- package/dist/ui/shared/app-store.js +2 -3
- package/dist/ui/shared/app-stream.js +1 -3
- package/dist/ui/shared/app.css +40 -1312
- package/dist/ui/shared/app.js +2 -5
- package/dist/ui/shared/header-tools.js +4 -6
- package/dist/ui/shared/instance-nav.js +7 -34
- package/dist/ui/shared/instance-switch.js +3 -4
- package/dist/ui/shared/loading.css +37 -0
- package/dist/ui/shared/panel-view.js +77 -4
- package/dist/ui/shared/signin.js +6 -39
- package/dist/ui/shared/tokens.css +0 -1
- package/dist/ui/shared/ui-dialog.js +2 -2
- package/dist/ui/shared/ui-menu.js +10 -30
- package/dist/ui/shared/ui-states.js +14 -3
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/vendor/vendor.json +2 -19
- package/dist/ui/shared/view-history.css +17 -0
- package/dist/ui/shared/view-history.js +438 -0
- package/dist/ui/shared/view-strings.js +16 -8
- package/dist/ui/signin.html +5 -2
- package/dist/unattended-install.d.ts +11 -2
- package/dist/unattended-install.js +39 -5
- package/dist/unattended-install.js.map +1 -1
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/providers.d.ts +5 -0
- package/dist/usage/providers.js +5 -0
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/quota-notices.d.ts +38 -0
- package/dist/usage/quota-notices.js +160 -0
- package/dist/usage/quota-notices.js.map +1 -0
- package/dist/usage/usage-api.d.ts +32 -0
- package/dist/usage/usage-api.js +96 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +6 -1
- package/dist/usage-rollup.js +16 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +16 -0
- package/dist/view-api.js +100 -16
- package/dist/view-api.js.map +1 -1
- package/dist/view-history.d.ts +84 -0
- package/dist/view-history.js +116 -0
- package/dist/view-history.js.map +1 -0
- package/dist/web-api.d.ts +6 -38
- package/dist/web-api.js +20 -255
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +2 -19
- package/dist/web-chat-history.js +3 -72
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-only-fleet.d.ts +0 -20
- package/dist/web-only-fleet.js +0 -21
- package/dist/web-only-fleet.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/dist/web-session.d.ts +0 -7
- package/dist/web-session.js +0 -2
- package/dist/web-session.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -4
- package/dist/web-shell-routes.js +2 -7
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/private-home.cjs +1 -1
- package/package.json +3 -9
- package/dist/agent-runtime-api.d.ts +0 -4
- package/dist/agent-runtime-api.js +0 -59
- package/dist/agent-runtime-api.js.map +0 -1
- package/dist/agent-runtime-change.d.ts +0 -74
- package/dist/agent-runtime-change.js +0 -102
- package/dist/agent-runtime-change.js.map +0 -1
- package/dist/backend/grok-model-cache.d.ts +0 -7
- package/dist/backend/grok-model-cache.js +0 -65
- package/dist/backend/grok-model-cache.js.map +0 -1
- package/dist/backend/native-pane.d.ts +0 -9
- package/dist/backend/native-pane.js +0 -59
- package/dist/backend/native-pane.js.map +0 -1
- package/dist/fleet-windows-runtime.d.ts +0 -97
- package/dist/fleet-windows-runtime.js +0 -449
- package/dist/fleet-windows-runtime.js.map +0 -1
- package/dist/general-binding.d.ts +0 -85
- package/dist/general-binding.js +0 -298
- package/dist/general-binding.js.map +0 -1
- package/dist/general-host-owner.d.ts +0 -8
- package/dist/general-host-owner.js +0 -26
- package/dist/general-host-owner.js.map +0 -1
- package/dist/general-role.d.ts +0 -5
- package/dist/general-role.js +0 -26
- package/dist/general-role.js.map +0 -1
- package/dist/local-pane-endpoint.d.ts +0 -46
- package/dist/local-pane-endpoint.js +0 -189
- package/dist/local-pane-endpoint.js.map +0 -1
- package/dist/local-pane-launcher.d.ts +0 -2
- package/dist/local-pane-launcher.js +0 -38
- package/dist/local-pane-launcher.js.map +0 -1
- package/dist/local-pane-stage.ps1 +0 -101
- package/dist/model-catalog-defaults.d.ts +0 -14
- package/dist/model-catalog-defaults.js +0 -20
- package/dist/model-catalog-defaults.js.map +0 -1
- package/dist/native-agent-admission.d.ts +0 -7
- package/dist/native-agent-admission.js +0 -26
- package/dist/native-agent-admission.js.map +0 -1
- package/dist/native-daemon-client.d.ts +0 -13
- package/dist/native-daemon-client.js +0 -28
- package/dist/native-daemon-client.js.map +0 -1
- package/dist/native-instance-api.d.ts +0 -14
- package/dist/native-instance-api.js +0 -205
- package/dist/native-instance-api.js.map +0 -1
- package/dist/native-instance-input.d.ts +0 -84
- package/dist/native-instance-input.js +0 -54
- package/dist/native-instance-input.js.map +0 -1
- package/dist/native-instance-registry.d.ts +0 -32
- package/dist/native-instance-registry.js +0 -105
- package/dist/native-instance-registry.js.map +0 -1
- package/dist/native-mcp.d.ts +0 -40
- package/dist/native-mcp.js +0 -278
- package/dist/native-mcp.js.map +0 -1
- package/dist/native-model-catalog.d.ts +0 -5
- package/dist/native-model-catalog.js +0 -76
- package/dist/native-model-catalog.js.map +0 -1
- package/dist/native-pane-crash-budget.d.ts +0 -20
- package/dist/native-pane-crash-budget.js +0 -60
- package/dist/native-pane-crash-budget.js.map +0 -1
- package/dist/native-pane-create.d.ts +0 -26
- package/dist/native-pane-create.js +0 -53
- package/dist/native-pane-create.js.map +0 -1
- package/dist/native-pane-lifecycle.d.ts +0 -65
- package/dist/native-pane-lifecycle.js +0 -230
- package/dist/native-pane-lifecycle.js.map +0 -1
- package/dist/native-pane-manager.d.ts +0 -59
- package/dist/native-pane-manager.js +0 -584
- package/dist/native-pane-manager.js.map +0 -1
- package/dist/native-pane-output-log.d.ts +0 -15
- package/dist/native-pane-output-log.js +0 -108
- package/dist/native-pane-output-log.js.map +0 -1
- package/dist/native-pane-reconciliation.d.ts +0 -4
- package/dist/native-pane-reconciliation.js +0 -40
- package/dist/native-pane-reconciliation.js.map +0 -1
- package/dist/native-pane-recovery.d.ts +0 -12
- package/dist/native-pane-recovery.js +0 -10
- package/dist/native-pane-recovery.js.map +0 -1
- package/dist/native-pane-tools.d.ts +0 -2
- package/dist/native-pane-tools.js +0 -14
- package/dist/native-pane-tools.js.map +0 -1
- package/dist/pane-port.d.ts +0 -118
- package/dist/pane-port.js +0 -2
- package/dist/pane-port.js.map +0 -1
- package/dist/runtime-api.d.ts +0 -8
- package/dist/runtime-api.js +0 -171
- package/dist/runtime-api.js.map +0 -1
- package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +0 -21
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +0 -2
- package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +0 -19
- package/dist/runtime-assets/@xterm/addon-serialize/package.json +0 -29
- package/dist/runtime-assets/@xterm/headless/LICENSE +0 -21
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +0 -2
- package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +0 -46
- package/dist/runtime-assets/@xterm/headless/package.json +0 -29
- package/dist/runtime-assets/node-pty/CONPTY-LICENSE +0 -21
- package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +0 -503
- package/dist/runtime-assets/node-pty/LICENSE +0 -69
- package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +0 -16
- package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +0 -47
- package/dist/runtime-assets/node-pty/lib/index.js +0 -52
- package/dist/runtime-assets/node-pty/lib/interfaces.js +0 -7
- package/dist/runtime-assets/node-pty/lib/shared/conout.js +0 -11
- package/dist/runtime-assets/node-pty/lib/terminal.js +0 -190
- package/dist/runtime-assets/node-pty/lib/types.js +0 -7
- package/dist/runtime-assets/node-pty/lib/unixTerminal.js +0 -346
- package/dist/runtime-assets/node-pty/lib/utils.js +0 -39
- package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +0 -125
- package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +0 -320
- package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +0 -199
- package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +0 -22
- package/dist/runtime-assets/node-pty/package.json +0 -64
- 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-build-info.d.ts +0 -9
- package/dist/runtime-build-info.js +0 -57
- package/dist/runtime-build-info.js.map +0 -1
- package/dist/runtime-bundle.d.ts +0 -19
- package/dist/runtime-bundle.js +0 -156
- package/dist/runtime-bundle.js.map +0 -1
- package/dist/runtime-cli.d.ts +0 -7
- package/dist/runtime-cli.js +0 -152
- package/dist/runtime-cli.js.map +0 -1
- package/dist/runtime-client-store.d.ts +0 -68
- package/dist/runtime-client-store.js +0 -344
- package/dist/runtime-client-store.js.map +0 -1
- package/dist/runtime-client.d.ts +0 -112
- package/dist/runtime-client.js +0 -736
- package/dist/runtime-client.js.map +0 -1
- package/dist/runtime-connection.d.ts +0 -22
- package/dist/runtime-connection.js +0 -93
- package/dist/runtime-connection.js.map +0 -1
- package/dist/runtime-coordinator.d.ts +0 -91
- package/dist/runtime-coordinator.js +0 -911
- package/dist/runtime-coordinator.js.map +0 -1
- package/dist/runtime-general-protocol.d.ts +0 -22
- package/dist/runtime-general-protocol.js +0 -57
- package/dist/runtime-general-protocol.js.map +0 -1
- package/dist/runtime-general-workspace.d.ts +0 -17
- package/dist/runtime-general-workspace.js +0 -213
- package/dist/runtime-general-workspace.js.map +0 -1
- package/dist/runtime-pane-assets.d.ts +0 -17
- package/dist/runtime-pane-assets.js +0 -23
- package/dist/runtime-pane-assets.js.map +0 -1
- package/dist/runtime-pane-boot.d.ts +0 -9
- package/dist/runtime-pane-boot.js +0 -47
- package/dist/runtime-pane-boot.js.map +0 -1
- package/dist/runtime-pane-codex-index.d.ts +0 -1
- package/dist/runtime-pane-codex-index.js +0 -47
- package/dist/runtime-pane-codex-index.js.map +0 -1
- package/dist/runtime-pane-config.d.ts +0 -16
- package/dist/runtime-pane-config.js +0 -54
- package/dist/runtime-pane-config.js.map +0 -1
- package/dist/runtime-pane-host.d.ts +0 -44
- package/dist/runtime-pane-host.js +0 -424
- package/dist/runtime-pane-host.js.map +0 -1
- package/dist/runtime-pane-job-owner.d.ts +0 -1
- package/dist/runtime-pane-job-owner.js +0 -29
- package/dist/runtime-pane-job-owner.js.map +0 -1
- package/dist/runtime-pane-job.ps1 +0 -116
- package/dist/runtime-pane-link.d.ts +0 -40
- package/dist/runtime-pane-link.js +0 -246
- package/dist/runtime-pane-link.js.map +0 -1
- package/dist/runtime-pane-mcp-protocol.d.ts +0 -32
- package/dist/runtime-pane-mcp-protocol.js +0 -39
- package/dist/runtime-pane-mcp-protocol.js.map +0 -1
- package/dist/runtime-pane-mcp-relay.d.ts +0 -17
- package/dist/runtime-pane-mcp-relay.js +0 -70
- package/dist/runtime-pane-mcp-relay.js.map +0 -1
- package/dist/runtime-pane-mcp.d.ts +0 -51
- package/dist/runtime-pane-mcp.js +0 -150
- package/dist/runtime-pane-mcp.js.map +0 -1
- package/dist/runtime-pane-native.d.ts +0 -45
- package/dist/runtime-pane-native.js +0 -65
- package/dist/runtime-pane-native.js.map +0 -1
- package/dist/runtime-pane-output.d.ts +0 -19
- package/dist/runtime-pane-output.js +0 -53
- package/dist/runtime-pane-output.js.map +0 -1
- package/dist/runtime-pane-port.d.ts +0 -139
- package/dist/runtime-pane-port.js +0 -558
- package/dist/runtime-pane-port.js.map +0 -1
- package/dist/runtime-pane-private.d.ts +0 -14
- package/dist/runtime-pane-private.js +0 -51
- package/dist/runtime-pane-private.js.map +0 -1
- package/dist/runtime-pane-private.ps1 +0 -14
- package/dist/runtime-pane-process.d.ts +0 -4
- package/dist/runtime-pane-process.js +0 -108
- package/dist/runtime-pane-process.js.map +0 -1
- package/dist/runtime-pane-protocol.d.ts +0 -172
- package/dist/runtime-pane-protocol.js +0 -195
- package/dist/runtime-pane-protocol.js.map +0 -1
- package/dist/runtime-pane-screen.d.ts +0 -19
- package/dist/runtime-pane-screen.js +0 -76
- package/dist/runtime-pane-screen.js.map +0 -1
- package/dist/runtime-pane-service.d.ts +0 -11
- package/dist/runtime-pane-service.js +0 -221
- package/dist/runtime-pane-service.js.map +0 -1
- package/dist/runtime-pane-stdio-protocol.d.ts +0 -54
- package/dist/runtime-pane-stdio-protocol.js +0 -94
- package/dist/runtime-pane-stdio-protocol.js.map +0 -1
- package/dist/runtime-pane-stdio.d.ts +0 -3
- package/dist/runtime-pane-stdio.js +0 -123
- package/dist/runtime-pane-stdio.js.map +0 -1
- package/dist/runtime-pane-store.d.ts +0 -33
- package/dist/runtime-pane-store.js +0 -164
- package/dist/runtime-pane-store.js.map +0 -1
- package/dist/runtime-pane-transcript-protocol.d.ts +0 -43
- package/dist/runtime-pane-transcript-protocol.js +0 -58
- package/dist/runtime-pane-transcript-protocol.js.map +0 -1
- package/dist/runtime-pane-transcript.d.ts +0 -45
- package/dist/runtime-pane-transcript.js +0 -605
- package/dist/runtime-pane-transcript.js.map +0 -1
- package/dist/runtime-pane-worker.d.ts +0 -1
- package/dist/runtime-pane-worker.js +0 -154
- package/dist/runtime-pane-worker.js.map +0 -1
- package/dist/runtime-protocol.d.ts +0 -162
- package/dist/runtime-protocol.js +0 -37
- package/dist/runtime-protocol.js.map +0 -1
- package/dist/runtime-switches.d.ts +0 -79
- package/dist/runtime-switches.js +0 -90
- package/dist/runtime-switches.js.map +0 -1
- package/dist/runtime-transcript-delta-reader.d.ts +0 -23
- package/dist/runtime-transcript-delta-reader.js +0 -74
- package/dist/runtime-transcript-delta-reader.js.map +0 -1
- package/dist/runtime-transcript-source.d.ts +0 -36
- package/dist/runtime-transcript-source.js +0 -189
- package/dist/runtime-transcript-source.js.map +0 -1
- package/dist/runtime-windows-bridge.d.ts +0 -41
- package/dist/runtime-windows-bridge.js +0 -157
- package/dist/runtime-windows-bridge.js.map +0 -1
- package/dist/settings-route-lifetime.d.ts +0 -14
- package/dist/settings-route-lifetime.js +0 -48
- package/dist/settings-route-lifetime.js.map +0 -1
- package/dist/tmux-pane-host.d.ts +0 -26
- package/dist/tmux-pane-host.js +0 -25
- package/dist/tmux-pane-host.js.map +0 -1
- package/dist/transcript-needles.d.ts +0 -35
- package/dist/transcript-needles.js +0 -158
- package/dist/transcript-needles.js.map +0 -1
- package/dist/transcript-records.d.ts +0 -21
- package/dist/transcript-records.js +0 -140
- package/dist/transcript-records.js.map +0 -1
- package/dist/ui/agent-proposals.js +0 -90
- package/dist/ui/agent-runtime-settings.js +0 -67
- package/dist/ui/agent-runtime.js +0 -124
- package/dist/ui/cli-picker.js +0 -42
- package/dist/ui/general-location.js +0 -71
- package/dist/ui/native-instance-settings.js +0 -44
- package/dist/ui/native-pane-recovery.js +0 -45
- package/dist/ui/panel-runtimes.js +0 -334
- package/dist/ui/panel-windows-agent.js +0 -86
- package/dist/ui/panel-windows-cli.js +0 -179
- package/dist/ui/provider-marks.js +0 -83
- package/dist/ui/runtime-catalog.js +0 -55
- package/dist/ui/settings-agent-filters.js +0 -94
- package/dist/ui/settings-layout.js +0 -245
- package/dist/ui/settings-outside.js +0 -176
- package/dist/ui/settings-shortcuts.js +0 -65
- package/dist/ui/shared/machine-name.js +0 -13
- package/dist/ui/shared/qrcode.module.js +0 -2237
- package/dist/ui/shared/settings-width.js +0 -31
- package/dist/ui/shared/vendor/qrcode-generator.LICENSE +0 -27
- package/dist/ui/windows-agents-nav.js +0 -2
- package/dist/ui/windows-cli-catalog.js +0 -108
- package/dist/web-remote-access.d.ts +0 -152
- package/dist/web-remote-access.js +0 -368
- package/dist/web-remote-access.js.map +0 -1
- package/dist/windows-agents.d.ts +0 -126
- package/dist/windows-agents.js +0 -490
- package/dist/windows-agents.js.map +0 -1
- package/dist/windows-cli-api.d.ts +0 -23
- package/dist/windows-cli-api.js +0 -159
- package/dist/windows-cli-api.js.map +0 -1
- package/dist/windows-cli-providers.d.ts +0 -115
- package/dist/windows-cli-providers.js +0 -429
- package/dist/windows-cli-providers.js.map +0 -1
- package/dist/windows-cli-worker.d.ts +0 -59
- package/dist/windows-cli-worker.js +0 -479
- package/dist/windows-cli-worker.js.map +0 -1
- package/dist/windows-cli.d.ts +0 -116
- package/dist/windows-cli.js +0 -407
- package/dist/windows-cli.js.map +0 -1
- package/dist/windows-native-mcp.d.ts +0 -19
- package/dist/windows-native-mcp.js +0 -143
- package/dist/windows-native-mcp.js.map +0 -1
- package/dist/windows-pane-mcp.d.ts +0 -26
- package/dist/windows-pane-mcp.js +0 -187
- package/dist/windows-pane-mcp.js.map +0 -1
- package/templates/runtime-assets/conpty-LICENSE +0 -21
- package/templates/runtime-assets/conpty-NOTICE.md +0 -503
- package/templates/runtime-assets/local-pane-stage.ps1 +0 -101
- package/templates/runtime-assets/runtime-pane-job.ps1 +0 -116
- package/templates/runtime-assets/runtime-pane-private.ps1 +0 -14
- package/templates/runtime-assets/xterm-LICENSE +0 -21
package/dist/daemon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { quotaNoticeProblem } from "./quota-notice-config.js";
|
|
2
|
+
import { quotaHistoryCount, quotaAfterInput } from "./pane-quota-history.js";
|
|
3
3
|
import { dialogScreenText } from "./dialog-screen-text.js";
|
|
4
4
|
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
5
5
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
@@ -22,7 +22,7 @@ import { assertNoSessionOomStop, readSessionOomStop, writeSessionOomStop, Sessio
|
|
|
22
22
|
import { mcpServerState } from "./mcp-liveness.js";
|
|
23
23
|
import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
24
24
|
import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
25
|
-
import { resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
25
|
+
import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
26
26
|
import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
27
27
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
28
28
|
import { TranscriptTurnLedger } from "./transcript-turns.js";
|
|
@@ -35,14 +35,14 @@ import { scopedFleetConfigEnvironment } from "./web-experimental.js";
|
|
|
35
35
|
import { readAgentFile } from "./agent-file-read.js";
|
|
36
36
|
import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
|
|
37
37
|
import { LOGIN_FLOWS } from "./login-flows.js";
|
|
38
|
-
import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
|
|
38
|
+
import { ProgressAccumulator, isSearchProgram, summarizeProgress } from "./tool-progress.js";
|
|
39
39
|
import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
|
|
40
40
|
import { isCrossInstanceVisibility } from "./cross-instance-notice.js";
|
|
41
41
|
import { ContextGuardian } from "./context-guardian.js";
|
|
42
42
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
43
43
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
44
44
|
import { MessageBus } from "./channel/message-bus.js";
|
|
45
|
-
import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
45
|
+
import { shellQuote, KiroCompatibilityError, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
46
46
|
import { getTmuxSession } from "./config.js";
|
|
47
47
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
48
48
|
import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
|
|
@@ -50,7 +50,6 @@ import { HangDetector } from "./hang-detector.js";
|
|
|
50
50
|
import { writeSecretFile } from "./secret-file.js";
|
|
51
51
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
52
52
|
import { answerRemoteCard } from "./remote-card-answer.js";
|
|
53
|
-
import { tmuxPaneHost } from "./tmux-pane-host.js";
|
|
54
53
|
import { buildFleetInstructions } from "./instructions.js";
|
|
55
54
|
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
56
55
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
@@ -58,13 +57,13 @@ import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedT
|
|
|
58
57
|
import { TurnReplyGuard, formatReplyDropDetection, turnExpectsPersonReply } from "./turn-reply-guard.js";
|
|
59
58
|
import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT_PAUSE } from "./backend/claude-transcript-errors.js";
|
|
60
59
|
import { t } from "./locale.js";
|
|
61
|
-
import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
|
|
60
|
+
import { claudeResetInstant, claudeResetTime, quotaPausedLine } from "./quota-notice.js";
|
|
62
61
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
63
|
-
import { reconcileNativePaneTarget } from "./native-pane-reconciliation.js";
|
|
64
62
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
65
63
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
66
64
|
import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
|
|
67
|
-
import { scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
65
|
+
import { markerConsumed, scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
66
|
+
import { QUEUED_HANDOFF_MAX_REDELIVERIES, QueuedHandoffStore } from "./queued-handoffs.js";
|
|
68
67
|
import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
|
|
69
68
|
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
70
69
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
@@ -155,6 +154,8 @@ export const HANDOFF_RECHECK_MS = 1_000;
|
|
|
155
154
|
export const WAIT_STOP_POLL_MS = 250;
|
|
156
155
|
/** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
|
|
157
156
|
export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
|
|
157
|
+
/** #3030: a queued hand-off's watch outlives a long turn (the record outlives the watch in any case). */
|
|
158
|
+
export const QUEUED_HANDOFF_WATCH_LIFETIME_MS = 12 * 60 * 60_000;
|
|
158
159
|
/**
|
|
159
160
|
* An instance publishing more than this many execution-state edges inside the
|
|
160
161
|
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
@@ -347,7 +348,8 @@ export function shellCommandLabel(command) {
|
|
|
347
348
|
if (!program)
|
|
348
349
|
return "";
|
|
349
350
|
const isBareWord = (t) => !!t && /^[A-Za-z][A-Za-z0-9_-]*$/.test(t);
|
|
350
|
-
|
|
351
|
+
// A search program's next word is what it searches for, not a subcommand: `grep <token>` keeps only `grep`.
|
|
352
|
+
const label = isBareWord(tokens[1]) && !isSearchProgram(program) ? `${program} ${tokens[1]}` : program;
|
|
351
353
|
return label.length > 40 ? `${label.slice(0, 39)}…` : label;
|
|
352
354
|
}
|
|
353
355
|
export class PaneStateMachine {
|
|
@@ -1116,7 +1118,6 @@ export class Daemon extends EventEmitter {
|
|
|
1116
1118
|
spawnGate;
|
|
1117
1119
|
stormWindow;
|
|
1118
1120
|
backendOutage;
|
|
1119
|
-
paneTransport;
|
|
1120
1121
|
/** #1314: fleet-directory fallbacks already reported to the topic (once per daemon, per field and path). */
|
|
1121
1122
|
promptFileFallbackNoticed = new Set();
|
|
1122
1123
|
/** Identity of this live Daemon object; changes on object stop/restart. */
|
|
@@ -1585,6 +1586,12 @@ export class Daemon extends EventEmitter {
|
|
|
1585
1586
|
pendingWhatsNew = null;
|
|
1586
1587
|
pasteQueueDepth = 0;
|
|
1587
1588
|
/** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
|
|
1589
|
+
/** #3030: people's messages a CLI never took (or that a restart left unverifiable), told once per batch. */
|
|
1590
|
+
unsettledHandoffs = { "not-taken": [], unverifiable: [] };
|
|
1591
|
+
unsettledHandoffsTimer = null;
|
|
1592
|
+
/** #3030: since when (monotonic) the CLI has been idle without a break; null while it is not. */
|
|
1593
|
+
instanceIdleSinceMono = null;
|
|
1594
|
+
queuedHandoffStore = null;
|
|
1588
1595
|
cancelledHumans = [];
|
|
1589
1596
|
cancelledHumansTimer = null;
|
|
1590
1597
|
firstDeliveryDelay = new FirstDeliveryDelay();
|
|
@@ -1789,6 +1796,80 @@ export class Daemon extends EventEmitter {
|
|
|
1789
1796
|
// still booting. Its new tmux window starts empty before resume restores the
|
|
1790
1797
|
// old transcript, so pre-ready scans must not lower this seeded baseline.
|
|
1791
1798
|
seededErrorCountBaselines = new Map();
|
|
1799
|
+
quotaReplay = null;
|
|
1800
|
+
paneQuotaPatterns() {
|
|
1801
|
+
if (!["codex", "grok"].includes(this.backend?.binaryName ?? ""))
|
|
1802
|
+
return [];
|
|
1803
|
+
const patterns = this.backend?.getErrorPatterns?.() ?? [];
|
|
1804
|
+
return this.backend?.binaryName === "codex" ? patterns.filter(ep => Daemon.isCodexUsageLimitPause(ep)) : patterns.filter(ep => ep.type === "quota");
|
|
1805
|
+
}
|
|
1806
|
+
async prepareQuotaReplayHistory(reuseWindow) {
|
|
1807
|
+
this.quotaReplay = null;
|
|
1808
|
+
const backend = this.backend, owner = this.tmux, window = owner?.getWindowId?.();
|
|
1809
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, attempt = this.launchAttempt;
|
|
1810
|
+
const current = () => backend === this.backend && owner === this.tmux && window === owner?.getWindowId?.()
|
|
1811
|
+
&& spawn === this.spawnGeneration && fence === this.launchFenceEpoch && attempt === this.launchAttempt;
|
|
1812
|
+
const replay = backend?.quotaReplayHistory?.();
|
|
1813
|
+
if (replay)
|
|
1814
|
+
this.quotaReplay = { ...replay, spawn, fence, attempt };
|
|
1815
|
+
// A Grok wake keeps its exited pane. The replacement's first ready pane is not history proof.
|
|
1816
|
+
if (reuseWindow && backend?.resumesPreservedQuotaHistory?.()) {
|
|
1817
|
+
let oldPane;
|
|
1818
|
+
try {
|
|
1819
|
+
oldPane = await owner.capturePane();
|
|
1820
|
+
}
|
|
1821
|
+
catch {
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
this.startupAdmission?.();
|
|
1825
|
+
if (!current())
|
|
1826
|
+
return;
|
|
1827
|
+
const patterns = this.paneQuotaPatterns();
|
|
1828
|
+
const messages = oldPane.split(/\r?\n/).filter(row => patterns.some(ep => ep.pattern.test(row)));
|
|
1829
|
+
this.quotaReplay = { messages, current: () => true, spawn, fence, attempt };
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
/** Before admission, this launch may absorb ONLY rows positively known before its resume. */
|
|
1833
|
+
takeQuotaReplayBaseline(pane) {
|
|
1834
|
+
const history = this.quotaReplay;
|
|
1835
|
+
if (!history)
|
|
1836
|
+
return;
|
|
1837
|
+
this.quotaReplay = null;
|
|
1838
|
+
if (history.spawn !== this.spawnGeneration || history.fence !== this.launchFenceEpoch || history.attempt !== this.launchAttempt || !history.current())
|
|
1839
|
+
return;
|
|
1840
|
+
for (const ep of this.paneQuotaPatterns()) {
|
|
1841
|
+
const count = Math.min(Daemon.countOccurrences(ep.pattern, pane), quotaHistoryCount(pane, ep.pattern, history.messages));
|
|
1842
|
+
if (!count)
|
|
1843
|
+
continue;
|
|
1844
|
+
const key = Daemon.errorPatternKey(ep);
|
|
1845
|
+
this.lastErrorCount.set(key, Math.max(this.lastErrorCount.get(key) ?? 0, count));
|
|
1846
|
+
this.logger.debug({ backend: this.backend?.binaryName, count, evidence: "before-resume" }, "Quota rows restored from history — not a new incident");
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
// Submission makes captured rows history even when no monitor reported them yet. Keep this acknowledged input's
|
|
1850
|
+
// marker as well: a new error can replace an old row in the viewport without increasing the total match count.
|
|
1851
|
+
quotaInput = null;
|
|
1852
|
+
commitPreInputQuotaHistory(evidence) {
|
|
1853
|
+
const history = evidence?.quotaHistory;
|
|
1854
|
+
if (!history || history.spawn !== this.spawnGeneration || history.fence !== this.launchFenceEpoch || history.attempt !== this.launchAttempt
|
|
1855
|
+
|| history.tmux !== this.tmux || history.window !== this.tmux?.getWindowId?.() || this.deliveryWritesStopping)
|
|
1856
|
+
return;
|
|
1857
|
+
for (const row of history.counts)
|
|
1858
|
+
this.lastErrorCount.set(row.key, row.count);
|
|
1859
|
+
this.quotaInput = history.marker && history.occurrences === 0 ? { history, reported: new Map() } : null;
|
|
1860
|
+
}
|
|
1861
|
+
quotaOccurrencesAfterInput(pane, ep) {
|
|
1862
|
+
if (!(this.backend?.binaryName === "codex" && Daemon.isCodexUsageLimitPause(ep)
|
|
1863
|
+
|| this.backend?.binaryName === "grok" && ep.type === "quota"))
|
|
1864
|
+
return null;
|
|
1865
|
+
const input = this.quotaInput, history = input?.history;
|
|
1866
|
+
if (!input || !history || history.spawn !== this.spawnGeneration || history.fence !== this.launchFenceEpoch
|
|
1867
|
+
|| history.attempt !== this.launchAttempt || history.tmux !== this.tmux || history.window !== this.tmux?.getWindowId?.()
|
|
1868
|
+
|| this.deliveryWritesStopping)
|
|
1869
|
+
return null;
|
|
1870
|
+
const suffix = quotaAfterInput(pane, history.marker);
|
|
1871
|
+
return suffix === null ? null : { input, count: Daemon.countOccurrences(ep.pattern, suffix) };
|
|
1872
|
+
}
|
|
1792
1873
|
lastDetectedErrorType = null;
|
|
1793
1874
|
/**
|
|
1794
1875
|
* #2822: Claude's per-model usage limit (`You've reached your <model> limit.`), known only from Claude's own record of
|
|
@@ -1810,12 +1891,46 @@ export class Daemon extends EventEmitter {
|
|
|
1810
1891
|
static CREDIT_BALANCE_LOW = CLAUDE_CREDIT_BALANCE_LOW;
|
|
1811
1892
|
/** #2834 (#816): Claude's usage-limit auto-continue note, as it records it (`system` / `informational`). */
|
|
1812
1893
|
static USAGE_LIMIT_PAUSE = CLAUDE_USAGE_LIMIT_PAUSE;
|
|
1813
|
-
/**
|
|
1814
|
-
|
|
1815
|
-
|
|
1894
|
+
/** Claude's note that its usage window opened again ("Usage limit reset · continuing automatically"). */
|
|
1895
|
+
static USAGE_LIMIT_RESET = /^Usage limit reset\b/i;
|
|
1896
|
+
/** Repeats of one usage pause ("reached again" every few minutes) collapse into the first notice for this long. */
|
|
1897
|
+
static USAGE_PAUSE_NOTICE_WINDOW_MS = 30 * 60_000;
|
|
1898
|
+
/** The usage-pause notice told last: when (monotonic), Claude's words for the reset, and that reset (wall clock). */
|
|
1899
|
+
usagePauseNotice = null;
|
|
1900
|
+
/**
|
|
1901
|
+
* #2834 (#816): Claude paused itself until its usage window opens — the same notice the screen pattern gave. `at`: when
|
|
1902
|
+
* Claude wrote it (its record's timestamp). One notice per pause: a reset time already past is not this pause (an old
|
|
1903
|
+
* note), and its repeats ("reached again") within the window — or naming the same reset, until it comes — say nothing
|
|
1904
|
+
* more. Claude's own "Usage limit reset" ends it.
|
|
1905
|
+
*/
|
|
1906
|
+
/** #3186 review: the lifecycle's second opinion dropped the notice (usage has capacity): nothing was told. */
|
|
1907
|
+
clearUsagePauseNotice() { this.usagePauseNotice = null; }
|
|
1908
|
+
noteClaudeInformational(text, at = null) {
|
|
1909
|
+
const line = text.trim();
|
|
1910
|
+
if (Daemon.USAGE_LIMIT_RESET.test(line)) {
|
|
1911
|
+
this.usagePauseNotice = null;
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
const match = Daemon.USAGE_LIMIT_PAUSE.exec(line);
|
|
1816
1915
|
if (!match)
|
|
1817
1916
|
return;
|
|
1818
1917
|
const when = match[1].trim();
|
|
1918
|
+
const wallNow = Date.now(), now = performance.now();
|
|
1919
|
+
const resetAt = claudeResetInstant(when, at ?? wallNow);
|
|
1920
|
+
if (resetAt !== null && resetAt <= wallNow) {
|
|
1921
|
+
this.logger.debug({ when, writtenAt: at }, "Claude usage-limit note names a reset already past — an old note, not told");
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
// A pause told earlier is this one while its own reset has not come (unknown: within the window): its repeats —
|
|
1925
|
+
// "reached again", or a relative time drifting ("in 3h", "in 2h 55m") — within the window, or naming the same reset.
|
|
1926
|
+
// Once its reset has passed, a later note is a new pause even without Claude's "Usage limit reset" (#3186 review).
|
|
1927
|
+
const told = this.usagePauseNotice;
|
|
1928
|
+
const toldOpen = !!told && (told.resetAt === null || wallNow < told.resetAt);
|
|
1929
|
+
if (told && toldOpen && (now - told.at < Daemon.USAGE_PAUSE_NOTICE_WINDOW_MS || (told.when === when && told.resetAt !== null))) {
|
|
1930
|
+
this.logger.debug({ when }, "Claude usage-limit note repeats a pause already told — collapsed");
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
this.usagePauseNotice = { at: now, when, resetAt };
|
|
1819
1934
|
this.emit("pty_error", { name: this.name, type: "quota", action: "notify", verifyQuota: true, skipCooldown: true, skipRecoveryWait: true,
|
|
1820
1935
|
message: `Claude Code usage limit reached — paused; continuing automatically ${when}`,
|
|
1821
1936
|
notice: quotaPausedLine("Claude", "usage", claudeResetTime(when)) });
|
|
@@ -1948,87 +2063,9 @@ export class Daemon extends EventEmitter {
|
|
|
1948
2063
|
this.errorRecoveryDeadlineAt = 0;
|
|
1949
2064
|
this.activeErrorPatternKey = null;
|
|
1950
2065
|
}
|
|
1951
|
-
/** Lazy default also preserves bare-prototype diagnostic/test construction. */
|
|
1952
|
-
get paneHost() { return this.paneTransport ?? tmuxPaneHost; }
|
|
1953
|
-
nativePaneLifecycle;
|
|
1954
|
-
nativeFleetBridge;
|
|
1955
|
-
nativeTranscriptSource;
|
|
1956
|
-
usesNativePane() { return !!this.nativePaneLifecycle; }
|
|
1957
|
-
logNativeInputRefusal(details) {
|
|
1958
|
-
this.logger.warn({ component: "runtime-pane-host", reason: "expected_seq_changed", ...details }, "Native pane input refused before write");
|
|
1959
|
-
}
|
|
1960
|
-
logNativeToolFailure(details) {
|
|
1961
|
-
const turn = this.turnReplyGuard.snapshot();
|
|
1962
|
-
this.logger.warn({ ...details, currentMessageId: turn?.target.messageId ?? null,
|
|
1963
|
-
guardGeneration: turn?.generation ?? null, guardPhase: turn?.phase ?? null }, "Native pane tool operation failed");
|
|
1964
|
-
}
|
|
1965
|
-
nativePaneCurrent() { return this.nativePaneLifecycle?.current() ?? false; }
|
|
1966
|
-
nativeEnterHold = null;
|
|
1967
|
-
/** Per-launch attention for a native delivery whose bounded input attempts were refused or uncertain, including non-outbox input. */
|
|
1968
|
-
nativeDeliveryHold() {
|
|
1969
|
-
const hold = this.nativeEnterHold;
|
|
1970
|
-
return hold && this.nativePaneLifecycle && !this.deliveryWritesStopping && hold.spawn === this.spawnGeneration && hold.fence === this.launchFenceEpoch
|
|
1971
|
-
? { key: hold.key, since: hold.since } : null;
|
|
1972
|
-
}
|
|
1973
|
-
nativePaneHasExited() {
|
|
1974
|
-
const snapshot = this.nativePaneLifecycle?.snapshot?.();
|
|
1975
|
-
return snapshot?.state === "exited" && snapshot.instance === this.name && this.tmux?.getWindowId() === snapshot.paneId;
|
|
1976
|
-
}
|
|
1977
|
-
/** Positive native exit evidence only, fenced to this exact launch and Daemon owner. */
|
|
1978
|
-
nativePaneExitCurrent(snapshot, owner) {
|
|
1979
|
-
const actual = this.nativePaneLifecycle?.snapshot?.();
|
|
1980
|
-
return !!actual && actual.state === "exited" && snapshot.state === "exited"
|
|
1981
|
-
&& sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.startupAborted
|
|
1982
|
-
&& ["hostBootId", "paneId", "launchId", "agentId", "instance", "sessionId", "commandSequence", "exitCode"].every(key => actual[key] === snapshot[key])
|
|
1983
|
-
&& this.tmux?.getWindowId() === snapshot.paneId;
|
|
1984
|
-
}
|
|
1985
|
-
async reconcileNativeDeliveries(outbox, current) {
|
|
1986
|
-
if (!this.nativePaneLifecycle)
|
|
1987
|
-
return false;
|
|
1988
|
-
return reconcileNativePaneTarget(outbox, this.name, this.nativePaneLifecycle, current);
|
|
1989
|
-
}
|
|
1990
|
-
/** Same-process transport for the owned native daemon; never exposed as a runner RPC. */
|
|
1991
|
-
setNativeFleetBridge(bridge) {
|
|
1992
|
-
if (!this.nativePaneLifecycle || this.nativeFleetBridge?.current())
|
|
1993
|
-
throw Error("Native Fleet bridge requires one bound pane owner");
|
|
1994
|
-
this.nativeFleetBridge = bridge;
|
|
1995
|
-
}
|
|
1996
|
-
/** Fleet admission and generation checks happen before this synchronous handoff. */
|
|
1997
|
-
acceptNativeFleetMessage(message) {
|
|
1998
|
-
if (!this.nativePaneLifecycle || !this.nativeFleetBridge?.current() || this.deliveryWritesStopping || this.runtimeMonitorsFrozen)
|
|
1999
|
-
return false;
|
|
2000
|
-
if (typeof message.type === "string" && FLEET_RESPONSE_TYPES.has(message.type)) {
|
|
2001
|
-
this.routeFleetResponse(message);
|
|
2002
|
-
return true;
|
|
2003
|
-
}
|
|
2004
|
-
const bridge = this.nativeFleetBridge;
|
|
2005
|
-
const current = () => this.nativeFleetBridge === bridge && bridge.current() && !this.deliveryWritesStopping && !this.runtimeMonitorsFrozen;
|
|
2006
|
-
return this.handleFleetMessage(message, reply => { if (current())
|
|
2007
|
-
this.broadcastFleetMessage(reply); }, current);
|
|
2008
|
-
}
|
|
2009
|
-
broadcastFleetMessage(message) {
|
|
2010
|
-
if (this.nativePaneLifecycle) {
|
|
2011
|
-
if (this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen && !this.deliveryWritesStopping)
|
|
2012
|
-
this.nativeFleetBridge.send(message);
|
|
2013
|
-
}
|
|
2014
|
-
else
|
|
2015
|
-
this.ipcServer?.broadcast(message);
|
|
2016
|
-
}
|
|
2017
|
-
fleetTransportAvailable() {
|
|
2018
|
-
return this.nativePaneLifecycle ? !!this.nativeFleetBridge?.current() && !this.runtimeMonitorsFrozen : !!this.ipcServer;
|
|
2019
|
-
}
|
|
2020
|
-
/** Internal lifecycle injection, before start. Native paths and provider files stay on their runner. */
|
|
2021
|
-
setNativePaneLifecycle(native) {
|
|
2022
|
-
if (this.tmux || this.ipcServer || this.nativePaneLifecycle)
|
|
2023
|
-
throw Error("Native pane adapter must be bound before daemon startup");
|
|
2024
|
-
this.nativePaneLifecycle = native;
|
|
2025
|
-
this.backend = native.backend;
|
|
2026
|
-
this.paneTransport = native.host;
|
|
2027
|
-
this.controlClient = native.observation;
|
|
2028
|
-
}
|
|
2029
2066
|
constructor(name, config, instanceDir, topicMode = false, backend, controlClient, rootLogger, runtimeIdentity, spawnGate, stormWindow,
|
|
2030
2067
|
/** Fleet-level "is this CLI's backend down?" memory — see backend-outage.ts. */
|
|
2031
|
-
backendOutage
|
|
2068
|
+
backendOutage) {
|
|
2032
2069
|
super();
|
|
2033
2070
|
this.name = name;
|
|
2034
2071
|
this.config = config;
|
|
@@ -2040,7 +2077,6 @@ export class Daemon extends EventEmitter {
|
|
|
2040
2077
|
this.spawnGate = spawnGate;
|
|
2041
2078
|
this.stormWindow = stormWindow;
|
|
2042
2079
|
this.backendOutage = backendOutage;
|
|
2043
|
-
this.paneTransport = paneTransport;
|
|
2044
2080
|
// The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
|
|
2045
2081
|
// sharing that object would activate cold changes before the normal restart/apply boundary.
|
|
2046
2082
|
this.config = structuredClone(config);
|
|
@@ -2063,8 +2099,7 @@ export class Daemon extends EventEmitter {
|
|
|
2063
2099
|
this.messageBus.setLogger(this.logger);
|
|
2064
2100
|
// General is the dispatcher — it must stay warm to route messages, so it is
|
|
2065
2101
|
// never auto-paused regardless of auto_pause_after.
|
|
2066
|
-
|
|
2067
|
-
const isGeneral = config.general_topic === true || generalByName(name, config);
|
|
2102
|
+
const isGeneral = config.general_topic === true || name === "general";
|
|
2068
2103
|
const autoPauseMinutes = isGeneral ? 0 : (typeof config.auto_pause_after === "number" ? config.auto_pause_after : 0); // default: disabled
|
|
2069
2104
|
this.autoPauseController = new AutoPauseController(Math.max(0, autoPauseMinutes) * 60_000, readLastInboundAt(instanceDir) ?? Date.now());
|
|
2070
2105
|
// #1209: every real completion clears the in-flight marker, so a
|
|
@@ -2076,6 +2111,9 @@ export class Daemon extends EventEmitter {
|
|
|
2076
2111
|
this.publishWorkingState("turn_closed");
|
|
2077
2112
|
};
|
|
2078
2113
|
}
|
|
2114
|
+
/** Register each issued HTTP credential with the same-process lifecycle owner. */
|
|
2115
|
+
agentCredentialIssuer;
|
|
2116
|
+
setAgentCredentialIssuer(issue) { this.agentCredentialIssuer = issue; }
|
|
2079
2117
|
/** FleetManager and this daemon share a process, so durable begin/ACK is a direct synchronous store call. */
|
|
2080
2118
|
setDeliveryOutboxPort(port) {
|
|
2081
2119
|
this.deliveryOutbox = port;
|
|
@@ -2123,19 +2161,12 @@ export class Daemon extends EventEmitter {
|
|
|
2123
2161
|
return outcome === "begun";
|
|
2124
2162
|
}
|
|
2125
2163
|
async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
|
|
2126
|
-
let checkpoint = null
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
if (!runtimeTranscript || !this.nativePaneLifecycle.current(runtimeTranscript))
|
|
2130
|
-
throw Error("Native transcript checkpoint is not current; no delivery was written");
|
|
2164
|
+
let checkpoint = null;
|
|
2165
|
+
try {
|
|
2166
|
+
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2131
2167
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2135
|
-
}
|
|
2136
|
-
catch (err) {
|
|
2137
|
-
this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
|
|
2138
|
-
}
|
|
2168
|
+
catch (err) {
|
|
2169
|
+
this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
|
|
2139
2170
|
}
|
|
2140
2171
|
const submissionMode = rawPaste ? "raw_paste" : steer
|
|
2141
2172
|
? "steer"
|
|
@@ -2151,9 +2182,8 @@ export class Daemon extends EventEmitter {
|
|
|
2151
2182
|
backendVersion,
|
|
2152
2183
|
windowId: windowId ?? this.tmux?.getWindowId() ?? null,
|
|
2153
2184
|
transcriptPath: checkpoint?.path ?? null,
|
|
2154
|
-
transcriptOffset:
|
|
2155
|
-
transcriptSessionId:
|
|
2156
|
-
...(runtimeTranscript ? { runtimeTranscript } : {}),
|
|
2185
|
+
transcriptOffset: checkpoint?.offset ?? null,
|
|
2186
|
+
transcriptSessionId: checkpoint?.sessionId ?? null,
|
|
2157
2187
|
submissionMode,
|
|
2158
2188
|
queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
|
|
2159
2189
|
};
|
|
@@ -2196,25 +2226,17 @@ export class Daemon extends EventEmitter {
|
|
|
2196
2226
|
const outbox = this.deliveryOutbox;
|
|
2197
2227
|
if (!checkpoint || !outbox?.markConsumed)
|
|
2198
2228
|
return;
|
|
2199
|
-
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
2200
|
-
return;
|
|
2201
2229
|
if (verdict.submissionMode !== "steer" && verdict.submissionMode !== "native_queue_handoff")
|
|
2202
2230
|
return;
|
|
2203
2231
|
if (!["claude-code", "codex"].includes(checkpoint.backend))
|
|
2204
2232
|
return;
|
|
2205
2233
|
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, boot = this.bootId;
|
|
2206
|
-
let location;
|
|
2207
|
-
try {
|
|
2208
|
-
location = checkpoint.runtime ? { reader: this.nativeTranscriptReader(checkpoint.runtime, delivery.deliveryId) } : { path: checkpoint.path, offset: checkpoint.offset };
|
|
2209
|
-
}
|
|
2210
|
-
catch {
|
|
2211
|
-
return;
|
|
2212
|
-
} // The owner changed; an unavailable consumed watch proves nothing.
|
|
2213
2234
|
consumedWatches.start({
|
|
2214
2235
|
deliveryId: delivery.deliveryId,
|
|
2215
2236
|
attemptNo: delivery.attemptNo,
|
|
2216
2237
|
backend: checkpoint.backend,
|
|
2217
|
-
|
|
2238
|
+
path: checkpoint.path,
|
|
2239
|
+
offset: checkpoint.offset,
|
|
2218
2240
|
current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch,
|
|
2219
2241
|
consumed: (via, evidence) => {
|
|
2220
2242
|
try {
|
|
@@ -2228,6 +2250,243 @@ export class Daemon extends EventEmitter {
|
|
|
2228
2250
|
logger: this.logger,
|
|
2229
2251
|
});
|
|
2230
2252
|
}
|
|
2253
|
+
handoffStore() {
|
|
2254
|
+
return this.queuedHandoffStore ??= new QueuedHandoffStore(this.instanceDir);
|
|
2255
|
+
}
|
|
2256
|
+
/** #3030: the CLI that holds a queued hand-off; a record is settled only by a later one. */
|
|
2257
|
+
handoffHolder() { return `${this.bootId}:${this.spawnGeneration}`; }
|
|
2258
|
+
/**
|
|
2259
|
+
* #3030: before the paste. A hand-off into a busy Claude Code / Codex queue gets its record — the transcript
|
|
2260
|
+
* checkpoint before the paste, the unique leading marker and what a redelivery needs — or, for a redelivery written
|
|
2261
|
+
* as an ordinary idle submission, its record moves its checkpoint here, so a crash right after this paste is still
|
|
2262
|
+
* judged from the transcript that will show it. A write that did not keep a record runs as before.
|
|
2263
|
+
*/
|
|
2264
|
+
async recordQueuedHandoff(formatted, handingOff, verdict, durableAttempt, humanVerificationId) {
|
|
2265
|
+
const track = verdict.trackHandoff;
|
|
2266
|
+
if (!track)
|
|
2267
|
+
return;
|
|
2268
|
+
if (!handingOff && !track.id) {
|
|
2269
|
+
// An earlier attempt of this delivery was a hand-off; this one is an idle submission.
|
|
2270
|
+
if (verdict.handoff)
|
|
2271
|
+
this.dropQueuedHandoff(verdict);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const backend = this.config.backend ?? this.backend?.binaryName ?? "";
|
|
2275
|
+
if (!["claude-code", "codex"].includes(backend))
|
|
2276
|
+
return;
|
|
2277
|
+
const marker = durableAttempt ? `[agend-delivery-id:${durableAttempt.deliveryId}]`
|
|
2278
|
+
: humanVerificationId ? `[agend-human-id:${humanVerificationId}]` : null;
|
|
2279
|
+
if (!marker || !formatted.startsWith(marker))
|
|
2280
|
+
return;
|
|
2281
|
+
let checkpoint = verdict.transcriptCheckpoint ?? null;
|
|
2282
|
+
if (!checkpoint) {
|
|
2283
|
+
try {
|
|
2284
|
+
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2285
|
+
}
|
|
2286
|
+
catch (err) {
|
|
2287
|
+
this.logger.debug({ err }, "Could not checkpoint the transcript before a queued hand-off");
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (!checkpoint)
|
|
2291
|
+
return; // nothing could judge it later: it runs as before (✅ at the hand-off)
|
|
2292
|
+
const entry = {
|
|
2293
|
+
id: track.id ?? randomUUID(), marker, backend, path: checkpoint.path, offset: checkpoint.offset,
|
|
2294
|
+
content: track.content, meta: track.meta, holder: this.handoffHolder(), createdAt: Date.now(),
|
|
2295
|
+
redeliveries: track.redeliveries ?? 0,
|
|
2296
|
+
};
|
|
2297
|
+
try {
|
|
2298
|
+
const dropped = this.handoffStore().put(entry);
|
|
2299
|
+
if (handingOff)
|
|
2300
|
+
verdict.handoff = entry;
|
|
2301
|
+
for (const old of dropped)
|
|
2302
|
+
this.logger.warn({ handoff: old.id, createdAt: old.createdAt }, "Queued hand-off record dropped: too many on record");
|
|
2303
|
+
}
|
|
2304
|
+
catch (err) {
|
|
2305
|
+
this.logger.warn({ err }, "Could not record a queued hand-off; it runs as before (✅ at the hand-off)");
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* #3030: after pushChannelMessage's write. A hand-off on record whose paste went out is watched (✅ when a turn
|
|
2310
|
+
* takes it); one that was never written goes. A redelivery that ended as an ordinary submission is done; one that a
|
|
2311
|
+
* cancel dropped goes too (a cancel is the person's); one stopped before its write stays for the next CLI.
|
|
2312
|
+
*/
|
|
2313
|
+
afterTrackedWrite(meta, verdict, sent, deliveryEpoch) {
|
|
2314
|
+
try {
|
|
2315
|
+
// A stale epoch with writes still allowed is a cancel (the person's); a stop fences writes too, and keeps records.
|
|
2316
|
+
const cancelled = !this.isDeliveryEpochCurrent(deliveryEpoch) && !this.deliveryWritesStopping;
|
|
2317
|
+
if (verdict.handoff) {
|
|
2318
|
+
if (sent || verdict.paneWriteStarted)
|
|
2319
|
+
this.watchQueuedHandoff(verdict.handoff);
|
|
2320
|
+
else if (!verdict.trackHandoff?.id || cancelled)
|
|
2321
|
+
this.dropQueuedHandoff(verdict);
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
const id = meta.handoff_redelivery;
|
|
2325
|
+
if (!id)
|
|
2326
|
+
return;
|
|
2327
|
+
if (sent || verdict.paneWriteStarted || cancelled) {
|
|
2328
|
+
this.handoffStore().remove(id);
|
|
2329
|
+
this.logger.info({ handoff: id, sent, proof: verdict.proof ?? null }, "Queued hand-off redelivered as an ordinary submission");
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
catch (err) {
|
|
2333
|
+
this.logger.warn({ err }, "Could not update the queued hand-off record after its write");
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
redeliveryStillOurs(meta) {
|
|
2337
|
+
const holder = this.handoffHolder();
|
|
2338
|
+
if (meta.handoff_holder !== holder)
|
|
2339
|
+
return false;
|
|
2340
|
+
try {
|
|
2341
|
+
return this.handoffStore().get(meta.handoff_redelivery)?.holder === holder;
|
|
2342
|
+
}
|
|
2343
|
+
catch {
|
|
2344
|
+
return false;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
dropQueuedHandoff(verdict) {
|
|
2348
|
+
const entry = verdict.handoff;
|
|
2349
|
+
verdict.handoff = undefined;
|
|
2350
|
+
if (!entry)
|
|
2351
|
+
return;
|
|
2352
|
+
try {
|
|
2353
|
+
this.handoffStore().remove(entry.id);
|
|
2354
|
+
}
|
|
2355
|
+
catch (err) {
|
|
2356
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a queued hand-off record");
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* #3030: the hand-off's CLI is the current one: watch its transcript for the turn that takes it (✅ then), and for an
|
|
2361
|
+
* idle CLI that never did (reported). A stop ends the watch; the record stays for the next CLI to settle.
|
|
2362
|
+
*/
|
|
2363
|
+
watchQueuedHandoff(entry) {
|
|
2364
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2365
|
+
const started = consumedWatches.start({
|
|
2366
|
+
deliveryId: `handoff:${entry.id}`,
|
|
2367
|
+
attemptNo: entry.redeliveries,
|
|
2368
|
+
backend: entry.backend,
|
|
2369
|
+
path: entry.path,
|
|
2370
|
+
offset: entry.offset,
|
|
2371
|
+
marker: entry.marker,
|
|
2372
|
+
// A cancel or stop moves the epoch: the watch ends quietly (a cancel cleared the record, a stop leaves it).
|
|
2373
|
+
current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch
|
|
2374
|
+
&& epoch === this.deliveryEpoch,
|
|
2375
|
+
consumed: (via) => this.settleQueuedHandoff(entry, "taken", `transcript:${via}`),
|
|
2376
|
+
idleForMs: () => this.instanceIdleSinceMono === null ? null : performance.now() - this.instanceIdleSinceMono,
|
|
2377
|
+
notTaken: () => this.settleQueuedHandoff(entry, "not-taken", "transcript:absent-while-idle"),
|
|
2378
|
+
// A queued message waits for the running turn, however long that is.
|
|
2379
|
+
lifetimeMs: QUEUED_HANDOFF_WATCH_LIFETIME_MS,
|
|
2380
|
+
logger: this.logger,
|
|
2381
|
+
});
|
|
2382
|
+
if (!started) {
|
|
2383
|
+
// No watch (the fleet's cap): ✅ as before; the record still lets a restart judge it.
|
|
2384
|
+
const status = channelStatus(entry.meta);
|
|
2385
|
+
if (status)
|
|
2386
|
+
this.emit("message_confirmed", status);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
/** #3030: a record's end — taken (✅), or never taken / not judgeable (❌, told once per batch). */
|
|
2390
|
+
settleQueuedHandoff(entry, outcome, proof) {
|
|
2391
|
+
try {
|
|
2392
|
+
this.handoffStore().remove(entry.id);
|
|
2393
|
+
}
|
|
2394
|
+
catch (err) {
|
|
2395
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a settled queued hand-off record");
|
|
2396
|
+
}
|
|
2397
|
+
const meta = entry.meta;
|
|
2398
|
+
const status = channelStatus(meta);
|
|
2399
|
+
if (outcome === "taken") {
|
|
2400
|
+
this.logger.info({ handoff: entry.id, proof, redeliveries: entry.redeliveries }, "Queued hand-off taken by the CLI");
|
|
2401
|
+
if (status)
|
|
2402
|
+
this.emit("message_confirmed", status);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
this.logger.warn({ handoff: entry.id, outcome, proof, redeliveries: entry.redeliveries }, "Queued hand-off was not taken by the CLI");
|
|
2406
|
+
if (meta.from_instance) {
|
|
2407
|
+
this.reportCrossInstanceDeliveryFailure(meta, { reached: true, phase: "handoff-not-consumed", proof: outcome === "not-taken" ? "not-taken" : "unverifiable" });
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
if (!status)
|
|
2411
|
+
return;
|
|
2412
|
+
this.unsettledHandoffs[outcome].push({ chatId: status.chatId, messageId: status.messageId, ...(status.threadId ? { threadId: status.threadId } : {}) });
|
|
2413
|
+
if (this.unsettledHandoffsTimer)
|
|
2414
|
+
clearTimeout(this.unsettledHandoffsTimer);
|
|
2415
|
+
this.unsettledHandoffsTimer = setTimeout(() => {
|
|
2416
|
+
this.unsettledHandoffsTimer = null;
|
|
2417
|
+
for (const reason of ["not-taken", "unverifiable"]) {
|
|
2418
|
+
const messages = this.unsettledHandoffs[reason].splice(0);
|
|
2419
|
+
if (messages.length)
|
|
2420
|
+
this.emit("human_handoffs_unsettled", { reason, messages });
|
|
2421
|
+
}
|
|
2422
|
+
}, CANCELLED_HUMANS_NOTICE_MS);
|
|
2423
|
+
this.unsettledHandoffsTimer.unref?.();
|
|
2424
|
+
}
|
|
2425
|
+
/**
|
|
2426
|
+
* #3030: right after a CLI came up — the records left by CLIs that are gone. Their queue went with them, so each is
|
|
2427
|
+
* judged by its own transcript from its checkpoint: taken there (✅, nothing pasted); proven not taken (the whole
|
|
2428
|
+
* delta read, the marker absent or only enqueued) → redelivered once, with the same marker, through the ordinary
|
|
2429
|
+
* queue; anything that cannot be judged → never pasted, reported. Ownership is evidence: never pasted twice.
|
|
2430
|
+
*/
|
|
2431
|
+
async reconcileQueuedHandoffs() {
|
|
2432
|
+
let entries;
|
|
2433
|
+
try {
|
|
2434
|
+
entries = this.handoffStore().list().filter(e => e.holder !== this.handoffHolder());
|
|
2435
|
+
}
|
|
2436
|
+
catch (err) {
|
|
2437
|
+
this.logger.warn({ err }, "Could not read the queued hand-off records");
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
if (!entries.length)
|
|
2441
|
+
return;
|
|
2442
|
+
// Captured before any await: a reconciliation that outlives its CLI can never claim the next CLI's holder.
|
|
2443
|
+
const holder = this.handoffHolder();
|
|
2444
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2445
|
+
const fresh = () => spawn === this.spawnGeneration && fence === this.launchFenceEpoch && epoch === this.deliveryEpoch
|
|
2446
|
+
&& !this.deliveryWritesStopping;
|
|
2447
|
+
let redelivered = 0, taken = 0, unverifiable = 0;
|
|
2448
|
+
for (const entry of entries) {
|
|
2449
|
+
const kind = await scanTranscriptForDeliveryMarker(entry.path, entry.offset, entry.backend, { marker: entry.marker });
|
|
2450
|
+
// This CLI is gone already, or a cancel came: the next CLI settles them (a cancel cleared them).
|
|
2451
|
+
if (!fresh())
|
|
2452
|
+
return;
|
|
2453
|
+
// …and the record is still the one that was read (another reconciliation or a cancel did not change it meanwhile).
|
|
2454
|
+
let current;
|
|
2455
|
+
try {
|
|
2456
|
+
current = this.handoffStore().get(entry.id);
|
|
2457
|
+
}
|
|
2458
|
+
catch {
|
|
2459
|
+
current = undefined;
|
|
2460
|
+
}
|
|
2461
|
+
if (!current || current.holder !== entry.holder || current.redeliveries !== entry.redeliveries)
|
|
2462
|
+
continue;
|
|
2463
|
+
if (markerConsumed(kind)) {
|
|
2464
|
+
taken++;
|
|
2465
|
+
this.settleQueuedHandoff(entry, "taken", `restart-transcript:${kind}`);
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (kind === "unavailable" || entry.redeliveries >= QUEUED_HANDOFF_MAX_REDELIVERIES) {
|
|
2469
|
+
unverifiable++;
|
|
2470
|
+
this.settleQueuedHandoff(entry, "unverifiable", kind === "unavailable" ? "restart-transcript:unavailable" : "redelivery-limit");
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
2473
|
+
// Proven not taken: the CLI that queued it is gone and its transcript, read to the end, never took it.
|
|
2474
|
+
const next = { ...entry, holder, redeliveries: entry.redeliveries + 1 };
|
|
2475
|
+
try {
|
|
2476
|
+
this.handoffStore().put(next);
|
|
2477
|
+
}
|
|
2478
|
+
catch (err) {
|
|
2479
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not update a queued hand-off record; not redelivered now");
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
redelivered++;
|
|
2483
|
+
// The person's receipt was settled by the first write: the redelivery carries its marker, not its receipt binding.
|
|
2484
|
+
const { human_receipt_id: _r, human_owner_key: _o, human_daemon_owner: _d, human_wire_sha256: _w, ...meta } = entry.meta;
|
|
2485
|
+
this.pushChannelMessage(entry.content, { ...meta, handoff_redelivery: entry.id,
|
|
2486
|
+
handoff_redeliveries: String(next.redeliveries), handoff_holder: next.holder }, undefined, epoch);
|
|
2487
|
+
}
|
|
2488
|
+
this.logger.info({ instance: this.name, redelivered, taken, unverifiable, proof: "transcript" }, "Queued hand-offs left by the previous CLI settled");
|
|
2489
|
+
}
|
|
2231
2490
|
finishDurableSubmission(delivery, verdict) {
|
|
2232
2491
|
// Legacy backends may accept a pane write without enough pane evidence to
|
|
2233
2492
|
// claim submission. Keep that row inspectable instead of recording a false
|
|
@@ -2295,34 +2554,24 @@ export class Daemon extends EventEmitter {
|
|
|
2295
2554
|
const spawn = this.spawnGeneration;
|
|
2296
2555
|
const fence = this.launchFenceEpoch;
|
|
2297
2556
|
let last = "unavailable";
|
|
2298
|
-
|
|
2299
|
-
let
|
|
2300
|
-
let evidence = native ? "runtime-transcript-unconfirmed" : OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
2557
|
+
let outcome = "delivered";
|
|
2558
|
+
let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
2301
2559
|
// #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
|
|
2302
2560
|
// in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
|
|
2303
2561
|
const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
|
|
2304
2562
|
let found = false;
|
|
2305
2563
|
try {
|
|
2306
|
-
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
2307
|
-
throw Error("Native proof cannot read a coordinator-local transcript path");
|
|
2308
|
-
const nativeReader = checkpoint.runtime ? this.nativeTranscriptReader(checkpoint.runtime, needle) : null;
|
|
2309
2564
|
for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
|
|
2310
2565
|
if (this.deliveryWritesStopping)
|
|
2311
2566
|
return; // reconciliation owns the row now
|
|
2312
|
-
const look =
|
|
2313
|
-
if (this.deliveryWritesStopping)
|
|
2314
|
-
return;
|
|
2315
|
-
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
2316
|
-
last = "unavailable";
|
|
2317
|
-
break;
|
|
2318
|
-
}
|
|
2567
|
+
const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
2319
2568
|
if (look === "user" || look === "absorbed" || look === "queued") {
|
|
2320
2569
|
found = true;
|
|
2321
2570
|
outcome = "delivered";
|
|
2322
2571
|
evidence = transcriptMarkerEvidence(look, !!rawPaste);
|
|
2323
2572
|
break;
|
|
2324
2573
|
}
|
|
2325
|
-
last = look
|
|
2574
|
+
last = look;
|
|
2326
2575
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
2327
2576
|
last = "unavailable";
|
|
2328
2577
|
break;
|
|
@@ -2334,13 +2583,13 @@ export class Daemon extends EventEmitter {
|
|
|
2334
2583
|
// `uncertain`: re-check the same fixed epochs after the last await, before settling (#1207 P2).
|
|
2335
2584
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch)
|
|
2336
2585
|
last = "unavailable";
|
|
2337
|
-
if (last === "no-match" &&
|
|
2586
|
+
if (last === "no-match" && evidence === OUTPUT_EDGE_ONLY_EVIDENCE) {
|
|
2338
2587
|
outcome = "uncertain";
|
|
2339
2588
|
evidence = "unverifiable-no-transcript-marker";
|
|
2340
2589
|
}
|
|
2341
2590
|
}
|
|
2342
2591
|
catch (err) {
|
|
2343
|
-
this.logger.debug({ err }, "Transcript proof
|
|
2592
|
+
this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
|
|
2344
2593
|
}
|
|
2345
2594
|
if (rawPaste && !found) {
|
|
2346
2595
|
outcome = "uncertain";
|
|
@@ -2527,130 +2776,128 @@ export class Daemon extends EventEmitter {
|
|
|
2527
2776
|
}
|
|
2528
2777
|
}
|
|
2529
2778
|
catch { /* corrupt/missing — ignore */ }
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
sessions.push(sessionName);
|
|
2571
|
-
}
|
|
2779
|
+
// 1. IPC server — bridge between MCP server (Claude's child) and daemon
|
|
2780
|
+
const sockPath = join(this.instanceDir, "channel.sock");
|
|
2781
|
+
this.ipcServer = new IpcServer(sockPath, this.logger);
|
|
2782
|
+
// Forward IPC server errors as daemon events (prevents unhandled 'error' crash).
|
|
2783
|
+
// Guard: only forward post-listen errors — startup errors are handled by listen() rejection.
|
|
2784
|
+
let ipcListening = false;
|
|
2785
|
+
this.ipcServer.on("error", (err) => {
|
|
2786
|
+
if (!ipcListening)
|
|
2787
|
+
return; // startup errors handled by listen() rejection
|
|
2788
|
+
this.logger.error({ err, name: this.name }, "IPC server error");
|
|
2789
|
+
this.emit("error", err);
|
|
2790
|
+
});
|
|
2791
|
+
await this.ipcServer.listen();
|
|
2792
|
+
this.startupAdmission?.();
|
|
2793
|
+
ipcListening = true;
|
|
2794
|
+
// Permanent IPC dispatcher: routes responses to pending requests by type+id key
|
|
2795
|
+
this.ipcServer.on("message", (msg) => this.routeFleetResponse(msg));
|
|
2796
|
+
// IPC message relay: when daemon wants to push a channel message to Claude,
|
|
2797
|
+
// it broadcasts to all IPC clients (the MCP server is one of them).
|
|
2798
|
+
// When MCP server sends a tool_call, daemon handles it via the messageBus.
|
|
2799
|
+
this.ipcServer.on("message", (msg, socket) => {
|
|
2800
|
+
if (msg.type === "tool_call") {
|
|
2801
|
+
// MCP server forwarding a Claude tool call (reply, react, edit, download)
|
|
2802
|
+
this.handleToolCall(msg, socket);
|
|
2803
|
+
}
|
|
2804
|
+
else if (msg.type === "mcp_ready") {
|
|
2805
|
+
this.handleMcpReady(msg, socket);
|
|
2806
|
+
}
|
|
2807
|
+
else if (msg.type === "pong") {
|
|
2808
|
+
this.handleMcpPong(msg, socket);
|
|
2809
|
+
}
|
|
2810
|
+
else if (msg.type === "query_sessions") {
|
|
2811
|
+
// Fleet manager asks for all registered session names (catches sessions
|
|
2812
|
+
// that sent mcp_ready before fleet manager connected).
|
|
2813
|
+
const sessions = [];
|
|
2814
|
+
for (const [s, sessionName] of this.socketSessionNames) {
|
|
2815
|
+
if (!s.destroyed && sessionName !== this.name) {
|
|
2816
|
+
// Individual mcp_ready for initial registration path
|
|
2817
|
+
this.ipcServer?.send(socket, { type: "mcp_ready", sessionName });
|
|
2818
|
+
sessions.push(sessionName);
|
|
2572
2819
|
}
|
|
2573
|
-
// Batch response for prune path
|
|
2574
|
-
this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
|
|
2575
2820
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
this.
|
|
2586
|
-
|
|
2587
|
-
|
|
2821
|
+
// Batch response for prune path
|
|
2822
|
+
this.ipcServer?.send(socket, { type: "query_sessions_response", sessions });
|
|
2823
|
+
}
|
|
2824
|
+
else if (msg.type === "fleet_inbound") {
|
|
2825
|
+
// Fleet manager routed a message to us (topic mode)
|
|
2826
|
+
const meta = msg.meta;
|
|
2827
|
+
const targetSession = msg.targetSession;
|
|
2828
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
2829
|
+
void this.wake().then(() => {
|
|
2830
|
+
this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
|
|
2831
|
+
}).catch(err => {
|
|
2832
|
+
this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
|
|
2833
|
+
this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
|
|
2588
2834
|
});
|
|
2589
|
-
this.tmux = acquired.pane;
|
|
2590
|
-
this.nativeTranscriptSource = acquired.source;
|
|
2591
|
-
nativeDisposition = acquired.disposition;
|
|
2592
|
-
this.lastSpawnAt = Date.now();
|
|
2593
|
-
this.setProcessStatus("running");
|
|
2594
|
-
this.bindInstanceStateOutputListener(this.tmux.getWindowId());
|
|
2595
|
-
this.saveSessionId();
|
|
2596
|
-
// New/resumed native processes can still be painting their first frame. Keep deliveries behind the same
|
|
2597
|
-
// bounded dialog/readiness scan as local startup. An already-running reattachment may be busy: do not
|
|
2598
|
-
// wait for that work to finish or inject a startup key into it.
|
|
2599
|
-
if (acquired.disposition !== "reattached") {
|
|
2600
|
-
if (!await this.finishStartupScan(120_000))
|
|
2601
|
-
throw Error("Native CLI exited during startup; retained session was not replaced");
|
|
2602
|
-
this.startupAdmission?.();
|
|
2603
|
-
if (this.startupAborted || this.deliveryWritesStopping || !this.nativePaneLifecycle.current())
|
|
2604
|
-
throw Error("Native launch was superseded");
|
|
2605
|
-
}
|
|
2606
2835
|
}
|
|
2607
|
-
|
|
2608
|
-
this.
|
|
2836
|
+
else if (msg.type === "raw_paste") {
|
|
2837
|
+
this.handleRawPasteIpcMessage(msg);
|
|
2609
2838
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
// 2. Tmux — ensure session, create window if not alive
|
|
2613
|
-
await this.paneHost.ensureSession(this.tmuxSessionName);
|
|
2614
|
-
this.startupAdmission?.();
|
|
2615
|
-
this.tmux = this.paneHost.bind(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
2616
|
-
// Strategy A: always start fresh Claude window (MCP server has no reconnection)
|
|
2617
|
-
// Kill any existing window from previous run
|
|
2618
|
-
const windowIdFile = join(this.instanceDir, "window-id");
|
|
2619
|
-
if (existsSync(windowIdFile)) {
|
|
2620
|
-
const savedId = readFileSync(windowIdFile, "utf-8").trim();
|
|
2621
|
-
if (savedId) {
|
|
2622
|
-
const oldTmux = this.paneHost.bind(this.tmuxSessionName, savedId);
|
|
2623
|
-
if (await oldTmux.isWindowAlive()) {
|
|
2624
|
-
this.startupAdmission?.();
|
|
2625
|
-
await this.checkpointSessionId();
|
|
2626
|
-
this.startupAdmission?.();
|
|
2627
|
-
await oldTmux.killWindow();
|
|
2628
|
-
this.startupAdmission?.();
|
|
2629
|
-
this.logger.info({ savedId }, "Killed old tmux window for fresh start");
|
|
2630
|
-
}
|
|
2631
|
-
}
|
|
2839
|
+
else if (msg.type === "config_update") {
|
|
2840
|
+
this.applyConfigUpdate(msg.config);
|
|
2632
2841
|
}
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
if (!resumed) {
|
|
2637
|
-
await this.injectSnapshotMessage(this.startupAdmission);
|
|
2638
|
-
this.startupAdmission?.();
|
|
2842
|
+
else if (msg.type === "steer") {
|
|
2843
|
+
const meta = (msg.meta ?? {});
|
|
2844
|
+
this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
2639
2845
|
}
|
|
2640
|
-
else {
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2846
|
+
else if (msg.type === "btw") {
|
|
2847
|
+
const meta = (msg.meta ?? {});
|
|
2848
|
+
this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
2849
|
+
}
|
|
2850
|
+
else if (msg.type === "fleet_schedule_trigger") {
|
|
2851
|
+
const payload = msg.payload;
|
|
2852
|
+
const meta = msg.meta;
|
|
2853
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
2854
|
+
void this.wake().then(() => this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch)).catch(err => {
|
|
2855
|
+
this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
else if (msg.type === "query_instance_state") {
|
|
2859
|
+
void this.respondToInstanceStateQuery(msg, socket);
|
|
2860
|
+
}
|
|
2861
|
+
});
|
|
2862
|
+
// 2. Tmux — ensure session, create window if not alive
|
|
2863
|
+
await TmuxManager.ensureSession(this.tmuxSessionName);
|
|
2864
|
+
this.startupAdmission?.();
|
|
2865
|
+
this.tmux = new TmuxManager(this.tmuxSessionName, "", resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
2866
|
+
// Strategy A: always start fresh Claude window (MCP server has no reconnection)
|
|
2867
|
+
// Kill any existing window from previous run
|
|
2868
|
+
const windowIdFile = join(this.instanceDir, "window-id");
|
|
2869
|
+
if (existsSync(windowIdFile)) {
|
|
2870
|
+
const savedId = readFileSync(windowIdFile, "utf-8").trim();
|
|
2871
|
+
if (savedId) {
|
|
2872
|
+
const oldTmux = new TmuxManager(this.tmuxSessionName, savedId);
|
|
2873
|
+
if (await oldTmux.isWindowAlive()) {
|
|
2874
|
+
this.startupAdmission?.();
|
|
2875
|
+
await this.checkpointSessionId();
|
|
2876
|
+
this.startupAdmission?.();
|
|
2877
|
+
await oldTmux.killWindow();
|
|
2878
|
+
this.startupAdmission?.();
|
|
2879
|
+
this.logger.info({ savedId }, "Killed old tmux window for fresh start");
|
|
2644
2880
|
}
|
|
2645
|
-
catch { /* may not exist */ }
|
|
2646
2881
|
}
|
|
2647
2882
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2883
|
+
const resumed = await this.spawnClaudeWindow();
|
|
2884
|
+
this.startupAdmission?.();
|
|
2885
|
+
this.isNewSession = !resumed;
|
|
2886
|
+
if (!resumed) {
|
|
2887
|
+
await this.injectSnapshotMessage(this.startupAdmission);
|
|
2888
|
+
this.startupAdmission?.();
|
|
2889
|
+
}
|
|
2890
|
+
else {
|
|
2891
|
+
// Clean up stale snapshot file — resume restored full context, snapshot not needed
|
|
2892
|
+
try {
|
|
2893
|
+
unlinkSync(join(this.instanceDir, "rotation-state.json"));
|
|
2894
|
+
}
|
|
2895
|
+
catch { /* may not exist */ }
|
|
2896
|
+
}
|
|
2650
2897
|
// #1209: conditional continuation for the turn interrupted by the last
|
|
2651
2898
|
// restart, if the gates allow. Detached; the gate
|
|
2652
2899
|
// consumes its one-shot marker first and never throws into boot.
|
|
2653
|
-
const legacyResume = this.
|
|
2900
|
+
const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2654
2901
|
const recoveryOwner = JSON.stringify(this.interactionOwner());
|
|
2655
2902
|
void Promise.allSettled([legacyResume]).then(() => {
|
|
2656
2903
|
if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
|
|
@@ -2669,7 +2916,7 @@ export class Daemon extends EventEmitter {
|
|
|
2669
2916
|
// (statusline transcript); codex/kiro/opencode read their CLI's own
|
|
2670
2917
|
// conversation store via a pluggable source. Backends with no known
|
|
2671
2918
|
// source stay inert exactly as before.
|
|
2672
|
-
this.transcriptMonitor = this.buildTranscriptMonitor(
|
|
2919
|
+
this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
|
|
2673
2920
|
const transcriptMonitor = this.transcriptMonitor;
|
|
2674
2921
|
const transcriptFence = this.launchFenceEpoch;
|
|
2675
2922
|
// Baseline in the isolate before this daemon accepts new work. A
|
|
@@ -2705,7 +2952,7 @@ export class Daemon extends EventEmitter {
|
|
|
2705
2952
|
this.publishActivity(null);
|
|
2706
2953
|
});
|
|
2707
2954
|
this.transcriptMonitor.on("api_error", (text, status, _at, errorKind) => this.noteClaudeApiError(text, status, errorKind));
|
|
2708
|
-
this.transcriptMonitor.on("system_informational", (text) => this.noteClaudeInformational(text));
|
|
2955
|
+
this.transcriptMonitor.on("system_informational", (text, at) => this.noteClaudeInformational(text, at));
|
|
2709
2956
|
this.transcriptMonitor.on("user_prompt", () => this.noteClaudePrompt());
|
|
2710
2957
|
this.transcriptMonitor.on("assistant_text", (text) => {
|
|
2711
2958
|
this.noteClaudeOutput();
|
|
@@ -2725,15 +2972,13 @@ export class Daemon extends EventEmitter {
|
|
|
2725
2972
|
if (hangConfig?.enabled !== false) {
|
|
2726
2973
|
this.hangDetector = new HangDetector();
|
|
2727
2974
|
}
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
this.
|
|
2734
|
-
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2975
|
+
// 8. Context guardian
|
|
2976
|
+
const statusFile = join(this.instanceDir, "statusline.json");
|
|
2977
|
+
this.guardian = new ContextGuardian(this.config.context_guardian ?? {}, this.logger, statusFile);
|
|
2978
|
+
this.guardian.startWatching();
|
|
2979
|
+
this.guardian.on("status_update", () => {
|
|
2980
|
+
this.saveSessionId();
|
|
2981
|
+
});
|
|
2737
2982
|
// Context rotation removed: all CLI backends have built-in auto-compact.
|
|
2738
2983
|
// Crash recovery (health check + respawn with snapshot) is retained below.
|
|
2739
2984
|
}
|
|
@@ -2750,6 +2995,8 @@ export class Daemon extends EventEmitter {
|
|
|
2750
2995
|
this.startErrorMonitor();
|
|
2751
2996
|
}
|
|
2752
2997
|
this.startInstanceStateMonitor();
|
|
2998
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
2999
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
2753
3000
|
this.logger.info(`${this.name} ready`);
|
|
2754
3001
|
}
|
|
2755
3002
|
/**
|
|
@@ -2784,7 +3031,7 @@ export class Daemon extends EventEmitter {
|
|
|
2784
3031
|
this.socketPids.delete(socket);
|
|
2785
3032
|
// Notify fleet manager so it can clean up sessionRegistry
|
|
2786
3033
|
if (sessionName !== this.name) {
|
|
2787
|
-
this.
|
|
3034
|
+
this.ipcServer?.broadcast({ type: "session_disconnected", sessionName });
|
|
2788
3035
|
}
|
|
2789
3036
|
});
|
|
2790
3037
|
}
|
|
@@ -2800,16 +3047,11 @@ export class Daemon extends EventEmitter {
|
|
|
2800
3047
|
// sub-agent's server may have left a dead pid in the slot).
|
|
2801
3048
|
this.noteMcpProofOfLife("mcp_ready", typeof msg.pid === "number" ? msg.pid : undefined);
|
|
2802
3049
|
}
|
|
2803
|
-
this.
|
|
3050
|
+
this.ipcServer?.broadcast({ type: "mcp_ready", sessionName });
|
|
2804
3051
|
}
|
|
2805
3052
|
checkMcpServerAlive() {
|
|
2806
3053
|
if (this.isPaused)
|
|
2807
3054
|
return;
|
|
2808
|
-
if (this.nativePaneLifecycle) {
|
|
2809
|
-
if (this.nativePaneLifecycle.mcpProof() === "alive")
|
|
2810
|
-
this.noteMcpProofOfLife("connection");
|
|
2811
|
-
return; // No native PID/socket guessing or restart from an unknown helper observation.
|
|
2812
|
-
}
|
|
2813
3055
|
const status = mcpServerState(this.instanceDir);
|
|
2814
3056
|
if (status.state === "alive") {
|
|
2815
3057
|
this.mcpServedGeneration = this.spawnGeneration;
|
|
@@ -2893,8 +3135,6 @@ export class Daemon extends EventEmitter {
|
|
|
2893
3135
|
* server from a third-party one, and failed the whole launch for either.
|
|
2894
3136
|
*/
|
|
2895
3137
|
checkFleetMcpNeverConnected() {
|
|
2896
|
-
if (this.nativePaneLifecycle)
|
|
2897
|
-
return; // Native helper presence owns this observation.
|
|
2898
3138
|
if (this.config.agent_mode === "cli")
|
|
2899
3139
|
return; // no MCP server by design
|
|
2900
3140
|
if (this.lastSpawnAt === 0 || this.mcpServedGeneration === this.spawnGeneration)
|
|
@@ -3105,8 +3345,6 @@ export class Daemon extends EventEmitter {
|
|
|
3105
3345
|
* proof that tools are being served, whatever channel.mcp.pid says.
|
|
3106
3346
|
*/
|
|
3107
3347
|
mcpServerAlive() {
|
|
3108
|
-
if (this.nativePaneLifecycle)
|
|
3109
|
-
return this.nativePaneLifecycle.mcpProof() === "alive" ? { alive: true, source: "connection" } : { alive: false, source: "none" };
|
|
3110
3348
|
const status = mcpServerState(this.instanceDir);
|
|
3111
3349
|
if (status.state === "alive")
|
|
3112
3350
|
return { alive: true, source: "pid", pid: status.pid };
|
|
@@ -3171,7 +3409,7 @@ export class Daemon extends EventEmitter {
|
|
|
3171
3409
|
this.mcpDeathNotifiedForPid = null;
|
|
3172
3410
|
this.mcpDeathDeferredForPid = null;
|
|
3173
3411
|
this.clearMcpRestartRequest();
|
|
3174
|
-
if (
|
|
3412
|
+
if (pid && mcpServerState(this.instanceDir).state !== "alive")
|
|
3175
3413
|
this.repairMcpPidSlot(pid, source);
|
|
3176
3414
|
}
|
|
3177
3415
|
emitMcpRecovered(source, pid) {
|
|
@@ -3218,26 +3456,6 @@ export class Daemon extends EventEmitter {
|
|
|
3218
3456
|
// ended this instance's health loop because scheduleNext() sat after the
|
|
3219
3457
|
// throwing code. Catch, log, and keep checking.
|
|
3220
3458
|
try {
|
|
3221
|
-
if (this.nativePaneLifecycle) {
|
|
3222
|
-
if (this.healthCheckPaused) {
|
|
3223
|
-
scheduleNext();
|
|
3224
|
-
return;
|
|
3225
|
-
}
|
|
3226
|
-
const native = this.nativePaneLifecycle, pane = this.tmux;
|
|
3227
|
-
const status = await pane?.getPaneStatus();
|
|
3228
|
-
if (this.runtimeMonitorsFrozen || native !== this.nativePaneLifecycle || pane !== this.tmux)
|
|
3229
|
-
return;
|
|
3230
|
-
const snapshot = native.snapshot?.();
|
|
3231
|
-
if (status?.alive === false && snapshot?.state === "exited") {
|
|
3232
|
-
this.healthCheckPaused = true;
|
|
3233
|
-
this.setProcessStatus(snapshot.exitCode === 0 ? "stopped" : "crashed");
|
|
3234
|
-
this.emit("native_pane_exited", { name: this.name, snapshot, owner: this.interactionOwner() });
|
|
3235
|
-
}
|
|
3236
|
-
else if (status?.alive)
|
|
3237
|
-
this.checkMcpServerAlive();
|
|
3238
|
-
scheduleNext();
|
|
3239
|
-
return;
|
|
3240
|
-
}
|
|
3241
3459
|
// Instance directory removed externally (e.g. `rm -rf ~/.agend/instances/<name>`).
|
|
3242
3460
|
// Stop the loop permanently — otherwise every tick triggers a respawn, whose
|
|
3243
3461
|
// writeRotationSnapshot fails with ENOENT and gets caught as "Failed to respawn",
|
|
@@ -3253,8 +3471,6 @@ export class Daemon extends EventEmitter {
|
|
|
3253
3471
|
scheduleNext();
|
|
3254
3472
|
return;
|
|
3255
3473
|
}
|
|
3256
|
-
// The CLI owns the MCP server process, so the daemon can only observe it.
|
|
3257
|
-
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3258
3474
|
// Human-readable backend label for logs (e.g. "claude", "kiro-cli")
|
|
3259
3475
|
const cliLabel = this.backend?.binaryName ?? "CLI";
|
|
3260
3476
|
const healthTmux = this.tmux;
|
|
@@ -3275,6 +3491,9 @@ export class Daemon extends EventEmitter {
|
|
|
3275
3491
|
if (discardStaleHealth())
|
|
3276
3492
|
return;
|
|
3277
3493
|
if (paneStatus?.alive) {
|
|
3494
|
+
// A CLI shutdown also closes its MCP children. Revive tools only for
|
|
3495
|
+
// a positively live pane, never while its exit is being classified.
|
|
3496
|
+
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3278
3497
|
// Capture only a known primary while it lives. No extra tmux call: PID comes
|
|
3279
3498
|
// from this same status read. Fast/unobserved deaths remain ordinary crashes.
|
|
3280
3499
|
const key = this.oomSessionKey();
|
|
@@ -3321,6 +3540,29 @@ export class Daemon extends EventEmitter {
|
|
|
3321
3540
|
return;
|
|
3322
3541
|
}
|
|
3323
3542
|
}
|
|
3543
|
+
// tmux marks the PTY dead when its fd closes, before SIGCHLD can fill
|
|
3544
|
+
// pane_dead_status. Claude /exit (and any other CLI) can hit this gap.
|
|
3545
|
+
// Give the status four bounded re-probes; missing is never exit zero.
|
|
3546
|
+
if (paneStatus && !paneStatus.alive && paneStatus.exitCode === undefined) {
|
|
3547
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
3548
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
3549
|
+
if (discardStaleHealth())
|
|
3550
|
+
return;
|
|
3551
|
+
const observed = await healthTmux.getPaneStatus();
|
|
3552
|
+
if (discardStaleHealth())
|
|
3553
|
+
return;
|
|
3554
|
+
if (observed)
|
|
3555
|
+
paneStatus = observed;
|
|
3556
|
+
if (paneStatus.alive || paneStatus.exitCode !== undefined)
|
|
3557
|
+
break;
|
|
3558
|
+
}
|
|
3559
|
+
if (paneStatus.alive) {
|
|
3560
|
+
this.windowQueryFailureTicks = 0;
|
|
3561
|
+
this.stormWindow?.noteWindowAlive(this.name);
|
|
3562
|
+
scheduleNext();
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3324
3566
|
// paneStatus === null → window gone entirely (e.g. tmux server crash)
|
|
3325
3567
|
// paneStatus.alive === false → pane dead, exit code available
|
|
3326
3568
|
const exitCode = paneStatus?.exitCode;
|
|
@@ -3349,13 +3591,13 @@ export class Daemon extends EventEmitter {
|
|
|
3349
3591
|
let inWindowLossBurst = false;
|
|
3350
3592
|
let nullReason;
|
|
3351
3593
|
if (!paneStatus) {
|
|
3352
|
-
const serverAlive = await
|
|
3594
|
+
const serverAlive = await TmuxManager.sessionExists(this.tmuxSessionName);
|
|
3353
3595
|
if (discardStaleHealth())
|
|
3354
3596
|
return;
|
|
3355
3597
|
// A server may have already restarted before this daemon's health
|
|
3356
3598
|
// tick. The new PID is the durable generation boundary: treat it as
|
|
3357
3599
|
// a fleet storm even though `has-session` is true again.
|
|
3358
|
-
const serverPid = serverAlive ? await
|
|
3600
|
+
const serverPid = serverAlive ? await TmuxManager.getServerPid(this.tmuxSessionName) : null;
|
|
3359
3601
|
if (discardStaleHealth())
|
|
3360
3602
|
return;
|
|
3361
3603
|
const generationChanged = serverAlive && this.stormWindow?.observeServerAlive(serverPid) === true;
|
|
@@ -3384,7 +3626,7 @@ export class Daemon extends EventEmitter {
|
|
|
3384
3626
|
// the window truly no longer exists vs a transient query glitch.
|
|
3385
3627
|
nullReason = "no_window";
|
|
3386
3628
|
try {
|
|
3387
|
-
const windows = await
|
|
3629
|
+
const windows = await TmuxManager.listWindowsStrict(this.tmuxSessionName, this.controlClient);
|
|
3388
3630
|
if (discardStaleHealth())
|
|
3389
3631
|
return;
|
|
3390
3632
|
this.windowQueryFailureTicks = 0;
|
|
@@ -3504,6 +3746,8 @@ export class Daemon extends EventEmitter {
|
|
|
3504
3746
|
try {
|
|
3505
3747
|
await this.spawnClaudeWindow();
|
|
3506
3748
|
this.setProcessStatus("running");
|
|
3749
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3750
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3507
3751
|
this.logger.info("Recovered from background session conflict");
|
|
3508
3752
|
this.emit("crash_respawn", this.name);
|
|
3509
3753
|
}
|
|
@@ -3629,10 +3873,10 @@ export class Daemon extends EventEmitter {
|
|
|
3629
3873
|
// Wrapped in try-catch: if tmux server is dead, listWindows throws —
|
|
3630
3874
|
// must not block spawnClaudeWindow (which calls ensureSession).
|
|
3631
3875
|
try {
|
|
3632
|
-
const windows = await
|
|
3876
|
+
const windows = await TmuxManager.listWindows(this.tmuxSessionName);
|
|
3633
3877
|
for (const w of windows) {
|
|
3634
3878
|
if (w.name === this.name) {
|
|
3635
|
-
const tm =
|
|
3879
|
+
const tm = new TmuxManager(this.tmuxSessionName, w.id);
|
|
3636
3880
|
await tm.killWindow();
|
|
3637
3881
|
}
|
|
3638
3882
|
}
|
|
@@ -3663,6 +3907,8 @@ export class Daemon extends EventEmitter {
|
|
|
3663
3907
|
catch { /* may not exist */ }
|
|
3664
3908
|
}
|
|
3665
3909
|
this.setProcessStatus("running");
|
|
3910
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3911
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3666
3912
|
this.logger.info({ resumed }, `Respawned ${cliLabel} window after crash`);
|
|
3667
3913
|
this.emit("crash_respawn", this.name);
|
|
3668
3914
|
// Emit the per-instance audit event while the storm is still active
|
|
@@ -3775,7 +4021,7 @@ export class Daemon extends EventEmitter {
|
|
|
3775
4021
|
// Force the next ready capture to emit a fresh transition after respawn.
|
|
3776
4022
|
this.instanceState = "working";
|
|
3777
4023
|
}
|
|
3778
|
-
this.
|
|
4024
|
+
this.ipcServer?.broadcast({
|
|
3779
4025
|
type: "instance_process_state",
|
|
3780
4026
|
instanceName: this.name,
|
|
3781
4027
|
status,
|
|
@@ -3971,15 +4217,12 @@ export class Daemon extends EventEmitter {
|
|
|
3971
4217
|
for (const key of dialog.keys) {
|
|
3972
4218
|
if (stale())
|
|
3973
4219
|
return;
|
|
3974
|
-
const keyScreen = this.nativePaneLifecycle ? await pollTmux.capturePane() : currentPane;
|
|
3975
|
-
if (stale() || !Daemon.dialogMatches(dialog, keyScreen))
|
|
3976
|
-
return;
|
|
3977
4220
|
let sent = false;
|
|
3978
4221
|
if (SPECIAL_KEYS.has(key)) {
|
|
3979
|
-
sent = await
|
|
4222
|
+
sent = await pollTmux.sendSpecialKey(key);
|
|
3980
4223
|
}
|
|
3981
4224
|
else if (dialog.literalKeys) {
|
|
3982
|
-
sent = await
|
|
4225
|
+
sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
|
|
3983
4226
|
}
|
|
3984
4227
|
else {
|
|
3985
4228
|
sent = await pollTmux.pasteText(key, this.systemPasteOptions());
|
|
@@ -4179,7 +4422,13 @@ export class Daemon extends EventEmitter {
|
|
|
4179
4422
|
for (const ep of patterns) {
|
|
4180
4423
|
const key = Daemon.errorPatternKey(ep);
|
|
4181
4424
|
const count = countMatches(ep.pattern);
|
|
4182
|
-
|
|
4425
|
+
let seen = this.lastErrorCount.get(key) ?? 0;
|
|
4426
|
+
const afterInput = this.quotaOccurrencesAfterInput(pane, ep);
|
|
4427
|
+
// Only a new echo of our acknowledged input can re-arm a same-count replacement. Old rows before that echo,
|
|
4428
|
+
// an ambiguous/missing marker or a superseded input owner retain the ordinary count-based decision.
|
|
4429
|
+
const newQuotaAfterInput = !!afterInput && afterInput.count > (afterInput.input.reported.get(key) ?? 0);
|
|
4430
|
+
if (newQuotaAfterInput)
|
|
4431
|
+
seen = Math.min(seen, count - 1);
|
|
4183
4432
|
// #2282: a resumed exhausted429 row is history, not even a new
|
|
4184
4433
|
// notification/failover incident; so is a resumed codex workspace-limit row
|
|
4185
4434
|
// (#2844: a wake must not pause again on it). Wait for the first live
|
|
@@ -4201,7 +4450,7 @@ export class Daemon extends EventEmitter {
|
|
|
4201
4450
|
// the lifecycle, whose usage probe decides (available / reserve / exhausted).
|
|
4202
4451
|
if (Daemon.isCodexUsageLimitPause(ep)) {
|
|
4203
4452
|
const ack = this.codexReserveAck;
|
|
4204
|
-
if (ack && ack.generation === this.spawnGeneration && count <= ack.count) {
|
|
4453
|
+
if (ack && ack.generation === this.spawnGeneration && count <= ack.count && !newQuotaAfterInput) {
|
|
4205
4454
|
// The handled incident's lines can scroll out of the capture buffer as
|
|
4206
4455
|
// the reserve session talks on. Lower the proof (and the occurrence
|
|
4207
4456
|
// baseline) with them: otherwise `count <= ack.count` would keep
|
|
@@ -4242,6 +4491,8 @@ export class Daemon extends EventEmitter {
|
|
|
4242
4491
|
continue;
|
|
4243
4492
|
}
|
|
4244
4493
|
this.lastErrorCount.set(key, count);
|
|
4494
|
+
if (afterInput)
|
|
4495
|
+
afterInput.input.reported.set(key, Math.max(afterInput.input.reported.get(key) ?? 0, afterInput.count));
|
|
4245
4496
|
// Kiro's `auto` model picks a concrete model per turn and reroutes around
|
|
4246
4497
|
// a temporarily unavailable one on its own — telling the user to run
|
|
4247
4498
|
// /model would ask them to fix something the CLI is already handling.
|
|
@@ -4342,20 +4593,7 @@ export class Daemon extends EventEmitter {
|
|
|
4342
4593
|
*/
|
|
4343
4594
|
async sendEscape() {
|
|
4344
4595
|
const cancelKey = this.backend?.getCancelKey() ?? "Escape";
|
|
4345
|
-
|
|
4346
|
-
if (!tmux)
|
|
4347
|
-
return;
|
|
4348
|
-
if (this.nativePaneLifecycle) {
|
|
4349
|
-
// A native pane's composer proof cannot see a permission dialog, so an unproved key is withheld there. The cancel
|
|
4350
|
-
// is the person's own interrupt, keyed on the screen read just before (as the dialog answer path is): what Esc
|
|
4351
|
-
// does on Linux's tmux pane, on the same screen, with the same key.
|
|
4352
|
-
const screen = await tmux.capturePane().catch(() => null);
|
|
4353
|
-
if (screen === null)
|
|
4354
|
-
return;
|
|
4355
|
-
await tmux.sendSpecialKey(cancelKey, undefined, { kind: "dialog", screen });
|
|
4356
|
-
return;
|
|
4357
|
-
}
|
|
4358
|
-
await tmux.sendSpecialKey(cancelKey);
|
|
4596
|
+
await this.tmux?.sendSpecialKey(cancelKey);
|
|
4359
4597
|
}
|
|
4360
4598
|
/**
|
|
4361
4599
|
* Drop every not-yet-started pane delivery which predates a user cancel.
|
|
@@ -4375,6 +4613,14 @@ export class Daemon extends EventEmitter {
|
|
|
4375
4613
|
// (Completion paths clear via the guard's onComplete; a cancel with no
|
|
4376
4614
|
// later idle edge would otherwise leave the marker behind.)
|
|
4377
4615
|
clearInFlightTurnMarker(this.instanceDir);
|
|
4616
|
+
// #3030: what the person cancelled is not kept for a redelivery (nor reported as not taken).
|
|
4617
|
+
try {
|
|
4618
|
+
for (const entry of this.handoffStore().list())
|
|
4619
|
+
this.handoffStore().remove(entry.id);
|
|
4620
|
+
}
|
|
4621
|
+
catch (err) {
|
|
4622
|
+
this.logger.warn({ err }, "Could not clear queued hand-off records on cancel");
|
|
4623
|
+
}
|
|
4378
4624
|
this.deliveryEpoch = fleetEpoch === undefined
|
|
4379
4625
|
? this.deliveryEpoch + 1
|
|
4380
4626
|
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
@@ -4413,7 +4659,7 @@ export class Daemon extends EventEmitter {
|
|
|
4413
4659
|
return;
|
|
4414
4660
|
const outcome = follower.look(pane);
|
|
4415
4661
|
if (outcome) {
|
|
4416
|
-
this.
|
|
4662
|
+
this.ipcServer?.broadcast({ type: "compact_pane_result", id, owner, outcome });
|
|
4417
4663
|
return;
|
|
4418
4664
|
}
|
|
4419
4665
|
}
|
|
@@ -4433,7 +4679,7 @@ export class Daemon extends EventEmitter {
|
|
|
4433
4679
|
const reportSwitch = (result) => {
|
|
4434
4680
|
if (!ownSwitch)
|
|
4435
4681
|
return;
|
|
4436
|
-
this.
|
|
4682
|
+
this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
|
|
4437
4683
|
};
|
|
4438
4684
|
// Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
|
|
4439
4685
|
// (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
|
|
@@ -4448,7 +4694,7 @@ export class Daemon extends EventEmitter {
|
|
|
4448
4694
|
if (!notifyWritten || told)
|
|
4449
4695
|
return;
|
|
4450
4696
|
told = true;
|
|
4451
|
-
this.
|
|
4697
|
+
this.ipcServer?.broadcast({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
|
|
4452
4698
|
...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
|
|
4453
4699
|
if (written && paneFollower)
|
|
4454
4700
|
void this.followCompactPane(notifyWritten, owner, paneFollower);
|
|
@@ -4610,11 +4856,10 @@ export class Daemon extends EventEmitter {
|
|
|
4610
4856
|
const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
|
|
4611
4857
|
if (this.tmux !== tmux)
|
|
4612
4858
|
return false;
|
|
4613
|
-
const
|
|
4614
|
-
const now = claudeSwitchDialog(screen);
|
|
4859
|
+
const now = claudeSwitchDialog(await read());
|
|
4615
4860
|
if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
|
|
4616
4861
|
return false;
|
|
4617
|
-
return
|
|
4862
|
+
return tmux.sendSpecialKey(key);
|
|
4618
4863
|
});
|
|
4619
4864
|
const gone = async () => {
|
|
4620
4865
|
const until = performance.now() + OWN_SWITCH_SETTLE_MS;
|
|
@@ -4703,12 +4948,9 @@ export class Daemon extends EventEmitter {
|
|
|
4703
4948
|
return false;
|
|
4704
4949
|
const specialKeys = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
|
|
4705
4950
|
for (const key of dialog.keys) {
|
|
4706
|
-
const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : currentPane;
|
|
4707
|
-
if (!matches(keyScreen))
|
|
4708
|
-
return false;
|
|
4709
4951
|
const ok = specialKeys.has(key)
|
|
4710
|
-
? await
|
|
4711
|
-
: await
|
|
4952
|
+
? await this.tmux.sendSpecialKey(key)
|
|
4953
|
+
: await this.tmux.sendKeys(key);
|
|
4712
4954
|
if (!ok)
|
|
4713
4955
|
return false;
|
|
4714
4956
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
@@ -4951,7 +5193,7 @@ export class Daemon extends EventEmitter {
|
|
|
4951
5193
|
await this.tmux.sendSpecialKey("Escape").catch(() => false);
|
|
4952
5194
|
return true;
|
|
4953
5195
|
}
|
|
4954
|
-
async stop(
|
|
5196
|
+
async stop() {
|
|
4955
5197
|
this.publishWorkingState("before_stop");
|
|
4956
5198
|
this.logger.info("Stopping daemon instance");
|
|
4957
5199
|
this.clearQuitRelaunchWatch();
|
|
@@ -4973,18 +5215,8 @@ export class Daemon extends EventEmitter {
|
|
|
4973
5215
|
}
|
|
4974
5216
|
if (this.adapter)
|
|
4975
5217
|
await this.adapter.stop();
|
|
4976
|
-
if (this.nativePaneLifecycle) {
|
|
4977
|
-
this.healthCheckPaused = true;
|
|
4978
|
-
this.saveSessionId();
|
|
4979
|
-
await this.nativePaneLifecycle.release(options.nativePane ?? "detach");
|
|
4980
|
-
try {
|
|
4981
|
-
unlinkSync(join(this.instanceDir, "daemon.pid"));
|
|
4982
|
-
}
|
|
4983
|
-
catch { /* metadata only */ }
|
|
4984
|
-
return;
|
|
4985
|
-
}
|
|
4986
5218
|
// Notify MCP servers of graceful shutdown (prevents reconnect attempts)
|
|
4987
|
-
this.
|
|
5219
|
+
this.ipcServer?.broadcast({ type: "shutdown" });
|
|
4988
5220
|
// Quit CLI FIRST — this kills MCP server child processes cleanly.
|
|
4989
5221
|
// IPC must stay open during quit so MCP servers receive the shutdown message.
|
|
4990
5222
|
if (this.tmux) {
|
|
@@ -5130,24 +5362,6 @@ export class Daemon extends EventEmitter {
|
|
|
5130
5362
|
freshChoice: this.recoveryFreshChoice || this.isNewSession && !this.bootSkippedResume, crashLoopFresh: this.recoveryCrashLoopFresh,
|
|
5131
5363
|
cancelled: this.recoveryCancelled, background: this.workingBackgroundObservation };
|
|
5132
5364
|
}
|
|
5133
|
-
/** Observe this host General before its approved switch. No keys, process signals or provider writes. */
|
|
5134
|
-
async generalHostOwner() {
|
|
5135
|
-
const pane = this.tmux, owner = JSON.stringify(this.interactionOwner());
|
|
5136
|
-
if (!this.config.general_topic || this.nativePaneLifecycle || !pane || !/^@\d+$/.test(pane.getWindowId()))
|
|
5137
|
-
throw Error("General host owner is unavailable");
|
|
5138
|
-
const status = await pane.getPaneStatus();
|
|
5139
|
-
if (!status?.alive || !status.pid || status.pid <= 1)
|
|
5140
|
-
throw Error("General host process cannot be proven");
|
|
5141
|
-
const checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint();
|
|
5142
|
-
if (this.tmux !== pane || owner !== JSON.stringify(this.interactionOwner()))
|
|
5143
|
-
throw Error("General host owner changed during observation");
|
|
5144
|
-
const backend = this.config.backend ?? "claude-code";
|
|
5145
|
-
let sessionId = this.backend?.getSessionId() ?? undefined;
|
|
5146
|
-
if (backend === "codex" && checkpoint)
|
|
5147
|
-
sessionId = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(checkpoint.path)?.[1];
|
|
5148
|
-
return { kind: "tmux", windowId: pane.getWindowId(), pid: status.pid,
|
|
5149
|
-
...(typeof sessionId === "string" && sessionId.length > 0 && sessionId.length <= 256 && !/[\p{Cc}\p{Cf}]/u.test(sessionId) ? { pin: { backend, sessionId } } : {}) };
|
|
5150
|
-
}
|
|
5151
5365
|
/** Local system work: no user turn, channel reply target, wake or replay of an original request. */
|
|
5152
5366
|
async deliverWorkingRecovery(content, delivery, admitted) {
|
|
5153
5367
|
const tmux = this.tmux, owner = JSON.stringify(this.interactionOwner()), epoch = this.deliveryEpoch;
|
|
@@ -5168,11 +5382,7 @@ export class Daemon extends EventEmitter {
|
|
|
5168
5382
|
|| !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
|
|
5169
5383
|
|| this.backend?.backgroundWorkRunning?.(pane) === true)
|
|
5170
5384
|
return;
|
|
5171
|
-
if (
|
|
5172
|
-
|| !!this.backend?.isDeliveryInputReadyPane && !this.backend.isDeliveryInputReadyPane(pane)
|
|
5173
|
-
: await tmux.getPaneInputMode?.() !== "raw")
|
|
5174
|
-
return;
|
|
5175
|
-
if (!current())
|
|
5385
|
+
if (await tmux.getPaneInputMode?.() !== "raw" || !current())
|
|
5176
5386
|
return;
|
|
5177
5387
|
const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
|
|
5178
5388
|
if (!current())
|
|
@@ -5263,7 +5473,7 @@ export class Daemon extends EventEmitter {
|
|
|
5263
5473
|
this.logger.debug({ err }, "Interaction listener failed");
|
|
5264
5474
|
}
|
|
5265
5475
|
try {
|
|
5266
|
-
this.
|
|
5476
|
+
this.ipcServer?.broadcast({ type: "instance_interaction", instanceName: this.name, interaction });
|
|
5267
5477
|
}
|
|
5268
5478
|
catch (err) {
|
|
5269
5479
|
this.logger.debug({ err }, "Interaction broadcast failed");
|
|
@@ -5351,56 +5561,6 @@ export class Daemon extends EventEmitter {
|
|
|
5351
5561
|
}
|
|
5352
5562
|
return true;
|
|
5353
5563
|
}
|
|
5354
|
-
/** Shared Fleet inputs: socket and native transports retain the same wake, epoch and raw-paste semantics. */
|
|
5355
|
-
handleFleetMessage(msg, send, current = () => true) {
|
|
5356
|
-
if (msg.type === "query_sessions") {
|
|
5357
|
-
send({ type: "query_sessions_response", sessions: [] });
|
|
5358
|
-
}
|
|
5359
|
-
else if (msg.type === "fleet_inbound") {
|
|
5360
|
-
const meta = msg.meta;
|
|
5361
|
-
const targetSession = msg.targetSession;
|
|
5362
|
-
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
5363
|
-
void this.wake().then(() => {
|
|
5364
|
-
if (!current()) {
|
|
5365
|
-
this.deferDurableDelivery(meta, "native channel owner changed before pane submission");
|
|
5366
|
-
return;
|
|
5367
|
-
}
|
|
5368
|
-
this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
|
|
5369
|
-
}).catch(err => {
|
|
5370
|
-
this.deferDurableDelivery(meta, `target wake failed before pane submission: ${err.message}`);
|
|
5371
|
-
this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
|
|
5372
|
-
});
|
|
5373
|
-
}
|
|
5374
|
-
else if (msg.type === "raw_paste") {
|
|
5375
|
-
this.handleRawPasteIpcMessage(msg);
|
|
5376
|
-
}
|
|
5377
|
-
else if (msg.type === "config_update") {
|
|
5378
|
-
this.applyConfigUpdate(msg.config);
|
|
5379
|
-
}
|
|
5380
|
-
else if (msg.type === "steer") {
|
|
5381
|
-
const meta = (msg.meta ?? {});
|
|
5382
|
-
this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
5383
|
-
}
|
|
5384
|
-
else if (msg.type === "btw") {
|
|
5385
|
-
const meta = (msg.meta ?? {});
|
|
5386
|
-
this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
5387
|
-
}
|
|
5388
|
-
else if (msg.type === "fleet_schedule_trigger") {
|
|
5389
|
-
const payload = msg.payload;
|
|
5390
|
-
const meta = msg.meta;
|
|
5391
|
-
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
5392
|
-
void this.wake().then(() => { if (current())
|
|
5393
|
-
this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch); }).catch(err => {
|
|
5394
|
-
this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
|
|
5395
|
-
});
|
|
5396
|
-
}
|
|
5397
|
-
else if (msg.type === "query_instance_state") {
|
|
5398
|
-
void this.respondToInstanceStateQuery(msg, undefined, send);
|
|
5399
|
-
}
|
|
5400
|
-
else
|
|
5401
|
-
return false;
|
|
5402
|
-
return true;
|
|
5403
|
-
}
|
|
5404
5564
|
/**
|
|
5405
5565
|
* Answer a FleetManager state query. Most callers only need the cached state,
|
|
5406
5566
|
* but lifecycle decisions such as post-reply Cancel retirement need an
|
|
@@ -5408,7 +5568,7 @@ export class Daemon extends EventEmitter {
|
|
|
5408
5568
|
* the last transition can stay "working" even though the CLI has since
|
|
5409
5569
|
* settled without emitting another control-mode output record.
|
|
5410
5570
|
*/
|
|
5411
|
-
async respondToInstanceStateQuery(msg, socket
|
|
5571
|
+
async respondToInstanceStateQuery(msg, socket) {
|
|
5412
5572
|
try {
|
|
5413
5573
|
if (msg.deliveryIdle === true) {
|
|
5414
5574
|
await this.captureAndEvaluateInstanceState("delivery_idle_gate");
|
|
@@ -5417,7 +5577,7 @@ export class Daemon extends EventEmitter {
|
|
|
5417
5577
|
await this.captureAndEvaluateInstanceState("state_query");
|
|
5418
5578
|
}
|
|
5419
5579
|
const snapshot = this.getInstanceStateSnapshot();
|
|
5420
|
-
|
|
5580
|
+
this.ipcServer?.send(socket, {
|
|
5421
5581
|
type: "instance_state_response",
|
|
5422
5582
|
requestId: msg.requestId,
|
|
5423
5583
|
instanceName: this.name,
|
|
@@ -5425,11 +5585,7 @@ export class Daemon extends EventEmitter {
|
|
|
5425
5585
|
state: this.isPaused ? "paused" : snapshot.state,
|
|
5426
5586
|
processStatus: this.processStatus,
|
|
5427
5587
|
pausedAt: this.lastPausedAt,
|
|
5428
|
-
};
|
|
5429
|
-
if (send)
|
|
5430
|
-
send(reply);
|
|
5431
|
-
else if (socket)
|
|
5432
|
-
this.ipcServer?.send(socket, reply);
|
|
5588
|
+
});
|
|
5433
5589
|
}
|
|
5434
5590
|
catch (err) {
|
|
5435
5591
|
this.logger.debug({ err: err.message }, "Instance state query failed");
|
|
@@ -5469,49 +5625,7 @@ export class Daemon extends EventEmitter {
|
|
|
5469
5625
|
static AUTH_RECONFIRM_ROWS = 15;
|
|
5470
5626
|
/** Why the deferred pause was requested; what its marker records (Phase 2a). */
|
|
5471
5627
|
pendingPauseReason = "auth";
|
|
5472
|
-
nativePauseProof = null;
|
|
5473
|
-
/** Consume the exact idle proof synchronously after the lifecycle's await, before fencing and closing. */
|
|
5474
|
-
nativePauseStillReady() {
|
|
5475
|
-
const proof = this.nativePauseProof;
|
|
5476
|
-
this.nativePauseProof = null;
|
|
5477
|
-
if (proof?.())
|
|
5478
|
-
return true;
|
|
5479
|
-
this.pauseRequested = false;
|
|
5480
|
-
return false;
|
|
5481
|
-
}
|
|
5482
|
-
/** Native lifecycle pause uses the ordinary idle/background/admission proof before closing its owned Job. */
|
|
5483
|
-
async nativePauseReady(admission) {
|
|
5484
|
-
this.nativePauseProof = null;
|
|
5485
|
-
const native = this.nativePaneLifecycle, pane = this.tmux, owner = this.interactionOwner();
|
|
5486
|
-
const allowStuck = this.pauseAllowStuck;
|
|
5487
|
-
this.pauseAllowStuck = false;
|
|
5488
|
-
const ready = () => !!native && native === this.nativePaneLifecycle && native.current() && this.tmux === pane
|
|
5489
|
-
&& sameInteractionOwner(owner, this.interactionOwner()) && !this.runtimeMonitorsFrozen && !this.spawning
|
|
5490
|
-
&& this.processStatus === "running" && this.pasteQueueDepth === 0
|
|
5491
|
-
&& (this.instanceState === "idle" || allowStuck && this.instanceState === "stuck") && (!admission || admission.current());
|
|
5492
|
-
if (!pane || !ready()) {
|
|
5493
|
-
this.pauseRequested = false;
|
|
5494
|
-
return false;
|
|
5495
|
-
}
|
|
5496
|
-
let text;
|
|
5497
|
-
try {
|
|
5498
|
-
text = await pane.capturePane();
|
|
5499
|
-
}
|
|
5500
|
-
catch {
|
|
5501
|
-
this.pauseRequested = false;
|
|
5502
|
-
return false;
|
|
5503
|
-
}
|
|
5504
|
-
if (!ready() || admission && !admission.accepts(text) || this.backend?.backgroundWorkRunning?.(text)
|
|
5505
|
-
|| !allowStuck && !this.paneAuthoritativelyIdle(text)) {
|
|
5506
|
-
this.pauseRequested = false;
|
|
5507
|
-
return false;
|
|
5508
|
-
}
|
|
5509
|
-
this.nativePauseProof = ready;
|
|
5510
|
-
return true;
|
|
5511
|
-
}
|
|
5512
5628
|
async pause(reason = "operator", admission) {
|
|
5513
|
-
if (this.nativePaneLifecycle)
|
|
5514
|
-
throw Error("Native pause is owned by its Fleet lifecycle transition");
|
|
5515
5629
|
if (admission && !admission.current())
|
|
5516
5630
|
return;
|
|
5517
5631
|
if (this.pauseWakeState === "paused")
|
|
@@ -5607,7 +5721,7 @@ export class Daemon extends EventEmitter {
|
|
|
5607
5721
|
writePausedMarker(this.instanceDir, this.lastPausedAt ?? Date.now(), reason);
|
|
5608
5722
|
this.publishWorkingState("execution");
|
|
5609
5723
|
this.logger.info({ pausedAt: this.lastPausedAt }, "Instance auto-paused");
|
|
5610
|
-
this.
|
|
5724
|
+
this.ipcServer?.broadcast({
|
|
5611
5725
|
type: "instance_state", instanceName: this.name, state: "paused", pausedAt: this.lastPausedAt,
|
|
5612
5726
|
});
|
|
5613
5727
|
this.emit("auto_paused", { name: this.name, pausedAt: this.lastPausedAt });
|
|
@@ -5702,7 +5816,7 @@ export class Daemon extends EventEmitter {
|
|
|
5702
5816
|
this.resumeRuntimeMonitors();
|
|
5703
5817
|
this.publishWorkingState("execution");
|
|
5704
5818
|
this.logger.info("Instance auto-woke");
|
|
5705
|
-
this.
|
|
5819
|
+
this.ipcServer?.broadcast({
|
|
5706
5820
|
type: "instance_state", instanceName: this.name, state: this.instanceState, pausedAt: null,
|
|
5707
5821
|
});
|
|
5708
5822
|
this.emit("auto_woke", { name: this.name });
|
|
@@ -5803,6 +5917,10 @@ export class Daemon extends EventEmitter {
|
|
|
5803
5917
|
if (snapshot.state === "working" && this.recoveryBootSettled
|
|
5804
5918
|
&& this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
|
|
5805
5919
|
this.recoveryNativeWork = true;
|
|
5920
|
+
if (snapshot.state !== "idle")
|
|
5921
|
+
this.instanceIdleSinceMono = null;
|
|
5922
|
+
else if (this.instanceState !== "idle" || this.instanceIdleSinceMono === null)
|
|
5923
|
+
this.instanceIdleSinceMono = performance.now();
|
|
5806
5924
|
this.instanceState = snapshot.state;
|
|
5807
5925
|
this.acceptedInstanceSnapshot = { ...snapshot };
|
|
5808
5926
|
// Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
|
|
@@ -5862,7 +5980,7 @@ export class Daemon extends EventEmitter {
|
|
|
5862
5980
|
this.logStateEdge(previous, snapshot);
|
|
5863
5981
|
this.publishWorkingState("execution");
|
|
5864
5982
|
this.emit("instance_state", { name: this.name, ...snapshot });
|
|
5865
|
-
this.
|
|
5983
|
+
this.ipcServer?.broadcast({ type: "instance_state", instanceName: this.name, ...snapshot });
|
|
5866
5984
|
if (snapshot.state === "stuck" && pane && this.instanceStateReadyPattern) {
|
|
5867
5985
|
this.handleStuckTransition(pane, snapshot, this.instanceStateReadyPattern);
|
|
5868
5986
|
}
|
|
@@ -5914,7 +6032,7 @@ export class Daemon extends EventEmitter {
|
|
|
5914
6032
|
* message can arrive mid-turn and sit queued, and marking at arrival would let
|
|
5915
6033
|
* the PREVIOUS turn's idle edge consume (and reset) the new turn's state.
|
|
5916
6034
|
*/
|
|
5917
|
-
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch) {
|
|
6035
|
+
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch, queuedHandoff = false) {
|
|
5918
6036
|
// Cancel may have interrupted an in-flight paste before its promise settled.
|
|
5919
6037
|
// Its late success must not re-arm the turn that the user just cancelled.
|
|
5920
6038
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
@@ -5962,7 +6080,9 @@ export class Daemon extends EventEmitter {
|
|
|
5962
6080
|
// during the continuation boots clean instead of chaining resumes.
|
|
5963
6081
|
// #2544: a reply that settled before this arm already answered the person —
|
|
5964
6082
|
// writing a marker would re-inject a continuation for a finished turn.
|
|
5965
|
-
|
|
6083
|
+
// #3030: a message only queued in the busy CLI has not started a turn: the interrupted turn is still the running one,
|
|
6084
|
+
// and a queued message lost to a restart is the hand-off record's to redeliver, not a continuation's.
|
|
6085
|
+
if (!meta.resumedContinuationOf && !armed.completionDelivered && !queuedHandoff) {
|
|
5966
6086
|
const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
|
|
5967
6087
|
const resumeCwd = this.config.working_directory ?? "";
|
|
5968
6088
|
writeInFlightTurnMarker(this.instanceDir, {
|
|
@@ -6194,7 +6314,7 @@ export class Daemon extends EventEmitter {
|
|
|
6194
6314
|
// outbound action suppresses a duplicate proxy; only a delivered
|
|
6195
6315
|
// human-facing channel action satisfies the completion guard below.
|
|
6196
6316
|
if (!turn.outboundDelivered && this.config.mcp_proxy_reply === true
|
|
6197
|
-
&&
|
|
6317
|
+
&& mcpServerState(this.instanceDir).state === "dead") {
|
|
6198
6318
|
this.turnReplyGuard.complete(turn.generation);
|
|
6199
6319
|
void this.sendProxyReply(pane, turn.target);
|
|
6200
6320
|
return;
|
|
@@ -6571,7 +6691,7 @@ export class Daemon extends EventEmitter {
|
|
|
6571
6691
|
}
|
|
6572
6692
|
});
|
|
6573
6693
|
}
|
|
6574
|
-
if (!this.
|
|
6694
|
+
if (!this.ipcServer) {
|
|
6575
6695
|
this.logger.error(`${logLabel} failed — no adapter or fleet IPC route`);
|
|
6576
6696
|
return Promise.resolve(false);
|
|
6577
6697
|
}
|
|
@@ -6595,7 +6715,7 @@ export class Daemon extends EventEmitter {
|
|
|
6595
6715
|
resolve(delivered);
|
|
6596
6716
|
});
|
|
6597
6717
|
try {
|
|
6598
|
-
this.
|
|
6718
|
+
this.ipcServer.broadcast({
|
|
6599
6719
|
type: "fleet_outbound",
|
|
6600
6720
|
tool: "reply",
|
|
6601
6721
|
args,
|
|
@@ -7166,7 +7286,7 @@ export class Daemon extends EventEmitter {
|
|
|
7166
7286
|
if (next === this.currentActivity)
|
|
7167
7287
|
return;
|
|
7168
7288
|
this.currentActivity = next;
|
|
7169
|
-
this.
|
|
7289
|
+
this.ipcServer?.broadcast({
|
|
7170
7290
|
type: "instance_activity",
|
|
7171
7291
|
instanceName: this.name,
|
|
7172
7292
|
activity: next,
|
|
@@ -7219,6 +7339,10 @@ export class Daemon extends EventEmitter {
|
|
|
7219
7339
|
else if (update.reply_completion_guard === null) {
|
|
7220
7340
|
delete this.config.reply_completion_guard;
|
|
7221
7341
|
}
|
|
7342
|
+
if (update.quota_notice === null)
|
|
7343
|
+
delete this.config.quota_notice;
|
|
7344
|
+
else if (update.quota_notice !== undefined && !quotaNoticeProblem(update.quota_notice))
|
|
7345
|
+
this.config.quota_notice = structuredClone(update.quota_notice);
|
|
7222
7346
|
if (typeof update.mcp_proxy_reply === "boolean")
|
|
7223
7347
|
this.config.mcp_proxy_reply = update.mcp_proxy_reply;
|
|
7224
7348
|
else if (update.mcp_proxy_reply === null)
|
|
@@ -7243,7 +7367,7 @@ export class Daemon extends EventEmitter {
|
|
|
7243
7367
|
else
|
|
7244
7368
|
this.config.auto_pause_after = nextAutoPause;
|
|
7245
7369
|
if (nextAutoPause !== previousAutoPause) {
|
|
7246
|
-
const isGeneral = this.config.general_topic === true ||
|
|
7370
|
+
const isGeneral = this.config.general_topic === true || this.name === "general";
|
|
7247
7371
|
this.autoPauseController.reconfigure((isGeneral ? 0 : nextAutoPause) * 60_000);
|
|
7248
7372
|
}
|
|
7249
7373
|
}
|
|
@@ -7323,7 +7447,7 @@ export class Daemon extends EventEmitter {
|
|
|
7323
7447
|
return;
|
|
7324
7448
|
this.lastProgressBroadcast = rendered;
|
|
7325
7449
|
this.lastProgressBroadcastAt = Date.now();
|
|
7326
|
-
this.
|
|
7450
|
+
this.ipcServer?.broadcast({
|
|
7327
7451
|
type: "instance_progress",
|
|
7328
7452
|
instanceName: this.name,
|
|
7329
7453
|
progress: rendered,
|
|
@@ -7339,7 +7463,7 @@ export class Daemon extends EventEmitter {
|
|
|
7339
7463
|
if (this.lastProgressBroadcast !== "") {
|
|
7340
7464
|
this.lastProgressBroadcast = "";
|
|
7341
7465
|
this.lastProgressBroadcastAt = Date.now();
|
|
7342
|
-
this.
|
|
7466
|
+
this.ipcServer?.broadcast({ type: "instance_progress", instanceName: this.name, progress: "" });
|
|
7343
7467
|
}
|
|
7344
7468
|
}
|
|
7345
7469
|
/**
|
|
@@ -7367,10 +7491,12 @@ export class Daemon extends EventEmitter {
|
|
|
7367
7491
|
// Command name only — never the arguments. See shellCommandLabel.
|
|
7368
7492
|
if (name === "Bash")
|
|
7369
7493
|
return `$ ${shellCommandLabel(String(inp.command ?? ""))}`;
|
|
7494
|
+
// The tool name only — never the search pattern: it can be a literal secret someone is looking for, and this label
|
|
7495
|
+
// reaches the channel's progress line.
|
|
7370
7496
|
if (name === "Glob")
|
|
7371
|
-
return
|
|
7497
|
+
return "Glob";
|
|
7372
7498
|
if (name === "Grep")
|
|
7373
|
-
return
|
|
7499
|
+
return "Grep";
|
|
7374
7500
|
if (name === "Agent")
|
|
7375
7501
|
return "Agent (subagent)";
|
|
7376
7502
|
if (name.startsWith("mcp__agend__"))
|
|
@@ -7504,7 +7630,7 @@ export class Daemon extends EventEmitter {
|
|
|
7504
7630
|
reportCrossInstanceDeliveryFailure(meta, verdict, error) {
|
|
7505
7631
|
if (!meta.from_instance)
|
|
7506
7632
|
return;
|
|
7507
|
-
this.
|
|
7633
|
+
this.ipcServer?.broadcast({
|
|
7508
7634
|
type: "cross_instance_delivery_failed",
|
|
7509
7635
|
senderSession: meta.from_instance,
|
|
7510
7636
|
targetInstance: this.name,
|
|
@@ -7741,6 +7867,13 @@ export class Daemon extends EventEmitter {
|
|
|
7741
7867
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7742
7868
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
7743
7869
|
this.deferDurableDelivery(meta, "delivery epoch changed while queued");
|
|
7870
|
+
// #3030: a cancel is the person's — a queued redelivery it dropped is not redelivered later.
|
|
7871
|
+
if (meta.handoff_redelivery && !this.deliveryWritesStopping) {
|
|
7872
|
+
try {
|
|
7873
|
+
this.handoffStore().remove(meta.handoff_redelivery);
|
|
7874
|
+
}
|
|
7875
|
+
catch { /* the next CLI judges it */ }
|
|
7876
|
+
}
|
|
7744
7877
|
return;
|
|
7745
7878
|
}
|
|
7746
7879
|
if (this.config.pre_task_command) {
|
|
@@ -7751,13 +7884,26 @@ export class Daemon extends EventEmitter {
|
|
|
7751
7884
|
// #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
|
|
7752
7885
|
// (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
|
|
7753
7886
|
// byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
|
|
7754
|
-
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id
|
|
7887
|
+
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id && !meta.handoff_redelivery
|
|
7888
|
+
? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7755
7889
|
const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
|
|
7756
7890
|
const status = channelStatus(meta);
|
|
7757
7891
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
7758
7892
|
// the previous turn's tool list.
|
|
7759
7893
|
this.resetToolProgress();
|
|
7760
7894
|
verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7895
|
+
// #3030: a hand-off into the CLI's own queue stays on record until a turn takes it (redeliveries included).
|
|
7896
|
+
if (!content.startsWith("/raw ") || meta.from_instance) {
|
|
7897
|
+
const { handoff_redelivery: redeliveryOf, handoff_redeliveries: redeliveries, handoff_holder: _holder, ...kept } = meta;
|
|
7898
|
+
verdict.trackHandoff = { content, meta: kept,
|
|
7899
|
+
...(redeliveryOf ? { id: redeliveryOf, redeliveries: Number(redeliveries) || 0 } : {}) };
|
|
7900
|
+
}
|
|
7901
|
+
// #3030: a redelivery is written only by the CLI it was issued for, and only while its record still says so —
|
|
7902
|
+
// one queued for a CLI that was replaced meanwhile is that next CLI's reconciliation's to judge again.
|
|
7903
|
+
if (meta.handoff_redelivery && !this.redeliveryStillOurs(meta)) {
|
|
7904
|
+
this.logger.info({ handoff: meta.handoff_redelivery }, "Queued hand-off redelivery dropped: its CLI was replaced before the write");
|
|
7905
|
+
return;
|
|
7906
|
+
}
|
|
7761
7907
|
const durableAttempt = this.durableDeliveryAttempt(meta, content);
|
|
7762
7908
|
if (durableAttempt === false) {
|
|
7763
7909
|
this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
|
|
@@ -7780,10 +7926,11 @@ export class Daemon extends EventEmitter {
|
|
|
7780
7926
|
if (whatsNew && (sent || verdict.paneWriteStarted))
|
|
7781
7927
|
this.recordWhatsNewSent(whatsNew);
|
|
7782
7928
|
}
|
|
7929
|
+
this.afterTrackedWrite(meta, verdict, !!sent, deliveryEpoch);
|
|
7783
7930
|
if (sent) {
|
|
7784
7931
|
delivered = true;
|
|
7785
7932
|
this.finishDurableSubmission(durable, verdict);
|
|
7786
|
-
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7933
|
+
this.markTurnStarted(meta, formatted, deliveryEpoch, !!verdict.handoff);
|
|
7787
7934
|
}
|
|
7788
7935
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
7789
7936
|
this.finishDurableDelivery(durable, "uncertain", `${verdict.phase ?? "post-submit"}:${verdict.proof ?? "unproven"}`, verdict);
|
|
@@ -8864,14 +9011,13 @@ export class Daemon extends EventEmitter {
|
|
|
8864
9011
|
if (this.remoteCardAnswering)
|
|
8865
9012
|
return { outcome: "refused", reason: "another answer to this card is being keyed in" };
|
|
8866
9013
|
this.remoteCardAnswering = true;
|
|
8867
|
-
let answerScreen = null;
|
|
8868
9014
|
try {
|
|
8869
9015
|
const outcome = await answerRemoteCard({
|
|
8870
9016
|
locked: fn => this.paneWriteLock.run(fn),
|
|
8871
9017
|
// An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
|
|
8872
|
-
read:
|
|
8873
|
-
key: key => this.tmux !== tmux
|
|
8874
|
-
paste: text => this.tmux !== tmux
|
|
9018
|
+
read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) : Promise.resolve(null),
|
|
9019
|
+
key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
|
|
9020
|
+
paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
|
|
8875
9021
|
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
8876
9022
|
now: () => performance.now(),
|
|
8877
9023
|
}, pane => {
|
|
@@ -9142,19 +9288,12 @@ export class Daemon extends EventEmitter {
|
|
|
9142
9288
|
if (!check || !this.tmux)
|
|
9143
9289
|
return !check;
|
|
9144
9290
|
try {
|
|
9145
|
-
const
|
|
9146
|
-
|
|
9147
|
-
: await this.tmux.getPaneInputMode?.() === "raw";
|
|
9148
|
-
// ConPTY has no POSIX termios mode. Its closed launch plus current helper handshake is the input barrier;
|
|
9149
|
-
// the same strict composer detector below is still required. Never grant native unknown-layout fallback.
|
|
9150
|
-
if (!inputActive) {
|
|
9291
|
+
const mode = await this.tmux.getPaneInputMode?.();
|
|
9292
|
+
if (mode !== "raw") {
|
|
9151
9293
|
this.resetFooterFallback();
|
|
9152
9294
|
return false;
|
|
9153
9295
|
}
|
|
9154
|
-
|
|
9155
|
-
if (this.nativePaneLifecycle && (!this.nativePaneLifecycle.current() || this.nativePaneLifecycle.mcpProof() !== "alive"))
|
|
9156
|
-
return false;
|
|
9157
|
-
return this.deliveryInputReadyPane(pane);
|
|
9296
|
+
return this.deliveryInputReadyPane(await this.tmux.capturePane());
|
|
9158
9297
|
}
|
|
9159
9298
|
catch {
|
|
9160
9299
|
this.resetFooterFallback();
|
|
@@ -9928,86 +10067,34 @@ export class Daemon extends EventEmitter {
|
|
|
9928
10067
|
// tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
|
|
9929
10068
|
// the CLI this paste went to is never sent to its successor.
|
|
9930
10069
|
const tmux = this.tmux;
|
|
9931
|
-
const backend = this.backend;
|
|
9932
|
-
const native = !!this.nativePaneLifecycle;
|
|
9933
10070
|
const generation = this.spawnGeneration;
|
|
9934
10071
|
const fence = this.launchFenceEpoch;
|
|
9935
|
-
const windowId = native ? tmux?.getWindowId() : undefined;
|
|
9936
|
-
// M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
|
|
9937
|
-
// tmux object and window — so the spawn generation and launch fence are part of every pane's fence: an Enter meant
|
|
9938
|
-
// for the CLI this paste went to is never sent to its successor. A native Windows pane keeps #2958's full fence too
|
|
9939
|
-
// (its launch can be replaced under the same handle).
|
|
9940
10072
|
const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
|
|
9941
|
-
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent())
|
|
9942
|
-
&& (!native || (!!windowId && !!backend && this.backend === backend && tmux.getWindowId() === windowId));
|
|
10073
|
+
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
|
|
9943
10074
|
if (!current())
|
|
9944
10075
|
return false;
|
|
9945
10076
|
// The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
if (reason === "output_changed" && current() && !this.nativeDeliveryHold())
|
|
9950
|
-
this.nativeEnterHold = {
|
|
9951
|
-
key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence,
|
|
9952
|
-
};
|
|
9953
|
-
if (reason === "pane-in-mode")
|
|
9954
|
-
this.logger.warn({ phase, reason }, "The pane stayed in a tmux mode; Enter withheld");
|
|
9955
|
-
else
|
|
9956
|
-
this.logger.warn({ ...tmux?.getLastInputDiagnostic?.(), phase, reason, retryCount: positiveRefusals }, "Native delivery Enter withheld by its safety check");
|
|
10077
|
+
const hold = () => {
|
|
10078
|
+
onInputHeld?.("pane-in-mode");
|
|
10079
|
+
this.logger.warn({ phase }, "The pane stayed in a tmux mode; Enter withheld");
|
|
9957
10080
|
return false;
|
|
9958
10081
|
};
|
|
9959
10082
|
// 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
|
|
9960
10083
|
// end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
|
|
9961
|
-
// past the bound.
|
|
9962
|
-
//
|
|
9963
|
-
// The fresh capture still binds the native host output sequence; ownership and positive-ACK-only retries remain.
|
|
10084
|
+
// past the bound. (beta.5: #2958's composer proof that used to sit between the two looks is reverted, decision
|
|
10085
|
+
// f57474aa: it rejected Codex's footer after a resume and while busy. #2957 is open again.)
|
|
9964
10086
|
const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
|
|
9965
|
-
let pane;
|
|
9966
|
-
let positiveRefusals = 0, enterMarked = false, positiveRetryDeadline = Infinity, linuxDialogRead = false;
|
|
9967
10087
|
for (;;) {
|
|
9968
|
-
const mode = await this.waitWhilePaneInMode(phase, current,
|
|
10088
|
+
const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
|
|
9969
10089
|
if (mode === "stale")
|
|
9970
10090
|
return false;
|
|
9971
10091
|
if (mode === "held")
|
|
9972
|
-
return hold(
|
|
9973
|
-
if (native) {
|
|
9974
|
-
// S-35: a native pane's capture refuses while output is still arriving ("Pane changed during capture; observe it
|
|
9975
|
-
// again"). Right after a turn the CLI repaints for a moment; one refused read left the message unsent for ever.
|
|
9976
|
-
// Read again a few times — reading types nothing — and still hold if it never settles.
|
|
9977
|
-
let captured;
|
|
9978
|
-
for (let attempt = 0; captured === undefined; attempt++) {
|
|
9979
|
-
try {
|
|
9980
|
-
captured = await tmux.capturePane();
|
|
9981
|
-
}
|
|
9982
|
-
catch {
|
|
9983
|
-
if (!current())
|
|
9984
|
-
return false;
|
|
9985
|
-
if (attempt >= 4)
|
|
9986
|
-
return hold("capture_failed");
|
|
9987
|
-
await new Promise(done => setTimeout(done, 150 * (attempt + 1)));
|
|
9988
|
-
if (!current())
|
|
9989
|
-
return false;
|
|
9990
|
-
}
|
|
9991
|
-
}
|
|
9992
|
-
const screen = captured;
|
|
9993
|
-
pane = screen;
|
|
9994
|
-
if (!current())
|
|
9995
|
-
return false;
|
|
9996
|
-
try {
|
|
9997
|
-
if (backend?.getRuntimeDialogs?.().some(dialog => Daemon.dialogMatches(dialog, screen)))
|
|
9998
|
-
return hold("dialog");
|
|
9999
|
-
}
|
|
10000
|
-
catch {
|
|
10001
|
-
return hold("check_failed");
|
|
10002
|
-
}
|
|
10003
|
-
if (!current())
|
|
10004
|
-
return false;
|
|
10005
|
-
}
|
|
10092
|
+
return hold();
|
|
10006
10093
|
if (await this.paneInTmuxMode(phase)) {
|
|
10007
10094
|
if (!current())
|
|
10008
10095
|
return false;
|
|
10009
10096
|
if (performance.now() >= modeDeadline)
|
|
10010
|
-
return hold(
|
|
10097
|
+
return hold();
|
|
10011
10098
|
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
10012
10099
|
if (!current())
|
|
10013
10100
|
return false;
|
|
@@ -10015,143 +10102,54 @@ export class Daemon extends EventEmitter {
|
|
|
10015
10102
|
}
|
|
10016
10103
|
if (!current())
|
|
10017
10104
|
return false;
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
catch {
|
|
10031
|
-
finalPane = null;
|
|
10032
|
-
}
|
|
10033
|
-
if (!current())
|
|
10034
|
-
return false;
|
|
10035
|
-
let dialog;
|
|
10036
|
-
try {
|
|
10037
|
-
dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
|
|
10038
|
-
}
|
|
10039
|
-
catch {
|
|
10040
|
-
dialog = undefined;
|
|
10041
|
-
}
|
|
10042
|
-
if (dialog !== undefined) {
|
|
10043
|
-
onInputHeld("dialog");
|
|
10044
|
-
this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
|
|
10045
|
-
return false;
|
|
10046
|
-
}
|
|
10047
|
-
}
|
|
10048
|
-
if (positiveRefusals && performance.now() >= positiveRetryDeadline)
|
|
10049
|
-
return hold("output_changed");
|
|
10050
|
-
// W1: persist the "Enter may have happened" boundary before tmux receives
|
|
10051
|
-
// the key. A crash after this commit is never auto-replayed without a
|
|
10052
|
-
// positive transcript marker, even if the stale pane still shows composer.
|
|
10053
|
-
if (!enterMarked && durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
|
|
10054
|
-
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
10055
|
-
return false;
|
|
10056
|
-
}
|
|
10057
|
-
enterMarked = true;
|
|
10058
|
-
const sent = await (native && pane !== undefined ? tmux.sendSpecialKey("Enter", undefined, { kind: "delivery", screen: pane, retryCount: positiveRefusals }) : tmux.sendSpecialKey("Enter"));
|
|
10059
|
-
// A busy renderer can change seq after our capture. Only a positive zero-input ACK admits a NEW proof/key;
|
|
10060
|
-
// An Enter retry never re-pastes, retries an uncertain write, reuses stale proof or re-commits the durable boundary.
|
|
10061
|
-
if (!sent && native && tmux.lastInputWasNotSent?.() === true
|
|
10062
|
-
&& tmux.getLastSendSpecialKeyError() === "Pane changed after observation; input was not sent"
|
|
10063
|
-
&& current()) {
|
|
10064
|
-
if (!positiveRefusals)
|
|
10065
|
-
positiveRetryDeadline = Math.min(modeDeadline, performance.now() + 2000);
|
|
10066
|
-
if (positiveRefusals >= 4 || performance.now() >= positiveRetryDeadline)
|
|
10067
|
-
return hold("output_changed");
|
|
10068
|
-
positiveRefusals++;
|
|
10069
|
-
await new Promise(done => setTimeout(done, 100));
|
|
10070
|
-
if (!current())
|
|
10071
|
-
return false;
|
|
10072
|
-
if (performance.now() >= positiveRetryDeadline)
|
|
10073
|
-
return hold("output_changed");
|
|
10074
|
-
continue;
|
|
10075
|
-
}
|
|
10076
|
-
if (!sent) {
|
|
10077
|
-
const details = { phase, tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure" };
|
|
10078
|
-
if (native)
|
|
10079
|
-
this.logger.warn({ ...tmux?.getLastInputDiagnostic?.(), ...details, retryCount: positiveRefusals }, "Native delivery Enter gave up");
|
|
10080
|
-
else
|
|
10081
|
-
this.logger.error(details, "tmux send-keys Enter failed during message delivery");
|
|
10082
|
-
}
|
|
10083
|
-
if (sent && native && current())
|
|
10084
|
-
this.nativeEnterHold = null;
|
|
10085
|
-
return sent;
|
|
10086
|
-
}
|
|
10087
|
-
}
|
|
10088
|
-
/** A pre-existing native busy-paste race: retry bytes only after an exact, owned zero-input acknowledgement. */
|
|
10089
|
-
async sendDeliveryPaste(text, stillCurrent) {
|
|
10090
|
-
const pane = this.tmux, native = this.nativePaneLifecycle;
|
|
10091
|
-
if (!native)
|
|
10092
|
-
return pane.pasteBuffer(text);
|
|
10093
|
-
const backend = this.backend, generation = this.spawnGeneration, fence = this.launchFenceEpoch, windowId = pane.getWindowId();
|
|
10094
|
-
const current = () => !!windowId && this.tmux === pane && this.nativePaneLifecycle === native && this.backend === backend
|
|
10095
|
-
&& this.spawnGeneration === generation && this.launchFenceEpoch === fence && pane.getWindowId() === windowId
|
|
10096
|
-
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
|
|
10097
|
-
let retryCount = 0;
|
|
10098
|
-
const hold = (reason) => {
|
|
10099
|
-
if (current() && !this.nativeDeliveryHold())
|
|
10100
|
-
this.nativeEnterHold = { key: `${this.bootId}:${generation}:${fence}`, since: Date.now(), spawn: generation, fence };
|
|
10101
|
-
this.logger.warn({ ...pane.getLastInputDiagnostic?.(), reason, retryCount }, "Native delivery paste held by its safety check");
|
|
10102
|
-
return false;
|
|
10103
|
-
};
|
|
10104
|
-
const deadline = performance.now() + 2000;
|
|
10105
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
10106
|
-
retryCount = attempt;
|
|
10107
|
-
if (!current())
|
|
10108
|
-
return false;
|
|
10109
|
-
if (performance.now() >= deadline)
|
|
10110
|
-
return hold("output_changed");
|
|
10111
|
-
let screen;
|
|
10112
|
-
for (let read = 0; read < 5 && screen === undefined; read++) {
|
|
10113
|
-
try {
|
|
10114
|
-
screen = await pane.capturePane();
|
|
10115
|
-
}
|
|
10116
|
-
catch { /* Reads do not construct an input operation. */ }
|
|
10117
|
-
if (!current())
|
|
10118
|
-
return false;
|
|
10119
|
-
if (performance.now() >= deadline)
|
|
10120
|
-
return hold("capture_failed");
|
|
10121
|
-
if (screen === undefined && read < 4)
|
|
10122
|
-
await new Promise(done => setTimeout(done, 100));
|
|
10123
|
-
}
|
|
10124
|
-
if (screen === undefined)
|
|
10125
|
-
return hold("capture_failed");
|
|
10105
|
+
break;
|
|
10106
|
+
}
|
|
10107
|
+
// M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468): a dialog the backend RECOGNISES
|
|
10108
|
+
// (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the paste and this Enter
|
|
10109
|
+
// would take the key as its answer. Read the pane once more: such a dialog withholds the Enter, the caller keeps the
|
|
10110
|
+
// delivery uncertain (Needs-you), and the dialog scanner parks the dialog for a person. Nothing else is judged here
|
|
10111
|
+
// — an unknown or unreadable screen sends, exactly as before: #2958's composer proof (decision f57474aa) is NOT back.
|
|
10112
|
+
// Only a caller that can hold (onInputHeld: the delivery's initial submit, settled uncertain → Needs-you) asks: one
|
|
10113
|
+
// that cannot would read `false` as a failure and paste again. Only the backend's OWN dialogs count — not AgEnD's
|
|
10114
|
+
// unrecognised-prompt guess — so an unknown screen still sends.
|
|
10115
|
+
if (onInputHeld) {
|
|
10116
|
+
let finalPane = null;
|
|
10126
10117
|
try {
|
|
10127
|
-
|
|
10128
|
-
return hold("dialog");
|
|
10118
|
+
finalPane = await tmux.capturePane();
|
|
10129
10119
|
}
|
|
10130
10120
|
catch {
|
|
10131
|
-
|
|
10121
|
+
finalPane = null;
|
|
10132
10122
|
}
|
|
10133
10123
|
if (!current())
|
|
10134
10124
|
return false;
|
|
10135
|
-
|
|
10136
|
-
return hold("output_changed");
|
|
10137
|
-
let sent;
|
|
10125
|
+
let dialog;
|
|
10138
10126
|
try {
|
|
10139
|
-
|
|
10127
|
+
dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
|
|
10140
10128
|
}
|
|
10141
10129
|
catch {
|
|
10142
|
-
|
|
10130
|
+
dialog = undefined;
|
|
10143
10131
|
}
|
|
10144
|
-
if (
|
|
10145
|
-
|
|
10146
|
-
|
|
10132
|
+
if (dialog !== undefined) {
|
|
10133
|
+
onInputHeld?.("dialog");
|
|
10134
|
+
this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
|
|
10147
10135
|
return false;
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10136
|
+
}
|
|
10137
|
+
}
|
|
10138
|
+
// W1: persist the "Enter may have happened" boundary before tmux receives
|
|
10139
|
+
// the key. A crash after this commit is never auto-replayed without a
|
|
10140
|
+
// positive transcript marker, even if the stale pane still shows composer.
|
|
10141
|
+
if (durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
|
|
10142
|
+
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
10143
|
+
return false;
|
|
10153
10144
|
}
|
|
10154
|
-
|
|
10145
|
+
const sent = await tmux.sendSpecialKey("Enter");
|
|
10146
|
+
if (!sent) {
|
|
10147
|
+
this.logger.error({
|
|
10148
|
+
phase,
|
|
10149
|
+
tmuxError: this.tmux.getLastSendSpecialKeyError() ?? "unknown tmux send-keys failure",
|
|
10150
|
+
}, "tmux send-keys Enter failed during message delivery");
|
|
10151
|
+
}
|
|
10152
|
+
return sent;
|
|
10155
10153
|
}
|
|
10156
10154
|
async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
|
|
10157
10155
|
// Last and defaulted so the positional callers that ignore the outcome stay
|
|
@@ -10189,9 +10187,6 @@ export class Daemon extends EventEmitter {
|
|
|
10189
10187
|
const tmuxAtWrite = this.tmux;
|
|
10190
10188
|
const steerCurrent = () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping
|
|
10191
10189
|
&& this.tmux === tmuxAtWrite && this.spawnGeneration === spawnAtWrite;
|
|
10192
|
-
// Alpha (S-36, Prism #2989 P2): a NATIVE pane keeps #2958's pre-paste owner fence through the initial Enter and every
|
|
10193
|
-
// retry Enter (a pane replaced while the paste awaits never takes this delivery's Enter); a tmux pane follows main.
|
|
10194
|
-
const nativeOwner = this.nativePaneLifecycle ? steerCurrent : undefined;
|
|
10195
10190
|
// Bug A: paste with backoff. Transient failures are usually a stale window id
|
|
10196
10191
|
// after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
|
|
10197
10192
|
const maxAttempts = 3;
|
|
@@ -10259,8 +10254,8 @@ export class Daemon extends EventEmitter {
|
|
|
10259
10254
|
}
|
|
10260
10255
|
verdict.durableBeginCommitted = true;
|
|
10261
10256
|
verdict.submissionMode = attemptEvidence.submissionMode;
|
|
10262
|
-
if (attemptEvidence.
|
|
10263
|
-
verdict.transcriptCheckpoint =
|
|
10257
|
+
if (attemptEvidence.transcriptPath && attemptEvidence.transcriptOffset !== null) {
|
|
10258
|
+
verdict.transcriptCheckpoint = {
|
|
10264
10259
|
backend: attemptEvidence.backend,
|
|
10265
10260
|
path: attemptEvidence.transcriptPath,
|
|
10266
10261
|
offset: attemptEvidence.transcriptOffset,
|
|
@@ -10274,6 +10269,17 @@ export class Daemon extends EventEmitter {
|
|
|
10274
10269
|
}
|
|
10275
10270
|
}
|
|
10276
10271
|
}
|
|
10272
|
+
// #3030: a hand-off into a busy CLI's own queue is on record before its paste (write-ahead): that queue lives only
|
|
10273
|
+
// in the CLI process, and a stop before a turn takes it would lose it without a trace.
|
|
10274
|
+
if (verdict.trackHandoff && !rawPaste) {
|
|
10275
|
+
await this.recordQueuedHandoff(formatted, handingOffToNativeQueue, verdict, durableAttempt, humanVerificationId);
|
|
10276
|
+
if (stillCurrent && !stillCurrent()) {
|
|
10277
|
+
if (!verdict.trackHandoff.id)
|
|
10278
|
+
this.dropQueuedHandoff(verdict);
|
|
10279
|
+
verdict.fenced = !verdict.durableBeginCommitted;
|
|
10280
|
+
return false;
|
|
10281
|
+
}
|
|
10282
|
+
}
|
|
10277
10283
|
if (lookBeforePaste) {
|
|
10278
10284
|
await lookBeforePaste();
|
|
10279
10285
|
// The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
|
|
@@ -10325,15 +10331,7 @@ export class Daemon extends EventEmitter {
|
|
|
10325
10331
|
}
|
|
10326
10332
|
const pasteGeneration = this.spawnGeneration;
|
|
10327
10333
|
beforePaste?.();
|
|
10328
|
-
const pasted = await this.
|
|
10329
|
-
if (!pasted && this.nativePaneLifecycle) {
|
|
10330
|
-
// The native write attempt is retained as uncertain, including exhausted positive refusals. Never route an
|
|
10331
|
-
// unknown/partial write through legacy window recovery or abort it back to an automatically replayable row.
|
|
10332
|
-
verdict.paneWriteStarted = true;
|
|
10333
|
-
verdict.phase = "paste";
|
|
10334
|
-
verdict.proof = "native-paste-held";
|
|
10335
|
-
return false;
|
|
10336
|
-
}
|
|
10334
|
+
const pasted = await this.tmux.pasteBuffer(formatted);
|
|
10337
10335
|
if (pasted)
|
|
10338
10336
|
this.rememberPaste(formatted, pasteGeneration);
|
|
10339
10337
|
if (!pasted) {
|
|
@@ -10420,18 +10418,15 @@ export class Daemon extends EventEmitter {
|
|
|
10420
10418
|
const enterMono = performance.now();
|
|
10421
10419
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
10422
10420
|
let inputHeld = null;
|
|
10423
|
-
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof
|
|
10421
|
+
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
|
|
10424
10422
|
if (inputHeld) {
|
|
10425
10423
|
// #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
|
|
10426
10424
|
// the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
|
|
10427
10425
|
verdict.phase = "submit-enter";
|
|
10428
|
-
|
|
10429
|
-
// and each native safety check say which.
|
|
10430
|
-
verdict.proof = inputHeld === "pane-in-mode" ? "pane-in-mode-held" : nativeOwner ? `native-input-${inputHeld}`
|
|
10431
|
-
: inputHeld === "dialog" ? "dialog-on-pane" : `input-held-${inputHeld}`;
|
|
10426
|
+
verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
|
|
10432
10427
|
return false;
|
|
10433
10428
|
}
|
|
10434
|
-
if (
|
|
10429
|
+
if (steerComposerProof && !steerCurrent()) {
|
|
10435
10430
|
verdict.phase = "submit-enter";
|
|
10436
10431
|
verdict.proof = "fenced-before-enter";
|
|
10437
10432
|
return false;
|
|
@@ -10440,6 +10435,7 @@ export class Daemon extends EventEmitter {
|
|
|
10440
10435
|
return this.failDelivery(verdict, status, "submit-enter", "pre-enter-deadline");
|
|
10441
10436
|
return this.failDelivery(verdict, status, "submit-enter", "tmux-send-keys-failed");
|
|
10442
10437
|
}
|
|
10438
|
+
this.commitPreInputQuotaHistory(pasteBaseline);
|
|
10443
10439
|
// The proof's whole budget, from the Enter: every capture and dialog probe inside it, a late answer dropped.
|
|
10444
10440
|
const steerProofDeadline = performance.now() + NATIVE_QUEUE_PASTE_VERIFY_MS + POST_ENTER_PROOF_WINDOW_MS;
|
|
10445
10441
|
// Kiro's legacy TUI can swallow Enter while it is still processing a large
|
|
@@ -10460,7 +10456,7 @@ export class Daemon extends EventEmitter {
|
|
|
10460
10456
|
if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
|
|
10461
10457
|
await new Promise(r => setTimeout(r, 1_000));
|
|
10462
10458
|
const retryAt = Date.now();
|
|
10463
|
-
if (await this.sendDeliveryEnter("queue-less-defensive-retry"
|
|
10459
|
+
if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
|
|
10464
10460
|
enterAt = retryAt;
|
|
10465
10461
|
enterRetry = true;
|
|
10466
10462
|
this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
|
|
@@ -10521,8 +10517,9 @@ export class Daemon extends EventEmitter {
|
|
|
10521
10517
|
}
|
|
10522
10518
|
const proof = await this.confirmSubmitted(signature, pasteBaseline);
|
|
10523
10519
|
if (proof === "submitted") {
|
|
10524
|
-
|
|
10525
|
-
|
|
10520
|
+
// ✅ native queue accepted — unless it is on record (#3030): then ✅ waits for the turn that takes it.
|
|
10521
|
+
if (status && !verdict.handoff)
|
|
10522
|
+
this.emit("message_confirmed", status);
|
|
10526
10523
|
return true;
|
|
10527
10524
|
}
|
|
10528
10525
|
if (proof === "unverifiable") {
|
|
@@ -10554,8 +10551,8 @@ export class Daemon extends EventEmitter {
|
|
|
10554
10551
|
this.logger.warn("Paste reached the pane but could not be verified as submitted — accepting without proof");
|
|
10555
10552
|
verdict.phase = "best-effort-submission";
|
|
10556
10553
|
verdict.proof = "unverified";
|
|
10557
|
-
if (status)
|
|
10558
|
-
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10554
|
+
if (status && !verdict.handoff)
|
|
10555
|
+
this.emit("message_confirmed", status); // ✅ (best-effort; on record: when taken, #3030)
|
|
10559
10556
|
return true;
|
|
10560
10557
|
}
|
|
10561
10558
|
// raw_paste has no unique marker and is not allowed to re-paste or
|
|
@@ -10570,6 +10567,10 @@ export class Daemon extends EventEmitter {
|
|
|
10570
10567
|
// confirmation ladder. "stranded" is the case that used to be a silent
|
|
10571
10568
|
// ✅ — the text was visible, so the old check called it delivered.
|
|
10572
10569
|
this.logger.warn({ proof }, "Native-queue paste not confirmed as submitted — retrying via idle-gated delivery");
|
|
10570
|
+
// #3030: what follows is an idle submission (or a failure the caller reports): no longer a queued hand-off.
|
|
10571
|
+
if (verdict.handoff && !verdict.trackHandoff?.id)
|
|
10572
|
+
this.dropQueuedHandoff(verdict);
|
|
10573
|
+
verdict.handoff = undefined;
|
|
10573
10574
|
if (windowId && this.controlClient && !(await this.controlClient.waitUntilIdle(windowId))) {
|
|
10574
10575
|
verdict.phase = "native-queue-idle-wait";
|
|
10575
10576
|
verdict.proof = "idle-wait-failed";
|
|
@@ -10593,7 +10594,7 @@ export class Daemon extends EventEmitter {
|
|
|
10593
10594
|
// again. The prompt is back now, so an Enter can land.
|
|
10594
10595
|
this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
|
|
10595
10596
|
const strandedAt = Date.now();
|
|
10596
|
-
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"
|
|
10597
|
+
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
|
|
10597
10598
|
return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
|
|
10598
10599
|
}
|
|
10599
10600
|
// The recovery Enter can be accepted before Codex paints its new
|
|
@@ -10646,7 +10647,7 @@ export class Daemon extends EventEmitter {
|
|
|
10646
10647
|
}
|
|
10647
10648
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
10648
10649
|
const retryAt = Date.now();
|
|
10649
|
-
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"
|
|
10650
|
+
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
|
|
10650
10651
|
return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
|
|
10651
10652
|
}
|
|
10652
10653
|
if (windowId && this.controlClient) {
|
|
@@ -10698,7 +10699,7 @@ export class Daemon extends EventEmitter {
|
|
|
10698
10699
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
10699
10700
|
const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
|
|
10700
10701
|
const retryAt = Date.now();
|
|
10701
|
-
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"
|
|
10702
|
+
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
|
|
10702
10703
|
return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
|
|
10703
10704
|
}
|
|
10704
10705
|
submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
|
|
@@ -10786,7 +10787,7 @@ export class Daemon extends EventEmitter {
|
|
|
10786
10787
|
}
|
|
10787
10788
|
// No control client to observe output: fall back to the legacy double-Enter.
|
|
10788
10789
|
await new Promise(r => setTimeout(r, 1000));
|
|
10789
|
-
await this.sendDeliveryEnter("unobserved-defensive-retry"
|
|
10790
|
+
await this.sendDeliveryEnter("unobserved-defensive-retry");
|
|
10790
10791
|
if (status)
|
|
10791
10792
|
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10792
10793
|
}
|
|
@@ -11006,22 +11007,14 @@ export class Daemon extends EventEmitter {
|
|
|
11006
11007
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
11007
11008
|
*/
|
|
11008
11009
|
transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
|
|
11009
|
-
if (this.nativePaneLifecycle && !checkpoint.runtime)
|
|
11010
|
-
throw Error("Native proof cannot read a coordinator-local transcript path");
|
|
11011
11010
|
const launchAtWrite = this.launchFenceEpoch;
|
|
11012
11011
|
return {
|
|
11013
|
-
reader:
|
|
11012
|
+
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
11014
11013
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
11015
|
-
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true)
|
|
11016
|
-
&& (!checkpoint.runtime || this.nativePaneLifecycle?.current(checkpoint.runtime) === true),
|
|
11014
|
+
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
|
|
11017
11015
|
onProof,
|
|
11018
11016
|
};
|
|
11019
11017
|
}
|
|
11020
|
-
nativeTranscriptReader(reference, needle) {
|
|
11021
|
-
if (!this.nativePaneLifecycle || !this.nativePaneLifecycle.current(reference))
|
|
11022
|
-
throw Error("Native delivery transcript owner is unavailable");
|
|
11023
|
-
return this.nativePaneLifecycle.proofReader(reference, needle);
|
|
11024
|
-
}
|
|
11025
11018
|
/** The CLI this daemon drives, for log lines shared by every backend ("claude-code", "codex", …). */
|
|
11026
11019
|
cliLabel() {
|
|
11027
11020
|
return this.config.backend ?? this.backend?.binaryName ?? "CLI";
|
|
@@ -11161,12 +11154,18 @@ export class Daemon extends EventEmitter {
|
|
|
11161
11154
|
async capturePaneEvidence(signature) {
|
|
11162
11155
|
if (!this.tmux)
|
|
11163
11156
|
return null;
|
|
11157
|
+
const owner = this.tmux, window = owner.getWindowId?.(), spawn = this.spawnGeneration, fence = this.launchFenceEpoch, launch = this.launchAttempt;
|
|
11164
11158
|
// Bounded re-probe: a capture that fails here costs the delivery its only
|
|
11165
11159
|
// "before" picture, and a momentary failure should not decide anything.
|
|
11166
11160
|
for (let attempt = 1; attempt <= BASELINE_CAPTURE_ATTEMPTS; attempt++) {
|
|
11167
11161
|
try {
|
|
11168
11162
|
const pane = await this.tmux.capturePane();
|
|
11169
11163
|
const evidence = this.paneEvidence(pane, signature);
|
|
11164
|
+
if (owner === this.tmux && window === owner.getWindowId?.() && spawn === this.spawnGeneration && fence === this.launchFenceEpoch && launch === this.launchAttempt) {
|
|
11165
|
+
const counts = this.paneQuotaPatterns().map(ep => ({ key: Daemon.errorPatternKey(ep), count: Daemon.countOccurrences(ep.pattern, pane) }));
|
|
11166
|
+
if (counts.length)
|
|
11167
|
+
evidence.quotaHistory = { spawn, fence, attempt: launch, tmux: owner, window, counts, marker: signature.value, occurrences: evidence.payload };
|
|
11168
|
+
}
|
|
11170
11169
|
const input = this.inputRegion(pane);
|
|
11171
11170
|
if (input && agendMessageInInput(input.text)) {
|
|
11172
11171
|
// Whatever we paste now lands after it, and one Enter submits both as
|
|
@@ -11265,14 +11264,7 @@ export class Daemon extends EventEmitter {
|
|
|
11265
11264
|
* (the retries included). Without a guard it is exactly the old unconditional path.
|
|
11266
11265
|
*/
|
|
11267
11266
|
async submitSystemPaste(text, label, guard, admission) {
|
|
11268
|
-
|
|
11269
|
-
// (#2958's fence, reverted on main for tmux panes) — a pane replaced while the paste awaits never takes its Enter.
|
|
11270
|
-
const native = !!this.nativePaneLifecycle;
|
|
11271
|
-
const ownerTmux = this.tmux, ownerGeneration = this.spawnGeneration, ownerFence = this.launchFenceEpoch;
|
|
11272
|
-
const ownerWindow = native ? ownerTmux?.getWindowId() : undefined;
|
|
11273
|
-
const owner = () => !native || (!!ownerTmux && !!ownerWindow && this.tmux === ownerTmux && ownerTmux.getWindowId() === ownerWindow
|
|
11274
|
-
&& this.spawnGeneration === ownerGeneration && this.launchFenceEpoch === ownerFence && !this.deliveryWritesStopping);
|
|
11275
|
-
const current = guard || admission || native ? () => { admission?.(); return owner() && (guard?.current() ?? true); } : undefined;
|
|
11267
|
+
const current = guard || admission ? () => { admission?.(); return guard?.current() ?? true; } : undefined;
|
|
11276
11268
|
if (current && !current())
|
|
11277
11269
|
return false;
|
|
11278
11270
|
if (!this.tmux)
|
|
@@ -11308,6 +11300,7 @@ export class Daemon extends EventEmitter {
|
|
|
11308
11300
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
11309
11301
|
if (!(await this.sendDeliveryEnter(label, current)))
|
|
11310
11302
|
return false;
|
|
11303
|
+
this.commitPreInputQuotaHistory(baseline);
|
|
11311
11304
|
let proof = await this.confirmSubmitted(signature, baseline);
|
|
11312
11305
|
if (current && !current())
|
|
11313
11306
|
return false;
|
|
@@ -11392,13 +11385,13 @@ export class Daemon extends EventEmitter {
|
|
|
11392
11385
|
&& sameInteractionOwner(recoveryOwner, this.interactionOwner());
|
|
11393
11386
|
const previousWindowId = this.tmux?.getWindowId();
|
|
11394
11387
|
try {
|
|
11395
|
-
const windows = await
|
|
11388
|
+
const windows = await TmuxManager.listWindows(this.tmuxSessionName, this.controlClient);
|
|
11396
11389
|
if (!recoveryCurrent() || this.tmux !== previousTmux)
|
|
11397
11390
|
return undefined;
|
|
11398
11391
|
const match = windows.find(w => w.name === this.name);
|
|
11399
11392
|
if (!match)
|
|
11400
11393
|
return undefined;
|
|
11401
|
-
this.tmux =
|
|
11394
|
+
this.tmux = new TmuxManager(this.tmuxSessionName, match.id, resolveTmuxLogicalSize(this.config.terminal), this.controlClient);
|
|
11402
11395
|
const recoveredTmux = this.tmux;
|
|
11403
11396
|
writeFileSync(join(this.instanceDir, "window-id"), match.id);
|
|
11404
11397
|
// The window we were talking to is gone; leaving it registered means the
|
|
@@ -11461,8 +11454,6 @@ export class Daemon extends EventEmitter {
|
|
|
11461
11454
|
return false;
|
|
11462
11455
|
}
|
|
11463
11456
|
getWindowId() {
|
|
11464
|
-
if (this.nativePaneLifecycle)
|
|
11465
|
-
return this.nativePaneLifecycle.current() ? this.tmux?.getWindowId() : undefined;
|
|
11466
11457
|
try {
|
|
11467
11458
|
return readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() || undefined;
|
|
11468
11459
|
}
|
|
@@ -11494,55 +11485,7 @@ export class Daemon extends EventEmitter {
|
|
|
11494
11485
|
* Handle a tool call from the MCP server (forwarded by Claude).
|
|
11495
11486
|
* Routes to the channel adapter via MessageBus.
|
|
11496
11487
|
*/
|
|
11497
|
-
|
|
11498
|
-
handlePaneToolCall(input, lease) {
|
|
11499
|
-
const refusal = nativePaneToolRefusal(input.tool, input.args);
|
|
11500
|
-
if (refusal)
|
|
11501
|
-
return Promise.resolve({ result: null, error: refusal });
|
|
11502
|
-
if (!lease.current() || !lease.allowed(input.tool, input.args) || this.deliveryWritesStopping)
|
|
11503
|
-
return Promise.resolve({ result: null, error: "Pane tool permission is no longer current" });
|
|
11504
|
-
const fence = this.launchFenceEpoch;
|
|
11505
|
-
const current = () => lease.current() && lease.allowed(input.tool, input.args) && !this.deliveryWritesStopping && fence === this.launchFenceEpoch;
|
|
11506
|
-
return new Promise(resolve => {
|
|
11507
|
-
let settled = false;
|
|
11508
|
-
const finish = (value) => { if (settled)
|
|
11509
|
-
return; settled = true; clearTimeout(timer); resolve(value); };
|
|
11510
|
-
const timer = setTimeout(() => finish({ result: null, error: "Pane tool result is unconfirmed; inspect its operation before retrying" }), daemonBudgetMs(input.tool) + 1000);
|
|
11511
|
-
timer.unref?.();
|
|
11512
|
-
try {
|
|
11513
|
-
this.handleToolCall({ ...input, requestId: ++this.fleetRequestSeq }, undefined, {
|
|
11514
|
-
current,
|
|
11515
|
-
dispatch: (tool, args, respond) => {
|
|
11516
|
-
if (!current()) {
|
|
11517
|
-
respond(null, "Pane tool permission was revoked before dispatch");
|
|
11518
|
-
return true;
|
|
11519
|
-
}
|
|
11520
|
-
const dispatched = lease.dispatch?.(tool, args);
|
|
11521
|
-
if (!dispatched)
|
|
11522
|
-
return false;
|
|
11523
|
-
void dispatched.then(result => respond(result), () => respond(null, "Pane tool operation was refused"));
|
|
11524
|
-
return true;
|
|
11525
|
-
},
|
|
11526
|
-
// Resolving the relay is not proof that its HTTP response reached the provider. No response-written receipt yet.
|
|
11527
|
-
send: value => {
|
|
11528
|
-
finish({ result: value.result, ...(typeof value.error === "string" ? { error: value.error } : {}),
|
|
11529
|
-
...(typeof value.errorCode === "string" || typeof value.errorCode === "number" ? { errorCode: value.errorCode } : {}) });
|
|
11530
|
-
return false;
|
|
11531
|
-
},
|
|
11532
|
-
});
|
|
11533
|
-
}
|
|
11534
|
-
catch {
|
|
11535
|
-
finish({ result: null, error: "Pane tool operation failed" });
|
|
11536
|
-
}
|
|
11537
|
-
});
|
|
11538
|
-
}
|
|
11539
|
-
/** Only the authority lease with a verified helper stdout ACK calls this internal barrier. */
|
|
11540
|
-
paneToolResponseWritten(operationId, current) {
|
|
11541
|
-
if (!current() || this.deliveryWritesStopping || !/^[a-f0-9-]{36}$/i.test(operationId))
|
|
11542
|
-
return;
|
|
11543
|
-
this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
|
|
11544
|
-
}
|
|
11545
|
-
handleToolCall(msg, socket, peer) {
|
|
11488
|
+
handleToolCall(msg, socket) {
|
|
11546
11489
|
const tool = msg.tool;
|
|
11547
11490
|
const args = (msg.args ?? {});
|
|
11548
11491
|
const requestId = msg.requestId;
|
|
@@ -11550,20 +11493,20 @@ export class Daemon extends EventEmitter {
|
|
|
11550
11493
|
this.logger.debug({ tool, requestId }, "Tool call from MCP server");
|
|
11551
11494
|
// A tool call can only come from a live MCP server of ours — proof of life
|
|
11552
11495
|
// that outranks whatever the pid slot says.
|
|
11553
|
-
if (
|
|
11554
|
-
this.noteMcpProofOfLife("tool_call",
|
|
11496
|
+
if (this.socketSessionNames.get(socket) === this.name) {
|
|
11497
|
+
this.noteMcpProofOfLife("tool_call", this.socketPids.get(socket));
|
|
11555
11498
|
}
|
|
11556
11499
|
// A tool invocation is not delivery evidence. Capture which obligation it
|
|
11557
11500
|
// belongs to now, then settle it only after the adapter/fleet responds.
|
|
11558
11501
|
// Explicitly mapped sibling sessions must not satisfy this instance's turn;
|
|
11559
11502
|
// an unmapped socket is retained for legacy/tests where mcp_ready was not
|
|
11560
11503
|
// observed before the first tool call.
|
|
11561
|
-
const sourceSession =
|
|
11504
|
+
const sourceSession = this.socketSessionNames.get(socket);
|
|
11562
11505
|
const replyAttempt = TURN_OUTBOUND_TOOLS.has(tool) && (!sourceSession || sourceSession === this.name)
|
|
11563
11506
|
? this.turnReplyGuard.beginToolAttempt(tool === "reply", TURN_COMPLETION_TOOLS.has(tool))
|
|
11564
11507
|
: null;
|
|
11565
11508
|
// For now, log and respond. Full adapter routing will be wired in fleet manager.
|
|
11566
|
-
const respond = (result, error
|
|
11509
|
+
const respond = (result, error) => {
|
|
11567
11510
|
// A message that verifiably went out stands down the dead-MCP proxy reply
|
|
11568
11511
|
// for this turn: the agent proved it can still speak for itself.
|
|
11569
11512
|
if (!error && result != null && TURN_OUTBOUND_TOOLS.has(tool)) {
|
|
@@ -11604,21 +11547,14 @@ export class Daemon extends EventEmitter {
|
|
|
11604
11547
|
// failed" and report unknown immediately rather than starting recovery.
|
|
11605
11548
|
this.turnReplyGuard.settleToolAttempt(replyAttempt, false);
|
|
11606
11549
|
}
|
|
11607
|
-
const
|
|
11608
|
-
const sent = peer ? peer.current() && peer.send(packet) : socket ? this.ipcServer?.send(socket, packet) ?? false : false;
|
|
11550
|
+
const sent = this.ipcServer?.send(socket, { requestId, result, error, operationId }) ?? false;
|
|
11609
11551
|
if (!sent) {
|
|
11610
|
-
|
|
11611
|
-
this.logger.debug({ tool, requestId }, "Native MCP result awaits transport-written confirmation");
|
|
11612
|
-
else
|
|
11613
|
-
this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
|
|
11552
|
+
this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
|
|
11614
11553
|
}
|
|
11615
11554
|
else if (operationId) {
|
|
11616
11555
|
this.emit("mcp_tool_response_written", { operationId, daemonBootId: this.bootId });
|
|
11617
11556
|
}
|
|
11618
11557
|
};
|
|
11619
|
-
// Native Fleet/task operations carry their lease to the existing dispatcher mutation admission points.
|
|
11620
|
-
if (peer?.dispatch?.(tool, args, respond))
|
|
11621
|
-
return;
|
|
11622
11558
|
// Repo checkout — handled locally in daemon (no fleet-manager)
|
|
11623
11559
|
if (tool === "checkout_repo") {
|
|
11624
11560
|
this.handleCheckoutRepo(args, respond);
|
|
@@ -11647,7 +11583,7 @@ export class Daemon extends EventEmitter {
|
|
|
11647
11583
|
clearTimeout(timeout);
|
|
11648
11584
|
respond(respMsg.result, respMsg.error);
|
|
11649
11585
|
});
|
|
11650
|
-
this.
|
|
11586
|
+
this.ipcServer?.broadcast({
|
|
11651
11587
|
type,
|
|
11652
11588
|
payload: args,
|
|
11653
11589
|
meta: { instance_name: this.name },
|
|
@@ -11673,7 +11609,7 @@ export class Daemon extends EventEmitter {
|
|
|
11673
11609
|
clearTimeout(timeout);
|
|
11674
11610
|
resolve({ result: respMsg.result, error: respMsg.error });
|
|
11675
11611
|
});
|
|
11676
|
-
this.
|
|
11612
|
+
this.ipcServer?.broadcast({ type: WEB_THEME_IPC[tool], payload, meta: { instance_name: this.name }, fleetRequestId: fleetReqId });
|
|
11677
11613
|
});
|
|
11678
11614
|
void (async () => {
|
|
11679
11615
|
if (tool === "preview_web_appearance") {
|
|
@@ -11750,7 +11686,7 @@ export class Daemon extends EventEmitter {
|
|
|
11750
11686
|
clearTimeout(timeout);
|
|
11751
11687
|
respond(respMsg.result, respMsg.error);
|
|
11752
11688
|
});
|
|
11753
|
-
this.
|
|
11689
|
+
this.ipcServer?.broadcast({
|
|
11754
11690
|
type: "fleet_task",
|
|
11755
11691
|
payload: args,
|
|
11756
11692
|
meta: { instance_name: this.name },
|
|
@@ -11773,7 +11709,7 @@ export class Daemon extends EventEmitter {
|
|
|
11773
11709
|
clearTimeout(timeout);
|
|
11774
11710
|
respond(respMsg.result, respMsg.error);
|
|
11775
11711
|
});
|
|
11776
|
-
this.
|
|
11712
|
+
this.ipcServer?.broadcast({
|
|
11777
11713
|
type: typeMap[tool],
|
|
11778
11714
|
payload: args,
|
|
11779
11715
|
meta: { instance_name: this.name, working_directory: this.config.working_directory },
|
|
@@ -11800,7 +11736,7 @@ export class Daemon extends EventEmitter {
|
|
|
11800
11736
|
clearTimeout(timeout);
|
|
11801
11737
|
respond(respMsg.result, respMsg.error);
|
|
11802
11738
|
});
|
|
11803
|
-
this.
|
|
11739
|
+
this.ipcServer?.broadcast({
|
|
11804
11740
|
type: typeMap[tool],
|
|
11805
11741
|
payload: args,
|
|
11806
11742
|
meta: {
|
|
@@ -11815,11 +11751,11 @@ export class Daemon extends EventEmitter {
|
|
|
11815
11751
|
}
|
|
11816
11752
|
if (CROSS_INSTANCE_TOOLS.has(tool)) {
|
|
11817
11753
|
// Route to fleet manager via IPC (topic mode only)
|
|
11818
|
-
if (this.topicMode && this.
|
|
11754
|
+
if (this.topicMode && this.ipcServer) {
|
|
11819
11755
|
// Use fleetRequestId (not requestId) to avoid MCP server resolving the
|
|
11820
11756
|
// pending tool call prematurely when it receives the broadcast.
|
|
11821
11757
|
const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
|
|
11822
|
-
const senderSessionName =
|
|
11758
|
+
const senderSessionName = this.socketSessionNames.get(socket);
|
|
11823
11759
|
// Shared table, so delete_instance and the deployment tools get the same
|
|
11824
11760
|
// budget here as the MCP client expects (the ad-hoc list omitted them).
|
|
11825
11761
|
const crossTimeoutMs = daemonBudgetMs(tool);
|
|
@@ -11831,7 +11767,7 @@ export class Daemon extends EventEmitter {
|
|
|
11831
11767
|
clearTimeout(timeout);
|
|
11832
11768
|
respond(respMsg.result, respMsg.error);
|
|
11833
11769
|
});
|
|
11834
|
-
this.
|
|
11770
|
+
this.ipcServer.broadcast({
|
|
11835
11771
|
type: "fleet_outbound",
|
|
11836
11772
|
tool,
|
|
11837
11773
|
args,
|
|
@@ -11885,12 +11821,12 @@ export class Daemon extends EventEmitter {
|
|
|
11885
11821
|
}, daemonBudgetMs(tool));
|
|
11886
11822
|
this.pendingIpcRequests.set(outboundKey, (respMsg) => {
|
|
11887
11823
|
clearTimeout(timeout);
|
|
11888
|
-
respond(respMsg.result, respMsg.error
|
|
11824
|
+
respond(respMsg.result, respMsg.error);
|
|
11889
11825
|
});
|
|
11890
11826
|
// Register the waiter before broadcasting. A fast/mock adapter can settle
|
|
11891
11827
|
// immediately; broadcasting first left a response-before-registration
|
|
11892
11828
|
// window that degraded into a timeout despite a completed POST.
|
|
11893
|
-
this.
|
|
11829
|
+
this.ipcServer?.broadcast({
|
|
11894
11830
|
type: "fleet_outbound",
|
|
11895
11831
|
tool,
|
|
11896
11832
|
args,
|
|
@@ -12021,7 +11957,7 @@ export class Daemon extends EventEmitter {
|
|
|
12021
11957
|
// The daemon used to read `general_topic` alone while `isGeneralInstance`
|
|
12022
11958
|
// also accepted the name, so an instance called `general` without the flag
|
|
12023
11959
|
// was a worker here and a general everywhere else.
|
|
12024
|
-
const isGeneral = this.config.general_topic === true ||
|
|
11960
|
+
const isGeneral = this.config.general_topic === true || this.name === "general";
|
|
12025
11961
|
// No third argument for an ordinary instance: the default is `worker` now,
|
|
12026
11962
|
// and naming it here would be a second place to change it.
|
|
12027
11963
|
const toolSet = isGeneral
|
|
@@ -12183,6 +12119,8 @@ export class Daemon extends EventEmitter {
|
|
|
12183
12119
|
}
|
|
12184
12120
|
this.cancelCapacityNudge("the instance was stopped, paused or respawned");
|
|
12185
12121
|
this.lastErrorEpisode = null;
|
|
12122
|
+
this.quotaReplay = null;
|
|
12123
|
+
this.quotaInput = null;
|
|
12186
12124
|
this.nudgeBaselines = null;
|
|
12187
12125
|
// A relaunch settles any outstanding quit watch: the CLI came back.
|
|
12188
12126
|
this.clearQuitRelaunchWatch();
|
|
@@ -12397,8 +12335,6 @@ export class Daemon extends EventEmitter {
|
|
|
12397
12335
|
catch { /* best effort */ }
|
|
12398
12336
|
}
|
|
12399
12337
|
async spawnClaudeWindow() {
|
|
12400
|
-
if (this.nativePaneLifecycle)
|
|
12401
|
-
throw Error("Native panes never enter local resume/fresh fallback");
|
|
12402
12338
|
this.assertSessionOomAdmission();
|
|
12403
12339
|
this.beginSpawn();
|
|
12404
12340
|
let resumedSuccessfully = false;
|
|
@@ -12985,14 +12921,6 @@ export class Daemon extends EventEmitter {
|
|
|
12985
12921
|
await this.museUsageRelay.stop().catch(() => { });
|
|
12986
12922
|
this.museUsageRelay = null;
|
|
12987
12923
|
}
|
|
12988
|
-
if (this.nativePaneLifecycle) {
|
|
12989
|
-
await this.nativePaneLifecycle.release("detach");
|
|
12990
|
-
try {
|
|
12991
|
-
unlinkSync(join(this.instanceDir, "daemon.pid"));
|
|
12992
|
-
}
|
|
12993
|
-
catch { /* metadata only */ }
|
|
12994
|
-
return;
|
|
12995
|
-
}
|
|
12996
12924
|
try {
|
|
12997
12925
|
await this.killProcessTree("SIGTERM", "startup aborted");
|
|
12998
12926
|
}
|
|
@@ -13022,15 +12950,13 @@ export class Daemon extends EventEmitter {
|
|
|
13022
12950
|
}
|
|
13023
12951
|
/** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
|
|
13024
12952
|
async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
|
|
13025
|
-
if (this.nativePaneLifecycle)
|
|
13026
|
-
throw Error("Native processes can only be closed through their owned runtime pane");
|
|
13027
12953
|
const windowId = this.tmux?.getWindowId();
|
|
13028
12954
|
// Before createWindow succeeds there is no pane owned by this daemon.
|
|
13029
12955
|
// tmux interprets an empty selector as the session's active window.
|
|
13030
12956
|
if (!windowId?.trim())
|
|
13031
12957
|
return;
|
|
13032
12958
|
try {
|
|
13033
|
-
const pid = await
|
|
12959
|
+
const pid = await TmuxManager.getPanePid(this.tmuxSessionName, windowId);
|
|
13034
12960
|
if (pid) {
|
|
13035
12961
|
this.noteStopRequest(signal, reason);
|
|
13036
12962
|
process.kill(-pid, signal);
|
|
@@ -13052,7 +12978,12 @@ export class Daemon extends EventEmitter {
|
|
|
13052
12978
|
|| this.startupAborted || this.deliveryWritesStopping || this.runtimeMonitorsFrozen || this.healthCheckPaused)) {
|
|
13053
12979
|
throw new Error("OOM recovery cancelled: the instance was stopped, paused or replaced while memory was unavailable");
|
|
13054
12980
|
}
|
|
13055
|
-
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs)
|
|
12981
|
+
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs).catch(err => {
|
|
12982
|
+
// Includes an in-place binary replacement during asynchronous launch preparation.
|
|
12983
|
+
if (err instanceof KiroCompatibilityError)
|
|
12984
|
+
this.emit("kiro_compatibility_hold", err.message);
|
|
12985
|
+
throw err;
|
|
12986
|
+
});
|
|
13056
12987
|
};
|
|
13057
12988
|
const ready = !this.spawnGate ? await enter() : await this.spawnGate.run({
|
|
13058
12989
|
instanceName: this.name,
|
|
@@ -13244,7 +13175,7 @@ export class Daemon extends EventEmitter {
|
|
|
13244
13175
|
whatsNewRoles() {
|
|
13245
13176
|
if (this.runtimeIdentity?.kind === "classic")
|
|
13246
13177
|
return ["classic"];
|
|
13247
|
-
if (this.config.general_topic === true ||
|
|
13178
|
+
if (this.config.general_topic === true || this.name === "general")
|
|
13248
13179
|
return ["general"];
|
|
13249
13180
|
const set = resolveToolSet(this.config, this.name);
|
|
13250
13181
|
return set === "coordinator" || set === "full" ? ["worker", "coordinator"] : ["worker"];
|
|
@@ -13271,12 +13202,15 @@ export class Daemon extends EventEmitter {
|
|
|
13271
13202
|
}
|
|
13272
13203
|
}
|
|
13273
13204
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
13274
|
-
if (this.nativePaneLifecycle)
|
|
13275
|
-
throw Error("Native relaunch requires its owned exit and a new lifecycle reservation");
|
|
13276
13205
|
this.assertSessionOomAdmission();
|
|
13277
13206
|
this.startupAdmission?.();
|
|
13278
13207
|
const launchFence = this.launchFenceEpoch;
|
|
13279
13208
|
const backendConfig = this.buildBackendConfig();
|
|
13209
|
+
// A compatibility refusal is not a failed resume: no configuration, pin, history or pane may change.
|
|
13210
|
+
if (this.backend?.assertLaunchSupported) {
|
|
13211
|
+
this.backend.assertLaunchSupported(backendConfig);
|
|
13212
|
+
this.emit("kiro_compatibility_hold", null);
|
|
13213
|
+
}
|
|
13280
13214
|
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
13281
13215
|
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
13282
13216
|
// Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
|
|
@@ -13313,6 +13247,7 @@ export class Daemon extends EventEmitter {
|
|
|
13313
13247
|
const agentTokenPath = join(this.instanceDir, "agent.token");
|
|
13314
13248
|
const agentToken = randomBytes(32).toString("hex");
|
|
13315
13249
|
const tokenWrite = writeSecretFile(agentTokenPath, agentToken);
|
|
13250
|
+
this.agentCredentialIssuer?.(agentToken);
|
|
13316
13251
|
if (!tokenWrite.ok) {
|
|
13317
13252
|
// Do not fail the spawn over it — an instance that cannot start is worse
|
|
13318
13253
|
// than one whose token is readable. But say so, loudly: the whole point of
|
|
@@ -13358,6 +13293,8 @@ export class Daemon extends EventEmitter {
|
|
|
13358
13293
|
// Every launched command re-arms the passive-transient check, including a
|
|
13359
13294
|
// retry inside the same spawn: its load is a new one.
|
|
13360
13295
|
this.launchAttempt++;
|
|
13296
|
+
await this.prepareQuotaReplayHistory(reuseWindow);
|
|
13297
|
+
this.startupAdmission?.();
|
|
13361
13298
|
this.interactionObservation.reset(this.interactionOwner());
|
|
13362
13299
|
this.clearInteractionConfirmation();
|
|
13363
13300
|
// A launch that cannot paint a passive transient (Codex: a fresh start
|
|
@@ -13377,8 +13314,8 @@ export class Daemon extends EventEmitter {
|
|
|
13377
13314
|
this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
|
|
13378
13315
|
}
|
|
13379
13316
|
// Ensure tmux session exists (may have been destroyed if all windows died)
|
|
13380
|
-
await
|
|
13381
|
-
if (this.stormWindow?.observeServerAlive(await
|
|
13317
|
+
await TmuxManager.ensureSession(this.tmuxSessionName);
|
|
13318
|
+
if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
|
|
13382
13319
|
this.emit("tmux_server_crash", this.name);
|
|
13383
13320
|
}
|
|
13384
13321
|
this.startupAdmission?.();
|
|
@@ -13514,6 +13451,7 @@ export class Daemon extends EventEmitter {
|
|
|
13514
13451
|
let lastDialog = null;
|
|
13515
13452
|
let lastTransient = null;
|
|
13516
13453
|
let captureFailures = 0;
|
|
13454
|
+
let lastPane = null;
|
|
13517
13455
|
let attempts = 0;
|
|
13518
13456
|
// A safety-critical startup choice is one-shot. If the CLI has not
|
|
13519
13457
|
// repainted after Enter, let the following hold-only entry report it
|
|
@@ -13537,11 +13475,13 @@ export class Daemon extends EventEmitter {
|
|
|
13537
13475
|
try {
|
|
13538
13476
|
pane = await this.tmux.capturePane();
|
|
13539
13477
|
this.startupAdmission?.();
|
|
13478
|
+
lastPane = pane;
|
|
13540
13479
|
}
|
|
13541
13480
|
catch (err) {
|
|
13542
13481
|
this.startupAdmission?.();
|
|
13543
13482
|
// Transient tmux trouble is not evidence about the CLI. Returning false
|
|
13544
13483
|
// here would clear the session; retry within the budget instead.
|
|
13484
|
+
lastPane = null;
|
|
13545
13485
|
captureFailures++;
|
|
13546
13486
|
this.logger.debug({ err, captureFailures }, "capture-pane failed during the startup dialog scan — retrying");
|
|
13547
13487
|
// Only a POSITIVE dead pane ends the scan; an unanswerable liveness
|
|
@@ -13652,15 +13592,12 @@ export class Daemon extends EventEmitter {
|
|
|
13652
13592
|
// each have seen a respawn, pause or stop.
|
|
13653
13593
|
if (!owned())
|
|
13654
13594
|
return false;
|
|
13655
|
-
const keyScreen = this.nativePaneLifecycle ? await this.tmux.capturePane() : answeredPane;
|
|
13656
|
-
if (!owned() || !Daemon.dialogMatches(dialog, keyScreen))
|
|
13657
|
-
return false;
|
|
13658
13595
|
if (key === "Up" || key === "Down" || key === "Enter" || key === "Escape") {
|
|
13659
|
-
if (!await
|
|
13596
|
+
if (!await this.tmux.sendSpecialKey(key))
|
|
13660
13597
|
return false;
|
|
13661
13598
|
}
|
|
13662
13599
|
else {
|
|
13663
|
-
if (!await
|
|
13600
|
+
if (!await this.tmux.sendKeys(key))
|
|
13664
13601
|
return false;
|
|
13665
13602
|
}
|
|
13666
13603
|
await new Promise(r => setTimeout(r, 200));
|
|
@@ -13674,8 +13611,7 @@ export class Daemon extends EventEmitter {
|
|
|
13674
13611
|
// Wait for next screen to render — bounded by what is left of the budget.
|
|
13675
13612
|
const renderWait = Math.max(0, Math.min(10_000, remaining()));
|
|
13676
13613
|
if (this.controlClient) {
|
|
13677
|
-
const wid = this.
|
|
13678
|
-
: readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
|
|
13614
|
+
const wid = readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim();
|
|
13679
13615
|
await this.controlClient.waitForIdle(wid, renderWait);
|
|
13680
13616
|
}
|
|
13681
13617
|
else {
|
|
@@ -13711,15 +13647,15 @@ export class Daemon extends EventEmitter {
|
|
|
13711
13647
|
// scan parked on the old limit text.
|
|
13712
13648
|
const codexReady = this.backend.binaryName !== "codex"
|
|
13713
13649
|
|| this.isCodexLivePaneSnapshot(pane);
|
|
13714
|
-
|
|
13715
|
-
? this.backend.readInputRow(pane) !== null : this.backend?.isDeliveryInputReadyPane?.(pane) === true);
|
|
13716
|
-
if (readyPatternMatches && codexReady && nativeComposerReady) {
|
|
13650
|
+
if (readyPatternMatches && codexReady) {
|
|
13717
13651
|
cleanReadyPolls++;
|
|
13718
13652
|
if (cleanReadyPolls >= 2 || remaining() <= 0) {
|
|
13719
13653
|
// A real ready prompt (with no fatal dialog on screen — those are
|
|
13720
13654
|
// matched above, before this check) means the fatal screen is gone,
|
|
13721
13655
|
// e.g. the user fixed the config and the instance respawned.
|
|
13722
13656
|
this.fatalStartupBlocked = false;
|
|
13657
|
+
if (owned())
|
|
13658
|
+
this.takeQuotaReplayBaseline(pane);
|
|
13723
13659
|
return true;
|
|
13724
13660
|
}
|
|
13725
13661
|
await sleep();
|
|
@@ -13758,6 +13694,8 @@ export class Daemon extends EventEmitter {
|
|
|
13758
13694
|
this.logger.warn({ attempts, budgetMs, captureFailures }, "Startup scan exhausted without a ready prompt or a known dialog — assuming ready (unknown CLI screen)");
|
|
13759
13695
|
}
|
|
13760
13696
|
this.startupAdmission?.();
|
|
13697
|
+
if (owned() && lastPane !== null && !lastDialog && !lastTransient)
|
|
13698
|
+
this.takeQuotaReplayBaseline(lastPane);
|
|
13761
13699
|
return true;
|
|
13762
13700
|
}
|
|
13763
13701
|
/**
|
|
@@ -13838,16 +13776,6 @@ export class Daemon extends EventEmitter {
|
|
|
13838
13776
|
void refresh.call(this.backend).then(() => this.saveSessionId(), () => { });
|
|
13839
13777
|
}
|
|
13840
13778
|
saveSessionId() {
|
|
13841
|
-
if (this.nativePaneLifecycle) {
|
|
13842
|
-
try {
|
|
13843
|
-
this.nativePaneLifecycle.checkpointSession();
|
|
13844
|
-
this.ownedWorkingSession = this.backend?.getSessionId() ?? null;
|
|
13845
|
-
}
|
|
13846
|
-
catch (err) {
|
|
13847
|
-
this.logger.warn({ err }, "Native conversation checkpoint is unconfirmed");
|
|
13848
|
-
}
|
|
13849
|
-
return;
|
|
13850
|
-
}
|
|
13851
13779
|
// When a resume failure has forced a fresh start, don't persist the stale id
|
|
13852
13780
|
// back from statusline.json — that would re-arm --continue and re-loop.
|
|
13853
13781
|
if (this.skipResume)
|