@songsid/agend 2.3.3 → 2.3.4-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/README.zh-TW.md +3 -3
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agend-home.d.ts +13 -0
- package/dist/agend-home.js +42 -0
- package/dist/agend-home.js.map +1 -1
- package/dist/agent-cli-instructions.md +2 -2
- 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/auth-api.d.ts +16 -3
- package/dist/auth-api.js +32 -8
- 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/authority/web-authority.d.ts +15 -1
- package/dist/authority/web-authority.js +27 -10
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +100 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +35 -0
- package/dist/backend/codex-session-lookup.js +93 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +27 -0
- package/dist/backend/codex.js +136 -23
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +13 -6
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +68 -5
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +13 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +6 -0
- package/dist/backend-availability.js +13 -5
- package/dist/backend-availability.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- 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 +27 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +238 -18
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/inbox-temp-name.d.ts +9 -0
- package/dist/channel/inbox-temp-name.js +13 -0
- package/dist/channel/inbox-temp-name.js.map +1 -0
- 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/chat-log-ids.d.ts +48 -0
- package/dist/chat-log-ids.js +182 -0
- package/dist/chat-log-ids.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +8 -1
- package/dist/classic-channel-manager.js +24 -2
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +4 -6
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +18 -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 +52 -0
- package/dist/daemon.js +538 -35
- package/dist/daemon.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 +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/dialog-screen-text.d.ts +16 -0
- package/dist/dialog-screen-text.js +51 -0
- package/dist/dialog-screen-text.js.map +1 -0
- package/dist/env-file.d.ts +2 -0
- package/dist/env-file.js +13 -8
- package/dist/env-file.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 +172 -12
- package/dist/fleet-manager.js +897 -139
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-patch.d.ts +3 -0
- package/dist/fleet-yaml-patch.js +103 -0
- package/dist/fleet-yaml-patch.js.map +1 -0
- package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
- 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/model-discovery/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +14 -4
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/install-stream.d.ts +53 -0
- package/dist/install-stream.js +134 -0
- package/dist/install-stream.js.map +1 -0
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +22 -2
- package/dist/instance-lifecycle.js +103 -19
- 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 +2 -2
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/interaction-observation.d.ts +1 -0
- package/dist/interaction-observation.js +1 -1
- package/dist/interaction-observation.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/kiro-resume-summary.d.ts +37 -0
- package/dist/kiro-resume-summary.js +85 -0
- package/dist/kiro-resume-summary.js.map +1 -0
- package/dist/locale.js +130 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +30 -0
- package/dist/log-service.js +36 -3
- 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 +12 -0
- package/dist/logger.js +43 -1
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.js +9 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.d.ts +6 -0
- package/dist/login-manager.js +10 -1
- package/dist/login-manager.js.map +1 -1
- package/dist/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/needs-you-hub.d.ts +6 -1
- package/dist/needs-you-hub.js +31 -8
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +22 -2
- package/dist/needs-you.js +17 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/next-steps.d.ts +12 -2
- package/dist/next-steps.js +19 -3
- package/dist/next-steps.js.map +1 -1
- package/dist/office-share-page.d.ts +1 -1
- package/dist/office-share-page.js +6 -2
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +57 -0
- package/dist/office-share-service.js +76 -11
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +8 -0
- package/dist/office-share.js +34 -0
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +59 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +10 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.js +27 -2
- package/dist/package-preimage.js.map +1 -1
- package/dist/page-share.d.ts +122 -0
- package/dist/page-share.js +199 -0
- package/dist/page-share.js.map +1 -0
- package/dist/public-preview.d.ts +4 -0
- package/dist/public-preview.js +5 -4
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-gateway.js +5 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/quickstart-api.d.ts +5 -0
- package/dist/quickstart-api.js +1 -1
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +40 -0
- package/dist/quickstart.js +174 -28
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.js +2 -0
- package/dist/remote-card-answer.js.map +1 -1
- package/dist/remote-card-prompt.js +4 -2
- package/dist/remote-card-prompt.js.map +1 -1
- package/dist/reply-buttons-controller.js +10 -0
- package/dist/reply-buttons-controller.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/selection-card.d.ts +20 -0
- package/dist/selection-card.js +202 -0
- package/dist/selection-card.js.map +1 -0
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +16 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +5 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/setup-host.js +2 -2
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/stall-reporter.d.ts +21 -0
- package/dist/stall-reporter.js +82 -0
- package/dist/stall-reporter.js.map +1 -0
- package/dist/tmux-control.d.ts +2 -2
- package/dist/tmux-control.js +4 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +6 -3
- package/dist/tmux-exec-helper.js +115 -53
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +6 -0
- package/dist/tmux-manager.js +17 -0
- package/dist/tmux-manager.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 +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +4 -0
- package/dist/tunnel/cloudflared-install.js +20 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +8 -1
- package/dist/tunnel/cloudflared.js +34 -6
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.d.ts +3 -3
- package/dist/tunnel/lease.js +23 -19
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js +6 -7
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/ui/chat-store.js +28 -4
- package/dist/ui/chat-strings.js +40 -4
- package/dist/ui/chat-thread.js +52 -10
- package/dist/ui/fleet-canvas.js +4 -3
- package/dist/ui/fleet-office-model.js +55 -22
- package/dist/ui/fleet-office-overview.js +6 -38
- package/dist/ui/fleet-office-pack.js +4 -3
- package/dist/ui/fleet-office-skin.js +114 -21
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office-zones.js +2 -2
- package/dist/ui/fleet-office.js +41 -9
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/page-share-dialog.js +191 -0
- package/dist/ui/panel-chat.js +16 -0
- package/dist/ui/panel-log.js +7 -1
- package/dist/ui/panel-needs.js +17 -8
- package/dist/ui/panel-notices.js +98 -0
- package/dist/ui/panel-settings.js +9 -2
- package/dist/ui/preview-pages.js +41 -0
- package/dist/ui/preview-panel-actions.js +161 -0
- package/dist/ui/preview-panel.js +209 -28
- package/dist/ui/preview-versions.js +155 -0
- package/dist/ui/preview.js +88 -3
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +6 -2
- package/dist/ui/shared/app-i18n.js +6 -6
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app-route.js +4 -0
- package/dist/ui/shared/app-shell.js +9 -5
- package/dist/ui/shared/app-store.js +71 -1
- package/dist/ui/shared/app-stream.js +27 -1
- package/dist/ui/shared/app.css +94 -4
- package/dist/ui/shared/app.js +22 -1
- package/dist/ui/shared/office/guild.png +0 -0
- package/dist/ui/shared/office/school.png +0 -0
- package/dist/ui/shared/panel-view.js +339 -17
- package/dist/ui/shared/signin.js +12 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- 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 +42 -0
- package/dist/ui/shared/web-authorize.js +3 -1
- package/dist/unattended-install.d.ts +6 -0
- package/dist/unattended-install.js +79 -0
- package/dist/unattended-install.js.map +1 -0
- 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/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 +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +31 -0
- package/dist/view-api.js +186 -41
- 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 +32 -0
- package/dist/web-api.js +195 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +20 -0
- package/dist/web-chat-history.js +21 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +5 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-login.d.ts +6 -3
- package/dist/web-login.js +26 -15
- package/dist/web-login.js.map +1 -1
- package/dist/web-notices.d.ts +51 -0
- package/dist/web-notices.js +118 -0
- package/dist/web-notices.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +1 -1
- package/dist/web-office-pack-tools.js +1 -1
- package/dist/web-office-pack-tools.js.map +1 -1
- package/dist/web-only-fleet.d.ts +22 -0
- package/dist/web-only-fleet.js +13 -0
- package/dist/web-only-fleet.js.map +1 -0
- package/dist/web-preview.d.ts +32 -3
- package/dist/web-preview.js +52 -19
- package/dist/web-preview.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 +12 -0
- package/dist/web-session.js +22 -8
- 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 +2 -0
- package/dist/web-shell-routes.js +3 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/agend-install.cjs +16 -2
- package/launcher/private-home.cjs +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identit
|
|
|
3
3
|
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
4
4
|
import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
|
|
5
5
|
import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
6
|
+
import { maskSecrets } from "./log-steps.js";
|
|
6
7
|
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
7
8
|
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
8
9
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
@@ -27,7 +28,10 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
|
|
|
27
28
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
28
29
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
29
30
|
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
31
|
+
import { unattendedInstallCommand, loginShell } from "./unattended-install.js";
|
|
32
|
+
import { InstallStream } from "./install-stream.js";
|
|
30
33
|
import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
|
|
34
|
+
import { daemonTopicMode, isWebOnlyFleetConfig } from "./web-only-fleet.js";
|
|
31
35
|
import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
|
|
32
36
|
import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
|
|
33
37
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
@@ -36,7 +40,7 @@ import { withinBudget } from "./monotonic-budget.js";
|
|
|
36
40
|
import { measureSyncWork } from "./sync-work-attribution.js";
|
|
37
41
|
import { RuntimeCpuProfiler, ProfileBusyError, profileDuration } from "./runtime-cpu-profile.js";
|
|
38
42
|
import { ProfileControlServer } from "./profile-control.js";
|
|
39
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync,
|
|
43
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, realpathSync, lstatSync, closeSync } from "node:fs";
|
|
40
44
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
41
45
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
42
46
|
import { freemem, totalmem, homedir } from "node:os";
|
|
@@ -53,6 +57,7 @@ import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./ki
|
|
|
53
57
|
import { fleetLabel } from "./fleet-label.js";
|
|
54
58
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
55
59
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
60
|
+
import { startStallReporter } from "./stall-reporter.js";
|
|
56
61
|
import { sdNotify, sdNotifyBlocking, setSdNotifyWarn, startSdWatchdog } from "./sd-notify.js";
|
|
57
62
|
import { StartupMemoryDeadline, fleetStopConcurrency } from "./startup-memory-deadline.js";
|
|
58
63
|
import { readFleetMemory } from "./process-memory.js";
|
|
@@ -71,7 +76,8 @@ import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
|
71
76
|
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
72
77
|
import { EventLog } from "./event-log.js";
|
|
73
78
|
import { binaryProbe } from "./binary-probe.js";
|
|
74
|
-
import {
|
|
79
|
+
import { deletePlannedTopic, planInstanceTopicDelete, topicDestination, topicMustStay } from "./instance-topic-delete.js";
|
|
80
|
+
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes, findExecutableIn, resolveBackendBinary } from "./backend-availability.js";
|
|
75
81
|
import { classifySqliteOpenError } from "./sqlite-open-errors.js";
|
|
76
82
|
import { OutboxOpenError } from "./outbox-open-error.js";
|
|
77
83
|
import { AdapterWorld } from "./adapter-world.js";
|
|
@@ -86,6 +92,7 @@ import { formatFleetTime } from "./tz-utils.js";
|
|
|
86
92
|
import { createAdapter } from "./channel/factory.js";
|
|
87
93
|
import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
|
|
88
94
|
import { TelegramAdapter } from "./channel/adapters/telegram.js";
|
|
95
|
+
import { MissedOfflineStore } from "./missed-offline.js";
|
|
89
96
|
import { createBackend } from "./backend/factory.js";
|
|
90
97
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
91
98
|
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
@@ -106,7 +113,7 @@ import { WebhookEmitter } from "./webhook-emitter.js";
|
|
|
106
113
|
import { TmuxControlClient } from "./tmux-control.js";
|
|
107
114
|
import { safeHandler } from "./safe-async.js";
|
|
108
115
|
import { RoutingEngine } from "./routing-engine.js";
|
|
109
|
-
import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, checkBinaryInstalled, } from "./instance-lifecycle.js";
|
|
116
|
+
import { InstanceLifecycle, SupersededStartError, BACKEND_INSTALLATION_INFO, checkBinaryInstalled, checkBinaryInstalledAsync, } from "./instance-lifecycle.js";
|
|
110
117
|
import { TopicArchiver } from "./topic-archiver.js";
|
|
111
118
|
import { StatuslineWatcher } from "./statusline-watcher.js";
|
|
112
119
|
import { applyInstanceConfigPatch, outboundHandlers } from "./outbound-handlers.js";
|
|
@@ -165,6 +172,7 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
165
172
|
}
|
|
166
173
|
import { handleWebRequest, webPeerOf, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
167
174
|
import { LogService } from "./log-service.js";
|
|
175
|
+
import { viewHistory } from "./view-history.js";
|
|
168
176
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
|
|
169
177
|
import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
|
|
170
178
|
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
@@ -197,7 +205,7 @@ import { envFileKeys } from "./token-env-name.js";
|
|
|
197
205
|
import { discordGeneralChannelId } from "./discord-general.js";
|
|
198
206
|
import { parseEnvText } from "./env-file.js";
|
|
199
207
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
200
|
-
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
208
|
+
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, setCapacityLookup, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
201
209
|
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
202
210
|
import { LoginSession } from "./login-manager.js";
|
|
203
211
|
import { runOnLoginShell } from "./login-shell.js";
|
|
@@ -228,6 +236,8 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
|
|
|
228
236
|
import { recentChatContext } from "./log-tail.js";
|
|
229
237
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
230
238
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
239
|
+
import { AgentCredentials, agentCredentialPolicy } from "./agent-credentials.js";
|
|
240
|
+
import { writeSecretFile } from "./secret-file.js";
|
|
231
241
|
import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
|
|
232
242
|
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
|
|
233
243
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
@@ -264,7 +274,8 @@ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCooki
|
|
|
264
274
|
import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
|
|
265
275
|
import { isWebPageNavigation } from "./web-shell-routes.js";
|
|
266
276
|
import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
|
|
267
|
-
import {
|
|
277
|
+
import { WebNotices, WEB_NOTICES_FILE } from "./web-notices.js";
|
|
278
|
+
import { authorityNow, evaluateWebAuthority, HOST_OPERATOR, VERIFIED_SESSION_HOURS, webAuthorityPolicy } from "./authority/web-authority.js";
|
|
268
279
|
import { AuthorityStateStore } from "./authority/authority-state.js";
|
|
269
280
|
import { SessionAuthorizations } from "./authority/session-authorizations.js";
|
|
270
281
|
import { SessionAuthorizationRequests } from "./web-session-authorization.js";
|
|
@@ -285,7 +296,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
285
296
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
286
297
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
287
298
|
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
288
|
-
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
299
|
+
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile, listConfiguredProfiles } from "./backend/credential-profile.js";
|
|
289
300
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
290
301
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
291
302
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
@@ -300,6 +311,13 @@ import { SpawnGate } from "./spawn-gate.js";
|
|
|
300
311
|
import { BackendOutageTracker } from "./backend-outage.js";
|
|
301
312
|
import { canUnlockAdvancedTips, DailyTipScheduler, selectTip, visibleTipLevels, } from "./tips.js";
|
|
302
313
|
import { getTmuxSession } from "./config.js";
|
|
314
|
+
import { OwnStatusMessages, discordMessageKey } from "./chat-log-ids.js";
|
|
315
|
+
import { PROFILE_TIMELINE_FILE, PROFILE_UNRECORDED, recordProfileLaunch } from "./usage/profile-timeline.js";
|
|
316
|
+
import { readQuotaHistory } from "./usage/quota-history.js";
|
|
317
|
+
import { estimateCapacity } from "./usage/capacity.js";
|
|
318
|
+
import { covered, hourSources } from "./usage-rollup.js";
|
|
319
|
+
import { KEEP_DAYS } from "./cache-ledger.js";
|
|
320
|
+
import { ratesFor } from "./cache-prices.js";
|
|
303
321
|
// Closed migration cohort: these 11 names existed immediately before the first
|
|
304
322
|
// managed-skill publisher (a1ac499f; parent e267e684). Never grow this set when
|
|
305
323
|
// adding a bundled skill: its new name is not proof of old AgEnD ownership.
|
|
@@ -441,6 +459,8 @@ const CANCEL_BTN_LEDGER_FILE = "cancel-buttons.json";
|
|
|
441
459
|
const PROGRESS_UPDATE_INTERVAL_MS = 60_000;
|
|
442
460
|
/** Floor between tool-progress-driven bubble edits (Telegram flood safety). */
|
|
443
461
|
const TOOL_PROGRESS_EDIT_MIN_MS = 4_000;
|
|
462
|
+
/** M-2281: how long `/login`'s install window may run before it is stopped. */
|
|
463
|
+
const INSTALL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
444
464
|
/** Elapsed time is only shown once work has clearly outlasted a quick answer. */
|
|
445
465
|
/**
|
|
446
466
|
* Default delay before the button starts showing elapsed time. Configurable via
|
|
@@ -449,6 +469,8 @@ const TOOL_PROGRESS_EDIT_MIN_MS = 4_000;
|
|
|
449
469
|
* while anything real shows signs of life well before the old two minutes.
|
|
450
470
|
*/
|
|
451
471
|
const PROGRESS_MIN_ELAPSED_MS = 30_000;
|
|
472
|
+
/** M-2391: the threshold tick fires this long after it, never a millisecond before (timers can fire early). */
|
|
473
|
+
const PROGRESS_THRESHOLD_MARGIN_MS = 50;
|
|
452
474
|
/** How much of a tool summary the progress line will show before eliding. */
|
|
453
475
|
const PROGRESS_ACTIVITY_MAX_CHARS = 48;
|
|
454
476
|
/**
|
|
@@ -620,6 +642,14 @@ function emojiListFilter(opts) {
|
|
|
620
642
|
}
|
|
621
643
|
/** #1920: a guest's scope until phase 2 derives one: nothing. */
|
|
622
644
|
const NO_GUEST_SCOPE = new Set();
|
|
645
|
+
/** #1490 row 27: a restart asked for one lifecycle of an instance after it was stopped, restarted or replaced. */
|
|
646
|
+
export class RestartSupersededError extends Error {
|
|
647
|
+
/** `shown`: the instance as the chat list names it (shownInstanceName). */
|
|
648
|
+
constructor(shown) {
|
|
649
|
+
super(t("restart.superseded", shown));
|
|
650
|
+
this.name = "RestartSupersededError";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
623
653
|
export class FleetManager {
|
|
624
654
|
dataDir;
|
|
625
655
|
static signalTarget = null;
|
|
@@ -871,6 +901,7 @@ export class FleetManager {
|
|
|
871
901
|
startupRetryNotices = new Map();
|
|
872
902
|
/** Unsupported-CLI refusals waiting to go out, grouped by reason (#1109). */
|
|
873
903
|
unsupportedCliNotices = new Map();
|
|
904
|
+
kiroCompatibilityHolds = new Map();
|
|
874
905
|
kiroIncompatNotices = new Map();
|
|
875
906
|
/** Backoff between automatic startup retries; the last step repeats while the backend is down. */
|
|
876
907
|
static STARTUP_RETRY_BACKOFF_MS = [60_000, 5 * 60_000, 15 * 60_000];
|
|
@@ -899,6 +930,7 @@ export class FleetManager {
|
|
|
899
930
|
kiroMigrationRuns = new Map();
|
|
900
931
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
901
932
|
needsYou = null;
|
|
933
|
+
missedOfflineStore = null;
|
|
902
934
|
configHistory = null;
|
|
903
935
|
configHistoryTimer = null;
|
|
904
936
|
loadedFleetSource = null;
|
|
@@ -1014,11 +1046,15 @@ export class FleetManager {
|
|
|
1014
1046
|
discordPresenceEagerPending = false;
|
|
1015
1047
|
discordPresenceInFlight = null;
|
|
1016
1048
|
static DISCORD_PRESENCE_REFRESH_MS = 15 * 60_000;
|
|
1049
|
+
/** #3066 §1d: how often a fleet with no Discord bot reads the quota for its history (the user's choice: 30 min). */
|
|
1050
|
+
static QUOTA_BACKGROUND_READ_MS = 30 * 60_000;
|
|
1051
|
+
quotaReadTimer = null;
|
|
1017
1052
|
/** Days of event/activity history to keep. */
|
|
1018
1053
|
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
1019
1054
|
watchdogTimer = null;
|
|
1020
1055
|
startupMemoryDeadline = null;
|
|
1021
1056
|
eventLoopWatch = null;
|
|
1057
|
+
stallReporter = null;
|
|
1022
1058
|
startedAt = 0;
|
|
1023
1059
|
stormOpenNotifyTimer = null;
|
|
1024
1060
|
memoryLogAt = null;
|
|
@@ -1062,6 +1098,37 @@ export class FleetManager {
|
|
|
1062
1098
|
webChatRemovals = new Map();
|
|
1063
1099
|
/** #1774: a removal under way — its replies' fate waits for its outcome (success: theirs no more; failure: theirs). */
|
|
1064
1100
|
webChatRemovalsPending = new Map();
|
|
1101
|
+
/**
|
|
1102
|
+
* #1490 row 27: "still the same lifecycle of `name`" — its lifecycle epoch, moved on synchronously by every stop,
|
|
1103
|
+
* restart and removal (the map is never pruned, so a same-name replacement has a later one). Taken when a button
|
|
1104
|
+
* or menu is claimed or offered, asked after every await before acting by name.
|
|
1105
|
+
*/
|
|
1106
|
+
/** #1490 row 27: an instance as the chat list names it — its display name where it has one, else its name. */
|
|
1107
|
+
shownInstanceName(name) {
|
|
1108
|
+
try {
|
|
1109
|
+
const classic = this.classicChannels?.getAll().find(ch => ch.instanceName === name) ?? null;
|
|
1110
|
+
return resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic, profile: profileIdentities(this.dataDir).get(name) }).display_name || name;
|
|
1111
|
+
}
|
|
1112
|
+
catch {
|
|
1113
|
+
return name;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
lifecyclePin(name) {
|
|
1117
|
+
// Fail closed: a lifecycle that cannot say its epoch holds nothing (and never stops a prompt from being posted).
|
|
1118
|
+
let epoch;
|
|
1119
|
+
try {
|
|
1120
|
+
epoch = this.lifecycle?.epochOf(name);
|
|
1121
|
+
}
|
|
1122
|
+
catch {
|
|
1123
|
+
return () => false;
|
|
1124
|
+
}
|
|
1125
|
+
return () => { try {
|
|
1126
|
+
return !this.shuttingDown && this.lifecycle?.epochOf(name) === epoch;
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return false;
|
|
1130
|
+
} };
|
|
1131
|
+
}
|
|
1065
1132
|
webChatGeneration(name) { return this.webChatGenerations.get(name) ?? 0; }
|
|
1066
1133
|
/** #1774: who an instance's reply is from, taken when it is sent: the name's generation and its daemon then. */
|
|
1067
1134
|
webChatIdentity(name) { return { generation: this.webChatGeneration(name), daemon: this.daemons.get(name) }; }
|
|
@@ -1201,6 +1268,11 @@ export class FleetManager {
|
|
|
1201
1268
|
officeExists: id => this.officeStore.list().offices.some(o => o.id === id),
|
|
1202
1269
|
ownerCurrent: owner => this.officeShareOwnerCurrent(owner),
|
|
1203
1270
|
changed: () => this.emitSseEvent("office_shares", {}),
|
|
1271
|
+
// #2911: page shares on the same tunnel. Their end (expired, stopped, the feature off, the maker no longer an admin,
|
|
1272
|
+
// the fleet stopping) is said in the page's own channel, as its start is (pageShareNotice); never its URL.
|
|
1273
|
+
pageConfig: () => this.fleetConfig?.web?.page_share,
|
|
1274
|
+
pageCdn: () => this.fleetConfig?.web?.preview_cdn === true,
|
|
1275
|
+
pagesChanged: (list, ended) => this.pageSharesChanged(list, ended),
|
|
1204
1276
|
log: (event, detail) => this.logger.info({ event, ...(detail ? { detail } : {}) }, "Office share link"),
|
|
1205
1277
|
}));
|
|
1206
1278
|
}
|
|
@@ -1215,6 +1287,17 @@ export class FleetManager {
|
|
|
1215
1287
|
: share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
|
|
1216
1288
|
this.notifyFleetError(text, { throttle: false });
|
|
1217
1289
|
}
|
|
1290
|
+
/** #2911: the open page shares changed — the page hears it; each one handed out that ended is said in its channel. */
|
|
1291
|
+
pageSharesChanged(_list, ended) {
|
|
1292
|
+
this.emitSseEvent("page_shares", {});
|
|
1293
|
+
for (const s of ended)
|
|
1294
|
+
this.notifyInstanceTopic(s.instance, t("page_share.ended_notice", s.title || t("page_share.untitled")));
|
|
1295
|
+
}
|
|
1296
|
+
/** #2911: a page share was made — said in the channel of the instance whose chat it came from (never its URL). */
|
|
1297
|
+
pageShareNotice(who, share) {
|
|
1298
|
+
const minutes = Math.round((share.expiresAt - share.createdAt) / 60_000);
|
|
1299
|
+
this.notifyInstanceTopic(share.instance, t("page_share.created_notice", who, share.title || t("page_share.untitled"), minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`));
|
|
1300
|
+
}
|
|
1218
1301
|
/** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
|
|
1219
1302
|
officeShareOwnerCurrent(owner) {
|
|
1220
1303
|
const m = /^person:([^:]+):(.+)$/.exec(owner);
|
|
@@ -1612,6 +1695,17 @@ export class FleetManager {
|
|
|
1612
1695
|
const actor = verified();
|
|
1613
1696
|
if (!actor)
|
|
1614
1697
|
return null;
|
|
1698
|
+
// #2910 D1: a web-only fleet's host operator (its `agend web --code` session on this computer) confirms its own
|
|
1699
|
+
// change — the trust `agend settings confirm` has here; there is no account to be an admin of and no General to
|
|
1700
|
+
// ask. Asked again at execution: still the host operator now (this computer, not revoked or run out), still web-only.
|
|
1701
|
+
const hostOperator = (a) => !!a && a.adapter_id === HOST_OPERATOR.adapterId && a.user_id === HOST_OPERATOR.userId && this.isWebOnlyFleet();
|
|
1702
|
+
if (hostOperator(actor)) {
|
|
1703
|
+
return {
|
|
1704
|
+
label: t("settings_confirm.host_operator_label"),
|
|
1705
|
+
current: () => hostOperator(verified()),
|
|
1706
|
+
done: view => { this.logger.info({ id: view.id, state: view.state }, "Settings change confirmed from the web by this computer's admin"); },
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1615
1709
|
const chat = this.settingsChatAuthority(store, id);
|
|
1616
1710
|
if (!chat)
|
|
1617
1711
|
return null;
|
|
@@ -2078,6 +2172,22 @@ export class FleetManager {
|
|
|
2078
2172
|
* startup-only key at startup. See checkStartupSignatureConsistency(). */
|
|
2079
2173
|
fleetSignatureMismatch = null;
|
|
2080
2174
|
healthServerListening = false;
|
|
2175
|
+
agentCredentialsCache;
|
|
2176
|
+
get agentCredentials() {
|
|
2177
|
+
return this.agentCredentialsCache ??= new AgentCredentials(name => agentCredentialPolicy(this.fleetConfig, name, this.isClassicInstance(name)), name => {
|
|
2178
|
+
try {
|
|
2179
|
+
const token = randomBytes(32).toString("hex");
|
|
2180
|
+
const result = writeSecretFile(join(this.getInstanceDir(name), "agent.token"), token);
|
|
2181
|
+
if (!result.ok)
|
|
2182
|
+
this.logger.error({ name, mode: result.mode, reason: result.reason }, "Agent token file is not owner-only");
|
|
2183
|
+
return token;
|
|
2184
|
+
}
|
|
2185
|
+
catch {
|
|
2186
|
+
this.logger.error({ name }, "Agent credential renewal failed; prior generation remains retired");
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2081
2191
|
constructor(dataDir) {
|
|
2082
2192
|
this.dataDir = dataDir;
|
|
2083
2193
|
FleetManager.signalTarget = this;
|
|
@@ -2100,6 +2210,7 @@ export class FleetManager {
|
|
|
2100
2210
|
this.topicCommands = new TopicCommands(this);
|
|
2101
2211
|
this.topicArchiver = new TopicArchiver(this);
|
|
2102
2212
|
this.statuslineWatcher = new StatuslineWatcher(this);
|
|
2213
|
+
this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
|
|
2103
2214
|
}
|
|
2104
2215
|
/** #1266: a reply's buttons — the store (reply-buttons.db) and what a click does. Created on first use. */
|
|
2105
2216
|
replyButtonsCtl = null;
|
|
@@ -2458,6 +2569,10 @@ export class FleetManager {
|
|
|
2458
2569
|
const daemon = this.daemons.get(name);
|
|
2459
2570
|
if (daemon?.bootId === daemonBootId)
|
|
2460
2571
|
this.listenForWorkingState(name, daemon);
|
|
2572
|
+
// #3066 §1d: a Codex launch records the credential profile it runs under, so its usage is told by account.
|
|
2573
|
+
if (daemon?.bootId === daemonBootId && this.backendNameOf(name) === "codex") {
|
|
2574
|
+
void recordProfileLaunch(join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE), this.credentialProfileOf(name, "codex") ?? "default");
|
|
2575
|
+
}
|
|
2461
2576
|
// #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
|
|
2462
2577
|
// request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
|
|
2463
2578
|
this.scheduleDeliveryOutboxPump();
|
|
@@ -3295,6 +3410,7 @@ export class FleetManager {
|
|
|
3295
3410
|
* guild channel.
|
|
3296
3411
|
*/
|
|
3297
3412
|
reregisterClassicChannels() {
|
|
3413
|
+
this.agentCredentialsCache?.observePolicies();
|
|
3298
3414
|
if (!this.classicChannels)
|
|
3299
3415
|
return;
|
|
3300
3416
|
const channels = this.classicChannels.getAll();
|
|
@@ -3985,6 +4101,13 @@ export class FleetManager {
|
|
|
3985
4101
|
changed: () => this.emitSseEvent("status", this.getUiStatus()),
|
|
3986
4102
|
});
|
|
3987
4103
|
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Claude history in the View: an instance's earlier conversation for its View, from the transcript the Log reads —
|
|
4106
|
+
* the View's route applied its own access rules first (view-api.ts); the Log's switch plays no part.
|
|
4107
|
+
*/
|
|
4108
|
+
viewHistory(name, q) {
|
|
4109
|
+
return viewHistory((n, rq) => this.log.readBack(n, rq), name, q);
|
|
4110
|
+
}
|
|
3988
4111
|
/** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
|
|
3989
4112
|
logSteps(name, q) { return this.log.steps(name, q); }
|
|
3990
4113
|
/**
|
|
@@ -5607,6 +5730,23 @@ export class FleetManager {
|
|
|
5607
5730
|
if (resumePaused)
|
|
5608
5731
|
this.lifecycle.markActivitySeedNow(name);
|
|
5609
5732
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
5733
|
+
// #1490 row 67: off-loop resolve before skill/instruction publication
|
|
5734
|
+
// constructs a backend (resolveKnowledgeWorkDir). Capture stop/lifecycle
|
|
5735
|
+
// ownership first: a Stop during this wait finishes and bumps the epoch,
|
|
5736
|
+
// and the inner lifecycle check would otherwise capture the new epoch and
|
|
5737
|
+
// accept the stale start. Lifecycle start looks up again before its own
|
|
5738
|
+
// createBackend; the probe cache makes that cheap.
|
|
5739
|
+
const epoch = this.lifecycle.epochOf(name);
|
|
5740
|
+
const stopGen = this.explicitStopGeneration.get(name) ?? 0;
|
|
5741
|
+
const resolved = await resolveBackendBinary(backend);
|
|
5742
|
+
execution?.assert();
|
|
5743
|
+
if (this.lifecycle.epochOf(name) !== epoch
|
|
5744
|
+
|| (this.explicitStopGeneration.get(name) ?? 0) !== stopGen) {
|
|
5745
|
+
throw new SupersededStartError(name);
|
|
5746
|
+
}
|
|
5747
|
+
const binary = BACKEND_INSTALLATION_INFO[backend]?.binary;
|
|
5748
|
+
if (binary)
|
|
5749
|
+
noteResolvedBinary(binary, resolved ?? binary);
|
|
5610
5750
|
if (config.general_topic) {
|
|
5611
5751
|
this.ensureGeneralInstructions(config.working_directory, backend, name);
|
|
5612
5752
|
}
|
|
@@ -5690,8 +5830,7 @@ export class FleetManager {
|
|
|
5690
5830
|
}
|
|
5691
5831
|
/** Recreate a daemon for a marker-only paused instance after an explicit wake/delivery. */
|
|
5692
5832
|
async startPersistedPausedInstance(name, transition) {
|
|
5693
|
-
const topicMode = this.fleetConfig
|
|
5694
|
-
|| !!this.fleetConfig?.channels?.some(channel => channel.mode === "topic");
|
|
5833
|
+
const topicMode = daemonTopicMode(this.fleetConfig);
|
|
5695
5834
|
const fleetConfig = this.fleetConfig?.instances[name];
|
|
5696
5835
|
if (fleetConfig) {
|
|
5697
5836
|
await this.startInstance(name, fleetConfig, topicMode, "fleet-topic", false, transition);
|
|
@@ -5902,8 +6041,7 @@ export class FleetManager {
|
|
|
5902
6041
|
this.logger.info({ name, attempt: attempt + 1 }, "Startup retry deferred — tmux storm window is blocking spawns");
|
|
5903
6042
|
return;
|
|
5904
6043
|
}
|
|
5905
|
-
const topicMode = this.fleetConfig
|
|
5906
|
-
|| !!this.fleetConfig?.channels?.some(channel => channel.mode === "topic");
|
|
6044
|
+
const topicMode = daemonTopicMode(this.fleetConfig);
|
|
5907
6045
|
this.startupRetriesRunning.add(name);
|
|
5908
6046
|
try {
|
|
5909
6047
|
await this.spawnGate.run({
|
|
@@ -6065,6 +6203,17 @@ export class FleetManager {
|
|
|
6065
6203
|
this.queueKiroIncompatNotice(name, err.message);
|
|
6066
6204
|
return true;
|
|
6067
6205
|
}
|
|
6206
|
+
/** No pane is needed for a compatibility refusal to remain in Needs you. */
|
|
6207
|
+
setKiroCompatibilityHold(name, message) {
|
|
6208
|
+
const holds = this.kiroCompatibilityHolds ??= new Map();
|
|
6209
|
+
if ((message === null && !holds.has(name)) || (message !== null && holds.get(name)?.message === message))
|
|
6210
|
+
return;
|
|
6211
|
+
if (message === null)
|
|
6212
|
+
holds.delete(name);
|
|
6213
|
+
else
|
|
6214
|
+
holds.set(name, { message, since: Date.now() });
|
|
6215
|
+
this.needsYou?.poke();
|
|
6216
|
+
}
|
|
6068
6217
|
/**
|
|
6069
6218
|
* LifecycleContext: a kiro instance the installed kiro-cli cannot run as
|
|
6070
6219
|
* configured (#1109) — at start, or at a respawn after kiro-cli replaced
|
|
@@ -6259,6 +6408,8 @@ export class FleetManager {
|
|
|
6259
6408
|
}
|
|
6260
6409
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
6261
6410
|
async restartSingleInstance(name, opts) {
|
|
6411
|
+
if (opts?.ownerCurrent && !opts.ownerCurrent())
|
|
6412
|
+
throw new RestartSupersededError(this.shownInstanceName(name));
|
|
6262
6413
|
const migration = this.kiroMigrationRuns?.get(name);
|
|
6263
6414
|
if (migration && migration.token !== opts?.kiroMigration)
|
|
6264
6415
|
throw new Error(t("kiro_migrate.in_progress"));
|
|
@@ -6426,7 +6577,7 @@ export class FleetManager {
|
|
|
6426
6577
|
const backend = config.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
6427
6578
|
return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, this.fleetConfig?.defaults, backend) : null };
|
|
6428
6579
|
});
|
|
6429
|
-
const topicMode = this.fleetConfig
|
|
6580
|
+
const topicMode = daemonTopicMode(this.fleetConfig);
|
|
6430
6581
|
await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => opts?.kiroMigration ? this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition, undefined, opts.kiroMigration)
|
|
6431
6582
|
: this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
|
|
6432
6583
|
this.stormWindow.markRecovered(name);
|
|
@@ -6729,6 +6880,7 @@ export class FleetManager {
|
|
|
6729
6880
|
if (!publicPreviewWanted(this.fleetConfig?.web))
|
|
6730
6881
|
return null;
|
|
6731
6882
|
return new PublicPreview({ exposureId: id, expiresAt, dashboardOrigin, isCurrent, manager: this.getPreviewTunnel(),
|
|
6883
|
+
cdn: () => this.fleetConfig?.web?.preview_cdn === true,
|
|
6732
6884
|
provider: () => new CloudflaredProvider({ binaryName: binary, protocol }),
|
|
6733
6885
|
log: (event, detail) => this.logger.info({ event, exposureId: id, ...(detail ? { detail } : {}) }, "Public web link") });
|
|
6734
6886
|
},
|
|
@@ -7558,7 +7710,7 @@ export class FleetManager {
|
|
|
7558
7710
|
return { kind: "refused" };
|
|
7559
7711
|
if (device.tier === "guest")
|
|
7560
7712
|
return this.requestGuestDeviceCode(device, input, token);
|
|
7561
|
-
const paused = this.webLoginCodes.pausedForMs();
|
|
7713
|
+
const paused = this.webLoginCodes.pausedForMs(input.exposureId ?? "local");
|
|
7562
7714
|
if (paused > 0)
|
|
7563
7715
|
return { kind: "limited", retryAfterMs: paused };
|
|
7564
7716
|
const admitted = this.codeRequestLimiter.admit(device.idHash);
|
|
@@ -7690,7 +7842,8 @@ export class FleetManager {
|
|
|
7690
7842
|
return;
|
|
7691
7843
|
// The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
|
|
7692
7844
|
// sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
|
|
7693
|
-
|
|
7845
|
+
// #2910 D2: a web-only fleet has no /web to revoke with — its Notice names the host's way (every browser signed out).
|
|
7846
|
+
this.notifyFleetError(t(this.webOnlyPromptPlace() ? "web.login_notice_web_only" : "web.login_notice", info.label, info.surface, info.handle.slice(0, 8)));
|
|
7694
7847
|
}
|
|
7695
7848
|
getDashboardAccess() {
|
|
7696
7849
|
return { ready: this.healthServerListening, token: this.webToken };
|
|
@@ -8042,6 +8195,9 @@ export class FleetManager {
|
|
|
8042
8195
|
this.watchdogTimer = startSdWatchdog();
|
|
8043
8196
|
this.eventLoopWatch?.stop();
|
|
8044
8197
|
this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
|
|
8198
|
+
// #3009: the event-loop watch runs on the main thread; this one does not, so a stall that never ends is still named.
|
|
8199
|
+
this.stallReporter?.stop();
|
|
8200
|
+
this.stallReporter = startStallReporter({ logFile: join(this.dataDir, "daemon.log") });
|
|
8045
8201
|
try {
|
|
8046
8202
|
await this.startAllWithinDeadline(configPath, deadline);
|
|
8047
8203
|
}
|
|
@@ -8055,6 +8211,8 @@ export class FleetManager {
|
|
|
8055
8211
|
this.watchdogTimer = null;
|
|
8056
8212
|
this.eventLoopWatch?.stop();
|
|
8057
8213
|
this.eventLoopWatch = null;
|
|
8214
|
+
this.stallReporter?.stop();
|
|
8215
|
+
this.stallReporter = null;
|
|
8058
8216
|
}
|
|
8059
8217
|
}
|
|
8060
8218
|
}
|
|
@@ -8110,7 +8268,8 @@ export class FleetManager {
|
|
|
8110
8268
|
// Must run before the first General/CLI spawn: pressure can hold startup.
|
|
8111
8269
|
// Logs remain available even before the adapters and health listener exist.
|
|
8112
8270
|
this.memoryPressure.start();
|
|
8113
|
-
|
|
8271
|
+
// #2910: a web-only fleet's agents run in topic mode too (their cross-instance tools work; the dashboard is their place).
|
|
8272
|
+
const topicMode = daemonTopicMode(fleet);
|
|
8114
8273
|
// Set tmux socket isolation for custom AGEND_HOME
|
|
8115
8274
|
const { getTmuxSocketName: getSocket } = await import("./paths.js");
|
|
8116
8275
|
TmuxManager.setSocketName(getSocket());
|
|
@@ -8321,13 +8480,18 @@ export class FleetManager {
|
|
|
8321
8480
|
const generalDir = join(getAgendHome(), name);
|
|
8322
8481
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
8323
8482
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
8483
|
+
const resolved = await resolveBackendBinary(backendName);
|
|
8484
|
+
const binary = BACKEND_INSTALLATION_INFO[backendName]?.binary;
|
|
8485
|
+
if (binary)
|
|
8486
|
+
noteResolvedBinary(binary, resolved ?? binary);
|
|
8324
8487
|
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
8325
8488
|
fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
|
|
8326
8489
|
generalsCreated = true;
|
|
8327
8490
|
}
|
|
8328
8491
|
if (generalsCreated && !this.configHistory?.isFallback(this.configPath))
|
|
8329
8492
|
this.saveFleetConfig();
|
|
8330
|
-
|
|
8493
|
+
// #2910: the scheduler (schedules, the task board, decisions) runs on a web-only fleet too; it needs no chat.
|
|
8494
|
+
if (topicMode) {
|
|
8331
8495
|
const schedulerConfig = {
|
|
8332
8496
|
...DEFAULT_SCHEDULER_CONFIG,
|
|
8333
8497
|
...this.fleetConfig?.defaults.scheduler,
|
|
@@ -8474,6 +8638,9 @@ export class FleetManager {
|
|
|
8474
8638
|
if (others.length > 0 || classicAtBoot.length > 0) {
|
|
8475
8639
|
await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
|
|
8476
8640
|
}
|
|
8641
|
+
// #3066 §1d (Fable r1): on every fleet — a web-only one (#2910) has no chat connection at all, and is the clearest
|
|
8642
|
+
// case with no Discord bot reading. Each tick decides for itself whether to read (backgroundQuotaRead).
|
|
8643
|
+
this.startQuotaBackgroundReads();
|
|
8477
8644
|
if (topicMode && (fleet.channel || fleet.channels?.length)) {
|
|
8478
8645
|
await adapterStartup;
|
|
8479
8646
|
this.startDiscordUsagePresence();
|
|
@@ -8645,6 +8812,178 @@ export class FleetManager {
|
|
|
8645
8812
|
this.logger.warn({ health }, "Fleet started with problems — see /health");
|
|
8646
8813
|
}
|
|
8647
8814
|
}
|
|
8815
|
+
/**
|
|
8816
|
+
* #3066 §1d (the user: yes, every 30 min): the quota history (usage/quota-history.ts) records only the readings AgEnD
|
|
8817
|
+
* makes. A Discord bot's presence refresh reads every 15 min; without one, nothing reads while nobody looks. So, with no
|
|
8818
|
+
* Discord bot and while some account has a running agent, read every 30 min — the same cached read (its 5-minute TTL
|
|
8819
|
+
* still bounds the vendor calls), recorded like any other.
|
|
8820
|
+
*/
|
|
8821
|
+
startQuotaBackgroundReads() {
|
|
8822
|
+
if (this.quotaReadTimer)
|
|
8823
|
+
clearInterval(this.quotaReadTimer);
|
|
8824
|
+
this.quotaReadTimer = setInterval(() => { void this.backgroundQuotaRead(); }, FleetManager.QUOTA_BACKGROUND_READ_MS);
|
|
8825
|
+
this.quotaReadTimer.unref?.();
|
|
8826
|
+
// #3066 §1: the measured capacity, published for the usage reads (get_usage, the page) — refreshed every 10 min.
|
|
8827
|
+
setCapacityLookup((provider, metric) => this.capacityEstimates.get(`${provider}\u0000${metric}`) ?? null);
|
|
8828
|
+
if (this.capacityTimer)
|
|
8829
|
+
clearInterval(this.capacityTimer);
|
|
8830
|
+
void this.refreshCapacity();
|
|
8831
|
+
this.capacityTimer = setInterval(() => { void this.refreshCapacity(); }, FleetManager.CAPACITY_REFRESH_MS);
|
|
8832
|
+
this.capacityTimer.unref?.();
|
|
8833
|
+
}
|
|
8834
|
+
/** #3066 §1: the latest estimates, by `provider\0metric` (what usage-api's lookup reads, synchronously). */
|
|
8835
|
+
capacityEstimates = new Map();
|
|
8836
|
+
capacityTimer = null;
|
|
8837
|
+
static CAPACITY_REFRESH_MS = 10 * 60_000;
|
|
8838
|
+
/**
|
|
8839
|
+
* #3066 §1: estimate each account's weekly 100% from the quota history (§1d) and the usage ledger's hours — both read
|
|
8840
|
+
* asynchronously, the arithmetic small (usage/capacity.ts). An account is its usage provider id ("claude",
|
|
8841
|
+
* "claude:hsinlin", "codex:yanxianchen"): its hours are the ledger sources of that backend under that credential profile
|
|
8842
|
+
* (Codex's "unrecorded" hours are no account's). A per-model weekly ("Sonnet (weekly)") counts only that model's tokens.
|
|
8843
|
+
*
|
|
8844
|
+
* Prism #3073: no estimate rather than a wrong one —
|
|
8845
|
+
* - a window is measured only where the ledger has its hours: kept (31 days) and scanned (`days.coverage`) for every
|
|
8846
|
+
* instance of the account's backend, and every instance whose hours count for it; the quota history goes back 12 weeks;
|
|
8847
|
+
* - a profile literally named "default", "unknown" or "unrecorded" (configured, on disk, or in the quota history) makes
|
|
8848
|
+
* those hours ambiguous: neither the host
|
|
8849
|
+
* account nor the named one is estimated (each would be credited the other's use, or lose its own);
|
|
8850
|
+
* - only a metric the vendor marks per-model (`scope: "model"`) is filtered by model; another named weekly pool
|
|
8851
|
+
* ("Luna Reserve (weekly)", Antigravity pools) is no model name and is not estimated.
|
|
8852
|
+
*/
|
|
8853
|
+
async refreshCapacity() {
|
|
8854
|
+
try {
|
|
8855
|
+
const readings = await readQuotaHistory();
|
|
8856
|
+
const groups = new Map();
|
|
8857
|
+
for (const r of readings) {
|
|
8858
|
+
const k = `${r.provider}\u0000${r.metric}`;
|
|
8859
|
+
(groups.get(k) ?? groups.set(k, []).get(k)).push(r);
|
|
8860
|
+
}
|
|
8861
|
+
if (!groups.size) {
|
|
8862
|
+
this.capacityEstimates = new Map();
|
|
8863
|
+
return 0;
|
|
8864
|
+
}
|
|
8865
|
+
const data = await this.cacheServiceFor().usageData();
|
|
8866
|
+
const now = Date.now(), DAY = 24 * 3_600_000, HOUR = 3_600_000;
|
|
8867
|
+
const keptFrom = now - KEEP_DAYS * DAY + HOUR; // the oldest hour still in every ledger (an hour of margin)
|
|
8868
|
+
// Profiles whose names are also the ledger's own tags: their hours cannot be told from the host's / unattributed.
|
|
8869
|
+
// Any trace of such a profile counts (Prism #3073 r2), not only today's config — a binding removed or switched
|
|
8870
|
+
// leaves its hours in the ledger: configured now, its directory on this host, or its account in the quota history.
|
|
8871
|
+
const RESERVED = new Set(["default", "unknown", PROFILE_UNRECORDED]);
|
|
8872
|
+
const historyProviders = new Set(readings.map((r) => r.provider));
|
|
8873
|
+
const reservedNamed = new Map();
|
|
8874
|
+
const reservedFor = (backend) => {
|
|
8875
|
+
const known = reservedNamed.get(backend);
|
|
8876
|
+
if (known)
|
|
8877
|
+
return known;
|
|
8878
|
+
const found = new Set(listConfiguredProfiles(this.fleetConfig, backend).filter((n) => RESERVED.has(n)));
|
|
8879
|
+
const base = usageProviderIdForBackend(backend);
|
|
8880
|
+
for (const n of RESERVED) {
|
|
8881
|
+
if (base && historyProviders.has(`${base}:${n}`))
|
|
8882
|
+
found.add(n);
|
|
8883
|
+
if (credentialHomeSpec(backend) && existsSync(credentialProfileHome(this.dataDir, backend, n)))
|
|
8884
|
+
found.add(n);
|
|
8885
|
+
}
|
|
8886
|
+
return reservedNamed.set(backend, found).get(backend);
|
|
8887
|
+
};
|
|
8888
|
+
// Every instance of a backend may have used any of its accounts (bindings change): an account is measured only
|
|
8889
|
+
// where all of them were scanned (Prism #3073 r2) — one still waiting for its first scan has no hours yet, not none.
|
|
8890
|
+
const daysOfBase = new Map();
|
|
8891
|
+
for (const inst of data.instances) {
|
|
8892
|
+
const base = usageProviderIdForBackend(inst.inst.backend);
|
|
8893
|
+
if (base)
|
|
8894
|
+
(daysOfBase.get(base) ?? daysOfBase.set(base, []).get(base)).push(inst.days);
|
|
8895
|
+
}
|
|
8896
|
+
// provider id → hour → model key → totals; and the instances whose hours count for it (their scan coverage).
|
|
8897
|
+
const byProvider = new Map();
|
|
8898
|
+
const daysOf = new Map();
|
|
8899
|
+
for (const inst of data.instances) {
|
|
8900
|
+
for (const [hk, hour] of Object.entries(inst.ledger.hours)) {
|
|
8901
|
+
for (const src of hourSources(hour)) {
|
|
8902
|
+
const base = usageProviderIdForBackend(src.backend);
|
|
8903
|
+
if (!base)
|
|
8904
|
+
continue;
|
|
8905
|
+
// Ambiguous (a named profile with the tag's name), or no account's: counted for no one, so an account whose
|
|
8906
|
+
// hours these may be has none — and gets no estimate, never a low one.
|
|
8907
|
+
if (reservedFor(src.backend).has(src.profile) || src.profile === "unknown" || src.profile === PROFILE_UNRECORDED)
|
|
8908
|
+
continue;
|
|
8909
|
+
const provider = src.profile === "default" ? base : `${base}:${src.profile}`;
|
|
8910
|
+
const list = daysOf.get(provider) ?? daysOf.set(provider, []).get(provider);
|
|
8911
|
+
if (!list.includes(inst.days))
|
|
8912
|
+
list.push(inst.days);
|
|
8913
|
+
const hours = byProvider.get(provider) ?? byProvider.set(provider, new Map()).get(provider);
|
|
8914
|
+
const into = hours.get(Number(hk)) ?? hours.set(Number(hk), Object.create(null)).get(Number(hk));
|
|
8915
|
+
for (const [mk, tot] of Object.entries(src.m)) {
|
|
8916
|
+
const cur = into[mk] ??= [0, 0, 0, 0, 0, 0, 0];
|
|
8917
|
+
for (let i = 0; i < 7; i++)
|
|
8918
|
+
cur[i] = (cur[i] ?? 0) + (tot[i] ?? 0);
|
|
8919
|
+
}
|
|
8920
|
+
}
|
|
8921
|
+
}
|
|
8922
|
+
}
|
|
8923
|
+
const next = new Map();
|
|
8924
|
+
for (const [key, list] of groups) {
|
|
8925
|
+
const [provider, metric] = key.split("\u0000");
|
|
8926
|
+
// A per-model weekly ("Sonnet (weekly)", the vendor's scope "model"): that model's tokens only. Another named
|
|
8927
|
+
// pool is not a model: no estimate. "Weekly", "Weekly limit": everything.
|
|
8928
|
+
const named = /^(.+) \((?:weekly|monthly|\d+d)\)$/i.exec(metric)?.[1]?.toLowerCase() ?? null;
|
|
8929
|
+
const perModel = list.some((r) => r.scope === "model");
|
|
8930
|
+
if (named && !perModel)
|
|
8931
|
+
continue;
|
|
8932
|
+
const hours = [];
|
|
8933
|
+
for (const [hour, models] of byProvider.get(provider) ?? []) {
|
|
8934
|
+
let api = 0, output = 0, all = 0;
|
|
8935
|
+
for (const [mk, tot] of Object.entries(models)) {
|
|
8936
|
+
const bar = mk.lastIndexOf("|"), model = mk.slice(0, bar), long = mk.slice(bar + 1) === "1";
|
|
8937
|
+
if (named && !model.toLowerCase().includes(named))
|
|
8938
|
+
continue;
|
|
8939
|
+
const [, uncached, read, w5, w1h, w, out] = tot;
|
|
8940
|
+
const r = ratesFor(model, long ? Number.MAX_SAFE_INTEGER : 0);
|
|
8941
|
+
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;
|
|
8942
|
+
output += out;
|
|
8943
|
+
all += uncached + read + w5 + w1h + w + out;
|
|
8944
|
+
}
|
|
8945
|
+
if (all > 0)
|
|
8946
|
+
hours.push({ hour, api, output, all });
|
|
8947
|
+
}
|
|
8948
|
+
const base = provider.split(":")[0];
|
|
8949
|
+
const scanned = [...new Set([...(daysOf.get(provider) ?? []), ...(daysOfBase.get(base) ?? [])])];
|
|
8950
|
+
const available = (from, to) => {
|
|
8951
|
+
if (from < keptFrom || !scanned.length)
|
|
8952
|
+
return false;
|
|
8953
|
+
for (let h = from; h <= to; h += HOUR)
|
|
8954
|
+
if (!scanned.every((d) => covered(d, h)))
|
|
8955
|
+
return false;
|
|
8956
|
+
return true;
|
|
8957
|
+
};
|
|
8958
|
+
const est = estimateCapacity(list, hours, available);
|
|
8959
|
+
if (est)
|
|
8960
|
+
next.set(key, est);
|
|
8961
|
+
}
|
|
8962
|
+
this.capacityEstimates = next;
|
|
8963
|
+
return next.size;
|
|
8964
|
+
}
|
|
8965
|
+
catch (err) {
|
|
8966
|
+
this.logger.debug({ err }, "capacity estimate refresh failed");
|
|
8967
|
+
return -1;
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8970
|
+
/** One background quota read, when nothing else reads (no Discord bot) and an account has a running agent. */
|
|
8971
|
+
async backgroundQuotaRead() {
|
|
8972
|
+
if (this.shuttingDown)
|
|
8973
|
+
return false;
|
|
8974
|
+
if ([...this.adapters.values()].some(a => a.type === "discord" && typeof a.setActivity === "function"))
|
|
8975
|
+
return false;
|
|
8976
|
+
const active = this.getActiveUsageProviderIds();
|
|
8977
|
+
if (active.size === 0)
|
|
8978
|
+
return false;
|
|
8979
|
+
try {
|
|
8980
|
+
await getUsageSnapshot(false, active);
|
|
8981
|
+
return true;
|
|
8982
|
+
}
|
|
8983
|
+
catch {
|
|
8984
|
+
return false;
|
|
8985
|
+
}
|
|
8986
|
+
}
|
|
8648
8987
|
/** Keep Discord profile activity aligned with the same cached usage source as /usage. */
|
|
8649
8988
|
startDiscordUsagePresence() {
|
|
8650
8989
|
if (this.discordPresenceTimer)
|
|
@@ -9109,6 +9448,9 @@ export class FleetManager {
|
|
|
9109
9448
|
return;
|
|
9110
9449
|
await this.handleInboundMessage(msg);
|
|
9111
9450
|
}, this.logger, "adapter.message"));
|
|
9451
|
+
// #2940: a deleted message's chat-log line goes (and it is never logged after this).
|
|
9452
|
+
adapter.on("message_deleted", (e) => { if (isCurrentAdapter())
|
|
9453
|
+
this.onPlatformMessageDeleted(e); });
|
|
9112
9454
|
adapter.on("reaction", safeHandler(async (r) => {
|
|
9113
9455
|
if (!isCurrentAdapter())
|
|
9114
9456
|
return;
|
|
@@ -9117,6 +9459,11 @@ export class FleetManager {
|
|
|
9117
9459
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
9118
9460
|
await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
|
|
9119
9461
|
}, this.logger, "adapter.callback_query"));
|
|
9462
|
+
adapter.on("missed_offline", (ev) => {
|
|
9463
|
+
if (!isCurrentAdapter())
|
|
9464
|
+
return;
|
|
9465
|
+
this.recordMissedOffline(ev);
|
|
9466
|
+
});
|
|
9120
9467
|
this.bindTopicClosedHandler(adapter, adapterId, "adapter.topic_closed");
|
|
9121
9468
|
// Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
|
|
9122
9469
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
@@ -9259,6 +9606,9 @@ export class FleetManager {
|
|
|
9259
9606
|
return;
|
|
9260
9607
|
await this.handleInboundMessage(msg);
|
|
9261
9608
|
}, this.logger, `adapter[${adapterId}].message`));
|
|
9609
|
+
// #2940: a deleted message's chat-log line goes (and it is never logged after this).
|
|
9610
|
+
adapter.on("message_deleted", (e) => { if (isCurrentAdapter())
|
|
9611
|
+
this.onPlatformMessageDeleted(e); });
|
|
9262
9612
|
adapter.on("reaction", safeHandler(async (r) => {
|
|
9263
9613
|
if (!isCurrentAdapter())
|
|
9264
9614
|
return;
|
|
@@ -9267,6 +9617,11 @@ export class FleetManager {
|
|
|
9267
9617
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
9268
9618
|
await this.receiveAdapterCallback(data, adapterId, adapter, isCurrentAdapter);
|
|
9269
9619
|
}, this.logger, `adapter[${adapterId}].callback_query`));
|
|
9620
|
+
adapter.on("missed_offline", (ev) => {
|
|
9621
|
+
if (!isCurrentAdapter())
|
|
9622
|
+
return;
|
|
9623
|
+
this.recordMissedOffline(ev);
|
|
9624
|
+
});
|
|
9270
9625
|
this.bindTopicClosedHandler(adapter, adapterId, `adapter[${adapterId}].topic_closed`);
|
|
9271
9626
|
// Slash commands: classic bot + admin commands
|
|
9272
9627
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
@@ -10003,6 +10358,34 @@ export class FleetManager {
|
|
|
10003
10358
|
}
|
|
10004
10359
|
return true;
|
|
10005
10360
|
}
|
|
10361
|
+
/** File a Telegram update that was too old to inject; Needs you lists it until Acknowledge. */
|
|
10362
|
+
recordMissedOffline(ev) {
|
|
10363
|
+
if (!this.missedOfflineStore)
|
|
10364
|
+
this.missedOfflineStore = new MissedOfflineStore(this.dataDir);
|
|
10365
|
+
const stub = {
|
|
10366
|
+
source: "telegram", adapterId: ev.adapterId, chatId: ev.chatId, threadId: ev.threadId,
|
|
10367
|
+
messageId: ev.messageId, userId: ev.userId, username: ev.username, text: ev.preview,
|
|
10368
|
+
timestamp: ev.timestamp, isBotMessage: false,
|
|
10369
|
+
};
|
|
10370
|
+
const threadId = this.inboundRouteThreadId(stub);
|
|
10371
|
+
const target = threadId ? this.resolveInboundTarget(stub, threadId) : undefined;
|
|
10372
|
+
let instance = target?.name ?? "";
|
|
10373
|
+
if (!instance) {
|
|
10374
|
+
instance = Object.entries(this.fleetConfig?.instances ?? {}).find(([name, cfg]) => cfg.general_topic && this.getInstanceAdapterId(name) === ev.adapterId)?.[0] ?? "";
|
|
10375
|
+
}
|
|
10376
|
+
const place = [ev.chatTitle, ev.threadId ? `#${ev.threadId}` : ""].filter(Boolean).join(" · ") || ev.chatId;
|
|
10377
|
+
this.missedOfflineStore.add({
|
|
10378
|
+
id: `missed:${ev.adapterId}:${ev.updateId}`,
|
|
10379
|
+
adapterId: ev.adapterId,
|
|
10380
|
+
instance,
|
|
10381
|
+
username: ev.username,
|
|
10382
|
+
place,
|
|
10383
|
+
preview: ev.preview,
|
|
10384
|
+
since: ev.timestamp.getTime(),
|
|
10385
|
+
});
|
|
10386
|
+
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");
|
|
10387
|
+
this.needsYou?.poke();
|
|
10388
|
+
}
|
|
10006
10389
|
/**
|
|
10007
10390
|
* #1085: coincident topic numbers must not route across Telegram groups.
|
|
10008
10391
|
*/
|
|
@@ -10836,7 +11219,10 @@ export class FleetManager {
|
|
|
10836
11219
|
// else to go, and "retry shortly" would have the agent retry forever. It goes to the web chat.
|
|
10837
11220
|
const webOnlyReply = tool === "reply" && this.worlds.size === 0 && this.isWebOnlyFleet();
|
|
10838
11221
|
if (this.worlds.size === 0 && !webOnlyReply) {
|
|
10839
|
-
|
|
11222
|
+
// #2910: on a web-only fleet there will never be an adapter — say so, so the agent does not retry forever.
|
|
11223
|
+
respond(null, this.isWebOnlyFleet()
|
|
11224
|
+
? `${tool} is not available on a web-only fleet (no Discord or Telegram connection); answer with reply`
|
|
11225
|
+
: "Channel adapters are not ready — retry shortly");
|
|
10840
11226
|
return;
|
|
10841
11227
|
}
|
|
10842
11228
|
// Resolve threadId: use sender's topic_id if sender is a known fleet instance,
|
|
@@ -11036,9 +11422,9 @@ export class FleetManager {
|
|
|
11036
11422
|
}
|
|
11037
11423
|
/** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
|
|
11038
11424
|
/** No chat platform is configured at all: the web dashboard is the fleet's only surface. */
|
|
11425
|
+
/** #2910: no Discord or Telegram connection at all (src/web-only-fleet.ts). */
|
|
11039
11426
|
isWebOnlyFleet() {
|
|
11040
|
-
|
|
11041
|
-
return config != null && !config.channel && !(config.channels?.length);
|
|
11427
|
+
return isWebOnlyFleetConfig(this.fleetConfig);
|
|
11042
11428
|
}
|
|
11043
11429
|
/** On a fleet with no chat platform, the dashboard is where an instance-health prompt is posted (#1307 item 6). */
|
|
11044
11430
|
webOnlyPromptPlace() {
|
|
@@ -11413,6 +11799,9 @@ export class FleetManager {
|
|
|
11413
11799
|
// through the target's adapter, so a Telegram-created schedule for a
|
|
11414
11800
|
// Discord target announced its failure into the wrong platform.
|
|
11415
11801
|
const adapter = this.scheduleSourceAdapter(schedule);
|
|
11802
|
+
// #2910 D2: a web-only fleet has no chat to say it in: a Notice about the schedule's target.
|
|
11803
|
+
if (!adapter && this.recordWebNotice("schedule", `⏰ Schedule "${schedule.label ?? schedule.id}" trigger failed: instance ${schedule.target} is offline.`, schedule.target))
|
|
11804
|
+
return;
|
|
11416
11805
|
if (!adapter) {
|
|
11417
11806
|
this.logger.warn({ scheduleId: schedule.id, chatId: schedule.reply_chat_id, source: schedule.source }, "No adapter can reach the schedule's source chat — failure notice not sent");
|
|
11418
11807
|
return;
|
|
@@ -11620,6 +12009,11 @@ export class FleetManager {
|
|
|
11620
12009
|
let result;
|
|
11621
12010
|
switch (msg.type) {
|
|
11622
12011
|
case "fleet_decision_create": {
|
|
12012
|
+
const supersedes = this.decisionSupersedes("ipc-typed", instanceName, payload.supersedes);
|
|
12013
|
+
if ("error" in supersedes) {
|
|
12014
|
+
ipc.send({ type: "fleet_decision_response", fleetRequestId, error: supersedes.error });
|
|
12015
|
+
return;
|
|
12016
|
+
}
|
|
11623
12017
|
// Prune expired decisions on create
|
|
11624
12018
|
db.pruneExpiredDecisions();
|
|
11625
12019
|
result = db.createDecision({
|
|
@@ -11630,7 +12024,7 @@ export class FleetManager {
|
|
|
11630
12024
|
tags: payload.tags,
|
|
11631
12025
|
ttl_days: payload.ttl_days,
|
|
11632
12026
|
created_by: instanceName,
|
|
11633
|
-
supersedes:
|
|
12027
|
+
supersedes: supersedes.id,
|
|
11634
12028
|
});
|
|
11635
12029
|
break;
|
|
11636
12030
|
}
|
|
@@ -11807,6 +12201,21 @@ export class FleetManager {
|
|
|
11807
12201
|
return "ok";
|
|
11808
12202
|
}
|
|
11809
12203
|
}
|
|
12204
|
+
/**
|
|
12205
|
+
* `post_decision` with `supersedes` changes a decision someone else may have recorded (it stops being active): the
|
|
12206
|
+
* same permission as `update_decision`, asked through the same live check (tool set + dashboard switches), on both
|
|
12207
|
+
* create paths. An empty or absent value supersedes nothing and needs nothing; anything else must be an id.
|
|
12208
|
+
*/
|
|
12209
|
+
decisionSupersedes(sink, instanceName, value) {
|
|
12210
|
+
if (value === undefined || value === null || value === "")
|
|
12211
|
+
return { id: undefined };
|
|
12212
|
+
if (typeof value !== "string")
|
|
12213
|
+
return { error: "supersedes must be a decision id" };
|
|
12214
|
+
const permitted = this.checkToolPermission(sink, instanceName, "update_decision");
|
|
12215
|
+
if (!permitted.allowed)
|
|
12216
|
+
return { error: `Superseding a decision needs update_decision. ${permitted.message}` };
|
|
12217
|
+
return { id: value };
|
|
12218
|
+
}
|
|
11810
12219
|
async handleDecisionCrudHttp(instance, op, args) {
|
|
11811
12220
|
if (!this.scheduler)
|
|
11812
12221
|
return { error: "Scheduler not available" };
|
|
@@ -11822,16 +12231,24 @@ export class FleetManager {
|
|
|
11822
12231
|
if (!title || !content)
|
|
11823
12232
|
return { error: "title and content are required" };
|
|
11824
12233
|
const scope = args.scope === "fleet" ? "fleet" : "project";
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
12234
|
+
const supersedes = this.decisionSupersedes("agent-endpoint", instance, args.supersedes);
|
|
12235
|
+
if ("error" in supersedes)
|
|
12236
|
+
return { error: supersedes.error };
|
|
12237
|
+
try {
|
|
12238
|
+
return db.createDecision({
|
|
12239
|
+
project_root: projectRoot,
|
|
12240
|
+
scope,
|
|
12241
|
+
title,
|
|
12242
|
+
content,
|
|
12243
|
+
tags: asStrArr(args.tags),
|
|
12244
|
+
ttl_days: asNum(args.ttl_days),
|
|
12245
|
+
supersedes: supersedes.id,
|
|
12246
|
+
created_by: instance,
|
|
12247
|
+
});
|
|
12248
|
+
}
|
|
12249
|
+
catch (err) {
|
|
12250
|
+
return { error: err.message }; // a refused predecessor: nothing was inserted
|
|
12251
|
+
}
|
|
11835
12252
|
}
|
|
11836
12253
|
// #2152: titles by default (agend-agent decision-list); one decision's full text by id (decision-get <id>).
|
|
11837
12254
|
// The CLI sends include_archived (snake case, as the MCP tool): both spellings are read.
|
|
@@ -12120,16 +12537,89 @@ export class FleetManager {
|
|
|
12120
12537
|
}
|
|
12121
12538
|
return adapter.createTopic(topicName);
|
|
12122
12539
|
}
|
|
12123
|
-
|
|
12124
|
-
|
|
12125
|
-
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12131
|
-
this.
|
|
12132
|
-
|
|
12540
|
+
/**
|
|
12541
|
+
* Delete an agent and, unless asked not to, its own chat topic (user decision: by default). The topic is planned
|
|
12542
|
+
* before the removal (its id and bot go with the fleet.yaml entry) and deleted through the agent's OWN bot after it;
|
|
12543
|
+
* the outcome is the platform's answer (instance-topic-delete.ts). A topic that cannot go never fails the removal.
|
|
12544
|
+
*/
|
|
12545
|
+
async removeInstanceAndTopic(name, authorization, opts) {
|
|
12546
|
+
const plan = opts.deleteTopic ? this.planInstanceTopicDelete(name) : null;
|
|
12547
|
+
if (opts.execution)
|
|
12548
|
+
await this.removeInstance(name, authorization, opts.execution);
|
|
12549
|
+
else
|
|
12550
|
+
await this.removeInstance(name, authorization);
|
|
12551
|
+
// #3055 review (Prism P1): asked again right before the platform call — the fleet as it is NOW, not as planned
|
|
12552
|
+
// before the removal was awaited (a new owner of the topic, or the bot rebuilt).
|
|
12553
|
+
return deletePlannedTopic(plan, this.logger, (p) => {
|
|
12554
|
+
if (this.instanceTopicAdapter(p.adapterId) !== p.adapter)
|
|
12555
|
+
return "the agent's bot changed while it was being deleted, so its topic was left";
|
|
12556
|
+
return topicMustStay(p.destination, this.topicOwnership(), null); // the original is gone: every owner now counts
|
|
12557
|
+
}, { begin: (d) => this.topicDeletionsInFlight.add(d), end: (d) => this.topicDeletionsInFlight.delete(d) });
|
|
12558
|
+
}
|
|
12559
|
+
/** #3055: destinations whose platform deletion is running now; a binding to one waits until it is over. */
|
|
12560
|
+
topicDeletionsInFlight = new Set();
|
|
12561
|
+
/** True while a topic with this id is being deleted on any connection (a new binding of it is refused meanwhile). */
|
|
12562
|
+
isTopicDeletionInFlight(topicId) {
|
|
12563
|
+
const tail = `:${topicId}`;
|
|
12564
|
+
return [...this.topicDeletionsInFlight].some(d => d.endsWith(tail));
|
|
12565
|
+
}
|
|
12566
|
+
/**
|
|
12567
|
+
* #3055 review r2 (Prism P1-3): would this instance config bind a topic whose deletion is running now? Compared by
|
|
12568
|
+
* physical destination, so the same thread number in another Telegram group is not held. Settings asks it at commit.
|
|
12569
|
+
*/
|
|
12570
|
+
topicBindingInFlight(inst) {
|
|
12571
|
+
if (this.topicDeletionsInFlight.size === 0 || inst.topic_id == null || inst.topic_id === "")
|
|
12572
|
+
return false;
|
|
12573
|
+
const adapterId = typeof inst.channel_id === "string" && inst.channel_id ? inst.channel_id : this.getPrimaryAdapterId();
|
|
12574
|
+
return this.topicDeletionsInFlight.has(this.destinationFor(adapterId, inst.topic_id));
|
|
12575
|
+
}
|
|
12576
|
+
/** Any topic deletion running now (a connection's group or General may not be re-pointed meanwhile). */
|
|
12577
|
+
topicDeletionPending() { return this.topicDeletionsInFlight.size > 0; }
|
|
12578
|
+
/** The physical destination of `topicId` on the bot `adapterId`. */
|
|
12579
|
+
destinationFor(adapterId, topicId) {
|
|
12580
|
+
const world = adapterId ? this.worlds.get(adapterId) : undefined;
|
|
12581
|
+
const platform = world?.adapter?.type ?? this.getChannelConfig(adapterId)?.type;
|
|
12582
|
+
const groupId = world?.groupId ?? String(this.getChannelConfig(adapterId)?.group_id ?? "");
|
|
12583
|
+
return topicDestination(platform, groupId, adapterId, topicId);
|
|
12584
|
+
}
|
|
12585
|
+
/** The adapter of the bot `adapterId` names — never a substitute for a bot that is not there. */
|
|
12586
|
+
instanceTopicAdapter(adapterId) {
|
|
12587
|
+
return (adapterId ? this.worlds.get(adapterId)?.adapter : this.worlds.size === 0 ? this.adapter ?? undefined : undefined) ?? undefined;
|
|
12588
|
+
}
|
|
12589
|
+
/**
|
|
12590
|
+
* Topic ownership by physical destination (#3055 review, Prism P1): an instance's topic is where its bot's platform
|
|
12591
|
+
* puts it (a Telegram group + thread, a Discord channel), so two bots in one group, or two instances on one
|
|
12592
|
+
* channel, are seen to share it; and every connection's own General is protected, with or without a General agent.
|
|
12593
|
+
*/
|
|
12594
|
+
topicOwnership() {
|
|
12595
|
+
const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
|
|
12596
|
+
const destinationOf = (n) => {
|
|
12597
|
+
const cfg = this.fleetConfig?.instances?.[n];
|
|
12598
|
+
if (!cfg || cfg.topic_id == null || cfg.topic_id === "")
|
|
12599
|
+
return null;
|
|
12600
|
+
return this.destinationFor(this.getInstanceAdapterId(n), cfg.topic_id);
|
|
12601
|
+
};
|
|
12602
|
+
const generalDestinations = () => {
|
|
12603
|
+
const out = new Set();
|
|
12604
|
+
for (const ch of channels) {
|
|
12605
|
+
const id = ch.id ?? ch.type;
|
|
12606
|
+
const groupId = this.worlds.get(id)?.groupId ?? (ch.group_id != null ? String(ch.group_id) : undefined);
|
|
12607
|
+
if (ch.type === "telegram")
|
|
12608
|
+
out.add(topicDestination("telegram", groupId, id, 1));
|
|
12609
|
+
if (ch.type === "discord") {
|
|
12610
|
+
const gcid = discordGeneralChannelId(ch);
|
|
12611
|
+
if (gcid)
|
|
12612
|
+
out.add(topicDestination("discord", groupId, id, gcid));
|
|
12613
|
+
}
|
|
12614
|
+
}
|
|
12615
|
+
return out;
|
|
12616
|
+
};
|
|
12617
|
+
return { instances: this.fleetConfig?.instances, destinationOf, generalDestinations };
|
|
12618
|
+
}
|
|
12619
|
+
/** What deleting `name`'s topic would act on: its own bot only — a bot that is not there is never substituted. */
|
|
12620
|
+
planInstanceTopicDelete(name) {
|
|
12621
|
+
const adapterId = this.getInstanceAdapterId(name);
|
|
12622
|
+
return planInstanceTopicDelete({ name, adapterId, adapter: this.instanceTopicAdapter(adapterId), ...this.topicOwnership() });
|
|
12133
12623
|
}
|
|
12134
12624
|
getForumTopicDeleter(adapterId) {
|
|
12135
12625
|
const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
|
|
@@ -12530,6 +13020,7 @@ export class FleetManager {
|
|
|
12530
13020
|
Object.assign(current, resolved);
|
|
12531
13021
|
}
|
|
12532
13022
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
13023
|
+
this.agentCredentialsCache?.observePolicies();
|
|
12533
13024
|
this.refreshSavedDashboardPermissions();
|
|
12534
13025
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
12535
13026
|
}
|
|
@@ -12818,6 +13309,9 @@ export class FleetManager {
|
|
|
12818
13309
|
await this.lifecycle.remove(name, authorization, execution, transition);
|
|
12819
13310
|
else
|
|
12820
13311
|
await this.lifecycle.remove(name, authorization);
|
|
13312
|
+
// #1490 row 27: its hang/exit offers were for the instance now gone (stopInstance clears them; removal stops
|
|
13313
|
+
// through the lifecycle directly). The offers' own lifecycle pins refuse them too.
|
|
13314
|
+
this.clearNoncePromptsForInstance(name);
|
|
12821
13315
|
// #1774: removed — its replies still copying their files are no one's to show, replaced or not (a failed removal
|
|
12822
13316
|
// throws above: its replies stay its own).
|
|
12823
13317
|
this.webChatRemovals.set(name, { below: removedBelow, ...(removedDaemon !== undefined ? { daemon: removedDaemon } : {}) });
|
|
@@ -14221,6 +14715,8 @@ export class FleetManager {
|
|
|
14221
14715
|
* back to posting at the bare group id, which is the defect this replaced.
|
|
14222
14716
|
*/
|
|
14223
14717
|
postDailySummary(text) {
|
|
14718
|
+
if (this.recordWebNotice("summary", text))
|
|
14719
|
+
return; // #2910 D2: a web-only fleet's Notices
|
|
14224
14720
|
const target = this.fleetNoticeTarget();
|
|
14225
14721
|
if (!this.adapter || !target) {
|
|
14226
14722
|
this.logger.warn("Daily summary has no postable target — set a General topic or channel.options.general_channel_id");
|
|
@@ -14273,6 +14769,10 @@ export class FleetManager {
|
|
|
14273
14769
|
dispatched = true;
|
|
14274
14770
|
}
|
|
14275
14771
|
}
|
|
14772
|
+
// #2910 D2: a web-only fleet has no General: the notice goes to the dashboard's Notices instead (and spends the
|
|
14773
|
+
// throttle window like a post would).
|
|
14774
|
+
if (!dispatched && this.recordWebNotice("fleet", body))
|
|
14775
|
+
dispatched = true;
|
|
14276
14776
|
if (!dispatched) {
|
|
14277
14777
|
// Do NOT record the throttle key. It used to be claimed before delivery
|
|
14278
14778
|
// was attempted, so a message nobody could receive — every fleet error
|
|
@@ -14295,6 +14795,31 @@ export class FleetManager {
|
|
|
14295
14795
|
}
|
|
14296
14796
|
return true;
|
|
14297
14797
|
}
|
|
14798
|
+
webNotices = null;
|
|
14799
|
+
/** #2910 D2 (web-api): the Notices on a web-only fleet; null on any other (its General says them). */
|
|
14800
|
+
webNoticesFeed() { return this.webOnlyPromptPlace() ? this.webNoticeStore() : null; }
|
|
14801
|
+
/** #2910 D2: the web-only fleet's Notices (web-notices.json), read on first use. */
|
|
14802
|
+
webNoticeStore() {
|
|
14803
|
+
if (!this.dataDir)
|
|
14804
|
+
return null;
|
|
14805
|
+
this.webNotices ??= new WebNotices(join(this.dataDir, WEB_NOTICES_FILE), this.logger);
|
|
14806
|
+
return this.webNotices;
|
|
14807
|
+
}
|
|
14808
|
+
/**
|
|
14809
|
+
* #2910 D2: on a web-only fleet (no chat connection at all), keep what General or an agent's topic would have been
|
|
14810
|
+
* told as a Notice, and push it to the dashboard. False on any other fleet (its chat says it; nothing is kept).
|
|
14811
|
+
*/
|
|
14812
|
+
recordWebNotice(kind, text, instance) {
|
|
14813
|
+
if (!this.webOnlyPromptPlace())
|
|
14814
|
+
return false;
|
|
14815
|
+
const store = this.webNoticeStore();
|
|
14816
|
+
if (!store)
|
|
14817
|
+
return false;
|
|
14818
|
+
const known = instance !== undefined && Object.hasOwn(this.fleetConfig?.instances ?? {}, instance) ? instance : undefined;
|
|
14819
|
+
const notice = store.add({ kind, text: stripPlaceRefs(text), ...(known ? { instance: known } : {}) });
|
|
14820
|
+
this.emitSseEvent("notice", { notice, unread: store.unread() });
|
|
14821
|
+
return true;
|
|
14822
|
+
}
|
|
14298
14823
|
static FLEET_ERROR_THROTTLE_MS = 10 * 60_000;
|
|
14299
14824
|
fleetErrorNotices = new Map();
|
|
14300
14825
|
/**
|
|
@@ -14308,6 +14833,9 @@ export class FleetManager {
|
|
|
14308
14833
|
* would have to make this async and change every caller.
|
|
14309
14834
|
*/
|
|
14310
14835
|
notifyInstanceTopic(instanceName, text, extraOpts) {
|
|
14836
|
+
// #2910 D2: a web-only fleet has no topics: what the agent's topic would have been told is a Notice about it.
|
|
14837
|
+
if (this.webOnlyPromptPlace())
|
|
14838
|
+
return this.recordWebNotice("instance", text, instanceName);
|
|
14311
14839
|
const send = this.sendToInstanceTopic(instanceName, text, extraOpts);
|
|
14312
14840
|
if (!send)
|
|
14313
14841
|
return false;
|
|
@@ -14458,6 +14986,7 @@ export class FleetManager {
|
|
|
14458
14986
|
threadId: opts.threadId,
|
|
14459
14987
|
expiredText: opts.expiredText,
|
|
14460
14988
|
createdAt: Date.now(),
|
|
14989
|
+
lifecycleCurrent: this.lifecyclePin(opts.instanceName),
|
|
14461
14990
|
...opts.extra,
|
|
14462
14991
|
};
|
|
14463
14992
|
entry.timer = setTimeout(() => {
|
|
@@ -14809,6 +15338,7 @@ export class FleetManager {
|
|
|
14809
15338
|
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
14810
15339
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
14811
15340
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
15341
|
+
...(this.backendNameOf(name) === "codex" ? { profileTimelinePath: join(this.getInstanceDir(name), PROFILE_TIMELINE_FILE) } : {}),
|
|
14812
15342
|
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
14813
15343
|
}));
|
|
14814
15344
|
const taken = new Set(fleet.map(i => i.name));
|
|
@@ -14820,6 +15350,9 @@ export class FleetManager {
|
|
|
14820
15350
|
claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
|
|
14821
15351
|
profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14822
15352
|
kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
15353
|
+
// #3066 §1d (Fable r1 #2): a ClassicBot Codex room records its launches too (onDaemonReady) — read them the same way.
|
|
15354
|
+
...((this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend) === "codex"
|
|
15355
|
+
? { profileTimelinePath: join(this.getInstanceDir(ch.instanceName), PROFILE_TIMELINE_FILE) } : {}),
|
|
14823
15356
|
}));
|
|
14824
15357
|
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
14825
15358
|
}
|
|
@@ -14980,7 +15513,8 @@ export class FleetManager {
|
|
|
14980
15513
|
out.push({
|
|
14981
15514
|
name, state: p.state ?? undefined,
|
|
14982
15515
|
interaction: p.interaction ? { kind: p.interaction.kind, owner: wait?.owner ?? null, episode: p.interaction.episode, since: p.interaction.since } : null,
|
|
14983
|
-
interactionSummary: p.interaction_summary ?? null,
|
|
15516
|
+
interactionSummary: p.interaction?.kind === "kiro_choice" ? t("kiro3.running") : p.interaction_summary ?? null,
|
|
15517
|
+
kiroCompatibility: this.kiroCompatibilityHolds?.get(name),
|
|
14984
15518
|
pauseReason, pausedAt,
|
|
14985
15519
|
crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
|
|
14986
15520
|
signIn: pauseReason === "auth" ? this.signInFor(name) : null,
|
|
@@ -15056,6 +15590,10 @@ export class FleetManager {
|
|
|
15056
15590
|
throw new Error("the delivery outbox is not open");
|
|
15057
15591
|
return this.deliveryOutbox.acknowledge(deliveryId, by);
|
|
15058
15592
|
},
|
|
15593
|
+
missedOffline: () => (this.missedOfflineStore?.list(Date.now()) ?? []).map(m => ({
|
|
15594
|
+
id: m.id, instance: m.instance, owner: m.adapterId, username: m.username, place: m.place, preview: m.preview, since: m.since,
|
|
15595
|
+
})),
|
|
15596
|
+
acknowledgeMissed: (id, by) => this.missedOfflineStore?.acknowledge(id, by) ?? false,
|
|
15059
15597
|
tasks: () => this.needsYouTaskItems(),
|
|
15060
15598
|
backendSignIns: () => this.needsYouBackendSignIns(),
|
|
15061
15599
|
configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
|
|
@@ -15584,6 +16122,7 @@ export class FleetManager {
|
|
|
15584
16122
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
15585
16123
|
authorization: input.session ? this.sessionAuthorizationStore()?.get(input.session.handle) ?? null : null,
|
|
15586
16124
|
policy: { ...webAuthorityPolicy(this.fleetConfig?.web), enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now: Date.now(),
|
|
16125
|
+
hostOperator: this.isWebOnlyFleet(),
|
|
15587
16126
|
});
|
|
15588
16127
|
}
|
|
15589
16128
|
sessionAuthorizations = null;
|
|
@@ -15616,6 +16155,33 @@ export class FleetManager {
|
|
|
15616
16155
|
store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
|
|
15617
16156
|
return { ok: true, ...store.authorize(session.handle, session.owner, now, state.epoch, now + policy.maxSessionMs, peer) };
|
|
15618
16157
|
}
|
|
16158
|
+
/**
|
|
16159
|
+
* #2910 D1 (auth-api, at sign-in): a web-only fleet's host session — signed in with the host's own code (`agend web
|
|
16160
|
+
* --code`) on this computer — is the host operator, authorized from now for max_session_hours (as an account's
|
|
16161
|
+
* Confirm would be), under the current epoch: a revocation ends it, and only a new sign-in on this computer gives it
|
|
16162
|
+
* again. A fleet with a chat connection, another session, or another peer: nothing (§3 as before).
|
|
16163
|
+
*/
|
|
16164
|
+
authorizeHostSession(session, peer) {
|
|
16165
|
+
const store = this.sessionAuthorizationStore();
|
|
16166
|
+
if (!store || !session.host || session.owner || !this.isWebOnlyFleet())
|
|
16167
|
+
return;
|
|
16168
|
+
const state = this.webAuthorityState(), now = Date.now(), policy = webAuthorityPolicy(this.fleetConfig?.web);
|
|
16169
|
+
const check = evaluateWebAuthority({
|
|
16170
|
+
session, gateway: false, peer, tokenEpoch: this.webToken ? tokenEpoch(this.webToken) : null,
|
|
16171
|
+
isAdmin: () => false,
|
|
16172
|
+
authorization: { handle: session.handle, owner: HOST_OPERATOR, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch },
|
|
16173
|
+
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
16174
|
+
hostOperator: true,
|
|
16175
|
+
});
|
|
16176
|
+
if (check.refusal) {
|
|
16177
|
+
this.logger.info({ handle: session.handle, refusal: check.refusal }, "Host web session not authorized");
|
|
16178
|
+
return;
|
|
16179
|
+
}
|
|
16180
|
+
const live = new Set(this.webSessions?.list(undefined, () => true).map(r => r.handle) ?? []);
|
|
16181
|
+
store.prune(a => a.epoch === state.epoch && now <= Math.min(a.until, a.authorizedAt + policy.maxSessionMs) && live.has(a.handle));
|
|
16182
|
+
const { durable } = store.authorize(session.handle, HOST_OPERATOR, now, state.epoch, now + policy.maxSessionMs, peer);
|
|
16183
|
+
this.logger.info({ handle: session.handle, durable }, "Host web session authorized (this computer's admin)");
|
|
16184
|
+
}
|
|
15619
16185
|
authorityStateStore = null;
|
|
15620
16186
|
/**
|
|
15621
16187
|
* #1939 §10: the switch, "Only this computer can authorize" and the revocation epoch in force for the configuration as
|
|
@@ -15671,7 +16237,10 @@ export class FleetManager {
|
|
|
15671
16237
|
const sessions = new Map((this.webSessions?.list(undefined, () => true) ?? []).map(r => [r.handle, r]));
|
|
15672
16238
|
const authorized = state.enabled ? (this.sessionAuthorizationStore()?.list() ?? [])
|
|
15673
16239
|
.filter(a => a.epoch === state.epoch && sessions.has(a.handle) && now <= Math.min(a.until, a.authorizedAt + maxMs))
|
|
15674
|
-
.map(a => ({ handle: a.handle, label: sessions.get(a.handle).label,
|
|
16240
|
+
.map(a => ({ handle: a.handle, label: sessions.get(a.handle).label,
|
|
16241
|
+
// #2910 D1: the host operator's own session says whose it is, not "host:operator".
|
|
16242
|
+
account: a.owner.adapterId === HOST_OPERATOR.adapterId && a.owner.userId === HOST_OPERATOR.userId && this.isWebOnlyFleet()
|
|
16243
|
+
? t("settings_confirm.host_operator_label") : `${a.owner.adapterId}:${a.owner.userId}`, authorizedAt: a.authorizedAt,
|
|
15675
16244
|
until: Math.min(a.until, a.authorizedAt + maxMs, sessions.get(a.handle).absoluteExpiry),
|
|
15676
16245
|
// Over a link: the temporary one (§0.3), else the named one (a record from before §0.3 has no peer).
|
|
15677
16246
|
...(sessions.get(a.handle).surface === "gateway" ? (a.peer === "quick_link" ? { quick: true } : { named: true }) : {}) })) : [];
|
|
@@ -15739,8 +16308,9 @@ export class FleetManager {
|
|
|
15739
16308
|
return r.ok ? { ok: true, until: Math.min(r.authorization.until, record.absoluteExpiry) } : { ok: false, refusal: r.refusal };
|
|
15740
16309
|
},
|
|
15741
16310
|
signOut: (handle) => {
|
|
15742
|
-
this.webSessions?.revokeByHandle(handle);
|
|
15743
|
-
this.sessionAuthorizationStore()?.prune(a => a.handle !== handle);
|
|
16311
|
+
const session = this.webSessions?.revokeByHandle(handle) ?? { found: false, durable: true };
|
|
16312
|
+
const authorization = this.sessionAuthorizationStore()?.prune(a => a.handle !== handle) ?? { dropped: 0, durable: true };
|
|
16313
|
+
return { durable: session.durable, authorizationDurable: authorization.durable };
|
|
15744
16314
|
},
|
|
15745
16315
|
event: (type, payload) => { try {
|
|
15746
16316
|
this.eventLog?.insert("web", type, payload);
|
|
@@ -15753,6 +16323,7 @@ export class FleetManager {
|
|
|
15753
16323
|
expired: (c) => t("web_auth.card_expired", c.label),
|
|
15754
16324
|
refused: (c) => t("web_auth.card_refused", c.label, t(`web_auth.refusal_${c.refusal}`)),
|
|
15755
16325
|
revoked: (c) => t("web_auth.card_revoked", c.label),
|
|
16326
|
+
notSaved: (c) => t("web_auth.card_not_saved", c.label),
|
|
15756
16327
|
ended: (c) => t("web_auth.card_ended", c.label),
|
|
15757
16328
|
generalHint: () => t("web_auth.general_hint"),
|
|
15758
16329
|
confirmLabel: () => t("web_auth.confirm"),
|
|
@@ -15781,6 +16352,8 @@ export class FleetManager {
|
|
|
15781
16352
|
isAdmin: (userId, adapterId) => this.adminGate(userId, adapterId) === "ok",
|
|
15782
16353
|
authorization: s?.owner ? { handle: s.handle, owner: s.owner, authorizedAt: now, until: now + policy.maxSessionMs, epoch: state.epoch } : null,
|
|
15783
16354
|
policy: { ...policy, enabled: state.enabled, loopbackOnly: state.loopbackOnly, namedLink: state.namedLink, quickLink: state.quickLink }, epoch: state.epoch, now,
|
|
16355
|
+
// #2910 D1: a host operator's session is never asked about (no account to ask): only a sign-in authorizes it.
|
|
16356
|
+
hostOperator: this.isWebOnlyFleet(),
|
|
15784
16357
|
});
|
|
15785
16358
|
return r.refusal;
|
|
15786
16359
|
}
|
|
@@ -16088,8 +16661,10 @@ export class FleetManager {
|
|
|
16088
16661
|
? [{ action: "resume", label: t("inst.session_oom_resume") }, { action: "fresh", label: t("inst.session_oom_fresh") }]
|
|
16089
16662
|
: [{ action: "restart", label: t("exit.restart") }, { action: "ignore", label: t("exit.ignore") }];
|
|
16090
16663
|
const extra = sessionOomRetry ? { sessionOomRetry, ...(current ? { confirmationCurrent: current } : {}) } : undefined;
|
|
16091
|
-
this.notifyInstanceTopic(instanceName, (oom ? message : t("exit.instance_notice", instanceName)) + placeSuffix({ kind: "general" }, "General"));
|
|
16092
16664
|
const web = this.webOnlyPromptPlace();
|
|
16665
|
+
// A pointer to General's controls; on a web-only fleet (#2910 D2) the controls are in Needs you and nothing points there.
|
|
16666
|
+
if (!web)
|
|
16667
|
+
this.notifyInstanceTopic(instanceName, (oom ? message : t("exit.instance_notice", instanceName)) + placeSuffix({ kind: "general" }, "General"));
|
|
16093
16668
|
if (web) {
|
|
16094
16669
|
await this.postNonceButtonPrompt({
|
|
16095
16670
|
prefix, alertType: "exit_restart", instanceName, ...web,
|
|
@@ -16136,21 +16711,27 @@ export class FleetManager {
|
|
|
16136
16711
|
if (claimed === "consumed")
|
|
16137
16712
|
return true;
|
|
16138
16713
|
const { entry: pending, action } = claimed;
|
|
16714
|
+
// #1490 row 27: the lifecycle the button was offered for — a replacement since (removal does not clear prompts) is stale.
|
|
16715
|
+
const current = pending.lifecycleCurrent ?? this.lifecyclePin(pending.instanceName);
|
|
16139
16716
|
this.eventLog?.insert(pending.instanceName, oom ? "session_oom_action" : "normal_exit_action", { action, userId: data.userId });
|
|
16140
16717
|
if (action === "ignore") {
|
|
16141
16718
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.ignored", pending.instanceName));
|
|
16142
16719
|
return true;
|
|
16143
16720
|
}
|
|
16721
|
+
if (!current()) {
|
|
16722
|
+
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("restart.superseded", this.shownInstanceName(pending.instanceName)));
|
|
16723
|
+
return true;
|
|
16724
|
+
}
|
|
16144
16725
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("exit.restarting", pending.instanceName));
|
|
16145
16726
|
try {
|
|
16146
|
-
await this.restartSingleInstance(pending.instanceName, { explicit: true, ...(oom ? { freshStart: action === "fresh", sessionOomRetry: pending.sessionOomRetry,
|
|
16727
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true, ownerCurrent: current, ...(oom ? { freshStart: action === "fresh", sessionOomRetry: pending.sessionOomRetry,
|
|
16147
16728
|
...(pending.confirmationCurrent ? { recoveryCurrent: pending.confirmationCurrent } : {}) } : {}) });
|
|
16148
16729
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("exit.restarted", pending.instanceName), pending.threadId);
|
|
16149
16730
|
}
|
|
16150
16731
|
catch (err) {
|
|
16151
16732
|
const message = err instanceof Error ? err.message : String(err);
|
|
16152
16733
|
this.logger.error({ err, instanceName: pending.instanceName }, "Normal-exit restart failed");
|
|
16153
|
-
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("exit.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show normal-exit restart error"));
|
|
16734
|
+
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, err instanceof RestartSupersededError ? message : t("exit.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show normal-exit restart error"));
|
|
16154
16735
|
}
|
|
16155
16736
|
return true;
|
|
16156
16737
|
}
|
|
@@ -16168,11 +16749,19 @@ export class FleetManager {
|
|
|
16168
16749
|
* contain usernames or secret-adjacent text and stays in daemon.log.
|
|
16169
16750
|
*/
|
|
16170
16751
|
async notifyInteractivePrompt(instanceName, kind) {
|
|
16171
|
-
|
|
16752
|
+
// #2910 D2: on a web-only fleet the assist prompt is in Needs you; there is no General to point to.
|
|
16753
|
+
if (!this.webOnlyPromptPlace())
|
|
16754
|
+
this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName) + placeSuffix({ kind: "general" }, "General"));
|
|
16172
16755
|
// Multi-world fleets can have one General per bot/platform. Route the assist
|
|
16173
16756
|
// request to the General bound to the same adapter as the blocked instance.
|
|
16174
16757
|
const generalName = this.findGeneralInstance(this.getInstanceAdapterId(instanceName));
|
|
16175
16758
|
if (!generalName) {
|
|
16759
|
+
// #2910 D4: a web-only fleet with no General to ask for help — and a password or sudo prompt is not answered from a
|
|
16760
|
+
// web page: a Notice says where it waits (its View, where the person types it).
|
|
16761
|
+
if (this.webOnlyPromptPlace()) {
|
|
16762
|
+
this.recordWebNotice("instance", t("interactive.web_only_notice", instanceName, this.interactivePromptLabel(kind)), instanceName);
|
|
16763
|
+
return;
|
|
16764
|
+
}
|
|
16176
16765
|
this.logger.warn({ instanceName }, "Interactive prompt has no General topic notification target");
|
|
16177
16766
|
return;
|
|
16178
16767
|
}
|
|
@@ -16331,6 +16920,10 @@ export class FleetManager {
|
|
|
16331
16920
|
* was gone; this is the fallback).
|
|
16332
16921
|
*/
|
|
16333
16922
|
async postRemoteCard(instanceName, card) {
|
|
16923
|
+
// #2910 D4: a CLI's own dialog read as a card (selection-card.ts) is put to people only on a web-only fleet — the
|
|
16924
|
+
// dashboard; a chat fleet keeps its parked notice and its General's controls, as before.
|
|
16925
|
+
if (card.kind === "question" && card.webOnly && !this.webOnlyPromptPlace())
|
|
16926
|
+
return;
|
|
16334
16927
|
this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
|
|
16335
16928
|
const place = this.remoteCardPlace(instanceName);
|
|
16336
16929
|
if (!place) {
|
|
@@ -16651,7 +17244,14 @@ export class FleetManager {
|
|
|
16651
17244
|
return;
|
|
16652
17245
|
}
|
|
16653
17246
|
const publication = this.beginCancelButtonPublication(instanceName, correlationId);
|
|
16654
|
-
|
|
17247
|
+
// M-2391: a re-post below a reply (preserveProgress) continues the same turn — its elapsed time continues from
|
|
17248
|
+
// the live bubble's start, as its tool list does. Restarting it at 0 meant an agent replying more often than the
|
|
17249
|
+
// threshold never showed elapsed time at all. A fresh turn (no preserve) starts at 0 as before.
|
|
17250
|
+
const carriedStart = preserveProgress ? this.liveButtonStart(instanceName) : undefined;
|
|
17251
|
+
const carriedMs = carriedStart !== undefined ? Math.max(0, Date.now() - carriedStart) : 0;
|
|
17252
|
+
const initialProgressText = carriedStart !== undefined && carriedMs >= this.progressMinElapsedMs()
|
|
17253
|
+
? FleetManager.progressText(carriedMs, null, this.progressMinElapsedMs(), this.progressPrefixFor(instanceName, carriedMs))
|
|
17254
|
+
: FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName, 0, 1));
|
|
16655
17255
|
try {
|
|
16656
17256
|
const sent = await adapter.notifyAlert(chatId, {
|
|
16657
17257
|
type: "cancel",
|
|
@@ -16660,6 +17260,9 @@ export class FleetManager {
|
|
|
16660
17260
|
choices: [{ id: `cancel:${instanceName}`, label: t("cancel.button") }],
|
|
16661
17261
|
}, threadId ? { threadId } : undefined);
|
|
16662
17262
|
publication.inFlight = false;
|
|
17263
|
+
// #2940: a status bubble of ours — never a chat-log line, for any bot of this fleet in the room; one a sibling
|
|
17264
|
+
// already logged (its gateway event can come before this answer) is taken out again.
|
|
17265
|
+
this.noteOwnStatusMessage(adapter, sent);
|
|
16663
17266
|
const entry = {
|
|
16664
17267
|
instanceName,
|
|
16665
17268
|
adapterId,
|
|
@@ -16671,7 +17274,7 @@ export class FleetManager {
|
|
|
16671
17274
|
// Elapsed time is measured from when this button was posted — i.e. from
|
|
16672
17275
|
// when the work was handed over — not from the pane's working transition,
|
|
16673
17276
|
// which resets if the CLI blips idle mid-turn.
|
|
16674
|
-
startedAt: Date.now(),
|
|
17277
|
+
startedAt: carriedStart ?? Date.now(),
|
|
16675
17278
|
// Matches the text notifyAlert just posted, so the first 60s tick does
|
|
16676
17279
|
// not re-edit identical text — which put a "(edited)" mark on Discord
|
|
16677
17280
|
// with nothing visibly changed.
|
|
@@ -16794,14 +17397,16 @@ export class FleetManager {
|
|
|
16794
17397
|
* Make a tool summary safe to paste into a channel message.
|
|
16795
17398
|
*
|
|
16796
17399
|
* The text is agent-controlled (it is built from tool inputs — file paths,
|
|
16797
|
-
* shell commands), so it gets flattened to one line, capped,
|
|
16798
|
-
* the two Discord mass-mention triggers.
|
|
16799
|
-
*
|
|
17400
|
+
* shell commands), so it gets flattened to one line, secrets masked, capped,
|
|
17401
|
+
* and stripped of the two Discord mass-mention triggers. Masking comes BEFORE
|
|
17402
|
+
* the cap: a cap can cut a mask's context but must never be what hides (or
|
|
17403
|
+
* fails to hide) a secret. Neither channel renders it with a parse mode, so no
|
|
17404
|
+
* markup escaping is needed beyond that.
|
|
16800
17405
|
*/
|
|
16801
17406
|
static sanitizeActivity(activity) {
|
|
16802
17407
|
if (!activity)
|
|
16803
17408
|
return null;
|
|
16804
|
-
const flat = activity.replace(/\s+/g, " ").replace(/@(everyone|here)/g, "@$1").trim();
|
|
17409
|
+
const flat = maskSecrets(activity.replace(/\s+/g, " ")).replace(/@(everyone|here)/g, "@$1").trim();
|
|
16805
17410
|
if (!flat)
|
|
16806
17411
|
return null;
|
|
16807
17412
|
return flat.length > PROGRESS_ACTIVITY_MAX_CHARS
|
|
@@ -16920,6 +17525,12 @@ export class FleetManager {
|
|
|
16920
17525
|
}
|
|
16921
17526
|
return PROGRESS_MIN_ELAPSED_MS;
|
|
16922
17527
|
}
|
|
17528
|
+
/** M-2391: when the instance's live (not retiring) bubble started — the current turn's start — or undefined. */
|
|
17529
|
+
liveButtonStart(instanceName) {
|
|
17530
|
+
const starts = [...this.cancelButtons.values()].filter((e) => e.instanceName === instanceName && !e.retiring && typeof e.startedAt === "number")
|
|
17531
|
+
.map((e) => e.startedAt);
|
|
17532
|
+
return starts.length ? Math.min(...starts) : undefined;
|
|
17533
|
+
}
|
|
16923
17534
|
startProgressTicker(entry) {
|
|
16924
17535
|
const tick = () => this.refreshBubble(entry);
|
|
16925
17536
|
entry.progressTimer = setInterval(tick, PROGRESS_UPDATE_INTERVAL_MS);
|
|
@@ -16929,7 +17540,9 @@ export class FleetManager {
|
|
|
16929
17540
|
// one additional edit per turn that lives past the threshold.
|
|
16930
17541
|
const firstAt = this.progressMinElapsedMs() - (Date.now() - (entry.startedAt ?? Date.now()));
|
|
16931
17542
|
if (firstAt > 0 && firstAt < PROGRESS_UPDATE_INTERVAL_MS) {
|
|
16932
|
-
|
|
17543
|
+
// M-2391: a little past the threshold — a timer can fire a millisecond before Date.now() reaches it, which
|
|
17544
|
+
// rendered the unchanged "處理中…" text and skipped the edit until the next 60 s interval.
|
|
17545
|
+
const firstTick = setTimeout(tick, firstAt + PROGRESS_THRESHOLD_MARGIN_MS);
|
|
16933
17546
|
firstTick.unref?.();
|
|
16934
17547
|
}
|
|
16935
17548
|
}
|
|
@@ -17247,7 +17860,10 @@ export class FleetManager {
|
|
|
17247
17860
|
return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
|
|
17248
17861
|
dataDir: this.dataDir, lock: this.loginWindow,
|
|
17249
17862
|
current: owner => this.browserSignInOwnerCurrent(owner),
|
|
17250
|
-
installed: async () =>
|
|
17863
|
+
installed: async () => {
|
|
17864
|
+
const found = await Promise.all(ONBOARDING_BACKENDS.map(async (backend) => (await this.isCliInstalledOffLoop(backend)) ? backend : null));
|
|
17865
|
+
return found.filter((backend) => backend !== null);
|
|
17866
|
+
},
|
|
17251
17867
|
checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
|
|
17252
17868
|
checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
|
|
17253
17869
|
transport: () => createStructuredSignInTransport(),
|
|
@@ -17351,13 +17967,13 @@ export class FleetManager {
|
|
|
17351
17967
|
* #2233: `/login` for a dashboard requester. The same controller and checks; a CLI that is not installed is refused
|
|
17352
17968
|
* with its install command (chat's install-first runs a terminal of its own, which the dashboard does not offer).
|
|
17353
17969
|
*/
|
|
17354
|
-
startWebDashboardLogin(backendArg, chat, opts) {
|
|
17970
|
+
async startWebDashboardLogin(backendArg, chat, opts) {
|
|
17355
17971
|
const wanted = LOGIN_BACKEND_ALIASES[backendArg.toLowerCase()] ?? backendArg.toLowerCase();
|
|
17356
17972
|
if (isRemovedBackend(wanted))
|
|
17357
|
-
return
|
|
17973
|
+
return removedBackendMessage(wanted);
|
|
17358
17974
|
const info = BACKEND_INSTALLATION_INFO[wanted];
|
|
17359
|
-
if (info && LOGIN_FLOWS[wanted]?.remoteLogin !== "unsupported" && !this.
|
|
17360
|
-
return
|
|
17975
|
+
if (info && LOGIN_FLOWS[wanted]?.remoteLogin !== "unsupported" && !(await this.isCliInstalledOffLoop(wanted))) {
|
|
17976
|
+
return t("login.web_dashboard_not_installed", wanted, info.install);
|
|
17361
17977
|
}
|
|
17362
17978
|
if (!opts.skipAuthCheck)
|
|
17363
17979
|
this.recordLoginFlow(wanted, "login", chat.userId);
|
|
@@ -17405,8 +18021,10 @@ export class FleetManager {
|
|
|
17405
18021
|
? removedBackendMessage(backend)
|
|
17406
18022
|
: !installed.has(backend)
|
|
17407
18023
|
? (BACKEND_INSTALLATION_INFO[backend] ? t("login.install_by_name", backend) : t("login.install_on_host", backend))
|
|
18024
|
+
// #2981: one short line in the bare /login list — the full refusal (why, and what to run) is `/login <it>`'s;
|
|
18025
|
+
// a bare /login used to post Antigravity's whole paragraph to someone who never asked about it.
|
|
17408
18026
|
: flow?.remoteLogin === "unsupported"
|
|
17409
|
-
? t("login.
|
|
18027
|
+
? t("login.host_only", flow.command)
|
|
17410
18028
|
: backend === "opencode" ? t("login.unsupported", backend) : t("login.no_remote_flow", backend)}`).join("\n");
|
|
17411
18029
|
await chat.adapter.sendText(chat.chatId, guidance, { threadId: chat.threadId })
|
|
17412
18030
|
.catch(err => this.logger.warn({ err }, "Failed to post unsupported login guidance"));
|
|
@@ -17585,7 +18203,8 @@ export class FleetManager {
|
|
|
17585
18203
|
return admitted.refusal;
|
|
17586
18204
|
opts = { ...opts, profile: admitted.profile };
|
|
17587
18205
|
}
|
|
17588
|
-
|
|
18206
|
+
// M-2281: asked off the fleet loop — this runs on a chat click.
|
|
18207
|
+
if (BACKEND_INSTALLATION_INFO[wanted] && !(await this.isCliInstalledOffLoop(wanted))) {
|
|
17589
18208
|
const flow = LOGIN_FLOWS[wanted];
|
|
17590
18209
|
this.recordLoginFlow(wanted, flow && flow.remoteLogin !== "unsupported" ? "install_then_login" : "install_only", chat.userId);
|
|
17591
18210
|
return opts.profile !== undefined
|
|
@@ -17810,6 +18429,8 @@ export class FleetManager {
|
|
|
17810
18429
|
// installer, judge by exit code, verify the binary on a fresh login shell,
|
|
17811
18430
|
// then sign in (the hand-off below).
|
|
17812
18431
|
activeInstall = null;
|
|
18432
|
+
/** M-2281: how long `/login`'s install window may run (a field so a smoke can shorten it). */
|
|
18433
|
+
installTimeoutMs = INSTALL_TIMEOUT_MS;
|
|
17813
18434
|
/** An install that succeeded, between its success line and the sign-in it hands over to (#1131). */
|
|
17814
18435
|
installHandoff = null;
|
|
17815
18436
|
/** Start a CLI install session. Caller enforces admin. */
|
|
@@ -17818,11 +18439,13 @@ export class FleetManager {
|
|
|
17818
18439
|
const info = BACKEND_INSTALLATION_INFO[backend];
|
|
17819
18440
|
if (!info)
|
|
17820
18441
|
return t("install.unsupported", backendArg);
|
|
17821
|
-
if (checkBinaryInstalled(info.binary))
|
|
17822
|
-
return t("install.already", backend, info.binary);
|
|
17823
18442
|
// Reserve the fleet-wide window before the first await (shared with web
|
|
17824
18443
|
// login). Owned by this method until the session is published.
|
|
17825
18444
|
const claim = this.loginWindow.tryClaim("install", backend);
|
|
18445
|
+
if (claim && await checkBinaryInstalledAsync(info.binary)) {
|
|
18446
|
+
this.loginWindow.release(claim);
|
|
18447
|
+
return t("install.already", backend, info.binary);
|
|
18448
|
+
}
|
|
17826
18449
|
if (!claim)
|
|
17827
18450
|
return this.loginWindow.busyMessage();
|
|
17828
18451
|
let tmux;
|
|
@@ -17842,16 +18465,44 @@ export class FleetManager {
|
|
|
17842
18465
|
// A synthetic flow: success is decided by the installer's exit code
|
|
17843
18466
|
// (LoginSession treats a clean exit as success), never by pane text. The
|
|
17844
18467
|
// session only judges that verdict; it does not read the output for URLs or prompts.
|
|
18468
|
+
// M-2281 (#2980): no terminal to ask on, low priority, stoppable as a whole, the user's own $SHELL — see
|
|
18469
|
+
// unattended-install.ts. A prompt nobody can answer fails at once instead of waiting out the timeout.
|
|
18470
|
+
const shell = (await loginShell()) ?? null;
|
|
18471
|
+
if (!this.loginWindow.isCurrent(claim)) {
|
|
18472
|
+
this.loginWindow.release(claim);
|
|
18473
|
+
return t("login.web_shutting_down");
|
|
18474
|
+
}
|
|
17845
18475
|
const flow = {
|
|
17846
18476
|
backend,
|
|
17847
|
-
command: info.unattended ?? info.install,
|
|
18477
|
+
command: unattendedInstallCommand(info.unattended ?? info.install, shell),
|
|
17848
18478
|
successPattern: /$^/,
|
|
17849
|
-
timeoutMs:
|
|
18479
|
+
timeoutMs: this.installTimeoutMs,
|
|
17850
18480
|
};
|
|
18481
|
+
// M-2281: the installer's output, live, in the chat that asked — no page shows this window.
|
|
18482
|
+
// Telegram gets its own <pre> (its markdown conversion keeps a fence as literal backticks); others a fenced block.
|
|
18483
|
+
const frameStyle = chat.adapter.type === "telegram" ? "html" : "markdown";
|
|
18484
|
+
const stream = new InstallStream({
|
|
18485
|
+
send: text => chat.adapter.sendText(chat.chatId, text, { threadId: chat.threadId, allowedMentions: { parse: [] } }),
|
|
18486
|
+
edit: (messageId, text) => chat.adapter.editMessage(chat.chatId, messageId, text, chat.threadId, { format: frameStyle }),
|
|
18487
|
+
warn: (message, err) => this.logger.debug({ backend, err: err?.message ?? String(err) }, message),
|
|
18488
|
+
}, t("install.stream_running", backend), undefined, undefined, frameStyle);
|
|
18489
|
+
const minutes = String(Math.max(1, Math.round(this.installTimeoutMs / 60_000)));
|
|
17851
18490
|
const session = new LoginSession(flow, tmux, {
|
|
18491
|
+
onProgress: pane => stream.update(pane),
|
|
17852
18492
|
onDone: async ({ ok, detail, cleanupFailed }) => {
|
|
17853
18493
|
this.activeInstall = null;
|
|
17854
18494
|
this.loginWindow.release(claim);
|
|
18495
|
+
// Between the install ending and the sign-in starting there is no
|
|
18496
|
+
// session to cancel; this hand-off is what `/login cancel` stops — armed
|
|
18497
|
+
// before the first await here, so a cancel during any of them counts.
|
|
18498
|
+
const handoff = ok ? { backend, cancelled: false } : null;
|
|
18499
|
+
if (handoff)
|
|
18500
|
+
this.installHandoff = handoff;
|
|
18501
|
+
// The live view's last frame first, so the result line below follows the output it is about.
|
|
18502
|
+
await stream.finish(ok ? t("install.stream_ok", backend)
|
|
18503
|
+
: detail === "timeout" ? t("install.stream_timeout", backend, minutes)
|
|
18504
|
+
: detail === "cancelled" ? t("install.stream_cancelled", backend)
|
|
18505
|
+
: t("install.stream_failed", backend)).catch(() => { });
|
|
17855
18506
|
if (cleanupFailed) {
|
|
17856
18507
|
await chat.adapter.sendText(chat.chatId, t("login.web_cleanup_failed", backend), { threadId: chat.threadId }).catch(() => { });
|
|
17857
18508
|
}
|
|
@@ -17859,18 +18510,17 @@ export class FleetManager {
|
|
|
17859
18510
|
// A cancel is user-initiated — the cancel command's own reply already
|
|
17860
18511
|
// said so; a second message here would be a duplicate.
|
|
17861
18512
|
if (detail !== "cancelled") {
|
|
17862
|
-
await chat.adapter.sendText(chat.chatId, t("install.failed", backend, detail), { threadId: chat.threadId }).catch(() => { });
|
|
18513
|
+
await chat.adapter.sendText(chat.chatId, detail === "timeout" ? t("install.timeout", backend, minutes) : t("install.failed", backend, detail), { threadId: chat.threadId }).catch(() => { });
|
|
17863
18514
|
}
|
|
17864
18515
|
return;
|
|
17865
18516
|
}
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
// here on, as the login-shell lookup below is an await.
|
|
17869
|
-
const handoff = { backend, cancelled: false };
|
|
17870
|
-
this.installHandoff = handoff;
|
|
18517
|
+
if (!handoff || handoff.cancelled || this.installHandoff !== handoff)
|
|
18518
|
+
return;
|
|
17871
18519
|
// The installer may only have added the binary to a profile PATH; a
|
|
17872
18520
|
// fresh login shell sees that, the fleet process's PATH may not.
|
|
17873
|
-
|
|
18521
|
+
// #3009 (leader, cid-3017-3029-overlap): both lookups off the fleet's main thread — the login shell's own check,
|
|
18522
|
+
// then the installer's own directories through backend-availability's check (a killable child tests each one).
|
|
18523
|
+
const installedAt = (await this.locateBinaryOnLoginShell(info.binary)) ?? (await this.locateInstalledBackend(backend));
|
|
17874
18524
|
if (handoff.cancelled || this.installHandoff !== handoff)
|
|
17875
18525
|
return;
|
|
17876
18526
|
if (!installedAt) {
|
|
@@ -17940,6 +18590,11 @@ export class FleetManager {
|
|
|
17940
18590
|
const info = BACKEND_INSTALLATION_INFO[backend];
|
|
17941
18591
|
return !!info && checkBinaryInstalled(info.binary);
|
|
17942
18592
|
}
|
|
18593
|
+
/** M-2281: isCliInstalled without holding the fleet loop (a chat click's path: `which` on a slow PATH can take seconds). */
|
|
18594
|
+
async isCliInstalledOffLoop(backend) {
|
|
18595
|
+
const info = BACKEND_INSTALLATION_INFO[backend];
|
|
18596
|
+
return !!info && await checkBinaryInstalledAsync(info.binary);
|
|
18597
|
+
}
|
|
17943
18598
|
/** Abort the active install (`/login cancel`) and remove its window. */
|
|
17944
18599
|
async cancelInstallSession() {
|
|
17945
18600
|
if (!this.activeInstall)
|
|
@@ -17957,38 +18612,27 @@ export class FleetManager {
|
|
|
17957
18612
|
*/
|
|
17958
18613
|
async locateBinaryOnLoginShell(binary) {
|
|
17959
18614
|
try {
|
|
17960
|
-
|
|
18615
|
+
// #3009: the executable-file check runs inside the same login shell (a killable child), never as accessSync/statSync
|
|
18616
|
+
// on the fleet's thread — a path on a filesystem that stopped answering held the whole fleet there.
|
|
18617
|
+
const result = await runOnLoginShell(`p=$(command -v ${binary}) && [ -f "$p" ] && [ -x "$p" ] && printf '%s\n' "$p"`);
|
|
17961
18618
|
if (result.status !== 0)
|
|
17962
18619
|
return null;
|
|
17963
18620
|
const path = result.stdout.trim().split("\n").pop()?.trim() ?? "";
|
|
17964
|
-
|
|
17965
|
-
return null;
|
|
17966
|
-
accessSync(path, fsConstants.X_OK);
|
|
17967
|
-
return statSync(path).isFile() ? path : null;
|
|
18621
|
+
return isAbsolute(path) ? path : null;
|
|
17968
18622
|
}
|
|
17969
18623
|
catch {
|
|
17970
18624
|
return null;
|
|
17971
18625
|
}
|
|
17972
18626
|
}
|
|
17973
18627
|
/**
|
|
17974
|
-
* #1092: the binary in one of the directories its own installer installs to,
|
|
17975
|
-
*
|
|
17976
|
-
*
|
|
17977
|
-
*
|
|
18628
|
+
* #1092: the binary in one of the directories its own installer installs to, when no login shell can see it (the
|
|
18629
|
+
* installer recorded its PATH in an interactive-only rc file) — only those directories (#1092). #3009: through
|
|
18630
|
+
* backend-availability's install-directory check (the one resolveBackendBinary uses): one killable child, never
|
|
18631
|
+
* accessSync/statSync on the fleet's thread. An executable file counts, a symlink to one too (codex's launcher is one).
|
|
17978
18632
|
*/
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
continue;
|
|
17983
|
-
const path = join(dir, info.binary);
|
|
17984
|
-
try {
|
|
17985
|
-
accessSync(path, fsConstants.X_OK);
|
|
17986
|
-
if (statSync(path).isFile())
|
|
17987
|
-
return path;
|
|
17988
|
-
}
|
|
17989
|
-
catch { /* not there */ }
|
|
17990
|
-
}
|
|
17991
|
-
return null;
|
|
18633
|
+
async locateInstalledBackend(backend) {
|
|
18634
|
+
const info = BACKEND_INSTALLATION_INFO[backend];
|
|
18635
|
+
return info ? (await findExecutableIn(info.binary, info.binDirs?.(process.env, homedir()) ?? [])) ?? null : null;
|
|
17992
18636
|
}
|
|
17993
18637
|
/** Put an installed binary's directory on the fleet's PATH (#1059), once. */
|
|
17994
18638
|
adoptBinaryDirectory(binaryPath, backend) {
|
|
@@ -18304,20 +18948,26 @@ export class FleetManager {
|
|
|
18304
18948
|
if (claimed === "consumed")
|
|
18305
18949
|
return true;
|
|
18306
18950
|
const { entry: pending, action } = claimed;
|
|
18951
|
+
// #1490 row 27: the lifecycle the button was offered for — a replacement since (removal does not clear prompts) is stale.
|
|
18952
|
+
const current = pending.lifecycleCurrent ?? this.lifecyclePin(pending.instanceName);
|
|
18307
18953
|
this.eventLog?.insert(pending.instanceName, "hang_action", { action, userId: data.userId });
|
|
18308
18954
|
if (action === "wait") {
|
|
18309
18955
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("hang.waiting", pending.instanceName));
|
|
18310
18956
|
return true;
|
|
18311
18957
|
}
|
|
18958
|
+
if (!current()) {
|
|
18959
|
+
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("restart.superseded", this.shownInstanceName(pending.instanceName)));
|
|
18960
|
+
return true;
|
|
18961
|
+
}
|
|
18312
18962
|
await this.retireNonceButtons(pending, pending.messageId ?? data.messageId, t("hang.restarting", pending.instanceName));
|
|
18313
18963
|
try {
|
|
18314
|
-
await this.restartSingleInstance(pending.instanceName, { explicit: true });
|
|
18964
|
+
await this.restartSingleInstance(pending.instanceName, { explicit: true, ownerCurrent: current });
|
|
18315
18965
|
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("hang.restarted", pending.instanceName), pending.threadId);
|
|
18316
18966
|
}
|
|
18317
18967
|
catch (err) {
|
|
18318
18968
|
const message = err instanceof Error ? err.message : String(err);
|
|
18319
18969
|
this.logger.error({ err, instanceName: pending.instanceName }, "Hang force-restart failed");
|
|
18320
|
-
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("hang.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show hang restart error"));
|
|
18970
|
+
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, err instanceof RestartSupersededError ? message : t("hang.restart_failed", pending.instanceName, message), pending.threadId).catch(editErr => this.logger.warn({ err: editErr, instanceName: pending.instanceName }, "Failed to show hang restart error"));
|
|
18321
18971
|
}
|
|
18322
18972
|
return true;
|
|
18323
18973
|
}
|
|
@@ -18393,6 +19043,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18393
19043
|
resolveKnowledgeWorkDir(workDir, backend, instanceName) {
|
|
18394
19044
|
// Backend resolution may create/normalize the real cwd. Instructions and
|
|
18395
19045
|
// skills must land where the CLI actually runs, not an assumed path.
|
|
19046
|
+
// Caller pins resolveBackendBinary first so createBackend does not `which`.
|
|
18396
19047
|
try {
|
|
18397
19048
|
const resolved = createBackend(backend, join(getAgendHome(), "cli-env"))
|
|
18398
19049
|
.resolveWorkingDirectory?.(workDir, instanceName);
|
|
@@ -18771,6 +19422,45 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18771
19422
|
await adapter.sendText(msg.chatId, t("cmd.not_a_command"), { threadId: msg.threadId });
|
|
18772
19423
|
return true;
|
|
18773
19424
|
}
|
|
19425
|
+
/** #2940: this fleet's own status bubbles and the messages the platform said were deleted (ids, never text). */
|
|
19426
|
+
ownStatusMessages = new OwnStatusMessages();
|
|
19427
|
+
/**
|
|
19428
|
+
* #2940: a status bubble of ours was posted: never logged, and a line already logged for it (by a sibling bot) goes.
|
|
19429
|
+
* Discord only (#2943 review: the only platform where our bots see each other's posts), keyed by channel and id.
|
|
19430
|
+
*/
|
|
19431
|
+
noteOwnStatusMessage(adapter, sent) {
|
|
19432
|
+
if (adapter.type !== "discord")
|
|
19433
|
+
return;
|
|
19434
|
+
const key = discordMessageKey(sent.threadId ?? sent.chatId, sent.messageId);
|
|
19435
|
+
if (!key)
|
|
19436
|
+
return;
|
|
19437
|
+
this.ownStatusMessages.addStatus(key);
|
|
19438
|
+
const removed = ClassicChannelManager.loggedLines.remove(key);
|
|
19439
|
+
if (removed)
|
|
19440
|
+
this.logger.debug({ key, removed }, "A status bubble logged before its id was known was taken out of the chat log");
|
|
19441
|
+
}
|
|
19442
|
+
/** #2940: Discord says a message was deleted — its chat-log line goes, and it is never logged after this. */
|
|
19443
|
+
onPlatformMessageDeleted(e) {
|
|
19444
|
+
const key = discordMessageKey(e?.channelId, e?.messageId);
|
|
19445
|
+
if (!key)
|
|
19446
|
+
return;
|
|
19447
|
+
this.ownStatusMessages.markDeleted(key);
|
|
19448
|
+
const removed = ClassicChannelManager.loggedLines.remove(key);
|
|
19449
|
+
if (removed)
|
|
19450
|
+
this.logger.debug({ key, removed }, "A deleted message was taken out of the chat log");
|
|
19451
|
+
}
|
|
19452
|
+
/** #2940: the key a Discord message's chat-log line is kept under (none for another platform: nothing is tracked). */
|
|
19453
|
+
chatLogKey(msg) {
|
|
19454
|
+
return msg.source === "discord" ? discordMessageKey(msg.threadId ?? msg.chatId, msg.messageId) : undefined;
|
|
19455
|
+
}
|
|
19456
|
+
/** #2940: whether `msg` must not enter the chat log (only its key is looked at; the debug trail names keys, not text). */
|
|
19457
|
+
chatLogExcludes(instanceName, msg) {
|
|
19458
|
+
const key = this.chatLogKey(msg);
|
|
19459
|
+
if (!this.ownStatusMessages.excluded(key))
|
|
19460
|
+
return false;
|
|
19461
|
+
this.logger.debug({ instanceName, key }, "Not logged: a status bubble of this fleet, or a deleted message");
|
|
19462
|
+
return true;
|
|
19463
|
+
}
|
|
18774
19464
|
/** Handle a message in a classic channel: log it, forward only /chat messages */
|
|
18775
19465
|
async handleClassicChannelMessage(instanceName, msg) {
|
|
18776
19466
|
const text = msg.text ?? "";
|
|
@@ -18814,7 +19504,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18814
19504
|
: (msg.attachments?.length
|
|
18815
19505
|
? ` [${msg.attachments.map(a => `${a.kind === "photo" ? "📷" : "📎"} ${a.filename || a.kind}`).join(", ")}]`
|
|
18816
19506
|
: "");
|
|
18817
|
-
|
|
19507
|
+
// #2940: one of this fleet's own status bubbles (any bot's), or a message already deleted, never enters the log.
|
|
19508
|
+
if (this.chatLogExcludes(instanceName, msg))
|
|
19509
|
+
return;
|
|
19510
|
+
ClassicChannelManager.logMessage(instanceName, msg.username, text + collabAttachTag, msg.timestamp, msg.replyToText, this.chatLogKey(msg));
|
|
18818
19511
|
this.logger.info({ instanceName, user: msg.username, textLen: text.length, attachments: msg.attachments?.length ?? 0, source: msg.source }, "Collab mode message");
|
|
18819
19512
|
// Check for @mention trigger: must be exact <@BOT_USER_ID>, not @everyone/@here.
|
|
18820
19513
|
// Each bot matches ONLY its own id. A secondary bot must NOT fall back to the
|
|
@@ -18915,7 +19608,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18915
19608
|
const attachmentTag = saved ? ` [${saved.kind === "photo" ? "📷" : "📎"} saved: ${saved.paths.join(", ")}]`
|
|
18916
19609
|
: msg.attachments?.length ? ` [${msg.attachments.map(a => `📎 ${a.kind}${a.filename ? `: ${a.filename}` : ""}`).join(", ")}]`
|
|
18917
19610
|
: "";
|
|
18918
|
-
|
|
19611
|
+
// #2940: likewise in a room without collab (a bot's message reaches here only through a /chat text command).
|
|
19612
|
+
if (!this.chatLogExcludes(instanceName, msg))
|
|
19613
|
+
ClassicChannelManager.logMessage(instanceName, msg.username, text + attachmentTag, msg.timestamp, msg.replyToText, this.chatLogKey(msg));
|
|
18919
19614
|
// Bare attachment without /chat: save + log only, don't trigger agent
|
|
18920
19615
|
if (!isChat) {
|
|
18921
19616
|
const reactAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
@@ -19671,7 +20366,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19671
20366
|
}));
|
|
19672
20367
|
const timer = setTimeout(() => this.pendingEffortSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19673
20368
|
timer.unref?.();
|
|
19674
|
-
this.pendingEffortSelects.set(nonce, { instanceName: name, userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond });
|
|
20369
|
+
this.pendingEffortSelects.set(nonce, { instanceName: name, instanceCurrent: this.lifecyclePin(name), userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond });
|
|
19675
20370
|
try {
|
|
19676
20371
|
await data.respondChoices(t("effort.menu", this.effortMenuHeader(name)), choices);
|
|
19677
20372
|
}
|
|
@@ -19706,7 +20401,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19706
20401
|
}
|
|
19707
20402
|
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19708
20403
|
timer.unref?.();
|
|
19709
|
-
this.pendingEffortSelects.set(nonce, { instanceName, userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20404
|
+
this.pendingEffortSelects.set(nonce, { instanceName, instanceCurrent: this.lifecyclePin(instanceName), userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
19710
20405
|
try {
|
|
19711
20406
|
const menuMessageId = await adapter.promptUser(chatId, t("effort.menu", this.effortMenuHeader(instanceName)), choices, { threadId });
|
|
19712
20407
|
const pending = this.pendingEffortSelects.get(nonce);
|
|
@@ -19731,6 +20426,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19731
20426
|
const pending = this.pendingEffortSelects.get(match[1]);
|
|
19732
20427
|
if (!pending)
|
|
19733
20428
|
return true;
|
|
20429
|
+
// #1490 row 27: the menu was for one lifecycle of the instance; stopped, restarted or replaced since, it is stale.
|
|
20430
|
+
if (!pending.instanceCurrent()) {
|
|
20431
|
+
this.pendingEffortSelects.delete(match[1]);
|
|
20432
|
+
clearTimeout(pending.timer);
|
|
20433
|
+
data.ack?.(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName)));
|
|
20434
|
+
await pending.respond(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))).catch(() => { });
|
|
20435
|
+
return true;
|
|
20436
|
+
}
|
|
19734
20437
|
// The admin who opened the menu, through the adapter that posted it, in the same channel — and still an admin
|
|
19735
20438
|
// when they click (#754 audit): the menu lives for a minute, and a click carries its own callback data.
|
|
19736
20439
|
const cbChannel = data.threadId ?? data.chatId;
|
|
@@ -19773,17 +20476,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19773
20476
|
let result;
|
|
19774
20477
|
try {
|
|
19775
20478
|
// Asked again after the progress edit awaited above: the instance may have moved, or the admin lost admin.
|
|
19776
|
-
result =
|
|
19777
|
-
|
|
19778
|
-
|
|
19779
|
-
if (
|
|
19780
|
-
|
|
20479
|
+
result = !pending.instanceCurrent() ? t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))
|
|
20480
|
+
: this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId)
|
|
20481
|
+
? await withSpawnHoldReply(async (text) => {
|
|
20482
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
20483
|
+
if (progressMsgId)
|
|
20484
|
+
await pending.adapter.editMessage(pending.adapterChatId, progressMsgId, text, pending.adapterThreadId);
|
|
20485
|
+
else
|
|
20486
|
+
await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId });
|
|
20487
|
+
}
|
|
19781
20488
|
else
|
|
19782
|
-
await pending.
|
|
19783
|
-
}
|
|
19784
|
-
else
|
|
19785
|
-
await pending.respond(text);
|
|
19786
|
-
}, () => this.applyEffort(pending.instanceName, level)) : t("menu.click_stale");
|
|
20489
|
+
await pending.respond(text);
|
|
20490
|
+
}, () => this.applyEffort(pending.instanceName, level, { current: pending.instanceCurrent })) : t("menu.click_stale");
|
|
19787
20491
|
}
|
|
19788
20492
|
catch (err) {
|
|
19789
20493
|
this.logger.error({ err, instance: pending.instanceName, level }, "Effort switch failed");
|
|
@@ -19840,7 +20544,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19840
20544
|
const choices = this.modelMenuChoices(name, nonce, options, currentModel);
|
|
19841
20545
|
const timer = setTimeout(() => this.pendingModelSelects.delete(nonce), CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19842
20546
|
timer.unref?.();
|
|
19843
|
-
this.pendingModelSelects.set(nonce, { instanceName: name, model: "", userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond, respondChoices: data.respondChoices });
|
|
20547
|
+
this.pendingModelSelects.set(nonce, { instanceName: name, instanceCurrent: this.lifecyclePin(name), model: "", userId: data.userId, channelId: data.channelId, adapterId, timer, respond: data.respond, respondChoices: data.respondChoices });
|
|
19844
20548
|
try {
|
|
19845
20549
|
await data.respondChoices(t("model.menu", `**${currentDisplay}**`), choices);
|
|
19846
20550
|
}
|
|
@@ -19876,7 +20580,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
19876
20580
|
}
|
|
19877
20581
|
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
19878
20582
|
timer.unref?.();
|
|
19879
|
-
this.pendingModelSelects.set(nonce, { instanceName, model: "", userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
20583
|
+
this.pendingModelSelects.set(nonce, { instanceName, instanceCurrent: this.lifecyclePin(instanceName), model: "", userId, channelId, adapterId, timer, respond, adapter, adapterChatId: chatId, adapterThreadId: threadId });
|
|
19880
20584
|
try {
|
|
19881
20585
|
const menuMessageId = await adapter.promptUser(chatId, t("model.menu", currentDisplay), choices, { threadId });
|
|
19882
20586
|
const pending = this.pendingModelSelects.get(nonce);
|
|
@@ -20049,6 +20753,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20049
20753
|
const pending = this.pendingModelSelects.get(match[1]);
|
|
20050
20754
|
if (!pending)
|
|
20051
20755
|
return true;
|
|
20756
|
+
// #1490 row 27: the menu was for one lifecycle of the instance; stopped, restarted or replaced since, it is stale.
|
|
20757
|
+
if (!pending.instanceCurrent()) {
|
|
20758
|
+
this.pendingModelSelects.delete(match[1]);
|
|
20759
|
+
clearTimeout(pending.timer);
|
|
20760
|
+
data.ack?.(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName)));
|
|
20761
|
+
await pending.respond(t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))).catch(() => { });
|
|
20762
|
+
return true;
|
|
20763
|
+
}
|
|
20052
20764
|
// The admin who opened the menu, through the adapter that posted it, in the same channel — and still an admin
|
|
20053
20765
|
// when they click (#754 audit): the menu lives for a minute, and a click carries its own callback data.
|
|
20054
20766
|
const cbChannel = data.threadId ?? data.chatId;
|
|
@@ -20109,17 +20821,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20109
20821
|
let result;
|
|
20110
20822
|
try {
|
|
20111
20823
|
// Asked again after the progress edit awaited above: the instance may have moved, or the admin lost admin.
|
|
20112
|
-
result =
|
|
20113
|
-
|
|
20114
|
-
|
|
20115
|
-
if (
|
|
20116
|
-
|
|
20824
|
+
result = !pending.instanceCurrent() ? t("menu.instance_replaced", this.shownInstanceName(pending.instanceName))
|
|
20825
|
+
: this.menuClickStillCurrent(pending.instanceName, data.userId, pending.channelId, adapterId, pending.adapterChatId)
|
|
20826
|
+
? await withSpawnHoldReply(async (text) => {
|
|
20827
|
+
if (pending.adapter && pending.adapterChatId) {
|
|
20828
|
+
if (progressMsgId)
|
|
20829
|
+
await pending.adapter.editMessage(pending.adapterChatId, progressMsgId, text, pending.adapterThreadId);
|
|
20830
|
+
else
|
|
20831
|
+
await pending.adapter.sendText(pending.adapterChatId, text, { threadId: pending.adapterThreadId });
|
|
20832
|
+
}
|
|
20117
20833
|
else
|
|
20118
|
-
await pending.
|
|
20119
|
-
}
|
|
20120
|
-
else
|
|
20121
|
-
await pending.respond(text);
|
|
20122
|
-
}, () => this.applyModel(pending.instanceName, model)) : t("menu.click_stale");
|
|
20834
|
+
await pending.respond(text);
|
|
20835
|
+
}, () => this.applyModel(pending.instanceName, model, { current: pending.instanceCurrent })) : t("menu.click_stale");
|
|
20123
20836
|
}
|
|
20124
20837
|
catch (err) {
|
|
20125
20838
|
this.logger.error({ err, instance: pending.instanceName, model }, "Model switch failed");
|
|
@@ -20212,7 +20925,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20212
20925
|
* restart backends only read it at launch, so it is persisted and the
|
|
20213
20926
|
* instance respawns.
|
|
20214
20927
|
*/
|
|
20215
|
-
async applyEffort(instanceName, requested) {
|
|
20928
|
+
async applyEffort(instanceName, requested, opts) {
|
|
20929
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first and fenced into the restart.
|
|
20930
|
+
if (opts?.current && !opts.current())
|
|
20931
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20216
20932
|
const level = requested.trim().toLowerCase();
|
|
20217
20933
|
const backendName = this.backendNameForInstance(instanceName);
|
|
20218
20934
|
let strategy = "unsupported";
|
|
@@ -20247,10 +20963,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20247
20963
|
this.pasteRawToClassicInstance(instanceName, `/effort ${applied}`);
|
|
20248
20964
|
return `${warn}${t("effort.runtime_success", instanceName, applied)}`;
|
|
20249
20965
|
}
|
|
20250
|
-
await this.restartSingleInstance(instanceName);
|
|
20966
|
+
await this.restartSingleInstance(instanceName, { ownerCurrent: opts?.current });
|
|
20251
20967
|
return `${warn}${t("effort.restart_success", instanceName, applied)}`;
|
|
20252
20968
|
}
|
|
20253
|
-
async applyModel(instanceName, model) {
|
|
20969
|
+
async applyModel(instanceName, model, opts) {
|
|
20970
|
+
// #1490 row 27: a menu's choice is for the lifecycle it was offered to — asked first, after the runtime switch's
|
|
20971
|
+
// await, and fenced into the restart.
|
|
20972
|
+
if (opts?.current && !opts.current())
|
|
20973
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20254
20974
|
// Reject model names with newlines or control characters: they would be
|
|
20255
20975
|
// persisted to fleet.yaml and pasted raw into the CLI (P3 from #1490 audit).
|
|
20256
20976
|
if (/[\x00-\x1f\x7f]/.test(model)) {
|
|
@@ -20276,6 +20996,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20276
20996
|
// #1690: claude-code's runtime switch reports what it came to; the model is saved unless claude was told No.
|
|
20277
20997
|
if (strategy === "runtime" && backendName === "claude-code") {
|
|
20278
20998
|
const result = await this.runRuntimeModelSwitch(instanceName, model);
|
|
20999
|
+
if (opts?.current && !opts.current())
|
|
21000
|
+
return t("menu.instance_replaced", this.shownInstanceName(instanceName));
|
|
20279
21001
|
if (result.outcome === "mismatch")
|
|
20280
21002
|
return `${warn}${t("model.switch_mismatch", instanceName, result.shown, model)}`;
|
|
20281
21003
|
if (result.outcome === "not_sent")
|
|
@@ -20296,7 +21018,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20296
21018
|
this.pasteRawToClassicInstance(instanceName, `/model ${model}`);
|
|
20297
21019
|
return `${warn}${t("model.runtime_success", instanceName, model)}${this.effortSuffix(instanceName)}`;
|
|
20298
21020
|
}
|
|
20299
|
-
await this.restartSingleInstance(instanceName);
|
|
21021
|
+
await this.restartSingleInstance(instanceName, { ownerCurrent: opts?.current });
|
|
20300
21022
|
return `${warn}${t("model.restart_success", instanceName, model)}${this.effortSuffix(instanceName)}`;
|
|
20301
21023
|
}
|
|
20302
21024
|
/** Save an instance's model (fleet.yaml, or its ClassicBot channel). False when neither holds it. */
|
|
@@ -21175,6 +21897,22 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21175
21897
|
// because we asked it to. Set synchronously — doStopAll runs to its first
|
|
21176
21898
|
// await in the same tick as the signal handler, so no event can slip in.
|
|
21177
21899
|
this.shuttingDown = true;
|
|
21900
|
+
// Stop receive admission before the first await or any instance shutdown; outbound
|
|
21901
|
+
// adapters remain available below to retire notices and pending prompts.
|
|
21902
|
+
for (const state of this.adapterState.values()) {
|
|
21903
|
+
if (state.retryTimer) {
|
|
21904
|
+
clearTimeout(state.retryTimer);
|
|
21905
|
+
state.retryTimer = undefined;
|
|
21906
|
+
}
|
|
21907
|
+
}
|
|
21908
|
+
const inboundStopped = Promise.all([...this.adapters.values()].map(async (adapter) => {
|
|
21909
|
+
try {
|
|
21910
|
+
await adapter.stopReceiving?.();
|
|
21911
|
+
}
|
|
21912
|
+
catch (err) {
|
|
21913
|
+
this.logger.warn({ adapterId: adapter.id, err }, "Adapter receive stop failed; inbound remains fenced");
|
|
21914
|
+
}
|
|
21915
|
+
}));
|
|
21178
21916
|
this.workingRecovery?.reset();
|
|
21179
21917
|
if (this.workingRecoveryTimer)
|
|
21180
21918
|
clearInterval(this.workingRecoveryTimer);
|
|
@@ -21229,9 +21967,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21229
21967
|
}
|
|
21230
21968
|
this.eventLoopWatch?.stop();
|
|
21231
21969
|
this.eventLoopWatch = null;
|
|
21970
|
+
this.stallReporter?.stop();
|
|
21971
|
+
this.stallReporter = null;
|
|
21232
21972
|
// A login/install window is a dedicated tmux server with its own TTL
|
|
21233
21973
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
21234
21974
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
21975
|
+
await inboundStopped;
|
|
21235
21976
|
await publicStopped;
|
|
21236
21977
|
await sharesStopped?.catch(() => { });
|
|
21237
21978
|
await profileStopped;
|
|
@@ -21242,13 +21983,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21242
21983
|
await this.cpuProfileControl?.close();
|
|
21243
21984
|
this.cpuProfileControl = null;
|
|
21244
21985
|
await this.shutdownLoginWindows();
|
|
21245
|
-
// Cancel adapter retry timers
|
|
21246
|
-
for (const state of this.adapterState.values()) {
|
|
21247
|
-
if (state.retryTimer) {
|
|
21248
|
-
clearTimeout(state.retryTimer);
|
|
21249
|
-
state.retryTimer = undefined;
|
|
21250
|
-
}
|
|
21251
|
-
}
|
|
21252
21986
|
this.clearStatuslineWatchers();
|
|
21253
21987
|
this.costGuard?.stop();
|
|
21254
21988
|
this.dailySummary?.stop();
|
|
@@ -21286,6 +22020,15 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21286
22020
|
clearInterval(this.discordPresenceTimer);
|
|
21287
22021
|
this.discordPresenceTimer = null;
|
|
21288
22022
|
}
|
|
22023
|
+
if (this.quotaReadTimer) {
|
|
22024
|
+
clearInterval(this.quotaReadTimer);
|
|
22025
|
+
this.quotaReadTimer = null;
|
|
22026
|
+
}
|
|
22027
|
+
if (this.capacityTimer) {
|
|
22028
|
+
clearInterval(this.capacityTimer);
|
|
22029
|
+
this.capacityTimer = null;
|
|
22030
|
+
}
|
|
22031
|
+
setCapacityLookup(null);
|
|
21289
22032
|
if (this.discordPresenceEagerTimer) {
|
|
21290
22033
|
clearTimeout(this.discordPresenceEagerTimer);
|
|
21291
22034
|
this.discordPresenceEagerTimer = null;
|
|
@@ -21590,6 +22333,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21590
22333
|
// mark every row done and tell the user "changes applied".
|
|
21591
22334
|
return { rejected: why };
|
|
21592
22335
|
}
|
|
22336
|
+
this.agentCredentialsCache?.observePolicies();
|
|
21593
22337
|
if (this.loadedFleetSource)
|
|
21594
22338
|
this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
|
|
21595
22339
|
this.migrateOfficeStorage();
|
|
@@ -21624,7 +22368,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
21624
22368
|
this.reconcilePreviewListener();
|
|
21625
22369
|
this.publicWebLink?.refresh();
|
|
21626
22370
|
const newInstances = this.fleetConfig.instances;
|
|
21627
|
-
const topicMode = this.fleetConfig
|
|
22371
|
+
const topicMode = daemonTopicMode(this.fleetConfig);
|
|
21628
22372
|
// Only what a fresh process can adopt, and only relative to the signature
|
|
21629
22373
|
// this process came up on: a Settings edit mutates this.fleetConfig in place
|
|
21630
22374
|
// before the reload, so comparing the pre-load copy sees nothing at all.
|
|
@@ -22490,6 +23234,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22490
23234
|
}
|
|
22491
23235
|
async runConnectionBindingApply(job, binding, execution) {
|
|
22492
23236
|
try {
|
|
23237
|
+
// #3055 review r2: a connection is not re-pointed while one of its topics is being deleted (nothing is changed).
|
|
23238
|
+
if (this.topicDeletionPending())
|
|
23239
|
+
throw new Error("a topic deletion is running; try again in a moment");
|
|
22493
23240
|
await this.rebuildAdapterForBinding(job.connectionId, binding, execution);
|
|
22494
23241
|
job.result = "applied";
|
|
22495
23242
|
}
|
|
@@ -23203,7 +23950,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
23203
23950
|
}
|
|
23204
23951
|
const fleet = this.loadConfig(this.configPath);
|
|
23205
23952
|
this.fleetConfig = fleet;
|
|
23206
|
-
const topicMode = fleet
|
|
23953
|
+
const topicMode = daemonTopicMode(fleet);
|
|
23207
23954
|
const restartProgress = new RestartProgress(this.runnableStartupCount(fleet, topicMode), restartStartedAt, this.logger);
|
|
23208
23955
|
// Phase 1: generals first
|
|
23209
23956
|
const restartEntries = Object.entries(fleet.instances);
|
|
@@ -23355,7 +24102,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
23355
24102
|
// Only notify when target is genuinely newer (semver), so a beta user on
|
|
23356
24103
|
// 2.0.8-beta.16 is never told that stable 2.0.7 is "available".
|
|
23357
24104
|
if (target && this.semverGt(target, currentVersion)) {
|
|
23358
|
-
|
|
24105
|
+
// #2910 D2 (#2925 review): a web-only fleet first — even with a General instance it has no chat for /update.
|
|
24106
|
+
const generalId = this.webOnlyPromptPlace() ? null : this.findGeneralInstance();
|
|
23359
24107
|
if (generalId) {
|
|
23360
24108
|
// No release URL — Discord's SuppressEmbeds proved unreliable and the
|
|
23361
24109
|
// link preview looked bad. Version + /update instruction is enough.
|
|
@@ -23366,6 +24114,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
23366
24114
|
if (key)
|
|
23367
24115
|
this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
|
|
23368
24116
|
}
|
|
24117
|
+
else if (this.webOnlyPromptPlace() && (fromChannel || updateNoticeKey(currentVersion, target))) {
|
|
24118
|
+
// #2910 D2: a web-only fleet: a Notice, once per version (the check runs daily); installed from this computer.
|
|
24119
|
+
const text = t("update.available_web_only", `v${target}`, `v${currentVersion}`);
|
|
24120
|
+
if (this.webNoticeStore()?.latest("update")?.text !== text)
|
|
24121
|
+
this.recordWebNotice("update", text);
|
|
24122
|
+
}
|
|
23369
24123
|
}
|
|
23370
24124
|
}
|
|
23371
24125
|
catch { /* silent — network issues */ }
|
|
@@ -23679,7 +24433,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
23679
24433
|
if (decision.reason === "no-credential" && req.method === "GET"
|
|
23680
24434
|
&& String(req.headers.accept ?? "").includes("text/html")
|
|
23681
24435
|
&& isWebPageNavigation(requestPath)) {
|
|
23682
|
-
serveSigninPage(res, 401);
|
|
24436
|
+
serveSigninPage(res, 401, { webOnly: this.isWebOnlyFleet() });
|
|
23683
24437
|
return;
|
|
23684
24438
|
}
|
|
23685
24439
|
res.writeHead(decision.status);
|
|
@@ -23902,7 +24656,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
23902
24656
|
(async () => {
|
|
23903
24657
|
let held = false;
|
|
23904
24658
|
try {
|
|
23905
|
-
const topicMode = this.fleetConfig
|
|
24659
|
+
const topicMode = daemonTopicMode(this.fleetConfig);
|
|
23906
24660
|
await withSpawnHoldReply(async (message) => {
|
|
23907
24661
|
held = true;
|
|
23908
24662
|
res.writeHead(202, { "Content-Type": "application/json" });
|
|
@@ -24137,7 +24891,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24137
24891
|
}
|
|
24138
24892
|
const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
|
|
24139
24893
|
// #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
|
|
24140
|
-
extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; }
|
|
24894
|
+
extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; },
|
|
24895
|
+
// #2911 (7): CDN libraries in previews, as fleet.yaml says now (a change applies to the next preview started).
|
|
24896
|
+
cdn: () => this.fleetConfig?.web?.preview_cdn === true });
|
|
24141
24897
|
this.previewListener = listener;
|
|
24142
24898
|
listener.server.on("error", (err) => {
|
|
24143
24899
|
if (this.previewListener !== listener)
|
|
@@ -24357,6 +25113,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
24357
25113
|
// #1685: host memory pressure while it slows or holds new agents (the dashboard's indicator; macOS elevated is
|
|
24358
25114
|
// shown here rather than in chat). null when normal, unknown or advisory.
|
|
24359
25115
|
host_memory: this.hostMemoryIndicator(),
|
|
25116
|
+
// #2910 D2: a web-only fleet's newest Notice — a polling page (no SSE `notice`) reads the list again when it moves.
|
|
25117
|
+
...(this.webOnlyPromptPlace() ? { notices_newest: this.webNoticeStore()?.newest() ?? 0 } : {}),
|
|
24360
25118
|
// #2015: the largest processes while it lasts (name, pid, RSS, and whose: an instance, the fleet or tmux).
|
|
24361
25119
|
...(this.hostMemoryIndicator() && this.memoryTop ? { host_memory_top: this.memoryTop.processes } : {}),
|
|
24362
25120
|
};
|