@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/fleet-manager.js
CHANGED
|
@@ -1,30 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { nativePaneToolAvailable, nativePaneToolRefusal } from "./native-pane-tools.js";
|
|
5
|
-
import { windowsAgentsFor } from "./windows-agents.js";
|
|
6
|
-
import { AgentRuntimeChanges } from "./agent-runtime-change.js";
|
|
1
|
+
import { QuotaNotices } from "./usage/quota-notices.js";
|
|
2
|
+
import { quotaNoticeRule } from "./quota-notice-config.js";
|
|
3
|
+
import { onUsageSnapshot } from "./usage/usage-api.js";
|
|
7
4
|
import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
|
|
8
5
|
import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
|
|
9
6
|
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
10
7
|
import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
|
|
11
8
|
import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
9
|
+
import { maskSecrets } from "./log-steps.js";
|
|
12
10
|
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
13
11
|
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
14
12
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
15
13
|
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
16
14
|
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
17
15
|
import { SettingsBaselines } from "./settings-baseline.js";
|
|
18
|
-
import {
|
|
19
|
-
import { settingsDisplay } from "./settings-change.js";
|
|
20
|
-
import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
|
|
16
|
+
import { SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
|
|
21
17
|
import { SettingsHttpConfirmation } from "./settings-http-confirmation.js";
|
|
22
18
|
import { SettingsControlServer } from "./settings-control.js";
|
|
23
|
-
import {
|
|
24
|
-
import { GeneralManagedFileError } from "./runtime-general-protocol.js";
|
|
25
|
-
import { GeneralBindings, generalBindingRevision, parseGeneralSwitchInput, switchGeneralLocation } from "./general-binding.js";
|
|
26
|
-
import { confirmGeneralHostExit } from "./general-host-owner.js";
|
|
27
|
-
import { settingsFingerprint, SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
|
|
19
|
+
import { SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
|
|
28
20
|
import { performance } from "node:perf_hooks";
|
|
29
21
|
import { withSpawnHoldReply } from "./spawn-hold-reply.js";
|
|
30
22
|
import { gatewayRequestContext } from "./web-request-context.js";
|
|
@@ -39,7 +31,7 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
|
|
|
39
31
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
40
32
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
41
33
|
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
42
|
-
import { unattendedInstallCommand, loginShell } from "./unattended-install.js";
|
|
34
|
+
import { installExitMarker, unattendedInstallCommand, loginShell } from "./unattended-install.js";
|
|
43
35
|
import { InstallStream } from "./install-stream.js";
|
|
44
36
|
import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
|
|
45
37
|
import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
|
|
@@ -54,7 +46,7 @@ import { ProfileControlServer } from "./profile-control.js";
|
|
|
54
46
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, realpathSync, lstatSync, closeSync } from "node:fs";
|
|
55
47
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
56
48
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
57
|
-
import { freemem, totalmem, homedir
|
|
49
|
+
import { freemem, totalmem, homedir } from "node:os";
|
|
58
50
|
import { effectiveCpuCount } from "./cpu-quota.js";
|
|
59
51
|
import { createServer } from "node:http";
|
|
60
52
|
import { Worker } from "node:worker_threads";
|
|
@@ -87,6 +79,7 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
|
87
79
|
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
88
80
|
import { EventLog } from "./event-log.js";
|
|
89
81
|
import { binaryProbe } from "./binary-probe.js";
|
|
82
|
+
import { deletePlannedTopic, planInstanceTopicDelete, topicDestination, topicMustStay } from "./instance-topic-delete.js";
|
|
90
83
|
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
|
|
91
84
|
import { classifySqliteOpenError } from "./sqlite-open-errors.js";
|
|
92
85
|
import { OutboxOpenError } from "./outbox-open-error.js";
|
|
@@ -96,18 +89,17 @@ import { startEventLoopWatch } from "./event-loop-watch.js";
|
|
|
96
89
|
import { TmuxManager } from "./tmux-manager.js";
|
|
97
90
|
import { AccessManager } from "./channel/access-manager.js";
|
|
98
91
|
import { IpcClient } from "./channel/ipc-bridge.js";
|
|
99
|
-
import {
|
|
100
|
-
import { usesChannelAddress, TOOLS } from "./channel/mcp-tools.js";
|
|
92
|
+
import { usesChannelAddress } from "./channel/mcp-tools.js";
|
|
101
93
|
import { ParkedNotices, PARKED_NOTICE_LEDGER_FILE } from "./parked-notices.js";
|
|
102
94
|
import { formatFleetTime } from "./tz-utils.js";
|
|
103
95
|
import { createAdapter } from "./channel/factory.js";
|
|
104
96
|
import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
|
|
105
97
|
import { TelegramAdapter } from "./channel/adapters/telegram.js";
|
|
98
|
+
import { MissedOfflineStore } from "./missed-offline.js";
|
|
106
99
|
import { createBackend } from "./backend/factory.js";
|
|
107
100
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
108
101
|
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
109
102
|
import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
|
|
110
|
-
import { readGrokModelCache } from "./backend/grok-model-cache.js";
|
|
111
103
|
import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
112
104
|
import { createLogger, rotateLogIfNeeded } from "./logger.js";
|
|
113
105
|
import { processAttachments } from "./channel/attachment-handler.js";
|
|
@@ -154,6 +146,20 @@ export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked", "review"];
|
|
|
154
146
|
* " \t " and [" ", "\t"] both normalize to undefined (not treated as explicit
|
|
155
147
|
* filters). The same result drives the live-only default and the cap decision.
|
|
156
148
|
*/
|
|
149
|
+
/**
|
|
150
|
+
* #3195: the saved value of each plain-update field the call named (assignee, priority, result), read back from the
|
|
151
|
+
* task — so the answer shows what was persisted, not what was asked. Status is always in the ack already.
|
|
152
|
+
*/
|
|
153
|
+
export function savedFields(t, named) {
|
|
154
|
+
const out = {};
|
|
155
|
+
if (named.assignee !== undefined)
|
|
156
|
+
out.assignee = t.assignee;
|
|
157
|
+
if (named.priority !== undefined)
|
|
158
|
+
out.priority = t.priority;
|
|
159
|
+
if (named.result !== undefined)
|
|
160
|
+
out.result = t.result;
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
157
163
|
export function normalizeStatusFilter(v) {
|
|
158
164
|
if (typeof v === "string") {
|
|
159
165
|
const s = v.trim();
|
|
@@ -182,17 +188,8 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
182
188
|
return tasks;
|
|
183
189
|
}
|
|
184
190
|
import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
185
|
-
import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
|
|
186
|
-
import { localWindowsCliEnabled, remoteRuntimeEnabled, setConfiguredRuntimeSwitches, maxAgentsPerMachine } from "./runtime-switches.js";
|
|
187
|
-
import { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
188
|
-
import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
|
|
189
|
-
import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
|
|
190
|
-
import { NativeMcpSessions } from "./native-mcp.js";
|
|
191
|
-
import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
|
|
192
|
-
import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED } from "./native-instance-registry.js";
|
|
193
|
-
import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
|
|
194
|
-
import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
|
|
195
191
|
import { LogService } from "./log-service.js";
|
|
192
|
+
import { viewHistory } from "./view-history.js";
|
|
196
193
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
|
|
197
194
|
import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
|
|
198
195
|
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
@@ -225,7 +222,7 @@ import { envFileKeys } from "./token-env-name.js";
|
|
|
225
222
|
import { discordGeneralChannelId } from "./discord-general.js";
|
|
226
223
|
import { parseEnvText } from "./env-file.js";
|
|
227
224
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
228
|
-
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
225
|
+
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, setCapacityLookup, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
229
226
|
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
230
227
|
import { LoginSession } from "./login-manager.js";
|
|
231
228
|
import { runOnLoginShell } from "./login-shell.js";
|
|
@@ -256,6 +253,8 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
|
|
|
256
253
|
import { recentChatContext } from "./log-tail.js";
|
|
257
254
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
258
255
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
256
|
+
import { AgentCredentials, agentCredentialPolicy } from "./agent-credentials.js";
|
|
257
|
+
import { writeSecretFile } from "./secret-file.js";
|
|
259
258
|
import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
|
|
260
259
|
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
|
|
261
260
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
@@ -286,14 +285,13 @@ import { CompactWatch, launchIdentity } from "./compact-watch.js";
|
|
|
286
285
|
import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
|
|
287
286
|
import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
|
|
288
287
|
import { sharedRolloutIndex } from "./rollout-index.js";
|
|
289
|
-
import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
|
|
288
|
+
import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardOffIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
|
|
290
289
|
import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
291
290
|
import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
|
|
292
291
|
import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
|
|
293
292
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
294
293
|
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
295
294
|
import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
|
|
296
|
-
import { isOutsideLinkOwner, OUTSIDE_LINK_OWNER, OUTSIDE_LINK_RETRY_MS, OUTSIDE_OWN_CLOSES, outsideDuration, outsideTimer, TOTP_STEP_SECONDS, WEB_REMOTE_ACCESS_FILE, WebRemoteAccess } from "./web-remote-access.js";
|
|
297
295
|
import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
298
296
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
299
297
|
import { SessionAuthorizations } from "./authority/session-authorizations.js";
|
|
@@ -315,7 +313,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
315
313
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
316
314
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
317
315
|
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
318
|
-
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
316
|
+
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
|
|
319
317
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
320
318
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
321
319
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
@@ -331,6 +329,12 @@ import { BackendOutageTracker } from "./backend-outage.js";
|
|
|
331
329
|
import { canUnlockAdvancedTips, DailyTipScheduler, selectTip, visibleTipLevels, } from "./tips.js";
|
|
332
330
|
import { getTmuxSession } from "./config.js";
|
|
333
331
|
import { OwnStatusMessages, discordMessageKey } from "./chat-log-ids.js";
|
|
332
|
+
import { PROFILE_TIMELINE_FILE, PROFILE_UNRECORDED, recordProfileLaunch } from "./usage/profile-timeline.js";
|
|
333
|
+
import { readQuotaHistory } from "./usage/quota-history.js";
|
|
334
|
+
import { estimateCapacity } from "./usage/capacity.js";
|
|
335
|
+
import { covered, hourSources } from "./usage-rollup.js";
|
|
336
|
+
import { KEEP_DAYS } from "./cache-ledger.js";
|
|
337
|
+
import { ratesFor } from "./cache-prices.js";
|
|
334
338
|
// Closed migration cohort: these 11 names existed immediately before the first
|
|
335
339
|
// managed-skill publisher (a1ac499f; parent e267e684). Never grow this set when
|
|
336
340
|
// adding a bundled skill: its new name is not proof of old AgEnD ownership.
|
|
@@ -482,6 +486,8 @@ const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
482
486
|
* while anything real shows signs of life well before the old two minutes.
|
|
483
487
|
*/
|
|
484
488
|
const PROGRESS_MIN_ELAPSED_MS = 30_000;
|
|
489
|
+
/** M-2391: the threshold tick fires this long after it, never a millisecond before (timers can fire early). */
|
|
490
|
+
const PROGRESS_THRESHOLD_MARGIN_MS = 50;
|
|
485
491
|
/** How much of a tool summary the progress line will show before eliding. */
|
|
486
492
|
const PROGRESS_ACTIVITY_MAX_CHARS = 48;
|
|
487
493
|
/**
|
|
@@ -762,974 +768,6 @@ export class FleetManager {
|
|
|
762
768
|
/** Live view of lifecycle.daemons — used throughout; not deprecated. */
|
|
763
769
|
get daemons() { return this.lifecycle.daemons; }
|
|
764
770
|
fleetConfig = null;
|
|
765
|
-
fleetWindowsRuntime = null;
|
|
766
|
-
nativeMcpStore = null;
|
|
767
|
-
paneManager = null;
|
|
768
|
-
localPaneRelay = null;
|
|
769
|
-
openingLocalPane = null;
|
|
770
|
-
runtimeHub = null;
|
|
771
|
-
runtimeExecutor = null;
|
|
772
|
-
runtimeStatusScheduled = false;
|
|
773
|
-
reconciledRemoteResults = new Set();
|
|
774
|
-
/** Machine credentials are separate from dashboard sessions and per-turn agent capabilities. */
|
|
775
|
-
runtimeCoordinator() {
|
|
776
|
-
if (!remoteRuntimeEnabled())
|
|
777
|
-
throw new Error("Remote runtimes are not enabled");
|
|
778
|
-
return this.runtimeHub ??= new RuntimeCoordinator(this.dataDir, {
|
|
779
|
-
handleMcp: (machineId, runId, body) => this.runtimeWindowsExecutor().mcp(machineId, runId, body),
|
|
780
|
-
handlePaneMcp: async (machineId, body, current) => (await this.nativePanes()).handle(machineId, body, current),
|
|
781
|
-
warn: (fields, message) => this.logger.warn(fields, message),
|
|
782
|
-
changed: event => {
|
|
783
|
-
if (event?.type === "machine" && this.runtimeHub?.machine(event.machineId)?.status !== "online") {
|
|
784
|
-
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {}))
|
|
785
|
-
if (config.machine_id === event.machineId) {
|
|
786
|
-
this.nativeMcpStore?.closeInstance(name);
|
|
787
|
-
if (config.general_topic) {
|
|
788
|
-
this.cancelPendingDeliveries(name);
|
|
789
|
-
this.holdGeneralOldDeliveries(name);
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
this.needsYou?.poke();
|
|
793
|
-
}
|
|
794
|
-
if (this.runtimeStatusScheduled || this.shuttingDown)
|
|
795
|
-
return;
|
|
796
|
-
this.runtimeStatusScheduled = true;
|
|
797
|
-
queueMicrotask(() => {
|
|
798
|
-
this.runtimeStatusScheduled = false;
|
|
799
|
-
if (this.shuttingDown)
|
|
800
|
-
return;
|
|
801
|
-
try {
|
|
802
|
-
this.fleetWindowsRuntime?.importHistory();
|
|
803
|
-
for (const snapshot of this.runtimeHub?.tasks() ?? [])
|
|
804
|
-
this.reconcileRemoteDelivery(snapshot.machineId, snapshot.task);
|
|
805
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
806
|
-
this.scheduleDeliveryOutboxPump();
|
|
807
|
-
}
|
|
808
|
-
catch (error) {
|
|
809
|
-
this.logger.warn({ err: error }, "Runtime status reconciliation failed");
|
|
810
|
-
}
|
|
811
|
-
});
|
|
812
|
-
},
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
runtimeWindowsExecutor() {
|
|
816
|
-
return this.runtimeExecutor ??= new RuntimeWindowsBridge(this, () => remoteRuntimeEnabled() && !this.shuttingDown ? this.runtimeCoordinator() : null);
|
|
817
|
-
}
|
|
818
|
-
/** Per-turn capabilities for native MCP, separate from browser and Linux-agent credentials. */
|
|
819
|
-
nativeMcpSessions() {
|
|
820
|
-
return this.nativeMcpStore ??= new NativeMcpSessions(this);
|
|
821
|
-
}
|
|
822
|
-
/** The same instance registry and delivery entry point, with a native Windows execution adapter. */
|
|
823
|
-
nativeInstanceRuntime() {
|
|
824
|
-
return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
|
|
825
|
-
owner: this, fleet: () => this.fleetConfig, save: () => this.saveFleetConfig(), stopping: () => this.shuttingDown, history: this.webChatHistory,
|
|
826
|
-
openMcp: (instance, requestId, sandbox, stillCurrent) => this.healthServerListening
|
|
827
|
-
? this.nativeMcpSessions().grant({ instance, requestId, sandbox, stillCurrent, endpoint: `http://127.0.0.1:${this.fleetConfig?.health_port ?? 19280}/agent` }) : null,
|
|
828
|
-
warn: (message, error) => this.logger.warn({ err: error }, message),
|
|
829
|
-
removed: name => this.nativeInstanceRemoved(name),
|
|
830
|
-
paneCurrent: name => this.daemons.get(name)?.nativePaneCurrent?.() === true,
|
|
831
|
-
machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
|
|
832
|
-
machineGeneration: id => this.runtimeCoordinator().machine(id)?.bootId ?? null,
|
|
833
|
-
paneAvailable: id => this.runtimeCoordinator().machine(id)?.paneProtocol === 1,
|
|
834
|
-
validateMachine: (id, cli, sandbox) => {
|
|
835
|
-
const machine = this.runtimeCoordinator().machine(id);
|
|
836
|
-
const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
|
|
837
|
-
if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
|
|
838
|
-
throw new Error("Choose an online runtime and an available CLI");
|
|
839
|
-
if (sandbox === "read-only" && !provider.readOnlySupport)
|
|
840
|
-
throw new Error("This runtime CLI cannot enforce read-only execution");
|
|
841
|
-
},
|
|
842
|
-
emit: (event, data) => {
|
|
843
|
-
this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
|
|
844
|
-
if (event === "activity")
|
|
845
|
-
this.scheduleDeliveryOutboxPump();
|
|
846
|
-
},
|
|
847
|
-
}, { bridge: this.runtimeWindowsExecutor() });
|
|
848
|
-
}
|
|
849
|
-
agentRuntimeChangeService;
|
|
850
|
-
agentRuntimeChanges() {
|
|
851
|
-
if (this.agentRuntimeChangeService)
|
|
852
|
-
return this.agentRuntimeChangeService;
|
|
853
|
-
const windowPins = new Map();
|
|
854
|
-
return this.agentRuntimeChangeService ??= new AgentRuntimeChanges({
|
|
855
|
-
config: name => Object.hasOwn(this.fleetConfig?.instances ?? {}, name) ? this.fleetConfig.instances[name] : undefined,
|
|
856
|
-
defaults: () => this.fleetConfig?.defaults,
|
|
857
|
-
generation: () => [this.managerBootId, this.settingsGeneration],
|
|
858
|
-
epoch: name => this.lifecycle.epochOf(name),
|
|
859
|
-
backendState: (name, backend) => !isWindowsInstance(this.fleetConfig?.instances[name]) ? this.backendAvailabilityFor(name).find(row => row.backend === backend)?.state : undefined,
|
|
860
|
-
transition: (name, run) => this.lifecycle.runTransition(name, async (transition) => {
|
|
861
|
-
const config = this.fleetConfig?.instances[name];
|
|
862
|
-
if (!isWindowsInstance(config)) {
|
|
863
|
-
const path = join(this.getInstanceDir(name), "window-id");
|
|
864
|
-
let pin;
|
|
865
|
-
if (existsSync(path)) {
|
|
866
|
-
if (statSync(path).size > 128)
|
|
867
|
-
throw Error("Invalid saved CLI window identity");
|
|
868
|
-
pin = readFileSync(path, "utf8").trim();
|
|
869
|
-
if (!/^@\d+$/.test(pin))
|
|
870
|
-
throw Error("Invalid saved CLI window identity");
|
|
871
|
-
}
|
|
872
|
-
else if (this.daemons.has(name))
|
|
873
|
-
throw Error("The current CLI window identity is unavailable");
|
|
874
|
-
windowPins.set(name, pin);
|
|
875
|
-
}
|
|
876
|
-
try {
|
|
877
|
-
return await run(async () => {
|
|
878
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
879
|
-
this.lifecycle.invalidate(name);
|
|
880
|
-
this.nativeInstanceRuntime().setPaused(name, true);
|
|
881
|
-
}
|
|
882
|
-
else
|
|
883
|
-
await this.stopInstance(name, transition);
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
finally {
|
|
887
|
-
windowPins.delete(name);
|
|
888
|
-
}
|
|
889
|
-
}),
|
|
890
|
-
idle: name => {
|
|
891
|
-
if (this.shuttingDown || this.lifecycle.hasWorkLease(name) || this.deliveryOutbox?.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending", "uncertain"]))
|
|
892
|
-
return false;
|
|
893
|
-
const config = this.fleetConfig?.instances[name];
|
|
894
|
-
if (!config)
|
|
895
|
-
return false;
|
|
896
|
-
if (isWindowsInstance(config)) {
|
|
897
|
-
try {
|
|
898
|
-
const agent = this.nativeInstanceRuntime().get(name);
|
|
899
|
-
if (!agent || agent.blocked || agent.activeId && !isNativePaneInstance(config))
|
|
900
|
-
return false;
|
|
901
|
-
}
|
|
902
|
-
catch {
|
|
903
|
-
return false;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
if (!this.daemons.has(name) && !isWindowsInstance(config) && existsSync(join(this.getInstanceDir(name), "window-id")))
|
|
907
|
-
return false;
|
|
908
|
-
return !this.daemons.has(name) || this.getInstanceExecutionState(name) === "idle";
|
|
909
|
-
},
|
|
910
|
-
prepare: async (name, before, target, current) => {
|
|
911
|
-
if (!current())
|
|
912
|
-
throw Error("Runtime change authority expired");
|
|
913
|
-
if (isWindowsInstance(before))
|
|
914
|
-
this.nativeInstanceRuntime().importHistory(name);
|
|
915
|
-
if (target.runtime === "windows")
|
|
916
|
-
return this.nativeInstanceRuntime().prepareRuntimeChange(name, before, target, current);
|
|
917
|
-
const sameAccount = !isWindowsInstance(before) && target.backend === (before.backend || this.fleetConfig?.defaults?.backend || "claude-code");
|
|
918
|
-
const availability = (sameAccount ? this.backendAvailabilityFor(name) : this.backendAvailabilityRows()).find(row => row.backend === target.backend);
|
|
919
|
-
if (availability && ["not_installed", "not_signed_in", "too_old"].includes(availability.state))
|
|
920
|
-
throw Error("Choose an available CLI on the Fleet host");
|
|
921
|
-
const supplied = target.directory.trim(), cwd = supplied || join(this.dataDir, "workspaces", name);
|
|
922
|
-
if (!isAbsolute(cwd) || /^[A-Za-z]:[\\/]/.test(cwd) || /[\x00-\x1f]/.test(cwd))
|
|
923
|
-
throw Error("Enter an absolute Linux working directory or leave it empty");
|
|
924
|
-
if (!current())
|
|
925
|
-
throw Error("Runtime change authority expired");
|
|
926
|
-
if (supplied) {
|
|
927
|
-
if (!statSync(cwd).isDirectory())
|
|
928
|
-
throw Error("The Linux working directory is not a directory");
|
|
929
|
-
}
|
|
930
|
-
else
|
|
931
|
-
mkdirSync(cwd, { recursive: true, mode: 0o700 });
|
|
932
|
-
const config = { ...before, runtime: "wsl", working_directory: cwd, backend: target.backend, model: target.model };
|
|
933
|
-
if (before.backend !== target.backend)
|
|
934
|
-
config.model_failover = [];
|
|
935
|
-
for (const key of ["machine_id", "native_agent_id", "native_adapter", "native_sandbox"])
|
|
936
|
-
delete config[key];
|
|
937
|
-
if (!sameAccount)
|
|
938
|
-
delete config.backend_options;
|
|
939
|
-
return { config, discard: () => { } };
|
|
940
|
-
},
|
|
941
|
-
closed: async (name, before) => {
|
|
942
|
-
if (this.daemons.has(name))
|
|
943
|
-
return false;
|
|
944
|
-
if (isWindowsInstance(before))
|
|
945
|
-
return !this.nativeInstanceRuntime().get(name)?.activeId;
|
|
946
|
-
// An unreadable tmux state is not evidence that the old process exited.
|
|
947
|
-
const session = getTmuxSession();
|
|
948
|
-
if (!await TmuxManager.sessionExistsStrict(session))
|
|
949
|
-
return true;
|
|
950
|
-
const pin = windowPins.get(name);
|
|
951
|
-
return !(await TmuxManager.listWindowsStrict(session)).some(window => window.name === name || pin && window.id === pin);
|
|
952
|
-
},
|
|
953
|
-
fresh: (name, requestId) => {
|
|
954
|
-
const dir = this.getInstanceDir(name), moved = [];
|
|
955
|
-
const restore = () => { for (const [source, saved] of moved.reverse())
|
|
956
|
-
if (!existsSync(source) && existsSync(saved))
|
|
957
|
-
renameSync(saved, source); };
|
|
958
|
-
try {
|
|
959
|
-
for (const file of ["session-id", "session-id.backend"]) {
|
|
960
|
-
const source = join(dir, file), saved = join(dir, `${file}.runtime-${requestId}`);
|
|
961
|
-
if (existsSync(source)) {
|
|
962
|
-
if (existsSync(saved))
|
|
963
|
-
throw Error("Session backup already exists");
|
|
964
|
-
renameSync(source, saved);
|
|
965
|
-
moved.push([source, saved]);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
968
|
-
return restore;
|
|
969
|
-
}
|
|
970
|
-
catch (error) {
|
|
971
|
-
restore();
|
|
972
|
-
throw error;
|
|
973
|
-
}
|
|
974
|
-
},
|
|
975
|
-
save: (name, before, next) => {
|
|
976
|
-
if (this.fleetConfig?.instances[name] !== before)
|
|
977
|
-
throw Error("Agent settings changed before saving Runtime");
|
|
978
|
-
// Mark the replacement stopped before committing its config. A marker failure must not leave a saved
|
|
979
|
-
// Windows config referring to a profile the preparation rollback would then remove.
|
|
980
|
-
writePausedMarker(this.getInstanceDir(name), Date.now(), "operator");
|
|
981
|
-
const fleet = this.fleetConfig;
|
|
982
|
-
fleet.instances[name] = next;
|
|
983
|
-
try {
|
|
984
|
-
this.saveFleetConfig();
|
|
985
|
-
}
|
|
986
|
-
catch (error) {
|
|
987
|
-
fleet.instances[name] = before;
|
|
988
|
-
throw error;
|
|
989
|
-
}
|
|
990
|
-
// A move deliberately leaves the replacement stopped; its next explicit Start opens the fresh CLI.
|
|
991
|
-
},
|
|
992
|
-
changed: name => { this.instanceStateCache.delete(name); this.instanceProcessStatus.delete(name); this.emitSseEvent("status", this.getUiStatus()); },
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
/** Explicit dashboard opt-in, with the same General confirmation and ordinary topic/Daemon routing. */
|
|
996
|
-
async createNativePaneInstance(profile, channelId, authorized, execution) {
|
|
997
|
-
const { createNativePane } = await import("./native-pane-create.js");
|
|
998
|
-
return createNativePane({ fleet: () => this.fleetConfig, runtime: this.nativeInstanceRuntime(),
|
|
999
|
-
topic: target => {
|
|
1000
|
-
const world = this.worlds.get(target.id), adapter = world?.adapter;
|
|
1001
|
-
if (!world || !adapter?.createTopic || !adapter.deleteTopic)
|
|
1002
|
-
throw Error("The approved connection cannot create and compensate a topic");
|
|
1003
|
-
const same = () => {
|
|
1004
|
-
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
1005
|
-
const matches = channels.filter(row => (row.id ?? row.type) === target.id);
|
|
1006
|
-
return !this.shuttingDown && this.worlds.get(target.id) === world && world.adapter === adapter && matches.length === 1 && matches[0].type === target.type && matches[0].group_id === target.group_id;
|
|
1007
|
-
};
|
|
1008
|
-
return { current: same, create: name => adapter.createTopic(name), remove: id => adapter.deleteTopic(id) };
|
|
1009
|
-
},
|
|
1010
|
-
epoch: name => this.lifecycle.epochOf(name),
|
|
1011
|
-
start: async (name, config, current) => {
|
|
1012
|
-
if (!current())
|
|
1013
|
-
throw Error("Native creation expired before start");
|
|
1014
|
-
await this.startInstance(name, config, true);
|
|
1015
|
-
if (!current())
|
|
1016
|
-
throw Error("Native creation expired during start");
|
|
1017
|
-
await this.connectIpcToInstance(name);
|
|
1018
|
-
if (!current())
|
|
1019
|
-
throw Error("Native creation expired during channel connection");
|
|
1020
|
-
},
|
|
1021
|
-
close: (name, config, epoch) => this.lifecycle.runTransition(name, async (transition) => {
|
|
1022
|
-
if (this.fleetConfig?.instances[name] !== config || this.lifecycle.epochOf(name) !== epoch)
|
|
1023
|
-
return false;
|
|
1024
|
-
await this.stopInstance(name, transition);
|
|
1025
|
-
return this.fleetConfig?.instances[name] === config && this.lifecycle.epochOf(name) === epoch + 1;
|
|
1026
|
-
}),
|
|
1027
|
-
routes: () => { if (this.fleetConfig)
|
|
1028
|
-
this.routing.rebuild(this.fleetConfig); },
|
|
1029
|
-
warn: error => this.logger.warn({ err: error }, "Native creation cleanup is held; retained identity and topic require attention"), }, profile, channelId, () => !this.shuttingDown && authorized(), execution);
|
|
1030
|
-
}
|
|
1031
|
-
/** Admin-only settings path: validate first, then close the exact old launch before changing cold fields. */
|
|
1032
|
-
async updateNativePaneConfig(name, patch, authorized, transaction) {
|
|
1033
|
-
return this.lifecycle.runTransition(name, async (transition) => {
|
|
1034
|
-
const before = this.fleetConfig?.instances[name];
|
|
1035
|
-
if (!isNativePaneInstance(before))
|
|
1036
|
-
throw Error("Persistent Windows instance not found");
|
|
1037
|
-
const runtime = this.nativeInstanceRuntime(), generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1038
|
-
let expectedGeneration = generation;
|
|
1039
|
-
const current = () => authorized() && !this.shuttingDown && this.fleetConfig?.instances[name] === before
|
|
1040
|
-
&& (this.explicitStopGeneration.get(name) ?? 0) === expectedGeneration
|
|
1041
|
-
&& (before.machine_id ? remoteRuntimeEnabled() : localWindowsCliEnabled());
|
|
1042
|
-
if (!current())
|
|
1043
|
-
throw Error("Native settings authority expired");
|
|
1044
|
-
runtime.previewUpdate(name, patch);
|
|
1045
|
-
const cold = ["working_directory", "model", "systemPrompt", "native_sandbox"].some(key => Object.hasOwn(patch, key) && (patch[key] ?? "") !== (before[key] ?? ""));
|
|
1046
|
-
const running = this.daemons.get(name);
|
|
1047
|
-
if (cold) {
|
|
1048
|
-
if (running && !running.nativePaneHasExited() && (!await running.nativePauseReady({ current, accepts: () => true, revalidate: async () => current() }) || !running.nativePauseStillReady()))
|
|
1049
|
-
throw Error("Wait for this Windows agent to be idle before changing its launch settings");
|
|
1050
|
-
if (!current())
|
|
1051
|
-
throw Error("Native settings authority expired before closing its old pane");
|
|
1052
|
-
await this.stopInstance(name, transition);
|
|
1053
|
-
expectedGeneration = generation + 1;
|
|
1054
|
-
if (!current())
|
|
1055
|
-
throw Error("Native settings authority expired after closing its old pane");
|
|
1056
|
-
if (runtime.get(name)?.activeId)
|
|
1057
|
-
throw Error("Native pane exit is unconfirmed; its settings were not changed");
|
|
1058
|
-
}
|
|
1059
|
-
const result = runtime.update(name, patch, current, transaction), next = this.fleetConfig.instances[name];
|
|
1060
|
-
if (cold && running) {
|
|
1061
|
-
if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== next || (this.explicitStopGeneration.get(name) ?? 0) !== expectedGeneration)
|
|
1062
|
-
throw Error("Native settings saved; restart authority expired");
|
|
1063
|
-
await this.startInstance(name, next, true, "fleet-topic", false, transition);
|
|
1064
|
-
}
|
|
1065
|
-
return result;
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
async localNativePaneEndpoint() {
|
|
1069
|
-
const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
|
|
1070
|
-
throw Error("Local Windows runtime is switched off or draining"); };
|
|
1071
|
-
admit();
|
|
1072
|
-
if (this.localPaneDrain)
|
|
1073
|
-
throw Error("Local Windows runtime cleanup is pending");
|
|
1074
|
-
if (this.localPaneRelay?.online())
|
|
1075
|
-
return this.localPaneRelay;
|
|
1076
|
-
if (this.openingLocalPane)
|
|
1077
|
-
return this.openingLocalPane;
|
|
1078
|
-
const opening = (async () => {
|
|
1079
|
-
const [{ LocalPaneEndpoint }, { launchLocalPaneRelay, localPaneScope }, inventory] = await Promise.all([
|
|
1080
|
-
import("./local-pane-endpoint.js"), import("./local-pane-launcher.js"), this.runtimeWindowsExecutor().probe({ force: false }),
|
|
1081
|
-
]);
|
|
1082
|
-
admit();
|
|
1083
|
-
this.localPaneRelay?.stop();
|
|
1084
|
-
const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
|
|
1085
|
-
diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
|
|
1086
|
-
version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
|
|
1087
|
-
launch: () => launchLocalPaneRelay(this.dataDir, admit),
|
|
1088
|
-
mcp: async (body, current) => (await this.nativePanes()).handle("local", body, current),
|
|
1089
|
-
});
|
|
1090
|
-
try {
|
|
1091
|
-
admit();
|
|
1092
|
-
this.localPaneRelay = relay;
|
|
1093
|
-
return relay;
|
|
1094
|
-
}
|
|
1095
|
-
catch (error) {
|
|
1096
|
-
relay.stop();
|
|
1097
|
-
throw error;
|
|
1098
|
-
}
|
|
1099
|
-
})();
|
|
1100
|
-
this.openingLocalPane = opening;
|
|
1101
|
-
try {
|
|
1102
|
-
return await opening;
|
|
1103
|
-
}
|
|
1104
|
-
finally {
|
|
1105
|
-
if (this.openingLocalPane === opening)
|
|
1106
|
-
this.openingLocalPane = null;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
/** Persistent ownership is independent of the exec worker's per-turn capabilities. */
|
|
1110
|
-
async nativePanes() {
|
|
1111
|
-
if (this.paneManager)
|
|
1112
|
-
return this.paneManager;
|
|
1113
|
-
const { NativePaneManager } = await import("./native-pane-manager.js");
|
|
1114
|
-
if (this.paneManager)
|
|
1115
|
-
return this.paneManager;
|
|
1116
|
-
const store = windowsAgentsFor(this, this.runtimeWindowsExecutor());
|
|
1117
|
-
const allowed = (name, tool) => nativePaneToolAvailable(tool) && mayUseTool(resolveToolSet(this.fleetConfig?.instances[name], name), tool)
|
|
1118
|
-
&& !dashboardRefusalIn(this.fleetConfig, name, tool);
|
|
1119
|
-
return this.paneManager = new NativePaneManager({
|
|
1120
|
-
home: this.dataDir, store, config: name => this.fleetConfig?.instances[name],
|
|
1121
|
-
freshNotice: async (name, provider) => {
|
|
1122
|
-
const message = t(provider === "codex" ? "native_pane.codex_fresh_started" : "native_pane.fresh_started");
|
|
1123
|
-
this.eventLog?.insert(name, "native_pane_fresh_started", {});
|
|
1124
|
-
this.logger.info({ name }, message);
|
|
1125
|
-
await this.notifyInstanceTopic(name, message);
|
|
1126
|
-
},
|
|
1127
|
-
admit: (name, config) => {
|
|
1128
|
-
const now = this.fleetConfig?.instances[name];
|
|
1129
|
-
if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
|
|
1130
|
-
|| now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
|
|
1131
|
-
|| now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
|
|
1132
|
-
|| now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding)
|
|
1133
|
-
throw Error("Native pane configuration or lifetime changed");
|
|
1134
|
-
if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
|
|
1135
|
-
throw Error("Native runtime is switched off");
|
|
1136
|
-
},
|
|
1137
|
-
endpoint: async (agent, cleanup = false) => {
|
|
1138
|
-
if (!agent.machineId) {
|
|
1139
|
-
if (!cleanup)
|
|
1140
|
-
return this.localNativePaneEndpoint();
|
|
1141
|
-
if (!this.localPaneRelay)
|
|
1142
|
-
throw Error("Local cleanup transport is unavailable; pane remains reserved");
|
|
1143
|
-
return this.localPaneRelay;
|
|
1144
|
-
}
|
|
1145
|
-
const hub = cleanup ? this.runtimeHub : this.runtimeCoordinator(), machineId = agent.machineId;
|
|
1146
|
-
if (!hub)
|
|
1147
|
-
throw Error("Remote cleanup transport is unavailable; pane remains reserved");
|
|
1148
|
-
const draining = () => !!this.remotePaneDrain;
|
|
1149
|
-
return { machineId, online: () => !this.shuttingDown && (draining() || remoteRuntimeEnabled()) && this.runtimeHub === hub && hub.machine(machineId)?.status === "online",
|
|
1150
|
-
call: request => {
|
|
1151
|
-
if (this.runtimeHub !== hub || !(draining() || remoteRuntimeEnabled()) || draining() && request.action !== "snapshot" && request.action !== "close")
|
|
1152
|
-
return Promise.reject(Error("Remote pane transport only permits owned cleanup"));
|
|
1153
|
-
return hub.callPane(machineId, request, maxAgentsPerMachine());
|
|
1154
|
-
}, subscribe: listener => hub.subscribe(listener),
|
|
1155
|
-
epoch: () => hub.paneTransportEpoch(machineId), version: provider => hub.machine(machineId)?.inventory?.clis.find(row => row.id === provider && row.runnable)?.version ?? null };
|
|
1156
|
-
},
|
|
1157
|
-
instructions: (name, config) => buildNativeFleetInstructions({ instanceName: name, workingDirectory: config.working_directory,
|
|
1158
|
-
displayName: config.display_name, description: config.description, customPrompt: config.systemPrompt,
|
|
1159
|
-
workflow: config.workflow, taskBoard: !dashboardRefusalIn(this.fleetConfig, name, "task"),
|
|
1160
|
-
runtimeIdentity: { kind: "fleet-topic", backend: config.backend ?? "codex", model: config.model || "default" } }, config.general_topic === true),
|
|
1161
|
-
menu: name => TOOLS.filter(tool => allowed(name, tool.name)).map(tool => ({ ...tool })),
|
|
1162
|
-
allowed: (name, tool, args) => allowed(name, tool) && !nativePaneToolRefusal(tool, args), published: (name, daemon) => this.daemons.get(name) === daemon,
|
|
1163
|
-
invoke: async (daemon, input, current, name) => {
|
|
1164
|
-
if (input.tool === "list_models") {
|
|
1165
|
-
const { ListModelsArgs } = await import("./outbound-schemas.js");
|
|
1166
|
-
const parsed = ListModelsArgs.safeParse(input.args);
|
|
1167
|
-
if (!parsed.success || !current())
|
|
1168
|
-
return { result: null, error: "Invalid native model request or expired owner" };
|
|
1169
|
-
const target = parsed.data.instance_name ?? name;
|
|
1170
|
-
if (!isWindowsInstance(this.fleetConfig?.instances[target]))
|
|
1171
|
-
return { result: null, error: "Native panes can only read Windows runtime model inventories" };
|
|
1172
|
-
return { result: await this.listModelCatalog({ instanceName: target }) };
|
|
1173
|
-
}
|
|
1174
|
-
const ops = { list_instances: "list", describe_instance: "describe", task: "task", send_to_instance: "send",
|
|
1175
|
-
delegate_task: "delegate", request_information: "ask", report_result: "report", delivery_status: "delivery-status" };
|
|
1176
|
-
const { nativeAgentAdmissionContext } = await import("./native-agent-admission.js");
|
|
1177
|
-
const { dispatchAgentOperation } = await import("./agent-endpoint.js");
|
|
1178
|
-
return daemon.handlePaneToolCall(input, { current, allowed: tool => allowed(name, tool),
|
|
1179
|
-
dispatch: (tool, args) => ops[tool] ? dispatchAgentOperation(nativeAgentAdmissionContext(this, {
|
|
1180
|
-
instance: name, current: () => current() && allowed(name, tool), taskAllowed: () => current() && allowed(name, "task"),
|
|
1181
|
-
}), name, ops[tool], args, input.operationId) : undefined });
|
|
1182
|
-
},
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
/** Prepare only bundled role files; this neither switches General nor starts a provider process. */
|
|
1186
|
-
async prepareWindowsGeneralWorkspace(name, machineId, cli, bindingId, authorized) {
|
|
1187
|
-
const config = this.fleetConfig?.instances[name], hub = this.runtimeCoordinator(), epoch = hub.paneTransportEpoch(machineId);
|
|
1188
|
-
if (!config?.general_topic || !epoch)
|
|
1189
|
-
throw Error("General or its paired runtime is unavailable");
|
|
1190
|
-
const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
|
|
1191
|
-
&& this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
|
|
1192
|
-
const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertAuditedNativePaneVersion }] = await Promise.all([
|
|
1193
|
-
import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
|
|
1194
|
-
]);
|
|
1195
|
-
if (!current())
|
|
1196
|
-
throw Error("General workspace authority expired");
|
|
1197
|
-
assertAuditedNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
|
|
1198
|
-
const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
|
|
1199
|
-
const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
|
|
1200
|
-
if (!current())
|
|
1201
|
-
throw Error("General workspace authority expired");
|
|
1202
|
-
if (inventory?.capabilities?.generalWorkspace !== true)
|
|
1203
|
-
throw new NativePaneRecoveryError({ kind: "host-upgrade" });
|
|
1204
|
-
const result = await hub.callPane(machineId, request, maxAgentsPerMachine());
|
|
1205
|
-
if (!current())
|
|
1206
|
-
throw Error("General workspace authority expired");
|
|
1207
|
-
if (generalWorkspaceUpgrade(result, request))
|
|
1208
|
-
throw new NativePaneRecoveryError({ kind: "host-upgrade" });
|
|
1209
|
-
return validateGeneralWorkspaceReceipt(result, request);
|
|
1210
|
-
}
|
|
1211
|
-
generalLocationFlights = new Map();
|
|
1212
|
-
generalAttentionEpisodes = new Map();
|
|
1213
|
-
generalBindings() { return new GeneralBindings(this.dataDir); }
|
|
1214
|
-
generalLocations() {
|
|
1215
|
-
const store = this.generalBindings();
|
|
1216
|
-
return { generals: Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => cfg.general_topic).map(([name, cfg]) => {
|
|
1217
|
-
const record = store.read(name), machine = cfg.machine_id ? this.runtimeHub?.machine(cfg.machine_id) : null;
|
|
1218
|
-
return { name, displayName: cfg.display_name ?? name, location: isWindowsInstance(cfg) ? "windows" : "host",
|
|
1219
|
-
machineId: cfg.machine_id ?? null, cli: cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code", model: cfg.model ?? "",
|
|
1220
|
-
revision: generalBindingRevision(record, cfg), canSwitchBack: !!record && record.active !== "host",
|
|
1221
|
-
offline: isWindowsInstance(cfg) && (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(cfg.machine_id)),
|
|
1222
|
-
retryTarget: record?.pending?.phase === "selected" ? (record.pending.to === "host" ? { location: "host" } : (() => { const row = record.windows.find(item => item.id === record.pending.to); return { location: "windows", machineId: row.machineId, cli: row.cli, model: row.config.model ?? "" }; })()) : null,
|
|
1223
|
-
hold: record?.hold ?? (record?.pending ? record.pending.phase === "selected" ? "start" : "exit" : null), switching: this.generalLocationFlights.has(name) };
|
|
1224
|
-
}), machines: (this.runtimeHub?.listMachines() ?? []).filter(row => row.status !== "pending" && row.status !== "revoked").map(row => ({
|
|
1225
|
-
id: row.id, name: row.name, online: row.status === "online" && !!this.runtimeHub?.paneTransportEpoch(row.id),
|
|
1226
|
-
clis: row.inventory?.clis.filter(cli => cli.runnable && (cli.id === "claude-code" || cli.id === "codex")).map(cli => ({ id: cli.id, version: cli.version })) ?? [],
|
|
1227
|
-
})) };
|
|
1228
|
-
}
|
|
1229
|
-
/** Pure preflight for the confirmation diff and stale-tab refusal; never prepares files or starts a CLI. */
|
|
1230
|
-
previewGeneralLocation(raw) {
|
|
1231
|
-
const input = parseGeneralSwitchInput(raw), cfg = this.fleetConfig?.instances[input.name];
|
|
1232
|
-
if (!cfg?.general_topic || this.shuttingDown)
|
|
1233
|
-
throw Error("General is unavailable");
|
|
1234
|
-
const record = this.generalBindings().read(input.name);
|
|
1235
|
-
if (input.revision !== generalBindingRevision(record, cfg))
|
|
1236
|
-
throw Error("General location changed; read its settings again");
|
|
1237
|
-
const connections = cfg.channel_id ? [cfg.channel_id] : [];
|
|
1238
|
-
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
1239
|
-
if (channels.length && (connections.length !== 1 || channels.filter(row => (row.id ?? row.type) === connections[0]).length !== 1))
|
|
1240
|
-
throw Error("General's connection cannot be resolved");
|
|
1241
|
-
if (input.target.location === "host") {
|
|
1242
|
-
if (!record)
|
|
1243
|
-
throw Error("General's saved host location is unavailable");
|
|
1244
|
-
return { input, connections, summary: [t("general_location.confirm_host"), t("general_location.history")] };
|
|
1245
|
-
}
|
|
1246
|
-
const active = record?.windows.find(row => row.id === record.active);
|
|
1247
|
-
if (!record?.pending && record?.hold !== "start" && active?.machineId === input.target.machineId && active.cli === input.target.cli)
|
|
1248
|
-
throw Error("General already runs at this location");
|
|
1249
|
-
const machine = this.runtimeHub?.machine(input.target.machineId);
|
|
1250
|
-
if (!remoteRuntimeEnabled() || machine?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(machine.id))
|
|
1251
|
-
throw Error(t("general_location.offline"));
|
|
1252
|
-
const provider = input.target.cli;
|
|
1253
|
-
if (!machine.inventory?.clis.some(cli => cli.id === provider && cli.runnable))
|
|
1254
|
-
throw Error("The chosen Windows CLI is unavailable");
|
|
1255
|
-
return { input, connections, summary: [t("general_location.confirm_windows", settingsDisplay(machine.name)), t("general_location.history")] };
|
|
1256
|
-
}
|
|
1257
|
-
/** All entry points, including automatic native recovery, must respect an interrupted binding switch. */
|
|
1258
|
-
assertGeneralBindingStart(name, config) {
|
|
1259
|
-
if (!config.general_topic)
|
|
1260
|
-
return;
|
|
1261
|
-
try {
|
|
1262
|
-
this.generalBindings().assertStart(name, config);
|
|
1263
|
-
}
|
|
1264
|
-
catch (err) {
|
|
1265
|
-
this.logger.warn({ name, err }, "General location start held by its saved binding");
|
|
1266
|
-
this.needsYou?.poke();
|
|
1267
|
-
throw new UnsupportedCliError(t("general_location.exit_unconfirmed"));
|
|
1268
|
-
}
|
|
1269
|
-
}
|
|
1270
|
-
/** Only the Settings replay's confirmed admin can enter; the whole switch owns one lifecycle transition. */
|
|
1271
|
-
async switchGeneralLocation(raw, authorized, execution) {
|
|
1272
|
-
const { input } = this.previewGeneralLocation(raw), name = input.name;
|
|
1273
|
-
return this.lifecycle.runTransition(name, async (transition) => {
|
|
1274
|
-
execution.assert();
|
|
1275
|
-
if (!authorized() || this.generalLocationFlights.has(name))
|
|
1276
|
-
throw Error("General location authority expired or a switch is already running");
|
|
1277
|
-
const token = Symbol(name), generation = this.settingsGeneration;
|
|
1278
|
-
const destinationEpoch = input.target.location === "windows" ? this.runtimeHub?.paneTransportEpoch(input.target.machineId) : null;
|
|
1279
|
-
let expected = this.fleetConfig.instances[name], stopGeneration = this.explicitStopGeneration.get(name) ?? 0, epoch = this.lifecycle.epochOf(name);
|
|
1280
|
-
const hostSnapshot = !isWindowsInstance(expected) ? structuredClone(this.daemons.get(name)?.getConfigSnapshot() ?? expected) : undefined;
|
|
1281
|
-
const current = () => !this.shuttingDown && this.settingsGeneration === generation && authorized()
|
|
1282
|
-
&& this.generalLocationFlights.get(name) === token && this.fleetConfig?.instances[name] === expected
|
|
1283
|
-
&& (input.target.location !== "windows" || remoteRuntimeEnabled() && !!destinationEpoch && this.runtimeHub?.paneTransportEpoch(input.target.machineId) === destinationEpoch)
|
|
1284
|
-
&& (this.explicitStopGeneration.get(name) ?? 0) === stopGeneration && this.lifecycle.epochOf(name) === epoch;
|
|
1285
|
-
const check = () => { if (!current())
|
|
1286
|
-
throw Error("General location switch was superseded"); };
|
|
1287
|
-
this.generalLocationFlights.set(name, token);
|
|
1288
|
-
// Old ephemeral waiters carry this epoch; they cannot wake into the selected conversation.
|
|
1289
|
-
this.cancelPendingDeliveries(name);
|
|
1290
|
-
let lease = null;
|
|
1291
|
-
try {
|
|
1292
|
-
lease = await waitSettingsLease([settingsFileResource(this.getSettingsConfigPath()), `general:${this.dataDir}:${name}`], () => current() && execution.current(), performance.now() + execution.remainingMs, execution.owner);
|
|
1293
|
-
const store = this.generalBindings();
|
|
1294
|
-
const result = await switchGeneralLocation({ store, current: () => expected, assert: check,
|
|
1295
|
-
prepare: async (target, retained) => {
|
|
1296
|
-
if (target.location !== "windows")
|
|
1297
|
-
return null;
|
|
1298
|
-
const old = retained.windows.find(row => row.machineId === target.machineId && row.cli === target.cli);
|
|
1299
|
-
const id = old?.id ?? randomUUID(), agentId = old?.agentId ?? randomUUID();
|
|
1300
|
-
const publication = await this.prepareWindowsGeneralWorkspace(name, target.machineId, target.cli, id, () => current() && execution.current());
|
|
1301
|
-
check();
|
|
1302
|
-
execution.assert();
|
|
1303
|
-
const config = { ...structuredClone(expected), runtime: "windows", machine_id: target.machineId,
|
|
1304
|
-
native_adapter: "pane", native_agent_id: agentId, native_general_binding: id, native_sandbox: "workspace-write",
|
|
1305
|
-
backend: target.cli, model: target.model, working_directory: publication.cwd, systemPrompt: "", agent_mode: "cli" };
|
|
1306
|
-
// Host credentials, shell hooks and provider-specific options never become Windows launch configuration.
|
|
1307
|
-
for (const key of ["backend_options", "credential_profile", "pre_task_command"])
|
|
1308
|
-
delete config[key];
|
|
1309
|
-
windowsAgentsFor(this, this.runtimeWindowsExecutor()).ensureGeneralBinding({ id: agentId, instance: name, machineId: target.machineId,
|
|
1310
|
-
cli: target.cli, cwd: publication.cwd, name: (expected.display_name ?? name).slice(0, 64), model: target.model, instructions: "", description: expected.description ?? "" }, () => current() && execution.current());
|
|
1311
|
-
return { id, agentId, machineId: target.machineId, cli: target.cli, config };
|
|
1312
|
-
},
|
|
1313
|
-
idle: async () => {
|
|
1314
|
-
check();
|
|
1315
|
-
const daemon = this.daemons.get(name);
|
|
1316
|
-
return !daemon || daemon.getInstanceStateSnapshot().state === "idle" && daemon.getWorkingRecoveryState().idle
|
|
1317
|
-
&& daemon.getInteractionSnapshot().phase === "clear" && !this.lifecycle.hasWorkLease(name)
|
|
1318
|
-
&& Date.now() - (this.lastDeliveryAt.get(name) ?? 0) >= 2000;
|
|
1319
|
-
},
|
|
1320
|
-
hostSnapshot: () => hostSnapshot,
|
|
1321
|
-
capture: async () => {
|
|
1322
|
-
if (isWindowsInstance(expected))
|
|
1323
|
-
return undefined;
|
|
1324
|
-
const daemon = this.daemons.get(name);
|
|
1325
|
-
if (!daemon)
|
|
1326
|
-
throw Error(t("general_location.exit_unconfirmed"));
|
|
1327
|
-
const owner = await daemon.generalHostOwner();
|
|
1328
|
-
check();
|
|
1329
|
-
execution.assert();
|
|
1330
|
-
return owner;
|
|
1331
|
-
},
|
|
1332
|
-
stop: async (owner) => {
|
|
1333
|
-
check();
|
|
1334
|
-
const stopping = this.stopInstance(name, transition);
|
|
1335
|
-
stopGeneration++;
|
|
1336
|
-
epoch++;
|
|
1337
|
-
await stopping;
|
|
1338
|
-
check();
|
|
1339
|
-
if (isWindowsInstance(expected)) {
|
|
1340
|
-
const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(expected.native_agent_id);
|
|
1341
|
-
if (profile.activeId || profile.paneLaunchId)
|
|
1342
|
-
throw Error(t("general_location.exit_unconfirmed"));
|
|
1343
|
-
execution.mutate(() => this.holdGeneralOldDeliveries(name));
|
|
1344
|
-
return undefined;
|
|
1345
|
-
}
|
|
1346
|
-
if (!owner || !await confirmGeneralHostExit(owner, getTmuxSession()))
|
|
1347
|
-
throw Error(t("general_location.exit_unconfirmed"));
|
|
1348
|
-
check();
|
|
1349
|
-
execution.mutate(() => this.holdGeneralOldDeliveries(name));
|
|
1350
|
-
return owner.pin;
|
|
1351
|
-
},
|
|
1352
|
-
save: next => {
|
|
1353
|
-
check();
|
|
1354
|
-
const previous = expected;
|
|
1355
|
-
try {
|
|
1356
|
-
this.fleetConfig.instances[name] = next;
|
|
1357
|
-
this.saveFleetConfig([{ path: ["instances", name], value: next }]);
|
|
1358
|
-
expected = next;
|
|
1359
|
-
this.routing.rebuild(this.fleetConfig);
|
|
1360
|
-
}
|
|
1361
|
-
catch (err) {
|
|
1362
|
-
this.fleetConfig.instances[name] = previous;
|
|
1363
|
-
throw err;
|
|
1364
|
-
}
|
|
1365
|
-
},
|
|
1366
|
-
start: async () => {
|
|
1367
|
-
check();
|
|
1368
|
-
if (!execution.completed)
|
|
1369
|
-
throw Error("General's selected binding has no committed approval");
|
|
1370
|
-
// The committed receipt permits only this selected startup. Live authority still fences every awaited spawn boundary.
|
|
1371
|
-
const startup = new SettingsExecution({ current: () => current() && execution.completed && execution.remainingMs > 0,
|
|
1372
|
-
snapshot: () => this.fleetConfig });
|
|
1373
|
-
try {
|
|
1374
|
-
await this.startInstance(name, expected, true, "fleet-topic", false, transition, startup);
|
|
1375
|
-
}
|
|
1376
|
-
finally {
|
|
1377
|
-
startup.close();
|
|
1378
|
-
}
|
|
1379
|
-
check();
|
|
1380
|
-
const daemon = this.daemons.get(name);
|
|
1381
|
-
if (!daemon || daemon.getProcessStatus() !== "running" || isNativePaneInstance(expected) && !daemon.nativePaneCurrent())
|
|
1382
|
-
throw Error(t("general_location.start_failed"));
|
|
1383
|
-
await this.connectIpcToInstance(name);
|
|
1384
|
-
check();
|
|
1385
|
-
},
|
|
1386
|
-
notify: (kind, err) => {
|
|
1387
|
-
const message = t(kind === "exit" ? "general_location.exit_unconfirmed" : kind === "start" ? "general_location.start_failed" : "general_location.complete", isWindowsInstance(expected) ? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host"));
|
|
1388
|
-
this.eventLog?.insert(name, "general_location", { phase: kind, message });
|
|
1389
|
-
if (err)
|
|
1390
|
-
this.logger.warn({ name, phase: kind, err }, message);
|
|
1391
|
-
else
|
|
1392
|
-
this.logger.info({ name, phase: kind }, message);
|
|
1393
|
-
this.needsYou?.poke();
|
|
1394
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1395
|
-
this.notifyInstanceTopic(name, message);
|
|
1396
|
-
},
|
|
1397
|
-
}, input, execution);
|
|
1398
|
-
return { ...result, message: result.started ? t("general_location.complete", isWindowsInstance(expected)
|
|
1399
|
-
? this.runtimeHub?.machine(expected.machine_id)?.name ?? t("general_location.windows") : t("general_location.host")) : t("general_location.start_failed") };
|
|
1400
|
-
}
|
|
1401
|
-
finally {
|
|
1402
|
-
lease?.release();
|
|
1403
|
-
if (this.generalLocationFlights.get(name) === token)
|
|
1404
|
-
this.generalLocationFlights.delete(name);
|
|
1405
|
-
this.needsYou?.poke();
|
|
1406
|
-
}
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
/** An offline PC or a confirmed binding boundary holds old waiting input; it never becomes another conversation's queue. */
|
|
1410
|
-
holdGeneralOldDeliveries(name) {
|
|
1411
|
-
const outbox = this.deliveryOutbox;
|
|
1412
|
-
if (!outbox)
|
|
1413
|
-
return;
|
|
1414
|
-
for (const row of outbox.listPending().filter(row => row.targetInstance === name)) {
|
|
1415
|
-
if (row.state !== "submission_started")
|
|
1416
|
-
outbox.failBeforeBegin(row.deliveryId, t("general_location.input_not_replayed"));
|
|
1417
|
-
else if (row.targetDaemonBootId) {
|
|
1418
|
-
outbox.markTargetReconciliationPending(name, row.targetDaemonBootId);
|
|
1419
|
-
if (!outbox.reconcileAttempt(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "uncertain", t("general_location.input_not_replayed")))
|
|
1420
|
-
throw Error("General's old delivery could not be held");
|
|
1421
|
-
}
|
|
1422
|
-
else
|
|
1423
|
-
throw Error("General's old delivery has no confirmed owner");
|
|
1424
|
-
}
|
|
1425
|
-
this.needsYou?.poke();
|
|
1426
|
-
}
|
|
1427
|
-
generalLocationDeliveryHold(name) {
|
|
1428
|
-
const config = this.fleetConfig?.instances[name];
|
|
1429
|
-
if (!config?.general_topic)
|
|
1430
|
-
return null;
|
|
1431
|
-
if (this.generalLocationFlights?.has(name))
|
|
1432
|
-
return t("general_location.exit_unconfirmed");
|
|
1433
|
-
let record;
|
|
1434
|
-
try {
|
|
1435
|
-
record = this.generalBindings().read(name);
|
|
1436
|
-
}
|
|
1437
|
-
catch {
|
|
1438
|
-
return t("general_location.start_failed");
|
|
1439
|
-
}
|
|
1440
|
-
if (record?.pending || record?.hold)
|
|
1441
|
-
return t(record.pending?.phase === "selected" || record.hold === "start" ? "general_location.start_failed" : "general_location.exit_unconfirmed");
|
|
1442
|
-
if (isWindowsInstance(config) && config.machine_id && (!remoteRuntimeEnabled() || this.runtimeHub?.machine(config.machine_id)?.status !== "online" || !this.runtimeHub?.paneTransportEpoch(config.machine_id)))
|
|
1443
|
-
return t("general_location.offline");
|
|
1444
|
-
return null;
|
|
1445
|
-
}
|
|
1446
|
-
generalLocationAttention(name) {
|
|
1447
|
-
const cfg = this.fleetConfig?.instances[name];
|
|
1448
|
-
if (!cfg?.general_topic)
|
|
1449
|
-
return null;
|
|
1450
|
-
const detail = this.generalLocationDeliveryHold(name);
|
|
1451
|
-
if (!detail) {
|
|
1452
|
-
this.generalAttentionEpisodes.delete(name);
|
|
1453
|
-
return null;
|
|
1454
|
-
}
|
|
1455
|
-
let record = null;
|
|
1456
|
-
try {
|
|
1457
|
-
record = this.generalBindings().read(name);
|
|
1458
|
-
}
|
|
1459
|
-
catch { /* a corrupt journal is a visible hold too */ }
|
|
1460
|
-
const offline = !record?.pending && !record?.hold && isWindowsInstance(cfg) && detail === t("general_location.offline");
|
|
1461
|
-
const key = record?.pending?.id ?? `${record?.revision ?? "unknown"}:${offline ? "offline" : "hold"}`;
|
|
1462
|
-
let episode = this.generalAttentionEpisodes.get(name);
|
|
1463
|
-
if (!episode || episode.key !== key) {
|
|
1464
|
-
episode = { key, since: record?.pending?.at ?? Date.now() };
|
|
1465
|
-
this.generalAttentionEpisodes.set(name, episode);
|
|
1466
|
-
}
|
|
1467
|
-
return { ...episode, detail, offline };
|
|
1468
|
-
}
|
|
1469
|
-
async nativePaneFor(name, config, assertCurrent) {
|
|
1470
|
-
try {
|
|
1471
|
-
return await (await this.nativePanes()).prepare(name, config, assertCurrent);
|
|
1472
|
-
}
|
|
1473
|
-
catch (err) {
|
|
1474
|
-
if (config.general_topic && err instanceof GeneralManagedFileError) {
|
|
1475
|
-
assertCurrent();
|
|
1476
|
-
const store = this.generalBindings(), record = store.read(name);
|
|
1477
|
-
if (record && record.active === config.native_general_binding) {
|
|
1478
|
-
record.hold = "start";
|
|
1479
|
-
store.write(record);
|
|
1480
|
-
}
|
|
1481
|
-
this.logger.warn({ name, managedFile: err.file }, t("general_location.start_failed"));
|
|
1482
|
-
this.needsYou?.poke();
|
|
1483
|
-
throw new UnsupportedCliError(t("general_location.start_failed"));
|
|
1484
|
-
}
|
|
1485
|
-
throw err;
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
async recoverNativePane(name, launchId, authorized) {
|
|
1489
|
-
const config = this.fleetConfig?.instances[name];
|
|
1490
|
-
if (!isNativePaneInstance(config))
|
|
1491
|
-
throw Error("Persistent Windows pane is not configured");
|
|
1492
|
-
const generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1493
|
-
await this.lifecycle.runTransition(name, async (transition) => {
|
|
1494
|
-
const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
|
|
1495
|
-
throw Error(t("native_pane.authority_expired")); };
|
|
1496
|
-
current();
|
|
1497
|
-
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1498
|
-
throw Error(t("native_pane.still_running"));
|
|
1499
|
-
this.cancelStartupRetry(name);
|
|
1500
|
-
this.lifecycle.cancelNativeRecovery(name);
|
|
1501
|
-
await (await this.nativePanes()).recover(name, config, launchId, true, current);
|
|
1502
|
-
current();
|
|
1503
|
-
// The host supplied a positive exit before ordinary lifecycle cleanup. No signal or input is required.
|
|
1504
|
-
await this.lifecycle.stop(name, transition);
|
|
1505
|
-
current();
|
|
1506
|
-
this.instanceProcessStatus.set(name, "stopped");
|
|
1507
|
-
this.eventLog?.insert(name, "native_pane_recovered", { launchId, confirmedStopped: true });
|
|
1508
|
-
this.logger.info({ name, launchId }, "Windows saved pane recovered by an administrator; waiting for Start");
|
|
1509
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
holdNativePaneRecovery(name, error) {
|
|
1513
|
-
if (!(error instanceof NativePaneRecoveryError))
|
|
1514
|
-
return false;
|
|
1515
|
-
this.instanceProcessStatus.set(name, "stopped");
|
|
1516
|
-
this.logger.warn({ name, recovery: error.recovery }, error.message);
|
|
1517
|
-
this.eventLog?.insert(name, "native_pane_recovery_needed", { ...error.recovery });
|
|
1518
|
-
this.needsYou?.poke();
|
|
1519
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1520
|
-
return true;
|
|
1521
|
-
}
|
|
1522
|
-
async startFreshNativePane(name, launchId, sessionId, authorized) {
|
|
1523
|
-
const config = this.fleetConfig?.instances[name];
|
|
1524
|
-
if (!isNativePaneInstance(config))
|
|
1525
|
-
throw Error("Persistent Windows pane is not configured");
|
|
1526
|
-
const generation = this.explicitStopGeneration.get(name) ?? 0;
|
|
1527
|
-
await this.lifecycle.runTransition(name, async (transition) => {
|
|
1528
|
-
const current = () => { if (!authorized() || this.shuttingDown || this.fleetConfig?.instances[name] !== config || (this.explicitStopGeneration.get(name) ?? 0) !== generation)
|
|
1529
|
-
throw Error(t("native_pane.authority_expired")); };
|
|
1530
|
-
current();
|
|
1531
|
-
const held = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id).paneRecovery;
|
|
1532
|
-
if (held?.kind !== "session-unconfirmed" || held.launchId !== launchId || (held.sessionId ?? null) !== sessionId)
|
|
1533
|
-
throw Error(t("native_pane.changed"));
|
|
1534
|
-
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1535
|
-
throw Error(t("native_pane.still_running"));
|
|
1536
|
-
this.cancelStartupRetry(name);
|
|
1537
|
-
this.lifecycle.cancelNativeRecovery(name);
|
|
1538
|
-
await this.lifecycle.stop(name, transition);
|
|
1539
|
-
current();
|
|
1540
|
-
try {
|
|
1541
|
-
await (await this.nativePanes()).startFresh(name, config, launchId, sessionId, current);
|
|
1542
|
-
current();
|
|
1543
|
-
}
|
|
1544
|
-
catch (error) {
|
|
1545
|
-
current();
|
|
1546
|
-
this.needsYou?.poke();
|
|
1547
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1548
|
-
throw error;
|
|
1549
|
-
}
|
|
1550
|
-
this.eventLog?.insert(name, "native_pane_new_conversation", { launchId, sessionId });
|
|
1551
|
-
await this.startInstance(name, config, true, "fleet-topic", true, transition);
|
|
1552
|
-
current();
|
|
1553
|
-
this.needsYou?.poke();
|
|
1554
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1555
|
-
});
|
|
1556
|
-
}
|
|
1557
|
-
nativePanePinnedSession(name) {
|
|
1558
|
-
const config = this.fleetConfig?.instances[name];
|
|
1559
|
-
if (!isNativePaneInstance(config) || !config.native_agent_id)
|
|
1560
|
-
return null;
|
|
1561
|
-
const agent = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
|
|
1562
|
-
return agent.fleet_instance === name && agent.machineId === config.machine_id ? agent.threadId ?? null : null;
|
|
1563
|
-
}
|
|
1564
|
-
onNativePaneRecoverySettled(name) {
|
|
1565
|
-
if (this.daemons.get(name)?.nativePaneCurrent())
|
|
1566
|
-
this.instanceProcessStatus.delete(name);
|
|
1567
|
-
this.onAttentionChanged(name);
|
|
1568
|
-
this.scheduleDeliveryOutboxPump();
|
|
1569
|
-
}
|
|
1570
|
-
setNativePanePaused(name, paused) {
|
|
1571
|
-
const id = this.fleetConfig?.instances[name]?.native_agent_id;
|
|
1572
|
-
if (!id)
|
|
1573
|
-
throw Error("Native profile is missing");
|
|
1574
|
-
windowsAgentsFor(this, this.runtimeWindowsExecutor()).setPaused(id, paused);
|
|
1575
|
-
}
|
|
1576
|
-
async closeRetainedNativePane(name) {
|
|
1577
|
-
const config = this.fleetConfig?.instances[name];
|
|
1578
|
-
if (!config)
|
|
1579
|
-
throw Error("Native instance configuration is missing");
|
|
1580
|
-
await (await this.nativePanes()).closeRetained(name, config, () => { if (this.shuttingDown)
|
|
1581
|
-
throw Error("Fleet is stopping"); });
|
|
1582
|
-
}
|
|
1583
|
-
async reconcileNativePaneBeforeStart(name, daemon) {
|
|
1584
|
-
const outbox = this.deliveryOutbox;
|
|
1585
|
-
if (!outbox)
|
|
1586
|
-
return true;
|
|
1587
|
-
const epoch = this.lifecycle.epochOf(name);
|
|
1588
|
-
return daemon.reconcileNativeDeliveries(outbox, () => !this.shuttingDown && this.lifecycle.epochOf(name) === epoch);
|
|
1589
|
-
}
|
|
1590
|
-
/** M-2255: the Windows runtimes' switches the fleet's own state reflects (null until the start-up has applied them). */
|
|
1591
|
-
runtimeEffects = null;
|
|
1592
|
-
runtimeEffectsScheduled = false;
|
|
1593
|
-
localPaneDrain = null;
|
|
1594
|
-
remotePaneDrain = null;
|
|
1595
|
-
/** Revoke admissions synchronously; keep the captured transport only long enough for exact owned closes. */
|
|
1596
|
-
drainNativeRuntime(remote) {
|
|
1597
|
-
const existing = remote ? this.remotePaneDrain : this.localPaneDrain;
|
|
1598
|
-
if (existing)
|
|
1599
|
-
return existing;
|
|
1600
|
-
const hub = this.runtimeHub, relay = this.localPaneRelay;
|
|
1601
|
-
if (!remote)
|
|
1602
|
-
relay?.beginCleanup();
|
|
1603
|
-
const targets = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, config]) => isWindowsInstance(config) && !!config.machine_id === remote);
|
|
1604
|
-
for (const [name, config] of targets) {
|
|
1605
|
-
this.nativeMcpStore?.closeInstance(name);
|
|
1606
|
-
if (isNativePaneInstance(config)) {
|
|
1607
|
-
this.lifecycle.cancelNativeRecovery(name);
|
|
1608
|
-
this.lifecycle.invalidate(name);
|
|
1609
|
-
this.daemons.get(name)?.fenceDeliveryWritesForStop();
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
const run = Promise.resolve().then(async () => {
|
|
1613
|
-
const results = await Promise.allSettled(targets.filter(([, config]) => isNativePaneInstance(config)).map(([name]) => this.stopInstance(name)));
|
|
1614
|
-
results.forEach((result, index) => { if (result.status === "rejected")
|
|
1615
|
-
this.logger.warn({ err: result.reason, remote, index }, "Windows pane cleanup unconfirmed; its reservation is retained"); });
|
|
1616
|
-
try {
|
|
1617
|
-
if (remote)
|
|
1618
|
-
await hub?.shutdownPaneHosts();
|
|
1619
|
-
else
|
|
1620
|
-
await relay?.shutdownHost();
|
|
1621
|
-
}
|
|
1622
|
-
catch (error) {
|
|
1623
|
-
this.logger.warn({ err: error, remote }, "Windows pane host shutdown unconfirmed; owned state is retained");
|
|
1624
|
-
}
|
|
1625
|
-
}).finally(() => {
|
|
1626
|
-
if (remote) {
|
|
1627
|
-
hub?.close();
|
|
1628
|
-
if (this.runtimeHub === hub)
|
|
1629
|
-
this.runtimeHub = null;
|
|
1630
|
-
}
|
|
1631
|
-
else {
|
|
1632
|
-
relay?.stop();
|
|
1633
|
-
if (this.localPaneRelay === relay)
|
|
1634
|
-
this.localPaneRelay = null;
|
|
1635
|
-
closeWindowsBridge(this);
|
|
1636
|
-
}
|
|
1637
|
-
});
|
|
1638
|
-
if (remote)
|
|
1639
|
-
this.remotePaneDrain = run;
|
|
1640
|
-
else
|
|
1641
|
-
this.localPaneDrain = run;
|
|
1642
|
-
void run.finally(() => {
|
|
1643
|
-
if (remote) {
|
|
1644
|
-
if (this.remotePaneDrain === run)
|
|
1645
|
-
this.remotePaneDrain = null;
|
|
1646
|
-
}
|
|
1647
|
-
else if (this.localPaneDrain === run)
|
|
1648
|
-
this.localPaneDrain = null;
|
|
1649
|
-
if (!this.shuttingDown && (remote ? remoteRuntimeEnabled() : localWindowsCliEnabled())) {
|
|
1650
|
-
this.initializeNativeInstances();
|
|
1651
|
-
this.initializeNativeInstances(true);
|
|
1652
|
-
}
|
|
1653
|
-
try {
|
|
1654
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1655
|
-
}
|
|
1656
|
-
catch { }
|
|
1657
|
-
}).catch(error => this.logger.warn({ err: error }, "Windows runtime cleanup failed; retained ownership requires reconciliation"));
|
|
1658
|
-
return run;
|
|
1659
|
-
}
|
|
1660
|
-
/**
|
|
1661
|
-
* M-2255: Settings → Runtime's switches (fleet.yaml `runtimes`, or their environment override) in force — set each time
|
|
1662
|
-
* the configuration in force changes (a load, a save, a reload's rollback), next to the web authority's. Every gate reads
|
|
1663
|
-
* them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
|
|
1664
|
-
* sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
|
|
1665
|
-
*/
|
|
1666
|
-
observeRuntimeSwitches() {
|
|
1667
|
-
setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
|
|
1668
|
-
if (!this.runtimeEffects || this.runtimeEffectsScheduled)
|
|
1669
|
-
return;
|
|
1670
|
-
this.runtimeEffectsScheduled = true;
|
|
1671
|
-
queueMicrotask(() => { this.runtimeEffectsScheduled = false; this.applyRuntimeSwitches(); });
|
|
1672
|
-
}
|
|
1673
|
-
/**
|
|
1674
|
-
* Turned off: what it allowed ends now — the paired computers' connections (their agents show as offline) or this PC's
|
|
1675
|
-
* Windows process (a turn in progress is cancelled, as when the fleet stops), and those agents' tool sessions.
|
|
1676
|
-
* Turned on: the Windows agents are registered and their chat history read, as at start-up.
|
|
1677
|
-
*/
|
|
1678
|
-
applyRuntimeSwitches() {
|
|
1679
|
-
const was = this.runtimeEffects;
|
|
1680
|
-
if (!was || this.shuttingDown)
|
|
1681
|
-
return;
|
|
1682
|
-
const now = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
|
|
1683
|
-
if (was.windows_cli === now.windows_cli && was.remote_runtime === now.remote_runtime)
|
|
1684
|
-
return;
|
|
1685
|
-
this.runtimeEffects = now;
|
|
1686
|
-
try {
|
|
1687
|
-
if (was.remote_runtime && !now.remote_runtime) {
|
|
1688
|
-
void this.drainNativeRuntime(true);
|
|
1689
|
-
}
|
|
1690
|
-
if (was.windows_cli && !now.windows_cli) {
|
|
1691
|
-
void this.drainNativeRuntime(false);
|
|
1692
|
-
}
|
|
1693
|
-
if (!was.windows_cli && !was.remote_runtime && !this.localPaneDrain && !this.remotePaneDrain) {
|
|
1694
|
-
this.initializeNativeInstances();
|
|
1695
|
-
this.initializeNativeInstances(true);
|
|
1696
|
-
}
|
|
1697
|
-
this.logger.info({ windows_cli: now.windows_cli, remote_runtime: now.remote_runtime }, "Windows runtime switches applied");
|
|
1698
|
-
}
|
|
1699
|
-
catch (error) {
|
|
1700
|
-
this.logger.warn({ err: error }, "Windows runtime switches could not be applied in full");
|
|
1701
|
-
}
|
|
1702
|
-
try {
|
|
1703
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
1704
|
-
}
|
|
1705
|
-
catch { /* the next status frame carries it */ }
|
|
1706
|
-
}
|
|
1707
|
-
/** An unavailable opt-in runtime must not prevent the Linux fleet from starting or restoring its own history. */
|
|
1708
|
-
initializeNativeInstances(history = false) {
|
|
1709
|
-
if (!windowsCliEnabled())
|
|
1710
|
-
return;
|
|
1711
|
-
try {
|
|
1712
|
-
const runtime = this.nativeInstanceRuntime();
|
|
1713
|
-
if (history)
|
|
1714
|
-
runtime.importHistory();
|
|
1715
|
-
else
|
|
1716
|
-
runtime.register();
|
|
1717
|
-
}
|
|
1718
|
-
catch (error) {
|
|
1719
|
-
this.logger.warn({ err: error }, "Windows runtime state could not be loaded; affected native instances remain unavailable");
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
/** Native CLIs are headless: the manager owns the durable delivery generation instead of a tmux daemon. */
|
|
1723
|
-
deliveryBootId(name) {
|
|
1724
|
-
const config = this.fleetConfig?.instances[name];
|
|
1725
|
-
if (isNativeExecInstance(config)) {
|
|
1726
|
-
if (!this.nativeInstanceRuntime().available(name))
|
|
1727
|
-
return null;
|
|
1728
|
-
const machine = config.machine_id ? this.runtimeCoordinator().machine(config.machine_id) : null;
|
|
1729
|
-
return machine ? `runtime:${machine.id}:${machine.bootId ?? machine.generation}` : this.managerBootId;
|
|
1730
|
-
}
|
|
1731
|
-
return this.daemons.get(name)?.bootId ?? null;
|
|
1732
|
-
}
|
|
1733
771
|
rawFleetConfig = {};
|
|
1734
772
|
rawFleetDocument = null;
|
|
1735
773
|
/** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
|
|
@@ -1880,6 +918,7 @@ export class FleetManager {
|
|
|
1880
918
|
startupRetryNotices = new Map();
|
|
1881
919
|
/** Unsupported-CLI refusals waiting to go out, grouped by reason (#1109). */
|
|
1882
920
|
unsupportedCliNotices = new Map();
|
|
921
|
+
kiroCompatibilityHolds = new Map();
|
|
1883
922
|
kiroIncompatNotices = new Map();
|
|
1884
923
|
/** Backoff between automatic startup retries; the last step repeats while the backend is down. */
|
|
1885
924
|
static STARTUP_RETRY_BACKOFF_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
|
@@ -1908,6 +947,7 @@ export class FleetManager {
|
|
|
1908
947
|
kiroMigrationRuns = new Map();
|
|
1909
948
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
1910
949
|
needsYou = null;
|
|
950
|
+
missedOfflineStore = null;
|
|
1911
951
|
configHistory = null;
|
|
1912
952
|
configHistoryTimer = null;
|
|
1913
953
|
loadedFleetSource = null;
|
|
@@ -2023,6 +1063,9 @@ export class FleetManager {
|
|
|
2023
1063
|
discordPresenceEagerPending = false;
|
|
2024
1064
|
discordPresenceInFlight = null;
|
|
2025
1065
|
static DISCORD_PRESENCE_REFRESH_MS = 15 * 60_000;
|
|
1066
|
+
/** #3066 §1d: how often a fleet with no Discord bot reads the quota for its history (the user's choice: 30 min). */
|
|
1067
|
+
static QUOTA_BACKGROUND_READ_MS = 30 * 60_000;
|
|
1068
|
+
quotaReadTimer = null;
|
|
2026
1069
|
/** Days of event/activity history to keep. */
|
|
2027
1070
|
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
2028
1071
|
watchdogTimer = null;
|
|
@@ -2529,8 +1572,6 @@ export class FleetManager {
|
|
|
2529
1572
|
* authorizing revoked links and cookies until the fleet restarted.
|
|
2530
1573
|
*/
|
|
2531
1574
|
settingsGeneration = 0;
|
|
2532
|
-
/** #2942: the settings baseline the agent-requested Windows changes are checked against (set with the gate). */
|
|
2533
|
-
settingsBaselineReader = null;
|
|
2534
1575
|
settingsRuntimeRevision = 0;
|
|
2535
1576
|
settingsConfirmation = null;
|
|
2536
1577
|
settingsControl = null;
|
|
@@ -2587,9 +1628,8 @@ export class FleetManager {
|
|
|
2587
1628
|
return this.settingsConfirmation;
|
|
2588
1629
|
const generation = this.settingsGeneration;
|
|
2589
1630
|
const current = () => !this.shuttingDown && this.settingsGeneration === generation;
|
|
2590
|
-
const baselines =
|
|
1631
|
+
const baselines = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
|
|
2591
1632
|
config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
|
|
2592
|
-
generalLocation: input => this.previewGeneralLocation(input),
|
|
2593
1633
|
proof: (kind, target, id, binding, key) => {
|
|
2594
1634
|
const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
|
|
2595
1635
|
const proof = map.get(id);
|
|
@@ -2610,17 +1650,6 @@ export class FleetManager {
|
|
|
2610
1650
|
const store = new SettingsConfirmationStore({
|
|
2611
1651
|
audit: (event, fields) => this.logger.info({ event: `settings_confirmation.${event}`, ...fields }, "Settings confirmation audit"),
|
|
2612
1652
|
notify: view => view.state === "pending" ? this.promptSettingsChange(store, view) : Promise.resolve(),
|
|
2613
|
-
// alpha.2 (M-2300): an agent's request came, was decided or ran out — the open pages read the list again (a ping
|
|
2614
|
-
// with nothing in it: only a web-only fleet's verified owner may read it, GET /ui/agent-proposals).
|
|
2615
|
-
changed: view => {
|
|
2616
|
-
if (view.source !== "agent_request")
|
|
2617
|
-
return;
|
|
2618
|
-
this.agentProposalsRev++;
|
|
2619
|
-
broadcastSseEvent(this.sseClients, "agent_proposals", {}, err => this.logger.debug({ err }, "agent_proposals event failed"));
|
|
2620
|
-
// …and a status frame with the new revision: a tab from before the upgrade (its stream knows no `agent_proposals`)
|
|
2621
|
-
// applies status frames, and a polling page reads the same frame at its next /ui/poll.
|
|
2622
|
-
this.emitSseEvent("status", this.getUiStatus());
|
|
2623
|
-
},
|
|
2624
1653
|
});
|
|
2625
1654
|
const gate = new SettingsHttpConfirmation(store, {
|
|
2626
1655
|
principal: req => {
|
|
@@ -2634,7 +1663,6 @@ export class FleetManager {
|
|
|
2634
1663
|
&& sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
|
|
2635
1664
|
}, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
|
|
2636
1665
|
selfConfirm: (req, id) => this.settingsSelfConfirmation(store, req, id),
|
|
2637
|
-
directAgentCreation: true,
|
|
2638
1666
|
job: async (id) => {
|
|
2639
1667
|
const settlement = this.settingsJobSettlements.get(id);
|
|
2640
1668
|
if (!settlement)
|
|
@@ -2669,10 +1697,8 @@ export class FleetManager {
|
|
|
2669
1697
|
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
2670
1698
|
if (auth.kind !== "ok")
|
|
2671
1699
|
return null;
|
|
2672
|
-
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
|
|
2673
|
-
|
|
2674
|
-
// opened): the web-only fleet's owner, judged below by the same authority as here (hostOperator).
|
|
2675
|
-
if (gatewayRequestContext(req) && auth.session.remote !== "totp")
|
|
1700
|
+
// #2453 (§0.2): never over the public link — not even a named-link session that is web-verified there.
|
|
1701
|
+
if (gatewayRequestContext(req))
|
|
2676
1702
|
return null;
|
|
2677
1703
|
// …and never a change that turns the named-link option on: that one needs a General's card or the host.
|
|
2678
1704
|
if (!store.selfConfirmable(id))
|
|
@@ -2705,97 +1731,30 @@ export class FleetManager {
|
|
|
2705
1731
|
if (own.length !== 1 || own[0].type !== chat.platform || !chat.admin(actor.user_id))
|
|
2706
1732
|
return null;
|
|
2707
1733
|
const affected = [...store.affectedConnections(id)]; // read now: a finished change no longer has them
|
|
2708
|
-
|
|
2709
|
-
if (!destinations.length)
|
|
1734
|
+
if (!this.settingsNoticeGenerals(affected, chat.platform).length)
|
|
2710
1735
|
return null;
|
|
2711
1736
|
const name = this.webPerson({ adapterId: actor.adapter_id, userId: actor.user_id }).name;
|
|
2712
1737
|
return {
|
|
2713
1738
|
label: t("settings_confirm.web_verified_label", name),
|
|
2714
1739
|
// admin() asks chat.current() too; a General a card could go to must still be there (else the host CLI's).
|
|
2715
|
-
current: () => !!verified() && chat.admin(actor.user_id) &&
|
|
2716
|
-
followStop: async (name, begin) => { const route = destinations.find(item => item.current()); if (!route)
|
|
2717
|
-
throw Error("Settings route expired"); await route.followStop(name, begin); },
|
|
1740
|
+
current: () => !!verified() && chat.admin(actor.user_id) && this.settingsNoticeGenerals(affected, chat.platform).length > 0,
|
|
2718
1741
|
done: view => this.settingsWebConfirmedNotice(view, affected, name, chat.platform),
|
|
2719
1742
|
};
|
|
2720
1743
|
}
|
|
2721
|
-
/**
|
|
2722
|
-
* alpha.2 (M-2300; #2973's agent requests, junsion's §6): on a fleet with no chat connection there is no General to
|
|
2723
|
-
* post an agent's Windows request in — it waited for `agend settings confirm` on the host. Its owner (decision
|
|
2724
|
-
* 28c2d3a3: the `agend web --code` session on this computer, web-verified) now sees and decides it from the web's
|
|
2725
|
-
* Needs you. The authority is asked from the request each time, and again at execution by the store (`current`):
|
|
2726
|
-
* still that owner now (this computer, not revoked, signed out or run out), still a fleet with no chat connection,
|
|
2727
|
-
* never over the public link. Anyone else — a guest, a LAN or public-link session, a chat-connected fleet's admin —
|
|
2728
|
-
* gets nothing (a chat fleet keeps General's card as its way).
|
|
2729
|
-
*/
|
|
2730
|
-
/**
|
|
2731
|
-
* #2997 review (Prism P2): how many times an agent's request came or was decided — in a web-only fleet's status frame,
|
|
2732
|
-
* so a page that never hears the stream's ping (one polling /ui/poll, one reconnecting, a tab from before the upgrade)
|
|
2733
|
-
* reads the list again when it moves. A count, nothing more.
|
|
2734
|
-
*/
|
|
2735
|
-
agentProposalsRev = 0;
|
|
2736
|
-
agentProposalOwner(req) {
|
|
2737
|
-
// #3008: this computer's admin here, or signed in from outside (over the link connections from outside opened) —
|
|
2738
|
-
// asked of the LIVE session each time (#2997 review): a sign-out, an expiry, a revocation or the switch going off
|
|
2739
|
-
// while the request is applied fences it.
|
|
2740
|
-
return this.liveOwner(req, { hostOnly: false });
|
|
2741
|
-
}
|
|
2742
|
-
/** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
|
|
2743
|
-
webAgentProposals(req) {
|
|
2744
|
-
if (!this.agentProposalOwner(req))
|
|
2745
|
-
return null;
|
|
2746
|
-
return this.settingsGate().store.listBySource("agent_request");
|
|
2747
|
-
}
|
|
2748
|
-
/**
|
|
2749
|
-
* alpha.2: the owner's Approve or Reject of an agent's request. The store claims it before its first await (a second
|
|
2750
|
-
* click, from here or the host, finds it no longer pending) and asks the owner, the request (its fleet generation and
|
|
2751
|
-
* Windows switch) and the target machine again before and while it runs the same creation code the web uses. The
|
|
2752
|
-
* answer is always the request as it now stands: applied (with the new agent's name), rejected, expired, stale or
|
|
2753
|
-
* failed (with its reason), or already decided.
|
|
2754
|
-
*/
|
|
2755
|
-
async webDecideAgentProposal(req, id, decision) {
|
|
2756
|
-
const owner = this.agentProposalOwner(req);
|
|
2757
|
-
if (!owner)
|
|
2758
|
-
return { status: 403, body: { error: "owner_required", message: t("settings_confirm.web_owner_required") } };
|
|
2759
|
-
const store = this.settingsGate().store, before = store.inspect(id);
|
|
2760
|
-
if (!before || before.source !== "agent_request")
|
|
2761
|
-
return { status: 404, body: { error: "not_found", message: t("settings_confirm.no_longer_pending") } };
|
|
2762
|
-
try {
|
|
2763
|
-
const view = await store.decide(id, decision, { ...owner, via: "web" });
|
|
2764
|
-
this.logger.info({ id, decision, state: view.state }, "Agent request decided from the web by this computer's admin");
|
|
2765
|
-
return { status: 200, body: { view, agent: agentNameOf(view) } };
|
|
2766
|
-
}
|
|
2767
|
-
catch (err) {
|
|
2768
|
-
const code = err instanceof SettingsConfirmationError ? err.code : "decide_failed";
|
|
2769
|
-
const status = err instanceof SettingsConfirmationError ? err.status : 500;
|
|
2770
|
-
// Already decided (a double click, the host's command), run out or withdrawn: say what it is now.
|
|
2771
|
-
return { status, body: { error: code, message: t(code === "admin_required" ? "settings_confirm.web_owner_required" : "settings_confirm.no_longer_pending"), view: store.inspect(id) } };
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
/** A confirmation belongs to the server route/lifetime, not the availability of General's model. */
|
|
2775
|
-
settingsGeneralRoute(name, platform, excluded) {
|
|
2776
|
-
const config = this.fleetConfig?.instances[name];
|
|
2777
|
-
if (!config?.general_topic || this.shuttingDown || this.ipcStoppingInstances.has(name))
|
|
2778
|
-
return null;
|
|
2779
|
-
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
2780
|
-
if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform)
|
|
2781
|
-
return null;
|
|
2782
|
-
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
2783
|
-
if (!chatId || !topic)
|
|
2784
|
-
return null;
|
|
2785
|
-
const route = () => !this.shuttingDown && this.fleetConfig?.instances[name]?.general_topic === true
|
|
2786
|
-
&& this.adapterState.get(adapterId)?.status === "connected"
|
|
2787
|
-
&& this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
|
|
2788
|
-
&& this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic;
|
|
2789
|
-
const lifetime = new SettingsRouteLifetime(name, route, () => this.lifecycle.epochOf(name), () => this.ipcStoppingInstances.has(name));
|
|
2790
|
-
return lifetime.current() ? { adapterId, adapter, chatId, topic, current: lifetime.current, followStop: lifetime.followStop.bind(lifetime) } : null;
|
|
2791
|
-
}
|
|
2792
|
-
/** Connected, same-platform, unaffected server routes can post cards even while the model is offline. */
|
|
1744
|
+
/** #2170: the Generals a chat card could be posted in (promptSettingsChange's rule): connected, on the platform, unaffected. */
|
|
2793
1745
|
settingsNoticeGenerals(affected, platform) {
|
|
2794
|
-
const excluded = new Set(affected);
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
1746
|
+
const excluded = new Set(affected), found = [];
|
|
1747
|
+
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
1748
|
+
if (!config.general_topic || !this.daemons.get(name))
|
|
1749
|
+
continue;
|
|
1750
|
+
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
1751
|
+
if (!adapterId || !adapter || excluded.has(adapterId) || adapter.type !== platform || this.adapterState.get(adapterId)?.status !== "connected")
|
|
1752
|
+
continue;
|
|
1753
|
+
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
1754
|
+
if (chatId && topic)
|
|
1755
|
+
found.push({ adapterId, adapter, chatId, topic });
|
|
1756
|
+
}
|
|
1757
|
+
return found;
|
|
2799
1758
|
}
|
|
2800
1759
|
/**
|
|
2801
1760
|
* #2170: General's notice of a change confirmed from the web — the first General a chat card could have been posted
|
|
@@ -2854,119 +1813,6 @@ export class FleetManager {
|
|
|
2854
1813
|
return { platform, current, admin: userId => current() && !!userId
|
|
2855
1814
|
&& identities.every(owner => this.isFleetAdmin(userId, owner.id)) };
|
|
2856
1815
|
}
|
|
2857
|
-
/**
|
|
2858
|
-
* #2942: a Windows create or settings change an AGENT asked for. Nothing is created or written here: it becomes a
|
|
2859
|
-
* pending settings change whose General card a fleet admin must confirm (promptSettingsChange — the same admin rule
|
|
2860
|
-
* as every card). Its apply runs the same creation/patch code the web uses, inside the store's SettingsExecution, with
|
|
2861
|
-
* the confirming admin, the Windows switch and the target machine asked again at that moment.
|
|
2862
|
-
*/
|
|
2863
|
-
async proposeAgentWindowsChange(input) {
|
|
2864
|
-
const gate = this.settingsGate(), baselines = this.settingsBaselineReader;
|
|
2865
|
-
if (!baselines || !this.fleetConfig)
|
|
2866
|
-
throw new Error("Fleet configuration is unavailable");
|
|
2867
|
-
const generation = this.settingsGeneration, machineId = input.machineId;
|
|
2868
|
-
const switchOn = () => machineId ? remoteRuntimeEnabled() : localWindowsCliEnabled();
|
|
2869
|
-
const online = () => !machineId || (remoteRuntimeEnabled() && this.runtimeCoordinator().machine(machineId)?.status === "online");
|
|
2870
|
-
if (!switchOn())
|
|
2871
|
-
throw new Error(machineId ? "Remote runtime is not enabled" : LOCAL_WINDOWS_CLI_OFF);
|
|
2872
|
-
const baseline = await baselines.read(), snapshot = settingsFingerprint(baselines.snapshot());
|
|
2873
|
-
const { view } = gate.store.propose({ session: `agent:${input.requester}`, key: `agent-windows:${input.key}`,
|
|
2874
|
-
requestFingerprint: input.fingerprint, fingerprint: input.fingerprint, section: "channel", requestedBy: input.requester,
|
|
2875
|
-
source: "agent_request", summary: input.summary, bytes: input.bytes, authority: input.authority, affectedConnections: [],
|
|
2876
|
-
current: () => !this.shuttingDown && this.settingsGeneration === generation && switchOn(),
|
|
2877
|
-
snapshot: () => baselines.snapshot(),
|
|
2878
|
-
unchanged: async () => (await baselines.read()).fingerprint === baseline.fingerprint && settingsFingerprint(baselines.snapshot()) === snapshot,
|
|
2879
|
-
// The store's own fence (this confirming admin, the session, the state) plus the switch and machine now.
|
|
2880
|
-
apply: async (actorCurrent, execution) => {
|
|
2881
|
-
execution.assert();
|
|
2882
|
-
// alpha.2 polish: a paired machine whose CLI inventory has not arrived yet (just paired) is asked for its own
|
|
2883
|
-
// report first (it runs on that PC), so the creation validates against what is installed there — never a refusal
|
|
2884
|
-
// that only a manual Refresh would undo. Its answer or failure changes nothing else; the fence is asked again.
|
|
2885
|
-
if (machineId && online() && !inventoryProbed(this.runtimeCoordinator().machine(machineId)?.inventory)) {
|
|
2886
|
-
// `force`: the runner's cached answer on a fresh profile is the placeholder itself (#3005 rig recheck).
|
|
2887
|
-
try {
|
|
2888
|
-
await this.runtimeCoordinator().call(machineId, "probe", { force: true });
|
|
2889
|
-
}
|
|
2890
|
-
catch { /* validation says what is missing */ }
|
|
2891
|
-
execution.assert();
|
|
2892
|
-
}
|
|
2893
|
-
return input.apply(() => actorCurrent() && online(), execution);
|
|
2894
|
-
} });
|
|
2895
|
-
return view;
|
|
2896
|
-
}
|
|
2897
|
-
/** #2942: create_instance with runtime "windows" from an agent: validated like the web, then held for General. */
|
|
2898
|
-
async requestWindowsCreate(requester, rawArgs) {
|
|
2899
|
-
const cfg = this.fleetConfig;
|
|
2900
|
-
if (!cfg)
|
|
2901
|
-
throw new Error("Fleet configuration is unavailable");
|
|
2902
|
-
let parsed;
|
|
2903
|
-
try {
|
|
2904
|
-
parsed = parseNativeInstanceCreate(rawArgs);
|
|
2905
|
-
}
|
|
2906
|
-
catch {
|
|
2907
|
-
throw new Error("Invalid Windows agent request: check the directory, sandbox, backend and model");
|
|
2908
|
-
}
|
|
2909
|
-
// The web's own projection: the same validation, topic target and diff the admin's dashboard confirmation shows.
|
|
2910
|
-
let diff;
|
|
2911
|
-
try {
|
|
2912
|
-
diff = prepareSettingsEffect("POST", "/ui/instances", rawArgs, { config: cfg, classic: {} }).diff;
|
|
2913
|
-
}
|
|
2914
|
-
catch {
|
|
2915
|
-
throw new Error("Invalid Windows agent request: its topic connection is not one this fleet can use");
|
|
2916
|
-
}
|
|
2917
|
-
if (!diff)
|
|
2918
|
-
throw new Error("Invalid Windows agent request");
|
|
2919
|
-
return this.proposeAgentWindowsChange({ requester, key: diff.fingerprint, fingerprint: diff.fingerprint, summary: diff.summary,
|
|
2920
|
-
machineId: parsed.input.machineId, bytes: JSON.stringify(rawArgs).length,
|
|
2921
|
-
// The General card is the confirmation for an agent's request, whatever the requested connection (a topic or none).
|
|
2922
|
-
authority: { connections: diff.authority?.connections ?? [], primaryGeneral: true, unknown: false },
|
|
2923
|
-
apply: (allowed, execution) => createNativeWindowsInstance(this, parsed, allowed, execution) });
|
|
2924
|
-
}
|
|
2925
|
-
/** #2942: update_instance_config on a Windows agent: only the fields the web editor allows, held for General. */
|
|
2926
|
-
async requestWindowsConfig(requester, name, patch) {
|
|
2927
|
-
const inst = this.fleetConfig?.instances[name];
|
|
2928
|
-
if (!isWindowsInstance(inst))
|
|
2929
|
-
throw new Error("Windows agent not found");
|
|
2930
|
-
let edit;
|
|
2931
|
-
try {
|
|
2932
|
-
edit = NativeInstanceConfigEdit.parse(patch);
|
|
2933
|
-
}
|
|
2934
|
-
catch {
|
|
2935
|
-
throw new Error("Invalid Windows agent settings: display_name, description and model only");
|
|
2936
|
-
}
|
|
2937
|
-
const before = { display_name: inst.display_name ?? "", description: inst.description ?? "", model: inst.model ?? "" };
|
|
2938
|
-
const changed = Object.keys(edit).filter(key => edit[key] !== before[key]);
|
|
2939
|
-
if (!changed.length)
|
|
2940
|
-
throw new Error("Nothing to change: these settings already have these values");
|
|
2941
|
-
const summary = changed.map(key => `${name} ${key}: ${settingsDisplay(before[key])} → ${settingsDisplay(edit[key])}`);
|
|
2942
|
-
const machineId = typeof inst.machine_id === "string" ? inst.machine_id : undefined;
|
|
2943
|
-
// #2973 review (Prism P1): a persistent pane is bound to a chat connection; changing it (a model change restarts it)
|
|
2944
|
-
// needs that connection's admins as well as the primary General's, exactly as its creation did. An instance whose
|
|
2945
|
-
// binding names no single configured connection is refused (fail closed), never confirmed by the General alone.
|
|
2946
|
-
const connections = [];
|
|
2947
|
-
if (isNativePaneInstance(inst) || inst.channel_id !== undefined) {
|
|
2948
|
-
const bound = inst.channel_id, channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
2949
|
-
if (typeof bound !== "string" || channels.filter(ch => (ch.id ?? ch.type) === bound).length !== 1)
|
|
2950
|
-
throw new Error("This Windows agent's chat connection cannot be resolved; change its settings in the web dashboard");
|
|
2951
|
-
connections.push(bound);
|
|
2952
|
-
}
|
|
2953
|
-
return this.proposeAgentWindowsChange({ requester, key: `${name}:${settingsFingerprint(edit)}`, fingerprint: settingsFingerprint([name, edit]),
|
|
2954
|
-
summary, machineId, bytes: JSON.stringify(edit).length, authority: { connections, primaryGeneral: true, unknown: false },
|
|
2955
|
-
// #2973 review (Prism P2): the update may wait for its lifecycle turn; if the agent is rebound meanwhile (B → C), the
|
|
2956
|
-
// approval for B must not stop or change it on C. Pin the approved target and require the execution's snapshot to be
|
|
2957
|
-
// unchanged before every pre-commit effect; the post-commit restart keeps its own (refreshed) checks.
|
|
2958
|
-
apply: (allowed, execution) => {
|
|
2959
|
-
const pinned = { channel: inst.channel_id, machine: inst.machine_id, adapter: inst.native_adapter, runtime: inst.runtime };
|
|
2960
|
-
const sameTarget = () => {
|
|
2961
|
-
const now = this.fleetConfig?.instances[name];
|
|
2962
|
-
return !!now && now.channel_id === pinned.channel && now.machine_id === pinned.machine && now.native_adapter === pinned.adapter && now.runtime === pinned.runtime;
|
|
2963
|
-
};
|
|
2964
|
-
// Before the commit: the exact target and an unchanged snapshot. After runtime.update completes the transaction,
|
|
2965
|
-
// current() is false by design; the committed operation's restart receipt then needs the same actor, target and
|
|
2966
|
-
// switch checks plus `completed` — a completed execution can never authorize another write (mutate refuses).
|
|
2967
|
-
return updateNativeWindowsInstance(this, name, edit, () => allowed() && sameTarget() && (execution.completed || execution.current()), execution);
|
|
2968
|
-
} });
|
|
2969
|
-
}
|
|
2970
1816
|
async promptSettingsChange(store, view) {
|
|
2971
1817
|
const generation = this.settingsGeneration;
|
|
2972
1818
|
const authority = this.settingsChatAuthority(store, view.id);
|
|
@@ -2975,15 +1821,25 @@ export class FleetManager {
|
|
|
2975
1821
|
return;
|
|
2976
1822
|
}
|
|
2977
1823
|
const excluded = new Set(store.affectedConnections(view.id));
|
|
2978
|
-
for (const name of Object.
|
|
2979
|
-
|
|
2980
|
-
|
|
1824
|
+
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
1825
|
+
if (!config.general_topic)
|
|
1826
|
+
continue;
|
|
1827
|
+
const adapterId = this.getInstanceAdapterId(name), adapter = this.getAdapterForInstance(name);
|
|
1828
|
+
const general = this.daemons.get(name);
|
|
1829
|
+
if (!adapterId || !adapter || !general || excluded.has(adapterId) || adapter.type !== authority.platform)
|
|
1830
|
+
continue;
|
|
1831
|
+
const chatId = this.getGroupIdForInstance(name), topic = String(config.topic_id ?? "");
|
|
1832
|
+
if (!chatId || !topic)
|
|
2981
1833
|
continue;
|
|
2982
|
-
const { adapterId, adapter, chatId, topic } = route;
|
|
2983
1834
|
let retired = false, attached = false;
|
|
2984
1835
|
const postingDeadline = performance.now() + Math.min(5000, view.remaining_ms);
|
|
2985
|
-
const current = () => !retired && (attached || performance.now() < postingDeadline) && this.settingsGeneration === generation
|
|
2986
|
-
&&
|
|
1836
|
+
const current = () => !retired && (attached || performance.now() < postingDeadline) && !this.shuttingDown && this.settingsGeneration === generation
|
|
1837
|
+
&& this.fleetConfig?.instances[name]?.general_topic === true
|
|
1838
|
+
&& this.daemons.get(name) === general && !this.ipcStoppingInstances.has(name)
|
|
1839
|
+
&& this.adapterState.get(adapterId)?.status === "connected"
|
|
1840
|
+
&& this.getInstanceAdapterId(name) === adapterId && this.getAdapterForInstance(name) === adapter
|
|
1841
|
+
&& this.getGroupIdForInstance(name) === chatId && String(this.fleetConfig?.instances[name]?.topic_id ?? "") === topic
|
|
1842
|
+
&& authority.current();
|
|
2987
1843
|
if (!current())
|
|
2988
1844
|
continue;
|
|
2989
1845
|
try {
|
|
@@ -2993,7 +1849,7 @@ export class FleetManager {
|
|
|
2993
1849
|
t("settings_confirm.requester", view.requested_by), ...view.summary].join("\n"),
|
|
2994
1850
|
choices: [{ action: "confirm", label: t("settings_confirm.confirm_button") }, { action: "reject", label: t("settings_confirm.reject_button") }],
|
|
2995
1851
|
expiredText: t("settings_confirm.no_longer_pending"), timeoutMs: view.remaining_ms,
|
|
2996
|
-
extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId)
|
|
1852
|
+
extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId) },
|
|
2997
1853
|
});
|
|
2998
1854
|
void posted.then(nonce => {
|
|
2999
1855
|
if (!retired)
|
|
@@ -3261,7 +2117,6 @@ export class FleetManager {
|
|
|
3261
2117
|
try {
|
|
3262
2118
|
const view = await this.settingsGate().store.decide(entry.pendingChangeId, action, {
|
|
3263
2119
|
label: t("settings_confirm.admin_label", data.userId ?? ""),
|
|
3264
|
-
followStop: entry.confirmationStop,
|
|
3265
2120
|
current: () => entry.confirmationCurrent?.() === true && this.getAdapterForInstance(entry.instanceName) === entry.adapter
|
|
3266
2121
|
&& !!data.userId && entry.confirmationAdmin?.(data.userId) === true,
|
|
3267
2122
|
});
|
|
@@ -3334,6 +2189,22 @@ export class FleetManager {
|
|
|
3334
2189
|
* startup-only key at startup. See checkStartupSignatureConsistency(). */
|
|
3335
2190
|
fleetSignatureMismatch = null;
|
|
3336
2191
|
healthServerListening = false;
|
|
2192
|
+
agentCredentialsCache;
|
|
2193
|
+
get agentCredentials() {
|
|
2194
|
+
return this.agentCredentialsCache ??= new AgentCredentials(name => agentCredentialPolicy(this.fleetConfig, name, this.isClassicInstance(name)), name => {
|
|
2195
|
+
try {
|
|
2196
|
+
const token = randomBytes(32).toString("hex");
|
|
2197
|
+
const result = writeSecretFile(join(this.getInstanceDir(name), "agent.token"), token);
|
|
2198
|
+
if (!result.ok)
|
|
2199
|
+
this.logger.error({ name, mode: result.mode, reason: result.reason }, "Agent token file is not owner-only");
|
|
2200
|
+
return token;
|
|
2201
|
+
}
|
|
2202
|
+
catch {
|
|
2203
|
+
this.logger.error({ name }, "Agent credential renewal failed; prior generation remains retired");
|
|
2204
|
+
return null;
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
3337
2208
|
constructor(dataDir) {
|
|
3338
2209
|
this.dataDir = dataDir;
|
|
3339
2210
|
FleetManager.signalTarget = this;
|
|
@@ -3356,6 +2227,7 @@ export class FleetManager {
|
|
|
3356
2227
|
this.topicCommands = new TopicCommands(this);
|
|
3357
2228
|
this.topicArchiver = new TopicArchiver(this);
|
|
3358
2229
|
this.statuslineWatcher = new StatuslineWatcher(this);
|
|
2230
|
+
this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
|
|
3359
2231
|
}
|
|
3360
2232
|
/** #1266: a reply's buttons — the store (reply-buttons.db) and what a click does. Created on first use. */
|
|
3361
2233
|
replyButtonsCtl = null;
|
|
@@ -3636,15 +2508,12 @@ export class FleetManager {
|
|
|
3636
2508
|
}
|
|
3637
2509
|
/** Synchronous SQLite admission; the caller may acknowledge only after this returns. */
|
|
3638
2510
|
admitDurableDelivery(input) {
|
|
3639
|
-
const bindingHold = this.generalLocationDeliveryHold(input.targetInstance);
|
|
3640
|
-
if (bindingHold)
|
|
3641
|
-
throw Error(bindingHold);
|
|
3642
2511
|
const outbox = this.deliveryOutbox;
|
|
3643
2512
|
if (!outbox)
|
|
3644
2513
|
throw new Error("Durable delivery store is unavailable");
|
|
3645
|
-
const
|
|
3646
|
-
const sourceDaemonBootId = input.sourceDaemonBootId ??
|
|
3647
|
-
if (!
|
|
2514
|
+
const source = this.daemons.get(input.sourceInstance);
|
|
2515
|
+
const sourceDaemonBootId = input.sourceDaemonBootId ?? source?.bootId;
|
|
2516
|
+
if (!source || !sourceDaemonBootId || source.bootId !== sourceDaemonBootId) {
|
|
3648
2517
|
throw new Error("Source daemon generation is no longer current; outcome is unknown");
|
|
3649
2518
|
}
|
|
3650
2519
|
const sourceKey = `mcp:${input.sourceInstance}:${input.operationId}:${input.targetInstance}:${input.targetSession ?? ""}:${input.kind}`;
|
|
@@ -3678,7 +2547,7 @@ export class FleetManager {
|
|
|
3678
2547
|
const human = (item) => {
|
|
3679
2548
|
if (!item.authority || !state)
|
|
3680
2549
|
return item;
|
|
3681
|
-
const authority =
|
|
2550
|
+
const authority = authorityNow(item.authority, state);
|
|
3682
2551
|
return { ...item, authority, ...(authority.downgraded ? { source_instance: "human:web:unknown" } : {}) };
|
|
3683
2552
|
};
|
|
3684
2553
|
// #1641: a sender that sees only `queued` cannot tell "next in line" from "the target is stuck": say which.
|
|
@@ -3707,11 +2576,6 @@ export class FleetManager {
|
|
|
3707
2576
|
return this.daemons.get(target)?.notAcceptingReason?.() ?? null;
|
|
3708
2577
|
}
|
|
3709
2578
|
getDaemonBootId(instanceName) {
|
|
3710
|
-
if (this.generalLocationDeliveryHold(instanceName))
|
|
3711
|
-
return undefined;
|
|
3712
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
3713
|
-
return !this.shuttingDown && windowsCliEnabled() && this.nativeMcpStore?.hasActive(instanceName) ? this.managerBootId : undefined;
|
|
3714
|
-
}
|
|
3715
2579
|
return this.daemons.get(instanceName)?.bootId;
|
|
3716
2580
|
}
|
|
3717
2581
|
markDurableResponseDelivered(sourceInstance, operationId) {
|
|
@@ -3720,21 +2584,11 @@ export class FleetManager {
|
|
|
3720
2584
|
/** Called after a replacement Daemon object is installed in lifecycle.daemons. */
|
|
3721
2585
|
onDaemonReady(name, daemonBootId) {
|
|
3722
2586
|
const daemon = this.daemons.get(name);
|
|
3723
|
-
if (daemon?.bootId === daemonBootId)
|
|
2587
|
+
if (daemon?.bootId === daemonBootId)
|
|
3724
2588
|
this.listenForWorkingState(name, daemon);
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
try {
|
|
3729
|
-
if (this.generalBindings().completeSelected(name, cfg)) {
|
|
3730
|
-
this.needsYou?.poke();
|
|
3731
|
-
this.logger.info({ name }, "General location's selected start receipt settled after recovery");
|
|
3732
|
-
}
|
|
3733
|
-
}
|
|
3734
|
-
catch (err) {
|
|
3735
|
-
this.logger.warn({ name, err }, "General location start receipt remains held");
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
2589
|
+
// #3066 §1d: a Codex launch records the credential profile it runs under, so its usage is told by account.
|
|
2590
|
+
if (daemon?.bootId === daemonBootId && this.backendNameOf(name) === "codex") {
|
|
2591
|
+
void recordProfileLaunch(join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE), this.credentialProfileOf(name, "codex") ?? "default");
|
|
3738
2592
|
}
|
|
3739
2593
|
// #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
|
|
3740
2594
|
// request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
|
|
@@ -3879,15 +2733,6 @@ export class FleetManager {
|
|
|
3879
2733
|
this.deliveryPumpRunning = true;
|
|
3880
2734
|
this.deliveryPumpScheduled = false;
|
|
3881
2735
|
try {
|
|
3882
|
-
// Native workers have no resident daemon to wake. Pending durable work resumes their logical instance;
|
|
3883
|
-
// the Windows bridge still owns the single physical execution slot.
|
|
3884
|
-
for (const target of new Set(outbox.listPending().map(row => row.targetInstance))) {
|
|
3885
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[target])) {
|
|
3886
|
-
const runtime = this.nativeInstanceRuntime(), agent = runtime.get(target);
|
|
3887
|
-
if (agent?.paused && !agent.blocked)
|
|
3888
|
-
runtime.setPaused(target, false);
|
|
3889
|
-
}
|
|
3890
|
-
}
|
|
3891
2736
|
outbox.expireStale(undefined, undefined, target => {
|
|
3892
2737
|
const failure = this.wakeCoordinator?.wakeFailure(target);
|
|
3893
2738
|
return failure ? `target could not be woken (${failure})` : undefined;
|
|
@@ -3900,7 +2745,7 @@ export class FleetManager {
|
|
|
3900
2745
|
// Phase 2c: a lane a queue worker owns is never claimed here.
|
|
3901
2746
|
target => this.queueWorkers.has(target) || this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target)
|
|
3902
2747
|
? null
|
|
3903
|
-
: this.
|
|
2748
|
+
: this.daemons.get(target)?.bootId ?? null, this.activeDurableTargets, row => this.recoveryDeliveryEligible(row));
|
|
3904
2749
|
if (!claimed)
|
|
3905
2750
|
break;
|
|
3906
2751
|
this.activeDurableTargets.add(claimed.targetInstance);
|
|
@@ -3961,7 +2806,7 @@ export class FleetManager {
|
|
|
3961
2806
|
wanted: () => this.deliveryWorkerMode(target) === "on",
|
|
3962
2807
|
available: () => !this.shuttingDown && this.deliveryOutbox?.isOpen === true,
|
|
3963
2808
|
blocked: () => this.isInstanceRestarting(target) || this.wakeCoordinator?.blocksClaim(target) === true,
|
|
3964
|
-
daemonBootId: () => this.
|
|
2809
|
+
daemonBootId: () => this.daemons.get(target)?.bootId ?? null,
|
|
3965
2810
|
claim: bootId => this.deliveryOutbox?.claimNext(this.managerBootId, candidate => candidate === target ? bootId : null, new Set(), row => this.recoveryDeliveryEligible(row)),
|
|
3966
2811
|
tryAcquireBudget: () => {
|
|
3967
2812
|
if (this.activeDurableTargets.size >= 8)
|
|
@@ -3992,10 +2837,6 @@ export class FleetManager {
|
|
|
3992
2837
|
const outbox = this.deliveryOutbox;
|
|
3993
2838
|
if (!outbox)
|
|
3994
2839
|
return;
|
|
3995
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[claimed.targetInstance])) {
|
|
3996
|
-
await this.dispatchNativeDurableDelivery(claimed);
|
|
3997
|
-
return;
|
|
3998
|
-
}
|
|
3999
2840
|
const target = claimed.targetInstance;
|
|
4000
2841
|
const attempt = claimed.attemptNo;
|
|
4001
2842
|
const payload = { ...claimed.payload };
|
|
@@ -4077,85 +2918,6 @@ export class FleetManager {
|
|
|
4077
2918
|
}
|
|
4078
2919
|
await laneReleased;
|
|
4079
2920
|
}
|
|
4080
|
-
/** The ordinary durable lane, with provider completion proof instead of terminal/IPC submission proof. */
|
|
4081
|
-
async dispatchNativeDurableDelivery(claimed) {
|
|
4082
|
-
const outbox = this.deliveryOutbox;
|
|
4083
|
-
if (!outbox)
|
|
4084
|
-
return;
|
|
4085
|
-
const { deliveryId, targetDaemonBootId, attemptNo, targetInstance: target } = claimed;
|
|
4086
|
-
let begun = false;
|
|
4087
|
-
try {
|
|
4088
|
-
if (["raw_paste", "steer"].includes(claimed.kind)) {
|
|
4089
|
-
if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) === "begun")
|
|
4090
|
-
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "failed", "Windows runtime does not support terminal input or steering");
|
|
4091
|
-
return;
|
|
4092
|
-
}
|
|
4093
|
-
const meta = claimed.payload.meta && typeof claimed.payload.meta === "object" ? claimed.payload.meta : {};
|
|
4094
|
-
const task = await this.nativeInstanceRuntime().execute(target, { ...claimed.payload, requestId: deliveryId, meta: { ...meta, from_instance: claimed.sourceInstance, correlation_id: claimed.correlationId } }, {
|
|
4095
|
-
stillCurrent: () => !this.shuttingDown,
|
|
4096
|
-
beforeStart: () => { if (outbox.begin(deliveryId, targetDaemonBootId, attemptNo) !== "begun")
|
|
4097
|
-
throw new Error("Windows delivery attempt is no longer current"); begun = true; },
|
|
4098
|
-
});
|
|
4099
|
-
const outcome = task.outcomeConfirmed === false ? "uncertain" : task.status === "completed" ? "delivered" : "failed";
|
|
4100
|
-
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, outcome, task.error || task.status);
|
|
4101
|
-
// Return a requested peer report when the provider did not explicitly report it through scoped MCP.
|
|
4102
|
-
if (meta.requires_reply === "true" && claimed.correlationId && !task.reportedCorrelationIds?.includes(claimed.correlationId) && this.fleetConfig?.instances[claimed.sourceInstance]) {
|
|
4103
|
-
const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
|
|
4104
|
-
outbox.admit({ sourceKey: `native-result:${deliveryId}`, operationId: `native-result:${deliveryId}`, sourceInstance: target, sourceDaemonBootId: this.managerBootId,
|
|
4105
|
-
targetInstance: claimed.sourceInstance, kind: "fleet_inbound", correlationId: claimed.correlationId,
|
|
4106
|
-
payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: target, correlation_id: claimed.correlationId, requires_reply: "false", source: "native_runtime" } } });
|
|
4107
|
-
this.scheduleDeliveryOutboxPump();
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
catch (error) {
|
|
4111
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
4112
|
-
if (begun)
|
|
4113
|
-
outbox.complete(deliveryId, targetDaemonBootId, attemptNo, "uncertain", reason);
|
|
4114
|
-
else
|
|
4115
|
-
outbox.retryBeforeBegin(deliveryId, targetDaemonBootId, attemptNo, reason, 5000);
|
|
4116
|
-
this.logger.warn({ target, deliveryId, err: reason }, "Windows durable delivery did not complete");
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
4119
|
-
/** A runtime's persisted completion can outlive this manager. Resolve its original delivery fence, never send
|
|
4120
|
-
* the prompt again, and reconstruct a missing peer report using the same durable operation ID. */
|
|
4121
|
-
reconcileRemoteDelivery(machineId, task) {
|
|
4122
|
-
const outbox = this.deliveryOutbox;
|
|
4123
|
-
if (!outbox?.isOpen || !task.finishedAt || task.outcomeConfirmed !== true || !["completed", "failed", "cancelled"].includes(task.status))
|
|
4124
|
-
return;
|
|
4125
|
-
const key = `${task.id}:${task.finishedAt}:${task.status}`;
|
|
4126
|
-
if (this.reconciledRemoteResults.has(key))
|
|
4127
|
-
return;
|
|
4128
|
-
const row = outbox.get(task.id);
|
|
4129
|
-
if (!row || !isNativeExecInstance(this.fleetConfig?.instances[row.targetInstance]) || this.fleetConfig?.instances[row.targetInstance]?.machine_id !== machineId
|
|
4130
|
-
|| !row.targetDaemonBootId || this.activeDurableTargets.has(row.targetInstance))
|
|
4131
|
-
return;
|
|
4132
|
-
if (row.state === "submission_started" || row.state === "reconciliation_pending") {
|
|
4133
|
-
if (!outbox.complete(row.deliveryId, row.targetDaemonBootId, row.attemptNo, task.status === "completed" ? "delivered" : "failed", task.error || "Runtime completion recovered after reconnect"))
|
|
4134
|
-
return;
|
|
4135
|
-
}
|
|
4136
|
-
else if (row.state === "uncertain" && task.status === "completed") {
|
|
4137
|
-
if (outbox.markConsumed(row.deliveryId, row.targetDaemonBootId, row.attemptNo, "turn", "Paired runtime journal confirmed this exact execution completed") === "ignored")
|
|
4138
|
-
return;
|
|
4139
|
-
}
|
|
4140
|
-
else if (row.state !== "delivered" && row.state !== "failed")
|
|
4141
|
-
return;
|
|
4142
|
-
const meta = row.payload.meta && typeof row.payload.meta === "object" ? row.payload.meta : {};
|
|
4143
|
-
if (meta.requires_reply !== "true" || !row.correlationId || !this.fleetConfig?.instances[row.sourceInstance]) {
|
|
4144
|
-
this.reconciledRemoteResults.add(key);
|
|
4145
|
-
return;
|
|
4146
|
-
}
|
|
4147
|
-
const reports = outbox.queryStatusForInstance(row.targetInstance, { correlationId: row.correlationId, limit: 100 }).items;
|
|
4148
|
-
if (reports.some(report => report.source_instance === row.targetInstance && report.target_instance === row.sourceInstance
|
|
4149
|
-
&& report.kind === "report" && !["failed", "cancelled"].includes(report.state))) {
|
|
4150
|
-
this.reconciledRemoteResults.add(key);
|
|
4151
|
-
return;
|
|
4152
|
-
}
|
|
4153
|
-
const text = (task.status === "completed" ? task.output : `Windows task ${task.status}: ${task.error || task.output}`).slice(0, 8000);
|
|
4154
|
-
outbox.admit({ sourceKey: `native-result:${row.deliveryId}`, operationId: `native-result:${row.deliveryId}`, sourceInstance: row.targetInstance, sourceDaemonBootId: this.managerBootId,
|
|
4155
|
-
targetInstance: row.sourceInstance, kind: "fleet_inbound", correlationId: row.correlationId,
|
|
4156
|
-
payload: { type: "fleet_inbound", content: text || "Windows task completed without text output", meta: { from_instance: row.targetInstance, correlation_id: row.correlationId, requires_reply: "false", source: "native_runtime" } } });
|
|
4157
|
-
this.reconciledRemoteResults.add(key);
|
|
4158
|
-
}
|
|
4159
2921
|
async waitForDurableLaneRelease(claimed) {
|
|
4160
2922
|
const outbox = this.deliveryOutbox;
|
|
4161
2923
|
if (!outbox)
|
|
@@ -4612,7 +3374,6 @@ export class FleetManager {
|
|
|
4612
3374
|
this.warnAboutRemovedLoginMode(loaded);
|
|
4613
3375
|
this.enforceGuestSwitch();
|
|
4614
3376
|
this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
|
|
4615
|
-
this.observeRuntimeSwitches(); // M-2255: so are the Windows runtimes' switches
|
|
4616
3377
|
// Child tool/guidance readers must follow this fleet's actual config, including --config.
|
|
4617
3378
|
// Scope the path to its owning data directory so another inherited home's override cannot enable CW.
|
|
4618
3379
|
process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
|
|
@@ -4666,6 +3427,7 @@ export class FleetManager {
|
|
|
4666
3427
|
* guild channel.
|
|
4667
3428
|
*/
|
|
4668
3429
|
reregisterClassicChannels() {
|
|
3430
|
+
this.agentCredentialsCache?.observePolicies();
|
|
4669
3431
|
if (!this.classicChannels)
|
|
4670
3432
|
return;
|
|
4671
3433
|
const channels = this.classicChannels.getAll();
|
|
@@ -5041,8 +3803,6 @@ export class FleetManager {
|
|
|
5041
3803
|
}
|
|
5042
3804
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
5043
3805
|
getAdapterForInstance(name) {
|
|
5044
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
5045
|
-
return null;
|
|
5046
3806
|
const worldId = this.getInstanceAdapterId(name);
|
|
5047
3807
|
if (worldId)
|
|
5048
3808
|
return this.worlds.get(worldId)?.adapter ?? this.adapter;
|
|
@@ -5105,8 +3865,6 @@ export class FleetManager {
|
|
|
5105
3865
|
}
|
|
5106
3866
|
/** Get the group_id for an instance's bound adapter */
|
|
5107
3867
|
getGroupIdForInstance(name) {
|
|
5108
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
5109
|
-
return "";
|
|
5110
3868
|
const adapterId = this.getInstanceAdapterId(name);
|
|
5111
3869
|
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
5112
3870
|
return world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
@@ -5143,8 +3901,6 @@ export class FleetManager {
|
|
|
5143
3901
|
*/
|
|
5144
3902
|
getInstanceAdapterId(name) {
|
|
5145
3903
|
const cfg = this.fleetConfig?.instances[name];
|
|
5146
|
-
if (isNativeExecInstance(cfg))
|
|
5147
|
-
return undefined;
|
|
5148
3904
|
if (cfg)
|
|
5149
3905
|
return cfg.channel_id ?? this.getPrimaryAdapterId();
|
|
5150
3906
|
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined) {
|
|
@@ -5172,32 +3928,9 @@ export class FleetManager {
|
|
|
5172
3928
|
this.instanceWorldBinding.set(name, adapterId);
|
|
5173
3929
|
}
|
|
5174
3930
|
getInstanceStatus(name) {
|
|
5175
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
5176
|
-
try {
|
|
5177
|
-
return this.nativeInstanceRuntime().status(name);
|
|
5178
|
-
}
|
|
5179
|
-
catch {
|
|
5180
|
-
return "crashed";
|
|
5181
|
-
}
|
|
5182
|
-
}
|
|
5183
3931
|
if (this.lifecycle.isPaused(name))
|
|
5184
3932
|
return "paused";
|
|
5185
3933
|
const daemon = this.lifecycle.daemons.get(name);
|
|
5186
|
-
if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
|
|
5187
|
-
const config = this.fleetConfig.instances[name];
|
|
5188
|
-
if (config.machine_id ? !remoteRuntimeEnabled() : !localWindowsCliEnabled())
|
|
5189
|
-
return "stopped";
|
|
5190
|
-
if (daemon) {
|
|
5191
|
-
const native = daemon, status = native.getProcessStatus?.() ?? "crashed";
|
|
5192
|
-
return status === "running" && native.nativePaneCurrent?.() !== true ? "crashed" : status;
|
|
5193
|
-
}
|
|
5194
|
-
try {
|
|
5195
|
-
return this.nativeInstanceRuntime().get(name)?.activeId ? "crashed" : "stopped";
|
|
5196
|
-
}
|
|
5197
|
-
catch {
|
|
5198
|
-
return "crashed";
|
|
5199
|
-
}
|
|
5200
|
-
}
|
|
5201
3934
|
const processStatus = this.instanceProcessStatus.get(name);
|
|
5202
3935
|
// IPC can be disconnected during a respawn, so the event which announces
|
|
5203
3936
|
// the new live pane may be missed. The in-process daemon is authoritative
|
|
@@ -5231,14 +3964,6 @@ export class FleetManager {
|
|
|
5231
3964
|
}
|
|
5232
3965
|
}
|
|
5233
3966
|
getInstanceExecutionState(name) {
|
|
5234
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
5235
|
-
try {
|
|
5236
|
-
return this.nativeInstanceRuntime().execution(name);
|
|
5237
|
-
}
|
|
5238
|
-
catch {
|
|
5239
|
-
return null;
|
|
5240
|
-
}
|
|
5241
|
-
}
|
|
5242
3967
|
if (this.lifecycle.isPaused(name))
|
|
5243
3968
|
return null;
|
|
5244
3969
|
// Process status wins over a stale pane snapshot. A dead remain-on-exit pane
|
|
@@ -5249,8 +3974,6 @@ export class FleetManager {
|
|
|
5249
3974
|
}
|
|
5250
3975
|
/** Pure read of the single daemon-owned observation; no probe/capture or second tracker. */
|
|
5251
3976
|
getInstanceInteraction(name) {
|
|
5252
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name]))
|
|
5253
|
-
return null;
|
|
5254
3977
|
if (this.lifecycle.isPaused(name) || this.instanceProcessStatus.has(name))
|
|
5255
3978
|
return null;
|
|
5256
3979
|
const daemon = this.lifecycle.daemons.get(name);
|
|
@@ -5338,8 +4061,6 @@ export class FleetManager {
|
|
|
5338
4061
|
bindings.push([name, backend, profile]);
|
|
5339
4062
|
};
|
|
5340
4063
|
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {})) {
|
|
5341
|
-
if (isWindowsInstance(config))
|
|
5342
|
-
continue; // Windows sign-ins and usage do not belong to the Linux account.
|
|
5343
4064
|
// loadFleetConfig() has already merged the fleet default into each row.
|
|
5344
4065
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5345
4066
|
add(name, backend, instanceCredentialProfile(config, this.fleetConfig?.defaults, backend));
|
|
@@ -5397,6 +4118,13 @@ export class FleetManager {
|
|
|
5397
4118
|
changed: () => this.emitSseEvent("status", this.getUiStatus()),
|
|
5398
4119
|
});
|
|
5399
4120
|
}
|
|
4121
|
+
/**
|
|
4122
|
+
* Claude history in the View: an instance's earlier conversation for its View, from the transcript the Log reads —
|
|
4123
|
+
* the View's route applied its own access rules first (view-api.ts); the Log's switch plays no part.
|
|
4124
|
+
*/
|
|
4125
|
+
viewHistory(name, q) {
|
|
4126
|
+
return viewHistory((n, rq) => this.log.readBack(n, rq), name, q);
|
|
4127
|
+
}
|
|
5400
4128
|
/** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
|
|
5401
4129
|
logSteps(name, q) { return this.log.steps(name, q); }
|
|
5402
4130
|
/**
|
|
@@ -5946,8 +4674,6 @@ export class FleetManager {
|
|
|
5946
4674
|
* id is no platform message, and the echo is the fleet's own post. The web send calls this on the echo's lane.
|
|
5947
4675
|
*/
|
|
5948
4676
|
startWebChatProgress(instanceName) {
|
|
5949
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
|
|
5950
|
-
return Promise.resolve();
|
|
5951
4677
|
if (this.shuttingDown)
|
|
5952
4678
|
return Promise.resolve();
|
|
5953
4679
|
return this.sendCancelButton(instanceName);
|
|
@@ -6213,12 +4939,6 @@ export class FleetManager {
|
|
|
6213
4939
|
}
|
|
6214
4940
|
/** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
|
|
6215
4941
|
async deliverToInstance(instanceName, payload, options = {}) {
|
|
6216
|
-
if (this.generalLocationDeliveryHold(instanceName))
|
|
6217
|
-
return false;
|
|
6218
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
6219
|
-
return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
|
|
6220
|
-
...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
|
|
6221
|
-
}
|
|
6222
4942
|
const deliveryEpoch = this.getDeliveryEpoch(instanceName);
|
|
6223
4943
|
const deliveryPayload = { ...payload, delivery_epoch: deliveryEpoch };
|
|
6224
4944
|
const meta = payload.meta && typeof payload.meta === "object"
|
|
@@ -6776,10 +5496,6 @@ export class FleetManager {
|
|
|
6776
5496
|
* slot admission and joins any wake already in flight for the target.
|
|
6777
5497
|
*/
|
|
6778
5498
|
async explicitWake(name, timeoutMs = 30_000, retryStoppedSession = false) {
|
|
6779
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
6780
|
-
this.nativeInstanceRuntime().setPaused(name, false);
|
|
6781
|
-
return;
|
|
6782
|
-
}
|
|
6783
5499
|
if (this.sessionOomStopped(name)) {
|
|
6784
5500
|
if (!retryStoppedSession)
|
|
6785
5501
|
throw new SessionOomStoppedError(false, readSessionOomStop(this.getInstanceDir(name)));
|
|
@@ -6804,10 +5520,6 @@ export class FleetManager {
|
|
|
6804
5520
|
.sort((a, b) => (readLastInboundAt(this.getInstanceDir(a)) ?? 0) - (readLastInboundAt(this.getInstanceDir(b)) ?? 0))[0];
|
|
6805
5521
|
}
|
|
6806
5522
|
async changeInstancePauseState(name, action) {
|
|
6807
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
6808
|
-
this.nativeInstanceRuntime().setPaused(name, action === "pause");
|
|
6809
|
-
return action === "pause" ? "paused" : "awake";
|
|
6810
|
-
}
|
|
6811
5523
|
if (action === "wake") {
|
|
6812
5524
|
await this.explicitWake(name, 30_000, true);
|
|
6813
5525
|
this.enforceWarmCap(name); // manual wake still respects the fleet warm cap
|
|
@@ -6834,6 +5546,7 @@ export class FleetManager {
|
|
|
6834
5546
|
}
|
|
6835
5547
|
classicBehaviorUpdate(instanceName) {
|
|
6836
5548
|
return {
|
|
5549
|
+
quota_notice: this.classicChannels?.getQuotaNoticeByInstance?.(instanceName) ?? null,
|
|
6837
5550
|
reply_triggers: this.classicChannels?.getReplyTriggersByInstance(instanceName, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS,
|
|
6838
5551
|
tool_progress: this.classicChannels?.getToolProgressByInstance(instanceName, this.fleetConfig?.defaults?.tool_progress) ?? "off",
|
|
6839
5552
|
reply_completion_guard: this.classicChannels?.getReplyCompletionGuardByInstance(instanceName, this.fleetConfig?.defaults?.reply_completion_guard) ?? true,
|
|
@@ -6985,14 +5698,6 @@ export class FleetManager {
|
|
|
6985
5698
|
resumePaused = false,
|
|
6986
5699
|
/** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
|
|
6987
5700
|
transition, execution, kiroMigration) {
|
|
6988
|
-
if (isNativeExecInstance(config)) {
|
|
6989
|
-
execution?.assert();
|
|
6990
|
-
if (!windowsCliEnabled())
|
|
6991
|
-
throw new Error("Windows runtime is disabled");
|
|
6992
|
-
if (resumePaused)
|
|
6993
|
-
this.nativeInstanceRuntime().setPaused(name, false);
|
|
6994
|
-
return;
|
|
6995
|
-
}
|
|
6996
5701
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
6997
5702
|
if (migration && migration.token !== kiroMigration)
|
|
6998
5703
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -7043,29 +5748,27 @@ export class FleetManager {
|
|
|
7043
5748
|
if (resumePaused)
|
|
7044
5749
|
this.lifecycle.markActivitySeedNow(name);
|
|
7045
5750
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
7046
|
-
// #1490 row 67
|
|
5751
|
+
// #1490 row 67: off-loop resolve before skill/instruction publication
|
|
7047
5752
|
// constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
|
|
7048
5753
|
// ownership first: a Stop during this wait finishes and bumps the epoch,
|
|
7049
5754
|
// and the inner lifecycle check would otherwise capture the new epoch and
|
|
7050
5755
|
// accept the stale start. Lifecycle start looks up again before its own
|
|
7051
|
-
// createBackend; the probe cache makes that cheap.
|
|
7052
|
-
// agent's CLI is on that PC — nothing to resolve on this one.)
|
|
5756
|
+
// createBackend; the probe cache makes that cheap.
|
|
7053
5757
|
const epoch = this.lifecycle.epochOf(name);
|
|
7054
5758
|
const stopGen = this.explicitStopGeneration.get(name) ?? 0;
|
|
7055
|
-
const
|
|
7056
|
-
const resolved = windows ? null : await resolveBackendBinary(backend);
|
|
5759
|
+
const resolved = await resolveBackendBinary(backend);
|
|
7057
5760
|
execution?.assert();
|
|
7058
5761
|
if (this.lifecycle.epochOf(name) !== epoch
|
|
7059
5762
|
|| (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
|
|
7060
5763
|
throw new SupersededStartError(name);
|
|
7061
5764
|
}
|
|
7062
5765
|
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
7063
|
-
if (binary
|
|
5766
|
+
if (binary)
|
|
7064
5767
|
noteResolvedBinary(binary, resolved ?? binary);
|
|
7065
|
-
if (
|
|
5768
|
+
if (config.general_topic) {
|
|
7066
5769
|
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
7067
5770
|
}
|
|
7068
|
-
else if (
|
|
5771
|
+
else if (kind === "fleet-topic") {
|
|
7069
5772
|
// Workers get only role-eligible on-demand skills. Classic instances are
|
|
7070
5773
|
// deliberately excluded: their workspace and conversation lifecycle are
|
|
7071
5774
|
// managed independently from fleet-topic workers.
|
|
@@ -7080,7 +5783,7 @@ export class FleetManager {
|
|
|
7080
5783
|
this.logger.warn({ err, name, backend }, "Failed to sync worker skills — continuing startup");
|
|
7081
5784
|
}
|
|
7082
5785
|
}
|
|
7083
|
-
else if (
|
|
5786
|
+
else if (kind === "classic") {
|
|
7084
5787
|
// #2163: a Classic bot gets only the skills marked `classic` (its own look, the dashboard) — never the fleet's
|
|
7085
5788
|
// coordination playbooks (#557 kept those out of end-user bots) — and only into a workspace AgEnD manages: the
|
|
7086
5789
|
// room's own folder under <AGEND_HOME>/workspaces/. Anywhere else, nothing is written; `agend-agent skill <name>`
|
|
@@ -7097,7 +5800,7 @@ export class FleetManager {
|
|
|
7097
5800
|
}
|
|
7098
5801
|
}
|
|
7099
5802
|
const receipt = execution ? this.classicSettingsOwners.get(execution) : undefined;
|
|
7100
|
-
const identity = { kind, backend, model:
|
|
5803
|
+
const identity = { kind, backend, model: this.resolveInstanceModel(name).display };
|
|
7101
5804
|
if (receipt) {
|
|
7102
5805
|
await this.lifecycle.start(name, config, topicMode, identity, transition, execution, daemon => {
|
|
7103
5806
|
// Only this transition's actual publication can acquire its replacement.
|
|
@@ -7167,7 +5870,7 @@ export class FleetManager {
|
|
|
7167
5870
|
// them before grouping/staggering: startInstance() retains its own guard as
|
|
7168
5871
|
// a final backstop, but putting a no-op entry in this queue still consumes a
|
|
7169
5872
|
// full stagger slot for every distinct working directory.
|
|
7170
|
-
const runnableEntries = entries.filter(([name
|
|
5873
|
+
const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
|
|
7171
5874
|
// ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
|
|
7172
5875
|
// only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
|
|
7173
5876
|
const fleetNames = new Set(entries.map(([name]) => name));
|
|
@@ -7233,8 +5936,6 @@ export class FleetManager {
|
|
|
7233
5936
|
return false;
|
|
7234
5937
|
if (err instanceof SessionOomStoppedError)
|
|
7235
5938
|
await this.notifySessionOomStop(name);
|
|
7236
|
-
if (this.holdNativePaneRecovery(name, err))
|
|
7237
|
-
return false;
|
|
7238
5939
|
if (this.stopOnUnsupportedCli(name, err, what))
|
|
7239
5940
|
return false;
|
|
7240
5941
|
this.logger.error({ err, name }, `Failed to start ${what}`);
|
|
@@ -7376,8 +6077,6 @@ export class FleetManager {
|
|
|
7376
6077
|
if (err instanceof SupersededStartError)
|
|
7377
6078
|
return;
|
|
7378
6079
|
// An earlier attempt could not tell (unknown probe); this one could.
|
|
7379
|
-
if (this.holdNativePaneRecovery(name, err))
|
|
7380
|
-
return;
|
|
7381
6080
|
if (this.stopOnUnsupportedCli(name, err, "instance"))
|
|
7382
6081
|
return;
|
|
7383
6082
|
this.logger.error({ err, name, attempt: attempt + 1 }, "Automatic startup retry failed");
|
|
@@ -7522,6 +6221,17 @@ export class FleetManager {
|
|
|
7522
6221
|
this.queueKiroIncompatNotice(name, err.message);
|
|
7523
6222
|
return true;
|
|
7524
6223
|
}
|
|
6224
|
+
/** No pane is needed for a compatibility refusal to remain in Needs you. */
|
|
6225
|
+
setKiroCompatibilityHold(name, message) {
|
|
6226
|
+
const holds = this.kiroCompatibilityHolds ??= new Map();
|
|
6227
|
+
if ((message === null && !holds.has(name)) || (message !== null && holds.get(name)?.message === message))
|
|
6228
|
+
return;
|
|
6229
|
+
if (message === null)
|
|
6230
|
+
holds.delete(name);
|
|
6231
|
+
else
|
|
6232
|
+
holds.set(name, { message, since: Date.now() });
|
|
6233
|
+
this.needsYou?.poke();
|
|
6234
|
+
}
|
|
7525
6235
|
/**
|
|
7526
6236
|
* LifecycleContext: a kiro instance the installed kiro-cli cannot run as
|
|
7527
6237
|
* configured (#1109) — at start, or at a respawn after kiro-cli replaced
|
|
@@ -7628,7 +6338,7 @@ export class FleetManager {
|
|
|
7628
6338
|
return count;
|
|
7629
6339
|
}
|
|
7630
6340
|
configuredStartupInstanceNames(fleet, includeClassic) {
|
|
7631
|
-
const names = new Set(Object.
|
|
6341
|
+
const names = new Set(Object.keys(fleet.instances));
|
|
7632
6342
|
if (includeClassic) {
|
|
7633
6343
|
for (const channel of this.classicChannels?.getAll() ?? [])
|
|
7634
6344
|
names.add(channel.instanceName);
|
|
@@ -7679,10 +6389,6 @@ export class FleetManager {
|
|
|
7679
6389
|
return poster.postCompletion({ resolveAdapter: () => this.readyProgressAdapter(adapterId), chatId, threadId }, text);
|
|
7680
6390
|
}
|
|
7681
6391
|
async stopInstance(name, transition) {
|
|
7682
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
7683
|
-
this.nativeInstanceRuntime().setPaused(name, true);
|
|
7684
|
-
return { backgroundWorkEnded: false };
|
|
7685
|
-
}
|
|
7686
6392
|
this.explicitStopGeneration.set(name, (this.explicitStopGeneration.get(name) ?? 0) + 1);
|
|
7687
6393
|
this.cancelStartupRetry(name);
|
|
7688
6394
|
this.failoverActive.delete(name);
|
|
@@ -7722,16 +6428,6 @@ export class FleetManager {
|
|
|
7722
6428
|
async restartSingleInstance(name, opts) {
|
|
7723
6429
|
if (opts?.ownerCurrent && !opts.ownerCurrent())
|
|
7724
6430
|
throw new RestartSupersededError(this.shownInstanceName(name));
|
|
7725
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
7726
|
-
const runtime = this.nativeInstanceRuntime();
|
|
7727
|
-
if (runtime.get(name)?.blocked)
|
|
7728
|
-
throw new Error("Verify the disconnected Windows process stopped and recover this agent before restarting");
|
|
7729
|
-
runtime.cancel(name);
|
|
7730
|
-
runtime.setPaused(name, false);
|
|
7731
|
-
return;
|
|
7732
|
-
}
|
|
7733
|
-
if (opts?.explicit && isNativePaneInstance(this.fleetConfig?.instances[name]))
|
|
7734
|
-
this.lifecycle.resetNativeCrashBudget(name);
|
|
7735
6431
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
7736
6432
|
if (migration && migration.token !== opts?.kiroMigration)
|
|
7737
6433
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -7795,7 +6491,7 @@ export class FleetManager {
|
|
|
7795
6491
|
return run;
|
|
7796
6492
|
}
|
|
7797
6493
|
isInstanceRestarting(name) {
|
|
7798
|
-
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true
|
|
6494
|
+
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
|
|
7799
6495
|
}
|
|
7800
6496
|
/** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
|
|
7801
6497
|
restartInFlight(name) {
|
|
@@ -8090,9 +6786,6 @@ export class FleetManager {
|
|
|
8090
6786
|
log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
|
|
8091
6787
|
}
|
|
8092
6788
|
publicOwnerCurrent(owner) {
|
|
8093
|
-
// #3008: the outside link is this computer's admin's — while "Allow connections from outside" is on, web-only.
|
|
8094
|
-
if (isOutsideLinkOwner(owner))
|
|
8095
|
-
return !this.shuttingDown && this.outsideOn();
|
|
8096
6789
|
if (this.shuttingDown || !this.hasFleetAdmins(owner.adapterId) || !this.isFleetAdmin(owner.userId, owner.adapterId))
|
|
8097
6790
|
return false;
|
|
8098
6791
|
const configured = (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).find(ch => (ch.id ?? ch.type) === owner.adapterId);
|
|
@@ -8182,11 +6875,7 @@ export class FleetManager {
|
|
|
8182
6875
|
}
|
|
8183
6876
|
getPublicWebLink() {
|
|
8184
6877
|
this.initializeWebSessions();
|
|
8185
|
-
return this.publicWebLink ??= new PublicWebLink(
|
|
8186
|
-
}
|
|
8187
|
-
/** The public link's wiring to this fleet (a test builds one with the tunnel itself stubbed: `ensure`, `provider`). */
|
|
8188
|
-
publicWebLinkDeps() {
|
|
8189
|
-
return {
|
|
6878
|
+
return this.publicWebLink ??= new PublicWebLink({
|
|
8190
6879
|
dataDir: this.dataDir, web: () => this.fleetConfig?.web,
|
|
8191
6880
|
permitted: owner => this.publicOwnerCurrent(owner),
|
|
8192
6881
|
reserve: id => this.getTunnelLane().reserve("dashboard", id),
|
|
@@ -8219,9 +6908,7 @@ export class FleetManager {
|
|
|
8219
6908
|
if (world && world.adapter === owner.binding)
|
|
8220
6909
|
void world.adapter.sendText(owner.chatId, t("dashboard.public_cleanup"), { threadId: owner.threadId }).catch(() => { });
|
|
8221
6910
|
},
|
|
8222
|
-
|
|
8223
|
-
this.outsideLinkClosed(reason); },
|
|
8224
|
-
};
|
|
6911
|
+
});
|
|
8225
6912
|
}
|
|
8226
6913
|
publicLinkAutoOpen = null;
|
|
8227
6914
|
/**
|
|
@@ -9580,9 +8267,6 @@ export class FleetManager {
|
|
|
9580
8267
|
const fleet = this.loadConfig(configPath, { startup: true });
|
|
9581
8268
|
this.slimFleetConfigAtStartup();
|
|
9582
8269
|
this.logConfigWarningsAtStartup();
|
|
9583
|
-
this.initializeNativeInstances();
|
|
9584
|
-
// M-2255: from here on a change to the Windows runtimes' switches applies live (observeRuntimeSwitches).
|
|
9585
|
-
this.runtimeEffects = { windows_cli: localWindowsCliEnabled(), remote_runtime: remoteRuntimeEnabled() };
|
|
9586
8270
|
setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
|
|
9587
8271
|
// #1737 review: the marker this boot adopts — the startup clear may only ever clear this one.
|
|
9588
8272
|
const bootUpdateMarker = updateMarkerIdentity(this.dataDir);
|
|
@@ -9605,8 +8289,20 @@ export class FleetManager {
|
|
|
9605
8289
|
// #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
|
|
9606
8290
|
const topicMode = daemonTopicMode(fleet);
|
|
9607
8291
|
// Set tmux socket isolation for custom AGEND_HOME
|
|
9608
|
-
const { getTmuxSocketName: getSocket } = await import("./paths.js");
|
|
8292
|
+
const { getTmuxSocketName: getSocket, isDefaultAgendHome } = await import("./paths.js");
|
|
9609
8293
|
TmuxManager.setSocketName(getSocket());
|
|
8294
|
+
// M-2457 §7: after a raw downgrade from 2.4, its own tmux server may still run the agents — start none beside it.
|
|
8295
|
+
// main / the 2.3 line ONLY: on the 2.4 line (release/2.3.5-alpha) AgEnD's own server IS the one on `agend` — a
|
|
8296
|
+
// main → alpha sync must keep alpha's side of this block, never both.
|
|
8297
|
+
{
|
|
8298
|
+
const { newerTmuxServer, realGuardDeps, guardMessage } = await import("./bundled-tmux-guard.js");
|
|
8299
|
+
const hit = await newerTmuxServer(realGuardDeps(getAgendHome(), isDefaultAgendHome(), getSocket()));
|
|
8300
|
+
if (hit) {
|
|
8301
|
+
const message = guardMessage(hit);
|
|
8302
|
+
this.logger.error({ socket: hit.socket, confirmed: hit.confirmed }, message);
|
|
8303
|
+
throw new Error(message);
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
9610
8306
|
await TmuxManager.ensureSession(getTmuxSession());
|
|
9611
8307
|
this.tmuxSessionEnsured = true;
|
|
9612
8308
|
// #2072: what this host can run — probed in the background, published for every picker.
|
|
@@ -9614,7 +8310,7 @@ export class FleetManager {
|
|
|
9614
8310
|
// Pre-flight (advisory, fire-and-forget): warm the per-backend auth cache
|
|
9615
8311
|
// before instances spawn, so a CLI that comes up on its sign-in screen gets
|
|
9616
8312
|
// an instant 🔑 verdict instead of decaying into crash/MCP-died noise.
|
|
9617
|
-
this.lifecycle.primeAuthVerification(Object.values(fleet.instances).
|
|
8313
|
+
this.lifecycle.primeAuthVerification(Object.values(fleet.instances).map(config => {
|
|
9618
8314
|
const backend = config.backend ?? fleet.defaults?.backend ?? "claude-code";
|
|
9619
8315
|
// #1699 follow-up: each instance's own login — a profile is probed with its own environment.
|
|
9620
8316
|
return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, fleet.defaults, backend) : null };
|
|
@@ -9676,7 +8372,6 @@ export class FleetManager {
|
|
|
9676
8372
|
// #1565: the web chat as it was before this process — the configured instances' only (fleet.yaml and ClassicBot
|
|
9677
8373
|
// rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
|
|
9678
8374
|
this.restoreWebChat(fleet);
|
|
9679
|
-
this.initializeNativeInstances(true);
|
|
9680
8375
|
this.startSearchIndex(fleet);
|
|
9681
8376
|
// Parse-only source monitoring; it does not apply hand-edited Fleet settings.
|
|
9682
8377
|
this.configHistoryTimer = setInterval(() => this.configHistory?.check(), 1_000);
|
|
@@ -9859,7 +8554,7 @@ export class FleetManager {
|
|
|
9859
8554
|
}
|
|
9860
8555
|
}
|
|
9861
8556
|
// Phase 1: Start general instances first and wait for them
|
|
9862
|
-
const allEntries = Object.entries(fleet.instances)
|
|
8557
|
+
const allEntries = Object.entries(fleet.instances);
|
|
9863
8558
|
const generals = allEntries.filter(([_, cfg]) => cfg.general_topic);
|
|
9864
8559
|
const others = allEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
9865
8560
|
const startupProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), pendingUpdateProgress?.startedAt ?? startupStartedAt, this.logger, { mode: pendingProgressOperation === "full-restart" ? "reload" : pendingUpdateProgress ? "update" : "restart" });
|
|
@@ -9973,6 +8668,9 @@ export class FleetManager {
|
|
|
9973
8668
|
if (others.length > 0 || classicAtBoot.length > 0) {
|
|
9974
8669
|
await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
|
|
9975
8670
|
}
|
|
8671
|
+
// #3066 §1d (Fable r1): on every fleet — a web-only one (#2910) has no chat connection at all, and is the clearest
|
|
8672
|
+
// case with no Discord bot reading. Each tick decides for itself whether to read (backgroundQuotaRead).
|
|
8673
|
+
this.startQuotaBackgroundReads();
|
|
9976
8674
|
if (topicMode && (fleet.channel || fleet.channels?.length)) {
|
|
9977
8675
|
await adapterStartup;
|
|
9978
8676
|
this.startDiscordUsagePresence();
|
|
@@ -10098,10 +8796,6 @@ export class FleetManager {
|
|
|
10098
8796
|
// #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
|
|
10099
8797
|
this.startPublicLinkAutoOpen();
|
|
10100
8798
|
}
|
|
10101
|
-
// #3008: connections from outside were on when the fleet stopped: the link opens again (its address is new for a
|
|
10102
|
-
// quick link — Settings shows it on this computer).
|
|
10103
|
-
if (this.outsideOn())
|
|
10104
|
-
this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
|
|
10105
8799
|
// #1266: buttons that ended while AgEnD was down (expired, or chosen before a restart) are shown as ended now.
|
|
10106
8800
|
// #1823: and polls: a deadline that passed meanwhile closes now (results once); an open one's countdown is redrawn.
|
|
10107
8801
|
void this.replyButtons()?.sweep().catch(err => this.logger.warn({ err }, "Reply-button sweep failed"));
|
|
@@ -10148,6 +8842,214 @@ export class FleetManager {
|
|
|
10148
8842
|
this.logger.warn({ health }, "Fleet started with problems — see /health");
|
|
10149
8843
|
}
|
|
10150
8844
|
}
|
|
8845
|
+
/**
|
|
8846
|
+
* #3066 §1d (the user: yes, every 30 min): the quota history (usage/quota-history.ts) records only the readings AgEnD
|
|
8847
|
+
* makes. A Discord bot's presence refresh reads every 15 min; without one, nothing reads while nobody looks. So, with no
|
|
8848
|
+
* Discord bot and while some account has a running agent, read every 30 min — the same cached read (its 5-minute TTL
|
|
8849
|
+
* still bounds the vendor calls), recorded like any other.
|
|
8850
|
+
*/
|
|
8851
|
+
quotaNotices = null;
|
|
8852
|
+
stopQuotaObserver = null;
|
|
8853
|
+
quotaNoticePolicy() {
|
|
8854
|
+
const defaults = this.fleetConfig?.defaults.quota_notice;
|
|
8855
|
+
const wanted = defaults?.target ?? "general";
|
|
8856
|
+
const instances = this.fleetConfig?.instances ?? {};
|
|
8857
|
+
const target = wanted === "general"
|
|
8858
|
+
? Object.entries(instances).find(([name, c]) => c.general_topic === true || name === "general")?.[0]
|
|
8859
|
+
: Object.hasOwn(instances, wanted) ? wanted : undefined;
|
|
8860
|
+
const webOnly = !!this.webOnlyPromptPlace();
|
|
8861
|
+
const config = target ? instances[target] : undefined;
|
|
8862
|
+
const adapterId = target ? this.getInstanceAdapterId(target) : undefined;
|
|
8863
|
+
const groupId = target ? this.getChannelConfig(adapterId)?.group_id : undefined;
|
|
8864
|
+
const ready = !!target && (webOnly || (config?.topic_id != null && !!groupId && !!(this.getAdapterForInstance(target) ?? this.adapter)));
|
|
8865
|
+
return {
|
|
8866
|
+
target: this.shuttingDown ? "" : ready ? target : !target && wanted === "general" && webOnly ? "/notices" : "",
|
|
8867
|
+
destinationBinding: JSON.stringify([webOnly, adapterId, groupId, config?.topic_id]),
|
|
8868
|
+
locale: this.fleetConfig?.defaults.locale ?? getLocale(),
|
|
8869
|
+
subscriptions: this.shuttingDown ? [] : this.activeBackendBindings().flatMap(([name, backend, profile]) => {
|
|
8870
|
+
const provider = usageProviderIdForBackend(backend);
|
|
8871
|
+
if (!provider)
|
|
8872
|
+
return [];
|
|
8873
|
+
const providerId = profile ? `${provider}:${profile}` : provider;
|
|
8874
|
+
return [{ account: `${backend}:host:${profile ?? ""}`, providerId,
|
|
8875
|
+
rule: quotaNoticeRule(defaults, backend, instances[name]?.quota_notice ?? this.classicChannels?.getQuotaNoticeByInstance?.(name)) }];
|
|
8876
|
+
}),
|
|
8877
|
+
};
|
|
8878
|
+
}
|
|
8879
|
+
startQuotaBackgroundReads() {
|
|
8880
|
+
if (!this.quotaNotices) {
|
|
8881
|
+
this.quotaNotices = new QuotaNotices({
|
|
8882
|
+
file: join(this.dataDir, "quota-notices.json"), policy: () => this.quotaNoticePolicy(),
|
|
8883
|
+
send: (target, text) => target === "/notices" ? this.recordWebNotice("fleet", text) : this.notifyInstanceTopic(target, text),
|
|
8884
|
+
warn: err => this.logger.warn({ err }, "Optional quota reminders held: could not retain dispatch state"),
|
|
8885
|
+
});
|
|
8886
|
+
this.stopQuotaObserver = onUsageSnapshot(payload => { void this.quotaNotices?.observe(payload); });
|
|
8887
|
+
}
|
|
8888
|
+
if (this.quotaReadTimer)
|
|
8889
|
+
clearInterval(this.quotaReadTimer);
|
|
8890
|
+
this.quotaReadTimer = setInterval(() => { void this.backgroundQuotaRead(); }, FleetManager.QUOTA_BACKGROUND_READ_MS);
|
|
8891
|
+
this.quotaReadTimer.unref?.();
|
|
8892
|
+
// #3066 §1: the measured capacity, published for the usage reads (get_usage, the page) — refreshed every 10 min.
|
|
8893
|
+
setCapacityLookup((provider, metric) => this.capacityEstimates.get(`${provider}\u0000${metric}`) ?? null);
|
|
8894
|
+
if (this.capacityTimer)
|
|
8895
|
+
clearInterval(this.capacityTimer);
|
|
8896
|
+
void this.refreshCapacity();
|
|
8897
|
+
this.capacityTimer = setInterval(() => { void this.refreshCapacity(); }, FleetManager.CAPACITY_REFRESH_MS);
|
|
8898
|
+
this.capacityTimer.unref?.();
|
|
8899
|
+
}
|
|
8900
|
+
/** #3066 §1: the latest estimates, by `provider\0metric` (what usage-api's lookup reads, synchronously). */
|
|
8901
|
+
capacityEstimates = new Map();
|
|
8902
|
+
capacityTimer = null;
|
|
8903
|
+
static CAPACITY_REFRESH_MS = 10 * 60_000;
|
|
8904
|
+
/**
|
|
8905
|
+
* #3066 §1: estimate each account's weekly 100% from the quota history (§1d) and the usage ledger's hours — both read
|
|
8906
|
+
* asynchronously, the arithmetic small (usage/capacity.ts). An account is its usage provider id ("claude",
|
|
8907
|
+
* "claude:hsinlin", "codex:yanxianchen"): its hours are the ledger sources of that backend under that credential profile
|
|
8908
|
+
* (Codex's "unrecorded" hours are no account's). A per-model weekly ("Sonnet (weekly)") counts only that model's tokens.
|
|
8909
|
+
*
|
|
8910
|
+
* Prism #3073: no estimate rather than a wrong one —
|
|
8911
|
+
* - a window is measured only where the ledger has its hours: kept (31 days) and scanned (`days.coverage`) for every
|
|
8912
|
+
* instance of the account's backend, and every instance whose hours count for it; the quota history goes back 12 weeks;
|
|
8913
|
+
* - a profile literally named "default", "unknown" or "unrecorded" (configured, on disk, or in the quota history) makes
|
|
8914
|
+
* those hours ambiguous: neither the host
|
|
8915
|
+
* account nor the named one is estimated (each would be credited the other's use, or lose its own);
|
|
8916
|
+
* - only a metric the vendor marks per-model (`scope: "model"`) is filtered by model; another named weekly pool
|
|
8917
|
+
* ("Luna Reserve (weekly)", Antigravity pools) is no model name and is not estimated.
|
|
8918
|
+
*/
|
|
8919
|
+
async refreshCapacity() {
|
|
8920
|
+
try {
|
|
8921
|
+
const readings = await readQuotaHistory();
|
|
8922
|
+
const groups = new Map();
|
|
8923
|
+
for (const r of readings) {
|
|
8924
|
+
const k = `${r.provider}\u0000${r.metric}`;
|
|
8925
|
+
(groups.get(k) ?? groups.set(k, []).get(k)).push(r);
|
|
8926
|
+
}
|
|
8927
|
+
if (!groups.size) {
|
|
8928
|
+
this.capacityEstimates = new Map();
|
|
8929
|
+
return 0;
|
|
8930
|
+
}
|
|
8931
|
+
const data = await this.cacheServiceFor().usageData();
|
|
8932
|
+
const now = Date.now(), DAY = 24 * 3_600_000, HOUR = 3_600_000;
|
|
8933
|
+
const keptFrom = now - KEEP_DAYS * DAY + HOUR; // the oldest hour still in every ledger (an hour of margin)
|
|
8934
|
+
// Profiles whose names are also the ledger's own tags: their hours cannot be told from the host's / unattributed.
|
|
8935
|
+
// Any trace of such a profile counts (Prism #3073 r2), not only today's config — a binding removed or switched
|
|
8936
|
+
// leaves its hours in the ledger: configured now, its directory on this host, or its account in the quota history.
|
|
8937
|
+
const RESERVED = new Set(["default", "unknown", PROFILE_UNRECORDED]);
|
|
8938
|
+
const historyProviders = new Set(readings.map((r) => r.provider));
|
|
8939
|
+
const reservedNamed = new Map();
|
|
8940
|
+
const reservedFor = (backend) => {
|
|
8941
|
+
const known = reservedNamed.get(backend);
|
|
8942
|
+
if (known)
|
|
8943
|
+
return known;
|
|
8944
|
+
const found = new Set(listConfiguredProfiles(this.fleetConfig, backend).filter((n) => RESERVED.has(n)));
|
|
8945
|
+
const base = usageProviderIdForBackend(backend);
|
|
8946
|
+
for (const n of RESERVED) {
|
|
8947
|
+
if (base && historyProviders.has(`${base}:${n}`))
|
|
8948
|
+
found.add(n);
|
|
8949
|
+
if (credentialHomeSpec(backend) && existsSync(credentialProfileHome(this.dataDir, backend, n)))
|
|
8950
|
+
found.add(n);
|
|
8951
|
+
}
|
|
8952
|
+
return reservedNamed.set(backend, found).get(backend);
|
|
8953
|
+
};
|
|
8954
|
+
// Every instance of a backend may have used any of its accounts (bindings change): an account is measured only
|
|
8955
|
+
// where all of them were scanned (Prism #3073 r2) — one still waiting for its first scan has no hours yet, not none.
|
|
8956
|
+
const daysOfBase = new Map();
|
|
8957
|
+
for (const inst of data.instances) {
|
|
8958
|
+
const base = usageProviderIdForBackend(inst.inst.backend);
|
|
8959
|
+
if (base)
|
|
8960
|
+
(daysOfBase.get(base) ?? daysOfBase.set(base, []).get(base)).push(inst.days);
|
|
8961
|
+
}
|
|
8962
|
+
// provider id → hour → model key → totals; and the instances whose hours count for it (their scan coverage).
|
|
8963
|
+
const byProvider = new Map();
|
|
8964
|
+
const daysOf = new Map();
|
|
8965
|
+
for (const inst of data.instances) {
|
|
8966
|
+
for (const [hk, hour] of Object.entries(inst.ledger.hours)) {
|
|
8967
|
+
for (const src of hourSources(hour)) {
|
|
8968
|
+
const base = usageProviderIdForBackend(src.backend);
|
|
8969
|
+
if (!base)
|
|
8970
|
+
continue;
|
|
8971
|
+
// Ambiguous (a named profile with the tag's name), or no account's: counted for no one, so an account whose
|
|
8972
|
+
// hours these may be has none — and gets no estimate, never a low one.
|
|
8973
|
+
if (reservedFor(src.backend).has(src.profile) || src.profile === "unknown" || src.profile === PROFILE_UNRECORDED)
|
|
8974
|
+
continue;
|
|
8975
|
+
const provider = src.profile === "default" ? base : `${base}:${src.profile}`;
|
|
8976
|
+
const list = daysOf.get(provider) ?? daysOf.set(provider, []).get(provider);
|
|
8977
|
+
if (!list.includes(inst.days))
|
|
8978
|
+
list.push(inst.days);
|
|
8979
|
+
const hours = byProvider.get(provider) ?? byProvider.set(provider, new Map()).get(provider);
|
|
8980
|
+
const into = hours.get(Number(hk)) ?? hours.set(Number(hk), Object.create(null)).get(Number(hk));
|
|
8981
|
+
for (const [mk, tot] of Object.entries(src.m)) {
|
|
8982
|
+
const cur = into[mk] ??= [0, 0, 0, 0, 0, 0, 0];
|
|
8983
|
+
for (let i = 0; i < 7; i++)
|
|
8984
|
+
cur[i] = (cur[i] ?? 0) + (tot[i] ?? 0);
|
|
8985
|
+
}
|
|
8986
|
+
}
|
|
8987
|
+
}
|
|
8988
|
+
}
|
|
8989
|
+
const next = new Map();
|
|
8990
|
+
for (const [key, list] of groups) {
|
|
8991
|
+
const [provider, metric] = key.split("\u0000");
|
|
8992
|
+
// A per-model weekly ("Sonnet (weekly)", the vendor's scope "model"): that model's tokens only. Another named
|
|
8993
|
+
// pool is not a model: no estimate. "Weekly", "Weekly limit": everything.
|
|
8994
|
+
const named = /^(.+) \((?:weekly|monthly|\d+d)\)$/i.exec(metric)?.[1]?.toLowerCase() ?? null;
|
|
8995
|
+
const perModel = list.some((r) => r.scope === "model");
|
|
8996
|
+
if (named && !perModel)
|
|
8997
|
+
continue;
|
|
8998
|
+
const hours = [];
|
|
8999
|
+
for (const [hour, models] of byProvider.get(provider) ?? []) {
|
|
9000
|
+
let api = 0, output = 0, all = 0;
|
|
9001
|
+
for (const [mk, tot] of Object.entries(models)) {
|
|
9002
|
+
const bar = mk.lastIndexOf("|"), model = mk.slice(0, bar), long = mk.slice(bar + 1) === "1";
|
|
9003
|
+
if (named && !model.toLowerCase().includes(named))
|
|
9004
|
+
continue;
|
|
9005
|
+
const [, uncached, read, w5, w1h, w, out] = tot;
|
|
9006
|
+
const r = ratesFor(model, long ? Number.MAX_SAFE_INTEGER : 0);
|
|
9007
|
+
api = api === null || !r ? null : api + (uncached * r.input + read * r.read + w5 * r.write5m + w1h * r.write1h + w * r.write + out * r.output) / 1_000_000;
|
|
9008
|
+
output += out;
|
|
9009
|
+
all += uncached + read + w5 + w1h + w + out;
|
|
9010
|
+
}
|
|
9011
|
+
if (all > 0)
|
|
9012
|
+
hours.push({ hour, api, output, all });
|
|
9013
|
+
}
|
|
9014
|
+
const base = provider.split(":")[0];
|
|
9015
|
+
const scanned = [...new Set([...(daysOf.get(provider) ?? []), ...(daysOfBase.get(base) ?? [])])];
|
|
9016
|
+
const available = (from, to) => {
|
|
9017
|
+
if (from < keptFrom || !scanned.length)
|
|
9018
|
+
return false;
|
|
9019
|
+
for (let h = from; h <= to; h += HOUR)
|
|
9020
|
+
if (!scanned.every((d) => covered(d, h)))
|
|
9021
|
+
return false;
|
|
9022
|
+
return true;
|
|
9023
|
+
};
|
|
9024
|
+
const est = estimateCapacity(list, hours, available);
|
|
9025
|
+
if (est)
|
|
9026
|
+
next.set(key, est);
|
|
9027
|
+
}
|
|
9028
|
+
this.capacityEstimates = next;
|
|
9029
|
+
return next.size;
|
|
9030
|
+
}
|
|
9031
|
+
catch (err) {
|
|
9032
|
+
this.logger.debug({ err }, "capacity estimate refresh failed");
|
|
9033
|
+
return -1;
|
|
9034
|
+
}
|
|
9035
|
+
}
|
|
9036
|
+
/** One background quota read, when nothing else reads (no Discord bot) and an account has a running agent. */
|
|
9037
|
+
async backgroundQuotaRead() {
|
|
9038
|
+
if (this.shuttingDown)
|
|
9039
|
+
return false;
|
|
9040
|
+
if ([...this.adapters.values()].some(a => a.type === "discord" && typeof a.setActivity === "function"))
|
|
9041
|
+
return false;
|
|
9042
|
+
const active = this.getActiveUsageProviderIds();
|
|
9043
|
+
if (active.size === 0)
|
|
9044
|
+
return false;
|
|
9045
|
+
try {
|
|
9046
|
+
await getUsageSnapshot(false, active);
|
|
9047
|
+
return true;
|
|
9048
|
+
}
|
|
9049
|
+
catch {
|
|
9050
|
+
return false;
|
|
9051
|
+
}
|
|
9052
|
+
}
|
|
10151
9053
|
/** Keep Discord profile activity aligned with the same cached usage source as /usage. */
|
|
10152
9054
|
startDiscordUsagePresence() {
|
|
10153
9055
|
if (this.discordPresenceTimer)
|
|
@@ -10623,6 +9525,11 @@ export class FleetManager {
|
|
|
10623
9525
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
10624
9526
|
await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
|
|
10625
9527
|
}, this.logger, "adapter.callback_query"));
|
|
9528
|
+
adapter.on("missed_offline", (ev) => {
|
|
9529
|
+
if (!isCurrentAdapter())
|
|
9530
|
+
return;
|
|
9531
|
+
this.recordMissedOffline(ev);
|
|
9532
|
+
});
|
|
10626
9533
|
this.bindTopicClosedHandler(adapter, adapterId, "adapter.topic_closed");
|
|
10627
9534
|
// Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
|
|
10628
9535
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
@@ -10776,6 +9683,11 @@ export class FleetManager {
|
|
|
10776
9683
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
10777
9684
|
await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
|
|
10778
9685
|
}, this.logger, `adapter[${adapterId}].callback_query`));
|
|
9686
|
+
adapter.on("missed_offline", (ev) => {
|
|
9687
|
+
if (!isCurrentAdapter())
|
|
9688
|
+
return;
|
|
9689
|
+
this.recordMissedOffline(ev);
|
|
9690
|
+
});
|
|
10779
9691
|
this.bindTopicClosedHandler(adapter, adapterId, `adapter[${adapterId}].topic_closed`);
|
|
10780
9692
|
// Slash commands: classic bot + admin commands
|
|
10781
9693
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
@@ -10865,14 +9777,10 @@ export class FleetManager {
|
|
|
10865
9777
|
}
|
|
10866
9778
|
}
|
|
10867
9779
|
}
|
|
10868
|
-
const daemon = this.daemons.get(name);
|
|
10869
|
-
const native = daemon?.usesNativePane?.();
|
|
10870
9780
|
const sockPath = join(this.getInstanceDir(name), "channel.sock");
|
|
10871
|
-
if (!
|
|
9781
|
+
if (!existsSync(sockPath))
|
|
10872
9782
|
return;
|
|
10873
|
-
const ipc =
|
|
10874
|
-
? new NativeDaemonClient(daemon, () => this.daemons.get(name) === daemon && (!this.instanceIpcClients.has(name) || this.instanceIpcClients.get(name) === ipc))
|
|
10875
|
-
: new IpcClient(sockPath);
|
|
9783
|
+
const ipc = new IpcClient(sockPath);
|
|
10876
9784
|
try {
|
|
10877
9785
|
await ipc.connect();
|
|
10878
9786
|
this.instanceIpcClients.set(name, ipc);
|
|
@@ -10988,7 +9896,7 @@ export class FleetManager {
|
|
|
10988
9896
|
// connected, so seed the cache instead of waiting for another transition.
|
|
10989
9897
|
ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
|
|
10990
9898
|
this.logger.debug({ name }, "Connected to instance IPC");
|
|
10991
|
-
if (!
|
|
9899
|
+
if (!this.statuslineWatcher.has(name)) {
|
|
10992
9900
|
this.statuslineWatcher.watch(name);
|
|
10993
9901
|
}
|
|
10994
9902
|
// Auto-reconnect on disconnect (unless intentionally stopping)
|
|
@@ -11516,6 +10424,34 @@ export class FleetManager {
|
|
|
11516
10424
|
}
|
|
11517
10425
|
return true;
|
|
11518
10426
|
}
|
|
10427
|
+
/** File a Telegram update that was too old to inject; Needs you lists it until Acknowledge. */
|
|
10428
|
+
recordMissedOffline(ev) {
|
|
10429
|
+
if (!this.missedOfflineStore)
|
|
10430
|
+
this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
|
|
10431
|
+
const stub = {
|
|
10432
|
+
source: "telegram", adapterId: ev.adapterId, chatId: ev.chatId, threadId: ev.threadId,
|
|
10433
|
+
messageId: ev.messageId, userId: ev.userId, username: ev.username, text: ev.preview,
|
|
10434
|
+
timestamp: ev.timestamp, isBotMessage: false,
|
|
10435
|
+
};
|
|
10436
|
+
const threadId = this.inboundRouteThreadId(stub);
|
|
10437
|
+
const target = threadId ? this.resolveInboundTarget(stub, threadId) : undefined;
|
|
10438
|
+
let instance = target?.name ?? "";
|
|
10439
|
+
if (!instance) {
|
|
10440
|
+
instance = Object.entries(this.fleetConfig?.instances ?? {}).find(([name, cfg]) => cfg.general_topic && this.getInstanceAdapterId(name) === ev.adapterId)?.[0] ?? "";
|
|
10441
|
+
}
|
|
10442
|
+
const place = [ev.chatTitle, ev.threadId ? `#${ev.threadId}` : ""].filter(Boolean).join(" · ") || ev.chatId;
|
|
10443
|
+
this.missedOfflineStore.add({
|
|
10444
|
+
id: `missed:${ev.adapterId}:${ev.updateId}`,
|
|
10445
|
+
adapterId: ev.adapterId,
|
|
10446
|
+
instance,
|
|
10447
|
+
username: ev.username,
|
|
10448
|
+
place,
|
|
10449
|
+
preview: ev.preview,
|
|
10450
|
+
since: ev.timestamp.getTime(),
|
|
10451
|
+
});
|
|
10452
|
+
this.logger.info({ adapterId: ev.adapterId, updateId: ev.updateId, instance, chatId: ev.chatId, threadId: ev.threadId }, "Telegram message missed while AgEnD was offline — filed for Needs you");
|
|
10453
|
+
this.needsYou?.poke();
|
|
10454
|
+
}
|
|
11519
10455
|
/**
|
|
11520
10456
|
* #1085: coincident topic numbers must not route across Telegram groups.
|
|
11521
10457
|
*/
|
|
@@ -12035,12 +10971,6 @@ export class FleetManager {
|
|
|
12035
10971
|
if (msg.adapterId)
|
|
12036
10972
|
this.bindInstanceAdapter(generalInstance, msg.adapterId, true);
|
|
12037
10973
|
const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
12038
|
-
const locationHold = this.generalLocationDeliveryHold(generalInstance);
|
|
12039
|
-
if (locationHold) {
|
|
12040
|
-
await inboundAdapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
|
|
12041
|
-
this.needsYou?.poke();
|
|
12042
|
-
return;
|
|
12043
|
-
}
|
|
12044
10974
|
// React immediately — before any other API calls — through the status
|
|
12045
10975
|
// path, which tracks Telegram slot ownership and Discord status adds.
|
|
12046
10976
|
if (msg.chatId && msg.messageId) {
|
|
@@ -12128,13 +11058,6 @@ export class FleetManager {
|
|
|
12128
11058
|
if (instanceConfig?.general_topic && await this.topicCommands.handleGeneralCommand(msg, instanceName)) {
|
|
12129
11059
|
return;
|
|
12130
11060
|
}
|
|
12131
|
-
const locationHold = this.generalLocationDeliveryHold(instanceName);
|
|
12132
|
-
if (locationHold) {
|
|
12133
|
-
const adapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
12134
|
-
await adapter.sendText(msg.chatId, locationHold, { threadId: msg.threadId || undefined });
|
|
12135
|
-
this.needsYou?.poke();
|
|
12136
|
-
return;
|
|
12137
|
-
}
|
|
12138
11061
|
// Bind instance to the adapter that delivered this message
|
|
12139
11062
|
if (msg.adapterId)
|
|
12140
11063
|
this.bindInstanceAdapter(instanceName, msg.adapterId, true);
|
|
@@ -13152,6 +12075,11 @@ export class FleetManager {
|
|
|
13152
12075
|
let result;
|
|
13153
12076
|
switch (msg.type) {
|
|
13154
12077
|
case "fleet_decision_create": {
|
|
12078
|
+
const supersedes = this.decisionSupersedes("ipc-typed", instanceName, payload.supersedes);
|
|
12079
|
+
if ("error" in supersedes) {
|
|
12080
|
+
ipc.send({ type: "fleet_decision_response", fleetRequestId, error: supersedes.error });
|
|
12081
|
+
return;
|
|
12082
|
+
}
|
|
13155
12083
|
// Prune expired decisions on create
|
|
13156
12084
|
db.pruneExpiredDecisions();
|
|
13157
12085
|
result = db.createDecision({
|
|
@@ -13162,7 +12090,7 @@ export class FleetManager {
|
|
|
13162
12090
|
tags: payload.tags,
|
|
13163
12091
|
ttl_days: payload.ttl_days,
|
|
13164
12092
|
created_by: instanceName,
|
|
13165
|
-
supersedes:
|
|
12093
|
+
supersedes: supersedes.id,
|
|
13166
12094
|
});
|
|
13167
12095
|
break;
|
|
13168
12096
|
}
|
|
@@ -13339,6 +12267,21 @@ export class FleetManager {
|
|
|
13339
12267
|
return "ok";
|
|
13340
12268
|
}
|
|
13341
12269
|
}
|
|
12270
|
+
/**
|
|
12271
|
+
* `post_decision` with `supersedes` changes a decision someone else may have recorded (it stops being active): the
|
|
12272
|
+
* same permission as `update_decision`, asked through the same live check (tool set + dashboard switches), on both
|
|
12273
|
+
* create paths. An empty or absent value supersedes nothing and needs nothing; anything else must be an id.
|
|
12274
|
+
*/
|
|
12275
|
+
decisionSupersedes(sink, instanceName, value) {
|
|
12276
|
+
if (value === undefined || value === null || value === "")
|
|
12277
|
+
return { id: undefined };
|
|
12278
|
+
if (typeof value !== "string")
|
|
12279
|
+
return { error: "supersedes must be a decision id" };
|
|
12280
|
+
const permitted = this.checkToolPermission(sink, instanceName, "update_decision");
|
|
12281
|
+
if (!permitted.allowed)
|
|
12282
|
+
return { error: `Superseding a decision needs update_decision. ${permitted.message}` };
|
|
12283
|
+
return { id: value };
|
|
12284
|
+
}
|
|
13342
12285
|
async handleDecisionCrudHttp(instance, op, args) {
|
|
13343
12286
|
if (!this.scheduler)
|
|
13344
12287
|
return { error: "Scheduler not available" };
|
|
@@ -13354,16 +12297,24 @@ export class FleetManager {
|
|
|
13354
12297
|
if (!title || !content)
|
|
13355
12298
|
return { error: "title and content are required" };
|
|
13356
12299
|
const scope = args.scope === "fleet" ? "fleet" : "project";
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
12300
|
+
const supersedes = this.decisionSupersedes("agent-endpoint", instance, args.supersedes);
|
|
12301
|
+
if ("error" in supersedes)
|
|
12302
|
+
return { error: supersedes.error };
|
|
12303
|
+
try {
|
|
12304
|
+
return db.createDecision({
|
|
12305
|
+
project_root: projectRoot,
|
|
12306
|
+
scope,
|
|
12307
|
+
title,
|
|
12308
|
+
content,
|
|
12309
|
+
tags: asStrArr(args.tags),
|
|
12310
|
+
ttl_days: asNum(args.ttl_days),
|
|
12311
|
+
supersedes: supersedes.id,
|
|
12312
|
+
created_by: instance,
|
|
12313
|
+
});
|
|
12314
|
+
}
|
|
12315
|
+
catch (err) {
|
|
12316
|
+
return { error: err.message }; // a refused predecessor: nothing was inserted
|
|
12317
|
+
}
|
|
13367
12318
|
}
|
|
13368
12319
|
// #2152: titles by default (agend-agent decision-list); one decision's full text by id (decision-get <id>).
|
|
13369
12320
|
// The CLI sends include_archived (snake case, as the MCP tool): both spellings are read.
|
|
@@ -13406,6 +12357,8 @@ export class FleetManager {
|
|
|
13406
12357
|
const asStatusFilter = normalizeStatusFilter;
|
|
13407
12358
|
// #1336: small write ack — identifying fields only.
|
|
13408
12359
|
const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
|
|
12360
|
+
// #3195: an update's ack also carries what was SAVED for each field it named, read back from the task.
|
|
12361
|
+
const updatedAck = (t, named) => ({ ...ack(t), ...savedFields(t, named) });
|
|
13409
12362
|
// The board's notes for the write (a send-back reason, a comment the assignee had not seen): with the ack when any.
|
|
13410
12363
|
const noted = (a) => { const notes = db.board.takeNotes(); return notes.length ? { ...a, notes } : a; };
|
|
13411
12364
|
try {
|
|
@@ -13463,12 +12416,12 @@ export class FleetManager {
|
|
|
13463
12416
|
const id = asStr(args.id);
|
|
13464
12417
|
if (!id)
|
|
13465
12418
|
return { error: "id is required" };
|
|
13466
|
-
return noted(
|
|
12419
|
+
return noted(updatedAck(db.updateTask(db.getTaskByPrefix(id).id, {
|
|
13467
12420
|
status: asStatus(args.status),
|
|
13468
12421
|
assignee: asStr(args.assignee),
|
|
13469
12422
|
result: asStr(args.result),
|
|
13470
12423
|
priority: asPriority(args.priority),
|
|
13471
|
-
}, { kind: "instance", name: instance })));
|
|
12424
|
+
}, { kind: "instance", name: instance }), args));
|
|
13472
12425
|
}
|
|
13473
12426
|
default: return { error: `Unknown task action: ${action}` };
|
|
13474
12427
|
}
|
|
@@ -13622,7 +12575,8 @@ export class FleetManager {
|
|
|
13622
12575
|
result: payload.result,
|
|
13623
12576
|
priority: payload.priority,
|
|
13624
12577
|
}, { kind: "instance", name: meta.instance_name || instanceName });
|
|
13625
|
-
|
|
12578
|
+
// #3195: with what was SAVED for each field the update named, read back from the task.
|
|
12579
|
+
result = noted({ ...ack(logTask), ...savedFields(logTask, payload) });
|
|
13626
12580
|
break;
|
|
13627
12581
|
default:
|
|
13628
12582
|
throw new Error(`Unknown task action: ${action}`);
|
|
@@ -13652,16 +12606,89 @@ export class FleetManager {
|
|
|
13652
12606
|
}
|
|
13653
12607
|
return adapter.createTopic(topicName);
|
|
13654
12608
|
}
|
|
13655
|
-
|
|
13656
|
-
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
13661
|
-
|
|
13662
|
-
|
|
13663
|
-
this.
|
|
13664
|
-
|
|
12609
|
+
/**
|
|
12610
|
+
* Delete an agent and, unless asked not to, its own chat topic (user decision: by default). The topic is planned
|
|
12611
|
+
* before the removal (its id and bot go with the fleet.yaml entry) and deleted through the agent's OWN bot after it;
|
|
12612
|
+
* the outcome is the platform's answer (instance-topic-delete.ts). A topic that cannot go never fails the removal.
|
|
12613
|
+
*/
|
|
12614
|
+
async removeInstanceAndTopic(name, authorization, opts) {
|
|
12615
|
+
const plan = opts.deleteTopic ? this.planInstanceTopicDelete(name) : null;
|
|
12616
|
+
if (opts.execution)
|
|
12617
|
+
await this.removeInstance(name, authorization, opts.execution);
|
|
12618
|
+
else
|
|
12619
|
+
await this.removeInstance(name, authorization);
|
|
12620
|
+
// #3055 review (Prism P1): asked again right before the platform call — the fleet as it is NOW, not as planned
|
|
12621
|
+
// before the removal was awaited (a new owner of the topic, or the bot rebuilt).
|
|
12622
|
+
return deletePlannedTopic(plan, this.logger, (p) => {
|
|
12623
|
+
if (this.instanceTopicAdapter(p.adapterId) !== p.adapter)
|
|
12624
|
+
return "the agent's bot changed while it was being deleted, so its topic was left";
|
|
12625
|
+
return topicMustStay(p.destination, this.topicOwnership(), null); // the original is gone: every owner now counts
|
|
12626
|
+
}, { begin: (d) => this.topicDeletionsInFlight.add(d), end: (d) => this.topicDeletionsInFlight.delete(d) });
|
|
12627
|
+
}
|
|
12628
|
+
/** #3055: destinations whose platform deletion is running now; a binding to one waits until it is over. */
|
|
12629
|
+
topicDeletionsInFlight = new Set();
|
|
12630
|
+
/** True while a topic with this id is being deleted on any connection (a new binding of it is refused meanwhile). */
|
|
12631
|
+
isTopicDeletionInFlight(topicId) {
|
|
12632
|
+
const tail = `:${topicId}`;
|
|
12633
|
+
return [...this.topicDeletionsInFlight].some(d => d.endsWith(tail));
|
|
12634
|
+
}
|
|
12635
|
+
/**
|
|
12636
|
+
* #3055 review r2 (Prism P1-3): would this instance config bind a topic whose deletion is running now? Compared by
|
|
12637
|
+
* physical destination, so the same thread number in another Telegram group is not held. Settings asks it at commit.
|
|
12638
|
+
*/
|
|
12639
|
+
topicBindingInFlight(inst) {
|
|
12640
|
+
if (this.topicDeletionsInFlight.size === 0 || inst.topic_id == null || inst.topic_id === "")
|
|
12641
|
+
return false;
|
|
12642
|
+
const adapterId = typeof inst.channel_id === "string" && inst.channel_id ? inst.channel_id : this.getPrimaryAdapterId();
|
|
12643
|
+
return this.topicDeletionsInFlight.has(this.destinationFor(adapterId, inst.topic_id));
|
|
12644
|
+
}
|
|
12645
|
+
/** Any topic deletion running now (a connection's group or General may not be re-pointed meanwhile). */
|
|
12646
|
+
topicDeletionPending() { return this.topicDeletionsInFlight.size > 0; }
|
|
12647
|
+
/** The physical destination of `topicId` on the bot `adapterId`. */
|
|
12648
|
+
destinationFor(adapterId, topicId) {
|
|
12649
|
+
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
12650
|
+
const platform = world?.adapter?.type ?? this.getChannelConfig(adapterId)?.type;
|
|
12651
|
+
const groupId = world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
12652
|
+
return topicDestination(platform, groupId, adapterId, topicId);
|
|
12653
|
+
}
|
|
12654
|
+
/** The adapter of the bot `adapterId` names — never a substitute for a bot that is not there. */
|
|
12655
|
+
instanceTopicAdapter(adapterId) {
|
|
12656
|
+
return (adapterId ? this.worlds.get(adapterId)?.adapter : this.worlds.size === 0 ? this.adapter ?? undefined : undefined) ?? undefined;
|
|
12657
|
+
}
|
|
12658
|
+
/**
|
|
12659
|
+
* Topic ownership by physical destination (#3055 review, Prism P1): an instance's topic is where its bot's platform
|
|
12660
|
+
* puts it (a Telegram group + thread, a Discord channel), so two bots in one group, or two instances on one
|
|
12661
|
+
* channel, are seen to share it; and every connection's own General is protected, with or without a General agent.
|
|
12662
|
+
*/
|
|
12663
|
+
topicOwnership() {
|
|
12664
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
12665
|
+
const destinationOf = (n) => {
|
|
12666
|
+
const cfg = this.fleetConfig?.instances?.[n];
|
|
12667
|
+
if (!cfg || cfg.topic_id == null || cfg.topic_id === "")
|
|
12668
|
+
return null;
|
|
12669
|
+
return this.destinationFor(this.getInstanceAdapterId(n), cfg.topic_id);
|
|
12670
|
+
};
|
|
12671
|
+
const generalDestinations = () => {
|
|
12672
|
+
const out = new Set();
|
|
12673
|
+
for (const ch of channels) {
|
|
12674
|
+
const id = ch.id ?? ch.type;
|
|
12675
|
+
const groupId = this.worlds.get(id)?.groupId ?? (ch.group_id != null ? String(ch.group_id) : undefined);
|
|
12676
|
+
if (ch.type === "telegram")
|
|
12677
|
+
out.add(topicDestination("telegram", groupId, id, 1));
|
|
12678
|
+
if (ch.type === "discord") {
|
|
12679
|
+
const gcid = discordGeneralChannelId(ch);
|
|
12680
|
+
if (gcid)
|
|
12681
|
+
out.add(topicDestination("discord", groupId, id, gcid));
|
|
12682
|
+
}
|
|
12683
|
+
}
|
|
12684
|
+
return out;
|
|
12685
|
+
};
|
|
12686
|
+
return { instances: this.fleetConfig?.instances, destinationOf, generalDestinations };
|
|
12687
|
+
}
|
|
12688
|
+
/** What deleting `name`'s topic would act on: its own bot only — a bot that is not there is never substituted. */
|
|
12689
|
+
planInstanceTopicDelete(name) {
|
|
12690
|
+
const adapterId = this.getInstanceAdapterId(name);
|
|
12691
|
+
return planInstanceTopicDelete({ name, adapterId, adapter: this.instanceTopicAdapter(adapterId), ...this.topicOwnership() });
|
|
13665
12692
|
}
|
|
13666
12693
|
getForumTopicDeleter(adapterId) {
|
|
13667
12694
|
const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
|
|
@@ -14062,9 +13089,9 @@ export class FleetManager {
|
|
|
14062
13089
|
Object.assign(current, resolved);
|
|
14063
13090
|
}
|
|
14064
13091
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
13092
|
+
this.agentCredentialsCache?.observePolicies();
|
|
14065
13093
|
this.refreshSavedDashboardPermissions();
|
|
14066
13094
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
14067
|
-
this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
|
|
14068
13095
|
}
|
|
14069
13096
|
/** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
|
|
14070
13097
|
refreshSavedDashboardPermissions() {
|
|
@@ -14298,43 +13325,10 @@ export class FleetManager {
|
|
|
14298
13325
|
}
|
|
14299
13326
|
getSettingsConfigPath() { return this.configPath; }
|
|
14300
13327
|
async removeInstance(name, authorization, execution) {
|
|
14301
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name])) {
|
|
14302
|
-
execution?.assert();
|
|
14303
|
-
assertExplicitInstanceRemoval(authorization);
|
|
14304
|
-
if (isNativePaneInstance(this.fleetConfig?.instances[name])) {
|
|
14305
|
-
await this.stopInstance(name);
|
|
14306
|
-
execution?.assert();
|
|
14307
|
-
}
|
|
14308
|
-
this.nativeInstanceRuntime().remove(name, () => !this.shuttingDown && (!execution || execution.current()), execution);
|
|
14309
|
-
return;
|
|
14310
|
-
}
|
|
14311
13328
|
if (execution)
|
|
14312
13329
|
return this.lifecycle.runTransition(name, transition => this.removeInstanceOwned(name, authorization, execution, transition));
|
|
14313
13330
|
return this.removeInstanceOwned(name, authorization);
|
|
14314
13331
|
}
|
|
14315
|
-
/** Shared instance metadata goes with a native identity; its Windows workspace and provider sessions stay intact. */
|
|
14316
|
-
nativeInstanceRemoved(name) {
|
|
14317
|
-
const clean = (what, run) => { try {
|
|
14318
|
-
run();
|
|
14319
|
-
}
|
|
14320
|
-
catch (error) {
|
|
14321
|
-
this.logger.warn({ name, err: error }, `Native instance ${what} cleanup failed`);
|
|
14322
|
-
} };
|
|
14323
|
-
clean("delivery", () => { this.cancelPendingDeliveries(name); });
|
|
14324
|
-
forgetInstanceContext(name);
|
|
14325
|
-
clean("schedules", () => { for (const schedule of this.scheduler?.list(name) ?? [])
|
|
14326
|
-
this.scheduler.delete(schedule.id); });
|
|
14327
|
-
this.lastActivity.delete(name);
|
|
14328
|
-
this.lastInboundUser.delete(name);
|
|
14329
|
-
this.rateLimitWarnedAt.delete(name);
|
|
14330
|
-
this.lastInboundMsg.delete(name);
|
|
14331
|
-
clean("history", () => this.webChatHistory.forget(name));
|
|
14332
|
-
clean("search", () => this.searchIndex?.removeInstance(name));
|
|
14333
|
-
clean("files", () => this.webFiles.forget(name));
|
|
14334
|
-
clean("compact", () => this.compactWatchService?.forget(name));
|
|
14335
|
-
clean("preferences", () => this.webPrefs.forgetInstance(name));
|
|
14336
|
-
clean("appearance", () => { this.themeStore.forget(name); this.webAppearance.forget(name); this.appearanceTools.forget(name); this.mods.forgetInstance(name); this.officeActsInstance?.forget(name); this.officePackToolsInstance?.forget(name); });
|
|
14337
|
-
}
|
|
14338
13332
|
async removeInstanceOwned(name, authorization, execution, transition) {
|
|
14339
13333
|
execution?.assert();
|
|
14340
13334
|
assertExplicitInstanceRemoval(authorization);
|
|
@@ -14460,8 +13454,6 @@ export class FleetManager {
|
|
|
14460
13454
|
execution?.complete();
|
|
14461
13455
|
}
|
|
14462
13456
|
startStatuslineWatcher(name) {
|
|
14463
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
14464
|
-
return;
|
|
14465
13457
|
if (this.lifecycle.isPaused(name))
|
|
14466
13458
|
return;
|
|
14467
13459
|
this.statuslineWatcher.watch(name);
|
|
@@ -15910,10 +14902,6 @@ export class FleetManager {
|
|
|
15910
14902
|
* would have to make this async and change every caller.
|
|
15911
14903
|
*/
|
|
15912
14904
|
notifyInstanceTopic(instanceName, text, extraOpts) {
|
|
15913
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
15914
|
-
this.emitSseEvent("message", { instance: instanceName, role: "status", sender: "AgEnD", text, ts: new Date().toISOString() });
|
|
15915
|
-
return true;
|
|
15916
|
-
}
|
|
15917
14905
|
// #2910 D2: a web-only fleet has no topics: what the agent's topic would have been told is a Notice about it.
|
|
15918
14906
|
if (this.webOnlyPromptPlace())
|
|
15919
14907
|
return this.recordWebNotice("instance", text, instanceName);
|
|
@@ -16394,8 +15382,6 @@ export class FleetManager {
|
|
|
16394
15382
|
* (also for a backend that has no profiles, where the option is ignored).
|
|
16395
15383
|
*/
|
|
16396
15384
|
credentialProfileOf(name, backend) {
|
|
16397
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
16398
|
-
return null;
|
|
16399
15385
|
if (!credentialHomeSpec(backend))
|
|
16400
15386
|
return null;
|
|
16401
15387
|
const cfg = this.fleetConfig?.instances?.[name];
|
|
@@ -16418,9 +15404,10 @@ export class FleetManager {
|
|
|
16418
15404
|
}
|
|
16419
15405
|
/** The instances the cache analysis reads: fleet.yaml's, then the ClassicBot rooms (their workspace under the AgEnD home, their own backend). */
|
|
16420
15406
|
cacheInstances() {
|
|
16421
|
-
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).
|
|
15407
|
+
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
16422
15408
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
16423
15409
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
15410
|
+
...(this.backendNameOf(name) === "codex" ? { profileTimelinePath: join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE) } : {}),
|
|
16424
15411
|
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
16425
15412
|
}));
|
|
16426
15413
|
const taken = new Set(fleet.map(i => i.name));
|
|
@@ -16432,6 +15419,9 @@ export class FleetManager {
|
|
|
16432
15419
|
claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
|
|
16433
15420
|
profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
16434
15421
|
kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
15422
|
+
// #3066 §1d (Fable r1 #2): a ClassicBot Codex room records its launches too (onDaemonReady) — read them the same way.
|
|
15423
|
+
...((this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend) === "codex"
|
|
15424
|
+
? { profileTimelinePath: join(this.getInstanceDir(ch.instanceName), PROFILE_TIMELINE_FILE) } : {}),
|
|
16435
15425
|
}));
|
|
16436
15426
|
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
16437
15427
|
}
|
|
@@ -16592,25 +15582,12 @@ export class FleetManager {
|
|
|
16592
15582
|
out.push({
|
|
16593
15583
|
name, state: p.state ?? undefined,
|
|
16594
15584
|
interaction: p.interaction ? { kind: p.interaction.kind, owner: wait?.owner ?? null, episode: p.interaction.episode, since: p.interaction.since } : null,
|
|
16595
|
-
interactionSummary: p.interaction_summary ?? null,
|
|
15585
|
+
interactionSummary: p.interaction?.kind === "kiro_choice" ? t("kiro3.running") : p.interaction_summary ?? null,
|
|
15586
|
+
kiroCompatibility: this.kiroCompatibilityHolds?.get(name),
|
|
16596
15587
|
pauseReason, pausedAt,
|
|
16597
15588
|
crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
|
|
16598
15589
|
signIn: pauseReason === "auth" ? this.signInFor(name) : null,
|
|
16599
15590
|
usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
|
|
16600
|
-
nativeDeliveryHold: this.daemons.get(name)?.nativeDeliveryHold?.() ?? null,
|
|
16601
|
-
generalLocation: this.generalLocationAttention(name),
|
|
16602
|
-
paneRecovery: (() => {
|
|
16603
|
-
const config = this.fleetConfig?.instances[name];
|
|
16604
|
-
if (!isNativePaneInstance(config) || !config.native_agent_id)
|
|
16605
|
-
return null;
|
|
16606
|
-
try {
|
|
16607
|
-
const profile = windowsAgentsFor(this, this.runtimeWindowsExecutor()).get(config.native_agent_id);
|
|
16608
|
-
return profile.paneRecovery ? { key: `${profile.paneRecovery.kind}:${profile.paneRecovery.launchId ?? "-"}`, detail: new NativePaneRecoveryError(profile.paneRecovery).message, since: profile.paneRecoveryAt ?? profile.createdAt } : null;
|
|
16609
|
-
}
|
|
16610
|
-
catch {
|
|
16611
|
-
return null;
|
|
16612
|
-
}
|
|
16613
|
-
})(),
|
|
16614
15591
|
});
|
|
16615
15592
|
}
|
|
16616
15593
|
return out;
|
|
@@ -16666,8 +15643,6 @@ export class FleetManager {
|
|
|
16666
15643
|
const target = this.fleetNoticeTarget(world);
|
|
16667
15644
|
return target ? { chatId: target.chatId, ...(target.opts.threadId !== undefined ? { threadId: String(target.opts.threadId) } : {}) } : null;
|
|
16668
15645
|
},
|
|
16669
|
-
// The name the web page shows for the instance (its View alias, else its own name), never the raw id alone.
|
|
16670
|
-
instanceName: instance => resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[instance], profile: profileIdentities(this.dataDir).get(instance) }).display_name || instance,
|
|
16671
15646
|
instanceTopic: instance => {
|
|
16672
15647
|
const topic = this.fleetConfig?.instances[instance]?.topic_id;
|
|
16673
15648
|
if (topic != null)
|
|
@@ -16684,6 +15659,10 @@ export class FleetManager {
|
|
|
16684
15659
|
throw new Error("the delivery outbox is not open");
|
|
16685
15660
|
return this.deliveryOutbox.acknowledge(deliveryId, by);
|
|
16686
15661
|
},
|
|
15662
|
+
missedOffline: () => (this.missedOfflineStore?.list(Date.now()) ?? []).map(m => ({
|
|
15663
|
+
id: m.id, instance: m.instance, owner: m.adapterId, username: m.username, place: m.place, preview: m.preview, since: m.since,
|
|
15664
|
+
})),
|
|
15665
|
+
acknowledgeMissed: (id, by) => this.missedOfflineStore?.acknowledge(id, by) ?? false,
|
|
16687
15666
|
tasks: () => this.needsYouTaskItems(),
|
|
16688
15667
|
backendSignIns: () => this.needsYouBackendSignIns(),
|
|
16689
15668
|
configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
|
|
@@ -17212,7 +16191,7 @@ export class FleetManager {
|
|
|
17212
16191
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
17213
16192
|
authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
|
|
17214
16193
|
policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now: Date.now(),
|
|
17215
|
-
hostOperator: this.isWebOnlyFleet(),
|
|
16194
|
+
hostOperator: this.isWebOnlyFleet(),
|
|
17216
16195
|
});
|
|
17217
16196
|
}
|
|
17218
16197
|
sessionAuthorizations = null;
|
|
@@ -17272,269 +16251,6 @@ export class FleetManager {
|
|
|
17272
16251
|
const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
|
|
17273
16252
|
this.logger.info({ handle: session.handle, durable }, "Host web session authorized (this computer's admin)");
|
|
17274
16253
|
}
|
|
17275
|
-
// ── #3008: connections from outside (a web-only fleet; decision 28c2d3a3 rule 3, the user's simplified form) ──
|
|
17276
|
-
webRemoteAccessState = null;
|
|
17277
|
-
/** The authenticator, the outside switch and the wrong-code limiter (web-remote-access.json), read on first use. */
|
|
17278
|
-
webRemoteAccess() {
|
|
17279
|
-
if (!this.dataDir)
|
|
17280
|
-
return null;
|
|
17281
|
-
this.webRemoteAccessState ??= new WebRemoteAccess(join(this.dataDir, WEB_REMOTE_ACCESS_FILE), this.logger);
|
|
17282
|
-
return this.webRemoteAccessState;
|
|
17283
|
-
}
|
|
17284
|
-
/** "Allow connections from outside" is on (an app set up) and this is a web-only fleet. */
|
|
17285
|
-
outsideOn() {
|
|
17286
|
-
return this.isWebOnlyFleet() && (this.webRemoteAccess()?.outside ?? false);
|
|
17287
|
-
}
|
|
17288
|
-
/** The link connections from outside opened, while it is open and still theirs: its exposure. Else null. */
|
|
17289
|
-
outsideLink() {
|
|
17290
|
-
if (!this.outsideOn())
|
|
17291
|
-
return null;
|
|
17292
|
-
const open = this.publicWebLink?.openSignin();
|
|
17293
|
-
return open && isOutsideLinkOwner(open.owner) ? { exposureId: open.exposureId } : null;
|
|
17294
|
-
}
|
|
17295
|
-
/**
|
|
17296
|
-
* The request's live session, asked again each time `current()` is called (its own cookie authenticated anew, the same
|
|
17297
|
-
* handle): this web-only fleet's owner — this computer's admin, on this computer (`hostOnly`), or also signed in from
|
|
17298
|
-
* outside — and web-verified now. Null when it is not (a guest, a LAN session, a chat fleet's admin, nobody).
|
|
17299
|
-
*/
|
|
17300
|
-
liveOwner(req, opts) {
|
|
17301
|
-
if (!this.isWebOnlyFleet())
|
|
17302
|
-
return null;
|
|
17303
|
-
if (opts.hostOnly && gatewayRequestContext(req))
|
|
17304
|
-
return null;
|
|
17305
|
-
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
17306
|
-
if (auth.kind !== "ok")
|
|
17307
|
-
return null;
|
|
17308
|
-
const handle = auth.session.handle, peer = webPeerOf(req), gateway = !!gatewayRequestContext(req);
|
|
17309
|
-
const owner = () => {
|
|
17310
|
-
if (!this.isWebOnlyFleet())
|
|
17311
|
-
return false;
|
|
17312
|
-
const live = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
17313
|
-
if (live.kind !== "ok" || live.session.handle !== handle)
|
|
17314
|
-
return false;
|
|
17315
|
-
if (opts.hostOnly && (live.session.host !== true || peer !== "loopback"))
|
|
17316
|
-
return false;
|
|
17317
|
-
const now = this.webAuthority({ session: live.session, gateway, peer });
|
|
17318
|
-
const a = now.authority.kind === "web-verified" && now.authority.session?.handle === handle ? now.authority.actor : null;
|
|
17319
|
-
return !!a && a.adapter_id === HOST_OPERATOR.adapterId && a.user_id === HOST_OPERATOR.userId;
|
|
17320
|
-
};
|
|
17321
|
-
return owner() ? { label: t("settings_confirm.host_operator_label"), handle, current: owner } : null;
|
|
17322
|
-
}
|
|
17323
|
-
/** Every session signed in from outside ends (and the authorizations kept for them). */
|
|
17324
|
-
endOutsideSessions() {
|
|
17325
|
-
const ended = new Set(this.webSessions?.list(undefined, r => r.remote === "totp").map(r => r.handle) ?? []);
|
|
17326
|
-
if (!ended.size)
|
|
17327
|
-
return;
|
|
17328
|
-
this.sessionAuthorizationStore()?.prune(a => !ended.has(a.handle));
|
|
17329
|
-
const result = this.webSessions?.revokeAll(r => r.remote === "totp");
|
|
17330
|
-
if (result && !result.durable)
|
|
17331
|
-
this.logger.warn("Outside sessions ended, but that could not be saved");
|
|
17332
|
-
this.logger.info({ count: ended.size }, "Sessions signed in from outside ended");
|
|
17333
|
-
}
|
|
17334
|
-
/**
|
|
17335
|
-
* #1939 §10 / #3008: a stored receipt's authority as it reads now — the web authority state, and for this computer's
|
|
17336
|
-
* admin from outside the switch and the last revocation of remote authority (Prism P2 2).
|
|
17337
|
-
*/
|
|
17338
|
-
receiptAuthorityNow(stored, state = this.webAuthorityState()) {
|
|
17339
|
-
return authorityNow(stored, { ...state, outside: this.outsideOn(), outsideHandles: this.webRemoteAccess()?.admitted ?? new Set() });
|
|
17340
|
-
}
|
|
17341
|
-
/** #3008 (the sign-in page): this exposure is the outside link, open now. */
|
|
17342
|
-
isOutsideLink(exposureId) {
|
|
17343
|
-
return this.outsideLink()?.exposureId === exposureId;
|
|
17344
|
-
}
|
|
17345
|
-
/** What Settings shows: set up or not, the switch, a wait in force, the link — its address only to this computer. */
|
|
17346
|
-
webOutsideStatus(req) {
|
|
17347
|
-
const store = this.webRemoteAccess(), status = store?.status();
|
|
17348
|
-
const here = !!this.liveOwner(req, { hostOnly: true });
|
|
17349
|
-
const open = this.outsideOn() ? this.publicWebLink?.openSignin() : null;
|
|
17350
|
-
const mine = !!open && isOutsideLinkOwner(open.owner);
|
|
17351
|
-
const linkStatus = this.outsideOn() ? this.getPublicWebStatus() : null;
|
|
17352
|
-
return { webOnly: this.isWebOnlyFleet(), here, enrolled: !!status?.enrolled, enrolledAt: status?.enrolledAt ?? null,
|
|
17353
|
-
outside: this.outsideOn(), pausedForMs: status?.pausedForMs ?? 0,
|
|
17354
|
-
link: linkStatus && linkStatus.state !== "closed" ? { ...linkStatus, ...(here && mine ? { url: open.url } : {}), quick: !publicLinkNamedHostname(this.fleetConfig?.web) } : null };
|
|
17355
|
-
}
|
|
17356
|
-
/** Set up (or set up again): a new secret, shown once to this computer's admin. Null: not them. */
|
|
17357
|
-
webOutsideBeginEnrollment(req) {
|
|
17358
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17359
|
-
if (!owner || !store)
|
|
17360
|
-
return null;
|
|
17361
|
-
return store.beginEnrollment(owner.handle, hostname() || "fleet");
|
|
17362
|
-
}
|
|
17363
|
-
/** The new app's first code: on "ok" it replaces the old one, whose sessions from outside end. */
|
|
17364
|
-
webOutsideConfirmEnrollment(req, code) {
|
|
17365
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17366
|
-
if (!owner || !store || !owner.current())
|
|
17367
|
-
return "not_here";
|
|
17368
|
-
const had = store.enrolled;
|
|
17369
|
-
const r = store.confirmEnrollment(owner.handle, code);
|
|
17370
|
-
if (r === "ok") {
|
|
17371
|
-
this.logger.info({ again: had }, "Authenticator app set up for connections from outside");
|
|
17372
|
-
if (had)
|
|
17373
|
-
this.endOutsideSessions();
|
|
17374
|
-
}
|
|
17375
|
-
return r;
|
|
17376
|
-
}
|
|
17377
|
-
/** Forget the app: the switch goes off, the link closes, every session from outside ends. */
|
|
17378
|
-
webOutsideRemove(req) {
|
|
17379
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17380
|
-
if (!owner || !store || !owner.current())
|
|
17381
|
-
return "not_here";
|
|
17382
|
-
// #3008 review (Prism P2 1): shut at once whatever the disk says; say so when it could not be saved (retried).
|
|
17383
|
-
const saved = store.remove();
|
|
17384
|
-
this.closeOutsideLink("outside removed");
|
|
17385
|
-
this.logger.info({ saved }, "Authenticator app removed; connections from outside off");
|
|
17386
|
-
return saved ? "ok" : "unsaved";
|
|
17387
|
-
}
|
|
17388
|
-
/** The switch. On: needs an app set up; the link opens. Off: it closes and every session from outside ends. */
|
|
17389
|
-
webOutsideSwitch(req, on) {
|
|
17390
|
-
const owner = this.liveOwner(req, { hostOnly: true }), store = this.webRemoteAccess();
|
|
17391
|
-
if (!owner || !store || !owner.current())
|
|
17392
|
-
return "not_here";
|
|
17393
|
-
if (on && !publicLinkSettings(this.fleetConfig?.web).allowed)
|
|
17394
|
-
return "not_allowed";
|
|
17395
|
-
const r = store.setOutside(on);
|
|
17396
|
-
if (r === "not_enrolled")
|
|
17397
|
-
return r;
|
|
17398
|
-
this.logger.info({ on, saved: r === "ok" }, "Connections from outside switched");
|
|
17399
|
-
// #3008 review (Prism P2 1): Off shuts at once even when it could not be saved (the store retries); On opens only
|
|
17400
|
-
// once saved — a switch the disk does not hold would not survive a restart.
|
|
17401
|
-
if (!on)
|
|
17402
|
-
this.closeOutsideLink("outside off");
|
|
17403
|
-
else if (r === "ok")
|
|
17404
|
-
this.openOutsideLink([0, ...OUTSIDE_LINK_RETRY_MS]);
|
|
17405
|
-
return r;
|
|
17406
|
-
}
|
|
17407
|
-
outsideAttempt = null;
|
|
17408
|
-
outsideOpening = false;
|
|
17409
|
-
/**
|
|
17410
|
-
* Open the outside link: an attempt after each delay in turn until one opens (#3008 B: twice more at most — the quick
|
|
17411
|
-
* link's address service limits how often it may be asked). Every attempt asks again: the switch still on, the fleet
|
|
17412
|
-
* running. A final failure: one Notice; the switch stays on, so the next start tries again.
|
|
17413
|
-
*/
|
|
17414
|
-
openOutsideLink(delays) {
|
|
17415
|
-
this.outsideAttempt?.cancel();
|
|
17416
|
-
this.outsideAttempt = null;
|
|
17417
|
-
const attempt = (n) => {
|
|
17418
|
-
this.outsideAttempt = null;
|
|
17419
|
-
if (this.shuttingDown || !this.outsideOn() || this.outsideOpening)
|
|
17420
|
-
return;
|
|
17421
|
-
this.outsideOpening = true;
|
|
17422
|
-
// alpha CI 4abe58cd: an On right after an Off finds the old link still closing (its tunnel stopping): wait for that
|
|
17423
|
-
// close, then open — never "busy" and a 30 s retry. The switch is asked again after the wait by openUnattended
|
|
17424
|
-
// itself (permitted → publicOwnerCurrent: the fleet running, the switch on): an Off meanwhile opens nothing.
|
|
17425
|
-
const link = this.getPublicWebLink();
|
|
17426
|
-
void link.whenClosed().then(() => link.openUnattended(OUTSIDE_LINK_OWNER))
|
|
17427
|
-
.catch(() => "failed").then(outcome => {
|
|
17428
|
-
this.outsideOpening = false;
|
|
17429
|
-
if (outcome === "opened") {
|
|
17430
|
-
this.logger.info({ attempt: n + 1 }, "Outside link opened");
|
|
17431
|
-
return;
|
|
17432
|
-
}
|
|
17433
|
-
if (outcome === "busy" && this.outsideLink())
|
|
17434
|
-
return; // already open (ours)
|
|
17435
|
-
if (this.shuttingDown || !this.outsideOn())
|
|
17436
|
-
return;
|
|
17437
|
-
if (n + 1 < delays.length) {
|
|
17438
|
-
this.outsideAttempt = outsideTimer(() => attempt(n + 1), delays[n + 1]);
|
|
17439
|
-
return;
|
|
17440
|
-
}
|
|
17441
|
-
this.logger.warn({ attempts: n + 1 }, "Outside link could not be opened; giving up until the next start or switch");
|
|
17442
|
-
this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_failed")));
|
|
17443
|
-
});
|
|
17444
|
-
};
|
|
17445
|
-
this.outsideAttempt = outsideTimer(() => attempt(0), delays[0] ?? 0);
|
|
17446
|
-
}
|
|
17447
|
-
closeOutsideLink(reason) {
|
|
17448
|
-
this.outsideAttempt?.cancel();
|
|
17449
|
-
this.outsideAttempt = null;
|
|
17450
|
-
const open = this.publicWebLink?.openSignin();
|
|
17451
|
-
if (this.publicWebLink && (!open || isOutsideLinkOwner(open.owner)))
|
|
17452
|
-
void this.publicWebLink.close(reason);
|
|
17453
|
-
this.endOutsideSessions();
|
|
17454
|
-
}
|
|
17455
|
-
/** PublicWebLink.onClose for the outside link: what closed it decides what happens next (#3008 B). */
|
|
17456
|
-
outsideLinkClosed(reason) {
|
|
17457
|
-
if (this.shuttingDown || OUTSIDE_OWN_CLOSES.has(reason) || !this.outsideOn())
|
|
17458
|
-
return;
|
|
17459
|
-
// #3008 review (Prism P2 3): expiry first — its lifetime counts from the opening request, so it can run out while the
|
|
17460
|
-
// opening is still under way: the switch goes off then too, and that attempt's end (outsideOn false) retries nothing.
|
|
17461
|
-
if (reason === "expired") {
|
|
17462
|
-
// Its lifetime ran out: the switch goes off (only this computer turns it on again — a session from outside can't).
|
|
17463
|
-
this.outsideAttempt?.cancel();
|
|
17464
|
-
this.outsideAttempt = null;
|
|
17465
|
-
if (this.webRemoteAccess()?.setOutside(false) === "unsaved")
|
|
17466
|
-
this.logger.warn("Outside link expired; the switch's Off could not be saved yet (retried)");
|
|
17467
|
-
this.endOutsideSessions();
|
|
17468
|
-
this.logger.info("Outside link expired; connections from outside off");
|
|
17469
|
-
this.recordWebNotice("fleet", t("outside.notice_ended", t("outside.reason_expired")));
|
|
17470
|
-
return;
|
|
17471
|
-
}
|
|
17472
|
-
if (this.outsideOpening)
|
|
17473
|
-
return; // that attempt's own failure: its retry
|
|
17474
|
-
// Dropped (the tunnel exited, the gateway failed, the configuration changed): reopened up to twice.
|
|
17475
|
-
this.logger.info({ reason }, "Outside link closed; reopening");
|
|
17476
|
-
this.openOutsideLink(OUTSIDE_LINK_RETRY_MS);
|
|
17477
|
-
}
|
|
17478
|
-
/**
|
|
17479
|
-
* #3008 (auth-api POST /auth/totp): a sign-in through the outside link with the authenticator's code. "ok": the caller
|
|
17480
|
-
* makes the session (SessionRecord.remote) and then calls authorizeOutsideSession. Each wrong code counts; a wait that
|
|
17481
|
-
* starts is a Notice; a code that matches a step just outside the window logs how far this clock seems to be off.
|
|
17482
|
-
*/
|
|
17483
|
-
outsideSignIn(exposureId, code) {
|
|
17484
|
-
const link = this.outsideLink(), store = this.webRemoteAccess();
|
|
17485
|
-
if (!link || !store || !exposureId || link.exposureId !== exposureId)
|
|
17486
|
-
return { kind: "closed" };
|
|
17487
|
-
const v = store.verify(code);
|
|
17488
|
-
if (v.kind === "ok")
|
|
17489
|
-
return { kind: "ok" };
|
|
17490
|
-
if (v.kind === "paused")
|
|
17491
|
-
return v;
|
|
17492
|
-
if (v.kind === "not_enrolled")
|
|
17493
|
-
return { kind: "closed" };
|
|
17494
|
-
if (v.kind === "unavailable")
|
|
17495
|
-
return v;
|
|
17496
|
-
if (v.skewSteps !== null)
|
|
17497
|
-
this.logger.warn({ offsetSeconds: v.skewSteps * TOTP_STEP_SECONDS }, "A wrong authenticator code matched a nearby time step: this computer's clock may be off");
|
|
17498
|
-
if (v.paused) {
|
|
17499
|
-
this.logger.warn({ waitMs: v.retryAfterMs }, "Sign-in from outside paused after wrong codes");
|
|
17500
|
-
this.recordWebNotice("fleet", t("outside.notice_paused", outsideDuration(v.retryAfterMs), v.wrong));
|
|
17501
|
-
return { kind: "paused", retryAfterMs: v.retryAfterMs };
|
|
17502
|
-
}
|
|
17503
|
-
return { kind: "wrong" };
|
|
17504
|
-
}
|
|
17505
|
-
/**
|
|
17506
|
-
* #3008: a session just made by outsideSignIn's "ok" is this computer's admin for its authorization's life. False: it
|
|
17507
|
-
* could not be admitted (its handle not saved, #3015 review r3) — the caller ends it.
|
|
17508
|
-
*/
|
|
17509
|
-
authorizeOutsideSession(session, peer, label) {
|
|
17510
|
-
const store = this.sessionAuthorizationStore(), link = this.outsideLink();
|
|
17511
|
-
if (!store || session.remote !== "totp" || session.owner || !link || session.exposureId !== link.exposureId)
|
|
17512
|
-
return false;
|
|
17513
|
-
if (!this.webRemoteAccess()?.admit(session.handle)) {
|
|
17514
|
-
this.logger.warn({ handle: session.handle }, "Session from outside not admitted: it could not be saved");
|
|
17515
|
-
return false;
|
|
17516
|
-
}
|
|
17517
|
-
const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
|
|
17518
|
-
const check = evaluateWebAuthority({
|
|
17519
|
-
session, gateway: true, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
17520
|
-
isAdmin: () => false,
|
|
17521
|
-
authorization: { handle: session.handle, owner: HOST_OPERATOR, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
|
|
17522
|
-
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
17523
|
-
hostOperator: true, outside: link,
|
|
17524
|
-
});
|
|
17525
|
-
if (check.refusal) {
|
|
17526
|
-
this.logger.info({ handle: session.handle, refusal: check.refusal }, "Session from outside not authorized");
|
|
17527
|
-
return false;
|
|
17528
|
-
}
|
|
17529
|
-
{
|
|
17530
|
-
const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
|
|
17531
|
-
store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
|
|
17532
|
-
const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
|
|
17533
|
-
this.logger.info({ handle: session.handle, durable }, "Session from outside authorized (this computer's admin)");
|
|
17534
|
-
}
|
|
17535
|
-
this.recordWebNotice("fleet", t("outside.notice_signed_in", label));
|
|
17536
|
-
return true;
|
|
17537
|
-
}
|
|
17538
16254
|
authorityStateStore = null;
|
|
17539
16255
|
/**
|
|
17540
16256
|
* #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
|
|
@@ -17555,10 +16271,6 @@ export class FleetManager {
|
|
|
17555
16271
|
return true;
|
|
17556
16272
|
if (session.surface === "local")
|
|
17557
16273
|
return false;
|
|
17558
|
-
// #3008 review (Prism P2 4): signed in from outside with the authenticator's code — its opt-in is the outside
|
|
17559
|
-
// switch, not these options (the authority judges it so): it keeps its authorization.
|
|
17560
|
-
if (session.remote === "totp")
|
|
17561
|
-
return false;
|
|
17562
16274
|
const peer = a.peer ?? "named_link";
|
|
17563
16275
|
return (!!state.namedLinkClosed && peer === "named_link") || (!!state.quickLinkClosed && peer === "quick_link");
|
|
17564
16276
|
};
|
|
@@ -17665,8 +16377,9 @@ export class FleetManager {
|
|
|
17665
16377
|
return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
|
|
17666
16378
|
},
|
|
17667
16379
|
signOut: (handle) => {
|
|
17668
|
-
this.webSessions?.revokeByHandle(handle);
|
|
17669
|
-
this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
|
|
16380
|
+
const session = this.webSessions?.revokeByHandle(handle) ?? { found: false, durable: true };
|
|
16381
|
+
const authorization = this.sessionAuthorizationStore()?.prune(a => a.handle !== handle) ?? { dropped: 0, durable: true };
|
|
16382
|
+
return { durable: session.durable, authorizationDurable: authorization.durable };
|
|
17670
16383
|
},
|
|
17671
16384
|
event: (type, payload) => { try {
|
|
17672
16385
|
this.eventLog?.insert("web", type, payload);
|
|
@@ -17679,6 +16392,7 @@ export class FleetManager {
|
|
|
17679
16392
|
expired: (c) => t("web_auth.card_expired", c.label),
|
|
17680
16393
|
refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
|
|
17681
16394
|
revoked: (c) => t("web_auth.card_revoked", c.label),
|
|
16395
|
+
notSaved: (c) => t("web_auth.card_not_saved", c.label),
|
|
17682
16396
|
ended: (c) => t("web_auth.card_ended", c.label),
|
|
17683
16397
|
generalHint: () => t("web_auth.general_hint"),
|
|
17684
16398
|
confirmLabel: () => t("web_auth.confirm"),
|
|
@@ -17708,7 +16422,7 @@ export class FleetManager {
|
|
|
17708
16422
|
authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
|
|
17709
16423
|
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
17710
16424
|
// #2910 D1: a host operator's session is never asked about (no account to ask): only a sign-in authorizes it.
|
|
17711
|
-
hostOperator: this.isWebOnlyFleet(),
|
|
16425
|
+
hostOperator: this.isWebOnlyFleet(),
|
|
17712
16426
|
});
|
|
17713
16427
|
return r.refusal;
|
|
17714
16428
|
}
|
|
@@ -18574,8 +17288,6 @@ export class FleetManager {
|
|
|
18574
17288
|
publication.retirePending = true;
|
|
18575
17289
|
}
|
|
18576
17290
|
async sendCancelButton(instanceName, correlationId, preserveProgress = false) {
|
|
18577
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName]))
|
|
18578
|
-
return;
|
|
18579
17291
|
// Post first, retire after (see the tail of this method). Retiring up front
|
|
18580
17292
|
// meant that from the delete until the new message came back — a chat API
|
|
18581
17293
|
// round trip, and every reply goes through here — the instance had NO live
|
|
@@ -18601,7 +17313,14 @@ export class FleetManager {
|
|
|
18601
17313
|
return;
|
|
18602
17314
|
}
|
|
18603
17315
|
const publication = this.beginCancelButtonPublication(instanceName, correlationId);
|
|
18604
|
-
|
|
17316
|
+
// M-2391: a re-post below a reply (preserveProgress) continues the same turn — its elapsed time continues from
|
|
17317
|
+
// the live bubble's start, as its tool list does. Restarting it at 0 meant an agent replying more often than the
|
|
17318
|
+
// threshold never showed elapsed time at all. A fresh turn (no preserve) starts at 0 as before.
|
|
17319
|
+
const carriedStart = preserveProgress ? this.liveButtonStart(instanceName) : undefined;
|
|
17320
|
+
const carriedMs = carriedStart !== undefined ? Math.max(0, Date.now() - carriedStart) : 0;
|
|
17321
|
+
const initialProgressText = carriedStart !== undefined && carriedMs >= this.progressMinElapsedMs()
|
|
17322
|
+
? FleetManager.progressText(carriedMs, null, this.progressMinElapsedMs(), this.progressPrefixFor(instanceName, carriedMs))
|
|
17323
|
+
: FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName, 0, 1));
|
|
18605
17324
|
try {
|
|
18606
17325
|
const sent = await adapter.notifyAlert(chatId, {
|
|
18607
17326
|
type: "cancel",
|
|
@@ -18624,7 +17343,7 @@ export class FleetManager {
|
|
|
18624
17343
|
// Elapsed time is measured from when this button was posted — i.e. from
|
|
18625
17344
|
// when the work was handed over — not from the pane's working transition,
|
|
18626
17345
|
// which resets if the CLI blips idle mid-turn.
|
|
18627
|
-
startedAt: Date.now(),
|
|
17346
|
+
startedAt: carriedStart ?? Date.now(),
|
|
18628
17347
|
// Matches the text notifyAlert just posted, so the first 60s tick does
|
|
18629
17348
|
// not re-edit identical text — which put a "(edited)" mark on Discord
|
|
18630
17349
|
// with nothing visibly changed.
|
|
@@ -18747,14 +17466,16 @@ export class FleetManager {
|
|
|
18747
17466
|
* Make a tool summary safe to paste into a channel message.
|
|
18748
17467
|
*
|
|
18749
17468
|
* The text is agent-controlled (it is built from tool inputs — file paths,
|
|
18750
|
-
* shell commands), so it gets flattened to one line, capped,
|
|
18751
|
-
* the two Discord mass-mention triggers.
|
|
18752
|
-
*
|
|
17469
|
+
* shell commands), so it gets flattened to one line, secrets masked, capped,
|
|
17470
|
+
* and stripped of the two Discord mass-mention triggers. Masking comes BEFORE
|
|
17471
|
+
* the cap: a cap can cut a mask's context but must never be what hides (or
|
|
17472
|
+
* fails to hide) a secret. Neither channel renders it with a parse mode, so no
|
|
17473
|
+
* markup escaping is needed beyond that.
|
|
18753
17474
|
*/
|
|
18754
17475
|
static sanitizeActivity(activity) {
|
|
18755
17476
|
if (!activity)
|
|
18756
17477
|
return null;
|
|
18757
|
-
const flat = activity.replace(/\s+/g, " ").replace(/@(everyone|here)/g, "@$1").trim();
|
|
17478
|
+
const flat = maskSecrets(activity.replace(/\s+/g, " ")).replace(/@(everyone|here)/g, "@$1").trim();
|
|
18758
17479
|
if (!flat)
|
|
18759
17480
|
return null;
|
|
18760
17481
|
return flat.length > PROGRESS_ACTIVITY_MAX_CHARS
|
|
@@ -18873,6 +17594,12 @@ export class FleetManager {
|
|
|
18873
17594
|
}
|
|
18874
17595
|
return PROGRESS_MIN_ELAPSED_MS;
|
|
18875
17596
|
}
|
|
17597
|
+
/** M-2391: when the instance's live (not retiring) bubble started — the current turn's start — or undefined. */
|
|
17598
|
+
liveButtonStart(instanceName) {
|
|
17599
|
+
const starts = [...this.cancelButtons.values()].filter((e) => e.instanceName === instanceName && !e.retiring && typeof e.startedAt === "number")
|
|
17600
|
+
.map((e) => e.startedAt);
|
|
17601
|
+
return starts.length ? Math.min(...starts) : undefined;
|
|
17602
|
+
}
|
|
18876
17603
|
startProgressTicker(entry) {
|
|
18877
17604
|
const tick = () => this.refreshBubble(entry);
|
|
18878
17605
|
entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
|
|
@@ -18882,7 +17609,9 @@ export class FleetManager {
|
|
|
18882
17609
|
// one additional edit per turn that lives past the threshold.
|
|
18883
17610
|
const firstAt = this.progressMinElapsedMs() - (Date.now() - (entry.startedAt ?? Date.now()));
|
|
18884
17611
|
if (firstAt > 0 && firstAt < PROGRESS_UPDATE_INTERVAL_MS) {
|
|
18885
|
-
|
|
17612
|
+
// M-2391: a little past the threshold — a timer can fire a millisecond before Date.now() reaches it, which
|
|
17613
|
+
// rendered the unchanged "處理中…" text and skipped the edit until the next 60 s interval.
|
|
17614
|
+
const firstTick = setTimeout(tick, firstAt + PROGRESS_THRESHOLD_MARGIN_MS);
|
|
18886
17615
|
firstTick.unref?.();
|
|
18887
17616
|
}
|
|
18888
17617
|
}
|
|
@@ -19108,14 +17837,6 @@ export class FleetManager {
|
|
|
19108
17837
|
}
|
|
19109
17838
|
/** Interrupt an instance's current generation (cancel button / /cancel). */
|
|
19110
17839
|
cancelInstance(instanceName) {
|
|
19111
|
-
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
19112
|
-
this.cancelPendingDeliveries(instanceName);
|
|
19113
|
-
const cancelled = this.nativeInstanceRuntime().cancel(instanceName);
|
|
19114
|
-
this.lastInboundMsg.delete(instanceName);
|
|
19115
|
-
for (const message of this.webChatHistory.cancelPending(instanceName))
|
|
19116
|
-
this.emitSseEvent("delivery", { instance: instanceName, messageId: message.messageId, delivery: message.delivery });
|
|
19117
|
-
return cancelled !== null;
|
|
19118
|
-
}
|
|
19119
17840
|
const daemon = this.daemons.get(instanceName);
|
|
19120
17841
|
if (!daemon)
|
|
19121
17842
|
return false;
|
|
@@ -19820,9 +18541,12 @@ export class FleetManager {
|
|
|
19820
18541
|
this.loginWindow.release(claim);
|
|
19821
18542
|
return t("login.web_shutting_down");
|
|
19822
18543
|
}
|
|
18544
|
+
// The wrapper says its own exit code after this marker: tmux may never record it (unattended-install.ts).
|
|
18545
|
+
const exitMarker = installExitMarker();
|
|
19823
18546
|
const flow = {
|
|
19824
18547
|
backend,
|
|
19825
|
-
command: unattendedInstallCommand(info.unattended ?? info.install, shell),
|
|
18548
|
+
command: unattendedInstallCommand(info.unattended ?? info.install, shell, exitMarker),
|
|
18549
|
+
exitMarker,
|
|
19826
18550
|
successPattern: /$^/,
|
|
19827
18551
|
timeoutMs: this.installTimeoutMs,
|
|
19828
18552
|
};
|
|
@@ -20167,9 +18891,12 @@ export class FleetManager {
|
|
|
20167
18891
|
this.taskBoardWaker?.stop();
|
|
20168
18892
|
// #2303: each instance's cards at once, read at every claim (a fleet.yaml edit applies to the next one).
|
|
20169
18893
|
db.board.setCardLimit((name) => boardCardLimit(this.fleetConfig, name));
|
|
18894
|
+
// An instance with the task board switched off is handed no cards and sent no notices (read at each write and wake).
|
|
18895
|
+
db.board.setBoardOff((name) => taskBoardOffIn(this.fleetConfig, name));
|
|
20170
18896
|
const waker = this.taskBoardWaker = new TaskBoardWaker({
|
|
20171
18897
|
deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
|
|
20172
18898
|
isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
|
|
18899
|
+
boardOff: (name) => taskBoardOffIn(this.fleetConfig, name),
|
|
20173
18900
|
// #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
|
|
20174
18901
|
messagedAbout: (from, to, codes, sinceMs) => {
|
|
20175
18902
|
const outbox = this.deliveryOutbox;
|
|
@@ -20333,7 +19060,36 @@ export class FleetManager {
|
|
|
20333
19060
|
"antigravity": ".agents/AGENTS.md",
|
|
20334
19061
|
"mock": "CLAUDE.md",
|
|
20335
19062
|
};
|
|
20336
|
-
static GENERAL_INSTRUCTIONS =
|
|
19063
|
+
static GENERAL_INSTRUCTIONS = `# Fleet Coordinator
|
|
19064
|
+
|
|
19065
|
+
You are the fleet coordinator — the central entry point for this AgEnD fleet.
|
|
19066
|
+
You route tasks, manage instances, enforce policies, and synthesize results.
|
|
19067
|
+
Do NOT modify project files directly — delegate file changes to the project's instance.
|
|
19068
|
+
You CAN write code snippets, explain code, and answer technical questions directly.
|
|
19069
|
+
|
|
19070
|
+
## Task Routing
|
|
19071
|
+
|
|
19072
|
+
- **Handle directly**: no file/exec access needed, answerable from knowledge, ≤2 reasoning steps (Q&A, translation, status queries, code snippets).
|
|
19073
|
+
- **Delegate to 1 instance**: scoped to one project/repo, needs file access or execution.
|
|
19074
|
+
- **Coordinate multiple**: spans repos, outputs feed each other, or parallel helps (max 3 per task).
|
|
19075
|
+
|
|
19076
|
+
Instance discovery: start with list_instances(); follow the guidance in its response. Prefer reuse; never duplicate a running instance.
|
|
19077
|
+
|
|
19078
|
+
## Reply Contract
|
|
19079
|
+
|
|
19080
|
+
Every final response to the user contains: the result (the actual answer or deliverable) and gaps (anything incomplete — omit if none). Summarize instance reports; omit internal coordination noise.
|
|
19081
|
+
|
|
19082
|
+
## After Restart
|
|
19083
|
+
|
|
19084
|
+
BEFORE processing any new messages: 1. list_instances() 2. list_teams() 3. list_decisions(). Only then handle requests.
|
|
19085
|
+
|
|
19086
|
+
## Playbooks (on-demand skills)
|
|
19087
|
+
|
|
19088
|
+
Detailed procedures live in your skills — consult them when the situation comes up rather than from memory:
|
|
19089
|
+
- **delegation-playbook** — delegation protocol, loop prevention, parallel vs sequential, result/failure handling, team management, instance configuration tips.
|
|
19090
|
+
- **development-workflow** — the fleet-wide code-change policy you enforce when delegating code tasks.
|
|
19091
|
+
Plus the operational skills (fleet-health, instance-lifecycle, scheduling, session-management, …).
|
|
19092
|
+
`;
|
|
20337
19093
|
/** Ensure the general instance has its project instructions file + knowledge */
|
|
20338
19094
|
ensureGeneralInstructions(workDir, backendName, instanceName) {
|
|
20339
19095
|
measureSyncWork("fleet.generalInstructions", () => this.ensureGeneralInstructionsSync(workDir, backendName, instanceName));
|
|
@@ -21355,8 +20111,6 @@ export class FleetManager {
|
|
|
21355
20111
|
: model,
|
|
21356
20112
|
});
|
|
21357
20113
|
const fleetInstance = this.fleetConfig?.instances[instanceName];
|
|
21358
|
-
if (isWindowsInstance(fleetInstance))
|
|
21359
|
-
return fleetInstance.model?.trim() ? done(fleetInstance.model.trim(), "instance") : done("default", "unresolved", "this CLI does not report a default");
|
|
21360
20114
|
if (fleetInstance) {
|
|
21361
20115
|
if (fleetInstance.model?.trim())
|
|
21362
20116
|
return done(fleetInstance.model.trim(), "instance");
|
|
@@ -21438,7 +20192,7 @@ export class FleetManager {
|
|
|
21438
20192
|
if (this.fleetConfig?.defaults?.backend)
|
|
21439
20193
|
backends.add(this.fleetConfig.defaults.backend);
|
|
21440
20194
|
for (const inst of Object.values(this.fleetConfig?.instances ?? {}))
|
|
21441
|
-
if (
|
|
20195
|
+
if (inst.backend)
|
|
21442
20196
|
backends.add(inst.backend);
|
|
21443
20197
|
for (const ch of this.classicChannels?.getAll() ?? [])
|
|
21444
20198
|
if (ch.backend)
|
|
@@ -21453,8 +20207,6 @@ export class FleetManager {
|
|
|
21453
20207
|
// here; a ClassicBot room on a profile is probed on its first /model (catalogProfile knows rooms too).
|
|
21454
20208
|
const profiles = new Set();
|
|
21455
20209
|
for (const name of Object.keys(this.fleetConfig?.instances ?? {})) {
|
|
21456
|
-
if (isWindowsInstance(this.fleetConfig?.instances[name]))
|
|
21457
|
-
continue;
|
|
21458
20210
|
if (this.backendNameForInstance(name) !== "claude-code")
|
|
21459
20211
|
continue;
|
|
21460
20212
|
const profile = this.catalogProfile("claude-code", name);
|
|
@@ -21475,8 +20227,6 @@ export class FleetManager {
|
|
|
21475
20227
|
* instead of presenting an old list as a fresh one.
|
|
21476
20228
|
*/
|
|
21477
20229
|
async getModelOptionsWithSource(instanceName, opts = {}) {
|
|
21478
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
21479
|
-
return { models: (await this.listModelCatalog({ instanceName })).models, source: "cache" };
|
|
21480
20230
|
const backendName = this.backendNameForInstance(instanceName);
|
|
21481
20231
|
const profile = this.catalogProfile(backendName, instanceName);
|
|
21482
20232
|
const cached = this.readCliEnv(backendName, profile);
|
|
@@ -21532,21 +20282,6 @@ export class FleetManager {
|
|
|
21532
20282
|
*/
|
|
21533
20283
|
async listModelCatalog(opts = {}) {
|
|
21534
20284
|
const { instanceName } = opts;
|
|
21535
|
-
if (instanceName && isWindowsInstance(this.fleetConfig?.instances[instanceName])) {
|
|
21536
|
-
const config = this.fleetConfig.instances[instanceName], backend = config.backend ?? "codex";
|
|
21537
|
-
// Detection is admin-owned. A read never launches a provider or falls back to this host's account.
|
|
21538
|
-
let inventory = null;
|
|
21539
|
-
try {
|
|
21540
|
-
inventory = await this.runtimeWindowsExecutor().probe({ force: false, machineId: config.machine_id });
|
|
21541
|
-
}
|
|
21542
|
-
catch { /* offline remains unavailable */ }
|
|
21543
|
-
const row = inventory?.clis.find(value => value.id === backend);
|
|
21544
|
-
return { backend, scope: "instance", instance: instanceName, current_model: config.model || null,
|
|
21545
|
-
models: (row?.models ?? []).map(value => ({ id: value.id, label: value.name ?? value.id, ...(value.description ? { description: value.description } : {}) })),
|
|
21546
|
-
source: row?.models?.length ? "cache" : "fallback",
|
|
21547
|
-
...(inventory?.probedAt ? { probed_at: new Date(inventory.probedAt).toISOString() } : {}),
|
|
21548
|
-
note: "Windows runtime's last detected inventory; no coordinator account or live probe is used." };
|
|
21549
|
-
}
|
|
21550
20285
|
if (instanceName) {
|
|
21551
20286
|
const backend = this.backendNameForInstance(instanceName);
|
|
21552
20287
|
const resolved = this.resolveInstanceModel(instanceName);
|
|
@@ -21582,8 +20317,6 @@ export class FleetManager {
|
|
|
21582
20317
|
}
|
|
21583
20318
|
/** Ask a backend for its catalog using ONE instance's real config. Never throws. */
|
|
21584
20319
|
async instanceScopedModels(instanceName, backend) {
|
|
21585
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
21586
|
-
return [];
|
|
21587
20320
|
try {
|
|
21588
20321
|
const inst = this.fleetConfig?.instances?.[instanceName];
|
|
21589
20322
|
const instanceDir = this.getInstanceDir(instanceName);
|
|
@@ -22221,8 +20954,6 @@ export class FleetManager {
|
|
|
22221
20954
|
}
|
|
22222
20955
|
/** The instance's backend's effort metadata; agy's levels are its model's (#1825), the one the instance runs. */
|
|
22223
20956
|
instanceEffortMetadata(instanceName) {
|
|
22224
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22225
|
-
return { strategy: "unsupported", levels: [] };
|
|
22226
20957
|
const backend = this.backendNameForInstance(instanceName);
|
|
22227
20958
|
let model = null;
|
|
22228
20959
|
if (backend === "antigravity") {
|
|
@@ -22233,8 +20964,6 @@ export class FleetManager {
|
|
|
22233
20964
|
}
|
|
22234
20965
|
/** Configured effort for an instance: per-instance, else fleet default, else none. */
|
|
22235
20966
|
resolveInstanceEffort(instanceName) {
|
|
22236
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22237
|
-
return { effort: null, source: "unset" };
|
|
22238
20967
|
const own = this.fleetConfig?.instances[instanceName]?.effort;
|
|
22239
20968
|
if (own)
|
|
22240
20969
|
return { effort: own, source: "instance" };
|
|
@@ -22272,11 +21001,9 @@ export class FleetManager {
|
|
|
22272
21001
|
* instance respawns.
|
|
22273
21002
|
*/
|
|
22274
21003
|
async applyEffort(instanceName, requested, opts) {
|
|
22275
|
-
// #1490 row 27
|
|
21004
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
|
|
22276
21005
|
if (opts?.current && !opts.current())
|
|
22277
21006
|
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
22278
|
-
if (isWindowsInstance(this.fleetConfig?.instances[instanceName]))
|
|
22279
|
-
return t("effort.unsupported", this.backendNameForInstance(instanceName));
|
|
22280
21007
|
const level = requested.trim().toLowerCase();
|
|
22281
21008
|
const backendName = this.backendNameForInstance(instanceName);
|
|
22282
21009
|
let strategy = "unsupported";
|
|
@@ -22324,20 +21051,6 @@ export class FleetManager {
|
|
|
22324
21051
|
if (/[\x00-\x1f\x7f]/.test(model)) {
|
|
22325
21052
|
return t("model.invalid_chars");
|
|
22326
21053
|
}
|
|
22327
|
-
const native = this.fleetConfig?.instances[instanceName];
|
|
22328
|
-
if (isWindowsInstance(native)) {
|
|
22329
|
-
if (!isNativePaneInstance(native))
|
|
22330
|
-
return WINDOWS_CONFIG_REFUSED;
|
|
22331
|
-
const catalog = await this.listModelCatalog({ instanceName });
|
|
22332
|
-
if (!catalog.models.some(row => row.id === model))
|
|
22333
|
-
return t("model.unknown_name", model, catalog.models.map(row => row.id).join(", "));
|
|
22334
|
-
const id = native.native_agent_id, machine = native.machine_id;
|
|
22335
|
-
await this.updateNativePaneConfig(instanceName, { model }, () => {
|
|
22336
|
-
const now = this.fleetConfig?.instances[instanceName];
|
|
22337
|
-
return isNativePaneInstance(now) && now.native_agent_id === id && now.machine_id === machine;
|
|
22338
|
-
});
|
|
22339
|
-
return t("model.restart_success", instanceName, model);
|
|
22340
|
-
}
|
|
22341
21054
|
const backendName = this.backendNameForInstance(instanceName);
|
|
22342
21055
|
let strategy = "restart";
|
|
22343
21056
|
try {
|
|
@@ -22526,8 +21239,6 @@ export class FleetManager {
|
|
|
22526
21239
|
* profile's, when it is named and has one), claude's account catalog included. Read only: never a probe.
|
|
22527
21240
|
*/
|
|
22528
21241
|
modelCatalogFor(backend, instance) {
|
|
22529
|
-
if (instance && isWindowsInstance(this.fleetConfig?.instances[instance]))
|
|
22530
|
-
return [];
|
|
22531
21242
|
if (!KNOWN_BACKENDS.includes(backend) || backend === "mock")
|
|
22532
21243
|
return [];
|
|
22533
21244
|
const own = instance && Object.prototype.hasOwnProperty.call(this.fleetConfig?.instances ?? {}, instance) ? instance
|
|
@@ -22541,40 +21252,6 @@ export class FleetManager {
|
|
|
22541
21252
|
}
|
|
22542
21253
|
return out;
|
|
22543
21254
|
}
|
|
22544
|
-
/** Host-default fallback only: Grok has no modeled profile home. Never construct a backend or start a probe. */
|
|
22545
|
-
async cliModelCatalogFor(backend, instance) {
|
|
22546
|
-
if (backend !== "grok")
|
|
22547
|
-
return [];
|
|
22548
|
-
const fleet = this.fleetConfig;
|
|
22549
|
-
const configured = instance && Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) ? fleet.instances[instance] : undefined;
|
|
22550
|
-
const classic = this.classicChannels;
|
|
22551
|
-
const room = instance && !configured ? classic?.getAll().find(channel => channel.instanceName === instance) : undefined;
|
|
22552
|
-
if (instance && !configured && !room)
|
|
22553
|
-
return [];
|
|
22554
|
-
const daemon = room ? this.daemons.get(instance) : undefined;
|
|
22555
|
-
const grokHome = process.env.GROK_HOME?.trim() || join(homedir(), ".grok");
|
|
22556
|
-
const stillHostDefault = () => {
|
|
22557
|
-
if (this.fleetConfig !== fleet || (process.env.GROK_HOME?.trim() || join(homedir(), ".grok")) !== grokHome)
|
|
22558
|
-
return false;
|
|
22559
|
-
if (configured && (isWindowsInstance(configured) || !Object.prototype.hasOwnProperty.call(fleet?.instances ?? {}, instance) || fleet.instances[instance] !== configured))
|
|
22560
|
-
return false;
|
|
22561
|
-
if (room && (this.classicChannels !== classic || classic?.getAll().find(channel => channel.instanceName === instance) !== room || this.daemons.get(instance) !== daemon))
|
|
22562
|
-
return false;
|
|
22563
|
-
// The ordinary profile helpers deliberately ignore Grok: it has no credential-home mapping yet.
|
|
22564
|
-
// Check raw declarations, including invalid ones, rather than mistaking them for the host login.
|
|
22565
|
-
const defaults = fleet?.defaults?.backend_options;
|
|
22566
|
-
const profiles = configured
|
|
22567
|
-
? [configured.backend_options?.grok?.credential_profile, defaults?.grok?.credential_profile]
|
|
22568
|
-
: room
|
|
22569
|
-
? [mergeBackendOptions(defaults, room.backendOptions)?.grok?.credential_profile, daemon?.getConfigSnapshot?.().backend_options?.grok?.credential_profile]
|
|
22570
|
-
: [defaults?.grok?.credential_profile];
|
|
22571
|
-
return profiles.every(profile => profile === undefined || profile === null || profile === "");
|
|
22572
|
-
};
|
|
22573
|
-
if (!stillHostDefault())
|
|
22574
|
-
return [];
|
|
22575
|
-
const models = await readGrokModelCache(grokHome);
|
|
22576
|
-
return stillHostDefault() ? models : [];
|
|
22577
|
-
}
|
|
22578
21255
|
/**
|
|
22579
21256
|
* #2072 (#2078 review): the snapshot as THIS instance would see it. Its sign-in on a backend is its own credential
|
|
22580
21257
|
* profile's when it has one there — the host's default login says nothing about it. A profile's login is a file check
|
|
@@ -23129,6 +21806,7 @@ export class FleetManager {
|
|
|
23129
21806
|
...this.fleetConfig?.defaults,
|
|
23130
21807
|
working_directory: workDir,
|
|
23131
21808
|
lightweight: true,
|
|
21809
|
+
quota_notice: classicIdentity?.quotaNotice,
|
|
23132
21810
|
tool_progress: toolProgress ?? "off",
|
|
23133
21811
|
reply_completion_guard: replyCompletionGuard ?? true,
|
|
23134
21812
|
...(backend ? { backend } : {}),
|
|
@@ -23233,16 +21911,6 @@ export class FleetManager {
|
|
|
23233
21911
|
* settles, so a later genuine stop (after a restart) still does the work.
|
|
23234
21912
|
*/
|
|
23235
21913
|
stopAll() {
|
|
23236
|
-
this.lifecycle.cancelNativeRecovery?.();
|
|
23237
|
-
this.nativeMcpStore?.closeAll();
|
|
23238
|
-
this.runtimeExecutor?.close();
|
|
23239
|
-
this.runtimeExecutor = null;
|
|
23240
|
-
this.fleetWindowsRuntime = null;
|
|
23241
|
-
this.runtimeHub?.close();
|
|
23242
|
-
this.runtimeHub = null;
|
|
23243
|
-
this.localPaneRelay?.stop();
|
|
23244
|
-
this.localPaneRelay = null;
|
|
23245
|
-
closeWindowsBridge(this);
|
|
23246
21914
|
this.stopAllInFlight ??= this.doStopAll().finally(() => {
|
|
23247
21915
|
// #1956: after the stop's own tmux work; anything later goes direct. A later startAll starts it again.
|
|
23248
21916
|
if (this.tmuxExecHelperTimer) {
|
|
@@ -23305,6 +21973,25 @@ export class FleetManager {
|
|
|
23305
21973
|
// because we asked it to. Set synchronously — doStopAll runs to its first
|
|
23306
21974
|
// await in the same tick as the signal handler, so no event can slip in.
|
|
23307
21975
|
this.shuttingDown = true;
|
|
21976
|
+
this.stopQuotaObserver?.();
|
|
21977
|
+
this.stopQuotaObserver = null;
|
|
21978
|
+
const quotaNoticesStopped = this.quotaNotices?.stop();
|
|
21979
|
+
// Stop receive admission before the first await or any instance shutdown; outbound
|
|
21980
|
+
// adapters remain available below to retire notices and pending prompts.
|
|
21981
|
+
for (const state of this.adapterState.values()) {
|
|
21982
|
+
if (state.retryTimer) {
|
|
21983
|
+
clearTimeout(state.retryTimer);
|
|
21984
|
+
state.retryTimer = undefined;
|
|
21985
|
+
}
|
|
21986
|
+
}
|
|
21987
|
+
const inboundStopped = Promise.all([...this.adapters.values()].map(async (adapter) => {
|
|
21988
|
+
try {
|
|
21989
|
+
await adapter.stopReceiving?.();
|
|
21990
|
+
}
|
|
21991
|
+
catch (err) {
|
|
21992
|
+
this.logger.warn({ adapterId: adapter.id, err }, "Adapter receive stop failed; inbound remains fenced");
|
|
21993
|
+
}
|
|
21994
|
+
}));
|
|
23308
21995
|
this.workingRecovery?.reset();
|
|
23309
21996
|
if (this.workingRecoveryTimer)
|
|
23310
21997
|
clearInterval(this.workingRecoveryTimer);
|
|
@@ -23321,7 +22008,6 @@ export class FleetManager {
|
|
|
23321
22008
|
const settingsControlStopped = settingsControl?.close();
|
|
23322
22009
|
this.publicLinkAutoOpen?.stop();
|
|
23323
22010
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
23324
|
-
this.webRemoteAccessState?.close(); // #3008: an unsaved switch gets its last write
|
|
23325
22011
|
const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
|
|
23326
22012
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
23327
22013
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
@@ -23365,23 +22051,19 @@ export class FleetManager {
|
|
|
23365
22051
|
// A login/install window is a dedicated tmux server with its own TTL
|
|
23366
22052
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
23367
22053
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
22054
|
+
await inboundStopped;
|
|
23368
22055
|
await publicStopped;
|
|
23369
22056
|
await sharesStopped?.catch(() => { });
|
|
23370
22057
|
await profileStopped;
|
|
23371
22058
|
await needsStopped;
|
|
23372
22059
|
await settingsControlStopped;
|
|
23373
22060
|
await cacheStopped;
|
|
22061
|
+
await quotaNoticesStopped;
|
|
22062
|
+
this.quotaNotices = null;
|
|
23374
22063
|
await webChatStopped;
|
|
23375
22064
|
await this.cpuProfileControl?.close();
|
|
23376
22065
|
this.cpuProfileControl = null;
|
|
23377
22066
|
await this.shutdownLoginWindows();
|
|
23378
|
-
// Cancel adapter retry timers
|
|
23379
|
-
for (const state of this.adapterState.values()) {
|
|
23380
|
-
if (state.retryTimer) {
|
|
23381
|
-
clearTimeout(state.retryTimer);
|
|
23382
|
-
state.retryTimer = undefined;
|
|
23383
|
-
}
|
|
23384
|
-
}
|
|
23385
22067
|
this.clearStatuslineWatchers();
|
|
23386
22068
|
this.costGuard?.stop();
|
|
23387
22069
|
this.dailySummary?.stop();
|
|
@@ -23419,6 +22101,15 @@ export class FleetManager {
|
|
|
23419
22101
|
clearInterval(this.discordPresenceTimer);
|
|
23420
22102
|
this.discordPresenceTimer = null;
|
|
23421
22103
|
}
|
|
22104
|
+
if (this.quotaReadTimer) {
|
|
22105
|
+
clearInterval(this.quotaReadTimer);
|
|
22106
|
+
this.quotaReadTimer = null;
|
|
22107
|
+
}
|
|
22108
|
+
if (this.capacityTimer) {
|
|
22109
|
+
clearInterval(this.capacityTimer);
|
|
22110
|
+
this.capacityTimer = null;
|
|
22111
|
+
}
|
|
22112
|
+
setCapacityLookup(null);
|
|
23422
22113
|
if (this.discordPresenceEagerTimer) {
|
|
23423
22114
|
clearTimeout(this.discordPresenceEagerTimer);
|
|
23424
22115
|
this.discordPresenceEagerTimer = null;
|
|
@@ -23704,7 +22395,6 @@ export class FleetManager {
|
|
|
23704
22395
|
this.savedFleetConfigSnapshot = previousSavedSnapshot;
|
|
23705
22396
|
this.loadedFleetSource = previousSource;
|
|
23706
22397
|
this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
|
|
23707
|
-
this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
|
|
23708
22398
|
this.logger.error({
|
|
23709
22399
|
oldCount,
|
|
23710
22400
|
newCount,
|
|
@@ -23724,6 +22414,7 @@ export class FleetManager {
|
|
|
23724
22414
|
// mark every row done and tell the user "changes applied".
|
|
23725
22415
|
return { rejected: why };
|
|
23726
22416
|
}
|
|
22417
|
+
this.agentCredentialsCache?.observePolicies();
|
|
23727
22418
|
if (this.loadedFleetSource)
|
|
23728
22419
|
this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
|
|
23729
22420
|
this.migrateOfficeStorage();
|
|
@@ -24624,6 +23315,9 @@ export class FleetManager {
|
|
|
24624
23315
|
}
|
|
24625
23316
|
async runConnectionBindingApply(job, binding, execution) {
|
|
24626
23317
|
try {
|
|
23318
|
+
// #3055 review r2: a connection is not re-pointed while one of its topics is being deleted (nothing is changed).
|
|
23319
|
+
if (this.topicDeletionPending())
|
|
23320
|
+
throw new Error("a topic deletion is running; try again in a moment");
|
|
24627
23321
|
await this.rebuildAdapterForBinding(job.connectionId, binding, execution);
|
|
24628
23322
|
job.result = "applied";
|
|
24629
23323
|
}
|
|
@@ -25340,7 +24034,7 @@ export class FleetManager {
|
|
|
25340
24034
|
const topicMode = daemonTopicMode(fleet);
|
|
25341
24035
|
const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
|
|
25342
24036
|
// Phase 1: generals first
|
|
25343
|
-
const restartEntries = Object.entries(fleet.instances)
|
|
24037
|
+
const restartEntries = Object.entries(fleet.instances);
|
|
25344
24038
|
const restartGenerals = restartEntries.filter(([_, cfg]) => cfg.general_topic);
|
|
25345
24039
|
const restartOthers = restartEntries.filter(([_, cfg]) => !cfg.general_topic);
|
|
25346
24040
|
for (const [name, cfg] of restartGenerals) {
|
|
@@ -25794,9 +24488,6 @@ export class FleetManager {
|
|
|
25794
24488
|
return;
|
|
25795
24489
|
}
|
|
25796
24490
|
const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
|
|
25797
|
-
if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
|
|
25798
|
-
&& this.runtimeCoordinator().handlePublicRequest(req, res, new URL(req.url ?? "/", `http://localhost:${port}`)))
|
|
25799
|
-
return;
|
|
25800
24491
|
// Branding is public, after Host admission: automatic favicon requests
|
|
25801
24492
|
// must not become noisy web-token 401s in the browser console.
|
|
25802
24493
|
// #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
|
|
@@ -25823,8 +24514,7 @@ export class FleetManager {
|
|
|
25823
24514
|
if (decision.reason === "no-credential" && req.method === "GET"
|
|
25824
24515
|
&& String(req.headers.accept ?? "").includes("text/html")
|
|
25825
24516
|
&& isWebPageNavigation(requestPath)) {
|
|
25826
|
-
|
|
25827
|
-
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId) });
|
|
24517
|
+
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet() });
|
|
25828
24518
|
return;
|
|
25829
24519
|
}
|
|
25830
24520
|
res.writeHead(decision.status);
|
|
@@ -26181,12 +24871,6 @@ export class FleetManager {
|
|
|
26181
24871
|
if (!this.webToken || !this.webSessions)
|
|
26182
24872
|
this.initializeWebAuthTokens();
|
|
26183
24873
|
const server = createServer((req, res) => this.dispatchWebHttp(req, res, port, drain.closing));
|
|
26184
|
-
server.on("upgrade", (req, socket, head) => {
|
|
26185
|
-
if (!this.shuttingDown && remoteRuntimeEnabled() && isHostAllowed(req.headers.host, allowedHostNames(this.fleetConfig))
|
|
26186
|
-
&& this.runtimeCoordinator().handleUpgrade(req, socket, head))
|
|
26187
|
-
return;
|
|
26188
|
-
socket.destroy();
|
|
26189
|
-
});
|
|
26190
24874
|
// #1937: Node's default (300 s to receive a request) would cut a large upload on a slow link. The listener allows
|
|
26191
24875
|
// more; the upload route keeps its own clocks (no byte for a minute, or 30 minutes in all), and every other request
|
|
26192
24876
|
// keeps the old 300 s (limitRequestReceive in dispatchWebHttp).
|
|
@@ -26411,36 +25095,9 @@ export class FleetManager {
|
|
|
26411
25095
|
this.logger.debug({ err }, "bots unreadable (getUiStatus)");
|
|
26412
25096
|
}
|
|
26413
25097
|
const instances = names.map(name => {
|
|
26414
|
-
const nativeConfig = this.fleetConfig?.instances[name];
|
|
26415
|
-
if (isNativeExecInstance(nativeConfig)) {
|
|
26416
|
-
let runtime = null;
|
|
26417
|
-
let agent = null;
|
|
26418
|
-
try {
|
|
26419
|
-
runtime = this.nativeInstanceRuntime();
|
|
26420
|
-
agent = runtime.get(name);
|
|
26421
|
-
}
|
|
26422
|
-
catch { /* A damaged native profile is a crashed native row, never a Linux spawn. */ }
|
|
26423
|
-
const display_name = nativeConfig.display_name || agent?.name || name;
|
|
26424
|
-
const backend = nativeConfig.backend || agent?.cli || "codex";
|
|
26425
|
-
return { name, display_name, description: nativeConfig.description || undefined, tags: nativeConfig.tags || [],
|
|
26426
|
-
teams: instanceTeams(this.fleetConfig?.teams, name), runtime: "windows", native_agent_id: nativeConfig.native_agent_id,
|
|
26427
|
-
...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
|
|
26428
|
-
machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}),
|
|
26429
|
-
native_blocked: !agent || agent.blocked === true,
|
|
26430
|
-
native_busy: runtime?.busy(name) ?? false,
|
|
26431
|
-
backend,
|
|
26432
|
-
model: nativeConfig.model || "default", model_source: nativeConfig.model ? "instance" : "cli-default",
|
|
26433
|
-
status: agent && runtime ? runtime.status(name) : "crashed", state: agent && runtime ? runtime.execution(name) : null, execution_state: agent && runtime ? runtime.execution(name) : null,
|
|
26434
|
-
interaction: null, interaction_summary: null, context_pct: null, cost: null,
|
|
26435
|
-
last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined,
|
|
26436
|
-
avatar: avatarFor({ name, displayName: display_name }),
|
|
26437
|
-
capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: nativeProviderMcpSupported(backend), channels: false, terminal: false, attachments: false, usage: false, send_while_busy: false },
|
|
26438
|
-
...(!agent ? { runtime_error: "Windows profile is missing or invalid; restore its saved conversation before using this instance" }
|
|
26439
|
-
: agent.blocked ? { runtime_error: "Windows process outcome is unknown; verify it stopped before recovery" } : {}) };
|
|
26440
|
-
}
|
|
26441
25098
|
// #1956: read once for this instance; its cost, context and model come from it.
|
|
26442
|
-
const statusline =
|
|
26443
|
-
const cost =
|
|
25099
|
+
const statusline = readStatuslineJson(this.dataDir, name);
|
|
25100
|
+
const cost = statusline?.cost?.total_cost_usd ?? 0;
|
|
26444
25101
|
// Align with /ctx: statusline for claude-code, pane scrape for kiro/grok/codex.
|
|
26445
25102
|
const classic = classicOnly.includes(name);
|
|
26446
25103
|
const backend = classic
|
|
@@ -26448,7 +25105,7 @@ export class FleetManager {
|
|
|
26448
25105
|
: (this.fleetConfig?.instances[name]?.backend
|
|
26449
25106
|
?? this.fleetConfig?.defaults?.backend
|
|
26450
25107
|
?? "claude-code");
|
|
26451
|
-
const { context } =
|
|
25108
|
+
const { context } = resolveInstanceContext(this.dataDir, name, backend, { source: this.getPaneContextSource(name), statusline });
|
|
26452
25109
|
// context_pct: null when unavailable, not 0
|
|
26453
25110
|
const context_pct = context ?? null;
|
|
26454
25111
|
// Model: Only Claude Code has live statusline; others use the effective resolver.
|
|
@@ -26479,22 +25136,6 @@ export class FleetManager {
|
|
|
26479
25136
|
}
|
|
26480
25137
|
return {
|
|
26481
25138
|
name,
|
|
26482
|
-
general_topic: nativeConfig?.general_topic === true,
|
|
26483
|
-
...(isNativePaneInstance(nativeConfig) ? { runtime: "windows", native_adapter: "pane", native_agent_id: nativeConfig.native_agent_id,
|
|
26484
|
-
...(() => {
|
|
26485
|
-
try {
|
|
26486
|
-
const profile = this.nativeInstanceRuntime().get(name), bound = this.daemons.get(name);
|
|
26487
|
-
return { native_recovery: profile?.paneRecovery ?? (profile?.paneLaunchId && bound?.usesNativePane?.() && !bound.nativePaneCurrent()
|
|
26488
|
-
? { kind: "exit-unconfirmed", launchId: profile.paneLaunchId } : null) };
|
|
26489
|
-
}
|
|
26490
|
-
catch {
|
|
26491
|
-
return { native_recovery: null };
|
|
26492
|
-
}
|
|
26493
|
-
})(),
|
|
26494
|
-
capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
|
|
26495
|
-
commands: true, effort: false, log: true, details: true, channels: false, terminal: false, attachments: false, usage: false, send_while_busy: true },
|
|
26496
|
-
...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
|
|
26497
|
-
machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
|
|
26498
25139
|
display_name: identity.display_name || undefined,
|
|
26499
25140
|
avatar: image.avatar,
|
|
26500
25141
|
...(image.accent ? { accent: image.accent } : {}),
|
|
@@ -26517,8 +25158,6 @@ export class FleetManager {
|
|
|
26517
25158
|
role: identity.role || undefined,
|
|
26518
25159
|
tags: identity.tags,
|
|
26519
25160
|
teams: instanceTeams(this.fleetConfig?.teams, name), // #2172: the sidebar groups a team's members under it
|
|
26520
|
-
// #3008 G2: a web-only fleet's General (the agent its owner ticked) — the sidebar pins it first.
|
|
26521
|
-
...(this.isWebOnlyFleet() && this.fleetConfig?.instances[name]?.general_topic === true ? { general: true } : {}),
|
|
26522
25161
|
last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined, // #2179: its last reply to a person (ISO)
|
|
26523
25162
|
// #2855: its last reply that reached the channel — the only reply time a guest's sidebar gets (guestStatus).
|
|
26524
25163
|
last_reply_channel: this.webChatHistory.lastAgentReply(name, { channel: true }) ?? undefined,
|
|
@@ -26548,7 +25187,6 @@ export class FleetManager {
|
|
|
26548
25187
|
uptime: Math.floor((Date.now() - this.startedAt) / 1000),
|
|
26549
25188
|
// #1519 P7: how many chat connections are configured — 0 is a web-only fleet, which gets the first-run card.
|
|
26550
25189
|
connections: (this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).length,
|
|
26551
|
-
windows_cli: localWindowsCliEnabled(),
|
|
26552
25190
|
// #1665: the host preferences' revision — a page that has another re-reads /ui/prefs (no values in this frame).
|
|
26553
25191
|
prefs_rev: this.webPrefs.rev(),
|
|
26554
25192
|
// #1629: the themes' revision — the page re-reads GET /ui/themes when it moves.
|
|
@@ -26558,8 +25196,6 @@ export class FleetManager {
|
|
|
26558
25196
|
host_memory: this.hostMemoryIndicator(),
|
|
26559
25197
|
// #2910 D2: a web-only fleet's newest Notice — a polling page (no SSE `notice`) reads the list again when it moves.
|
|
26560
25198
|
...(this.webOnlyPromptPlace() ? { notices_newest: this.webNoticeStore()?.newest() ?? 0 } : {}),
|
|
26561
|
-
// alpha.2 (M-2300, #2997 review): agents' requests changed — the owner's page reads GET /ui/agent-proposals again.
|
|
26562
|
-
...(this.isWebOnlyFleet() ? { agent_proposals_rev: this.agentProposalsRev } : {}),
|
|
26563
25199
|
// #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
|
|
26564
25200
|
...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
|
|
26565
25201
|
};
|
|
@@ -27002,12 +25638,4 @@ load();
|
|
|
27002
25638
|
</script>
|
|
27003
25639
|
</body>
|
|
27004
25640
|
</html>`;
|
|
27005
|
-
/** alpha.2 (M-2300): the agent an applied Windows request created (its name, for the page's "Open chat"), or null. */
|
|
27006
|
-
export function agentNameOf(view) {
|
|
27007
|
-
const r = view.state === "applied" ? view.outcome?.result : null;
|
|
27008
|
-
if (!r || typeof r !== "object")
|
|
27009
|
-
return null;
|
|
27010
|
-
const o = r, name = o.name ?? o.instance_name ?? (o.agent && typeof o.agent === "object" ? o.agent.name : undefined);
|
|
27011
|
-
return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name) ? name : null;
|
|
27012
|
-
}
|
|
27013
25641
|
//# sourceMappingURL=fleet-manager.js.map
|