@songsid/agend 2.3.5-alpha.3 → 2.3.5-alpha.5
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/THIRD_PARTY_NOTICES.md +52 -0
- package/dist/accounts-view.d.ts +97 -0
- package/dist/accounts-view.js +116 -0
- package/dist/accounts-view.js.map +1 -0
- 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 +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +1 -1
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-runtime-api.d.ts +4 -0
- package/dist/agent-runtime-api.js +59 -0
- package/dist/agent-runtime-api.js.map +1 -0
- package/dist/agent-runtime-change.d.ts +76 -0
- package/dist/agent-runtime-change.js +114 -0
- package/dist/agent-runtime-change.js.map +1 -0
- package/dist/agent-workspace.d.ts +93 -0
- package/dist/agent-workspace.js +353 -0
- package/dist/agent-workspace.js.map +1 -0
- package/dist/auth-api.d.ts +27 -0
- package/dist/auth-api.js +168 -3
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +7 -0
- package/dist/backend/codex-footer-hints.js +41 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- 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-quota-history.d.ts +7 -0
- package/dist/backend/codex-quota-history.js +57 -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 +7 -0
- package/dist/backend/codex.js +85 -24
- 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 -1
- package/dist/backend/grok.js +301 -54
- 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/native-pane.d.ts +3 -1
- package/dist/backend/native-pane.js +2 -2
- package/dist/backend/native-pane.js.map +1 -1
- package/dist/backend/types.d.ts +13 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/build-provenance-cli.d.ts +3 -0
- package/dist/build-provenance-cli.js +11 -0
- package/dist/build-provenance-cli.js.map +1 -0
- package/dist/build-provenance.d.ts +20 -0
- package/dist/build-provenance.js +67 -0
- package/dist/build-provenance.js.map +1 -0
- package/dist/build-provenance.json +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/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/claude-auth-status.d.ts +48 -0
- package/dist/claude-auth-status.js +149 -0
- package/dist/claude-auth-status.js.map +1 -0
- package/dist/cli.js +138 -37
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +21 -1
- package/dist/config-validator.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 -1
- package/dist/daemon.js +547 -25
- 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 +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +1 -1
- package/dist/delivery-reconciliation.js +1 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/doctor.js +20 -9
- package/dist/doctor.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 +2 -0
- package/dist/fleet-manager.d.ts +362 -7
- package/dist/fleet-manager.js +1522 -91
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-windows-runtime.d.ts +8 -2
- package/dist/fleet-windows-runtime.js +93 -6
- package/dist/fleet-windows-runtime.js.map +1 -1
- package/dist/general-knowledge/skills/concepts/SKILL.md +425 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +2 -2
- 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/session-management/SKILL.md +1 -1
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +10 -10
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +31 -15
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/general-role.d.ts +1 -1
- package/dist/general-role.js +1 -0
- package/dist/general-role.js.map +1 -1
- package/dist/hub/admission.d.ts +28 -0
- package/dist/hub/admission.js +62 -0
- package/dist/hub/admission.js.map +1 -0
- package/dist/hub/attachments/limits.d.ts +20 -0
- package/dist/hub/attachments/limits.js +48 -0
- package/dist/hub/attachments/limits.js.map +1 -0
- package/dist/hub/attachments/store.d.ts +116 -0
- package/dist/hub/attachments/store.js +246 -0
- package/dist/hub/attachments/store.js.map +1 -0
- package/dist/hub/audit/index.d.ts +62 -0
- package/dist/hub/audit/index.js +344 -0
- package/dist/hub/audit/index.js.map +1 -0
- package/dist/hub/browser-certificate.d.ts +38 -0
- package/dist/hub/browser-certificate.js +247 -0
- package/dist/hub/browser-certificate.js.map +1 -0
- package/dist/hub/buttons-model.d.ts +9 -0
- package/dist/hub/buttons-model.js +42 -0
- package/dist/hub/buttons-model.js.map +1 -0
- package/dist/hub/buttons.d.ts +54 -0
- package/dist/hub/buttons.js +346 -0
- package/dist/hub/buttons.js.map +1 -0
- package/dist/hub/catalog.d.ts +74 -0
- package/dist/hub/catalog.js +195 -0
- package/dist/hub/catalog.js.map +1 -0
- package/dist/hub/client.d.ts +69 -0
- package/dist/hub/client.js +300 -0
- package/dist/hub/client.js.map +1 -0
- package/dist/hub/compose/member.d.ts +139 -0
- package/dist/hub/compose/member.js +450 -0
- package/dist/hub/compose/member.js.map +1 -0
- package/dist/hub/compose/server.d.ts +77 -0
- package/dist/hub/compose/server.js +349 -0
- package/dist/hub/compose/server.js.map +1 -0
- package/dist/hub/compose/views.d.ts +12 -0
- package/dist/hub/compose/views.js +4 -0
- package/dist/hub/compose/views.js.map +1 -0
- package/dist/hub/db.d.ts +56 -0
- package/dist/hub/db.js +188 -0
- package/dist/hub/db.js.map +1 -0
- package/dist/hub/directory/config.d.ts +39 -0
- package/dist/hub/directory/config.js +108 -0
- package/dist/hub/directory/config.js.map +1 -0
- package/dist/hub/directory/directory.d.ts +18 -0
- package/dist/hub/directory/directory.js +320 -0
- package/dist/hub/directory/directory.js.map +1 -0
- package/dist/hub/directory/index.d.ts +7 -0
- package/dist/hub/directory/index.js +4 -0
- package/dist/hub/directory/index.js.map +1 -0
- package/dist/hub/directory/profiles.d.ts +41 -0
- package/dist/hub/directory/profiles.js +90 -0
- package/dist/hub/directory/profiles.js.map +1 -0
- package/dist/hub/directory/types.d.ts +71 -0
- package/dist/hub/directory/types.js +14 -0
- package/dist/hub/directory/types.js.map +1 -0
- package/dist/hub/execution/cli-provider.d.ts +23 -0
- package/dist/hub/execution/cli-provider.js +341 -0
- package/dist/hub/execution/cli-provider.js.map +1 -0
- package/dist/hub/execution/index.d.ts +13 -0
- package/dist/hub/execution/index.js +8 -0
- package/dist/hub/execution/index.js.map +1 -0
- package/dist/hub/execution/model-broker.d.ts +24 -0
- package/dist/hub/execution/model-broker.js +208 -0
- package/dist/hub/execution/model-broker.js.map +1 -0
- package/dist/hub/execution/provider-setup.d.ts +11 -0
- package/dist/hub/execution/provider-setup.js +21 -0
- package/dist/hub/execution/provider-setup.js.map +1 -0
- package/dist/hub/execution/sandbox-runner.d.ts +1 -0
- package/dist/hub/execution/sandbox-runner.js +221 -0
- package/dist/hub/execution/sandbox-runner.js.map +1 -0
- package/dist/hub/execution/service.d.ts +136 -0
- package/dist/hub/execution/service.js +459 -0
- package/dist/hub/execution/service.js.map +1 -0
- package/dist/hub/execution/store.d.ts +61 -0
- package/dist/hub/execution/store.js +182 -0
- package/dist/hub/execution/store.js.map +1 -0
- package/dist/hub/execution/views.d.ts +20 -0
- package/dist/hub/execution/views.js +48 -0
- package/dist/hub/execution/views.js.map +1 -0
- package/dist/hub/http/compose.d.ts +20 -0
- package/dist/hub/http/compose.js +518 -0
- package/dist/hub/http/compose.js.map +1 -0
- package/dist/hub/http/core.d.ts +49 -0
- package/dist/hub/http/core.js +124 -0
- package/dist/hub/http/core.js.map +1 -0
- package/dist/hub/http/deps.d.ts +61 -0
- package/dist/hub/http/deps.js +2 -0
- package/dist/hub/http/deps.js.map +1 -0
- package/dist/hub/http/schema.d.ts +14 -0
- package/dist/hub/http/schema.js +80 -0
- package/dist/hub/http/schema.js.map +1 -0
- package/dist/hub/identity.d.ts +32 -0
- package/dist/hub/identity.js +80 -0
- package/dist/hub/identity.js.map +1 -0
- package/dist/hub/init/browser-certificate.d.ts +9 -0
- package/dist/hub/init/browser-certificate.js +78 -0
- package/dist/hub/init/browser-certificate.js.map +1 -0
- package/dist/hub/init/browser-tls.d.ts +18 -0
- package/dist/hub/init/browser-tls.js +62 -0
- package/dist/hub/init/browser-tls.js.map +1 -0
- package/dist/hub/init/cli.d.ts +3 -0
- package/dist/hub/init/cli.js +83 -0
- package/dist/hub/init/cli.js.map +1 -0
- package/dist/hub/init/directory-certificate.d.ts +24 -0
- package/dist/hub/init/directory-certificate.js +222 -0
- package/dist/hub/init/directory-certificate.js.map +1 -0
- package/dist/hub/init/existing-fleet.d.ts +6 -0
- package/dist/hub/init/existing-fleet.js +58 -0
- package/dist/hub/init/existing-fleet.js.map +1 -0
- package/dist/hub/init/handoff.d.ts +8 -0
- package/dist/hub/init/handoff.js +36 -0
- package/dist/hub/init/handoff.js.map +1 -0
- package/dist/hub/init/identity.d.ts +5 -0
- package/dist/hub/init/identity.js +24 -0
- package/dist/hub/init/identity.js.map +1 -0
- package/dist/hub/init/initializer.d.ts +24 -0
- package/dist/hub/init/initializer.js +385 -0
- package/dist/hub/init/initializer.js.map +1 -0
- package/dist/hub/init/inventory.d.ts +17 -0
- package/dist/hub/init/inventory.js +33 -0
- package/dist/hub/init/inventory.js.map +1 -0
- package/dist/hub/init/layout.d.ts +32 -0
- package/dist/hub/init/layout.js +53 -0
- package/dist/hub/init/layout.js.map +1 -0
- package/dist/hub/init/network.d.ts +18 -0
- package/dist/hub/init/network.js +104 -0
- package/dist/hub/init/network.js.map +1 -0
- package/dist/hub/init/plan-digest.d.ts +5 -0
- package/dist/hub/init/plan-digest.js +47 -0
- package/dist/hub/init/plan-digest.js.map +1 -0
- package/dist/hub/init/prompt.d.ts +26 -0
- package/dist/hub/init/prompt.js +110 -0
- package/dist/hub/init/prompt.js.map +1 -0
- package/dist/hub/init/secret-file.d.ts +1 -0
- package/dist/hub/init/secret-file.js +59 -0
- package/dist/hub/init/secret-file.js.map +1 -0
- package/dist/hub/init/text.d.ts +127 -0
- package/dist/hub/init/text.js +247 -0
- package/dist/hub/init/text.js.map +1 -0
- package/dist/hub/init/types.d.ts +114 -0
- package/dist/hub/init/types.js +9 -0
- package/dist/hub/init/types.js.map +1 -0
- package/dist/hub/init/wizard.d.ts +34 -0
- package/dist/hub/init/wizard.js +554 -0
- package/dist/hub/init/wizard.js.map +1 -0
- package/dist/hub/initialization.d.ts +23 -0
- package/dist/hub/initialization.js +28 -0
- package/dist/hub/initialization.js.map +1 -0
- package/dist/hub/member/agent-mode.d.ts +6 -0
- package/dist/hub/member/agent-mode.js +23 -0
- package/dist/hub/member/agent-mode.js.map +1 -0
- package/dist/hub/member/attachments.d.ts +29 -0
- package/dist/hub/member/attachments.js +107 -0
- package/dist/hub/member/attachments.js.map +1 -0
- package/dist/hub/member/exposure-api.d.ts +87 -0
- package/dist/hub/member/exposure-api.js +64 -0
- package/dist/hub/member/exposure-api.js.map +1 -0
- package/dist/hub/member/exposures.d.ts +123 -0
- package/dist/hub/member/exposures.js +282 -0
- package/dist/hub/member/exposures.js.map +1 -0
- package/dist/hub/member/pairing-api.d.ts +85 -0
- package/dist/hub/member/pairing-api.js +385 -0
- package/dist/hub/member/pairing-api.js.map +1 -0
- package/dist/hub/member/quota.d.ts +20 -0
- package/dist/hub/member/quota.js +98 -0
- package/dist/hub/member/quota.js.map +1 -0
- package/dist/hub/member-journal.d.ts +104 -0
- package/dist/hub/member-journal.js +336 -0
- package/dist/hub/member-journal.js.map +1 -0
- package/dist/hub/member-reconciliation.d.ts +14 -0
- package/dist/hub/member-reconciliation.js +97 -0
- package/dist/hub/member-reconciliation.js.map +1 -0
- package/dist/hub/mount.d.ts +39 -0
- package/dist/hub/mount.js +123 -0
- package/dist/hub/mount.js.map +1 -0
- package/dist/hub/pairing-client.d.ts +17 -0
- package/dist/hub/pairing-client.js +108 -0
- package/dist/hub/pairing-client.js.map +1 -0
- package/dist/hub/pairing.d.ts +65 -0
- package/dist/hub/pairing.js +211 -0
- package/dist/hub/pairing.js.map +1 -0
- package/dist/hub/policy.d.ts +63 -0
- package/dist/hub/policy.js +147 -0
- package/dist/hub/policy.js.map +1 -0
- package/dist/hub/preview.d.ts +32 -0
- package/dist/hub/preview.js +64 -0
- package/dist/hub/preview.js.map +1 -0
- package/dist/hub/private-files.d.ts +5 -0
- package/dist/hub/private-files.js +44 -0
- package/dist/hub/private-files.js.map +1 -0
- package/dist/hub/profile.d.ts +59 -0
- package/dist/hub/profile.js +73 -0
- package/dist/hub/profile.js.map +1 -0
- package/dist/hub/protocol.d.ts +779 -0
- package/dist/hub/protocol.js +93 -0
- package/dist/hub/protocol.js.map +1 -0
- package/dist/hub/quota.d.ts +24 -0
- package/dist/hub/quota.js +112 -0
- package/dist/hub/quota.js.map +1 -0
- package/dist/hub/representative.d.ts +141 -0
- package/dist/hub/representative.js +528 -0
- package/dist/hub/representative.js.map +1 -0
- package/dist/hub/retention.d.ts +17 -0
- package/dist/hub/retention.js +45 -0
- package/dist/hub/retention.js.map +1 -0
- package/dist/hub/rooms.d.ts +120 -0
- package/dist/hub/rooms.js +535 -0
- package/dist/hub/rooms.js.map +1 -0
- package/dist/hub/runtime.d.ts +106 -0
- package/dist/hub/runtime.js +298 -0
- package/dist/hub/runtime.js.map +1 -0
- package/dist/hub/server.d.ts +56 -0
- package/dist/hub/server.js +202 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/service-start.d.ts +17 -0
- package/dist/hub/service-start.js +109 -0
- package/dist/hub/service-start.js.map +1 -0
- package/dist/hub/sessions.d.ts +53 -0
- package/dist/hub/sessions.js +196 -0
- package/dist/hub/sessions.js.map +1 -0
- package/dist/hub/socket.d.ts +24 -0
- package/dist/hub/socket.js +79 -0
- package/dist/hub/socket.js.map +1 -0
- package/dist/hub/store.d.ts +77 -0
- package/dist/hub/store.js +88 -0
- package/dist/hub/store.js.map +1 -0
- package/dist/hub/tls.d.ts +31 -0
- package/dist/hub/tls.js +135 -0
- package/dist/hub/tls.js.map +1 -0
- package/dist/hub/types.d.ts +401 -0
- package/dist/hub/types.js +2 -0
- package/dist/hub/types.js.map +1 -0
- package/dist/hub/validation.d.ts +33 -0
- package/dist/hub/validation.js +136 -0
- package/dist/hub/validation.js.map +1 -0
- package/dist/hub/version-floor.d.ts +10 -0
- package/dist/hub/version-floor.js +45 -0
- package/dist/hub/version-floor.js.map +1 -0
- package/dist/hub-browser-certificate-policy.d.ts +31 -0
- package/dist/hub-browser-certificate-policy.js +25 -0
- package/dist/hub-browser-certificate-policy.js.map +1 -0
- package/dist/hub-membership-settings.d.ts +6 -0
- package/dist/hub-membership-settings.js +21 -0
- package/dist/hub-membership-settings.js.map +1 -0
- package/dist/hub-mode.d.ts +15 -0
- package/dist/hub-mode.js +17 -0
- package/dist/hub-mode.js.map +1 -0
- package/dist/hub-pairing-summary.d.ts +48 -0
- package/dist/hub-pairing-summary.js +36 -0
- package/dist/hub-pairing-summary.js.map +1 -0
- package/dist/hub-report-target.d.ts +32 -0
- package/dist/hub-report-target.js +33 -0
- package/dist/hub-report-target.js.map +1 -0
- package/dist/hub-retention-policy.d.ts +14 -0
- package/dist/hub-retention-policy.js +15 -0
- package/dist/hub-retention-policy.js.map +1 -0
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +17 -2
- package/dist/instance-lifecycle.js +58 -8
- 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 -1
- package/dist/instructions.js +1 -1
- 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/launchd-service-domain.d.ts +5 -0
- package/dist/launchd-service-domain.js +21 -0
- package/dist/launchd-service-domain.js.map +1 -0
- package/dist/locale.js +125 -3
- 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 +15 -7
- package/dist/login-flows.js +29 -3
- 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/ls-rows.js +3 -4
- package/dist/ls-rows.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/native-instance-api.d.ts +0 -1
- package/dist/native-instance-api.js +15 -3
- package/dist/native-instance-api.js.map +1 -1
- package/dist/native-instance-input.d.ts +3 -0
- package/dist/native-instance-input.js +6 -1
- package/dist/native-instance-input.js.map +1 -1
- package/dist/native-instance-registry.d.ts +6 -0
- package/dist/native-instance-registry.js +24 -0
- package/dist/native-instance-registry.js.map +1 -1
- package/dist/native-mcp.d.ts +3 -0
- package/dist/native-mcp.js +15 -2
- package/dist/native-mcp.js.map +1 -1
- package/dist/native-pane-lifecycle.d.ts +8 -0
- package/dist/native-pane-lifecycle.js +13 -4
- package/dist/native-pane-lifecycle.js.map +1 -1
- package/dist/native-pane-manager.js +17 -9
- package/dist/native-pane-manager.js.map +1 -1
- package/dist/needs-you-hub.d.ts +9 -1
- package/dist/needs-you-hub.js +46 -8
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +47 -2
- package/dist/needs-you.js +31 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/office-share-page.js +3 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share.d.ts +11 -0
- package/dist/office-share.js +14 -0
- package/dist/office-share.js.map +1 -1
- package/dist/office-talk.d.ts +26 -0
- package/dist/office-talk.js +50 -0
- package/dist/office-talk.js.map +1 -0
- package/dist/outbound-handlers.d.ts +14 -0
- package/dist/outbound-handlers.js +49 -6
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +33 -1
- package/dist/outbound-schemas.js +29 -16
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-quota-history.d.ts +9 -0
- package/dist/pane-quota-history.js +67 -0
- package/dist/pane-quota-history.js.map +1 -0
- package/dist/pause-marker.d.ts +1 -1
- package/dist/pause-marker.js +1 -1
- package/dist/pause-marker.js.map +1 -1
- 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 +7 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -0
- package/dist/public-web-link.js +9 -0
- 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.d.ts +13 -0
- package/dist/quickstart.js +81 -5
- package/dist/quickstart.js.map +1 -1
- 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/runtime-api.d.ts +2 -0
- package/dist/runtime-api.js +106 -13
- package/dist/runtime-api.js.map +1 -1
- package/dist/runtime-build-info.d.ts +9 -0
- package/dist/runtime-build-info.js +59 -0
- package/dist/runtime-build-info.js.map +1 -0
- package/dist/runtime-bundle.d.ts +6 -1
- package/dist/runtime-bundle.js +17 -9
- package/dist/runtime-bundle.js.map +1 -1
- package/dist/runtime-cli.d.ts +13 -1
- package/dist/runtime-cli.js +173 -19
- package/dist/runtime-cli.js.map +1 -1
- package/dist/runtime-client-store.d.ts +27 -1
- package/dist/runtime-client-store.js +86 -6
- package/dist/runtime-client-store.js.map +1 -1
- package/dist/runtime-client.d.ts +74 -0
- package/dist/runtime-client.js +336 -26
- package/dist/runtime-client.js.map +1 -1
- package/dist/runtime-connection.d.ts +22 -0
- package/dist/runtime-connection.js +93 -0
- package/dist/runtime-connection.js.map +1 -0
- package/dist/runtime-coordinator.d.ts +16 -0
- package/dist/runtime-coordinator.js +287 -62
- package/dist/runtime-coordinator.js.map +1 -1
- package/dist/runtime-credential-profiles.d.ts +78 -0
- package/dist/runtime-credential-profiles.js +280 -0
- package/dist/runtime-credential-profiles.js.map +1 -0
- package/dist/runtime-install.d.ts +50 -0
- package/dist/runtime-install.js +186 -0
- package/dist/runtime-install.js.map +1 -0
- package/dist/runtime-pane-assets.d.ts +1 -1
- package/dist/runtime-pane-assets.js +1 -1
- package/dist/runtime-pane-assets.js.map +1 -1
- package/dist/runtime-pane-boot.d.ts +7 -0
- package/dist/runtime-pane-boot.js +23 -0
- package/dist/runtime-pane-boot.js.map +1 -1
- package/dist/runtime-pane-codex-index.d.ts +10 -1
- package/dist/runtime-pane-codex-index.js +131 -27
- package/dist/runtime-pane-codex-index.js.map +1 -1
- package/dist/runtime-pane-host.d.ts +19 -0
- package/dist/runtime-pane-host.js +50 -7
- package/dist/runtime-pane-host.js.map +1 -1
- package/dist/runtime-pane-link.d.ts +7 -0
- package/dist/runtime-pane-link.js +93 -2
- package/dist/runtime-pane-link.js.map +1 -1
- package/dist/runtime-pane-mcp-relay.d.ts +1 -0
- package/dist/runtime-pane-mcp-relay.js +10 -0
- package/dist/runtime-pane-mcp-relay.js.map +1 -1
- package/dist/runtime-pane-mcp.d.ts +4 -0
- package/dist/runtime-pane-mcp.js +6 -3
- package/dist/runtime-pane-mcp.js.map +1 -1
- package/dist/runtime-pane-native.d.ts +32 -2
- package/dist/runtime-pane-native.js +55 -6
- package/dist/runtime-pane-native.js.map +1 -1
- package/dist/runtime-pane-port.d.ts +5 -0
- package/dist/runtime-pane-port.js +17 -1
- package/dist/runtime-pane-port.js.map +1 -1
- package/dist/runtime-pane-process.d.ts +1 -1
- package/dist/runtime-pane-process.js +11 -2
- package/dist/runtime-pane-process.js.map +1 -1
- package/dist/runtime-pane-protocol.d.ts +11 -1
- package/dist/runtime-pane-protocol.js +27 -5
- package/dist/runtime-pane-protocol.js.map +1 -1
- package/dist/runtime-pane-quota-history.d.ts +6 -0
- package/dist/runtime-pane-quota-history.js +44 -0
- package/dist/runtime-pane-quota-history.js.map +1 -0
- package/dist/runtime-pane-service.d.ts +5 -0
- package/dist/runtime-pane-service.js +47 -12
- package/dist/runtime-pane-service.js.map +1 -1
- package/dist/runtime-pane-transcript.d.ts +5 -0
- package/dist/runtime-pane-transcript.js +66 -5
- package/dist/runtime-pane-transcript.js.map +1 -1
- package/dist/runtime-pane-worker.js +19 -6
- package/dist/runtime-pane-worker.js.map +1 -1
- package/dist/runtime-protocol.d.ts +30 -2
- package/dist/runtime-protocol.js +30 -0
- package/dist/runtime-protocol.js.map +1 -1
- package/dist/runtime-update-activity.d.ts +21 -0
- package/dist/runtime-update-activity.js +51 -0
- package/dist/runtime-update-activity.js.map +1 -0
- package/dist/runtime-update-artifact.d.ts +31 -0
- package/dist/runtime-update-artifact.js +252 -0
- package/dist/runtime-update-artifact.js.map +1 -0
- package/dist/runtime-update-install.d.ts +31 -0
- package/dist/runtime-update-install.js +205 -0
- package/dist/runtime-update-install.js.map +1 -0
- package/dist/runtime-update-journal.d.ts +44 -0
- package/dist/runtime-update-journal.js +121 -0
- package/dist/runtime-update-journal.js.map +1 -0
- package/dist/runtime-update-lock.d.ts +48 -0
- package/dist/runtime-update-lock.js +217 -0
- package/dist/runtime-update-lock.js.map +1 -0
- package/dist/runtime-update-owners.d.ts +29 -0
- package/dist/runtime-update-owners.js +240 -0
- package/dist/runtime-update-owners.js.map +1 -0
- package/dist/runtime-update-protocol.d.ts +15 -0
- package/dist/runtime-update-protocol.js +13 -0
- package/dist/runtime-update-protocol.js.map +1 -0
- package/dist/runtime-windows-bridge.d.ts +2 -1
- package/dist/runtime-windows-bridge.js +11 -1
- package/dist/runtime-windows-bridge.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-text.js +1 -1
- package/dist/scheduler/task-board-text.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +13 -0
- package/dist/scheduler/task-board.js +28 -3
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/service-drain.d.ts +19 -0
- package/dist/service-drain.js +64 -0
- package/dist/service-drain.js.map +1 -0
- package/dist/service-installer.d.ts +7 -2
- package/dist/service-installer.js +13 -10
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +20 -0
- package/dist/settings-api.js +414 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +6 -0
- package/dist/settings-baseline.js +3 -1
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.js +29 -6
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.d.ts +13 -0
- package/dist/settings-effect.js +98 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +3 -0
- package/dist/settings-http-confirmation.js +28 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-instance-patch.js +27 -0
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-wizard.js +6 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-tool.d.ts +13 -1
- package/dist/task-board-tool.js +104 -4
- 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-binary.d.ts +59 -0
- package/dist/tmux-binary.js +200 -0
- package/dist/tmux-binary.js.map +1 -0
- package/dist/tmux-control.js +4 -4
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.js +32 -1
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -1
- package/dist/tmux-manager.js +45 -36
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.js +4 -3
- package/dist/tmux-read.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- 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 +10 -5
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +20 -0
- package/dist/transcript-monitor.js +47 -3
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-needles.d.ts +7 -0
- package/dist/transcript-needles.js +9 -1
- package/dist/transcript-needles.js.map +1 -1
- package/dist/transcript-records.d.ts +1 -1
- package/dist/transcript-records.js +2 -1
- package/dist/transcript-records.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/ui/account-identity.js +27 -0
- package/dist/ui/accounts-view.js +214 -0
- package/dist/ui/agent-runtime-settings.js +68 -0
- package/dist/ui/agent-runtime.js +20 -2
- package/dist/ui/backend-signin.js +39 -21
- package/dist/ui/chat-ambient.js +1 -1
- package/dist/ui/chat-md-strings.js +3 -0
- package/dist/ui/chat-render.js +3 -670
- package/dist/ui/chat-store.js +1 -1
- package/dist/ui/chat-strings.js +3 -333
- package/dist/ui/chat-thread.js +3 -919
- package/dist/ui/chat-time.js +3 -80
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/credential-profile-select.js +54 -0
- package/dist/ui/fleet-office-dialogue.d.ts +30 -0
- package/dist/ui/fleet-office-dialogue.js +382 -0
- package/dist/ui/fleet-office-pack.d.ts +9 -0
- package/dist/ui/fleet-office-pack.js +76 -4
- package/dist/ui/fleet-office-skin.js +23 -5
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office.js +173 -6
- package/dist/ui/fleet-task-card.js +10 -6
- package/dist/ui/fleet-task-common.js +12 -2
- package/dist/ui/fleet-task-menu.js +188 -0
- package/dist/ui/fleet-task-stats.js +124 -0
- package/dist/ui/fleet-task-view.js +340 -0
- package/dist/ui/fleet-tasks.js +131 -43
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/html-attachment.js +3 -40
- package/dist/ui/hub-agent-copy.js +23 -0
- package/dist/ui/hub-button-actions.js +65 -0
- package/dist/ui/hub-chat-store.js +158 -0
- package/dist/ui/hub-chat-strings.js +77 -0
- package/dist/ui/hub-client.js +329 -0
- package/dist/ui/hub-owner-strings.js +51 -0
- package/dist/ui/hub-provider-guidance.js +31 -0
- package/dist/ui/hub-quota.js +85 -0
- package/dist/ui/hub-strings.js +162 -0
- package/dist/ui/hub-switcher.js +27 -0
- package/dist/ui/native-instance-settings.js +5 -8
- package/dist/ui/native-pane-recovery.js +1 -1
- package/dist/ui/office-share-page.js +4 -2
- package/dist/ui/panel-chat.js +36 -49
- package/dist/ui/panel-details.js +1 -1
- package/dist/ui/panel-fleet.js +82 -12
- package/dist/ui/panel-hub.js +619 -0
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +24 -8
- package/dist/ui/panel-runtimes.js +239 -45
- package/dist/ui/panel-settings.js +266 -192
- package/dist/ui/panel-windows-cli.js +88 -42
- package/dist/ui/preview-pages.js +1 -1
- package/dist/ui/preview-panel.js +92 -6
- package/dist/ui/preview-versions.js +1 -1
- package/dist/ui/preview.js +3 -476
- package/dist/ui/runtime-commands.d.ts +4 -0
- package/dist/ui/runtime-commands.js +59 -0
- package/dist/ui/search-dialog.js +2 -0
- package/dist/ui/settings-access-strings.js +42 -0
- package/dist/ui/settings-agent-filters.js +94 -0
- package/dist/ui/settings-appearance.js +122 -23
- package/dist/ui/settings-build-version.js +35 -0
- package/dist/ui/settings-codex-copy.js +19 -0
- package/dist/ui/settings-devices.js +253 -0
- package/dist/ui/settings-dialogs.js +33 -12
- package/dist/ui/settings-guests.js +25 -11
- package/dist/ui/settings-hub-browser-certificate.js +74 -0
- package/dist/ui/settings-hub-membership.js +72 -0
- package/dist/ui/settings-hub-retention.js +52 -0
- package/dist/ui/settings-hub.js +196 -0
- package/dist/ui/settings-layout.js +242 -0
- package/dist/ui/settings-model.js +24 -2
- package/dist/ui/settings-outside.js +104 -38
- package/dist/ui/settings-registry.js +9 -4
- package/dist/ui/settings-shared-strings.js +143 -0
- package/dist/ui/settings-shared.js +345 -0
- package/dist/ui/settings-shortcuts.js +65 -0
- package/dist/ui/settings-strings.js +89 -32
- package/dist/ui/settings-web-authority.js +17 -11
- package/dist/ui/shared/accounts.css +15 -0
- 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 +6 -1
- package/dist/ui/shared/app-session.js +6 -2
- package/dist/ui/shared/app-shell.js +81 -15
- package/dist/ui/shared/app.css +1114 -13
- package/dist/ui/shared/app.js +12 -3
- package/dist/ui/shared/chat-composer.js +68 -0
- package/dist/ui/shared/chat-md-strings.js +9 -0
- package/dist/ui/shared/chat-render.js +682 -0
- package/dist/ui/shared/chat-strings.js +333 -0
- package/dist/ui/shared/chat-thread.js +969 -0
- package/dist/ui/shared/chat-time.js +80 -0
- package/dist/ui/shared/header-tools.js +3 -5
- package/dist/ui/shared/html-attachment.js +42 -0
- package/dist/ui/shared/hub-chat.css +85 -0
- package/dist/ui/shared/hub-owner.css +33 -0
- package/dist/ui/shared/hub-quota.css +8 -0
- package/dist/ui/shared/hub.css +71 -0
- package/dist/ui/shared/instance-nav.js +3 -2
- package/dist/ui/shared/machine-name.js +13 -0
- package/dist/ui/shared/page-head.js +8 -0
- package/dist/ui/shared/panel-view.js +77 -4
- package/dist/ui/shared/preview.js +476 -0
- package/dist/ui/shared/runtime-install.css +26 -0
- package/dist/ui/shared/settings-width.js +31 -0
- package/dist/ui/shared/side-rail.js +15 -0
- package/dist/ui/shared/signin.js +172 -0
- package/dist/ui/shared/task-board.css +95 -0
- package/dist/ui/shared/tokens.css +1 -0
- package/dist/ui/shared/ui-dialog.js +2 -2
- package/dist/ui/shared/ui-menu.js +3 -3
- package/dist/ui/shared/usage-dialog.js +18 -1
- 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 -0
- package/dist/ui/shared/workspace.css +20 -0
- package/dist/ui/signin.html +21 -0
- package/dist/ui/windows-cli-catalog.js +41 -12
- package/dist/ui/workspace-panel.js +189 -0
- package/dist/ui/workspace-strings.js +35 -0
- package/dist/unattended-install.d.ts +11 -2
- package/dist/unattended-install.js +41 -6
- 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.js +2 -1
- 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/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -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 +105 -23
- 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 +49 -2
- package/dist/web-api.js +279 -8
- package/dist/web-api.js.map +1 -1
- package/dist/web-backend-login.d.ts +23 -1
- package/dist/web-backend-login.js +11 -17
- package/dist/web-backend-login.js.map +1 -1
- package/dist/web-device-login.d.ts +153 -0
- package/dist/web-device-login.js +278 -0
- package/dist/web-device-login.js.map +1 -0
- package/dist/web-office-skins.d.ts +9 -0
- package/dist/web-office-skins.js +16 -1
- package/dist/web-office-skins.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 +35 -0
- package/dist/web-session.js +7 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +6 -0
- package/dist/web-shell-routes.js +7 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-terminal.d.ts +4 -30
- package/dist/web-terminal.js +9 -6
- package/dist/web-terminal.js.map +1 -1
- package/dist/windows-agents.d.ts +4 -1
- package/dist/windows-agents.js +14 -10
- package/dist/windows-agents.js.map +1 -1
- package/dist/windows-cli-api.d.ts +12 -1
- package/dist/windows-cli-api.js +29 -2
- package/dist/windows-cli-api.js.map +1 -1
- package/dist/windows-cli-providers.d.ts +0 -9
- package/dist/windows-cli-providers.js +4 -9
- package/dist/windows-cli-providers.js.map +1 -1
- package/dist/windows-cli-worker.d.ts +25 -0
- package/dist/windows-cli-worker.js +98 -7
- package/dist/windows-cli-worker.js.map +1 -1
- package/dist/windows-cli.d.ts +11 -0
- package/dist/windows-cli.js +17 -3
- package/dist/windows-cli.js.map +1 -1
- package/dist/yaml-shared-nodes.js +8 -3
- package/dist/yaml-shared-nodes.js.map +1 -1
- package/launcher/agend-install.cjs +8 -1
- package/launcher/launchd-session-type.cjs +14 -0
- package/launcher/postinstall.cjs +27 -2
- package/launcher/private-home.cjs +1 -1
- package/launcher/tmux-bundle.cjs +247 -0
- package/package.json +3 -2
- package/templates/launchd.plist.ejs +3 -1
- package/templates/runtime-install/install.ps1 +270 -0
package/dist/fleet-manager.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
+
import { readInstalledBuild } from "./build-provenance.js";
|
|
1
2
|
import { buildNativeFleetInstructions } from "./instructions.js";
|
|
2
3
|
import { GENERAL_ROLE_INSTRUCTIONS } from "./general-role.js";
|
|
3
4
|
import { NativePaneRecoveryError } from "./native-pane-recovery.js";
|
|
4
5
|
import { nativePaneToolAvailable, nativePaneToolRefusal } from "./native-pane-tools.js";
|
|
5
6
|
import { windowsAgentsFor } from "./windows-agents.js";
|
|
7
|
+
import { AgentRuntimeChanges } from "./agent-runtime-change.js";
|
|
6
8
|
import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
|
|
7
9
|
import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
|
|
8
10
|
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
9
11
|
import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
|
|
10
12
|
import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
13
|
+
import { maskSecrets } from "./log-steps.js";
|
|
11
14
|
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
12
15
|
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
13
16
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
14
17
|
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
15
18
|
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
16
19
|
import { SettingsBaselines } from "./settings-baseline.js";
|
|
20
|
+
import { hubMembershipSettings } from "./hub-membership-settings.js";
|
|
17
21
|
import { prepareSettingsEffect } from "./settings-effect.js";
|
|
18
22
|
import { settingsDisplay } from "./settings-change.js";
|
|
19
23
|
import { SettingsConfirmationError, SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
|
|
@@ -38,11 +42,13 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
|
|
|
38
42
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
39
43
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
40
44
|
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
41
|
-
import { unattendedInstallCommand, loginShell } from "./unattended-install.js";
|
|
45
|
+
import { installExitMarker, unattendedInstallCommand, loginShell } from "./unattended-install.js";
|
|
42
46
|
import { InstallStream } from "./install-stream.js";
|
|
43
47
|
import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
|
|
44
48
|
import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
|
|
45
49
|
import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
|
|
50
|
+
import { OfficeTalk } from "./office-talk.js";
|
|
51
|
+
import { pairingSummary } from "./hub-pairing-summary.js";
|
|
46
52
|
import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
|
|
47
53
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
48
54
|
import { ManagedTunnel } from "./tunnel/manager.js";
|
|
@@ -79,6 +85,11 @@ import { tmuxExecHelper } from "./tmux-exec-helper.js";
|
|
|
79
85
|
import { kiroLogGate } from "./kiro-log-gate.js";
|
|
80
86
|
const __filename = fileURLToPath(import.meta.url);
|
|
81
87
|
const __dirname = dirname(__filename);
|
|
88
|
+
// Describe this running program, not files an updater may replace later. No reads on dashboard polls.
|
|
89
|
+
const RUNNING_BUILD = readInstalledBuild();
|
|
90
|
+
if (RUNNING_BUILD.source)
|
|
91
|
+
Object.freeze(RUNNING_BUILD.source);
|
|
92
|
+
Object.freeze(RUNNING_BUILD);
|
|
82
93
|
/** Fallback access policy for a channel with no `access:` block — open (no gate). */
|
|
83
94
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
84
95
|
import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
@@ -86,6 +97,7 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
|
86
97
|
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
87
98
|
import { EventLog } from "./event-log.js";
|
|
88
99
|
import { binaryProbe } from "./binary-probe.js";
|
|
100
|
+
import { deletePlannedTopic, planInstanceTopicDelete, topicDestination, topicMustStay } from "./instance-topic-delete.js";
|
|
89
101
|
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
|
|
90
102
|
import { classifySqliteOpenError } from "./sqlite-open-errors.js";
|
|
91
103
|
import { OutboxOpenError } from "./outbox-open-error.js";
|
|
@@ -102,6 +114,7 @@ import { formatFleetTime } from "./tz-utils.js";
|
|
|
102
114
|
import { createAdapter } from "./channel/factory.js";
|
|
103
115
|
import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
|
|
104
116
|
import { TelegramAdapter } from "./channel/adapters/telegram.js";
|
|
117
|
+
import { MissedOfflineStore } from "./missed-offline.js";
|
|
105
118
|
import { createBackend } from "./backend/factory.js";
|
|
106
119
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
107
120
|
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
@@ -127,6 +140,7 @@ import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, che
|
|
|
127
140
|
import { TopicArchiver } from "./topic-archiver.js";
|
|
128
141
|
import { StatuslineWatcher } from "./statusline-watcher.js";
|
|
129
142
|
import { applyInstanceConfigPatch, outboundHandlers } from "./outbound-handlers.js";
|
|
143
|
+
import { HUB_REPORT_TARGET } from "./hub-report-target.js";
|
|
130
144
|
import { backendArgs, backendStep, joinBackendArgs, modelNameCarried, planDisplay } from "./backend-command.js";
|
|
131
145
|
import { DeliveryOutbox, DEFAULT_REPLY_OBLIGATION_TTL_MS, isSilentScheduleDelivery } from "./delivery-outbox.js";
|
|
132
146
|
// The target Daemon can legitimately wait 30 minutes for a busy pane before
|
|
@@ -153,6 +167,20 @@ export const LIVE_TASK_STATUSES = ["open", "claimed", "blocked", "review"];
|
|
|
153
167
|
* " \t " and [" ", "\t"] both normalize to undefined (not treated as explicit
|
|
154
168
|
* filters). The same result drives the live-only default and the cap decision.
|
|
155
169
|
*/
|
|
170
|
+
/**
|
|
171
|
+
* #3195: the saved value of each plain-update field the call named (assignee, priority, result), read back from the
|
|
172
|
+
* task — so the answer shows what was persisted, not what was asked. Status is always in the ack already.
|
|
173
|
+
*/
|
|
174
|
+
export function savedFields(t, named) {
|
|
175
|
+
const out = {};
|
|
176
|
+
if (named.assignee !== undefined)
|
|
177
|
+
out.assignee = t.assignee;
|
|
178
|
+
if (named.priority !== undefined)
|
|
179
|
+
out.priority = t.priority;
|
|
180
|
+
if (named.result !== undefined)
|
|
181
|
+
out.result = t.result;
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
156
184
|
export function normalizeStatusFilter(v) {
|
|
157
185
|
if (typeof v === "string") {
|
|
158
186
|
const s = v.trim();
|
|
@@ -182,19 +210,23 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
182
210
|
}
|
|
183
211
|
import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
184
212
|
import { windowsCliEnabled, closeWindowsBridge, inventoryProbed, LOCAL_WINDOWS_CLI_OFF } from "./windows-cli.js";
|
|
213
|
+
import { nativeCliVersionState } from "./runtime-pane-native.js";
|
|
185
214
|
import { localWindowsCliEnabled, remoteRuntimeEnabled, setConfiguredRuntimeSwitches, maxAgentsPerMachine } from "./runtime-switches.js";
|
|
215
|
+
import { RuntimeInstallService } from "./runtime-install.js";
|
|
216
|
+
import { RUNTIME_CREDENTIAL_CAPABILITY } from "./runtime-credential-profiles.js";
|
|
186
217
|
import { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
187
218
|
import { RuntimeWindowsBridge } from "./runtime-windows-bridge.js";
|
|
188
219
|
import { FleetWindowsRuntime } from "./fleet-windows-runtime.js";
|
|
189
220
|
import { NativeMcpSessions } from "./native-mcp.js";
|
|
190
221
|
import { nativeProviderMcpSupported } from "./windows-cli-providers.js";
|
|
191
|
-
import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED } from "./native-instance-registry.js";
|
|
222
|
+
import { isWindowsInstance, isNativePaneInstance, isNativeExecInstance, WINDOWS_CONFIG_REFUSED, nativeCredentialProfile } from "./native-instance-registry.js";
|
|
192
223
|
import { createNativeWindowsInstance, updateNativeWindowsInstance } from "./native-instance-api.js";
|
|
193
224
|
import { NativeInstanceConfigEdit, parseNativeInstanceCreate } from "./native-instance-input.js";
|
|
194
225
|
import { LogService } from "./log-service.js";
|
|
226
|
+
import { viewHistory } from "./view-history.js";
|
|
195
227
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
|
|
196
228
|
import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
|
|
197
|
-
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
229
|
+
import { WebChatDiskStore, isSafeInstanceDirName } from "./web-chat-store.js";
|
|
198
230
|
import { SeenUsers, resolveMentions } from "./web-mentions.js";
|
|
199
231
|
import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
|
|
200
232
|
import { agendVersion } from "./agent-whats-new.js";
|
|
@@ -224,8 +256,8 @@ import { envFileKeys } from "./token-env-name.js";
|
|
|
224
256
|
import { discordGeneralChannelId } from "./discord-general.js";
|
|
225
257
|
import { parseEnvText } from "./env-file.js";
|
|
226
258
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
227
|
-
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
228
|
-
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
259
|
+
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, setCapacityLookup, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
260
|
+
import { LOGIN_FLOWS, checkAuthStatus, checkAuthIdentity, authIdentitySupported, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
229
261
|
import { LoginSession } from "./login-manager.js";
|
|
230
262
|
import { runOnLoginShell } from "./login-shell.js";
|
|
231
263
|
import { tightenInstanceDirs } from "./private-dir.js";
|
|
@@ -255,6 +287,8 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
|
|
|
255
287
|
import { recentChatContext } from "./log-tail.js";
|
|
256
288
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
257
289
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
290
|
+
import { AgentCredentials, agentCredentialPolicy } from "./agent-credentials.js";
|
|
291
|
+
import { writeSecretFile } from "./secret-file.js";
|
|
258
292
|
import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
|
|
259
293
|
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
|
|
260
294
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
@@ -285,13 +319,14 @@ import { CompactWatch, launchIdentity } from "./compact-watch.js";
|
|
|
285
319
|
import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
|
|
286
320
|
import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
|
|
287
321
|
import { sharedRolloutIndex } from "./rollout-index.js";
|
|
288
|
-
import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
|
|
322
|
+
import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardOffIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
|
|
289
323
|
import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
|
|
290
324
|
import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
|
|
291
325
|
import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
|
|
292
326
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
293
|
-
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
327
|
+
import { guestSessionPolicy, localSessionPolicy, sessionVia, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
294
328
|
import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
|
|
329
|
+
import { DeviceLoginRequests } from "./web-device-login.js";
|
|
295
330
|
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";
|
|
296
331
|
import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
297
332
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
@@ -314,9 +349,10 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
314
349
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
315
350
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
316
351
|
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
317
|
-
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
352
|
+
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileEnv, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
|
|
318
353
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
319
|
-
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
354
|
+
import { classicProfile, mergeBackendOptions, profileKey, profileName, profileOf, readClassicBindings } from "./classic-bindings.js";
|
|
355
|
+
import { ACCOUNT_CLIS, PROFILE_CLIS, buildAccountsView, profilesOnDisk, weeklyUsage } from "./accounts-view.js";
|
|
320
356
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
321
357
|
import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
|
|
322
358
|
import { FleetStartup } from "./fleet-startup.js";
|
|
@@ -330,6 +366,12 @@ import { BackendOutageTracker } from "./backend-outage.js";
|
|
|
330
366
|
import { canUnlockAdvancedTips, DailyTipScheduler, selectTip, visibleTipLevels, } from "./tips.js";
|
|
331
367
|
import { getTmuxSession } from "./config.js";
|
|
332
368
|
import { OwnStatusMessages, discordMessageKey } from "./chat-log-ids.js";
|
|
369
|
+
import { PROFILE_TIMELINE_FILE, PROFILE_UNRECORDED, recordProfileLaunch } from "./usage/profile-timeline.js";
|
|
370
|
+
import { readQuotaHistory } from "./usage/quota-history.js";
|
|
371
|
+
import { estimateCapacity } from "./usage/capacity.js";
|
|
372
|
+
import { covered, hourSources } from "./usage-rollup.js";
|
|
373
|
+
import { KEEP_DAYS } from "./cache-ledger.js";
|
|
374
|
+
import { ratesFor } from "./cache-prices.js";
|
|
333
375
|
// Closed migration cohort: these 11 names existed immediately before the first
|
|
334
376
|
// managed-skill publisher (a1ac499f; parent e267e684). Never grow this set when
|
|
335
377
|
// adding a bundled skill: its new name is not proof of old AgEnD ownership.
|
|
@@ -481,6 +523,8 @@ const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
481
523
|
* while anything real shows signs of life well before the old two minutes.
|
|
482
524
|
*/
|
|
483
525
|
const PROGRESS_MIN_ELAPSED_MS = 30_000;
|
|
526
|
+
/** M-2391: the threshold tick fires this long after it, never a millisecond before (timers can fire early). */
|
|
527
|
+
const PROGRESS_THRESHOLD_MARGIN_MS = 50;
|
|
484
528
|
/** How much of a tool summary the progress line will show before eliding. */
|
|
485
529
|
const PROGRESS_ACTIVITY_MAX_CHARS = 48;
|
|
486
530
|
/**
|
|
@@ -767,6 +811,9 @@ export class FleetManager {
|
|
|
767
811
|
localPaneRelay = null;
|
|
768
812
|
openingLocalPane = null;
|
|
769
813
|
runtimeHub = null;
|
|
814
|
+
runtimeInstall;
|
|
815
|
+
/** One service for the public installer routes and the administrator API: the hash the dashboard shows is the hash of what is sent. */
|
|
816
|
+
runtimeInstaller() { return this.runtimeInstall ??= new RuntimeInstallService(); }
|
|
770
817
|
runtimeExecutor = null;
|
|
771
818
|
runtimeStatusScheduled = false;
|
|
772
819
|
reconciledRemoteResults = new Set();
|
|
@@ -777,6 +824,7 @@ export class FleetManager {
|
|
|
777
824
|
return this.runtimeHub ??= new RuntimeCoordinator(this.dataDir, {
|
|
778
825
|
handleMcp: (machineId, runId, body) => this.runtimeWindowsExecutor().mcp(machineId, runId, body),
|
|
779
826
|
handlePaneMcp: async (machineId, body, current) => (await this.nativePanes()).handle(machineId, body, current),
|
|
827
|
+
warn: (fields, message) => this.logger.warn(fields, message),
|
|
780
828
|
changed: event => {
|
|
781
829
|
if (event?.type === "machine" && this.runtimeHub?.machine(event.machineId)?.status !== "online") {
|
|
782
830
|
for (const [name, config] of Object.entries(this.fleetConfig?.instances ?? {}))
|
|
@@ -817,6 +865,8 @@ export class FleetManager {
|
|
|
817
865
|
nativeMcpSessions() {
|
|
818
866
|
return this.nativeMcpStore ??= new NativeMcpSessions(this);
|
|
819
867
|
}
|
|
868
|
+
/** Retire only existing native capabilities; ordinary lifecycle changes do not instantiate the Windows path. */
|
|
869
|
+
invalidateNativeMcp(name) { this.nativeMcpStore?.closeInstance(name); }
|
|
820
870
|
/** The same instance registry and delivery entry point, with a native Windows execution adapter. */
|
|
821
871
|
nativeInstanceRuntime() {
|
|
822
872
|
return this.fleetWindowsRuntime ??= new FleetWindowsRuntime({
|
|
@@ -827,13 +877,25 @@ export class FleetManager {
|
|
|
827
877
|
removed: name => this.nativeInstanceRemoved(name),
|
|
828
878
|
paneCurrent: name => this.daemons.get(name)?.nativePaneCurrent?.() === true,
|
|
829
879
|
machineAvailable: id => remoteRuntimeEnabled() && this.runtimeCoordinator().machine(id)?.status === "online",
|
|
830
|
-
|
|
880
|
+
machineGeneration: id => this.runtimeCoordinator().machine(id)?.bootId ?? null,
|
|
881
|
+
paneAvailable: id => this.runtimeCoordinator().machine(id)?.paneProtocol === 1,
|
|
882
|
+
validateMachine: (id, cli, sandbox, credentialProfile) => {
|
|
831
883
|
const machine = this.runtimeCoordinator().machine(id);
|
|
832
884
|
const provider = machine?.inventory?.clis.find(row => row.id === cli && row.runnable && row.supports.resume);
|
|
833
885
|
if (!machine || machine.status !== "online" || machine.platform !== "win32" || !provider)
|
|
834
886
|
throw new Error("Choose an online runtime and an available CLI");
|
|
835
887
|
if (sandbox === "read-only" && !provider.readOnlySupport)
|
|
836
888
|
throw new Error("This runtime CLI cannot enforce read-only execution");
|
|
889
|
+
if (credentialProfile !== undefined) {
|
|
890
|
+
// Fail closed at selection time, not at the first message: an old Runtime would ignore the name and run another account.
|
|
891
|
+
if (!machine.runtimeInfo?.capabilities.includes(RUNTIME_CREDENTIAL_CAPABILITY))
|
|
892
|
+
throw new Error("This computer's Runtime is too old to choose a Claude account. Update the Runtime on that computer, then try again.");
|
|
893
|
+
const row = machine.inventory?.credentialProfiles?.profiles.find(profile => profile.name === credentialProfile);
|
|
894
|
+
if (!row)
|
|
895
|
+
throw new Error(`This computer has not reported a credential profile named "${credentialProfile}". On that computer run: agend-runtime login claude-code ${credentialProfile}, then choose Detect again in that computer's CLI diagnostics (Settings → Runtime).`);
|
|
896
|
+
if (row.loggedIn === false)
|
|
897
|
+
throw new Error(`Credential profile "${credentialProfile}" is not signed in on that computer. Sign in there: agend-runtime login claude-code ${credentialProfile}`);
|
|
898
|
+
}
|
|
837
899
|
},
|
|
838
900
|
emit: (event, data) => {
|
|
839
901
|
this.emitSseEvent(event, event === "status" ? this.getUiStatus() : data);
|
|
@@ -842,6 +904,152 @@ export class FleetManager {
|
|
|
842
904
|
},
|
|
843
905
|
}, { bridge: this.runtimeWindowsExecutor() });
|
|
844
906
|
}
|
|
907
|
+
agentRuntimeChangeService;
|
|
908
|
+
agentRuntimeChanges() {
|
|
909
|
+
if (this.agentRuntimeChangeService)
|
|
910
|
+
return this.agentRuntimeChangeService;
|
|
911
|
+
const windowPins = new Map();
|
|
912
|
+
return this.agentRuntimeChangeService ??= new AgentRuntimeChanges({
|
|
913
|
+
config: name => Object.hasOwn(this.fleetConfig?.instances ?? {}, name) ? this.fleetConfig.instances[name] : undefined,
|
|
914
|
+
defaults: () => this.fleetConfig?.defaults,
|
|
915
|
+
generation: () => [this.managerBootId, this.settingsGeneration],
|
|
916
|
+
epoch: name => this.lifecycle.epochOf(name),
|
|
917
|
+
backendState: (name, backend) => !isWindowsInstance(this.fleetConfig?.instances[name]) ? this.backendAvailabilityFor(name).find(row => row.backend === backend)?.state : undefined,
|
|
918
|
+
transition: (name, run) => this.lifecycle.runTransition(name, async (transition) => {
|
|
919
|
+
const config = this.fleetConfig?.instances[name];
|
|
920
|
+
if (!isWindowsInstance(config)) {
|
|
921
|
+
const path = join(this.getInstanceDir(name), "window-id");
|
|
922
|
+
let pin;
|
|
923
|
+
if (existsSync(path)) {
|
|
924
|
+
if (statSync(path).size > 128)
|
|
925
|
+
throw Error("Invalid saved CLI window identity");
|
|
926
|
+
pin = readFileSync(path, "utf8").trim();
|
|
927
|
+
if (!/^@\d+$/.test(pin))
|
|
928
|
+
throw Error("Invalid saved CLI window identity");
|
|
929
|
+
}
|
|
930
|
+
else if (this.daemons.has(name))
|
|
931
|
+
throw Error("The current CLI window identity is unavailable");
|
|
932
|
+
windowPins.set(name, pin);
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
return await run(async () => {
|
|
936
|
+
if (isNativeExecInstance(this.fleetConfig?.instances[name])) {
|
|
937
|
+
this.lifecycle.invalidate(name);
|
|
938
|
+
this.nativeInstanceRuntime().setPaused(name, true);
|
|
939
|
+
}
|
|
940
|
+
else
|
|
941
|
+
await this.stopInstance(name, transition);
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
finally {
|
|
945
|
+
windowPins.delete(name);
|
|
946
|
+
}
|
|
947
|
+
}),
|
|
948
|
+
idle: name => {
|
|
949
|
+
if (this.shuttingDown || this.lifecycle.hasWorkLease(name) || this.deliveryOutbox?.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending", "uncertain"]))
|
|
950
|
+
return false;
|
|
951
|
+
const config = this.fleetConfig?.instances[name];
|
|
952
|
+
if (!config)
|
|
953
|
+
return false;
|
|
954
|
+
if (isWindowsInstance(config)) {
|
|
955
|
+
try {
|
|
956
|
+
const agent = this.nativeInstanceRuntime().get(name);
|
|
957
|
+
if (!agent || agent.blocked || agent.activeId && !isNativePaneInstance(config))
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
catch {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (!this.daemons.has(name) && !isWindowsInstance(config) && existsSync(join(this.getInstanceDir(name), "window-id")))
|
|
965
|
+
return false;
|
|
966
|
+
return !this.daemons.has(name) || this.getInstanceExecutionState(name) === "idle";
|
|
967
|
+
},
|
|
968
|
+
prepare: async (name, before, target, current) => {
|
|
969
|
+
if (!current())
|
|
970
|
+
throw Error("Runtime change authority expired");
|
|
971
|
+
if (isWindowsInstance(before))
|
|
972
|
+
this.nativeInstanceRuntime().importHistory(name);
|
|
973
|
+
if (target.runtime === "windows")
|
|
974
|
+
return this.nativeInstanceRuntime().prepareRuntimeChange(name, before, target, current);
|
|
975
|
+
const sameAccount = !isWindowsInstance(before) && target.backend === (before.backend || this.fleetConfig?.defaults?.backend || "claude-code");
|
|
976
|
+
const availability = (sameAccount ? this.backendAvailabilityFor(name) : this.backendAvailabilityRows()).find(row => row.backend === target.backend);
|
|
977
|
+
if (availability && ["not_installed", "not_signed_in", "too_old"].includes(availability.state))
|
|
978
|
+
throw Error("Choose an available CLI on the Fleet host");
|
|
979
|
+
const supplied = target.directory.trim(), cwd = supplied || join(this.dataDir, "workspaces", name);
|
|
980
|
+
if (!isAbsolute(cwd) || /^[A-Za-z]:[\\/]/.test(cwd) || /[\x00-\x1f]/.test(cwd))
|
|
981
|
+
throw Error("Enter an absolute Linux working directory or leave it empty");
|
|
982
|
+
if (!current())
|
|
983
|
+
throw Error("Runtime change authority expired");
|
|
984
|
+
if (supplied) {
|
|
985
|
+
if (!statSync(cwd).isDirectory())
|
|
986
|
+
throw Error("The Linux working directory is not a directory");
|
|
987
|
+
}
|
|
988
|
+
else
|
|
989
|
+
mkdirSync(cwd, { recursive: true, mode: 0o700 });
|
|
990
|
+
const config = { ...before, runtime: "wsl", working_directory: cwd, backend: target.backend, model: target.model };
|
|
991
|
+
if (before.backend !== target.backend)
|
|
992
|
+
config.model_failover = [];
|
|
993
|
+
for (const key of ["machine_id", "native_agent_id", "native_adapter", "native_sandbox"])
|
|
994
|
+
delete config[key];
|
|
995
|
+
if (!sameAccount)
|
|
996
|
+
delete config.backend_options;
|
|
997
|
+
return { config, discard: () => { } };
|
|
998
|
+
},
|
|
999
|
+
closed: async (name, before) => {
|
|
1000
|
+
if (this.daemons.has(name))
|
|
1001
|
+
return false;
|
|
1002
|
+
if (isWindowsInstance(before))
|
|
1003
|
+
return !this.nativeInstanceRuntime().get(name)?.activeId;
|
|
1004
|
+
// An unreadable tmux state is not evidence that the old process exited.
|
|
1005
|
+
const session = getTmuxSession();
|
|
1006
|
+
if (!await TmuxManager.sessionExistsStrict(session))
|
|
1007
|
+
return true;
|
|
1008
|
+
const pin = windowPins.get(name);
|
|
1009
|
+
return !(await TmuxManager.listWindowsStrict(session)).some(window => window.name === name || pin && window.id === pin);
|
|
1010
|
+
},
|
|
1011
|
+
fresh: (name, requestId) => {
|
|
1012
|
+
const dir = this.getInstanceDir(name), moved = [];
|
|
1013
|
+
const restore = () => { for (const [source, saved] of moved.reverse())
|
|
1014
|
+
if (!existsSync(source) && existsSync(saved))
|
|
1015
|
+
renameSync(saved, source); };
|
|
1016
|
+
try {
|
|
1017
|
+
for (const file of ["session-id", "session-id.backend"]) {
|
|
1018
|
+
const source = join(dir, file), saved = join(dir, `${file}.runtime-${requestId}`);
|
|
1019
|
+
if (existsSync(source)) {
|
|
1020
|
+
if (existsSync(saved))
|
|
1021
|
+
throw Error("Session backup already exists");
|
|
1022
|
+
renameSync(source, saved);
|
|
1023
|
+
moved.push([source, saved]);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return restore;
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
restore();
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
save: (name, before, next) => {
|
|
1034
|
+
if (this.fleetConfig?.instances[name] !== before)
|
|
1035
|
+
throw Error("Agent settings changed before saving Runtime");
|
|
1036
|
+
// Mark the replacement stopped before committing its config. A marker failure must not leave a saved
|
|
1037
|
+
// Windows config referring to a profile the preparation rollback would then remove.
|
|
1038
|
+
writePausedMarker(this.getInstanceDir(name), Date.now(), "operator");
|
|
1039
|
+
const fleet = this.fleetConfig;
|
|
1040
|
+
fleet.instances[name] = next;
|
|
1041
|
+
try {
|
|
1042
|
+
this.saveFleetConfig();
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
fleet.instances[name] = before;
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
// A move deliberately leaves the replacement stopped; its next explicit Start opens the fresh CLI.
|
|
1049
|
+
},
|
|
1050
|
+
changed: name => { this.instanceStateCache.delete(name); this.instanceProcessStatus.delete(name); this.emitSseEvent("status", this.getUiStatus()); },
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
845
1053
|
/** Explicit dashboard opt-in, with the same General confirmation and ordinary topic/Daemon routing. */
|
|
846
1054
|
async createNativePaneInstance(profile, channelId, authorized, execution) {
|
|
847
1055
|
const { createNativePane } = await import("./native-pane-create.js");
|
|
@@ -915,6 +1123,26 @@ export class FleetManager {
|
|
|
915
1123
|
return result;
|
|
916
1124
|
});
|
|
917
1125
|
}
|
|
1126
|
+
/** The local machine's inventory as last used to open its pane relay (status only; never a launch decision). */
|
|
1127
|
+
localPaneInventory = null;
|
|
1128
|
+
nativeCliStatus(cli, machineId, daemon) {
|
|
1129
|
+
if (cli !== "codex" && cli !== "claude-code")
|
|
1130
|
+
return {};
|
|
1131
|
+
// The version the running pane actually launched; only without one, the machine's last inventory.
|
|
1132
|
+
let running;
|
|
1133
|
+
try {
|
|
1134
|
+
running = daemon?.usesNativePane?.() ? daemon.nativePaneCliVersion() : undefined;
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
running = undefined;
|
|
1138
|
+
}
|
|
1139
|
+
const inventory = machineId ? this.runtimeHub?.machine(machineId)?.inventory : this.localPaneInventory;
|
|
1140
|
+
const version = running ?? inventory?.clis?.find(item => item.id === cli && item.runnable)?.version;
|
|
1141
|
+
if (version === undefined || version === null)
|
|
1142
|
+
return {};
|
|
1143
|
+
const result = nativeCliVersionState(cli, version);
|
|
1144
|
+
return { ...(result.version ? { native_cli_version: result.version } : {}), native_cli_verification: result.state };
|
|
1145
|
+
}
|
|
918
1146
|
async localNativePaneEndpoint() {
|
|
919
1147
|
const admit = () => { if (this.shuttingDown || this.localPaneDrain || !localWindowsCliEnabled())
|
|
920
1148
|
throw Error("Local Windows runtime is switched off or draining"); };
|
|
@@ -931,6 +1159,7 @@ export class FleetManager {
|
|
|
931
1159
|
]);
|
|
932
1160
|
admit();
|
|
933
1161
|
this.localPaneRelay?.stop();
|
|
1162
|
+
this.localPaneInventory = inventory;
|
|
934
1163
|
const relay = await LocalPaneEndpoint.open({ scope: localPaneScope(this.dataDir), admit, maxAgents: maxAgentsPerMachine,
|
|
935
1164
|
diagnostic: text => this.logger.warn({ stderr: text }, "Local Windows pane relay wrote to stderr"),
|
|
936
1165
|
version: provider => inventory.clis.find(row => row.id === provider && row.runnable)?.version ?? null,
|
|
@@ -979,8 +1208,12 @@ export class FleetManager {
|
|
|
979
1208
|
if (this.shuttingDown || !isNativePaneInstance(now) || now?.native_agent_id !== config.native_agent_id
|
|
980
1209
|
|| now?.machine_id !== config.machine_id || now?.working_directory !== config.working_directory || now?.backend !== config.backend
|
|
981
1210
|
|| now?.model !== config.model || now?.native_sandbox !== config.native_sandbox || now?.systemPrompt !== config.systemPrompt || now?.workflow !== config.workflow
|
|
982
|
-
|| now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding
|
|
1211
|
+
|| now?.general_topic !== config.general_topic || now?.native_general_binding !== config.native_general_binding
|
|
1212
|
+
|| nativeCredentialProfile(now) !== nativeCredentialProfile(config))
|
|
983
1213
|
throw Error("Native pane configuration or lifetime changed");
|
|
1214
|
+
// Fail closed: a Runtime that does not report the capability would start the terminal on the computer's own account.
|
|
1215
|
+
if (nativeCredentialProfile(config) && !this.runtimeCoordinator().machine(config.machine_id)?.runtimeInfo?.capabilities.includes(RUNTIME_CREDENTIAL_CAPABILITY))
|
|
1216
|
+
throw Error("This computer's Runtime is too old to choose a Claude account. Update the Runtime on that computer, then try again.");
|
|
984
1217
|
if (config.machine_id ? this.remotePaneDrain || !remoteRuntimeEnabled() : this.localPaneDrain || !localWindowsCliEnabled())
|
|
985
1218
|
throw Error("Native runtime is switched off");
|
|
986
1219
|
},
|
|
@@ -1039,12 +1272,12 @@ export class FleetManager {
|
|
|
1039
1272
|
throw Error("General or its paired runtime is unavailable");
|
|
1040
1273
|
const current = () => authorized() && !this.shuttingDown && remoteRuntimeEnabled() && this.runtimeHub === hub
|
|
1041
1274
|
&& this.fleetConfig?.instances[name] === config && hub.paneTransportEpoch(machineId) === epoch;
|
|
1042
|
-
const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, {
|
|
1275
|
+
const [{ loadGeneralKnowledge }, { validateGeneralWorkspaceRequest, validateGeneralWorkspaceReceipt, generalWorkspaceUpgrade }, { assertRunnableNativePaneVersion }] = await Promise.all([
|
|
1043
1276
|
import("./runtime-general-workspace.js"), import("./runtime-general-protocol.js"), import("./runtime-pane-native.js"),
|
|
1044
1277
|
]);
|
|
1045
1278
|
if (!current())
|
|
1046
1279
|
throw Error("General workspace authority expired");
|
|
1047
|
-
|
|
1280
|
+
assertRunnableNativePaneVersion(cli, hub.machine(machineId)?.inventory?.clis.find(row => row.id === cli && row.runnable)?.version ?? null);
|
|
1048
1281
|
const request = validateGeneralWorkspaceRequest({ action: "general-workspace", bindingId, instance: name, cli, packHash: loadGeneralKnowledge(cli).hash });
|
|
1049
1282
|
const inventory = await hub.callPane(machineId, { action: "list" }, maxAgentsPerMachine());
|
|
1050
1283
|
if (!current())
|
|
@@ -1058,6 +1291,14 @@ export class FleetManager {
|
|
|
1058
1291
|
throw new NativePaneRecoveryError({ kind: "host-upgrade" });
|
|
1059
1292
|
return validateGeneralWorkspaceReceipt(result, request);
|
|
1060
1293
|
}
|
|
1294
|
+
/** S3c (S-55): the member's exposure API, attached by S5's member composition (null: no Hub member here). */
|
|
1295
|
+
hubMemberExposureApi = null;
|
|
1296
|
+
hubMemberExposures() { return this.hubMemberExposureApi; }
|
|
1297
|
+
attachHubMemberExposures(api) { this.hubMemberExposureApi = api; }
|
|
1298
|
+
/** S3c PR B: the member's pairing API, attached by S5's member composition (null: no Hub member here). */
|
|
1299
|
+
hubMemberPairingApi = null;
|
|
1300
|
+
hubMemberPairing() { return this.hubMemberPairingApi; }
|
|
1301
|
+
attachHubMemberPairing(api) { this.hubMemberPairingApi = api; }
|
|
1061
1302
|
generalLocationFlights = new Map();
|
|
1062
1303
|
generalAttentionEpisodes = new Map();
|
|
1063
1304
|
generalBindings() { return new GeneralBindings(this.dataDir); }
|
|
@@ -1513,6 +1754,364 @@ export class FleetManager {
|
|
|
1513
1754
|
* them at once; what the fleet itself holds (paired computers' connections, this PC's Windows process, agents' tool
|
|
1514
1755
|
* sessions) follows just after, outside the save that changed them (applyRuntimeSwitches): no restart is needed.
|
|
1515
1756
|
*/
|
|
1757
|
+
/** S-51: the Hub switches follow the configuration in force (load, save, reload rollback), like the runtime ones. */
|
|
1758
|
+
observeHubSwitches() {
|
|
1759
|
+
this.observeHubAgentIncarnations();
|
|
1760
|
+
if (!this.hubEffectsReady || this.hubEffectsScheduled || this.shuttingDown)
|
|
1761
|
+
return;
|
|
1762
|
+
this.hubEffectsScheduled = true;
|
|
1763
|
+
queueMicrotask(() => { this.hubEffectsScheduled = false; void this.applyHubSwitches(); });
|
|
1764
|
+
}
|
|
1765
|
+
/** Reads the raw configuration at run time (the newest wins); off and never started: returns before any import. */
|
|
1766
|
+
async applyHubSwitches() {
|
|
1767
|
+
if (this.shuttingDown || !this.hubEffectsReady)
|
|
1768
|
+
return;
|
|
1769
|
+
const raw = this.rawFleetConfig;
|
|
1770
|
+
const on = raw?.hub?.enabled === true || raw?.hub_member?.enabled === true;
|
|
1771
|
+
if (!on && !this.hubRuntime && !this.hubRuntimeInit)
|
|
1772
|
+
return;
|
|
1773
|
+
try {
|
|
1774
|
+
const runtime = this.hubRuntime ?? (await this.hubRuntimeCreated());
|
|
1775
|
+
if (this.shuttingDown || !this.hubEffectsReady)
|
|
1776
|
+
return; // Stop owns it now (fenced in stopAll, closed in doStopAll)
|
|
1777
|
+
await this.observeHubMemberPairing(raw?.hub_member?.enabled === true);
|
|
1778
|
+
const current = this.rawFleetConfig;
|
|
1779
|
+
await runtime.apply({ hub: current?.hub, hub_member: current?.hub_member });
|
|
1780
|
+
}
|
|
1781
|
+
catch (err) {
|
|
1782
|
+
this.logger.warn({ err: err?.message ?? String(err) }, "Hub switches could not be applied");
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
/** Single flight: one import, one runtime, assigned once; every caller (and Stop) gets that same runtime. */
|
|
1786
|
+
hubRuntimeCreated() {
|
|
1787
|
+
this.hubRuntimeInit ??= (async () => {
|
|
1788
|
+
const { HubRuntime } = await import("./hub/runtime.js");
|
|
1789
|
+
const runtime = new HubRuntime({
|
|
1790
|
+
home: this.dataDir,
|
|
1791
|
+
log: (level, message, meta) => this.logger[level](meta ?? {}, message),
|
|
1792
|
+
onHoldsChanged: holds => { this.hubHolds = holds; this.needsYou?.poke(); },
|
|
1793
|
+
// S-51 PR 2b: the fleet's side of a Hub membership (pairing binding, local agents, attachment points).
|
|
1794
|
+
member: {
|
|
1795
|
+
// S3c's currentBinding: a fresh, fail-closed read of the pairing state (null unless consistent and connectable).
|
|
1796
|
+
boundHub: () => this.hubMemberBinding?.(this.dataDir)?.hubId ?? null,
|
|
1797
|
+
agents: () => this.hubMemberAgents(),
|
|
1798
|
+
// P1 representative mode (docs/design/hub-representative-agent.md): ordinary delivery + the report_result hook.
|
|
1799
|
+
representative: {
|
|
1800
|
+
quota: agent => this.hubAgentQuota(agent),
|
|
1801
|
+
deliver: input => this.deliverHubQuestion(input),
|
|
1802
|
+
agentToken: agent => this.hubAgentToken(agent),
|
|
1803
|
+
// attachments-v1: a question's files go into the agent's inbox; an answer's come from its own paths.
|
|
1804
|
+
files: { stateDir: this.dataDir,
|
|
1805
|
+
inboxDir: agent => isSafeInstanceDirName(agent) ? join(this.dataDir, "workspaces", agent, "inbox") : null,
|
|
1806
|
+
scratchDir: agent => isSafeInstanceDirName(agent) ? join(this.dataDir, "workspaces", agent, "hub-files") : null },
|
|
1807
|
+
attachReports: (handler, failed) => { this.hubReports = handler; this.hubDeliveryFailed = failed; },
|
|
1808
|
+
},
|
|
1809
|
+
attachExposures: api => this.attachHubMemberExposures(api),
|
|
1810
|
+
attachAttention: read => this.attachHubExecutionAttention(read),
|
|
1811
|
+
pokeAttention: () => this.needsYou?.poke(),
|
|
1812
|
+
linkState: state => { this.hubMemberLink = state; },
|
|
1813
|
+
},
|
|
1814
|
+
});
|
|
1815
|
+
// Stop began during the import: it holds this promise and closes the runtime; do not hand it out again.
|
|
1816
|
+
if (this.shuttingDown || !this.hubEffectsReady)
|
|
1817
|
+
runtime.fence();
|
|
1818
|
+
else
|
|
1819
|
+
this.hubRuntime = runtime;
|
|
1820
|
+
return runtime;
|
|
1821
|
+
})();
|
|
1822
|
+
return this.hubRuntimeInit;
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* S-51 PR 2b: while the member switch is on, the local owner's "Connect to a Hub" backend (S3c's pairing API) is
|
|
1826
|
+
* attached, paired or not; switched off, it is detached. A binding change re-composes the member side.
|
|
1827
|
+
*/
|
|
1828
|
+
async observeHubMemberPairing(on) {
|
|
1829
|
+
if (!on) {
|
|
1830
|
+
this.attachHubMemberPairing(null);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (this.hubMemberPairingApi)
|
|
1834
|
+
return;
|
|
1835
|
+
const { createHubMemberPairingApi, currentBinding } = await import("./hub/member/pairing-api.js");
|
|
1836
|
+
if (this.shuttingDown || !this.hubEffectsReady || this.hubMemberPairingApi)
|
|
1837
|
+
return;
|
|
1838
|
+
this.hubMemberBinding = currentBinding;
|
|
1839
|
+
this.attachHubMemberPairing(createHubMemberPairingApi({
|
|
1840
|
+
home: this.dataDir,
|
|
1841
|
+
enabled: () => this.rawFleetConfig?.hub_member?.enabled === true,
|
|
1842
|
+
link: () => (this.hubMemberLink === "online" ? "online" : "offline"),
|
|
1843
|
+
hold: () => (this.hubMemberLink === "revoked" ? "revoked" : null),
|
|
1844
|
+
// Disconnect: stop the member client now (the runtime re-composes from the saved binding, fencing first).
|
|
1845
|
+
fence: () => { void this.hubRuntime?.refresh(); },
|
|
1846
|
+
// A claim saved, a disconnect tombstone or a cancel: re-compose the member side for the new binding.
|
|
1847
|
+
bindingChanged: () => { void this.hubRuntime?.refresh(); },
|
|
1848
|
+
// Disconnect withdraws every published exposure locally (each saved before this returns).
|
|
1849
|
+
withdrawAll: () => this.hubMemberWithdrawAll(),
|
|
1850
|
+
}));
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Disconnect: every published exposure is withdrawn and saved before the disconnect reports success. Through the
|
|
1854
|
+
* running member's API, or — held or not composed — directly in the member's own store; a failure here makes the
|
|
1855
|
+
* disconnect report "unsaved" (S3c), so consent never outlives the disconnect.
|
|
1856
|
+
*/
|
|
1857
|
+
async hubMemberWithdrawAll() {
|
|
1858
|
+
const api = this.hubMemberExposureApi;
|
|
1859
|
+
if (api) {
|
|
1860
|
+
for (const view of api.list()) {
|
|
1861
|
+
if (view.exposureId && view.state === "published")
|
|
1862
|
+
await api.withdraw(view.exposureId, view.revision);
|
|
1863
|
+
}
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const hubId = this.hubMemberPairingApi?.boundHub()?.hubId ?? null;
|
|
1867
|
+
if (!hubId)
|
|
1868
|
+
return;
|
|
1869
|
+
const { withdrawMemberExposuresOffline } = await import("./hub/compose/member.js");
|
|
1870
|
+
await withdrawMemberExposuresOffline(this.dataDir, hubId);
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* The Hub exposure picker's agents (docs/design/hub-representative-agent.md): every configured instance normal
|
|
1874
|
+
* delivery addresses, General included, shown by its display name. The dedicated Hub daemon is not a fleet instance.
|
|
1875
|
+
*/
|
|
1876
|
+
hubMemberAgents() {
|
|
1877
|
+
return Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({ name, display: this.shownInstanceName(name) }));
|
|
1878
|
+
}
|
|
1879
|
+
/** Same cached usage source as the owner's rail, scoped to the representative's CLI and credential profile. */
|
|
1880
|
+
async hubAgentQuota(agent) {
|
|
1881
|
+
const binding = () => {
|
|
1882
|
+
const config = this.fleetConfig?.instances[agent];
|
|
1883
|
+
if (!config || isWindowsInstance(config))
|
|
1884
|
+
return null; // runner usage never belongs to a Linux login
|
|
1885
|
+
const backend = this.backendNameForInstance(agent), provider = usageProviderIdForBackend(backend);
|
|
1886
|
+
if (!provider)
|
|
1887
|
+
return null;
|
|
1888
|
+
const profile = this.credentialProfileOf(agent, backend);
|
|
1889
|
+
return profile ? `${provider}:${profile}` : provider;
|
|
1890
|
+
};
|
|
1891
|
+
const id = binding();
|
|
1892
|
+
const { quotaBand } = await import("./hub/member/quota.js");
|
|
1893
|
+
const paused = () => this.daemons.get(agent)?.quotaPaused?.() === true
|
|
1894
|
+
|| this.lifecycle.isPaused(agent) && readPauseReason(this.getInstanceDir(agent)) === "quota";
|
|
1895
|
+
if (!id)
|
|
1896
|
+
return { band: "unknown" };
|
|
1897
|
+
try {
|
|
1898
|
+
const snapshot = await getUsageSnapshot(false, new Set([id]));
|
|
1899
|
+
if (binding() !== id)
|
|
1900
|
+
return { band: "unknown" };
|
|
1901
|
+
return quotaBand(snapshot.providers.find(p => p.id === id), paused());
|
|
1902
|
+
}
|
|
1903
|
+
catch {
|
|
1904
|
+
return quotaBand(undefined, paused());
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* A published representative's lifecycle token (#3145, #3147 review): a DURABLE incarnation id per agent name, rotated
|
|
1909
|
+
* whenever its backend, folder or runtime changes and dropped when the instance is removed. A replacement (remove and
|
|
1910
|
+
* re-add, even with the same name and config, across restarts) or any retarget — including A→B→A — gets a new id, so
|
|
1911
|
+
* earlier consent never matches again without an explicit republish. Stopping or pausing changes nothing.
|
|
1912
|
+
*/
|
|
1913
|
+
hubAgentToken(agent) {
|
|
1914
|
+
const instances = this.fleetConfig?.instances ?? {};
|
|
1915
|
+
if (!Object.prototype.hasOwnProperty.call(instances, agent))
|
|
1916
|
+
return null;
|
|
1917
|
+
if (this.hubIncarnationsBroken)
|
|
1918
|
+
return null;
|
|
1919
|
+
let id;
|
|
1920
|
+
try {
|
|
1921
|
+
this.settleHubIncarnations();
|
|
1922
|
+
id = this.hubIncarnationsBroken ? null : this.hubAgentIncarnation(agent, instances[agent], true);
|
|
1923
|
+
}
|
|
1924
|
+
catch (err) {
|
|
1925
|
+
this.logger.warn({ err: err?.message, agent }, "Hub agent incarnation could not be saved; the agent is not a deliverable representative");
|
|
1926
|
+
return null;
|
|
1927
|
+
}
|
|
1928
|
+
return id ? createHash("sha256").update(JSON.stringify(["agend-hub-agent-v2", agent, id])).digest("hex") : null;
|
|
1929
|
+
}
|
|
1930
|
+
hubIncarnationsPath() { return join(this.dataDir, "hub-member", "agent-incarnations.json"); }
|
|
1931
|
+
/**
|
|
1932
|
+
* M-2451 (#3147 residual): written beside hub-member/, in the data directory itself, when an invalidation could not be
|
|
1933
|
+
* made durable in hub-member/. A later process that finds it invalidates every incarnation before handing out any
|
|
1934
|
+
* token, so a restart never revives consent the earlier process had to withdraw (A→B→A, or a removal, while hub-member/
|
|
1935
|
+
* was unwritable).
|
|
1936
|
+
*/
|
|
1937
|
+
hubIncarnationsMarkerPath() { return join(this.dataDir, "hub-member-invalidate.pending"); }
|
|
1938
|
+
/** Whether this process has settled the incarnations on disk (the marker handled, the file proven writable). */
|
|
1939
|
+
hubIncarnationsSettled = false;
|
|
1940
|
+
/**
|
|
1941
|
+
* Once per process, before the first token: an invalidation an earlier process could not persist (the marker) clears
|
|
1942
|
+
* every incarnation; and an existing file must be rewritable here, else this process is broken (no token at all) —
|
|
1943
|
+
* an incarnation this process could not rotate must never be handed out. Fails closed: any doubt marks it broken.
|
|
1944
|
+
*/
|
|
1945
|
+
settleHubIncarnations() {
|
|
1946
|
+
if (this.hubIncarnationsSettled || this.hubIncarnationsBroken || !this.dataDir)
|
|
1947
|
+
return;
|
|
1948
|
+
const path = this.hubIncarnationsPath(), marker = this.hubIncarnationsMarkerPath();
|
|
1949
|
+
if (existsSync(marker)) {
|
|
1950
|
+
try {
|
|
1951
|
+
unlinkSync(path);
|
|
1952
|
+
}
|
|
1953
|
+
catch (err) {
|
|
1954
|
+
if (err?.code !== "ENOENT") {
|
|
1955
|
+
this.hubIncarnationsBroken = true;
|
|
1956
|
+
this.logger.warn({ err: err?.message }, "An earlier Hub consent invalidation is still pending and cannot be applied; no Hub representative token is handed out");
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
// The clear is durable; a marker that cannot be removed only clears again next time (over-invalidation, never revival).
|
|
1961
|
+
try {
|
|
1962
|
+
unlinkSync(marker);
|
|
1963
|
+
}
|
|
1964
|
+
catch { /* left: harmless */ }
|
|
1965
|
+
this.logger.warn("An earlier Hub consent invalidation was applied: every Hub representative needs a republish");
|
|
1966
|
+
}
|
|
1967
|
+
if (existsSync(path))
|
|
1968
|
+
this.writeHubIncarnations(this.readHubIncarnations()); // throws (broken) when hub-member/ is not writable
|
|
1969
|
+
this.hubIncarnationsSettled = true;
|
|
1970
|
+
} /**
|
|
1971
|
+
* #3147 r2: set when an invalidation could not be made durable in any way (neither the atomic write nor removing the
|
|
1972
|
+
* file worked). From then on this process hands out no representative token at all, and the transitions the fleet
|
|
1973
|
+
* controls (a config save, a removal) are refused instead of applied.
|
|
1974
|
+
*/
|
|
1975
|
+
hubIncarnationsBroken = false;
|
|
1976
|
+
readHubIncarnations() {
|
|
1977
|
+
try {
|
|
1978
|
+
const value = JSON.parse(readFileSync(this.hubIncarnationsPath(), "utf8"));
|
|
1979
|
+
return value && typeof value === "object" && !Array.isArray(value) ? Object.assign(Object.create(null), value) : Object.create(null);
|
|
1980
|
+
}
|
|
1981
|
+
catch {
|
|
1982
|
+
return Object.create(null);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Persist the incarnation map: "written", or — when the atomic write fails — "cleared": the whole file is removed, which
|
|
1987
|
+
* durably invalidates EVERY earlier incarnation (all consent then needs a republish; never a revival). Throws (and
|
|
1988
|
+
* marks the process broken) only when neither is possible.
|
|
1989
|
+
*/
|
|
1990
|
+
writeHubIncarnations(map) {
|
|
1991
|
+
const path = this.hubIncarnationsPath(), tmp = `${path}.${process.pid}.tmp`;
|
|
1992
|
+
try {
|
|
1993
|
+
mkdirSync(join(this.dataDir, "hub-member"), { recursive: true, mode: 0o700 });
|
|
1994
|
+
writeFileSync(tmp, JSON.stringify(map), { mode: 0o600 });
|
|
1995
|
+
renameSync(tmp, path);
|
|
1996
|
+
return "written";
|
|
1997
|
+
}
|
|
1998
|
+
catch (err) {
|
|
1999
|
+
try {
|
|
2000
|
+
rmSync(tmp, { recursive: false, force: true });
|
|
2001
|
+
}
|
|
2002
|
+
catch { /* a directory planted there: left alone */ }
|
|
2003
|
+
try {
|
|
2004
|
+
unlinkSync(path);
|
|
2005
|
+
}
|
|
2006
|
+
catch (gone) {
|
|
2007
|
+
if (gone?.code !== "ENOENT") {
|
|
2008
|
+
this.hubIncarnationsBroken = true;
|
|
2009
|
+
// M-2451: record the withdrawal outside hub-member/, so a restart does not revive what this process withdrew.
|
|
2010
|
+
try {
|
|
2011
|
+
writeFileSync(this.hubIncarnationsMarkerPath(), "", { mode: 0o600 });
|
|
2012
|
+
}
|
|
2013
|
+
catch { /* the data directory too: nothing durable is left */ }
|
|
2014
|
+
throw err;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
this.logger.warn({ err: err?.message }, "Hub agent incarnations could not be saved; every earlier Hub consent is invalidated (republish needed)");
|
|
2018
|
+
return "cleared";
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
/** The agent's durable incarnation id (create: make one if none; else null). A changed fingerprint rotates it. */
|
|
2022
|
+
hubAgentIncarnation(agent, cfg, create) {
|
|
2023
|
+
const fp = createHash("sha256").update(JSON.stringify([cfg.backend ?? "claude-code", cfg.working_directory, cfg.runtime ?? "", cfg.machine_id ?? ""])).digest("hex");
|
|
2024
|
+
const map = this.readHubIncarnations(), entry = Object.prototype.hasOwnProperty.call(map, agent) ? map[agent] : undefined;
|
|
2025
|
+
if (entry && entry.fp === fp && typeof entry.id === "string")
|
|
2026
|
+
return entry.id;
|
|
2027
|
+
if (!entry && !create)
|
|
2028
|
+
return null;
|
|
2029
|
+
map[agent] = { id: randomUUID(), fp };
|
|
2030
|
+
// A rotation that could only clear the file still invalidated the old id durably; the new one is made next time.
|
|
2031
|
+
return this.writeHubIncarnations(map) === "written" ? map[agent].id : null;
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* Every configuration in force: rotate a recorded agent's incarnation when its backend/folder/runtime changed; drop
|
|
2035
|
+
* removed ones. strict (a config save the fleet is about to write): a failure to make that durable throws, so the save
|
|
2036
|
+
* is refused instead of applied. Otherwise (a load or reload of a hand-edited file) a failure is logged — the
|
|
2037
|
+
* invalidation then is the cleared file, or, if nothing could be written, this process hands out no token at all.
|
|
2038
|
+
*/
|
|
2039
|
+
observeHubAgentIncarnations(strict = false) {
|
|
2040
|
+
// Only against a loaded configuration: never forget every agent because none is loaded yet.
|
|
2041
|
+
if (!this.dataDir || !this.fleetConfig?.instances)
|
|
2042
|
+
return;
|
|
2043
|
+
try {
|
|
2044
|
+
this.settleHubIncarnations();
|
|
2045
|
+
if (this.hubIncarnationsBroken)
|
|
2046
|
+
throw new Error("Hub agent incarnations cannot be written here");
|
|
2047
|
+
if (!existsSync(this.hubIncarnationsPath()))
|
|
2048
|
+
return;
|
|
2049
|
+
const instances = this.fleetConfig.instances;
|
|
2050
|
+
for (const agent of Object.keys(this.readHubIncarnations())) {
|
|
2051
|
+
// Gone from the configuration in force (removed by hand in fleet.yaml, too): forgotten, never revived by a re-add.
|
|
2052
|
+
if (Object.prototype.hasOwnProperty.call(instances, agent))
|
|
2053
|
+
this.hubAgentIncarnation(agent, instances[agent], false);
|
|
2054
|
+
else
|
|
2055
|
+
this.forgetHubAgentIncarnation(agent, true);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
catch (err) {
|
|
2059
|
+
this.logger?.warn({ err: err?.message }, "Hub agent incarnations could not be checked");
|
|
2060
|
+
if (strict)
|
|
2061
|
+
throw new Error(`Hub consent for a changed agent could not be invalidated durably; the change is not saved (${err?.message})`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
/** A removed instance's incarnation goes with it (a later instance of the same name is a new one). strict: throw if not durable. */
|
|
2065
|
+
forgetHubAgentIncarnation(agent, strict = false) {
|
|
2066
|
+
try {
|
|
2067
|
+
if (!this.dataDir || !existsSync(this.hubIncarnationsPath()))
|
|
2068
|
+
return;
|
|
2069
|
+
const map = this.readHubIncarnations();
|
|
2070
|
+
if (!Object.prototype.hasOwnProperty.call(map, agent))
|
|
2071
|
+
return;
|
|
2072
|
+
delete map[agent];
|
|
2073
|
+
this.writeHubIncarnations(map);
|
|
2074
|
+
}
|
|
2075
|
+
catch (err) {
|
|
2076
|
+
this.logger.warn({ err: err?.message, agent }, "Hub agent incarnation could not be dropped");
|
|
2077
|
+
if (strict)
|
|
2078
|
+
throw new Error(`Hub consent for ${agent} could not be invalidated durably; refusing the change (${err?.message})`);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* A Hub question to its representative agent: ordinary cross-instance delivery (idle gate, wake, retries), from the
|
|
2083
|
+
* reserved Hub sender with the Hub-issued correlation id. beforeInput runs immediately before the IPC hand-off.
|
|
2084
|
+
*/
|
|
2085
|
+
async deliverHubQuestion(input) {
|
|
2086
|
+
const { HUB_REPORT_TARGET } = await import("./hub/representative.js");
|
|
2087
|
+
if (this.shuttingDown || !this.fleetConfig?.instances[input.agent])
|
|
2088
|
+
return false;
|
|
2089
|
+
// Windows agents run through their own runtime, which has no pre-input hook: not a P1 representative.
|
|
2090
|
+
if (isNativeExecInstance(this.fleetConfig.instances[input.agent]))
|
|
2091
|
+
return false;
|
|
2092
|
+
const ipcMeta = {
|
|
2093
|
+
chat_id: "",
|
|
2094
|
+
message_id: `hubmsg-${input.correlationId.slice(4)}`,
|
|
2095
|
+
user: `instance:${HUB_REPORT_TARGET}`,
|
|
2096
|
+
user_id: `instance:${HUB_REPORT_TARGET}`,
|
|
2097
|
+
ts: new Date().toISOString(),
|
|
2098
|
+
thread_id: "",
|
|
2099
|
+
from_instance: HUB_REPORT_TARGET,
|
|
2100
|
+
from_display: "Hub",
|
|
2101
|
+
correlation_id: input.correlationId,
|
|
2102
|
+
request_kind: "query",
|
|
2103
|
+
requires_reply: "true",
|
|
2104
|
+
task_summary: "A question from the Hub",
|
|
2105
|
+
};
|
|
2106
|
+
const result = await this.deliverToInstance(input.agent, { type: "fleet_inbound", targetSession: input.agent, content: input.text, meta: ipcMeta }, { isCrossInstance: true, stillCurrent: input.stillCurrent, beforeInput: input.beforeInput });
|
|
2107
|
+
return result !== false;
|
|
2108
|
+
}
|
|
2109
|
+
/** S-51: why the Hub (or this fleet's Hub membership) is not running, if it is held. Empty while off or running. */
|
|
2110
|
+
getHubHolds() {
|
|
2111
|
+
return this.hubHolds;
|
|
2112
|
+
}
|
|
2113
|
+
hubBrowserCertificate() { return this.hubRuntime?.browserCertificateSettings() ?? null; }
|
|
2114
|
+
hubRetention() { return this.hubRuntime?.retentionSettings() ?? null; }
|
|
1516
2115
|
observeRuntimeSwitches() {
|
|
1517
2116
|
setConfiguredRuntimeSwitches(this.fleetConfig?.runtimes);
|
|
1518
2117
|
if (!this.runtimeEffects || this.runtimeEffectsScheduled)
|
|
@@ -1581,6 +2180,28 @@ export class FleetManager {
|
|
|
1581
2180
|
return this.daemons.get(name)?.bootId ?? null;
|
|
1582
2181
|
}
|
|
1583
2182
|
rawFleetConfig = {};
|
|
2183
|
+
/**
|
|
2184
|
+
* S-51 (#3095, experimental, off by default): the Hub runtime. src/hub/runtime.js is imported only once a Hub switch
|
|
2185
|
+
* (hub.enabled / hub_member.enabled) is on; while both are off nothing Hub-related loads, opens or listens.
|
|
2186
|
+
*/
|
|
2187
|
+
hubRuntime = null;
|
|
2188
|
+
/** S3c's currentBinding reader, loaded with the pairing API (only while the member switch is on). */
|
|
2189
|
+
hubMemberBinding = null;
|
|
2190
|
+
/** The member link as the "Connect to a Hub" view shows it (S5's client lifecycle). */
|
|
2191
|
+
hubMemberLink = "offline";
|
|
2192
|
+
/** OutboundContext: the Hub representative's answer hook (null: no member service running, or not a Hub answer). */
|
|
2193
|
+
hubReport(input) {
|
|
2194
|
+
return this.hubReports?.(input) ?? null;
|
|
2195
|
+
}
|
|
2196
|
+
/** The member side's pane-failure hook for a Hub-issued correlation id (else null). */
|
|
2197
|
+
hubDeliveryFailed = null;
|
|
2198
|
+
/** The member side's report_result hook for HUB_REPORT_TARGET while a representative service runs (else null). */
|
|
2199
|
+
hubReports = null;
|
|
2200
|
+
/** The one in-flight creation (single flight): overlapping first observations share it, Stop waits for it. */
|
|
2201
|
+
hubRuntimeInit = null;
|
|
2202
|
+
hubEffectsReady = false;
|
|
2203
|
+
hubEffectsScheduled = false;
|
|
2204
|
+
hubHolds = [];
|
|
1584
2205
|
rawFleetDocument = null;
|
|
1585
2206
|
/** #2208: the paths a saveFleetConfig() wrote, for its check that nothing else changed. */
|
|
1586
2207
|
fleetDocumentWrites = null;
|
|
@@ -1730,6 +2351,7 @@ export class FleetManager {
|
|
|
1730
2351
|
startupRetryNotices = new Map();
|
|
1731
2352
|
/** Unsupported-CLI refusals waiting to go out, grouped by reason (#1109). */
|
|
1732
2353
|
unsupportedCliNotices = new Map();
|
|
2354
|
+
kiroCompatibilityHolds = new Map();
|
|
1733
2355
|
kiroIncompatNotices = new Map();
|
|
1734
2356
|
/** Backoff between automatic startup retries; the last step repeats while the backend is down. */
|
|
1735
2357
|
static STARTUP_RETRY_BACKOFF_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
|
@@ -1758,6 +2380,8 @@ export class FleetManager {
|
|
|
1758
2380
|
kiroMigrationRuns = new Map();
|
|
1759
2381
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
1760
2382
|
needsYou = null;
|
|
2383
|
+
hubExecutionAttention = null;
|
|
2384
|
+
missedOfflineStore = null;
|
|
1761
2385
|
configHistory = null;
|
|
1762
2386
|
configHistoryTimer = null;
|
|
1763
2387
|
loadedFleetSource = null;
|
|
@@ -1873,6 +2497,9 @@ export class FleetManager {
|
|
|
1873
2497
|
discordPresenceEagerPending = false;
|
|
1874
2498
|
discordPresenceInFlight = null;
|
|
1875
2499
|
static DISCORD_PRESENCE_REFRESH_MS = 15 * 60_000;
|
|
2500
|
+
/** #3066 §1d: how often a fleet with no Discord bot reads the quota for its history (the user's choice: 30 min). */
|
|
2501
|
+
static QUOTA_BACKGROUND_READ_MS = 30 * 60_000;
|
|
2502
|
+
quotaReadTimer = null;
|
|
1876
2503
|
/** Days of event/activity history to keep. */
|
|
1877
2504
|
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
1878
2505
|
watchdogTimer = null;
|
|
@@ -2068,6 +2695,8 @@ export class FleetManager {
|
|
|
2068
2695
|
/** #2855: a trusted guest's searches, per account. */
|
|
2069
2696
|
guestSearches = new OfficeWriteLimiter(undefined, { perWindow: GUEST_SEARCHES.perWindow, windowMs: GUEST_SEARCHES.windowMs });
|
|
2070
2697
|
officeSharesInstance;
|
|
2698
|
+
/** #2861: when each instance was last talked to and answered (no words), for share links' dialogue box. */
|
|
2699
|
+
officeTalk = new OfficeTalk();
|
|
2071
2700
|
/**
|
|
2072
2701
|
* #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
|
|
2073
2702
|
* their own (a manager and lease of its own: never the fleet's one tunnel lane nor the public link's preview tunnel).
|
|
@@ -2084,6 +2713,7 @@ export class FleetManager {
|
|
|
2084
2713
|
hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
|
|
2085
2714
|
skins: this.officeSkins,
|
|
2086
2715
|
levels: () => this.officeLevelsSnapshot(),
|
|
2716
|
+
talk: scope => this.officeTalk.recent(scope),
|
|
2087
2717
|
},
|
|
2088
2718
|
manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
|
|
2089
2719
|
ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
|
|
@@ -2440,6 +3070,26 @@ export class FleetManager {
|
|
|
2440
3070
|
const baselines = this.settingsBaselineReader = new SettingsBaselines({ dataDir: this.dataDir, configPath: () => this.configPath,
|
|
2441
3071
|
config: () => this.fleetConfig, current: () => [this.settingsGeneration, this.settingsRuntimeRevision, this.shuttingDown],
|
|
2442
3072
|
generalLocation: input => this.previewGeneralLocation(input),
|
|
3073
|
+
hubMembership: raw => hubMembershipSettings(raw),
|
|
3074
|
+
// S3c (S-55): a Hub exposure publish's confirmation summary, while a member composition is attached.
|
|
3075
|
+
hubBrowserCertificate: (input, binding) => {
|
|
3076
|
+
const control = this.hubBrowserCertificate();
|
|
3077
|
+
if (!control)
|
|
3078
|
+
throw Error("unavailable");
|
|
3079
|
+
return control.preview(input, binding);
|
|
3080
|
+
},
|
|
3081
|
+
hubExposure: (input) => {
|
|
3082
|
+
const api = this.hubMemberExposureApi;
|
|
3083
|
+
if (!api)
|
|
3084
|
+
throw Object.assign(new Error("hub_member_unavailable"), { code: "hub_member_unavailable" });
|
|
3085
|
+
return api.preview(input);
|
|
3086
|
+
},
|
|
3087
|
+
hubPairing: (action, input) => {
|
|
3088
|
+
const api = this.hubMemberPairingApi;
|
|
3089
|
+
if (!api)
|
|
3090
|
+
throw Object.assign(new Error("hub_member_unavailable"), { code: "hub_member_unavailable" });
|
|
3091
|
+
return pairingSummary(action, input, api.view(), api.boundHub());
|
|
3092
|
+
},
|
|
2443
3093
|
proof: (kind, target, id, binding, key) => {
|
|
2444
3094
|
const map = kind === "provider" ? this.providerSecretChallenges : kind === "binding" ? this.connectionBindingChallenges : this.connectionSecretChallenges;
|
|
2445
3095
|
const proof = map.get(id);
|
|
@@ -2484,6 +3134,7 @@ export class FleetManager {
|
|
|
2484
3134
|
&& sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
|
|
2485
3135
|
}, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
|
|
2486
3136
|
selfConfirm: (req, id) => this.settingsSelfConfirmation(store, req, id),
|
|
3137
|
+
directAgentCreation: true,
|
|
2487
3138
|
job: async (id) => {
|
|
2488
3139
|
const settlement = this.settingsJobSettlements.get(id);
|
|
2489
3140
|
if (!settlement)
|
|
@@ -2588,6 +3239,132 @@ export class FleetManager {
|
|
|
2588
3239
|
// while the request is applied fences it.
|
|
2589
3240
|
return this.liveOwner(req, { hostOnly: false });
|
|
2590
3241
|
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Refs #3061 (P2a): the new-device approval's one authority — the same one that decides an agent's request: this web-only
|
|
3244
|
+
* fleet's owner, asked of the LIVE session each time (a sign-out, an expiry, a revocation, the authority switch or
|
|
3245
|
+
* "Only this computer can authorize" turning a session into a non-owner is seen at once). A LAN session that signed in
|
|
3246
|
+
* with a one-time code, a session a device approval made, a guest, a chat fleet's admin and the CLI's header token are
|
|
3247
|
+
* none of them this: they are told so, and learn nothing about any request.
|
|
3248
|
+
*/
|
|
3249
|
+
deviceApprovalOwner(req) {
|
|
3250
|
+
return this.liveOwner(req, { hostOnly: false });
|
|
3251
|
+
}
|
|
3252
|
+
/**
|
|
3253
|
+
* Refs #3061 (auth-api): device-code sign-in is offered — a web-only fleet with web authorization on. With
|
|
3254
|
+
* `web.verified_authority.enabled: false` no one is an owner (liveOwner → null), so no one could approve: the three
|
|
3255
|
+
* `/auth/device-login*` routes answer 404 and the sign-in page shows no button. "Only this computer can authorize"
|
|
3256
|
+
* does not close it: this computer's own session can still approve.
|
|
3257
|
+
*/
|
|
3258
|
+
deviceLoginAvailable() {
|
|
3259
|
+
return this.isWebOnlyFleet() && this.webAuthorityState().enabled;
|
|
3260
|
+
}
|
|
3261
|
+
/** Refs #3061 (P2b): `web.login_qr` (default off), read per page served — the sign-in page then draws the approve link's QR code. */
|
|
3262
|
+
loginQrEnabled() {
|
|
3263
|
+
return this.fleetConfig?.web?.login_qr === true;
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* GET /ui/devices: is this a web-only fleet, may THIS session approve a new device and end another (is it the verified
|
|
3267
|
+
* owner), and — to an admin session, never to a guest or the CLI's header token — the signed-in devices (Refs #3061, P2b).
|
|
3268
|
+
* `authority`: web authorization is on. With it off nobody is an owner (liveOwner → null), so nobody may add a device or
|
|
3269
|
+
* sign another out; the page says why and points to `agend web-token rotate` on this computer instead of "use the AgEnD
|
|
3270
|
+
* computer", which would not help.
|
|
3271
|
+
*/
|
|
3272
|
+
webDevicesStatus(req) {
|
|
3273
|
+
const webOnly = this.isWebOnlyFleet();
|
|
3274
|
+
const owner = webOnly && !!this.deviceApprovalOwner(req);
|
|
3275
|
+
return { webOnly, owner, authority: webOnly && this.webAuthorityState().enabled, devices: webOnly ? this.webDeviceRows(req) : [] };
|
|
3276
|
+
}
|
|
3277
|
+
/**
|
|
3278
|
+
* Refs #3061 (P2b): the device list — this fleet's admin sessions, newest activity first (a guest's sessions are the
|
|
3279
|
+
* Guests page's, and a read-only tier has none): how each was approved, whether it carries owner authority, and which
|
|
3280
|
+
* one is the reader's own. The same people may read this as may read `GET /auth/sessions` (any admin session).
|
|
3281
|
+
*
|
|
3282
|
+
* `ownerVerified` is the §3 verdict for the session as it would be used from where it signs in (a host session from
|
|
3283
|
+
* this computer, a session from outside over the link): one that signed in with a one-time code or a device approval is
|
|
3284
|
+
* not an owner until the authenticator vouches for it. Display only — the checks that act on it run again at each use.
|
|
3285
|
+
*/
|
|
3286
|
+
webDeviceRows(req) {
|
|
3287
|
+
const auth = authorizeSession(req, this.webToken, this.webSessions, { touch: false });
|
|
3288
|
+
if (auth.kind !== "ok" || auth.session.tier !== "admin" || !this.webSessions)
|
|
3289
|
+
return [];
|
|
3290
|
+
const authorizations = this.sessionAuthorizationStore();
|
|
3291
|
+
return this.webSessions.list(auth.session.idHash, r => r.tier === "admin").map(summary => {
|
|
3292
|
+
const record = this.webSessions.findByHandle(summary.handle);
|
|
3293
|
+
const gateway = summary.surface === "gateway";
|
|
3294
|
+
const peer = gateway ? authorizations?.get(summary.handle)?.peer ?? "quick_link" : record?.host === true ? "loopback" : "lan";
|
|
3295
|
+
return { handle: summary.handle, label: summary.label, via: summary.via, current: summary.current, surface: summary.surface,
|
|
3296
|
+
created: summary.created, lastSeen: summary.lastSeen, expiresAt: Math.min(summary.absoluteExpiry, summary.idleExpiry),
|
|
3297
|
+
ownerVerified: !!record && !this.webAuthority({ session: record, gateway, peer }).refusal };
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Refs #3061 (P2b): may this request's session end ANOTHER session — one signed in elsewhere, or all of them (auth-api
|
|
3302
|
+
* `DELETE /auth/sessions…`, which an admin session could always do). On a web-only fleet that is the verified owner's:
|
|
3303
|
+
* a session a code or a device approval made is signed in but not an owner, and could otherwise sign out the owner's
|
|
3304
|
+
* own. A fleet with a chat connection is as it was (its admins are the chat's). Ending one's OWN session is never this.
|
|
3305
|
+
*/
|
|
3306
|
+
mayEndOtherSessions(req) {
|
|
3307
|
+
return !this.isWebOnlyFleet() || !!this.deviceApprovalOwner(req);
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* Refs #3061 (P2b): POST /ui/devices/signout {handle} — the owner ends another signed-in device (the settings list's
|
|
3311
|
+
* button). Only the verified owner, asked of the LIVE session each time (a non-owner learns nothing about any handle:
|
|
3312
|
+
* 403 before anything is looked up); only an admin session of this fleet (a guest's is the Guests page's; an unknown
|
|
3313
|
+
* handle and a guest's are the same 404); never the owner's own session here (Sign out is /auth/logout). The device's
|
|
3314
|
+
* authorization goes with it, so a copy of its cookie is nobody's.
|
|
3315
|
+
*/
|
|
3316
|
+
webDeviceSignOut(req, handle) {
|
|
3317
|
+
if (!this.isWebOnlyFleet() || !this.webSessions)
|
|
3318
|
+
return { status: 404, body: { error: "not_found" } };
|
|
3319
|
+
const owner = this.deviceApprovalOwner(req);
|
|
3320
|
+
if (!owner)
|
|
3321
|
+
return { status: 403, body: { error: "owner_required" } };
|
|
3322
|
+
if (handle === owner.handle)
|
|
3323
|
+
return { status: 400, body: { error: "this_device" } };
|
|
3324
|
+
const target = this.webSessions.findByHandle(handle);
|
|
3325
|
+
if (!target || target.tier !== "admin")
|
|
3326
|
+
return { status: 404, body: { error: "not_found" } };
|
|
3327
|
+
const { found, durable } = this.webSessions.revokeByHandle(handle, r => r.tier === "admin");
|
|
3328
|
+
if (!found)
|
|
3329
|
+
return { status: 404, body: { error: "not_found" } };
|
|
3330
|
+
this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
|
|
3331
|
+
this.logger.info({ by: owner.handle.slice(0, 8), handle, label: target.label, via: sessionVia(target), durable }, "Device signed out by the owner");
|
|
3332
|
+
if (!durable)
|
|
3333
|
+
return { status: 500, body: { ok: false, durable: false, error: "not_durable" } };
|
|
3334
|
+
return { status: 200, body: { ok: true } };
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* Refs #3061 (P2a): look a new device's code up (for the confirm screen), approve it, or deny it — the verified owner's,
|
|
3338
|
+
* on a web-only fleet. The answer is the request as it now stands. Approving only marks the request approved: the new
|
|
3339
|
+
* browser makes its own session when it next asks (auth-api POST /auth/device-login/poll), so no credential passes from
|
|
3340
|
+
* here to there — not in the answer, not through the fleet. A wrong code counts against one fleet-wide breaker; while
|
|
3341
|
+
* it is open every code (a right one too) waits.
|
|
3342
|
+
*/
|
|
3343
|
+
webDeviceLoginAct(req, op, code) {
|
|
3344
|
+
const owner = this.deviceApprovalOwner(req);
|
|
3345
|
+
if (!owner)
|
|
3346
|
+
return { status: 403, body: { error: "owner_required" } };
|
|
3347
|
+
const logins = this.webDeviceLogins, token = this.webToken;
|
|
3348
|
+
if (!logins || !token)
|
|
3349
|
+
return { status: 404, body: { error: "not_found" } };
|
|
3350
|
+
const epoch = tokenEpoch(token);
|
|
3351
|
+
const shown = (v) => ({ code: v.code, label: v.label, peer: v.peer, address: v.address, expiresAt: v.expiresAt, state: v.state });
|
|
3352
|
+
if (op === "lookup") {
|
|
3353
|
+
const r = logins.lookup(code, epoch);
|
|
3354
|
+
if (r.kind === "ok")
|
|
3355
|
+
return { status: 200, body: { request: shown(r.view) } };
|
|
3356
|
+
return r.kind === "paused" ? { status: 429, body: { error: "paused", retryAfterMs: r.retryAfterMs } } : { status: 404, body: { error: "not_found" } };
|
|
3357
|
+
}
|
|
3358
|
+
// (The owner above was asked of the live session just now, after the body was read; nothing is awaited since.)
|
|
3359
|
+
const r = logins.decide(code, op === "approve" ? "approved" : "denied", owner.handle, epoch);
|
|
3360
|
+
if (r.kind === "ok") {
|
|
3361
|
+
this.logger.info({ approvedBy: owner.handle.slice(0, 8), label: r.view.label, peer: r.view.peer, decision: r.view.state }, op === "approve" ? "New device sign-in approved" : "New device sign-in denied");
|
|
3362
|
+
return { status: 200, body: { request: shown(r.view) } };
|
|
3363
|
+
}
|
|
3364
|
+
if (r.kind === "decided")
|
|
3365
|
+
return { status: 409, body: { error: "already_decided", state: r.state } };
|
|
3366
|
+
return r.kind === "paused" ? { status: 429, body: { error: "paused", retryAfterMs: r.retryAfterMs } } : { status: 404, body: { error: "not_found" } };
|
|
3367
|
+
}
|
|
2591
3368
|
/** alpha.2: the agents' requests waiting for this web-only fleet's owner, or null when the reader is not that owner. */
|
|
2592
3369
|
webAgentProposals(req) {
|
|
2593
3370
|
if (!this.agentProposalOwner(req))
|
|
@@ -3145,6 +3922,8 @@ export class FleetManager {
|
|
|
3145
3922
|
sseHeartbeatMs = SSE_HEARTBEAT_MS;
|
|
3146
3923
|
/** The dashboard's login codes. Memory only: a code that outlives the process is a code nobody can prove was not copied. */
|
|
3147
3924
|
webLoginCodes = null;
|
|
3925
|
+
/** Refs #3061 (P2a): device-code sign-in requests (in memory; web-device-login.ts). Read by auth-api as `webDeviceLogins`. */
|
|
3926
|
+
webDeviceLogins = null;
|
|
3148
3927
|
/**
|
|
3149
3928
|
* #1920 step 4: guests' codes, apart from the admins' — one per account, so no guest's `/web` withdraws another's or
|
|
3150
3929
|
* an admin's code (#2093 review), at most 32 at once. Local sign-in only for now (audience "local").
|
|
@@ -3183,6 +3962,22 @@ export class FleetManager {
|
|
|
3183
3962
|
* startup-only key at startup. See checkStartupSignatureConsistency(). */
|
|
3184
3963
|
fleetSignatureMismatch = null;
|
|
3185
3964
|
healthServerListening = false;
|
|
3965
|
+
agentCredentialsCache;
|
|
3966
|
+
get agentCredentials() {
|
|
3967
|
+
return this.agentCredentialsCache ??= new AgentCredentials(name => agentCredentialPolicy(this.fleetConfig, name, this.isClassicInstance(name)), name => {
|
|
3968
|
+
try {
|
|
3969
|
+
const token = randomBytes(32).toString("hex");
|
|
3970
|
+
const result = writeSecretFile(join(this.getInstanceDir(name), "agent.token"), token);
|
|
3971
|
+
if (!result.ok)
|
|
3972
|
+
this.logger.error({ name, mode: result.mode, reason: result.reason }, "Agent token file is not owner-only");
|
|
3973
|
+
return token;
|
|
3974
|
+
}
|
|
3975
|
+
catch {
|
|
3976
|
+
this.logger.error({ name }, "Agent credential renewal failed; prior generation remains retired");
|
|
3977
|
+
return null;
|
|
3978
|
+
}
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3186
3981
|
constructor(dataDir) {
|
|
3187
3982
|
this.dataDir = dataDir;
|
|
3188
3983
|
FleetManager.signalTarget = this;
|
|
@@ -3205,6 +4000,7 @@ export class FleetManager {
|
|
|
3205
4000
|
this.topicCommands = new TopicCommands(this);
|
|
3206
4001
|
this.topicArchiver = new TopicArchiver(this);
|
|
3207
4002
|
this.statuslineWatcher = new StatuslineWatcher(this);
|
|
4003
|
+
this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
|
|
3208
4004
|
}
|
|
3209
4005
|
/** #1266: a reply's buttons — the store (reply-buttons.db) and what a click does. Created on first use. */
|
|
3210
4006
|
replyButtonsCtl = null;
|
|
@@ -3585,6 +4381,10 @@ export class FleetManager {
|
|
|
3585
4381
|
}
|
|
3586
4382
|
}
|
|
3587
4383
|
}
|
|
4384
|
+
// #3066 §1d: a Codex launch records the credential profile it runs under, so its usage is told by account.
|
|
4385
|
+
if (daemon?.bootId === daemonBootId && this.backendNameOf(name) === "codex") {
|
|
4386
|
+
void recordProfileLaunch(join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE), this.credentialProfileOf(name, "codex") ?? "default");
|
|
4387
|
+
}
|
|
3588
4388
|
// #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
|
|
3589
4389
|
// request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
|
|
3590
4390
|
this.scheduleDeliveryOutboxPump();
|
|
@@ -4462,6 +5262,7 @@ export class FleetManager {
|
|
|
4462
5262
|
this.enforceGuestSwitch();
|
|
4463
5263
|
this.observeWebAuthority(); // #2453 review: a reload's web-authority switches are seen at once
|
|
4464
5264
|
this.observeRuntimeSwitches(); // M-2255: so are the Windows runtimes' switches
|
|
5265
|
+
this.observeHubSwitches(); // S-51: and the Hub's (no-op until the fleet has started)
|
|
4465
5266
|
// Child tool/guidance readers must follow this fleet's actual config, including --config.
|
|
4466
5267
|
// Scope the path to its owning data directory so another inherited home's override cannot enable CW.
|
|
4467
5268
|
process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
|
|
@@ -4515,6 +5316,8 @@ export class FleetManager {
|
|
|
4515
5316
|
* guild channel.
|
|
4516
5317
|
*/
|
|
4517
5318
|
reregisterClassicChannels() {
|
|
5319
|
+
this.nativeMcpStore?.observePolicies();
|
|
5320
|
+
this.agentCredentialsCache?.observePolicies();
|
|
4518
5321
|
if (!this.classicChannels)
|
|
4519
5322
|
return;
|
|
4520
5323
|
const channels = this.classicChannels.getAll();
|
|
@@ -4567,13 +5370,9 @@ export class FleetManager {
|
|
|
4567
5370
|
}
|
|
4568
5371
|
/** AgEnD package version (for the Settings "current version" / What's New). */
|
|
4569
5372
|
get currentVersion() {
|
|
4570
|
-
|
|
4571
|
-
return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version ?? "unknown";
|
|
4572
|
-
}
|
|
4573
|
-
catch {
|
|
4574
|
-
return "unknown";
|
|
4575
|
-
}
|
|
5373
|
+
return RUNNING_BUILD.version;
|
|
4576
5374
|
}
|
|
5375
|
+
get currentBuild() { return RUNNING_BUILD; }
|
|
4577
5376
|
/**
|
|
4578
5377
|
* Resolve a slash-command target in a channel. Classic channels are looked up
|
|
4579
5378
|
* per-bot (same-channel multi-bot); a fleet-topic instance is found via the
|
|
@@ -5246,6 +6045,13 @@ export class FleetManager {
|
|
|
5246
6045
|
changed: () => this.emitSseEvent("status", this.getUiStatus()),
|
|
5247
6046
|
});
|
|
5248
6047
|
}
|
|
6048
|
+
/**
|
|
6049
|
+
* Claude history in the View: an instance's earlier conversation for its View, from the transcript the Log reads —
|
|
6050
|
+
* the View's route applied its own access rules first (view-api.ts); the Log's switch plays no part.
|
|
6051
|
+
*/
|
|
6052
|
+
viewHistory(name, q) {
|
|
6053
|
+
return viewHistory((n, rq) => this.log.readBack(n, rq), name, q);
|
|
6054
|
+
}
|
|
5249
6055
|
/** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
|
|
5250
6056
|
logSteps(name, q) { return this.log.steps(name, q); }
|
|
5251
6057
|
/**
|
|
@@ -5512,7 +6318,7 @@ export class FleetManager {
|
|
|
5512
6318
|
finish(false);
|
|
5513
6319
|
});
|
|
5514
6320
|
}
|
|
5515
|
-
async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold) {
|
|
6321
|
+
async deliverWithIdleGate(instanceName, payload, timeoutMs, deliveryEpoch, noInlineWake = false, stillCurrent, onStartRetryHold, beforeInput) {
|
|
5516
6322
|
if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
5517
6323
|
return false;
|
|
5518
6324
|
await this.holdDeliveryForStorm(instanceName, deliveryEpoch);
|
|
@@ -5551,7 +6357,8 @@ export class FleetManager {
|
|
|
5551
6357
|
const log = idleGateTimeoutLog(instanceName, timeoutMs, this.instanceStateCache.get(instanceName), Date.now());
|
|
5552
6358
|
this.logger[log.level](log.fields, log.message);
|
|
5553
6359
|
}
|
|
5554
|
-
|
|
6360
|
+
// The Hub representative's begin lease + input intent run inside the final connected hand-off (sendWhenConnected).
|
|
6361
|
+
const sent = await this.sendWhenConnected(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold, beforeInput);
|
|
5555
6362
|
if (!sent)
|
|
5556
6363
|
return false;
|
|
5557
6364
|
if (this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent)) {
|
|
@@ -5584,21 +6391,25 @@ export class FleetManager {
|
|
|
5584
6391
|
* `onStartRetryHold` (#2484, a person's message): an instance in its own start-retry backoff is waited for until that
|
|
5585
6392
|
* retry brings it up, however long the backoff, and fails only once the retries give up.
|
|
5586
6393
|
*/
|
|
5587
|
-
async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
|
|
6394
|
+
async sendWhenConnected(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold, beforeInput) {
|
|
5588
6395
|
if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
5589
6396
|
return false;
|
|
5590
6397
|
const queued = this.ipcWaitTails.get(instanceName);
|
|
5591
6398
|
if (!queued) {
|
|
5592
6399
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
5593
|
-
if (ipc?.connected
|
|
5594
|
-
|
|
6400
|
+
if (ipc?.connected) {
|
|
6401
|
+
if (beforeInput)
|
|
6402
|
+
return this.handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput);
|
|
6403
|
+
if (ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
6404
|
+
return true;
|
|
6405
|
+
}
|
|
5595
6406
|
}
|
|
5596
6407
|
const attempt = (queued ?? Promise.resolve())
|
|
5597
6408
|
.catch(() => { })
|
|
5598
6409
|
.then(() => {
|
|
5599
6410
|
if (!this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
5600
6411
|
return false;
|
|
5601
|
-
return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold);
|
|
6412
|
+
return this.sendAfterIpcReturns(instanceName, payload, deliveryEpoch, stillCurrent, onStartRetryHold, beforeInput);
|
|
5602
6413
|
});
|
|
5603
6414
|
// The chain stores a settled-either-way promise so one failed delivery cannot
|
|
5604
6415
|
// wedge every later one, and so `queued` above is safe to await unguarded.
|
|
@@ -5615,8 +6426,21 @@ export class FleetManager {
|
|
|
5615
6426
|
}
|
|
5616
6427
|
}
|
|
5617
6428
|
}
|
|
6429
|
+
/**
|
|
6430
|
+
* The Hub representative's hand-off (#3145): only once a connected client exists, after every reconnect/order wait,
|
|
6431
|
+
* the begin lease + input intent (beforeInput), then the fence and the LIVE client are checked again and the payload
|
|
6432
|
+
* goes in one attempt. After the intent is spent nothing here retries: false is the caller's uncertain outcome.
|
|
6433
|
+
*/
|
|
6434
|
+
async handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput) {
|
|
6435
|
+
if (!(await beforeInput()))
|
|
6436
|
+
return false;
|
|
6437
|
+
if (this.shuttingDown || !this.deliveryCurrent(instanceName, deliveryEpoch, stillCurrent))
|
|
6438
|
+
return false;
|
|
6439
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
6440
|
+
return !!ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload));
|
|
6441
|
+
}
|
|
5618
6442
|
/** Poll for the instance's IPC to come back, then send. Throws if it does not. */
|
|
5619
|
-
async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold) {
|
|
6443
|
+
async sendAfterIpcReturns(instanceName, payload, deliveryEpoch = this.getDeliveryEpoch(instanceName), stillCurrent, onStartRetryHold, beforeInput) {
|
|
5620
6444
|
let deadline = Date.now() + IPC_RECONNECT_GRACE_MS;
|
|
5621
6445
|
let warned = false;
|
|
5622
6446
|
let held = false;
|
|
@@ -5626,6 +6450,8 @@ export class FleetManager {
|
|
|
5626
6450
|
// Re-read every round: a reconnect replaces the IpcClient object entirely,
|
|
5627
6451
|
// so a cached reference would stay dead forever.
|
|
5628
6452
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
6453
|
+
if (ipc?.connected && beforeInput)
|
|
6454
|
+
return this.handOffAfterBeforeInput(instanceName, payload, deliveryEpoch, stillCurrent, beforeInput);
|
|
5629
6455
|
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
5630
6456
|
return true;
|
|
5631
6457
|
// #2484: while the instance waits for (or runs) its own start retry, the grace runs from now — so the message is
|
|
@@ -6065,6 +6891,8 @@ export class FleetManager {
|
|
|
6065
6891
|
if (this.generalLocationDeliveryHold(instanceName))
|
|
6066
6892
|
return false;
|
|
6067
6893
|
if (isNativeExecInstance(this.fleetConfig?.instances[instanceName])) {
|
|
6894
|
+
if (options.beforeInput)
|
|
6895
|
+
return false; // no pre-input hook on the Windows runtime
|
|
6068
6896
|
return this.nativeInstanceRuntime().deliver(instanceName, payload, { stillCurrent: options.stillCurrent,
|
|
6069
6897
|
...(options.authorityCurrent ? { authorityCurrent: options.authorityCurrent } : {}) });
|
|
6070
6898
|
}
|
|
@@ -6089,7 +6917,7 @@ export class FleetManager {
|
|
|
6089
6917
|
return false;
|
|
6090
6918
|
this.enforceWarmCap(instanceName); // woke one → evict a different LRU idle if over cap
|
|
6091
6919
|
}
|
|
6092
|
-
const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent, options.onStartRetryHold);
|
|
6920
|
+
const sent = await this.sendWhenConnected(instanceName, deliveryPayload, deliveryEpoch, options.stillCurrent, options.onStartRetryHold, options.beforeInput);
|
|
6093
6921
|
if (!sent)
|
|
6094
6922
|
return false;
|
|
6095
6923
|
// A cross-instance item arriving before the daemon observes this turn as
|
|
@@ -6100,7 +6928,7 @@ export class FleetManager {
|
|
|
6100
6928
|
return true;
|
|
6101
6929
|
}
|
|
6102
6930
|
const previous = this.idleGatedDeliveryTails.get(instanceName) ?? Promise.resolve();
|
|
6103
|
-
const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent, options.onStartRetryHold));
|
|
6931
|
+
const delivery = previous.catch(() => { }).then(() => this.deliverWithIdleGate(instanceName, deliveryPayload, options.idleTimeoutMs ?? 60_000, deliveryEpoch, options.noInlineWake === true, options.stillCurrent, options.onStartRetryHold, options.beforeInput));
|
|
6104
6932
|
this.idleGatedDeliveryTails.set(instanceName, delivery);
|
|
6105
6933
|
try {
|
|
6106
6934
|
return await delivery;
|
|
@@ -7371,6 +8199,17 @@ export class FleetManager {
|
|
|
7371
8199
|
this.queueKiroIncompatNotice(name, err.message);
|
|
7372
8200
|
return true;
|
|
7373
8201
|
}
|
|
8202
|
+
/** No pane is needed for a compatibility refusal to remain in Needs you. */
|
|
8203
|
+
setKiroCompatibilityHold(name, message) {
|
|
8204
|
+
const holds = this.kiroCompatibilityHolds ??= new Map();
|
|
8205
|
+
if ((message === null && !holds.has(name)) || (message !== null && holds.get(name)?.message === message))
|
|
8206
|
+
return;
|
|
8207
|
+
if (message === null)
|
|
8208
|
+
holds.delete(name);
|
|
8209
|
+
else
|
|
8210
|
+
holds.set(name, { message, since: Date.now() });
|
|
8211
|
+
this.needsYou?.poke();
|
|
8212
|
+
}
|
|
7374
8213
|
/**
|
|
7375
8214
|
* LifecycleContext: a kiro instance the installed kiro-cli cannot run as
|
|
7376
8215
|
* configured (#1109) — at start, or at a respawn after kiro-cli replaced
|
|
@@ -7877,6 +8716,12 @@ export class FleetManager {
|
|
|
7877
8716
|
},
|
|
7878
8717
|
});
|
|
7879
8718
|
}
|
|
8719
|
+
if (!this.webDeviceLogins) {
|
|
8720
|
+
this.webDeviceLogins = new DeviceLoginRequests({
|
|
8721
|
+
onEvent: event => { if (event === "breaker-open")
|
|
8722
|
+
this.logger.warn("Device sign-in approval paused: too many wrong codes"); },
|
|
8723
|
+
});
|
|
8724
|
+
}
|
|
7880
8725
|
if (!this.webGuestLoginCodes) {
|
|
7881
8726
|
this.webGuestLoginCodes = new GuestLoginCodes({
|
|
7882
8727
|
onEvent: event => {
|
|
@@ -8491,6 +9336,7 @@ export class FleetManager {
|
|
|
8491
9336
|
void this.publicWebLink?.close("dashboard revoke");
|
|
8492
9337
|
this.forgetPublicLinkOwner();
|
|
8493
9338
|
this.webLoginCodes.revoke();
|
|
9339
|
+
this.webDeviceLogins?.revokeAll(); // …and every new device still waiting to be approved (#3061)
|
|
8494
9340
|
this.webGuestLoginCodes.revokeAll();
|
|
8495
9341
|
this.guestRevokes++; // and every guest /web still on its way
|
|
8496
9342
|
// #1570: no browser may ask the sign-in page for a code any more.
|
|
@@ -9454,8 +10300,9 @@ export class FleetManager {
|
|
|
9454
10300
|
// #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
|
|
9455
10301
|
const topicMode = daemonTopicMode(fleet);
|
|
9456
10302
|
// Set tmux socket isolation for custom AGEND_HOME
|
|
9457
|
-
|
|
9458
|
-
|
|
10303
|
+
// M-2457: the bundled tmux's socket while it is on; getTmuxSocketName() as before while off.
|
|
10304
|
+
const { tmuxSocket } = await import("./tmux-binary.js");
|
|
10305
|
+
TmuxManager.setSocketName(tmuxSocket());
|
|
9459
10306
|
await TmuxManager.ensureSession(getTmuxSession());
|
|
9460
10307
|
this.tmuxSessionEnsured = true;
|
|
9461
10308
|
// #2072: what this host can run — probed in the background, published for every picker.
|
|
@@ -9720,6 +10567,9 @@ export class FleetManager {
|
|
|
9720
10567
|
await this.startHealthServer(fleet.health_port ?? 19280);
|
|
9721
10568
|
if (this.shuttingDown)
|
|
9722
10569
|
return;
|
|
10570
|
+
// S-51: from here on the Hub switches apply (an enabled Hub mounts now, beside the fleet; it never blocks startup).
|
|
10571
|
+
this.hubEffectsReady = true;
|
|
10572
|
+
this.observeHubSwitches();
|
|
9723
10573
|
if (generals.length > 0) {
|
|
9724
10574
|
for (const [name, cfg] of generals) {
|
|
9725
10575
|
try {
|
|
@@ -9822,6 +10672,9 @@ export class FleetManager {
|
|
|
9822
10672
|
if (others.length > 0 || classicAtBoot.length > 0) {
|
|
9823
10673
|
await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
|
|
9824
10674
|
}
|
|
10675
|
+
// #3066 §1d (Fable r1): on every fleet — a web-only one (#2910) has no chat connection at all, and is the clearest
|
|
10676
|
+
// case with no Discord bot reading. Each tick decides for itself whether to read (backgroundQuotaRead).
|
|
10677
|
+
this.startQuotaBackgroundReads();
|
|
9825
10678
|
if (topicMode && (fleet.channel || fleet.channels?.length)) {
|
|
9826
10679
|
await adapterStartup;
|
|
9827
10680
|
this.startDiscordUsagePresence();
|
|
@@ -9997,6 +10850,178 @@ export class FleetManager {
|
|
|
9997
10850
|
this.logger.warn({ health }, "Fleet started with problems — see /health");
|
|
9998
10851
|
}
|
|
9999
10852
|
}
|
|
10853
|
+
/**
|
|
10854
|
+
* #3066 §1d (the user: yes, every 30 min): the quota history (usage/quota-history.ts) records only the readings AgEnD
|
|
10855
|
+
* makes. A Discord bot's presence refresh reads every 15 min; without one, nothing reads while nobody looks. So, with no
|
|
10856
|
+
* Discord bot and while some account has a running agent, read every 30 min — the same cached read (its 5-minute TTL
|
|
10857
|
+
* still bounds the vendor calls), recorded like any other.
|
|
10858
|
+
*/
|
|
10859
|
+
startQuotaBackgroundReads() {
|
|
10860
|
+
if (this.quotaReadTimer)
|
|
10861
|
+
clearInterval(this.quotaReadTimer);
|
|
10862
|
+
this.quotaReadTimer = setInterval(() => { void this.backgroundQuotaRead(); }, FleetManager.QUOTA_BACKGROUND_READ_MS);
|
|
10863
|
+
this.quotaReadTimer.unref?.();
|
|
10864
|
+
// #3066 §1: the measured capacity, published for the usage reads (get_usage, the page) — refreshed every 10 min.
|
|
10865
|
+
setCapacityLookup((provider, metric) => this.capacityEstimates.get(`${provider}\u0000${metric}`) ?? null);
|
|
10866
|
+
if (this.capacityTimer)
|
|
10867
|
+
clearInterval(this.capacityTimer);
|
|
10868
|
+
void this.refreshCapacity();
|
|
10869
|
+
this.capacityTimer = setInterval(() => { void this.refreshCapacity(); }, FleetManager.CAPACITY_REFRESH_MS);
|
|
10870
|
+
this.capacityTimer.unref?.();
|
|
10871
|
+
}
|
|
10872
|
+
/** #3066 §1: the latest estimates, by `provider\0metric` (what usage-api's lookup reads, synchronously). */
|
|
10873
|
+
capacityEstimates = new Map();
|
|
10874
|
+
capacityTimer = null;
|
|
10875
|
+
static CAPACITY_REFRESH_MS = 10 * 60_000;
|
|
10876
|
+
/**
|
|
10877
|
+
* #3066 §1: estimate each account's weekly 100% from the quota history (§1d) and the usage ledger's hours — both read
|
|
10878
|
+
* asynchronously, the arithmetic small (usage/capacity.ts). An account is its usage provider id ("claude",
|
|
10879
|
+
* "claude:hsinlin", "codex:yanxianchen"): its hours are the ledger sources of that backend under that credential profile
|
|
10880
|
+
* (Codex's "unrecorded" hours are no account's). A per-model weekly ("Sonnet (weekly)") counts only that model's tokens.
|
|
10881
|
+
*
|
|
10882
|
+
* Prism #3073: no estimate rather than a wrong one —
|
|
10883
|
+
* - a window is measured only where the ledger has its hours: kept (31 days) and scanned (`days.coverage`) for every
|
|
10884
|
+
* instance of the account's backend, and every instance whose hours count for it; the quota history goes back 12 weeks;
|
|
10885
|
+
* - a profile literally named "default", "unknown" or "unrecorded" (configured, on disk, or in the quota history) makes
|
|
10886
|
+
* those hours ambiguous: neither the host
|
|
10887
|
+
* account nor the named one is estimated (each would be credited the other's use, or lose its own);
|
|
10888
|
+
* - only a metric the vendor marks per-model (`scope: "model"`) is filtered by model; another named weekly pool
|
|
10889
|
+
* ("Luna Reserve (weekly)", Antigravity pools) is no model name and is not estimated.
|
|
10890
|
+
*/
|
|
10891
|
+
async refreshCapacity() {
|
|
10892
|
+
try {
|
|
10893
|
+
const readings = await readQuotaHistory();
|
|
10894
|
+
const groups = new Map();
|
|
10895
|
+
for (const r of readings) {
|
|
10896
|
+
const k = `${r.provider}\u0000${r.metric}`;
|
|
10897
|
+
(groups.get(k) ?? groups.set(k, []).get(k)).push(r);
|
|
10898
|
+
}
|
|
10899
|
+
if (!groups.size) {
|
|
10900
|
+
this.capacityEstimates = new Map();
|
|
10901
|
+
return 0;
|
|
10902
|
+
}
|
|
10903
|
+
const data = await this.cacheServiceFor().usageData();
|
|
10904
|
+
const now = Date.now(), DAY = 24 * 3_600_000, HOUR = 3_600_000;
|
|
10905
|
+
const keptFrom = now - KEEP_DAYS * DAY + HOUR; // the oldest hour still in every ledger (an hour of margin)
|
|
10906
|
+
// Profiles whose names are also the ledger's own tags: their hours cannot be told from the host's / unattributed.
|
|
10907
|
+
// Any trace of such a profile counts (Prism #3073 r2), not only today's config — a binding removed or switched
|
|
10908
|
+
// leaves its hours in the ledger: configured now, its directory on this host, or its account in the quota history.
|
|
10909
|
+
const RESERVED = new Set(["default", "unknown", PROFILE_UNRECORDED]);
|
|
10910
|
+
const historyProviders = new Set(readings.map((r) => r.provider));
|
|
10911
|
+
const reservedNamed = new Map();
|
|
10912
|
+
const reservedFor = (backend) => {
|
|
10913
|
+
const known = reservedNamed.get(backend);
|
|
10914
|
+
if (known)
|
|
10915
|
+
return known;
|
|
10916
|
+
const found = new Set(listConfiguredProfiles(this.fleetConfig, backend).filter((n) => RESERVED.has(n)));
|
|
10917
|
+
const base = usageProviderIdForBackend(backend);
|
|
10918
|
+
for (const n of RESERVED) {
|
|
10919
|
+
if (base && historyProviders.has(`${base}:${n}`))
|
|
10920
|
+
found.add(n);
|
|
10921
|
+
if (credentialHomeSpec(backend) && existsSync(credentialProfileHome(this.dataDir, backend, n)))
|
|
10922
|
+
found.add(n);
|
|
10923
|
+
}
|
|
10924
|
+
return reservedNamed.set(backend, found).get(backend);
|
|
10925
|
+
};
|
|
10926
|
+
// Every instance of a backend may have used any of its accounts (bindings change): an account is measured only
|
|
10927
|
+
// where all of them were scanned (Prism #3073 r2) — one still waiting for its first scan has no hours yet, not none.
|
|
10928
|
+
const daysOfBase = new Map();
|
|
10929
|
+
for (const inst of data.instances) {
|
|
10930
|
+
const base = usageProviderIdForBackend(inst.inst.backend);
|
|
10931
|
+
if (base)
|
|
10932
|
+
(daysOfBase.get(base) ?? daysOfBase.set(base, []).get(base)).push(inst.days);
|
|
10933
|
+
}
|
|
10934
|
+
// provider id → hour → model key → totals; and the instances whose hours count for it (their scan coverage).
|
|
10935
|
+
const byProvider = new Map();
|
|
10936
|
+
const daysOf = new Map();
|
|
10937
|
+
for (const inst of data.instances) {
|
|
10938
|
+
for (const [hk, hour] of Object.entries(inst.ledger.hours)) {
|
|
10939
|
+
for (const src of hourSources(hour)) {
|
|
10940
|
+
const base = usageProviderIdForBackend(src.backend);
|
|
10941
|
+
if (!base)
|
|
10942
|
+
continue;
|
|
10943
|
+
// Ambiguous (a named profile with the tag's name), or no account's: counted for no one, so an account whose
|
|
10944
|
+
// hours these may be has none — and gets no estimate, never a low one.
|
|
10945
|
+
if (reservedFor(src.backend).has(src.profile) || src.profile === "unknown" || src.profile === PROFILE_UNRECORDED)
|
|
10946
|
+
continue;
|
|
10947
|
+
const provider = src.profile === "default" ? base : `${base}:${src.profile}`;
|
|
10948
|
+
const list = daysOf.get(provider) ?? daysOf.set(provider, []).get(provider);
|
|
10949
|
+
if (!list.includes(inst.days))
|
|
10950
|
+
list.push(inst.days);
|
|
10951
|
+
const hours = byProvider.get(provider) ?? byProvider.set(provider, new Map()).get(provider);
|
|
10952
|
+
const into = hours.get(Number(hk)) ?? hours.set(Number(hk), Object.create(null)).get(Number(hk));
|
|
10953
|
+
for (const [mk, tot] of Object.entries(src.m)) {
|
|
10954
|
+
const cur = into[mk] ??= [0, 0, 0, 0, 0, 0, 0];
|
|
10955
|
+
for (let i = 0; i < 7; i++)
|
|
10956
|
+
cur[i] = (cur[i] ?? 0) + (tot[i] ?? 0);
|
|
10957
|
+
}
|
|
10958
|
+
}
|
|
10959
|
+
}
|
|
10960
|
+
}
|
|
10961
|
+
const next = new Map();
|
|
10962
|
+
for (const [key, list] of groups) {
|
|
10963
|
+
const [provider, metric] = key.split("\u0000");
|
|
10964
|
+
// A per-model weekly ("Sonnet (weekly)", the vendor's scope "model"): that model's tokens only. Another named
|
|
10965
|
+
// pool is not a model: no estimate. "Weekly", "Weekly limit": everything.
|
|
10966
|
+
const named = /^(.+) \((?:weekly|monthly|\d+d)\)$/i.exec(metric)?.[1]?.toLowerCase() ?? null;
|
|
10967
|
+
const perModel = list.some((r) => r.scope === "model");
|
|
10968
|
+
if (named && !perModel)
|
|
10969
|
+
continue;
|
|
10970
|
+
const hours = [];
|
|
10971
|
+
for (const [hour, models] of byProvider.get(provider) ?? []) {
|
|
10972
|
+
let api = 0, output = 0, all = 0;
|
|
10973
|
+
for (const [mk, tot] of Object.entries(models)) {
|
|
10974
|
+
const bar = mk.lastIndexOf("|"), model = mk.slice(0, bar), long = mk.slice(bar + 1) === "1";
|
|
10975
|
+
if (named && !model.toLowerCase().includes(named))
|
|
10976
|
+
continue;
|
|
10977
|
+
const [, uncached, read, w5, w1h, w, out] = tot;
|
|
10978
|
+
const r = ratesFor(model, long ? Number.MAX_SAFE_INTEGER : 0);
|
|
10979
|
+
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;
|
|
10980
|
+
output += out;
|
|
10981
|
+
all += uncached + read + w5 + w1h + w + out;
|
|
10982
|
+
}
|
|
10983
|
+
if (all > 0)
|
|
10984
|
+
hours.push({ hour, api, output, all });
|
|
10985
|
+
}
|
|
10986
|
+
const base = provider.split(":")[0];
|
|
10987
|
+
const scanned = [...new Set([...(daysOf.get(provider) ?? []), ...(daysOfBase.get(base) ?? [])])];
|
|
10988
|
+
const available = (from, to) => {
|
|
10989
|
+
if (from < keptFrom || !scanned.length)
|
|
10990
|
+
return false;
|
|
10991
|
+
for (let h = from; h <= to; h += HOUR)
|
|
10992
|
+
if (!scanned.every((d) => covered(d, h)))
|
|
10993
|
+
return false;
|
|
10994
|
+
return true;
|
|
10995
|
+
};
|
|
10996
|
+
const est = estimateCapacity(list, hours, available);
|
|
10997
|
+
if (est)
|
|
10998
|
+
next.set(key, est);
|
|
10999
|
+
}
|
|
11000
|
+
this.capacityEstimates = next;
|
|
11001
|
+
return next.size;
|
|
11002
|
+
}
|
|
11003
|
+
catch (err) {
|
|
11004
|
+
this.logger.debug({ err }, "capacity estimate refresh failed");
|
|
11005
|
+
return -1;
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
/** One background quota read, when nothing else reads (no Discord bot) and an account has a running agent. */
|
|
11009
|
+
async backgroundQuotaRead() {
|
|
11010
|
+
if (this.shuttingDown)
|
|
11011
|
+
return false;
|
|
11012
|
+
if ([...this.adapters.values()].some(a => a.type === "discord" && typeof a.setActivity === "function"))
|
|
11013
|
+
return false;
|
|
11014
|
+
const active = this.getActiveUsageProviderIds();
|
|
11015
|
+
if (active.size === 0)
|
|
11016
|
+
return false;
|
|
11017
|
+
try {
|
|
11018
|
+
await getUsageSnapshot(false, active);
|
|
11019
|
+
return true;
|
|
11020
|
+
}
|
|
11021
|
+
catch {
|
|
11022
|
+
return false;
|
|
11023
|
+
}
|
|
11024
|
+
}
|
|
10000
11025
|
/** Keep Discord profile activity aligned with the same cached usage source as /usage. */
|
|
10001
11026
|
startDiscordUsagePresence() {
|
|
10002
11027
|
if (this.discordPresenceTimer)
|
|
@@ -10334,8 +11359,8 @@ export class FleetManager {
|
|
|
10334
11359
|
* that should be running is running
|
|
10335
11360
|
* - `degraded` — reachable, but something the operator should look at (an
|
|
10336
11361
|
* adapter retrying, an instance crashed or stopped)
|
|
10337
|
-
* - `down` —
|
|
10338
|
-
*
|
|
11362
|
+
* - `down` — no configured adapter is connected, or a dedicated Hub's
|
|
11363
|
+
* server mount is unavailable
|
|
10339
11364
|
*
|
|
10340
11365
|
* Deliberately does NOT gate the systemd watchdog — see the comment at the
|
|
10341
11366
|
* WATCHDOG timer for why.
|
|
@@ -10368,10 +11393,20 @@ export class FleetManager {
|
|
|
10368
11393
|
if (effectiveStatus === "connected")
|
|
10369
11394
|
connected++;
|
|
10370
11395
|
}
|
|
10371
|
-
const problems = [];
|
|
11396
|
+
const problems = [], warnings = [];
|
|
11397
|
+
const rawHub = this.rawFleetConfig?.hub;
|
|
11398
|
+
const hub = rawHub?.enabled === true ? { server: this.shuttingDown ? "stopped"
|
|
11399
|
+
: this.hubRuntime?.serverHealth?.(rawHub) ?? "unavailable" } : undefined;
|
|
11400
|
+
const hubOnly = !!hub && names.length === 0 && this.adapterState.size === 0;
|
|
11401
|
+
if (hub && hub.server !== "ready")
|
|
11402
|
+
problems.push(`Hub server is ${hub.server}`);
|
|
10372
11403
|
const hostMemory = this.memoryPressure.snapshot();
|
|
10373
|
-
if (
|
|
10374
|
-
|
|
11404
|
+
if (hostMemory.level === "critical" || hostMemory.level === "elevated") {
|
|
11405
|
+
const pressure = `host memory pressure is ${hostMemory.level}`;
|
|
11406
|
+
if (hubOnly)
|
|
11407
|
+
warnings.push(pressure);
|
|
11408
|
+
else if (!this.memoryPressure.advisoryOnly())
|
|
11409
|
+
problems.push(pressure);
|
|
10375
11410
|
}
|
|
10376
11411
|
if (this.adapterState.size > 0 && connected === 0)
|
|
10377
11412
|
problems.push("no channel adapter is connected");
|
|
@@ -10383,9 +11418,8 @@ export class FleetManager {
|
|
|
10383
11418
|
}
|
|
10384
11419
|
if (!this.startupComplete)
|
|
10385
11420
|
problems.push("startup has not completed");
|
|
10386
|
-
//
|
|
10387
|
-
|
|
10388
|
-
const status = this.adapterState.size > 0 && connected === 0
|
|
11421
|
+
// Preserve adapter scoring. A dedicated Hub also cannot serve its workload before its server mount is ready.
|
|
11422
|
+
const status = (this.adapterState.size > 0 && connected === 0) || (hubOnly && hub?.server !== "ready")
|
|
10389
11423
|
? "down"
|
|
10390
11424
|
: problems.length > 0 ? "degraded" : "ok";
|
|
10391
11425
|
return {
|
|
@@ -10397,7 +11431,7 @@ export class FleetManager {
|
|
|
10397
11431
|
memory: readFleetMemory(),
|
|
10398
11432
|
hostMemory: this.memoryTop && (hostMemory.level === "elevated" || hostMemory.level === "critical")
|
|
10399
11433
|
? { ...hostMemory, topProcesses: this.memoryTop.processes, topProcessesAt: this.memoryTop.at } : hostMemory,
|
|
10400
|
-
problems,
|
|
11434
|
+
problems, warnings, ...(hub ? { hub } : {}),
|
|
10401
11435
|
};
|
|
10402
11436
|
}
|
|
10403
11437
|
/** Start the primary adapter (backward-compatible, sets this.adapter) */
|
|
@@ -10472,6 +11506,11 @@ export class FleetManager {
|
|
|
10472
11506
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
10473
11507
|
await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
|
|
10474
11508
|
}, this.logger, "adapter.callback_query"));
|
|
11509
|
+
adapter.on("missed_offline", (ev) => {
|
|
11510
|
+
if (!isCurrentAdapter())
|
|
11511
|
+
return;
|
|
11512
|
+
this.recordMissedOffline(ev);
|
|
11513
|
+
});
|
|
10475
11514
|
this.bindTopicClosedHandler(adapter, adapterId, "adapter.topic_closed");
|
|
10476
11515
|
// Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
|
|
10477
11516
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
@@ -10625,6 +11664,11 @@ export class FleetManager {
|
|
|
10625
11664
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
10626
11665
|
await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
|
|
10627
11666
|
}, this.logger, `adapter[${adapterId}].callback_query`));
|
|
11667
|
+
adapter.on("missed_offline", (ev) => {
|
|
11668
|
+
if (!isCurrentAdapter())
|
|
11669
|
+
return;
|
|
11670
|
+
this.recordMissedOffline(ev);
|
|
11671
|
+
});
|
|
10628
11672
|
this.bindTopicClosedHandler(adapter, adapterId, `adapter[${adapterId}].topic_closed`);
|
|
10629
11673
|
// Slash commands: classic bot + admin commands
|
|
10630
11674
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
@@ -10784,6 +11828,9 @@ export class FleetManager {
|
|
|
10784
11828
|
this.eventLog?.insert(targetInstance, "cross_instance_delivery_failed", {
|
|
10785
11829
|
from: senderSession, correlation_id: correlationId, error,
|
|
10786
11830
|
});
|
|
11831
|
+
// A Hub question the pane never took: the representative holds it uncertain (never resent).
|
|
11832
|
+
if (senderSession === HUB_REPORT_TARGET && !senderInstance)
|
|
11833
|
+
this.hubDeliveryFailed?.(correlationId, targetInstance);
|
|
10787
11834
|
if (senderInstance) {
|
|
10788
11835
|
this.notifyInstanceTopic(senderInstance, t("cross_instance.delivery_failed_notice", targetInstance, displayError, correlationId));
|
|
10789
11836
|
void this.deliverToInstance(senderInstance, {
|
|
@@ -10902,13 +11949,12 @@ export class FleetManager {
|
|
|
10902
11949
|
try {
|
|
10903
11950
|
const { execFile } = await import("./child-process.js");
|
|
10904
11951
|
const { promisify } = await import("node:util");
|
|
10905
|
-
const {
|
|
11952
|
+
const { tmuxArgs, tmuxBinary, tmuxEnvOption } = await import("./tmux-binary.js");
|
|
10906
11953
|
// Honour socket isolation: without -L this queried the user's default
|
|
10907
11954
|
// tmux server instead of the fleet's, so under a custom AGEND_HOME the
|
|
10908
11955
|
// check was meaningless (it reported every pane dead).
|
|
10909
|
-
const
|
|
10910
|
-
|
|
10911
|
-
await promisify(execFile)("tmux", args, { timeout: 5_000 });
|
|
11956
|
+
const args = tmuxArgs(["list-panes", "-t", windowId]);
|
|
11957
|
+
await promisify(execFile)(tmuxBinary(), args, { timeout: 5_000, ...tmuxEnvOption() });
|
|
10912
11958
|
}
|
|
10913
11959
|
catch {
|
|
10914
11960
|
// Pane dead — respawn
|
|
@@ -11365,6 +12411,34 @@ export class FleetManager {
|
|
|
11365
12411
|
}
|
|
11366
12412
|
return true;
|
|
11367
12413
|
}
|
|
12414
|
+
/** File a Telegram update that was too old to inject; Needs you lists it until Acknowledge. */
|
|
12415
|
+
recordMissedOffline(ev) {
|
|
12416
|
+
if (!this.missedOfflineStore)
|
|
12417
|
+
this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
|
|
12418
|
+
const stub = {
|
|
12419
|
+
source: "telegram", adapterId: ev.adapterId, chatId: ev.chatId, threadId: ev.threadId,
|
|
12420
|
+
messageId: ev.messageId, userId: ev.userId, username: ev.username, text: ev.preview,
|
|
12421
|
+
timestamp: ev.timestamp, isBotMessage: false,
|
|
12422
|
+
};
|
|
12423
|
+
const threadId = this.inboundRouteThreadId(stub);
|
|
12424
|
+
const target = threadId ? this.resolveInboundTarget(stub, threadId) : undefined;
|
|
12425
|
+
let instance = target?.name ?? "";
|
|
12426
|
+
if (!instance) {
|
|
12427
|
+
instance = Object.entries(this.fleetConfig?.instances ?? {}).find(([name, cfg]) => cfg.general_topic && this.getInstanceAdapterId(name) === ev.adapterId)?.[0] ?? "";
|
|
12428
|
+
}
|
|
12429
|
+
const place = [ev.chatTitle, ev.threadId ? `#${ev.threadId}` : ""].filter(Boolean).join(" · ") || ev.chatId;
|
|
12430
|
+
this.missedOfflineStore.add({
|
|
12431
|
+
id: `missed:${ev.adapterId}:${ev.updateId}`,
|
|
12432
|
+
adapterId: ev.adapterId,
|
|
12433
|
+
instance,
|
|
12434
|
+
username: ev.username,
|
|
12435
|
+
place,
|
|
12436
|
+
preview: ev.preview,
|
|
12437
|
+
since: ev.timestamp.getTime(),
|
|
12438
|
+
});
|
|
12439
|
+
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");
|
|
12440
|
+
this.needsYou?.poke();
|
|
12441
|
+
}
|
|
11368
12442
|
/**
|
|
11369
12443
|
* #1085: coincident topic numbers must not route across Telegram groups.
|
|
11370
12444
|
*/
|
|
@@ -13001,6 +14075,11 @@ export class FleetManager {
|
|
|
13001
14075
|
let result;
|
|
13002
14076
|
switch (msg.type) {
|
|
13003
14077
|
case "fleet_decision_create": {
|
|
14078
|
+
const supersedes = this.decisionSupersedes("ipc-typed", instanceName, payload.supersedes);
|
|
14079
|
+
if ("error" in supersedes) {
|
|
14080
|
+
ipc.send({ type: "fleet_decision_response", fleetRequestId, error: supersedes.error });
|
|
14081
|
+
return;
|
|
14082
|
+
}
|
|
13004
14083
|
// Prune expired decisions on create
|
|
13005
14084
|
db.pruneExpiredDecisions();
|
|
13006
14085
|
result = db.createDecision({
|
|
@@ -13011,7 +14090,7 @@ export class FleetManager {
|
|
|
13011
14090
|
tags: payload.tags,
|
|
13012
14091
|
ttl_days: payload.ttl_days,
|
|
13013
14092
|
created_by: instanceName,
|
|
13014
|
-
supersedes:
|
|
14093
|
+
supersedes: supersedes.id,
|
|
13015
14094
|
});
|
|
13016
14095
|
break;
|
|
13017
14096
|
}
|
|
@@ -13188,6 +14267,21 @@ export class FleetManager {
|
|
|
13188
14267
|
return "ok";
|
|
13189
14268
|
}
|
|
13190
14269
|
}
|
|
14270
|
+
/**
|
|
14271
|
+
* `post_decision` with `supersedes` changes a decision someone else may have recorded (it stops being active): the
|
|
14272
|
+
* same permission as `update_decision`, asked through the same live check (tool set + dashboard switches), on both
|
|
14273
|
+
* create paths. An empty or absent value supersedes nothing and needs nothing; anything else must be an id.
|
|
14274
|
+
*/
|
|
14275
|
+
decisionSupersedes(sink, instanceName, value) {
|
|
14276
|
+
if (value === undefined || value === null || value === "")
|
|
14277
|
+
return { id: undefined };
|
|
14278
|
+
if (typeof value !== "string")
|
|
14279
|
+
return { error: "supersedes must be a decision id" };
|
|
14280
|
+
const permitted = this.checkToolPermission(sink, instanceName, "update_decision");
|
|
14281
|
+
if (!permitted.allowed)
|
|
14282
|
+
return { error: `Superseding a decision needs update_decision. ${permitted.message}` };
|
|
14283
|
+
return { id: value };
|
|
14284
|
+
}
|
|
13191
14285
|
async handleDecisionCrudHttp(instance, op, args) {
|
|
13192
14286
|
if (!this.scheduler)
|
|
13193
14287
|
return { error: "Scheduler not available" };
|
|
@@ -13203,16 +14297,24 @@ export class FleetManager {
|
|
|
13203
14297
|
if (!title || !content)
|
|
13204
14298
|
return { error: "title and content are required" };
|
|
13205
14299
|
const scope = args.scope === "fleet" ? "fleet" : "project";
|
|
13206
|
-
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
|
|
14300
|
+
const supersedes = this.decisionSupersedes("agent-endpoint", instance, args.supersedes);
|
|
14301
|
+
if ("error" in supersedes)
|
|
14302
|
+
return { error: supersedes.error };
|
|
14303
|
+
try {
|
|
14304
|
+
return db.createDecision({
|
|
14305
|
+
project_root: projectRoot,
|
|
14306
|
+
scope,
|
|
14307
|
+
title,
|
|
14308
|
+
content,
|
|
14309
|
+
tags: asStrArr(args.tags),
|
|
14310
|
+
ttl_days: asNum(args.ttl_days),
|
|
14311
|
+
supersedes: supersedes.id,
|
|
14312
|
+
created_by: instance,
|
|
14313
|
+
});
|
|
14314
|
+
}
|
|
14315
|
+
catch (err) {
|
|
14316
|
+
return { error: err.message }; // a refused predecessor: nothing was inserted
|
|
14317
|
+
}
|
|
13216
14318
|
}
|
|
13217
14319
|
// #2152: titles by default (agend-agent decision-list); one decision's full text by id (decision-get <id>).
|
|
13218
14320
|
// The CLI sends include_archived (snake case, as the MCP tool): both spellings are read.
|
|
@@ -13255,6 +14357,8 @@ export class FleetManager {
|
|
|
13255
14357
|
const asStatusFilter = normalizeStatusFilter;
|
|
13256
14358
|
// #1336: small write ack — identifying fields only.
|
|
13257
14359
|
const ack = (t) => ({ id: t.id, status: t.status, updated_at: t.updated_at });
|
|
14360
|
+
// #3195: an update's ack also carries what was SAVED for each field it named, read back from the task.
|
|
14361
|
+
const updatedAck = (t, named) => ({ ...ack(t), ...savedFields(t, named) });
|
|
13258
14362
|
// The board's notes for the write (a send-back reason, a comment the assignee had not seen): with the ack when any.
|
|
13259
14363
|
const noted = (a) => { const notes = db.board.takeNotes(); return notes.length ? { ...a, notes } : a; };
|
|
13260
14364
|
try {
|
|
@@ -13312,12 +14416,12 @@ export class FleetManager {
|
|
|
13312
14416
|
const id = asStr(args.id);
|
|
13313
14417
|
if (!id)
|
|
13314
14418
|
return { error: "id is required" };
|
|
13315
|
-
return noted(
|
|
14419
|
+
return noted(updatedAck(db.updateTask(db.getTaskByPrefix(id).id, {
|
|
13316
14420
|
status: asStatus(args.status),
|
|
13317
14421
|
assignee: asStr(args.assignee),
|
|
13318
14422
|
result: asStr(args.result),
|
|
13319
14423
|
priority: asPriority(args.priority),
|
|
13320
|
-
}, { kind: "instance", name: instance })));
|
|
14424
|
+
}, { kind: "instance", name: instance }), args));
|
|
13321
14425
|
}
|
|
13322
14426
|
default: return { error: `Unknown task action: ${action}` };
|
|
13323
14427
|
}
|
|
@@ -13471,7 +14575,8 @@ export class FleetManager {
|
|
|
13471
14575
|
result: payload.result,
|
|
13472
14576
|
priority: payload.priority,
|
|
13473
14577
|
}, { kind: "instance", name: meta.instance_name || instanceName });
|
|
13474
|
-
|
|
14578
|
+
// #3195: with what was SAVED for each field the update named, read back from the task.
|
|
14579
|
+
result = noted({ ...ack(logTask), ...savedFields(logTask, payload) });
|
|
13475
14580
|
break;
|
|
13476
14581
|
default:
|
|
13477
14582
|
throw new Error(`Unknown task action: ${action}`);
|
|
@@ -13501,16 +14606,101 @@ export class FleetManager {
|
|
|
13501
14606
|
}
|
|
13502
14607
|
return adapter.createTopic(topicName);
|
|
13503
14608
|
}
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
this.
|
|
13513
|
-
|
|
14609
|
+
/**
|
|
14610
|
+
* Delete an agent and, unless asked not to, its own chat topic (user decision: by default). The topic is planned
|
|
14611
|
+
* before the removal (its id and bot go with the fleet.yaml entry) and deleted through the agent's OWN bot after it;
|
|
14612
|
+
* the outcome is the platform's answer (instance-topic-delete.ts). A topic that cannot go never fails the removal.
|
|
14613
|
+
*/
|
|
14614
|
+
async removeInstanceAndTopic(name, authorization, opts) {
|
|
14615
|
+
const plan = opts.deleteTopic ? this.planInstanceTopicDelete(name) : null;
|
|
14616
|
+
if (opts.execution)
|
|
14617
|
+
await this.removeInstance(name, authorization, opts.execution);
|
|
14618
|
+
else
|
|
14619
|
+
await this.removeInstance(name, authorization);
|
|
14620
|
+
// #3055 review (Prism P1): asked again right before the platform call — the fleet as it is NOW, not as planned
|
|
14621
|
+
// before the removal was awaited (a new owner of the topic, or the bot rebuilt).
|
|
14622
|
+
return deletePlannedTopic(plan, this.logger, (p) => {
|
|
14623
|
+
if (this.instanceTopicAdapter(p.adapterId) !== p.adapter)
|
|
14624
|
+
return "the agent's bot changed while it was being deleted, so its topic was left";
|
|
14625
|
+
return topicMustStay(p.destination, this.topicOwnership(), null); // the original is gone: every owner now counts
|
|
14626
|
+
}, { begin: (d) => this.topicDeletionsInFlight.add(d), end: (d) => this.topicDeletionsInFlight.delete(d) });
|
|
14627
|
+
}
|
|
14628
|
+
/** #3055: destinations whose platform deletion is running now; a binding to one waits until it is over. */
|
|
14629
|
+
topicDeletionsInFlight = new Set();
|
|
14630
|
+
/** True while a topic with this id is being deleted on any connection (a new binding of it is refused meanwhile). */
|
|
14631
|
+
isTopicDeletionInFlight(topicId) {
|
|
14632
|
+
const tail = `:${topicId}`;
|
|
14633
|
+
return [...this.topicDeletionsInFlight].some(d => d.endsWith(tail));
|
|
14634
|
+
}
|
|
14635
|
+
/**
|
|
14636
|
+
* #3055 review r2 (Prism P1-3): would this instance config bind a topic whose deletion is running now? Compared by
|
|
14637
|
+
* physical destination, so the same thread number in another Telegram group is not held. Settings asks it at commit.
|
|
14638
|
+
*/
|
|
14639
|
+
topicBindingInFlight(inst) {
|
|
14640
|
+
if (this.topicDeletionsInFlight.size === 0 || inst.topic_id == null || inst.topic_id === "")
|
|
14641
|
+
return false;
|
|
14642
|
+
const adapterId = typeof inst.channel_id === "string" && inst.channel_id ? inst.channel_id : this.getPrimaryAdapterId();
|
|
14643
|
+
return this.topicDeletionsInFlight.has(this.destinationFor(adapterId, inst.topic_id));
|
|
14644
|
+
}
|
|
14645
|
+
/** Any topic deletion running now (a connection's group or General may not be re-pointed meanwhile). */
|
|
14646
|
+
topicDeletionPending() { return this.topicDeletionsInFlight.size > 0; }
|
|
14647
|
+
/** The physical destination of `topicId` on the bot `adapterId`. */
|
|
14648
|
+
destinationFor(adapterId, topicId) {
|
|
14649
|
+
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
14650
|
+
const platform = world?.adapter?.type ?? this.getChannelConfig(adapterId)?.type;
|
|
14651
|
+
const groupId = world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
14652
|
+
return topicDestination(platform, groupId, adapterId, topicId);
|
|
14653
|
+
}
|
|
14654
|
+
/** The adapter of the bot `adapterId` names — never a substitute for a bot that is not there. */
|
|
14655
|
+
instanceTopicAdapter(adapterId) {
|
|
14656
|
+
return (adapterId ? this.worlds.get(adapterId)?.adapter : this.worlds.size === 0 ? this.adapter ?? undefined : undefined) ?? undefined;
|
|
14657
|
+
}
|
|
14658
|
+
/**
|
|
14659
|
+
* Topic ownership by physical destination (#3055 review, Prism P1): an instance's topic is where its bot's platform
|
|
14660
|
+
* puts it (a Telegram group + thread, a Discord channel), so two bots in one group, or two instances on one
|
|
14661
|
+
* channel, are seen to share it; and every connection's own General is protected, with or without a General agent.
|
|
14662
|
+
*/
|
|
14663
|
+
topicOwnership() {
|
|
14664
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
14665
|
+
const destinationOf = (n) => {
|
|
14666
|
+
const cfg = this.fleetConfig?.instances?.[n];
|
|
14667
|
+
if (!cfg || cfg.topic_id == null || cfg.topic_id === "")
|
|
14668
|
+
return null;
|
|
14669
|
+
return this.destinationFor(this.topicAdapterIdOf(n), cfg.topic_id);
|
|
14670
|
+
};
|
|
14671
|
+
const generalDestinations = () => {
|
|
14672
|
+
const out = new Set();
|
|
14673
|
+
for (const ch of channels) {
|
|
14674
|
+
const id = ch.id ?? ch.type;
|
|
14675
|
+
const groupId = this.worlds.get(id)?.groupId ?? (ch.group_id != null ? String(ch.group_id) : undefined);
|
|
14676
|
+
if (ch.type === "telegram")
|
|
14677
|
+
out.add(topicDestination("telegram", groupId, id, 1));
|
|
14678
|
+
if (ch.type === "discord") {
|
|
14679
|
+
const gcid = discordGeneralChannelId(ch);
|
|
14680
|
+
if (gcid)
|
|
14681
|
+
out.add(topicDestination("discord", groupId, id, gcid));
|
|
14682
|
+
}
|
|
14683
|
+
}
|
|
14684
|
+
return out;
|
|
14685
|
+
};
|
|
14686
|
+
return { instances: this.fleetConfig?.instances, destinationOf, generalDestinations };
|
|
14687
|
+
}
|
|
14688
|
+
/**
|
|
14689
|
+
* The bot an agent's configured topic is on (#3105 review, Prism P1): a fleet.yaml agent's `channel_id` (else the
|
|
14690
|
+
* primary connection) whatever its runtime — a per-turn Windows agent has no execution adapter
|
|
14691
|
+
* (getInstanceAdapterId), yet its topic is still on that bot, and must be counted as an owner and deleted through it.
|
|
14692
|
+
* Topic planning and ownership only; execution routing is getInstanceAdapterId's and unchanged.
|
|
14693
|
+
*/
|
|
14694
|
+
topicAdapterIdOf(name) {
|
|
14695
|
+
const cfg = this.fleetConfig?.instances?.[name];
|
|
14696
|
+
if (cfg)
|
|
14697
|
+
return cfg.channel_id ?? this.getPrimaryAdapterId();
|
|
14698
|
+
return this.getInstanceAdapterId(name);
|
|
14699
|
+
}
|
|
14700
|
+
/** What deleting `name`'s topic would act on: its own bot only — a bot that is not there is never substituted. */
|
|
14701
|
+
planInstanceTopicDelete(name) {
|
|
14702
|
+
const adapterId = this.topicAdapterIdOf(name);
|
|
14703
|
+
return planInstanceTopicDelete({ name, adapterId, adapter: this.instanceTopicAdapter(adapterId), ...this.topicOwnership() });
|
|
13514
14704
|
}
|
|
13515
14705
|
getForumTopicDeleter(adapterId) {
|
|
13516
14706
|
const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
|
|
@@ -13789,6 +14979,8 @@ export class FleetManager {
|
|
|
13789
14979
|
this.observeWebAuthority(); // #2453 review: so is a web-authority switch (off is always seen)
|
|
13790
14980
|
if (!this.fleetConfig || !this.configPath)
|
|
13791
14981
|
return;
|
|
14982
|
+
// #3147 r2: a changed or removed Hub representative's consent is invalidated durably BEFORE the change is written.
|
|
14983
|
+
this.observeHubAgentIncarnations(true);
|
|
13792
14984
|
if (!this.savedFleetConfigSnapshot)
|
|
13793
14985
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
13794
14986
|
normalizeNoticeDelivery(this.fleetConfig);
|
|
@@ -13911,9 +15103,12 @@ export class FleetManager {
|
|
|
13911
15103
|
Object.assign(current, resolved);
|
|
13912
15104
|
}
|
|
13913
15105
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
15106
|
+
this.nativeMcpStore?.observePolicies();
|
|
15107
|
+
this.agentCredentialsCache?.observePolicies();
|
|
13914
15108
|
this.refreshSavedDashboardPermissions();
|
|
13915
15109
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
13916
15110
|
this.observeRuntimeSwitches(); // M-2255: a saved Windows runtime switch applies now
|
|
15111
|
+
this.observeHubSwitches(); // S-51: so does a saved Hub switch
|
|
13917
15112
|
}
|
|
13918
15113
|
/** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
|
|
13919
15114
|
refreshSavedDashboardPermissions() {
|
|
@@ -14223,6 +15418,7 @@ export class FleetManager {
|
|
|
14223
15418
|
const instanceDirToClean = this.getInstanceDir(name);
|
|
14224
15419
|
// #1774: replies sent from here on are a later instance's (a replacement registered while this one goes); the ones
|
|
14225
15420
|
// sent before wait for the outcome before they are shown.
|
|
15421
|
+
this.forgetHubAgentIncarnation(name, true); // #3147: a re-added instance never inherits Hub consent; not durable → refused
|
|
14226
15422
|
const removedBelow = this.webChatGeneration(name) + 1, removedDaemon = this.daemons.get(name);
|
|
14227
15423
|
this.webChatGenerations.set(name, removedBelow);
|
|
14228
15424
|
let settleRemoval;
|
|
@@ -16207,6 +17403,11 @@ export class FleetManager {
|
|
|
16207
17403
|
needsYouItems() {
|
|
16208
17404
|
return this.needsYou?.webItems() ?? [];
|
|
16209
17405
|
}
|
|
17406
|
+
/** Enabled member composition supplies only a local attention reader, never an approval/execution callback. */
|
|
17407
|
+
attachHubExecutionAttention(read) {
|
|
17408
|
+
this.hubExecutionAttention = read;
|
|
17409
|
+
this.needsYou?.poke();
|
|
17410
|
+
}
|
|
16210
17411
|
/**
|
|
16211
17412
|
* #1389: the org chart's structure for the web — the instances the dashboard lists (getUiStatus's names), General,
|
|
16212
17413
|
* fleet.yaml's teams, each instance's description and thread link. Read on demand; the live state rides the stream.
|
|
@@ -16270,6 +17471,7 @@ export class FleetManager {
|
|
|
16270
17471
|
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).filter(([, cfg]) => !isWindowsInstance(cfg)).map(([name, cfg]) => ({
|
|
16271
17472
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
16272
17473
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
17474
|
+
...(this.backendNameOf(name) === "codex" ? { profileTimelinePath: join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE) } : {}),
|
|
16273
17475
|
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
16274
17476
|
}));
|
|
16275
17477
|
const taken = new Set(fleet.map(i => i.name));
|
|
@@ -16281,6 +17483,9 @@ export class FleetManager {
|
|
|
16281
17483
|
claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
|
|
16282
17484
|
profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
16283
17485
|
kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
17486
|
+
// #3066 §1d (Fable r1 #2): a ClassicBot Codex room records its launches too (onDaemonReady) — read them the same way.
|
|
17487
|
+
...((this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend) === "codex"
|
|
17488
|
+
? { profileTimelinePath: join(this.getInstanceDir(ch.instanceName), PROFILE_TIMELINE_FILE) } : {}),
|
|
16284
17489
|
}));
|
|
16285
17490
|
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
16286
17491
|
}
|
|
@@ -16441,7 +17646,8 @@ export class FleetManager {
|
|
|
16441
17646
|
out.push({
|
|
16442
17647
|
name, state: p.state ?? undefined,
|
|
16443
17648
|
interaction: p.interaction ? { kind: p.interaction.kind, owner: wait?.owner ?? null, episode: p.interaction.episode, since: p.interaction.since } : null,
|
|
16444
|
-
interactionSummary: p.interaction_summary ?? null,
|
|
17649
|
+
interactionSummary: p.interaction?.kind === "kiro_choice" ? t("kiro3.running") : p.interaction_summary ?? null,
|
|
17650
|
+
kiroCompatibility: this.kiroCompatibilityHolds?.get(name),
|
|
16445
17651
|
pauseReason, pausedAt,
|
|
16446
17652
|
crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
|
|
16447
17653
|
signIn: pauseReason === "auth" ? this.signInFor(name) : null,
|
|
@@ -16533,8 +17739,18 @@ export class FleetManager {
|
|
|
16533
17739
|
throw new Error("the delivery outbox is not open");
|
|
16534
17740
|
return this.deliveryOutbox.acknowledge(deliveryId, by);
|
|
16535
17741
|
},
|
|
17742
|
+
missedOffline: () => (this.missedOfflineStore?.list(Date.now()) ?? []).map(m => ({
|
|
17743
|
+
id: m.id, instance: m.instance, owner: m.adapterId, username: m.username, place: m.place, preview: m.preview, since: m.since,
|
|
17744
|
+
})),
|
|
17745
|
+
acknowledgeMissed: (id, by) => this.missedOfflineStore?.acknowledge(id, by) ?? false,
|
|
16536
17746
|
tasks: () => this.needsYouTaskItems(),
|
|
16537
17747
|
backendSignIns: () => this.needsYouBackendSignIns(),
|
|
17748
|
+
hubExecution: () => this.hubExecutionAttention?.() ?? [],
|
|
17749
|
+
hubHolds: () => this.hubHolds.map(h => {
|
|
17750
|
+
// A provider hold carries a bounded setup reason; its guidance (S4b's copy) when present, else the code's text.
|
|
17751
|
+
const guidance = h.reason ? t(`hub.provider.${h.reason}`) : "";
|
|
17752
|
+
return { side: h.side, code: h.code, since: h.since, detail: guidance && guidance !== `hub.provider.${h.reason}` ? guidance : t(`hub.hold.${h.code}`) };
|
|
17753
|
+
}),
|
|
16538
17754
|
configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
|
|
16539
17755
|
...issue, detail: configIssueText(issue, t), owner: this.worlds.keys().next().value,
|
|
16540
17756
|
})),
|
|
@@ -17268,7 +18484,12 @@ export class FleetManager {
|
|
|
17268
18484
|
if (this.shuttingDown || !this.outsideOn() || this.outsideOpening)
|
|
17269
18485
|
return;
|
|
17270
18486
|
this.outsideOpening = true;
|
|
17271
|
-
|
|
18487
|
+
// alpha CI 4abe58cd: an On right after an Off finds the old link still closing (its tunnel stopping): wait for that
|
|
18488
|
+
// close, then open — never "busy" and a 30 s retry. The switch is asked again after the wait by openUnattended
|
|
18489
|
+
// itself (permitted → publicOwnerCurrent: the fleet running, the switch on): an Off meanwhile opens nothing.
|
|
18490
|
+
const link = this.getPublicWebLink();
|
|
18491
|
+
void link.whenClosed().then(() => link.openUnattended(OUTSIDE_LINK_OWNER))
|
|
18492
|
+
.catch(() => "failed").then(outcome => {
|
|
17272
18493
|
this.outsideOpening = false;
|
|
17273
18494
|
if (outcome === "opened") {
|
|
17274
18495
|
this.logger.info({ attempt: n + 1 }, "Outside link opened");
|
|
@@ -17446,7 +18667,7 @@ export class FleetManager {
|
|
|
17446
18667
|
// Over a link: the temporary one (§0.3), else the named one (a record from before §0.3 has no peer).
|
|
17447
18668
|
...(sessions.get(a.handle).surface === "gateway" ? (a.peer === "quick_link" ? { quick: true } : { named: true }) : {}) })) : [];
|
|
17448
18669
|
const host = this.fleetConfig?.web?.public_link?.hostname;
|
|
17449
|
-
return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null };
|
|
18670
|
+
return { state, configured, hours: VERIFIED_SESSION_HOURS, lanHosts, authorized, namedHost: typeof host === "string" && host ? host : null, webOnly: this.isWebOnlyFleet() };
|
|
17450
18671
|
}
|
|
17451
18672
|
authorizationRequests = null;
|
|
17452
18673
|
/**
|
|
@@ -17509,8 +18730,9 @@ export class FleetManager {
|
|
|
17509
18730
|
return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
|
|
17510
18731
|
},
|
|
17511
18732
|
signOut: (handle) => {
|
|
17512
|
-
this.webSessions?.revokeByHandle(handle);
|
|
17513
|
-
this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
|
|
18733
|
+
const session = this.webSessions?.revokeByHandle(handle) ?? { found: false, durable: true };
|
|
18734
|
+
const authorization = this.sessionAuthorizationStore()?.prune(a => a.handle !== handle) ?? { dropped: 0, durable: true };
|
|
18735
|
+
return { durable: session.durable, authorizationDurable: authorization.durable };
|
|
17514
18736
|
},
|
|
17515
18737
|
event: (type, payload) => { try {
|
|
17516
18738
|
this.eventLog?.insert("web", type, payload);
|
|
@@ -17523,6 +18745,7 @@ export class FleetManager {
|
|
|
17523
18745
|
expired: (c) => t("web_auth.card_expired", c.label),
|
|
17524
18746
|
refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
|
|
17525
18747
|
revoked: (c) => t("web_auth.card_revoked", c.label),
|
|
18748
|
+
notSaved: (c) => t("web_auth.card_not_saved", c.label),
|
|
17526
18749
|
ended: (c) => t("web_auth.card_ended", c.label),
|
|
17527
18750
|
generalHint: () => t("web_auth.general_hint"),
|
|
17528
18751
|
confirmLabel: () => t("web_auth.confirm"),
|
|
@@ -18445,7 +19668,14 @@ export class FleetManager {
|
|
|
18445
19668
|
return;
|
|
18446
19669
|
}
|
|
18447
19670
|
const publication = this.beginCancelButtonPublication(instanceName, correlationId);
|
|
18448
|
-
|
|
19671
|
+
// M-2391: a re-post below a reply (preserveProgress) continues the same turn — its elapsed time continues from
|
|
19672
|
+
// the live bubble's start, as its tool list does. Restarting it at 0 meant an agent replying more often than the
|
|
19673
|
+
// threshold never showed elapsed time at all. A fresh turn (no preserve) starts at 0 as before.
|
|
19674
|
+
const carriedStart = preserveProgress ? this.liveButtonStart(instanceName) : undefined;
|
|
19675
|
+
const carriedMs = carriedStart !== undefined ? Math.max(0, Date.now() - carriedStart) : 0;
|
|
19676
|
+
const initialProgressText = carriedStart !== undefined && carriedMs >= this.progressMinElapsedMs()
|
|
19677
|
+
? FleetManager.progressText(carriedMs, null, this.progressMinElapsedMs(), this.progressPrefixFor(instanceName, carriedMs))
|
|
19678
|
+
: FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName, 0, 1));
|
|
18449
19679
|
try {
|
|
18450
19680
|
const sent = await adapter.notifyAlert(chatId, {
|
|
18451
19681
|
type: "cancel",
|
|
@@ -18468,7 +19698,7 @@ export class FleetManager {
|
|
|
18468
19698
|
// Elapsed time is measured from when this button was posted — i.e. from
|
|
18469
19699
|
// when the work was handed over — not from the pane's working transition,
|
|
18470
19700
|
// which resets if the CLI blips idle mid-turn.
|
|
18471
|
-
startedAt: Date.now(),
|
|
19701
|
+
startedAt: carriedStart ?? Date.now(),
|
|
18472
19702
|
// Matches the text notifyAlert just posted, so the first 60s tick does
|
|
18473
19703
|
// not re-edit identical text — which put a "(edited)" mark on Discord
|
|
18474
19704
|
// with nothing visibly changed.
|
|
@@ -18591,14 +19821,16 @@ export class FleetManager {
|
|
|
18591
19821
|
* Make a tool summary safe to paste into a channel message.
|
|
18592
19822
|
*
|
|
18593
19823
|
* The text is agent-controlled (it is built from tool inputs — file paths,
|
|
18594
|
-
* shell commands), so it gets flattened to one line, capped,
|
|
18595
|
-
* the two Discord mass-mention triggers.
|
|
18596
|
-
*
|
|
19824
|
+
* shell commands), so it gets flattened to one line, secrets masked, capped,
|
|
19825
|
+
* and stripped of the two Discord mass-mention triggers. Masking comes BEFORE
|
|
19826
|
+
* the cap: a cap can cut a mask's context but must never be what hides (or
|
|
19827
|
+
* fails to hide) a secret. Neither channel renders it with a parse mode, so no
|
|
19828
|
+
* markup escaping is needed beyond that.
|
|
18597
19829
|
*/
|
|
18598
19830
|
static sanitizeActivity(activity) {
|
|
18599
19831
|
if (!activity)
|
|
18600
19832
|
return null;
|
|
18601
|
-
const flat = activity.replace(/\s+/g, " ").replace(/@(everyone|here)/g, "@$1").trim();
|
|
19833
|
+
const flat = maskSecrets(activity.replace(/\s+/g, " ")).replace(/@(everyone|here)/g, "@$1").trim();
|
|
18602
19834
|
if (!flat)
|
|
18603
19835
|
return null;
|
|
18604
19836
|
return flat.length > PROGRESS_ACTIVITY_MAX_CHARS
|
|
@@ -18717,6 +19949,12 @@ export class FleetManager {
|
|
|
18717
19949
|
}
|
|
18718
19950
|
return PROGRESS_MIN_ELAPSED_MS;
|
|
18719
19951
|
}
|
|
19952
|
+
/** M-2391: when the instance's live (not retiring) bubble started — the current turn's start — or undefined. */
|
|
19953
|
+
liveButtonStart(instanceName) {
|
|
19954
|
+
const starts = [...this.cancelButtons.values()].filter((e) => e.instanceName === instanceName && !e.retiring && typeof e.startedAt === "number")
|
|
19955
|
+
.map((e) => e.startedAt);
|
|
19956
|
+
return starts.length ? Math.min(...starts) : undefined;
|
|
19957
|
+
}
|
|
18720
19958
|
startProgressTicker(entry) {
|
|
18721
19959
|
const tick = () => this.refreshBubble(entry);
|
|
18722
19960
|
entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
|
|
@@ -18726,7 +19964,9 @@ export class FleetManager {
|
|
|
18726
19964
|
// one additional edit per turn that lives past the threshold.
|
|
18727
19965
|
const firstAt = this.progressMinElapsedMs() - (Date.now() - (entry.startedAt ?? Date.now()));
|
|
18728
19966
|
if (firstAt > 0 && firstAt < PROGRESS_UPDATE_INTERVAL_MS) {
|
|
18729
|
-
|
|
19967
|
+
// M-2391: a little past the threshold — a timer can fire a millisecond before Date.now() reaches it, which
|
|
19968
|
+
// rendered the unchanged "處理中…" text and skipped the edit until the next 60 s interval.
|
|
19969
|
+
const firstTick = setTimeout(tick, firstAt + PROGRESS_THRESHOLD_MARGIN_MS);
|
|
18730
19970
|
firstTick.unref?.();
|
|
18731
19971
|
}
|
|
18732
19972
|
}
|
|
@@ -19065,6 +20305,8 @@ export class FleetManager {
|
|
|
19065
20305
|
}
|
|
19066
20306
|
catch { /* safe metadata only */ }
|
|
19067
20307
|
broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
|
|
20308
|
+
if (event.phase === "done")
|
|
20309
|
+
this.forgetSignInIdentities(); // a sign-in may be a different account now
|
|
19068
20310
|
this.needsYou?.poke();
|
|
19069
20311
|
if (!this.browserSignInOwnerCurrent(owner))
|
|
19070
20312
|
return;
|
|
@@ -19103,7 +20345,10 @@ export class FleetManager {
|
|
|
19103
20345
|
cancel: () => this.cancelLoginSession(),
|
|
19104
20346
|
notice: ({ shown, label, handle }) => this.notifyFleetError(t("login.web_dashboard_notice", shown, label, handle.slice(0, 8))),
|
|
19105
20347
|
// No details in the event: each page reads its own view (GET /ui/backend-login), as its session may see it.
|
|
19106
|
-
|
|
20348
|
+
// The accounts are forgotten when ANY sign-in ends, even one a later one replaced (it may be a different account now), and not at every step of a
|
|
20349
|
+
// running one: `changed` only tells the pages to read again.
|
|
20350
|
+
changed: () => { broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")); },
|
|
20351
|
+
loginEnded: () => this.forgetSignInIdentities(),
|
|
19107
20352
|
});
|
|
19108
20353
|
return this.webBackendLoginsInstance;
|
|
19109
20354
|
}
|
|
@@ -19117,6 +20362,135 @@ export class FleetManager {
|
|
|
19117
20362
|
* #2233: Settings → Agents' sign-in list — every backend's default login as the availability snapshot has it, then
|
|
19118
20363
|
* each credential profile an agent uses (its own login state, read offline).
|
|
19119
20364
|
*/
|
|
20365
|
+
/** Which account each sign-in is, as the host's own `claude auth status` said (masked; see checkAuthIdentity). Keys are `backend\0profile` in a Map: a name is data, never a property. */
|
|
20366
|
+
signInIdentities = new Map();
|
|
20367
|
+
/** Reads in flight, by the same key: a second request for a sign-in that is already being read joins it instead of starting another process. */
|
|
20368
|
+
signInIdentityReads = new Map();
|
|
20369
|
+
/** Raised each time the accounts are forgotten: a read that began before is never allowed to write what it found. */
|
|
20370
|
+
signInIdentityGeneration = 0;
|
|
20371
|
+
signInIdentitySlots = 0;
|
|
20372
|
+
signInIdentityWaiters = [];
|
|
20373
|
+
signInIdentity(backend, profile) { return this.signInIdentities.get(`${backend}\0${profile ?? ""}`)?.identity ?? undefined; }
|
|
20374
|
+
/** A sign-in finished (or changed): forget what was read and let no read that was already running put its answer back. */
|
|
20375
|
+
forgetSignInIdentities() { this.signInIdentityGeneration++; this.signInIdentities.clear(); this.signInIdentityReads.clear(); }
|
|
20376
|
+
/** At most three `claude auth status` at once across the whole host, however many requests ask. */
|
|
20377
|
+
async withSignInIdentitySlot(task) {
|
|
20378
|
+
while (this.signInIdentitySlots >= 3)
|
|
20379
|
+
await new Promise(resolve => this.signInIdentityWaiters.push(resolve));
|
|
20380
|
+
this.signInIdentitySlots++;
|
|
20381
|
+
try {
|
|
20382
|
+
return await task();
|
|
20383
|
+
}
|
|
20384
|
+
finally {
|
|
20385
|
+
this.signInIdentitySlots--;
|
|
20386
|
+
this.signInIdentityWaiters.shift()?.();
|
|
20387
|
+
}
|
|
20388
|
+
}
|
|
20389
|
+
/** One `claude auth status` for a sign-in (the default one, or a credential profile's folder). */
|
|
20390
|
+
async readSignInIdentity(target) {
|
|
20391
|
+
let env;
|
|
20392
|
+
if (target.profile)
|
|
20393
|
+
env = credentialProfileEnv(this.dataDir, target.backend, target.profile) ?? undefined;
|
|
20394
|
+
return checkAuthIdentity(target.backend, env);
|
|
20395
|
+
}
|
|
20396
|
+
/**
|
|
20397
|
+
* Start reading, in the background, the account of each Claude Code sign-in that is signed in (the default and every profile an agent uses) and
|
|
20398
|
+
* not read in the last five minutes. It never waits and never blocks a request: the list shows what is already known, and the pages are told to
|
|
20399
|
+
* read it again when something new was learned. A sign-in already being read is not read twice, and a read that began before the accounts were
|
|
20400
|
+
* forgotten (a sign-in finished meanwhile) is dropped. No login, no network request of ours, nothing logged.
|
|
20401
|
+
*/
|
|
20402
|
+
refreshSignInIdentities() {
|
|
20403
|
+
const now = Date.now(), fresh = (target) => { const entry = this.signInIdentities.get(`${target.backend}\0${target.profile ?? ""}`); return !!entry && now - entry.at < 5 * 60_000; };
|
|
20404
|
+
const started = [];
|
|
20405
|
+
for (const target of this.backendSignInTargets()) {
|
|
20406
|
+
const key = `${target.backend}\0${target.profile ?? ""}`;
|
|
20407
|
+
if (target.state !== "usable" || !authIdentitySupported(target.backend) || fresh(target) || this.signInIdentityReads.has(key))
|
|
20408
|
+
continue;
|
|
20409
|
+
const generation = this.signInIdentityGeneration;
|
|
20410
|
+
const read = this.withSignInIdentitySlot(() => this.readSignInIdentity(target)).then(identity => {
|
|
20411
|
+
if (generation !== this.signInIdentityGeneration)
|
|
20412
|
+
return false; // forgotten while it ran: its answer may be the old account
|
|
20413
|
+
this.signInIdentities.set(key, { identity: identity ?? null, at: Date.now() });
|
|
20414
|
+
return true;
|
|
20415
|
+
}).catch(() => {
|
|
20416
|
+
// A read that threw is an answer too: remembered as "no account" for the same five minutes, so every GET does not try again.
|
|
20417
|
+
if (generation === this.signInIdentityGeneration)
|
|
20418
|
+
this.signInIdentities.set(key, { identity: null, at: Date.now() });
|
|
20419
|
+
return false;
|
|
20420
|
+
}).finally(() => { if (this.signInIdentityReads.get(key) === read)
|
|
20421
|
+
this.signInIdentityReads.delete(key); });
|
|
20422
|
+
this.signInIdentityReads.set(key, read);
|
|
20423
|
+
started.push(read);
|
|
20424
|
+
}
|
|
20425
|
+
if (started.length)
|
|
20426
|
+
void Promise.all(started).then(results => {
|
|
20427
|
+
if (results.some(Boolean))
|
|
20428
|
+
broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed")); // read the list again: it knows more now
|
|
20429
|
+
});
|
|
20430
|
+
}
|
|
20431
|
+
/**
|
|
20432
|
+
* #3212 PR 1: the Accounts view (accounts-view.ts) — every CLI's default sign-in and named profiles (used or only on
|
|
20433
|
+
* disk), who uses each and its weekly usage. Host rows only; a paired computer's rows come with #3190 Part 3c. Its
|
|
20434
|
+
* usage is the usage panel's cached read (never forced), given at most `usageWaitMs` before the view is sent without it.
|
|
20435
|
+
*/
|
|
20436
|
+
async accountsView(usageWaitMs = 1500) {
|
|
20437
|
+
const fleet = this.fleetConfig;
|
|
20438
|
+
const defaultBackend = fleet?.defaults?.backend ?? "claude-code";
|
|
20439
|
+
this.refreshSignInIdentities();
|
|
20440
|
+
const targets = this.backendSignInTargets();
|
|
20441
|
+
const diskProfiles = new Map();
|
|
20442
|
+
for (const backend of PROFILE_CLIS) {
|
|
20443
|
+
const rows = [];
|
|
20444
|
+
for (const profile of profilesOnDisk(this.dataDir, backend)) {
|
|
20445
|
+
let login = "unknown";
|
|
20446
|
+
try {
|
|
20447
|
+
login = credentialProfileLogin(this.dataDir, backend, profile).state;
|
|
20448
|
+
}
|
|
20449
|
+
catch { /* unknown */ }
|
|
20450
|
+
const signIn = dashboardSignInSupported(backend, profile);
|
|
20451
|
+
const hostCommand = signIn ? null : credentialProfileLoginCommand(this.dataDir, backend, profile);
|
|
20452
|
+
const identity = this.signInIdentity(backend, profile);
|
|
20453
|
+
rows.push({ profile, state: (login === "logged-in" ? "usable" : login === "signed-out" ? "not_signed_in" : "unknown"), signIn,
|
|
20454
|
+
...(hostCommand ? { hostCommand } : {}), ...(identity ? { identity } : {}) });
|
|
20455
|
+
}
|
|
20456
|
+
diskProfiles.set(backend, rows);
|
|
20457
|
+
}
|
|
20458
|
+
// Host agents only: an agent on a paired Windows computer runs on that computer's accounts (its own scope, later).
|
|
20459
|
+
const agents = Object.keys(fleet?.instances ?? {}).filter(name => !isWindowsInstance(fleet.instances[name])).sort()
|
|
20460
|
+
.map(name => { const backend = this.backendNameForInstance(name); return { name, backend, profile: this.credentialProfileOf(name, backend) }; });
|
|
20461
|
+
const classic = readClassicBindings(this.dataDir).map(b => {
|
|
20462
|
+
const backend = b.backend ?? defaultBackend;
|
|
20463
|
+
return { key: b.key, backend, profile: profileName(classicProfile(b.backend_options, fleet?.defaults?.backend_options, backend)) };
|
|
20464
|
+
});
|
|
20465
|
+
const fleetDefaults = new Map();
|
|
20466
|
+
for (const backend of PROFILE_CLIS) {
|
|
20467
|
+
const p = profileName(profileOf(fleet?.defaults?.backend_options, backend));
|
|
20468
|
+
if (p)
|
|
20469
|
+
fleetDefaults.set(backend, p);
|
|
20470
|
+
}
|
|
20471
|
+
// Weekly usage, from the usage panel's own (cached, never forced) read.
|
|
20472
|
+
const usage = new Map();
|
|
20473
|
+
const ids = new Map(); // provider id → backend\0profile
|
|
20474
|
+
for (const backend of ACCOUNT_CLIS) {
|
|
20475
|
+
const pid = usageProviderIdForBackend(backend);
|
|
20476
|
+
if (!pid)
|
|
20477
|
+
continue;
|
|
20478
|
+
ids.set(pid, `${backend}\0`);
|
|
20479
|
+
for (const p of new Set([...(diskProfiles.get(backend) ?? []).map(d => d.profile), ...targets.filter(t => t.backend === backend && t.profile).map(t => t.profile)]))
|
|
20480
|
+
ids.set(`${pid}:${p}`, `${backend}\0${p}`);
|
|
20481
|
+
}
|
|
20482
|
+
try {
|
|
20483
|
+
const payload = await Promise.race([getUsageSnapshot(false, undefined, { allowStale: true }), new Promise(r => { const tm = setTimeout(() => r(null), usageWaitMs); tm.unref?.(); })]);
|
|
20484
|
+
for (const provider of payload?.providers ?? []) {
|
|
20485
|
+
const at = ids.get(provider.id);
|
|
20486
|
+
const w = at ? weeklyUsage(provider) : undefined;
|
|
20487
|
+
if (at && w)
|
|
20488
|
+
usage.set(at, w);
|
|
20489
|
+
}
|
|
20490
|
+
}
|
|
20491
|
+
catch { /* the view without usage */ }
|
|
20492
|
+
return buildAccountsView({ targets, diskProfiles, agents, classic, fleetDefaults, usage });
|
|
20493
|
+
}
|
|
19120
20494
|
backendSignInTargets() {
|
|
19121
20495
|
const out = [];
|
|
19122
20496
|
for (const row of this.backendAvailabilityRows()) {
|
|
@@ -19128,6 +20502,7 @@ export class FleetManager {
|
|
|
19128
20502
|
...(flow?.remoteLogin === "unsupported" ? { hostCommand: flow.command } : {}),
|
|
19129
20503
|
...(row.state === "not_installed" && install ? { install } : {}),
|
|
19130
20504
|
...(row.state === "too_old" && install ? { update: install } : {}),
|
|
20505
|
+
...(this.signInIdentity(row.backend) ? { identity: this.signInIdentity(row.backend) } : {}),
|
|
19131
20506
|
});
|
|
19132
20507
|
}
|
|
19133
20508
|
const seen = new Set();
|
|
@@ -19145,7 +20520,7 @@ export class FleetManager {
|
|
|
19145
20520
|
const signIn = dashboardSignInSupported(backend, profile);
|
|
19146
20521
|
const hostCommand = signIn ? null : credentialProfileLoginCommand(this.dataDir, backend, profile);
|
|
19147
20522
|
out.push({ backend, profile, state: login === "logged-in" ? "usable" : login === "signed-out" ? "not_signed_in" : "unknown", signIn,
|
|
19148
|
-
...(hostCommand ? { hostCommand } : {}) });
|
|
20523
|
+
...(hostCommand ? { hostCommand } : {}), ...(this.signInIdentity(backend, profile) ? { identity: this.signInIdentity(backend, profile) } : {}) });
|
|
19149
20524
|
}
|
|
19150
20525
|
return out;
|
|
19151
20526
|
}
|
|
@@ -19664,9 +21039,12 @@ export class FleetManager {
|
|
|
19664
21039
|
this.loginWindow.release(claim);
|
|
19665
21040
|
return t("login.web_shutting_down");
|
|
19666
21041
|
}
|
|
21042
|
+
// The wrapper says its own exit code after this marker: tmux may never record it (unattended-install.ts).
|
|
21043
|
+
const exitMarker = installExitMarker();
|
|
19667
21044
|
const flow = {
|
|
19668
21045
|
backend,
|
|
19669
|
-
command: unattendedInstallCommand(info.unattended ?? info.install, shell),
|
|
21046
|
+
command: unattendedInstallCommand(info.unattended ?? info.install, shell, exitMarker),
|
|
21047
|
+
exitMarker,
|
|
19670
21048
|
successPattern: /$^/,
|
|
19671
21049
|
timeoutMs: this.installTimeoutMs,
|
|
19672
21050
|
};
|
|
@@ -19967,6 +21345,8 @@ export class FleetManager {
|
|
|
19967
21345
|
this.needsYou?.poke();
|
|
19968
21346
|
const onError = (err) => this.logger.debug({ err }, "SSE client write failed; evicting");
|
|
19969
21347
|
if (event === "message" && data && typeof data === "object") {
|
|
21348
|
+
// #2861: an Office share's dialogue box hears that someone talked to an instance (never what was said).
|
|
21349
|
+
this.officeTalk.noteMessage(data);
|
|
19970
21350
|
// A chat message: record it, and send it WITH its id so a reconnecting stream can ask for what it missed.
|
|
19971
21351
|
const m = data;
|
|
19972
21352
|
const recorded = this.webChatHistory.record({
|
|
@@ -20011,9 +21391,12 @@ export class FleetManager {
|
|
|
20011
21391
|
this.taskBoardWaker?.stop();
|
|
20012
21392
|
// #2303: each instance's cards at once, read at every claim (a fleet.yaml edit applies to the next one).
|
|
20013
21393
|
db.board.setCardLimit((name) => boardCardLimit(this.fleetConfig, name));
|
|
21394
|
+
// An instance with the task board switched off is handed no cards and sent no notices (read at each write and wake).
|
|
21395
|
+
db.board.setBoardOff((name) => taskBoardOffIn(this.fleetConfig, name));
|
|
20014
21396
|
const waker = this.taskBoardWaker = new TaskBoardWaker({
|
|
20015
21397
|
deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
|
|
20016
21398
|
isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
|
|
21399
|
+
boardOff: (name) => taskBoardOffIn(this.fleetConfig, name),
|
|
20017
21400
|
// #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
|
|
20018
21401
|
messagedAbout: (from, to, codes, sinceMs) => {
|
|
20019
21402
|
const outbox = this.deliveryOutbox;
|
|
@@ -23077,6 +24460,9 @@ export class FleetManager {
|
|
|
23077
24460
|
* settles, so a later genuine stop (after a restart) still does the work.
|
|
23078
24461
|
*/
|
|
23079
24462
|
stopAll() {
|
|
24463
|
+
// S-51: no new Hub admission, dispatch or listener work from this tick on; doStopAll awaits the close.
|
|
24464
|
+
this.hubEffectsReady = false;
|
|
24465
|
+
this.hubRuntime?.fence();
|
|
23080
24466
|
this.lifecycle.cancelNativeRecovery?.();
|
|
23081
24467
|
this.nativeMcpStore?.closeAll();
|
|
23082
24468
|
this.runtimeExecutor?.close();
|
|
@@ -23149,6 +24535,22 @@ export class FleetManager {
|
|
|
23149
24535
|
// because we asked it to. Set synchronously — doStopAll runs to its first
|
|
23150
24536
|
// await in the same tick as the signal handler, so no event can slip in.
|
|
23151
24537
|
this.shuttingDown = true;
|
|
24538
|
+
// Stop receive admission before the first await or any instance shutdown; outbound
|
|
24539
|
+
// adapters remain available below to retire notices and pending prompts.
|
|
24540
|
+
for (const state of this.adapterState.values()) {
|
|
24541
|
+
if (state.retryTimer) {
|
|
24542
|
+
clearTimeout(state.retryTimer);
|
|
24543
|
+
state.retryTimer = undefined;
|
|
24544
|
+
}
|
|
24545
|
+
}
|
|
24546
|
+
const inboundStopped = Promise.all([...this.adapters.values()].map(async (adapter) => {
|
|
24547
|
+
try {
|
|
24548
|
+
await adapter.stopReceiving?.();
|
|
24549
|
+
}
|
|
24550
|
+
catch (err) {
|
|
24551
|
+
this.logger.warn({ adapterId: adapter.id, err }, "Adapter receive stop failed; inbound remains fenced");
|
|
24552
|
+
}
|
|
24553
|
+
}));
|
|
23152
24554
|
this.workingRecovery?.reset();
|
|
23153
24555
|
if (this.workingRecoveryTimer)
|
|
23154
24556
|
clearInterval(this.workingRecoveryTimer);
|
|
@@ -23179,6 +24581,17 @@ export class FleetManager {
|
|
|
23179
24581
|
// #1565: the web chat's last changes reach the disk (bounded: a slow disk does not hold the shutdown).
|
|
23180
24582
|
const webChatStopped = this.webChatStore ? Promise.race([this.webChatStore.flush().catch(() => { }), new Promise(r => setTimeout(r, 3_000).unref?.())]) : Promise.resolve();
|
|
23181
24583
|
const profileStopped = this.runtimeCpuProfiler?.shutdown("fleet shutdown");
|
|
24584
|
+
// S-51: the Hub closes in the same drain, bounded so an unanswering listener does not hold the shutdown.
|
|
24585
|
+
const hubInit = this.hubRuntimeInit, hubNow = this.hubRuntime;
|
|
24586
|
+
this.hubRuntime = null;
|
|
24587
|
+
this.hubRuntimeInit = null;
|
|
24588
|
+
const hubClose = async () => {
|
|
24589
|
+
const runtime = hubNow ?? (hubInit ? await hubInit.catch(() => null) : null);
|
|
24590
|
+
await runtime?.close();
|
|
24591
|
+
};
|
|
24592
|
+
const hubStopped = hubNow || hubInit
|
|
24593
|
+
? Promise.race([hubClose().catch(err => this.logger.warn({ err: err?.message }, "Hub did not close cleanly")), new Promise(r => setTimeout(r, 10_000).unref?.())])
|
|
24594
|
+
: Promise.resolve();
|
|
23182
24595
|
this.ipcStoppingInstances.add("__fleet_stopping__");
|
|
23183
24596
|
// Release held delivery promises before awaiting daemon shutdown, then
|
|
23184
24597
|
// reject spawn work which has not started. Otherwise a storm backoff could
|
|
@@ -23209,9 +24622,11 @@ export class FleetManager {
|
|
|
23209
24622
|
// A login/install window is a dedicated tmux server with its own TTL
|
|
23210
24623
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
23211
24624
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
24625
|
+
await inboundStopped;
|
|
23212
24626
|
await publicStopped;
|
|
23213
24627
|
await sharesStopped?.catch(() => { });
|
|
23214
24628
|
await profileStopped;
|
|
24629
|
+
await hubStopped;
|
|
23215
24630
|
await needsStopped;
|
|
23216
24631
|
await settingsControlStopped;
|
|
23217
24632
|
await cacheStopped;
|
|
@@ -23219,13 +24634,6 @@ export class FleetManager {
|
|
|
23219
24634
|
await this.cpuProfileControl?.close();
|
|
23220
24635
|
this.cpuProfileControl = null;
|
|
23221
24636
|
await this.shutdownLoginWindows();
|
|
23222
|
-
// Cancel adapter retry timers
|
|
23223
|
-
for (const state of this.adapterState.values()) {
|
|
23224
|
-
if (state.retryTimer) {
|
|
23225
|
-
clearTimeout(state.retryTimer);
|
|
23226
|
-
state.retryTimer = undefined;
|
|
23227
|
-
}
|
|
23228
|
-
}
|
|
23229
24637
|
this.clearStatuslineWatchers();
|
|
23230
24638
|
this.costGuard?.stop();
|
|
23231
24639
|
this.dailySummary?.stop();
|
|
@@ -23263,6 +24671,15 @@ export class FleetManager {
|
|
|
23263
24671
|
clearInterval(this.discordPresenceTimer);
|
|
23264
24672
|
this.discordPresenceTimer = null;
|
|
23265
24673
|
}
|
|
24674
|
+
if (this.quotaReadTimer) {
|
|
24675
|
+
clearInterval(this.quotaReadTimer);
|
|
24676
|
+
this.quotaReadTimer = null;
|
|
24677
|
+
}
|
|
24678
|
+
if (this.capacityTimer) {
|
|
24679
|
+
clearInterval(this.capacityTimer);
|
|
24680
|
+
this.capacityTimer = null;
|
|
24681
|
+
}
|
|
24682
|
+
setCapacityLookup(null);
|
|
23266
24683
|
if (this.discordPresenceEagerTimer) {
|
|
23267
24684
|
clearTimeout(this.discordPresenceEagerTimer);
|
|
23268
24685
|
this.discordPresenceEagerTimer = null;
|
|
@@ -23549,6 +24966,7 @@ export class FleetManager {
|
|
|
23549
24966
|
this.loadedFleetSource = previousSource;
|
|
23550
24967
|
this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
|
|
23551
24968
|
this.observeRuntimeSwitches(); // M-2255: and the Windows runtimes' switches
|
|
24969
|
+
this.observeHubSwitches(); // S-51: and the Hub's
|
|
23552
24970
|
this.logger.error({
|
|
23553
24971
|
oldCount,
|
|
23554
24972
|
newCount,
|
|
@@ -23568,6 +24986,8 @@ export class FleetManager {
|
|
|
23568
24986
|
// mark every row done and tell the user "changes applied".
|
|
23569
24987
|
return { rejected: why };
|
|
23570
24988
|
}
|
|
24989
|
+
this.nativeMcpStore?.observePolicies();
|
|
24990
|
+
this.agentCredentialsCache?.observePolicies();
|
|
23571
24991
|
if (this.loadedFleetSource)
|
|
23572
24992
|
this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
|
|
23573
24993
|
this.migrateOfficeStorage();
|
|
@@ -24468,6 +25888,9 @@ export class FleetManager {
|
|
|
24468
25888
|
}
|
|
24469
25889
|
async runConnectionBindingApply(job, binding, execution) {
|
|
24470
25890
|
try {
|
|
25891
|
+
// #3055 review r2: a connection is not re-pointed while one of its topics is being deleted (nothing is changed).
|
|
25892
|
+
if (this.topicDeletionPending())
|
|
25893
|
+
throw new Error("a topic deletion is running; try again in a moment");
|
|
24471
25894
|
await this.rebuildAdapterForBinding(job.connectionId, binding, execution);
|
|
24472
25895
|
job.result = "applied";
|
|
24473
25896
|
}
|
|
@@ -25638,8 +27061,13 @@ export class FleetManager {
|
|
|
25638
27061
|
return;
|
|
25639
27062
|
}
|
|
25640
27063
|
const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
|
|
27064
|
+
const publicUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
25641
27065
|
if (remoteRuntimeEnabled() && !gatewayRequestContext(req)
|
|
25642
|
-
&& this.runtimeCoordinator().handlePublicRequest(req, res,
|
|
27066
|
+
&& this.runtimeCoordinator().handlePublicRequest(req, res, publicUrl))
|
|
27067
|
+
return;
|
|
27068
|
+
// The read-only installer files (/runtime/install.ps1 and the bundle ZIP) grant nothing. They answer 404 while remote runtimes
|
|
27069
|
+
// are off or on a public gateway link, and touch no other path.
|
|
27070
|
+
if (this.runtimeInstaller().handle(req, res, publicUrl))
|
|
25643
27071
|
return;
|
|
25644
27072
|
// Branding is public, after Host admission: automatic favicon requests
|
|
25645
27073
|
// must not become noisy web-token 401s in the browser console.
|
|
@@ -25668,7 +27096,7 @@ export class FleetManager {
|
|
|
25668
27096
|
&& String(req.headers.accept ?? "").includes("text/html")
|
|
25669
27097
|
&& isWebPageNavigation(requestPath)) {
|
|
25670
27098
|
const link = gatewayRequestContext(req);
|
|
25671
|
-
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId) });
|
|
27099
|
+
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet(), outside: !!link && this.isOutsideLink(link.exposureId), deviceLogin: !link && this.deviceLoginAvailable(), loginQr: this.loginQrEnabled() });
|
|
25672
27100
|
return;
|
|
25673
27101
|
}
|
|
25674
27102
|
res.writeHead(decision.status);
|
|
@@ -25840,6 +27268,7 @@ export class FleetManager {
|
|
|
25840
27268
|
res.end(JSON.stringify({
|
|
25841
27269
|
...sysInfo,
|
|
25842
27270
|
version: this.currentVersion,
|
|
27271
|
+
build: gatewayRequestContext(req) ? undefined : this.currentBuild,
|
|
25843
27272
|
instances: enriched,
|
|
25844
27273
|
publicLink: this.getPublicWebStatus(),
|
|
25845
27274
|
}));
|
|
@@ -26337,6 +27766,8 @@ export class FleetManager {
|
|
|
26337
27766
|
})(),
|
|
26338
27767
|
capabilities: { chat: true, settings: true, pause: true, cancel: true, teams: true, schedules: true, delegation: true, mcp: true,
|
|
26339
27768
|
commands: true, effort: false, log: true, details: true, channels: false, terminal: false, attachments: false, usage: false, send_while_busy: true },
|
|
27769
|
+
// cc3ce4fe: the version this machine's inventory reports for the agent's CLI, and whether AgEnD has audited it.
|
|
27770
|
+
...this.nativeCliStatus(nativeConfig.backend, nativeConfig.machine_id, this.daemons.get(name)),
|
|
26340
27771
|
...(nativeConfig.machine_id ? { machine_id: nativeConfig.machine_id, machine_name: this.runtimeHub?.machine(nativeConfig.machine_id)?.name,
|
|
26341
27772
|
machine_status: this.runtimeHub?.machine(nativeConfig.machine_id)?.status ?? "offline" } : {}) } : {}),
|
|
26342
27773
|
display_name: identity.display_name || undefined,
|