@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-cli.js +69 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +11 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +3 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/antigravity-usage-scan.d.ts +58 -0
- package/dist/antigravity-usage-scan.js +257 -0
- package/dist/antigravity-usage-scan.js.map +1 -0
- package/dist/antigravity-usage-worker.d.ts +1 -0
- package/dist/antigravity-usage-worker.js +10 -0
- package/dist/antigravity-usage-worker.js.map +1 -0
- package/dist/auth-api.js +4 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +52 -0
- package/dist/backend/kiro-classic-archive.js +444 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +83 -0
- package/dist/backend/kiro-identity.js +144 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-migration-verify.d.ts +10 -0
- package/dist/backend/kiro-migration-verify.js +52 -0
- package/dist/backend/kiro-migration-verify.js.map +1 -0
- package/dist/backend/kiro-retired-model.d.ts +62 -0
- package/dist/backend/kiro-retired-model.js +160 -0
- package/dist/backend/kiro-retired-model.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +21 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +8 -0
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +169 -2
- package/dist/backend/kiro.js +703 -28
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +108 -0
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-signin-guide.d.ts +1 -0
- package/dist/backend-signin-guide.js +9 -0
- package/dist/backend-signin-guide.js.map +1 -0
- package/dist/backend-signin-pending.d.ts +10 -0
- package/dist/backend-signin-pending.js +44 -0
- package/dist/backend-signin-pending.js.map +1 -0
- package/dist/browser-backend-signins.d.ts +56 -0
- package/dist/browser-backend-signins.js +189 -0
- package/dist/browser-backend-signins.js.map +1 -0
- package/dist/cache-ledger.d.ts +38 -2
- package/dist/cache-ledger.js +133 -8
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-prices.d.ts +2 -1
- package/dist/cache-prices.js +5 -2
- package/dist/cache-prices.js.map +1 -1
- package/dist/cache-service.d.ts +68 -0
- package/dist/cache-service.js +355 -13
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +71 -14
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/ipc-timeouts.js +3 -0
- package/dist/channel/ipc-timeouts.js.map +1 -1
- package/dist/channel/mcp-tools.js +41 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +11 -0
- package/dist/classic-channel-manager.js +39 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +54 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +55 -10
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +33 -1
- package/dist/config.js.map +1 -1
- package/dist/cpu-quota.d.ts +23 -0
- package/dist/cpu-quota.js +86 -0
- package/dist/cpu-quota.js.map +1 -0
- package/dist/daemon.d.ts +68 -0
- package/dist/daemon.js +448 -26
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +31 -6
- package/dist/dashboard-permissions.js +84 -9
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +1 -1
- package/dist/fleet-control-audit.js +3 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-manager.d.ts +133 -2
- package/dist/fleet-manager.js +905 -94
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +24 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +49 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +49 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +47 -9
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +54 -11
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +23 -0
- package/dist/instance-lifecycle.js +62 -12
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-db-reader.d.ts +5 -0
- package/dist/kiro-db-reader.js +18 -0
- package/dist/kiro-db-reader.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +2 -0
- package/dist/kiro-engine-status.js +9 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-migrate.d.ts +60 -0
- package/dist/kiro-migrate.js +160 -0
- package/dist/kiro-migrate.js.map +1 -0
- package/dist/kiro-session-proof.d.ts +41 -0
- package/dist/kiro-session-proof.js +89 -0
- package/dist/kiro-session-proof.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +2 -0
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/kiro-transcript-worker.js +1 -0
- package/dist/kiro-transcript-worker.js.map +1 -1
- package/dist/kiro-tui-forensic.d.ts +12 -0
- package/dist/kiro-tui-forensic.js +110 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/kiro-usage-run.d.ts +7 -0
- package/dist/kiro-usage-run.js +39 -0
- package/dist/kiro-usage-run.js.map +1 -0
- package/dist/kiro-usage-scan.d.ts +49 -0
- package/dist/kiro-usage-scan.js +223 -0
- package/dist/kiro-usage-scan.js.map +1 -0
- package/dist/kiro-usage-worker.d.ts +1 -0
- package/dist/kiro-usage-worker.js +10 -0
- package/dist/kiro-usage-worker.js.map +1 -0
- package/dist/kiro-usage.d.ts +58 -0
- package/dist/kiro-usage.js +310 -0
- package/dist/kiro-usage.js.map +1 -0
- package/dist/locale.js +230 -8
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/login-url.d.ts +4 -0
- package/dist/login-url.js +22 -0
- package/dist/login-url.js.map +1 -0
- package/dist/needs-you-hub.d.ts +2 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +10 -2
- package/dist/needs-you.js +10 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/notice-delivery.d.ts +16 -0
- package/dist/notice-delivery.js +35 -0
- package/dist/notice-delivery.js.map +1 -0
- package/dist/office-share-page.d.ts +16 -0
- package/dist/office-share-page.js +85 -0
- package/dist/office-share-page.js.map +1 -0
- package/dist/office-share-service.d.ts +91 -0
- package/dist/office-share-service.js +176 -0
- package/dist/office-share-service.js.map +1 -0
- package/dist/office-share.d.ts +268 -0
- package/dist/office-share.js +574 -0
- package/dist/office-share.js.map +1 -0
- package/dist/opencode-usage-scan.d.ts +28 -0
- package/dist/opencode-usage-scan.js +92 -0
- package/dist/opencode-usage-scan.js.map +1 -0
- package/dist/opencode-usage-worker.d.ts +1 -0
- package/dist/opencode-usage-worker.js +10 -0
- package/dist/opencode-usage-worker.js.map +1 -0
- package/dist/outbound-handlers.js +22 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +65 -2
- package/dist/outbound-schemas.js +56 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +7 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.js +13 -2
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.js +2 -1
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- package/dist/remote-card-prompt.d.ts +38 -0
- package/dist/remote-card-prompt.js +86 -0
- package/dist/remote-card-prompt.js.map +1 -0
- package/dist/reply-triggers.d.ts +2 -0
- package/dist/reply-triggers.js +3 -0
- package/dist/reply-triggers.js.map +1 -0
- package/dist/settings-api.d.ts +2 -0
- package/dist/settings-api.js +41 -16
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +20 -14
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +4 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +55 -6
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.d.ts +9 -0
- package/dist/setup-host.js +127 -6
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-signin-assets.d.ts +2 -0
- package/dist/setup-signin-assets.js +6 -0
- package/dist/setup-signin-assets.js.map +1 -0
- package/dist/setup-wizard.js +2 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/structured-signin-transport.d.ts +8 -0
- package/dist/structured-signin-transport.js +79 -0
- package/dist/structured-signin-transport.js.map +1 -0
- package/dist/structured-signin.d.ts +131 -0
- package/dist/structured-signin.js +388 -0
- package/dist/structured-signin.js.map +1 -0
- package/dist/tool-permissions.js +29 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +6 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +27 -2
- package/dist/transcript-sources.js +104 -40
- package/dist/transcript-sources.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +1 -1
- package/dist/turn-reply-guard.js +1 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -8
- package/dist/ui/backend-signin.js +63 -10
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +4 -4
- package/dist/ui/fleet-office-editor.js +68 -11
- package/dist/ui/fleet-office-layout.js +110 -35
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +167 -0
- package/dist/ui/fleet-office-pack.d.ts +13 -3
- package/dist/ui/fleet-office-pack.js +214 -24
- package/dist/ui/fleet-office-skin.js +98 -23
- package/dist/ui/fleet-office-store.js +82 -5
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +232 -21
- package/dist/ui/fleet-office.js +549 -186
- package/dist/ui/fleet-usage.js +19 -15
- package/dist/ui/office-share-dialog.js +145 -0
- package/dist/ui/office-share-page.js +164 -0
- package/dist/ui/panel-chat.js +22 -3
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +5 -5
- package/dist/ui/panel-settings.js +38 -12
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +22 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +18 -2
- package/dist/ui/shared/app-i18n.js +4 -2
- package/dist/ui/shared/app-shell.js +14 -6
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +85 -4
- package/dist/ui/shared/office/cozy.png +0 -0
- package/dist/ui/shared/side-panel.js +95 -0
- package/dist/ui/structured-signin.js +115 -0
- package/dist/usage-report.d.ts +20 -0
- package/dist/usage-report.js +54 -17
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +9 -1
- package/dist/usage-rollup.js +20 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +23 -1
- package/dist/web-api.js +292 -3
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-backend-login.d.ts +1 -7
- package/dist/web-backend-login.js +2 -24
- package/dist/web-backend-login.js.map +1 -1
- package/dist/web-office-acts.d.ts +173 -0
- package/dist/web-office-acts.js +285 -0
- package/dist/web-office-acts.js.map +1 -0
- package/dist/web-office-pack-read.d.ts +35 -0
- package/dist/web-office-pack-read.js +252 -0
- package/dist/web-office-pack-read.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +232 -0
- package/dist/web-office-pack-tools.js +754 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +71 -6
- package/dist/web-office-skins.js +321 -35
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +5 -2
- package/dist/web-office-store.js +69 -10
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/working-recovery.d.ts +2 -2
- package/dist/working-recovery.js +43 -8
- package/dist/working-recovery.js.map +1 -1
- package/dist/working-state-journal.d.ts +2 -0
- package/dist/working-state-journal.js +16 -1
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +3 -2
- package/templates/windows-shortcuts.ps1 +155 -140
package/dist/fleet-manager.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
|
|
2
|
+
import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
|
|
3
|
+
import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
|
|
4
|
+
import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
5
|
+
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
6
|
+
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
1
7
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
2
8
|
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
3
9
|
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
@@ -19,6 +25,9 @@ import { PublicLinkAutoOpen } from "./public-link-auto-open.js";
|
|
|
19
25
|
import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from "./public-link-owner.js";
|
|
20
26
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
21
27
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
28
|
+
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
29
|
+
import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
|
|
30
|
+
import { ALL_OFFICES } from "./office-share.js";
|
|
22
31
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
23
32
|
import { ManagedTunnel } from "./tunnel/manager.js";
|
|
24
33
|
import { withinBudget } from "./monotonic-budget.js";
|
|
@@ -28,13 +37,16 @@ import { ProfileControlServer } from "./profile-control.js";
|
|
|
28
37
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, lstatSync, constants as fsConstants, closeSync } from "node:fs";
|
|
29
38
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
30
39
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
31
|
-
import { freemem, totalmem,
|
|
40
|
+
import { freemem, totalmem, homedir } from "node:os";
|
|
41
|
+
import { effectiveCpuCount } from "./cpu-quota.js";
|
|
32
42
|
import { createServer } from "node:http";
|
|
33
43
|
import { Worker } from "node:worker_threads";
|
|
34
44
|
import { ProbeWorkerPool } from "./probe-worker-pool.js";
|
|
35
45
|
import { join, dirname, basename, delimiter, isAbsolute, relative, resolve as resolvePath } from "node:path";
|
|
36
46
|
import { fileURLToPath } from "node:url";
|
|
47
|
+
import { isDeepStrictEqual } from "node:util";
|
|
37
48
|
import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
49
|
+
import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./kiro-migrate.js";
|
|
38
50
|
import { fleetLabel } from "./fleet-label.js";
|
|
39
51
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
40
52
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
@@ -53,7 +65,7 @@ const __dirname = dirname(__filename);
|
|
|
53
65
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
54
66
|
import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
55
67
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
56
|
-
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
68
|
+
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
57
69
|
import { EventLog } from "./event-log.js";
|
|
58
70
|
import { binaryProbe } from "./binary-probe.js";
|
|
59
71
|
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
|
|
@@ -74,7 +86,7 @@ import { createBackend } from "./backend/factory.js";
|
|
|
74
86
|
import { readEffortMetadata } from "./backend/effort-metadata.js";
|
|
75
87
|
import { codexEffort, flagEffort } from "./backend/launch-effort.js";
|
|
76
88
|
import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
|
|
77
|
-
import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError } from "./backend/types.js";
|
|
89
|
+
import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
78
90
|
import { createLogger, rotateLogIfNeeded } from "./logger.js";
|
|
79
91
|
import { processAttachments } from "./channel/attachment-handler.js";
|
|
80
92
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
@@ -160,6 +172,8 @@ import { OwnerCssStore } from "./web-owner-css-store.js";
|
|
|
160
172
|
import { ThemeStore } from "./web-theme-store.js";
|
|
161
173
|
import { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
|
|
162
174
|
import { OfficeSkinStore } from "./web-office-skins.js";
|
|
175
|
+
import { OfficeActs } from "./web-office-acts.js";
|
|
176
|
+
import { OFFICE_PACK_TOOLS, OfficePackTools } from "./web-office-pack-tools.js";
|
|
163
177
|
import { WebAppearance } from "./web-appearance.js";
|
|
164
178
|
import { WebEffectLibrary } from "./web-effect-library.js";
|
|
165
179
|
import { WebEffectTrials } from "./web-effect-trials.js";
|
|
@@ -189,7 +203,7 @@ import { discordGeneralChannelId } from "./discord-general.js";
|
|
|
189
203
|
import { parseEnvText } from "./env-file.js";
|
|
190
204
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
191
205
|
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
192
|
-
import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
206
|
+
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
193
207
|
import { LoginSession } from "./login-manager.js";
|
|
194
208
|
import { runOnLoginShell } from "./login-shell.js";
|
|
195
209
|
import { tightenInstanceDirs } from "./private-dir.js";
|
|
@@ -209,12 +223,17 @@ import { admitProfileLogin, LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOG
|
|
|
209
223
|
import { WebBackendLogins, dashboardSignInSupported } from "./web-backend-login.js";
|
|
210
224
|
import { runBeforeDeadline } from "./deadline.js";
|
|
211
225
|
import { LoginWindowLock } from "./login-window-lock.js";
|
|
226
|
+
import { BrowserBackendSignIns } from "./browser-backend-signins.js";
|
|
227
|
+
import { createStructuredSignInTransport } from "./structured-signin-transport.js";
|
|
228
|
+
import { ONBOARDING_BACKENDS, readPendingBackendSignIns } from "./backend-signin-pending.js";
|
|
229
|
+
import { structuredSignInSupported } from "./structured-signin.js";
|
|
212
230
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
213
231
|
import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
232
|
+
import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteCardPrompt, REMOTE_CARD_TEXT_MAX } from "./remote-card-prompt.js";
|
|
214
233
|
import { recentChatContext } from "./log-tail.js";
|
|
215
234
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
216
235
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
217
|
-
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
236
|
+
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
|
|
218
237
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
219
238
|
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
220
239
|
import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
|
|
@@ -272,6 +291,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
272
291
|
/** A self-restart is a whole service restart; 120s is the apply budget, not this. */
|
|
273
292
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
274
293
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
294
|
+
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
275
295
|
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
276
296
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
277
297
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
@@ -463,12 +483,27 @@ const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
|
|
|
463
483
|
const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
464
484
|
const BACKEND_SELECT_CALLBACK_PREFIX = "backend-select:";
|
|
465
485
|
const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
|
|
486
|
+
/** #2675: a card a CLI shows in place of its composer (kiro V3's Question / MCP input), posted with its answers. */
|
|
487
|
+
const REMOTE_CARD_CALLBACK_PREFIX = "card-answer:";
|
|
488
|
+
const REMOTE_CARD_CALLBACK_RE = /^card-answer:([0-9a-f]{32}):(opt-\d{1,2}|sub-all|sub-\d{1,2}|back|skip|decline|cancel|text)$/;
|
|
489
|
+
/** A posted card's buttons stay this long; the card itself waits in the pane for as long as it takes. */
|
|
490
|
+
const REMOTE_CARD_TIMEOUT_MS = 12 * 60 * 60_000;
|
|
491
|
+
/** A reply already taken as a card's answer is remembered this long, so another bot's copy of it is dropped (#2675). */
|
|
492
|
+
const REMOTE_CARD_TAKEN_TTL_MS = 10 * 60_000;
|
|
466
493
|
const EXIT_RESTART_CALLBACK_PREFIX = "exit-restart:";
|
|
467
494
|
const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
|
|
468
495
|
/** #1386: how long a paused instance's marker reason is reused before it is read again (transitions drop it at once). */
|
|
469
496
|
const NEEDS_PAUSE_TTL_MS = 10_000;
|
|
470
497
|
const HANG_CALLBACK_PREFIX = "hang:";
|
|
471
498
|
const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
499
|
+
/** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
|
|
500
|
+
const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
|
|
501
|
+
/** #2692: a fleet admin's Approve / Reject on an agent's pending skin-pack change, in General. */
|
|
502
|
+
const OFFICE_PACK_CALLBACK_PREFIX = "office-pack:";
|
|
503
|
+
/** #2692: the skin-pack tools' IPC types (answered as web theme tools). */
|
|
504
|
+
const OFFICE_PACK_IPC = new Set(OFFICE_PACK_TOOLS.map(tool => `fleet_${tool}`));
|
|
505
|
+
/** Long enough for an admin to read the prompt; the move is judged again on the click. */
|
|
506
|
+
const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
|
|
472
507
|
/**
|
|
473
508
|
* The prompts the web dashboard also offers: the ones about an instance's own health, which a dashboard
|
|
474
509
|
* user — holding the full-fleet web credential — may answer exactly as a fleet admin on the platform may.
|
|
@@ -477,6 +512,7 @@ const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
|
477
512
|
*/
|
|
478
513
|
const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
|
|
479
514
|
HANG_CALLBACK_PREFIX, EXIT_RESTART_CALLBACK_PREFIX, OOM_RETRY_CALLBACK_PREFIX, INTERACTIVE_ASSIST_CALLBACK_PREFIX,
|
|
515
|
+
REMOTE_CARD_CALLBACK_PREFIX,
|
|
480
516
|
]);
|
|
481
517
|
/**
|
|
482
518
|
* Where a reply goes on a fleet with no chat platform (web track C4): it is "sent" by being shown in the
|
|
@@ -564,6 +600,7 @@ export const CLI_ENV_PROBE_DEADLINE_MS = 16_000;
|
|
|
564
600
|
* started ten CLIs together, saturated the CPU, and healthy starts then missed
|
|
565
601
|
* their startup budget — which used to cost the user their conversation. Cores
|
|
566
602
|
* bound how many can actually make progress; memory bounds how many fit.
|
|
603
|
+
* `cores` is what this process may use (cpuset and cgroup quota, #749), not the host's count.
|
|
567
604
|
*/
|
|
568
605
|
export function deriveSpawnConcurrency(freeMemMB, cores) {
|
|
569
606
|
const byMemory = Math.floor(freeMemMB / 300);
|
|
@@ -610,6 +647,7 @@ export class FleetManager {
|
|
|
610
647
|
workingRecovery = null;
|
|
611
648
|
workingRecoveryTimer = null;
|
|
612
649
|
workingRecoveryCause = "unknown_interruption";
|
|
650
|
+
workingRecoveryGroup = this.managerBootId;
|
|
613
651
|
recoveryEnabled(name) {
|
|
614
652
|
const own = this.fleetConfig?.instances[name]?.resume_interrupted_work;
|
|
615
653
|
return own ?? this.fleetConfig?.defaults?.resume_interrupted_work ?? true;
|
|
@@ -650,7 +688,16 @@ export class FleetManager {
|
|
|
650
688
|
const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
|
|
651
689
|
&& (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
|
|
652
690
|
.map(row => `${row.instance} (${row.reason})`);
|
|
653
|
-
store.summary(group, () => outbox.admitSystemNotice(general, `working-recovery-summary:${group}`, `[system:working-recovery-summary] Observed cause: unknown interruption${this.workingRecoveryCause === "unknown_interruption" ? "" : `; prior requested action: ${this.workingRecoveryCause}`}. ${rows.length} interrupted instances; continuation queued ${rows.filter(r => r.outcome === "admitted").length}, excluded ${rows.filter(r => r.outcome === "excluded").length}, still deferred ${rows.filter(r => r.outcome === "deferred").length}. ${rows.filter(r => r.reason === "advisory_background_unknown").length ? `Advisory, background unknown: ${rows.filter(r => r.reason === "advisory_background_unknown").map(r => r.instance).join(", ")}. ` : ""}${deferredAdvisories.length ? `Deferred advisory candidates (background unknown; all other gates still apply): ${deferredAdvisories.join(", ")}. ` : ""}Details are in the Log. Deferred work remains eligible; no action or confirmation is requested.`));
|
|
691
|
+
const posted = store.summary(group, () => outbox.admitSystemNotice(general, `working-recovery-summary:${group}`, `[system:working-recovery-summary] Observed cause: unknown interruption${this.workingRecoveryCause === "unknown_interruption" ? "" : `; prior requested action: ${this.workingRecoveryCause}`}. ${rows.length} interrupted instances; continuation queued ${rows.filter(r => r.outcome === "admitted").length}, excluded ${rows.filter(r => r.outcome === "excluded").length}, still deferred ${rows.filter(r => r.outcome === "deferred").length}. ${rows.filter(r => r.reason === "advisory_background_unknown").length ? `Advisory, background unknown: ${rows.filter(r => r.reason === "advisory_background_unknown").map(r => r.instance).join(", ")}. ` : ""}${deferredAdvisories.length ? `Deferred advisory candidates (background unknown; all other gates still apply): ${deferredAdvisories.join(", ")}. ` : ""}Details are in the Log. Deferred work remains eligible; no action or confirmation is requested.`));
|
|
692
|
+
if (posted) {
|
|
693
|
+
const counts = { interrupted: rows.length, queued: rows.filter(row => row.outcome === "admitted").length,
|
|
694
|
+
excluded: rows.filter(row => row.outcome === "excluded").length, deferred: rows.filter(row => row.outcome === "deferred").length };
|
|
695
|
+
const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
|
|
696
|
+
this.logger.info({ group, ...counts, observedCause: "unknown_interruption", requestedAction }, "Interrupted work recovery summary queued for General");
|
|
697
|
+
this.eventLog?.insert(general, "working_state", { reason: "recovery_summary", group, ...counts,
|
|
698
|
+
observedCause: "unknown_interruption", requestedAction,
|
|
699
|
+
message: `Interrupted work summary queued for General: ${counts.interrupted} interrupted; ${counts.queued} queued, ${counts.excluded} excluded, ${counts.deferred} deferred.` });
|
|
700
|
+
}
|
|
654
701
|
},
|
|
655
702
|
log: (episode, outcome, reason) => {
|
|
656
703
|
const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
|
|
@@ -881,6 +928,9 @@ export class FleetManager {
|
|
|
881
928
|
pendingBackendSelects = new Map();
|
|
882
929
|
/** nonce → pending button prompt (hang restart, interactive assist, clean-exit restart). */
|
|
883
930
|
pendingNonceButtons = new Map();
|
|
931
|
+
/** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
|
|
932
|
+
pendingKiroMigrations = new Map();
|
|
933
|
+
kiroMigrationRuns = new Map();
|
|
884
934
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
885
935
|
needsYou = null;
|
|
886
936
|
/** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
|
|
@@ -893,6 +943,12 @@ export class FleetManager {
|
|
|
893
943
|
* adapter emits — whatever fields its payload carries — can claim a dashboard click's authority.
|
|
894
944
|
*/
|
|
895
945
|
webPromptClicks = new WeakSet();
|
|
946
|
+
/** #2675: a dashboard click's typed answer, for the click it came with. */
|
|
947
|
+
webPromptTexts = new WeakMap();
|
|
948
|
+
/** #2675: the nonce of each instance's posted card. */
|
|
949
|
+
remoteCardPosts = new Map();
|
|
950
|
+
/** #2675: replies taken as a card's answer (`<chatId>|<messageId>` → monotonic expiry), so another bot's copy is dropped. */
|
|
951
|
+
takenCardReplies = new Map();
|
|
896
952
|
/** The web clicks consumeNonceCallback actually claimed (the others were refused or lost a race). */
|
|
897
953
|
webPromptClaims = new WeakSet();
|
|
898
954
|
// Model failover state
|
|
@@ -1147,6 +1203,190 @@ export class FleetManager {
|
|
|
1147
1203
|
officeSkinWrites = new OfficeWriteLimiter(undefined, { perWindow: 10, windowMs: 60_000 });
|
|
1148
1204
|
/** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
|
|
1149
1205
|
officeWrites = new OfficeWriteLimiter();
|
|
1206
|
+
officeSharesInstance;
|
|
1207
|
+
/**
|
|
1208
|
+
* #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
|
|
1209
|
+
* their own (a manager and lease of its own: never the fleet's one tunnel lane nor the public link's preview tunnel).
|
|
1210
|
+
*/
|
|
1211
|
+
get officeShares() {
|
|
1212
|
+
return (this.officeSharesInstance ??= new OfficeShareService({
|
|
1213
|
+
source: {
|
|
1214
|
+
status: () => this.getUiStatus(),
|
|
1215
|
+
org: () => this.orgChart(),
|
|
1216
|
+
offices: () => this.officeStore.list(),
|
|
1217
|
+
acts: () => this.officeActs.live(),
|
|
1218
|
+
traffic: () => this.trafficSnapshot(),
|
|
1219
|
+
needs: () => this.needsYouItems().map(i => i.instance),
|
|
1220
|
+
hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
|
|
1221
|
+
skins: this.officeSkins,
|
|
1222
|
+
},
|
|
1223
|
+
manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
|
|
1224
|
+
ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
|
|
1225
|
+
provider: binary => new CloudflaredProvider({ binaryName: binary, protocol: this.fleetConfig?.web?.public_link?.protocol ?? "http2" }),
|
|
1226
|
+
config: () => this.fleetConfig?.web?.office_share,
|
|
1227
|
+
officeExists: id => this.officeStore.list().offices.some(o => o.id === id),
|
|
1228
|
+
ownerCurrent: owner => this.officeShareOwnerCurrent(owner),
|
|
1229
|
+
changed: () => this.emitSseEvent("office_shares", {}),
|
|
1230
|
+
log: (event, detail) => this.logger.info({ event, ...(detail ? { detail } : {}) }, "Office share link"),
|
|
1231
|
+
}));
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* #2636: a share made or ended — said in General, as a sign-in is (who, which office, for how long; never its URL).
|
|
1235
|
+
* `share` null with `count`: "Revoke all".
|
|
1236
|
+
*/
|
|
1237
|
+
officeShareNotice(event, who, share, count) {
|
|
1238
|
+
const office = !share ? "" : share.office === ALL_OFFICES ? t("office_share.all") : this.officeStore.list().offices.find(o => o.id === share.office)?.name ?? share.office;
|
|
1239
|
+
const minutes = share ? Math.round((share.expiresAt - share.createdAt) / 60_000) : 0;
|
|
1240
|
+
const text = event === "created" ? t("office_share.created_notice", who, office, minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`)
|
|
1241
|
+
: share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
|
|
1242
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1243
|
+
}
|
|
1244
|
+
/** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
|
|
1245
|
+
officeShareOwnerCurrent(owner) {
|
|
1246
|
+
const m = /^person:([^:]+):(.+)$/.exec(owner);
|
|
1247
|
+
if (!m)
|
|
1248
|
+
return owner === "admin-session";
|
|
1249
|
+
return this.hasFleetAdmins(m[1]) && this.isFleetAdmin(m[2], m[1]);
|
|
1250
|
+
}
|
|
1251
|
+
officeActsInstance;
|
|
1252
|
+
/** #2578: the agents' acts in the Office view (office_view / office_set), memory only; every change is `office_acts`. */
|
|
1253
|
+
get officeActs() {
|
|
1254
|
+
return (this.officeActsInstance ??= new OfficeActs({
|
|
1255
|
+
offices: () => this.officeStore.list().offices,
|
|
1256
|
+
pack: id => this.officeSkins.pack(id),
|
|
1257
|
+
chart: () => { const c = this.orgChart(); return { names: Object.keys(c.instances), teams: c.teams }; },
|
|
1258
|
+
people: () => this.officePeople(),
|
|
1259
|
+
changed: acts => this.emitSseEvent("office_acts", { acts, now: Date.now() }),
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
officePackToolsInstance;
|
|
1263
|
+
/** #2692: the agents' skin-pack tools — their log, the admins' approval mode and the pending changes (memory only). */
|
|
1264
|
+
get officePackTools() {
|
|
1265
|
+
return (this.officePackToolsInstance ??= new OfficePackTools({
|
|
1266
|
+
skins: this.officeSkins, offices: this.officeStore,
|
|
1267
|
+
allowedAgents: () => this.officePackAgents(),
|
|
1268
|
+
notice: (text, pending) => { void this.officePackNotice(text, pending); },
|
|
1269
|
+
settled: (id, text) => this.settleOfficePackPrompt(id, text),
|
|
1270
|
+
changed: what => {
|
|
1271
|
+
if (what.skins !== undefined)
|
|
1272
|
+
this.emitSseEvent("office_skins", { rev: what.skins });
|
|
1273
|
+
if (what.offices !== undefined)
|
|
1274
|
+
this.emitSseEvent("offices", { rev: what.offices });
|
|
1275
|
+
if (what.changes)
|
|
1276
|
+
this.emitSseEvent("office_pack_changes", {});
|
|
1277
|
+
},
|
|
1278
|
+
logger: this.logger,
|
|
1279
|
+
}));
|
|
1280
|
+
}
|
|
1281
|
+
/** #2692: the instances whose `office_packs` feature is on (their own switch, else the defaults'): the Office page lists them. */
|
|
1282
|
+
officePackAgents() {
|
|
1283
|
+
const names = new Set([...Object.keys(this.fleetConfig?.instances ?? {}), ...(this.classicChannels?.getAll() ?? []).map(ch => ch.instanceName)]);
|
|
1284
|
+
return [...names].filter(name => dashboardRefusalIn(this.fleetConfig, name, "office_pack_upload") === null);
|
|
1285
|
+
}
|
|
1286
|
+
/** #2692: the nonce of each pending pack change's prompt in General (its buttons are retired when it is decided). */
|
|
1287
|
+
officePackPrompts = new Map();
|
|
1288
|
+
/**
|
|
1289
|
+
* #2692: tell the fleet's admins of an agent's pack change — one short message in General (notifyFleetError, never a
|
|
1290
|
+
* sign-in notice). A pending one comes with Approve / Reject (a fleet admin's, as a kiro move); where no General can
|
|
1291
|
+
* carry buttons it is the plain message, and the Office page decides it.
|
|
1292
|
+
*/
|
|
1293
|
+
async officePackNotice(text, pending) {
|
|
1294
|
+
if (!pending) {
|
|
1295
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const general = this.findGeneralInstance();
|
|
1299
|
+
const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
1300
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined;
|
|
1301
|
+
const chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
1302
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
1303
|
+
if (general && adapterId && adapter && chatId) {
|
|
1304
|
+
try {
|
|
1305
|
+
const nonce = await this.postNonceButtonPromptOrThrow({
|
|
1306
|
+
prefix: OFFICE_PACK_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: pending.instance, adapter, adapterId, chatId,
|
|
1307
|
+
...(topic ? { threadId: topic } : {}), message: text,
|
|
1308
|
+
choices: [{ action: "approve", label: t("office_packs.approve_button") }, { action: "reject", label: t("office_packs.reject_button") }],
|
|
1309
|
+
expiredText: t("office_packs.expired", ""), timeoutMs: Math.max(1_000, pending.expires_at - Date.now()),
|
|
1310
|
+
});
|
|
1311
|
+
if (this.officePackTools.pendingViews().some(v => v.id === pending.id))
|
|
1312
|
+
this.officePackPrompts.set(pending.id, nonce);
|
|
1313
|
+
else
|
|
1314
|
+
this.retireOfficePackNonce(nonce, t("office_packs.gone"));
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
this.logger.info({ err: err.message }, "Office pack approval prompt unavailable; the plain notice and the Office page remain");
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1322
|
+
}
|
|
1323
|
+
retireOfficePackNonce(nonce, text) {
|
|
1324
|
+
const entry = this.pendingNonceButtons.get(nonce);
|
|
1325
|
+
if (!entry || entry.prefix !== OFFICE_PACK_CALLBACK_PREFIX)
|
|
1326
|
+
return;
|
|
1327
|
+
this.pendingNonceButtons.delete(nonce);
|
|
1328
|
+
clearTimeout(entry.timer);
|
|
1329
|
+
void this.retireNonceButtons(entry, entry.messageId ?? "", text);
|
|
1330
|
+
}
|
|
1331
|
+
/** #2692: a pending change was decided (here or on the page) or expired: its prompt says so and loses its buttons. */
|
|
1332
|
+
settleOfficePackPrompt(id, text) {
|
|
1333
|
+
const nonce = this.officePackPrompts.get(id);
|
|
1334
|
+
this.officePackPrompts.delete(id);
|
|
1335
|
+
if (nonce)
|
|
1336
|
+
this.retireOfficePackNonce(nonce, text);
|
|
1337
|
+
}
|
|
1338
|
+
/** #2692: Approve / Reject on a pending pack change. Any fleet admin (the default nonce authorization), in General. */
|
|
1339
|
+
async handleOfficePackCallback(data, adapterId, adapter) {
|
|
1340
|
+
const claimed = this.consumeNonceCallback(OFFICE_PACK_CALLBACK_PREFIX, /^office-pack:([0-9a-f]{32}):(approve|reject)$/, data, adapterId, adapter);
|
|
1341
|
+
if (claimed === null)
|
|
1342
|
+
return false;
|
|
1343
|
+
if (claimed === "consumed")
|
|
1344
|
+
return true;
|
|
1345
|
+
const { entry, action } = claimed;
|
|
1346
|
+
const nonce = data.callbackData.slice(OFFICE_PACK_CALLBACK_PREFIX.length, OFFICE_PACK_CALLBACK_PREFIX.length + 32);
|
|
1347
|
+
const id = [...this.officePackPrompts].find(([, n]) => n === nonce)?.[0];
|
|
1348
|
+
if (id)
|
|
1349
|
+
this.officePackPrompts.delete(id);
|
|
1350
|
+
const decided = id ? this.officePackTools.decide(id, action, t("settings_confirm.admin_label", data.userId ?? "")) : null;
|
|
1351
|
+
await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, decided?.text ?? t("office_packs.gone"));
|
|
1352
|
+
return true;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* #2578: each instance's state as the org chart shows it (fleet-org.js orgState, from the same fields the status
|
|
1356
|
+
* frame and the Needs-you list carry) and its display name (resolveInstanceIdentity, as getUiStatus) — read once.
|
|
1357
|
+
*/
|
|
1358
|
+
officePeople() {
|
|
1359
|
+
const needs = new Map();
|
|
1360
|
+
for (const item of this.needsYouItems())
|
|
1361
|
+
needs.set(item.instance, (needs.get(item.instance) ?? 0) + 1);
|
|
1362
|
+
const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
|
|
1363
|
+
let profiles = null;
|
|
1364
|
+
return {
|
|
1365
|
+
stateOf: name => {
|
|
1366
|
+
if (!this.isKnownInstance(name))
|
|
1367
|
+
return "missing";
|
|
1368
|
+
const p = this.instancePresentation(name), status = this.getInstanceStatus(name);
|
|
1369
|
+
if (p.state === "awaiting_input" || (needs.get(name) ?? 0) > 0)
|
|
1370
|
+
return "needs";
|
|
1371
|
+
if (p.startup_phase === "queued" || p.startup_phase === "starting")
|
|
1372
|
+
return p.startup_phase;
|
|
1373
|
+
if (status === "crashed" || status === "paused")
|
|
1374
|
+
return status;
|
|
1375
|
+
if (status !== "running")
|
|
1376
|
+
return "stopped";
|
|
1377
|
+
return p.execution_state === "stuck" || p.execution_state === "working" ? p.execution_state : "idle";
|
|
1378
|
+
},
|
|
1379
|
+
displayName: name => {
|
|
1380
|
+
try {
|
|
1381
|
+
profiles ??= profileIdentities(this.dataDir);
|
|
1382
|
+
return resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic: classicRooms.get(name), profile: profiles.get(name) }).display_name || undefined;
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
return undefined;
|
|
1386
|
+
}
|
|
1387
|
+
},
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1150
1390
|
/** #1629: the theme tools, a person's reset and lock, and the one appearance rate limiter (shared with effects). */
|
|
1151
1391
|
get webAppearance() {
|
|
1152
1392
|
return (this.webAppearanceService ??= new WebAppearance(this.themeStore, () => this.fleetConfig?.web ?? null, () => { try {
|
|
@@ -1672,6 +1912,154 @@ export class FleetManager {
|
|
|
1672
1912
|
// No platform confirmation was reachable. Host inspection is still required.
|
|
1673
1913
|
this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
|
|
1674
1914
|
}
|
|
1915
|
+
kiroMigrateDeps() {
|
|
1916
|
+
let token;
|
|
1917
|
+
const current = (name) => {
|
|
1918
|
+
const run = this.kiroMigrationRuns.get(name);
|
|
1919
|
+
return !!token && run?.token === token && !this.shuttingDown
|
|
1920
|
+
&& (this.explicitStopGeneration.get(name) ?? 0) === run.stopGeneration;
|
|
1921
|
+
};
|
|
1922
|
+
return {
|
|
1923
|
+
agendHome: getAgendHome(), fleetConfig: () => this.fleetConfig, saveFleetConfig: () => this.saveFleetConfig(),
|
|
1924
|
+
status: name => this.getInstanceStatus(name),
|
|
1925
|
+
reserve: name => {
|
|
1926
|
+
if (this.kiroMigrationRuns.has(name) || this.restartsInFlight.has(name) || this.stopsInFlight.has(name))
|
|
1927
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
1928
|
+
token = Symbol(name);
|
|
1929
|
+
this.kiroMigrationRuns.set(name, { token, stopGeneration: this.explicitStopGeneration.get(name) ?? 0 });
|
|
1930
|
+
return { current: () => current(name), release: () => {
|
|
1931
|
+
if (this.kiroMigrationRuns.get(name)?.token === token)
|
|
1932
|
+
this.kiroMigrationRuns.delete(name);
|
|
1933
|
+
this.scheduleDeliveryOutboxPump();
|
|
1934
|
+
} };
|
|
1935
|
+
},
|
|
1936
|
+
preflight: async (name, plan) => {
|
|
1937
|
+
const compat = getCachedKiroCliCompatibility(resolveBinary("kiro-cli"));
|
|
1938
|
+
if (!compat.supportsTui || !compat.supportsInstanceAgent || !compat.agentEngines?.includes("v2")
|
|
1939
|
+
|| !/^2\.(?:3[4-9]|[4-9]\d|\d{3,})\./.test(compat.version ?? ""))
|
|
1940
|
+
throw new Error(t("kiro_migrate.version_required"));
|
|
1941
|
+
const daemon = this.daemons.get(name);
|
|
1942
|
+
if (this.getInstanceStatus(name) === "running" && (!daemon || !await daemon.kiroMigrationIdle()))
|
|
1943
|
+
throw new Error(t("kiro_migrate.idle_required"));
|
|
1944
|
+
if (!current(name) || daemon && this.daemons.get(name) !== daemon)
|
|
1945
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1946
|
+
const inst = this.fleetConfig?.instances[name];
|
|
1947
|
+
const model = daemon?.getModelOverride() ?? inst?.model ?? (plan.classicId ? readKiroV1SavedModel({ id: plan.classicId,
|
|
1948
|
+
workingDirectory: plan.workingDirectory, dbPath: kiroV1DbPath(plan.credentialProfile) }) : undefined);
|
|
1949
|
+
if (!model && plan.classicId)
|
|
1950
|
+
throw new Error(t("kiro_migrate.model_unknown"));
|
|
1951
|
+
return model ?? "";
|
|
1952
|
+
},
|
|
1953
|
+
restart: async (name) => {
|
|
1954
|
+
if (!current(name))
|
|
1955
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1956
|
+
const run = this.kiroMigrationRuns.get(name);
|
|
1957
|
+
const expected = run.stopGeneration + 1;
|
|
1958
|
+
// This restart owns exactly one stop. A person's concurrent stop must not be hidden by the workflow.
|
|
1959
|
+
try {
|
|
1960
|
+
await this.restartSingleInstance(name, { explicit: true, kiroMigration: token });
|
|
1961
|
+
}
|
|
1962
|
+
finally {
|
|
1963
|
+
// Even a failed replacement owns its completed stop; retain authority to clean up/pause it.
|
|
1964
|
+
if (this.kiroMigrationRuns.get(name) === run && this.explicitStopGeneration.get(name) === expected)
|
|
1965
|
+
run.stopGeneration = expected;
|
|
1966
|
+
}
|
|
1967
|
+
if (this.explicitStopGeneration.get(name) !== expected)
|
|
1968
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1969
|
+
},
|
|
1970
|
+
verify: async (name, id, model, archived) => {
|
|
1971
|
+
const daemon = this.daemons.get(name), deadline = performance.now() + 60_000;
|
|
1972
|
+
if (!daemon)
|
|
1973
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1974
|
+
while (performance.now() < deadline) {
|
|
1975
|
+
if (!current(name) || this.daemons.get(name) !== daemon)
|
|
1976
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1977
|
+
const observed = await daemon.kiroMigrationObservation();
|
|
1978
|
+
if (observed) {
|
|
1979
|
+
if (observed.sessionId !== id || observed.model !== model || resolvePath(observed.transcript) !== resolvePath(kiroV2SessionsDir(), id + ".jsonl"))
|
|
1980
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1981
|
+
const inst = this.fleetConfig.instances[name];
|
|
1982
|
+
const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: kiroV2SessionsDir(), migration: {
|
|
1983
|
+
agendHome: getAgendHome(), instance: name, workingDirectory: inst.working_directory, credentialProfile: instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli"),
|
|
1984
|
+
sessionId: id, sessionsDir: kiroV2SessionsDir(), dbPath: kiroV1DbPath(instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli")),
|
|
1985
|
+
model, archived, outboxDbPath: join(this.dataDir, "delivery-outbox.db"), knownInstances: Object.keys(this.fleetConfig.instances),
|
|
1986
|
+
} });
|
|
1987
|
+
if (!current(name) || this.daemons.get(name) !== daemon)
|
|
1988
|
+
throw new Error(t("kiro_migrate.superseded"));
|
|
1989
|
+
if (reply.kind !== "ok" || !reply.verification)
|
|
1990
|
+
throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_migrate.verification_failed"));
|
|
1991
|
+
return reply.verification;
|
|
1992
|
+
}
|
|
1993
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1994
|
+
}
|
|
1995
|
+
throw new Error(t("kiro_migrate.verification_failed"));
|
|
1996
|
+
},
|
|
1997
|
+
stop: async (name) => { try {
|
|
1998
|
+
await this.stopInstance(name);
|
|
1999
|
+
}
|
|
2000
|
+
finally {
|
|
2001
|
+
writePausedMarker(this.getInstanceDir(name), Date.now(), "error");
|
|
2002
|
+
} },
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
/**
|
|
2006
|
+
* #2598 B2, migrate_kiro_instance: judge the move, then ask in General for a fleet admin's Confirm. Nothing changes
|
|
2007
|
+
* here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
|
|
2008
|
+
*/
|
|
2009
|
+
async requestKiroMigration(name, startFresh) {
|
|
2010
|
+
const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
|
|
2011
|
+
if (!judged.ok)
|
|
2012
|
+
return judged;
|
|
2013
|
+
const general = this.findGeneralInstance();
|
|
2014
|
+
const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
2015
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined;
|
|
2016
|
+
const chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
2017
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
2018
|
+
if (!general || !adapterId || !adapter || !chatId)
|
|
2019
|
+
return { ok: false, error: t("kiro_migrate.no_general") };
|
|
2020
|
+
const { plan } = judged;
|
|
2021
|
+
try {
|
|
2022
|
+
const nonce = await this.postNonceButtonPromptOrThrow({
|
|
2023
|
+
prefix: KIRO_MIGRATE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name, adapter, adapterId, chatId,
|
|
2024
|
+
...(topic ? { threadId: topic } : {}),
|
|
2025
|
+
message: kiroMigrationPrompt(plan, this.getInstanceStatus(name) === "running"),
|
|
2026
|
+
choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
|
|
2027
|
+
expiredText: t("kiro_migrate.expired", name),
|
|
2028
|
+
timeoutMs: KIRO_MIGRATE_TIMEOUT_MS,
|
|
2029
|
+
});
|
|
2030
|
+
this.pendingKiroMigrations.set(nonce, plan);
|
|
2031
|
+
const expiry = setTimeout(() => this.pendingKiroMigrations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000);
|
|
2032
|
+
expiry.unref?.();
|
|
2033
|
+
}
|
|
2034
|
+
catch (err) {
|
|
2035
|
+
return { ok: false, error: `${t("kiro_migrate.no_general")} (${err.message})` };
|
|
2036
|
+
}
|
|
2037
|
+
return { ok: true, text: t("kiro_migrate.requested"), classic_conversation: plan.classicId };
|
|
2038
|
+
}
|
|
2039
|
+
/** A fleet admin's Confirm or Cancel on a kiro move. Any fleet admin (the default nonce authorization), in General. */
|
|
2040
|
+
async handleKiroMigrateCallback(data, adapterId, adapter) {
|
|
2041
|
+
const claimed = this.consumeNonceCallback(KIRO_MIGRATE_CALLBACK_PREFIX, /^kiro-migrate:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
|
|
2042
|
+
if (claimed === null)
|
|
2043
|
+
return false;
|
|
2044
|
+
if (claimed === "consumed")
|
|
2045
|
+
return true;
|
|
2046
|
+
const { entry, action } = claimed;
|
|
2047
|
+
const nonce = data.callbackData.slice(KIRO_MIGRATE_CALLBACK_PREFIX.length, KIRO_MIGRATE_CALLBACK_PREFIX.length + 32);
|
|
2048
|
+
const plan = this.pendingKiroMigrations.get(nonce);
|
|
2049
|
+
this.pendingKiroMigrations.delete(nonce);
|
|
2050
|
+
const messageId = entry.messageId ?? data.messageId;
|
|
2051
|
+
if (action === "cancel" || !plan) {
|
|
2052
|
+
await this.retireNonceButtons(entry, messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
|
|
2053
|
+
return true;
|
|
2054
|
+
}
|
|
2055
|
+
this.eventLog?.insert(plan.name, "kiro_migrate", { classicId: plan.classicId, userId: data.userId });
|
|
2056
|
+
await this.retireNonceButtons(entry, messageId, t("kiro_migrate.applying", plan.name));
|
|
2057
|
+
const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
|
|
2058
|
+
this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
|
|
2059
|
+
this.notifyFleetError(outcome.text);
|
|
2060
|
+
this.notifyInstanceTopic(plan.name, outcome.text);
|
|
2061
|
+
return true;
|
|
2062
|
+
}
|
|
1675
2063
|
async handleSettingsChangeCallback(data, adapterId, adapter) {
|
|
1676
2064
|
const result = this.consumeNonceCallback("settings-confirm:", /^settings-confirm:([0-9a-f]{32}):(confirm|reject)$/, data, adapterId, adapter, undefined, (userId, entry) => entry.confirmationAdmin?.(userId) === true);
|
|
1677
2065
|
if (!result)
|
|
@@ -2218,7 +2606,7 @@ export class FleetManager {
|
|
|
2218
2606
|
if (reason === "cancelled")
|
|
2219
2607
|
outbox.workingRecovery.cancel(name, observation.configuration, observation.sessionId);
|
|
2220
2608
|
if (entry.prior)
|
|
2221
|
-
outbox.workingRecovery.discover(name, entry.prior);
|
|
2609
|
+
outbox.workingRecovery.discover(name, entry.prior, this.workingRecoveryGroup);
|
|
2222
2610
|
const state = entry.current.observation.executionState ?? "unknown";
|
|
2223
2611
|
const counts = { tasks: entry.current.references.filter(r => r.kind === "task").length,
|
|
2224
2612
|
owed: entry.current.references.filter(r => r.kind === "owed").length,
|
|
@@ -2525,7 +2913,7 @@ export class FleetManager {
|
|
|
2525
2913
|
const explicit = this.fleetConfig?.defaults?.startup?.concurrency;
|
|
2526
2914
|
if (explicit != null)
|
|
2527
2915
|
return Math.max(1, Math.min(20, explicit));
|
|
2528
|
-
return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)),
|
|
2916
|
+
return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)), effectiveCpuCount());
|
|
2529
2917
|
}
|
|
2530
2918
|
/** #1685: "elevated" or "critical" while host memory pressure slows or holds new agents; otherwise null (an
|
|
2531
2919
|
* unknown macOS reading — the advisory case — is unknown, so null too). */
|
|
@@ -2742,8 +3130,9 @@ export class FleetManager {
|
|
|
2742
3130
|
// the next tunnel and says so, it does not block the fleet.
|
|
2743
3131
|
this.announceToolPermissionsChange();
|
|
2744
3132
|
this.tightenInstanceDirectories();
|
|
2745
|
-
// #1723: the public link's preview tunnel keeps its own lease beside it; a leftover one is reaped the same way
|
|
2746
|
-
|
|
3133
|
+
// #1723: the public link's preview tunnel keeps its own lease beside it; a leftover one is reaped the same way (and
|
|
3134
|
+
// #2636's Office share tunnel's).
|
|
3135
|
+
for (const leaseDir of [this.dataDir, join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR), join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR)]) {
|
|
2747
3136
|
void reapStaleTunnel(leaseDir)
|
|
2748
3137
|
.then(outcome => {
|
|
2749
3138
|
if (outcome.kind === "manual")
|
|
@@ -3308,7 +3697,7 @@ export class FleetManager {
|
|
|
3308
3697
|
await data.respond(t("permission.denied"));
|
|
3309
3698
|
return;
|
|
3310
3699
|
}
|
|
3311
|
-
this.fleetConfig
|
|
3700
|
+
setNoticeMode(this.fleetConfig, "tips", mode === "on");
|
|
3312
3701
|
this.saveFleetConfig();
|
|
3313
3702
|
await data.respond(t(mode === "on" ? "tips.enabled" : "tips.disabled"));
|
|
3314
3703
|
return;
|
|
@@ -5098,6 +5487,7 @@ export class FleetManager {
|
|
|
5098
5487
|
}
|
|
5099
5488
|
classicBehaviorUpdate(instanceName) {
|
|
5100
5489
|
return {
|
|
5490
|
+
reply_triggers: this.classicChannels?.getReplyTriggersByInstance(instanceName, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS,
|
|
5101
5491
|
tool_progress: this.classicChannels?.getToolProgressByInstance(instanceName, this.fleetConfig?.defaults?.tool_progress) ?? "off",
|
|
5102
5492
|
reply_completion_guard: this.classicChannels?.getReplyCompletionGuardByInstance(instanceName, this.fleetConfig?.defaults?.reply_completion_guard) ?? true,
|
|
5103
5493
|
};
|
|
@@ -5396,36 +5786,40 @@ export class FleetManager {
|
|
|
5396
5786
|
* config file races. Stagger applies between outer lifecycle admissions.
|
|
5397
5787
|
* TODO: per-instance startup timeout (existing issue, not introduced here)
|
|
5398
5788
|
*/
|
|
5399
|
-
async startInstancesWithConcurrency(entries, topicMode, onReady) {
|
|
5789
|
+
async startInstancesWithConcurrency(entries, topicMode, onReady, classic = []) {
|
|
5400
5790
|
// Persisted pauses are intentionally preserved across fleet restarts. Filter
|
|
5401
5791
|
// them before grouping/staggering: startInstance() retains its own guard as
|
|
5402
5792
|
// a final backstop, but putting a no-op entry in this queue still consumes a
|
|
5403
5793
|
// full stagger slot for every distinct working directory.
|
|
5404
5794
|
const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
|
|
5795
|
+
// ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
|
|
5796
|
+
// only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
|
|
5797
|
+
const fleetNames = new Set(entries.map(([name]) => name));
|
|
5798
|
+
const runnableClassic = classic.filter(ch => !this.lifecycle.isPaused(ch.instanceName) && !fleetNames.has(ch.instanceName));
|
|
5405
5799
|
const pausedCount = entries.length - runnableEntries.length;
|
|
5406
5800
|
if (pausedCount > 0) {
|
|
5407
5801
|
this.logger.info({ pausedCount }, "Paused instances excluded from startup queue");
|
|
5408
5802
|
}
|
|
5409
|
-
if (runnableEntries.length === 0)
|
|
5803
|
+
if (runnableEntries.length === 0 && runnableClassic.length === 0)
|
|
5410
5804
|
return;
|
|
5411
5805
|
if (this.fleetConfig?.defaults?.startup?.concurrency == null) {
|
|
5412
5806
|
this.logger.info({
|
|
5413
5807
|
concurrency: this.spawnConcurrency(),
|
|
5414
5808
|
freeMemMB: Math.round(freemem() / (1024 * 1024)),
|
|
5415
|
-
totalInstances: runnableEntries.length,
|
|
5809
|
+
totalInstances: runnableEntries.length + runnableClassic.length,
|
|
5416
5810
|
}, "Adaptive startup concurrency");
|
|
5417
5811
|
}
|
|
5418
5812
|
const startedAt = performance.now();
|
|
5419
5813
|
let active = 0, peakConcurrentStarts = 0, readyInstances = 0;
|
|
5420
|
-
|
|
5814
|
+
const admit = (name, workingDirectory, start) => this.spawnGate.run({
|
|
5421
5815
|
instanceName: name,
|
|
5422
|
-
workingDirectory
|
|
5816
|
+
workingDirectory,
|
|
5423
5817
|
reason: "startup",
|
|
5424
5818
|
stage: "lifecycle",
|
|
5425
5819
|
}, async () => {
|
|
5426
5820
|
peakConcurrentStarts = Math.max(peakConcurrentStarts, ++active);
|
|
5427
5821
|
try {
|
|
5428
|
-
if (await
|
|
5822
|
+
if (await start()) {
|
|
5429
5823
|
readyInstances++;
|
|
5430
5824
|
onReady?.(name);
|
|
5431
5825
|
}
|
|
@@ -5433,9 +5827,13 @@ export class FleetManager {
|
|
|
5433
5827
|
finally {
|
|
5434
5828
|
active--;
|
|
5435
5829
|
}
|
|
5436
|
-
})
|
|
5830
|
+
});
|
|
5831
|
+
await Promise.all([
|
|
5832
|
+
...runnableEntries.map(([name, config]) => admit(name, config.working_directory, () => this.startInstanceUnattended(name, config, topicMode, "instance"))),
|
|
5833
|
+
...runnableClassic.map(ch => admit(ch.instanceName, join(getAgendHome(), "workspaces", ch.instanceName), () => this.startClassicInstanceUnattended(ch, "classic instance"))),
|
|
5834
|
+
]);
|
|
5437
5835
|
this.logger.info({
|
|
5438
|
-
totalInstances: runnableEntries.length, readyInstances,
|
|
5836
|
+
totalInstances: runnableEntries.length + runnableClassic.length, classicInstances: runnableClassic.length, readyInstances,
|
|
5439
5837
|
elapsedSeconds: Math.round((performance.now() - startedAt) / 100) / 10,
|
|
5440
5838
|
peakConcurrentStarts,
|
|
5441
5839
|
}, "Fleet startup batch completed");
|
|
@@ -5735,6 +6133,11 @@ export class FleetManager {
|
|
|
5735
6133
|
stopOnUnsupportedCli(name, err, what) {
|
|
5736
6134
|
if (!(err instanceof UnsupportedCliError))
|
|
5737
6135
|
return false;
|
|
6136
|
+
if (err instanceof ResumeBlockedError) {
|
|
6137
|
+
this.logger.error({ name, reason: err.message }, `Not starting ${what}: retained session needs store repair`);
|
|
6138
|
+
void this.notifyFleetError(t("fleet.resume_blocked", name, err.message, err.remedy));
|
|
6139
|
+
return true;
|
|
6140
|
+
}
|
|
5738
6141
|
this.logger.error({ name, reason: err.message }, `Not starting ${what}: the installed CLI cannot run it as configured`);
|
|
5739
6142
|
this.queueUnsupportedCliNotice(name, err.message);
|
|
5740
6143
|
this.queueKiroIncompatNotice(name, err.message);
|
|
@@ -5934,6 +6337,9 @@ export class FleetManager {
|
|
|
5934
6337
|
}
|
|
5935
6338
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
5936
6339
|
async restartSingleInstance(name, opts) {
|
|
6340
|
+
const migration = this.kiroMigrationRuns?.get(name);
|
|
6341
|
+
if (migration && migration.token !== opts?.kiroMigration)
|
|
6342
|
+
throw new Error(t("kiro_migrate.in_progress"));
|
|
5937
6343
|
if (opts?.recoveryCurrent && !opts.recoveryCurrent())
|
|
5938
6344
|
throw new Error(t("exit.expired", name));
|
|
5939
6345
|
const retry = opts?.explicit ? opts.sessionOomRetry ?? captureSessionOomRetry(this.getInstanceDir(name)) : undefined;
|
|
@@ -5994,7 +6400,7 @@ export class FleetManager {
|
|
|
5994
6400
|
return run;
|
|
5995
6401
|
}
|
|
5996
6402
|
isInstanceRestarting(name) {
|
|
5997
|
-
return this.restartsInFlight.has(name);
|
|
6403
|
+
return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
|
|
5998
6404
|
}
|
|
5999
6405
|
/** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
|
|
6000
6406
|
restartInFlight(name) {
|
|
@@ -6855,9 +7261,22 @@ export class FleetManager {
|
|
|
6855
7261
|
...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
|
|
6856
7262
|
}).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
|
|
6857
7263
|
// #2348: `explicit` — grants are the scope (default deny); false: the derived scope of #1920, until grants are saved.
|
|
6858
|
-
|
|
7264
|
+
// #2667: the ClassicBot rooms there are now, apart from fleet.yaml's instances (a grant may name either: guestReach),
|
|
7265
|
+
// with its display name as the sidebar resolves it (a profile alias, else the room's own), when it has one.
|
|
7266
|
+
const profiles = profileIdentities(this.dataDir);
|
|
7267
|
+
const rooms = (this.classicChannels?.getAll() ?? []).filter((r) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, r.instanceName))
|
|
7268
|
+
.map((r) => {
|
|
7269
|
+
const shown = resolveInstanceIdentity({ classic: r, profile: profiles.get(r.instanceName) }).display_name;
|
|
7270
|
+
return { name: r.instanceName, ...(shown && shown !== r.instanceName ? { display_name: shown } : {}) };
|
|
7271
|
+
})
|
|
7272
|
+
.sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
|
|
7273
|
+
return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), rooms, connections, guests,
|
|
6859
7274
|
audit: this.guestAudit() };
|
|
6860
7275
|
}
|
|
7276
|
+
/** #2667: the ClassicBot rooms' instance names now — what a settings save's validation knows besides fleet.yaml's instances. */
|
|
7277
|
+
classicRoomNames() {
|
|
7278
|
+
return (this.classicChannels?.getAll() ?? []).map((r) => r.instanceName);
|
|
7279
|
+
}
|
|
6861
7280
|
/**
|
|
6862
7281
|
* #2499 review: the guest audit an admin reads — the newest grant changes and admin-sent codes from the event log
|
|
6863
7282
|
* (`fleet` rows: no agent's Log shows them). Through guestOverview only: Settings' admin gate, never over the public
|
|
@@ -7312,7 +7731,8 @@ export class FleetManager {
|
|
|
7312
7731
|
this.notifyFleetError(t("web.guest_login_notice", this.webPerson(info.owner).name, info.label, this.guestScope(info.owner).size, info.handle.slice(0, 8)));
|
|
7313
7732
|
return;
|
|
7314
7733
|
}
|
|
7315
|
-
|
|
7734
|
+
// Never by DM (hmsg-3b8ac2af: Telegram cannot DM a user who has not started the bot); a legacy "dm" reads as on.
|
|
7735
|
+
if (!loginNoticeMode(this.fleetConfig?.web))
|
|
7316
7736
|
return;
|
|
7317
7737
|
// The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
|
|
7318
7738
|
// sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
|
|
@@ -7686,7 +8106,8 @@ export class FleetManager {
|
|
|
7686
8106
|
}
|
|
7687
8107
|
}
|
|
7688
8108
|
async startAllWithinDeadline(configPath, deadline) {
|
|
7689
|
-
this.
|
|
8109
|
+
if (!this.structuredLoginCleanupFailed)
|
|
8110
|
+
this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
|
|
7690
8111
|
this.loginController?.reopen();
|
|
7691
8112
|
const startupStartedAt = Date.now();
|
|
7692
8113
|
FleetManager.signalTarget = this;
|
|
@@ -7947,12 +8368,7 @@ export class FleetManager {
|
|
|
7947
8368
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
7948
8369
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
7949
8370
|
this.ensureGeneralInstructions(generalDir, backendName, name);
|
|
7950
|
-
fleet.instances[name] =
|
|
7951
|
-
...DEFAULT_INSTANCE_CONFIG,
|
|
7952
|
-
working_directory: generalDir,
|
|
7953
|
-
general_topic: true,
|
|
7954
|
-
channel_id: need.adapterId,
|
|
7955
|
-
};
|
|
8371
|
+
fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
|
|
7956
8372
|
generalsCreated = true;
|
|
7957
8373
|
}
|
|
7958
8374
|
if (generalsCreated)
|
|
@@ -8098,9 +8514,11 @@ export class FleetManager {
|
|
|
8098
8514
|
this.rotateAllInstanceLogs();
|
|
8099
8515
|
}, 24 * 60 * 60_000);
|
|
8100
8516
|
this.logRotateTimer.unref?.();
|
|
8101
|
-
// Phase 2: Start remaining instances with staggered concurrency
|
|
8102
|
-
|
|
8103
|
-
|
|
8517
|
+
// Phase 2: Start remaining instances, and the ClassicBot rooms with them, with staggered concurrency. A room needs no
|
|
8518
|
+
// chat connection to spawn: its open-list is set above (reregisterClassicChannels) and inbound queues until it is up.
|
|
8519
|
+
const classicAtBoot = topicMode && (fleet.channel || fleet.channels?.length) ? this.classicChannels?.getAll() ?? [] : [];
|
|
8520
|
+
if (others.length > 0 || classicAtBoot.length > 0) {
|
|
8521
|
+
await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
|
|
8104
8522
|
}
|
|
8105
8523
|
if (topicMode && (fleet.channel || fleet.channels?.length)) {
|
|
8106
8524
|
await adapterStartup;
|
|
@@ -8142,22 +8560,7 @@ export class FleetManager {
|
|
|
8142
8560
|
// IPC is already wired by startInstancesWithConcurrency → startInstance →
|
|
8143
8561
|
// connectIpcToInstance. The previous 3s sleep + connectToInstances loop
|
|
8144
8562
|
// was redundant.
|
|
8145
|
-
//
|
|
8146
|
-
if (this.classicChannels) {
|
|
8147
|
-
const fleetBackend = this.fleetConfig?.defaults?.backend;
|
|
8148
|
-
const channels = this.classicChannels.getAll()
|
|
8149
|
-
.filter(ch => !this.lifecycle.isPaused(ch.instanceName));
|
|
8150
|
-
const concurrency = 3;
|
|
8151
|
-
let idx = 0;
|
|
8152
|
-
while (idx < channels.length) {
|
|
8153
|
-
const batch = channels.slice(idx, idx + concurrency);
|
|
8154
|
-
await Promise.allSettled(batch.map(async (ch) => {
|
|
8155
|
-
if (await this.startClassicInstanceUnattended(ch, "classic instance"))
|
|
8156
|
-
startupProgress.markReady();
|
|
8157
|
-
}));
|
|
8158
|
-
idx += concurrency;
|
|
8159
|
-
}
|
|
8160
|
-
}
|
|
8563
|
+
// The ClassicBot rooms started in Phase 2, with the fleet.
|
|
8161
8564
|
for (const name of Object.keys(fleet.instances)) {
|
|
8162
8565
|
this.startStatuslineWatcher(name);
|
|
8163
8566
|
}
|
|
@@ -8424,25 +8827,43 @@ export class FleetManager {
|
|
|
8424
8827
|
if (channelConfigs.length === 0)
|
|
8425
8828
|
return;
|
|
8426
8829
|
// Start ALL adapters in parallel — any single failure doesn't block others.
|
|
8427
|
-
const
|
|
8830
|
+
const settled = channelConfigs.map(() => false);
|
|
8831
|
+
// Each connection is recorded (connected, or retrying with its retry scheduled) as its own login settles.
|
|
8832
|
+
const recorded = channelConfigs.map((cfg, i) => (i === 0
|
|
8428
8833
|
? this.startSingleAdapter(fleet, cfg)
|
|
8429
|
-
: this.startAdditionalAdapter(cfg)))
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
}
|
|
8834
|
+
: this.startAdditionalAdapter(cfg)).then(() => ({ status: "fulfilled" }), (reason) => ({ status: "rejected", reason }))
|
|
8835
|
+
.then(result => { settled[i] = true; this.recordAdapterStartup(cfg, result); })
|
|
8836
|
+
.catch(err => this.logger.error({ err }, "Recording a chat connection's startup failed")));
|
|
8837
|
+
// #2642: bounded. Startup continues past a connection still logging in after this long (ClassicBot instances,
|
|
8838
|
+
// READY); that connection is recorded, or retried, when its login settles, and joins the fleet as it would anyway.
|
|
8839
|
+
let timer;
|
|
8840
|
+
const inTime = await Promise.race([
|
|
8841
|
+
Promise.all(recorded).then(() => true),
|
|
8842
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(false), FleetManager.ADAPTER_STARTUP_WAIT_MS); timer.unref?.(); }),
|
|
8843
|
+
]).finally(() => clearTimeout(timer));
|
|
8844
|
+
if (!inTime) {
|
|
8845
|
+
this.logger.warn({
|
|
8846
|
+
pending: channelConfigs.filter((_, i) => !settled[i]).map(cfg => cfg.id ?? cfg.type),
|
|
8847
|
+
waitedMs: FleetManager.ADAPTER_STARTUP_WAIT_MS,
|
|
8848
|
+
}, "Chat connection still connecting — fleet startup continues without waiting for it");
|
|
8444
8849
|
}
|
|
8445
8850
|
}
|
|
8851
|
+
/** How long fleet startup waits for its chat connections before it continues without the slow ones (#2642). */
|
|
8852
|
+
static ADAPTER_STARTUP_WAIT_MS = 60_000;
|
|
8853
|
+
/** Track a connection's startup outcome; a failure gets the background retry. */
|
|
8854
|
+
recordAdapterStartup(cfg, result) {
|
|
8855
|
+
const adapterId = cfg.id ?? cfg.type;
|
|
8856
|
+
if (result.status === "fulfilled") {
|
|
8857
|
+
this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
|
|
8858
|
+
return;
|
|
8859
|
+
}
|
|
8860
|
+
const err = result.reason;
|
|
8861
|
+
this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
|
|
8862
|
+
this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
|
|
8863
|
+
this.scheduleAdapterRetry(adapterId);
|
|
8864
|
+
// Notify admin via whichever adapter is already up
|
|
8865
|
+
this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
|
|
8866
|
+
}
|
|
8446
8867
|
/** Keep trying while the fleet runs, with a five-minute cap even after a long outage. */
|
|
8447
8868
|
scheduleAdapterRetry(adapterId) {
|
|
8448
8869
|
const INITIAL_DELAY_MS = 5_000;
|
|
@@ -9731,6 +10152,10 @@ export class FleetManager {
|
|
|
9731
10152
|
}))
|
|
9732
10153
|
return;
|
|
9733
10154
|
}
|
|
10155
|
+
// #2675: a reply to a posted question card is its typed answer (judged by who may talk to that instance), not a
|
|
10156
|
+
// message for the instance — which could not take one while its card is up anyway.
|
|
10157
|
+
if (await this.takeRemoteCardReply(msg))
|
|
10158
|
+
return;
|
|
9734
10159
|
// Access control — classic channels are open to all, others require an allowed
|
|
9735
10160
|
// user. This runs before the dedup claim, and is judged by the adapter that
|
|
9736
10161
|
// owns the topic rather than the one that happened to receive the message:
|
|
@@ -11122,7 +11547,7 @@ export class FleetManager {
|
|
|
11122
11547
|
if (!ipc || !fleetRequestId)
|
|
11123
11548
|
return;
|
|
11124
11549
|
const type = String(msg.type);
|
|
11125
|
-
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) ? "fleet_web_theme_response"
|
|
11550
|
+
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" || OFFICE_PACK_IPC.has(type) ? "fleet_web_theme_response"
|
|
11126
11551
|
: type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
|
|
11127
11552
|
: type.startsWith("fleet_decision_") ? "fleet_decision_response"
|
|
11128
11553
|
: type === "fleet_task" ? "fleet_task_response"
|
|
@@ -11176,7 +11601,11 @@ export class FleetManager {
|
|
|
11176
11601
|
|| type === "fleet_get_web_appearance" || type === "fleet_preview_web_appearance"
|
|
11177
11602
|
|| type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
|
|
11178
11603
|
|| type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
|
|
11179
|
-
|| type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
|
|
11604
|
+
|| type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
|
|
11605
|
+
|| type === "fleet_office_view" || type === "fleet_office_set"
|
|
11606
|
+
// #2692: the skin packs (OFFICE_PACK_IPC), answered as web theme tools.
|
|
11607
|
+
|| type === "fleet_office_pack_check" || type === "fleet_office_pack_upload" || type === "fleet_office_pack_delete"
|
|
11608
|
+
|| type === "fleet_office_pack_restore" || type === "fleet_office_pack_list" || type === "fleet_office_set_style") {
|
|
11180
11609
|
this.handleWebTheme(name, msg);
|
|
11181
11610
|
return;
|
|
11182
11611
|
}
|
|
@@ -11197,7 +11626,9 @@ export class FleetManager {
|
|
|
11197
11626
|
const result = tool === "get_web_appearance" || tool === "preview_web_appearance"
|
|
11198
11627
|
? this.appearanceTools.handle(instanceName, tool, args)
|
|
11199
11628
|
: tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
|
|
11200
|
-
: this.
|
|
11629
|
+
: tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
|
|
11630
|
+
: OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
|
|
11631
|
+
: this.webAppearance.handleTool(instanceName, tool, args);
|
|
11201
11632
|
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
|
|
11202
11633
|
}
|
|
11203
11634
|
catch (err) {
|
|
@@ -11633,6 +12064,10 @@ export class FleetManager {
|
|
|
11633
12064
|
return this.appearanceTools.handle(instance, tool, args);
|
|
11634
12065
|
if (tool === "export_web_mod" || tool === "propose_web_mod")
|
|
11635
12066
|
return this.agentMods.handle(instance, tool, args);
|
|
12067
|
+
if (tool === "office_view" || tool === "office_set")
|
|
12068
|
+
return this.officeActs.handle(instance, tool, args); // #2578
|
|
12069
|
+
if (OFFICE_PACK_IPC.has(`fleet_${tool}`))
|
|
12070
|
+
return this.officePackTools.handle(instance, tool, args); // #2692
|
|
11636
12071
|
return this.webAppearance.handleTool(instance, tool, args);
|
|
11637
12072
|
}
|
|
11638
12073
|
async handlePreviewEmojisHttp(instance, args) {
|
|
@@ -12067,6 +12502,33 @@ export class FleetManager {
|
|
|
12067
12502
|
return;
|
|
12068
12503
|
if (!this.savedFleetConfigSnapshot)
|
|
12069
12504
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
12505
|
+
normalizeNoticeDelivery(this.fleetConfig);
|
|
12506
|
+
normalizeOfficeStorage(this.fleetConfig);
|
|
12507
|
+
explicitPatches = explicitPatches.flatMap(patch => {
|
|
12508
|
+
const path = patch.path;
|
|
12509
|
+
// #2601 (and #2692's office_packs): a separately kept feature is written under its own key, never in `dashboard`.
|
|
12510
|
+
const separate = path.at(-2) === "dashboard" ? SEPARATE_FEATURE_KEYS[path.at(-1)] : undefined;
|
|
12511
|
+
if (separate && !patch.remove) {
|
|
12512
|
+
return [{ ...patch, path: [...path.slice(0, -2), separate] }, { path, remove: true }];
|
|
12513
|
+
}
|
|
12514
|
+
if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.keys(SEPARATE_FEATURE_KEYS).some(f => Object.hasOwn(patch.value, f))) {
|
|
12515
|
+
const dashboard = { ...patch.value }, moved = [];
|
|
12516
|
+
for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
|
|
12517
|
+
if (!Object.hasOwn(dashboard, feature))
|
|
12518
|
+
continue;
|
|
12519
|
+
const value = dashboard[feature];
|
|
12520
|
+
delete dashboard[feature];
|
|
12521
|
+
moved.push({ path: [...path.slice(0, -1), key], value, remove: value === null });
|
|
12522
|
+
}
|
|
12523
|
+
return [{ ...patch, value: dashboard }, ...moved];
|
|
12524
|
+
}
|
|
12525
|
+
// #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
|
|
12526
|
+
if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
|
|
12527
|
+
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "login"], remove: true }];
|
|
12528
|
+
if (patch.path.join(".") === "defaults.tips" && patch.value === "dm")
|
|
12529
|
+
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "tips"], remove: true }];
|
|
12530
|
+
return [patch];
|
|
12531
|
+
});
|
|
12070
12532
|
// Re-read immediately before patching so an unrelated concurrent/manual
|
|
12071
12533
|
// edit is retained. Invalid concurrent YAML is never overwritten.
|
|
12072
12534
|
const source = existsSync(this.configPath) ? readFileSync(this.configPath, "utf-8") : "{}\n";
|
|
@@ -12142,9 +12604,46 @@ export class FleetManager {
|
|
|
12142
12604
|
},
|
|
12143
12605
|
});
|
|
12144
12606
|
this.rawFleetConfig = loadRawFleetConfig(this.configPath);
|
|
12607
|
+
// The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
|
|
12608
|
+
// leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
|
|
12609
|
+
// Keep the FleetConfig object itself: Settings and other request contexts hold it.
|
|
12610
|
+
const resolvedInstances = loadFleetConfig(this.configPath).instances;
|
|
12611
|
+
for (const [name, resolved] of Object.entries(resolvedInstances)) {
|
|
12612
|
+
const current = this.fleetConfig.instances[name];
|
|
12613
|
+
if (!current) {
|
|
12614
|
+
this.fleetConfig.instances[name] = resolved;
|
|
12615
|
+
continue;
|
|
12616
|
+
}
|
|
12617
|
+
// Lifecycle rollback fences hold these object identities across save. Refresh their values, not their owner.
|
|
12618
|
+
for (const key of Object.keys(current))
|
|
12619
|
+
if (!Object.hasOwn(resolved, key))
|
|
12620
|
+
delete current[key];
|
|
12621
|
+
Object.assign(current, resolved);
|
|
12622
|
+
}
|
|
12145
12623
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
12624
|
+
this.refreshSavedDashboardPermissions();
|
|
12146
12625
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
12147
12626
|
}
|
|
12627
|
+
/** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
|
|
12628
|
+
refreshSavedDashboardPermissions() {
|
|
12629
|
+
if (!this.fleetConfig)
|
|
12630
|
+
return;
|
|
12631
|
+
const classicNames = new Set(this.classicChannels?.getAll().map(ch => ch.instanceName) ?? []);
|
|
12632
|
+
for (const [name, daemon] of this.daemons) {
|
|
12633
|
+
const next = this.fleetConfig.instances[name] ?? (classicNames.has(name) ? getEffectiveInstanceDefaults(this.fleetConfig.defaults) : undefined);
|
|
12634
|
+
const previous = daemon.getConfigSnapshot?.();
|
|
12635
|
+
if (!next || !previous)
|
|
12636
|
+
continue;
|
|
12637
|
+
if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access && previous.office_pack_access === next.office_pack_access)
|
|
12638
|
+
continue;
|
|
12639
|
+
// Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
|
|
12640
|
+
// detects that they require a restart. MCP menus also watch the raw file; this updates the next spawn's env.
|
|
12641
|
+
daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null, office_pack_access: next.office_pack_access ?? null });
|
|
12642
|
+
if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
|
|
12643
|
+
this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
|
|
12644
|
+
}
|
|
12645
|
+
}
|
|
12646
|
+
}
|
|
12148
12647
|
/** Why the config written to `candidate` must not replace fleet.yaml, or null: it does not load, or it adds errors. */
|
|
12149
12648
|
savedFleetConfigProblem(candidate) {
|
|
12150
12649
|
let after;
|
|
@@ -12173,7 +12672,7 @@ export class FleetManager {
|
|
|
12173
12672
|
logConfigWarningsAtStartup() {
|
|
12174
12673
|
let warnings = [];
|
|
12175
12674
|
try {
|
|
12176
|
-
warnings = validateFleetConfig(this.rawFleetConfig).warnings;
|
|
12675
|
+
warnings = validateFleetConfig(this.rawFleetConfig, { rooms: readClassicRoomNames(this.dataDir) }).warnings;
|
|
12177
12676
|
}
|
|
12178
12677
|
catch (err) {
|
|
12179
12678
|
this.logger.debug({ err }, "fleet.yaml validation failed (startup warnings)");
|
|
@@ -12182,7 +12681,46 @@ export class FleetManager {
|
|
|
12182
12681
|
this.logger.warn({ path: w.path }, `fleet.yaml: ${w.path ? `${w.path}: ` : ""}${w.message}`);
|
|
12183
12682
|
}
|
|
12184
12683
|
/** One-time upgrade migration; invalid YAML is never rewritten. */
|
|
12684
|
+
migrateNoticeDeliveryStorage() {
|
|
12685
|
+
if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
|
|
12686
|
+
|| this.fleetConfig.web?.notice_delivery !== undefined)
|
|
12687
|
+
&& validateFleetConfig(this.rawFleetConfig).valid) {
|
|
12688
|
+
try {
|
|
12689
|
+
this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
|
|
12690
|
+
this.saveFleetConfig();
|
|
12691
|
+
this.logger.info("Notices no longer go by DM: stored them as topic booleans and dropped the old DM routing");
|
|
12692
|
+
}
|
|
12693
|
+
catch (err) {
|
|
12694
|
+
this.logger.warn({ err }, "Could not migrate DM notice routing; keeping the original file");
|
|
12695
|
+
}
|
|
12696
|
+
}
|
|
12697
|
+
}
|
|
12698
|
+
migrateOfficeStorage() {
|
|
12699
|
+
if (!this.fleetConfig || !this.configPath || !hasLegacyOffice(this.rawFleetConfig) || !validateFleetConfig(this.rawFleetConfig).valid)
|
|
12700
|
+
return;
|
|
12701
|
+
try {
|
|
12702
|
+
this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
|
|
12703
|
+
const patches = [];
|
|
12704
|
+
const raw = this.rawFleetConfig;
|
|
12705
|
+
for (const [path, item] of [[["defaults"], raw.defaults], ...Object.entries(raw.instances ?? {}).map(([name, value]) => [["instances", name], value])]) {
|
|
12706
|
+
for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
|
|
12707
|
+
if (!item?.dashboard || !Object.hasOwn(item.dashboard, feature))
|
|
12708
|
+
continue;
|
|
12709
|
+
patches.push({ path: [...path, "dashboard", feature], remove: true });
|
|
12710
|
+
if (!Object.hasOwn(item, key) && typeof item.dashboard[feature] === "boolean")
|
|
12711
|
+
patches.push({ path: [...path, key], value: item.dashboard[feature] });
|
|
12712
|
+
}
|
|
12713
|
+
}
|
|
12714
|
+
this.saveFleetConfig(patches);
|
|
12715
|
+
this.logger.info("Stored Office access outside dashboard for downgrade compatibility");
|
|
12716
|
+
}
|
|
12717
|
+
catch (err) {
|
|
12718
|
+
this.logger.warn({ err }, "Could not migrate Office access; keeping the original file");
|
|
12719
|
+
}
|
|
12720
|
+
}
|
|
12185
12721
|
slimFleetConfigAtStartup() {
|
|
12722
|
+
this.migrateOfficeStorage();
|
|
12723
|
+
this.migrateNoticeDeliveryStorage();
|
|
12186
12724
|
const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
|
|
12187
12725
|
if (redundantPaths.length === 0)
|
|
12188
12726
|
return;
|
|
@@ -12405,6 +12943,8 @@ export class FleetManager {
|
|
|
12405
12943
|
this.effectToolsService?.forget(name);
|
|
12406
12944
|
this.mods.forgetInstance(name);
|
|
12407
12945
|
this.agentMods.forget(name);
|
|
12946
|
+
this.officeActsInstance?.forget(name);
|
|
12947
|
+
this.officePackToolsInstance?.forget(name);
|
|
12408
12948
|
}
|
|
12409
12949
|
catch (err) {
|
|
12410
12950
|
this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
|
|
@@ -14036,7 +14576,7 @@ export class FleetManager {
|
|
|
14036
14576
|
entry.web = {
|
|
14037
14577
|
text: web.text,
|
|
14038
14578
|
...(links ? { links } : {}),
|
|
14039
|
-
actions: opts.choices.map(c => ({ id: c.action, label: c.label })),
|
|
14579
|
+
actions: [...opts.choices.map(c => ({ id: c.action, label: c.label })), ...(opts.webInput ? [{ id: opts.webInput.action, label: opts.webInput.label, input: true }] : [])],
|
|
14040
14580
|
expiresAt: Date.now() + (opts.timeoutMs ?? NONCE_BUTTON_TIMEOUT_MS),
|
|
14041
14581
|
};
|
|
14042
14582
|
this.emitSseEvent("prompt", { instance: entry.instanceName, nonce, ...entry.web });
|
|
@@ -14211,6 +14751,10 @@ export class FleetManager {
|
|
|
14211
14751
|
return true;
|
|
14212
14752
|
if (await this.handleSettingsChangeCallback(data, adapterId, adapter))
|
|
14213
14753
|
return true;
|
|
14754
|
+
if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
|
|
14755
|
+
return true;
|
|
14756
|
+
if (await this.handleOfficePackCallback(data, adapterId, adapter))
|
|
14757
|
+
return true;
|
|
14214
14758
|
if (await this.handleDashboardCallback(data, adapterId, adapter))
|
|
14215
14759
|
return true;
|
|
14216
14760
|
if (await this.handleClassicApproval(data, adapterId, adapter))
|
|
@@ -14227,6 +14771,8 @@ export class FleetManager {
|
|
|
14227
14771
|
return true;
|
|
14228
14772
|
if (await this.handleInteractivePromptAssist(data, adapterId, adapter))
|
|
14229
14773
|
return true;
|
|
14774
|
+
if (await this.handleRemoteCardClick(data, adapterId, adapter))
|
|
14775
|
+
return true;
|
|
14230
14776
|
if (await this.handleClassicBackendSelection(data))
|
|
14231
14777
|
return true;
|
|
14232
14778
|
if (await this.handleModelSelection(data, adapterId))
|
|
@@ -14331,6 +14877,7 @@ export class FleetManager {
|
|
|
14331
14877
|
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
14332
14878
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
14333
14879
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
14880
|
+
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
14334
14881
|
}));
|
|
14335
14882
|
const taken = new Set(fleet.map(i => i.name));
|
|
14336
14883
|
const classic = (this.classicChannels?.getAll() ?? []).filter(ch => !taken.has(ch.instanceName)).map(ch => ({
|
|
@@ -14340,6 +14887,7 @@ export class FleetManager {
|
|
|
14340
14887
|
ledgerPath: join(this.getInstanceDir(ch.instanceName), "cache-ledger.json"),
|
|
14341
14888
|
claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
|
|
14342
14889
|
profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14890
|
+
kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14343
14891
|
}));
|
|
14344
14892
|
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
14345
14893
|
}
|
|
@@ -14360,8 +14908,19 @@ export class FleetManager {
|
|
|
14360
14908
|
return this.credentialProfileOf(name, "claude-code") ?? "default";
|
|
14361
14909
|
if (backend === "codex")
|
|
14362
14910
|
return "unrecorded";
|
|
14911
|
+
if (backend === "kiro-cli")
|
|
14912
|
+
return this.credentialProfileOf(name, "kiro-cli") ?? "default";
|
|
14913
|
+
if (backend === "grok" || backend === "muse" || backend === "opencode" || backend === "antigravity")
|
|
14914
|
+
return "default";
|
|
14363
14915
|
return undefined;
|
|
14364
14916
|
}
|
|
14917
|
+
/** #2122 §10: a Kiro instance with a credential profile writes its classic store in that profile's home. */
|
|
14918
|
+
kiroDbPathOf(name, backend) {
|
|
14919
|
+
if (backend !== "kiro-cli")
|
|
14920
|
+
return undefined;
|
|
14921
|
+
const profile = this.credentialProfileOf(name, "kiro-cli");
|
|
14922
|
+
return profile ? join(credentialProfileStoreHome(getAgendHome(), "kiro-cli", profile), "data.sqlite3") : undefined;
|
|
14923
|
+
}
|
|
14365
14924
|
/** #2122: the fleet's day boundary zone — defaults.scheduler.default_timezone, else this host's. */
|
|
14366
14925
|
usageZone() {
|
|
14367
14926
|
return this.fleetConfig?.defaults?.scheduler?.default_timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
@@ -14404,6 +14963,19 @@ export class FleetManager {
|
|
|
14404
14963
|
rolloutsUnreadable: root => sharedRolloutIndex(root).unreadable,
|
|
14405
14964
|
metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
|
|
14406
14965
|
zone: () => this.usageZone(),
|
|
14966
|
+
// #2122 §10: Kiro's shared classic store (XDG_DATA_HOME, as kiro-cli resolves it) and its KIRO_HOME.
|
|
14967
|
+
kiroDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "kiro-cli", "data.sqlite3"),
|
|
14968
|
+
kiroHome: () => kiroHome(),
|
|
14969
|
+
// #2122 §10 step 3: grok keeps its sessions under ~/.grok (grok.ts reads them there too).
|
|
14970
|
+
grokSessionsDir: () => join(homedir(), ".grok", "sessions"),
|
|
14971
|
+
// #2122 §10 step 4: muse keeps sessions under $XDG_DATA_HOME/muse (else ~/.local/share/muse), as muse.ts reads them.
|
|
14972
|
+
// #2122 §10 step 5: OpenCode's database, under XDG_DATA_HOME as opencode resolves it.
|
|
14973
|
+
// #2122 §10 step 6: agy's conversations, under ~/.gemini as the antigravity backend reads its settings.
|
|
14974
|
+
antigravityDir: () => join(homedir(), ".gemini", "antigravity-cli", "conversations"),
|
|
14975
|
+
// The version AgEnD's CLI probe last recorded for agy: its cache file, whatever its age (never runs agy).
|
|
14976
|
+
antigravityVersion: () => this.lastRecordedCliVersion("antigravity"),
|
|
14977
|
+
opencodeDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "opencode", "opencode.db"),
|
|
14978
|
+
museSessionsDir: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "muse", "sessions"),
|
|
14407
14979
|
log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
|
|
14408
14980
|
});
|
|
14409
14981
|
}
|
|
@@ -14524,6 +15096,7 @@ export class FleetManager {
|
|
|
14524
15096
|
return this.deliveryOutbox.acknowledge(deliveryId, by);
|
|
14525
15097
|
},
|
|
14526
15098
|
tasks: () => this.needsYouTaskItems(),
|
|
15099
|
+
backendSignIns: () => this.needsYouBackendSignIns(),
|
|
14527
15100
|
taskAction: action => this.taskActionFromChat(action),
|
|
14528
15101
|
settings: () => ({
|
|
14529
15102
|
liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
|
|
@@ -14673,8 +15246,12 @@ export class FleetManager {
|
|
|
14673
15246
|
const buttons = this.replyButtons();
|
|
14674
15247
|
return buttons ? buttons.clickWeb(instance, id, index, voter) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
|
|
14675
15248
|
}
|
|
14676
|
-
async clickWebPrompt(instance, nonce, action) {
|
|
14677
|
-
if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-
|
|
15249
|
+
async clickWebPrompt(instance, nonce, action, text) {
|
|
15250
|
+
if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z0-9-]{0,23}$/.test(action))
|
|
15251
|
+
return { status: 400, error: "Malformed prompt answer" };
|
|
15252
|
+
// #2675: only an action the page shows a text box for takes text, and it must say something.
|
|
15253
|
+
const input = this.pendingNonceButtons.get(nonce)?.web?.actions.find(a => a.id === action)?.input === true;
|
|
15254
|
+
if (input !== (text !== undefined) || (input && (!text.trim() || text.length > REMOTE_CARD_TEXT_MAX)))
|
|
14678
15255
|
return { status: 400, error: "Malformed prompt answer" };
|
|
14679
15256
|
const entry = this.pendingNonceButtons.get(nonce);
|
|
14680
15257
|
// Unknown is the same answer as answered or expired: the page drops the buttons either way.
|
|
@@ -14696,6 +15273,8 @@ export class FleetManager {
|
|
|
14696
15273
|
notice = n; },
|
|
14697
15274
|
};
|
|
14698
15275
|
this.webPromptClicks.add(data);
|
|
15276
|
+
if (text !== undefined)
|
|
15277
|
+
this.webPromptTexts.set(data, text);
|
|
14699
15278
|
await this.dispatchAdapterCallback(data, entry.adapterId, entry.adapter);
|
|
14700
15279
|
if (this.webPromptClaims.has(data))
|
|
14701
15280
|
return { status: 200 };
|
|
@@ -15753,6 +16332,163 @@ export class FleetManager {
|
|
|
15753
16332
|
}
|
|
15754
16333
|
return true;
|
|
15755
16334
|
}
|
|
16335
|
+
// ── #2675: a card a CLI shows in place of its composer, answered from the chat ─────────────────────────────────
|
|
16336
|
+
/**
|
|
16337
|
+
* Where an instance's card is put to people: its own place — its topic, or its ClassicBot room — or, on a fleet with
|
|
16338
|
+
* no chat platform, the dashboard. Never General: the answer is the instance's conversation.
|
|
16339
|
+
*/
|
|
16340
|
+
remoteCardPlace(instanceName) {
|
|
16341
|
+
const web = this.webOnlyPromptPlace();
|
|
16342
|
+
if (web)
|
|
16343
|
+
return web;
|
|
16344
|
+
const room = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
16345
|
+
if (room) {
|
|
16346
|
+
const adapterId = this.classicChannels?.getAdapterIdByInstance(instanceName) ?? this.getPrimaryAdapterId();
|
|
16347
|
+
const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
|
|
16348
|
+
return adapter && adapterId ? { adapter, adapterId, chatId: room } : null;
|
|
16349
|
+
}
|
|
16350
|
+
const adapterId = this.getInstanceAdapterId(instanceName);
|
|
16351
|
+
const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
|
|
16352
|
+
const chatId = this.getGroupIdForInstance(instanceName);
|
|
16353
|
+
const topicId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
16354
|
+
if (!adapter || !adapterId || !chatId || topicId == null)
|
|
16355
|
+
return null;
|
|
16356
|
+
return { adapter, adapterId, chatId, threadId: String(topicId) };
|
|
16357
|
+
}
|
|
16358
|
+
/**
|
|
16359
|
+
* The daemon saw a card held still on `instanceName`'s screen (remote_card): put it to the people who talk to that
|
|
16360
|
+
* instance, with a button per answer. A card already posted for the instance is retired first (the daemon said it
|
|
16361
|
+
* was gone; this is the fallback).
|
|
16362
|
+
*/
|
|
16363
|
+
async postRemoteCard(instanceName, card) {
|
|
16364
|
+
this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
|
|
16365
|
+
const place = this.remoteCardPlace(instanceName);
|
|
16366
|
+
if (!place) {
|
|
16367
|
+
this.logger.warn({ instanceName }, "A card is waiting on screen but the instance has no place to post it — the parked notice stands");
|
|
16368
|
+
return;
|
|
16369
|
+
}
|
|
16370
|
+
const prompt = remoteCardPrompt(instanceName, card);
|
|
16371
|
+
const nonce = await this.postNonceButtonPrompt({
|
|
16372
|
+
prefix: REMOTE_CARD_CALLBACK_PREFIX, alertType: "interactive_prompt", instanceName, ...place,
|
|
16373
|
+
message: prompt.message, choices: prompt.choices, webInput: prompt.webInput,
|
|
16374
|
+
expiredText: t("card.expired", instanceName), timeoutMs: REMOTE_CARD_TIMEOUT_MS,
|
|
16375
|
+
extra: { remoteCard: { identity: card.identity, kind: card.kind, options: card.kind === "question" ? card.options.map(o => ({ n: o.n, title: o.title })) : [] } },
|
|
16376
|
+
});
|
|
16377
|
+
if (nonce)
|
|
16378
|
+
this.remoteCardPosts.set(instanceName, nonce);
|
|
16379
|
+
}
|
|
16380
|
+
/**
|
|
16381
|
+
* The card is gone from the screen (remote_card_gone, `identity`; null: whichever is posted): its buttons collapse to
|
|
16382
|
+
* `text` if nobody answered them here.
|
|
16383
|
+
*/
|
|
16384
|
+
retireRemoteCard(instanceName, identity, text = t("card.answered_in_pane")) {
|
|
16385
|
+
const nonce = this.remoteCardPosts.get(instanceName);
|
|
16386
|
+
if (!nonce)
|
|
16387
|
+
return;
|
|
16388
|
+
const entry = this.pendingNonceButtons.get(nonce);
|
|
16389
|
+
if (entry && identity !== null && entry.remoteCard?.identity !== identity)
|
|
16390
|
+
return;
|
|
16391
|
+
this.remoteCardPosts.delete(instanceName);
|
|
16392
|
+
if (!entry)
|
|
16393
|
+
return;
|
|
16394
|
+
this.pendingNonceButtons.delete(nonce);
|
|
16395
|
+
if (entry.timer)
|
|
16396
|
+
clearTimeout(entry.timer);
|
|
16397
|
+
void this.retireNonceButtons(entry, entry.messageId ?? "", text);
|
|
16398
|
+
}
|
|
16399
|
+
/** Who may answer an instance's card: whoever may already talk to it there (a ClassicBot room is open to all). */
|
|
16400
|
+
mayAnswerRemoteCard(userId, entry) {
|
|
16401
|
+
if (this.classicChannels?.getChannelIdByInstance(entry.instanceName) === entry.chatId)
|
|
16402
|
+
return true;
|
|
16403
|
+
const am = this.worlds.get(entry.adapterId)?.accessManager ?? this.accessManager;
|
|
16404
|
+
return !am || am.isAllowed(userId);
|
|
16405
|
+
}
|
|
16406
|
+
/** A click on a card's button (platform or dashboard). */
|
|
16407
|
+
async handleRemoteCardClick(data, callbackAdapterId, receivingAdapter) {
|
|
16408
|
+
if (!data.callbackData.startsWith(REMOTE_CARD_CALLBACK_PREFIX))
|
|
16409
|
+
return false;
|
|
16410
|
+
// A typed answer from the dashboard must come with its text; refused before the claim, so the buttons stay.
|
|
16411
|
+
const match = REMOTE_CARD_CALLBACK_RE.exec(data.callbackData);
|
|
16412
|
+
if (match?.[2] === "text" && remoteCardAction("text", this.webPromptTexts.get(data)) === null) {
|
|
16413
|
+
data.ack?.(t("buttons.stale_notice"));
|
|
16414
|
+
return true;
|
|
16415
|
+
}
|
|
16416
|
+
const claimed = this.consumeNonceCallback(REMOTE_CARD_CALLBACK_PREFIX, REMOTE_CARD_CALLBACK_RE, data, callbackAdapterId, receivingAdapter, undefined, (userId, entry) => this.mayAnswerRemoteCard(userId, entry));
|
|
16417
|
+
if (claimed === null)
|
|
16418
|
+
return false;
|
|
16419
|
+
if (claimed === "consumed")
|
|
16420
|
+
return true;
|
|
16421
|
+
const action = remoteCardAction(claimed.action, this.webPromptTexts.get(data));
|
|
16422
|
+
const who = this.webPromptClicks.has(data) ? t("card.someone_on_dashboard") : (data.username ?? data.userId ?? "?");
|
|
16423
|
+
await this.answerRemoteCardFor(claimed.entry, action, who, data.messageId, data.editClicked);
|
|
16424
|
+
return true;
|
|
16425
|
+
}
|
|
16426
|
+
/**
|
|
16427
|
+
* An inbound message that replies to a posted question card, by someone who may answer it: its text is the typed
|
|
16428
|
+
* answer, and the message goes no further (true). Another bot's copy of a reply already taken is dropped (true).
|
|
16429
|
+
*/
|
|
16430
|
+
async takeRemoteCardReply(msg) {
|
|
16431
|
+
if (msg.isBotMessage || !msg.replyTo)
|
|
16432
|
+
return false;
|
|
16433
|
+
const now = performance.now();
|
|
16434
|
+
for (const [key, until] of this.takenCardReplies)
|
|
16435
|
+
if (until <= now)
|
|
16436
|
+
this.takenCardReplies.delete(key);
|
|
16437
|
+
const key = `${msg.chatId}|${msg.messageId}`;
|
|
16438
|
+
if (this.takenCardReplies.has(key))
|
|
16439
|
+
return true;
|
|
16440
|
+
for (const [nonce, entry] of this.pendingNonceButtons) {
|
|
16441
|
+
if (entry.prefix !== REMOTE_CARD_CALLBACK_PREFIX || entry.remoteCard?.kind !== "question")
|
|
16442
|
+
continue;
|
|
16443
|
+
// By place, whichever of the fleet's bots received the reply (a sibling's copy can arrive first, #2680 review), on
|
|
16444
|
+
// the post's own platform; who may answer is judged by the post's world (mayAnswerRemoteCard), as Needs you does.
|
|
16445
|
+
if (entry.chatId !== msg.chatId || entry.messageId !== msg.replyTo || entry.adapter.type !== msg.source)
|
|
16446
|
+
continue;
|
|
16447
|
+
if (!msg.userId || !this.mayAnswerRemoteCard(msg.userId, entry))
|
|
16448
|
+
return false; // not theirs to answer: it is just a message
|
|
16449
|
+
const action = remoteCardAction("text", msg.text ?? "");
|
|
16450
|
+
if (!action)
|
|
16451
|
+
return false;
|
|
16452
|
+
this.pendingNonceButtons.delete(nonce);
|
|
16453
|
+
if (entry.timer)
|
|
16454
|
+
clearTimeout(entry.timer);
|
|
16455
|
+
this.webPromptGone(entry);
|
|
16456
|
+
this.takenCardReplies.set(key, now + REMOTE_CARD_TAKEN_TTL_MS);
|
|
16457
|
+
await this.answerRemoteCardFor(entry, action, msg.username ?? msg.userId, entry.messageId ?? msg.replyTo);
|
|
16458
|
+
return true;
|
|
16459
|
+
}
|
|
16460
|
+
return false;
|
|
16461
|
+
}
|
|
16462
|
+
/**
|
|
16463
|
+
* Key a claimed answer into the instance's card (the daemon checks it is still that card and walks a verified
|
|
16464
|
+
* cursor), then collapse the post to what happened. When the card is still waiting and was not answered — refused
|
|
16465
|
+
* or stopped part-way — it is posted again, so it can still be answered from here.
|
|
16466
|
+
*/
|
|
16467
|
+
async answerRemoteCardFor(entry, action, who, messageId, editClicked) {
|
|
16468
|
+
if (this.remoteCardPosts.get(entry.instanceName) === entry.nonce)
|
|
16469
|
+
this.remoteCardPosts.delete(entry.instanceName);
|
|
16470
|
+
const daemon = this.daemons.get(entry.instanceName);
|
|
16471
|
+
const card = entry.remoteCard;
|
|
16472
|
+
let outcome;
|
|
16473
|
+
if (!card || !action)
|
|
16474
|
+
outcome = { outcome: "refused", reason: "malformed" };
|
|
16475
|
+
else if (!daemon?.answerRemoteCard)
|
|
16476
|
+
outcome = { outcome: "stale" };
|
|
16477
|
+
else {
|
|
16478
|
+
try {
|
|
16479
|
+
outcome = await daemon.answerRemoteCard(card.identity, action);
|
|
16480
|
+
}
|
|
16481
|
+
catch (err) {
|
|
16482
|
+
this.logger.warn({ err: err?.message, instanceName: entry.instanceName }, "Answering a card from the chat failed");
|
|
16483
|
+
outcome = { outcome: "stopped", step: "error" };
|
|
16484
|
+
}
|
|
16485
|
+
}
|
|
16486
|
+
this.eventLog?.insert(entry.instanceName, "remote_card_answer", { action: action?.kind ?? null, outcome: outcome.outcome });
|
|
16487
|
+
const choice = card && action ? remoteCardChoiceLabel(card, action) : "?";
|
|
16488
|
+
await this.retireNonceButtons(entry, messageId, remoteCardOutcomeText(entry.instanceName, who, choice, outcome), editClicked);
|
|
16489
|
+
if (outcome.outcome === "refused" || outcome.outcome === "stopped")
|
|
16490
|
+
daemon?.repostRemoteCard?.();
|
|
16491
|
+
}
|
|
15756
16492
|
// ── Cancel button ────────────────────────────────────────────────────
|
|
15757
16493
|
// Sent after delivering a user message to an instance; clicking it (or
|
|
15758
16494
|
// /cancel) sends Escape to the instance's pane to interrupt generation.
|
|
@@ -16528,6 +17264,55 @@ export class FleetManager {
|
|
|
16528
17264
|
}
|
|
16529
17265
|
return this.loginController;
|
|
16530
17266
|
}
|
|
17267
|
+
browserBackendSignInsInstance = null;
|
|
17268
|
+
structuredLoginCleanupFailed = false;
|
|
17269
|
+
pendingBackendSince = new Map();
|
|
17270
|
+
browserSignInOwnerCurrent(owner) {
|
|
17271
|
+
const record = owner;
|
|
17272
|
+
return !this.shuttingDown && typeof record.handle === "string" && this.webSessions?.findByHandle(record.handle) === owner
|
|
17273
|
+
&& this.isLocalAdminSession(record.handle);
|
|
17274
|
+
}
|
|
17275
|
+
/** #2599: default-account controls share the exact writer reservation with chat/profile/install. */
|
|
17276
|
+
get browserBackendSignIns() {
|
|
17277
|
+
return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
|
|
17278
|
+
dataDir: this.dataDir, lock: this.loginWindow,
|
|
17279
|
+
current: owner => this.browserSignInOwnerCurrent(owner),
|
|
17280
|
+
installed: async () => ONBOARDING_BACKENDS.filter(backend => this.isCliInstalled(backend)),
|
|
17281
|
+
checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
|
|
17282
|
+
checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
|
|
17283
|
+
transport: () => createStructuredSignInTransport(),
|
|
17284
|
+
changed: (event, owner, launched) => {
|
|
17285
|
+
try {
|
|
17286
|
+
this.eventLog?.insert("login", "browser_backend_signin", event);
|
|
17287
|
+
}
|
|
17288
|
+
catch { /* safe metadata only */ }
|
|
17289
|
+
broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
|
|
17290
|
+
this.needsYou?.poke();
|
|
17291
|
+
if (!this.browserSignInOwnerCurrent(owner))
|
|
17292
|
+
return;
|
|
17293
|
+
if (event.phase === "starting") {
|
|
17294
|
+
const record = owner;
|
|
17295
|
+
this.notifyFleetError(t("login.web_dashboard_notice", event.backend, record.label, record.handle.slice(0, 8)));
|
|
17296
|
+
}
|
|
17297
|
+
if (event.phase === "done") {
|
|
17298
|
+
void this.backendAvailability.refresh([event.backend], { force: true }).catch(() => { });
|
|
17299
|
+
if (launched)
|
|
17300
|
+
void this.recoverBackendInstances(event.backend, undefined, null).catch(() => this.logger.warn({ backend: event.backend }, "Post sign-in recovery failed"));
|
|
17301
|
+
}
|
|
17302
|
+
},
|
|
17303
|
+
});
|
|
17304
|
+
}
|
|
17305
|
+
needsYouBackendSignIns() {
|
|
17306
|
+
const pending = readPendingBackendSignIns(this.dataDir).pending;
|
|
17307
|
+
for (const backend of this.pendingBackendSince?.keys() ?? [])
|
|
17308
|
+
if (!pending.includes(backend))
|
|
17309
|
+
this.pendingBackendSince.delete(backend);
|
|
17310
|
+
return pending.map(backend => {
|
|
17311
|
+
if (!this.pendingBackendSince.has(backend))
|
|
17312
|
+
this.pendingBackendSince.set(backend, Date.now());
|
|
17313
|
+
return { backend, since: this.pendingBackendSince.get(backend), browser: structuredSignInSupported(backend) };
|
|
17314
|
+
});
|
|
17315
|
+
}
|
|
16531
17316
|
/** #2233: sign-ins started from the web dashboard (local admin sessions), on the same controller as chat's. */
|
|
16532
17317
|
webBackendLoginsInstance = null;
|
|
16533
17318
|
get webBackendLogins() {
|
|
@@ -16880,6 +17665,14 @@ export class FleetManager {
|
|
|
16880
17665
|
// "cancelled" is the detail both legacy onDone handlers keep quiet about —
|
|
16881
17666
|
// a stopping fleet must not announce "login failed — fleet shutdown".
|
|
16882
17667
|
await Promise.all([
|
|
17668
|
+
bounded(this.browserBackendSignInsInstance?.close().then(confirmed => {
|
|
17669
|
+
if (confirmed)
|
|
17670
|
+
this.browserBackendSignInsInstance = null;
|
|
17671
|
+
else {
|
|
17672
|
+
this.structuredLoginCleanupFailed = true;
|
|
17673
|
+
this.logger.error("Structured login cleanup failed; sign-in admission remains closed until the host is restarted.");
|
|
17674
|
+
}
|
|
17675
|
+
}), "browser-controls"),
|
|
16883
17676
|
bounded(this.loginController?.shutdown(), "web-login"),
|
|
16884
17677
|
bounded(this.activeInstall?.session.cancel("cancelled"), "install"),
|
|
16885
17678
|
]);
|
|
@@ -16896,6 +17689,13 @@ export class FleetManager {
|
|
|
16896
17689
|
this.installHandoff = null;
|
|
16897
17690
|
return t("login.cancelled", backend);
|
|
16898
17691
|
}
|
|
17692
|
+
const browserLogin = this.browserBackendSignInsInstance, browserBackend = browserLogin?.activeBackend;
|
|
17693
|
+
if (browserLogin && browserBackend) {
|
|
17694
|
+
const confirmed = await browserLogin.close();
|
|
17695
|
+
if (confirmed)
|
|
17696
|
+
this.browserBackendSignInsInstance = null;
|
|
17697
|
+
return confirmed ? t("login.cancelled", browserBackend) : t("login.browser_cleanup_failed");
|
|
17698
|
+
}
|
|
16899
17699
|
if (this.loginController?.isActive())
|
|
16900
17700
|
return this.loginController.cancel();
|
|
16901
17701
|
return t("login.no_session");
|
|
@@ -17067,7 +17867,7 @@ export class FleetManager {
|
|
|
17067
17867
|
// session only judges that verdict; it does not read the output for URLs or prompts.
|
|
17068
17868
|
const flow = {
|
|
17069
17869
|
backend,
|
|
17070
|
-
command: info.install,
|
|
17870
|
+
command: info.unattended ?? info.install,
|
|
17071
17871
|
successPattern: /$^/,
|
|
17072
17872
|
timeoutMs: 10 * 60 * 1000,
|
|
17073
17873
|
};
|
|
@@ -18068,8 +18868,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18068
18868
|
// its ping in the reference, not the text). One hop only: a reply to a message of ours that itself answered a bot
|
|
18069
18869
|
// (or the sender) is a chain of bots answering each other — a mention still carries it on. A reply to our answer
|
|
18070
18870
|
// to a person is the ordinary handoff, and is addressed.
|
|
18071
|
-
const
|
|
18072
|
-
const
|
|
18871
|
+
const replyTriggers = this.classicChannels?.getReplyTriggers(channelId, msg.adapterId, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS;
|
|
18872
|
+
const repliesToUs = replyTriggers && !!adapterBotUserId && msg.replyToAuthorId === adapterBotUserId && msg.replyToChain !== true;
|
|
18873
|
+
// Snapshot/quoted content is context, never the forwarder's own mention.
|
|
18874
|
+
const addressText = msg.source === "discord" ? (msg.humanOriginalBody ?? text) : text;
|
|
18875
|
+
const isMentioned = (mentionTag && addressText.includes(mentionTag)) || repliesToUs;
|
|
18073
18876
|
if (!isMentioned) {
|
|
18074
18877
|
// Bare attachment (no @mention) — already saved above; just acknowledge.
|
|
18075
18878
|
if (saved) {
|
|
@@ -18404,6 +19207,19 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18404
19207
|
catch { /* missing / stale / corrupt */ }
|
|
18405
19208
|
return null;
|
|
18406
19209
|
}
|
|
19210
|
+
/**
|
|
19211
|
+
* #2122 §10: the version the CLI probe last recorded for a backend — its cache file read whatever its age (the usage
|
|
19212
|
+
* reader's version gate needs the last known version, not a fresh one); null when none was recorded. Never a probe.
|
|
19213
|
+
*/
|
|
19214
|
+
lastRecordedCliVersion(backend) {
|
|
19215
|
+
try {
|
|
19216
|
+
const v = JSON.parse(readFileSync(this.cliEnvPath(backend), "utf-8")).version;
|
|
19217
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
19218
|
+
}
|
|
19219
|
+
catch {
|
|
19220
|
+
return null;
|
|
19221
|
+
}
|
|
19222
|
+
}
|
|
18407
19223
|
/** Return cached CLI versions immediately. Refresh is started after /sysinfo is sent. */
|
|
18408
19224
|
getBackendCliVersionSnapshot() {
|
|
18409
19225
|
const snapshot = {};
|
|
@@ -20341,8 +21157,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20341
21157
|
/** Signal-handler continuation, after successful graceful shutdown only. */
|
|
20342
21158
|
noteRequestedShutdown() {
|
|
20343
21159
|
const progress = readUpdateProgress(this.dataDir)?.progress;
|
|
20344
|
-
|
|
20345
|
-
|
|
21160
|
+
// Host CLI updates carry a generic marker with no chat progress target.
|
|
21161
|
+
const update = progress ? updateProgressOperation(progress) === "update"
|
|
21162
|
+
&& ["installed", "stopping", "starting"].includes(progress.stage) : isUpdateInProgress(this.dataDir);
|
|
20346
21163
|
this.recordWorkingRestartIntent(update ? "update" : "manual_restart");
|
|
20347
21164
|
}
|
|
20348
21165
|
recordWorkingRestartIntent(cause) {
|
|
@@ -20402,6 +21219,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20402
21219
|
const settingsControlStopped = settingsControl?.close();
|
|
20403
21220
|
this.publicLinkAutoOpen?.stop();
|
|
20404
21221
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
21222
|
+
const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
|
|
20405
21223
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
20406
21224
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
20407
21225
|
const needsStopped = this.stopNeedsYou();
|
|
@@ -20440,6 +21258,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20440
21258
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
20441
21259
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
20442
21260
|
await publicStopped;
|
|
21261
|
+
await sharesStopped?.catch(() => { });
|
|
20443
21262
|
await profileStopped;
|
|
20444
21263
|
await needsStopped;
|
|
20445
21264
|
await settingsControlStopped;
|
|
@@ -20792,6 +21611,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20792
21611
|
// mark every row done and tell the user "changes applied".
|
|
20793
21612
|
return { rejected: why };
|
|
20794
21613
|
}
|
|
21614
|
+
this.migrateOfficeStorage();
|
|
21615
|
+
this.migrateNoticeDeliveryStorage();
|
|
20795
21616
|
if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
|
|
20796
21617
|
this.canvasWorkerSettingChanged();
|
|
20797
21618
|
}
|
|
@@ -22340,6 +23161,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22340
23161
|
this.logger.info("No instances to restart");
|
|
22341
23162
|
return;
|
|
22342
23163
|
}
|
|
23164
|
+
this.workingRecoveryGroup = randomUUID();
|
|
22343
23165
|
this.recordWorkingRestartIntent("manual_restart");
|
|
22344
23166
|
this.workingRecovery?.reset();
|
|
22345
23167
|
this.logger.info(`Graceful restart: waiting for ${instanceNames.length} instances to idle...`);
|
|
@@ -22415,29 +23237,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22415
23237
|
}
|
|
22416
23238
|
// General is ready again; now its topic can own the live progress message.
|
|
22417
23239
|
await restartProgress.start(progressTarget);
|
|
22418
|
-
|
|
22419
|
-
|
|
23240
|
+
// The ClassicBot rooms (killed during orphan cleanup) restart with the fleet instances, in the same queue; their
|
|
23241
|
+
// open-lists are refreshed first so a room's inbound is never dropped while it starts.
|
|
23242
|
+
const restartClassic = topicMode ? this.classicChannels?.getAll() ?? [] : [];
|
|
23243
|
+
if (restartClassic.length > 0)
|
|
23244
|
+
this.reregisterClassicChannels();
|
|
23245
|
+
if (restartOthers.length > 0 || restartClassic.length > 0) {
|
|
23246
|
+
await this.startInstancesWithConcurrency(restartOthers, topicMode, () => restartProgress.markReady(), restartClassic);
|
|
22420
23247
|
}
|
|
22421
23248
|
if (topicMode) {
|
|
22422
23249
|
this.routing.rebuild(this.fleetConfig);
|
|
22423
23250
|
this.reregisterClassicChannels();
|
|
22424
23251
|
// startInstance already calls connectIpcToInstance, no need for connectToInstances here
|
|
22425
|
-
// Restart classic channel instances (killed during orphan cleanup)
|
|
22426
|
-
if (this.classicChannels) {
|
|
22427
|
-
const fleetBackend = this.fleetConfig?.defaults?.backend;
|
|
22428
|
-
const channels = this.classicChannels.getAll()
|
|
22429
|
-
.filter(ch => !this.lifecycle.isPaused(ch.instanceName));
|
|
22430
|
-
const concurrency = 3;
|
|
22431
|
-
let idx = 0;
|
|
22432
|
-
while (idx < channels.length) {
|
|
22433
|
-
const batch = channels.slice(idx, idx + concurrency);
|
|
22434
|
-
await Promise.allSettled(batch.map(async (ch) => {
|
|
22435
|
-
if (await this.startClassicInstanceUnattended(ch, "classic instance"))
|
|
22436
|
-
restartProgress.markReady();
|
|
22437
|
-
}));
|
|
22438
|
-
idx += concurrency;
|
|
22439
|
-
}
|
|
22440
|
-
}
|
|
22441
23252
|
for (const name of Object.keys(fleet.instances)) {
|
|
22442
23253
|
this.startStatuslineWatcher(name);
|
|
22443
23254
|
}
|