@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-cli.js +21 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +5 -0
- package/dist/agent-endpoint.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-identity.d.ts +64 -0
- package/dist/backend/kiro-identity.js +101 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- 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.d.ts +71 -2
- package/dist/backend/kiro.js +352 -11
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +59 -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/mcp-tools.js +29 -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 +21 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +53 -10
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +31 -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 +23 -0
- package/dist/daemon.js +106 -7
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +20 -6
- package/dist/dashboard-permissions.js +58 -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-manager.d.ts +72 -0
- package/dist/fleet-manager.js +507 -89
- 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 +14 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +37 -9
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +44 -11
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +19 -0
- package/dist/instance-lifecycle.js +55 -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 +49 -0
- package/dist/kiro-migrate.js +105 -0
- package/dist/kiro-migrate.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 +7 -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 +88 -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 +8 -2
- package/dist/needs-you.js +6 -0
- 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 +258 -0
- package/dist/office-share.js +549 -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 +26 -2
- package/dist/outbound-schemas.js +27 -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/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 +17 -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 +12 -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 +26 -2
- package/dist/transcript-sources.js +103 -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 +28 -8
- package/dist/ui/backend-signin.js +63 -10
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-editor.js +14 -9
- package/dist/ui/fleet-office-layout.js +70 -32
- 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 +7 -2
- package/dist/ui/fleet-office-pack.js +119 -15
- package/dist/ui/fleet-office-skin.js +98 -23
- package/dist/ui/fleet-office-store.js +39 -5
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +172 -12
- package/dist/ui/fleet-office.js +547 -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 +8 -1
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +4 -4
- package/dist/ui/panel-settings.js +37 -11
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +14 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +16 -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 +67 -2
- 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 +18 -0
- package/dist/web-api.js +201 -0
- 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-skins.d.ts +3 -2
- package/dist/web-office-skins.js +3 -2
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.js +40 -7
- 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,6 @@
|
|
|
1
|
+
import { hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
|
|
2
|
+
import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
|
|
3
|
+
import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
|
|
1
4
|
import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
|
|
2
5
|
import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
|
|
3
6
|
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
@@ -19,6 +22,9 @@ import { PublicLinkAutoOpen } from "./public-link-auto-open.js";
|
|
|
19
22
|
import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from "./public-link-owner.js";
|
|
20
23
|
import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
|
|
21
24
|
import { CloudflaredProvider } from "./tunnel/cloudflared.js";
|
|
25
|
+
import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
|
|
26
|
+
import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
|
|
27
|
+
import { ALL_OFFICES } from "./office-share.js";
|
|
22
28
|
import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
|
|
23
29
|
import { ManagedTunnel } from "./tunnel/manager.js";
|
|
24
30
|
import { withinBudget } from "./monotonic-budget.js";
|
|
@@ -28,13 +34,16 @@ import { ProfileControlServer } from "./profile-control.js";
|
|
|
28
34
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, lstatSync, constants as fsConstants, closeSync } from "node:fs";
|
|
29
35
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
30
36
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
31
|
-
import { freemem, totalmem,
|
|
37
|
+
import { freemem, totalmem, homedir } from "node:os";
|
|
38
|
+
import { effectiveCpuCount } from "./cpu-quota.js";
|
|
32
39
|
import { createServer } from "node:http";
|
|
33
40
|
import { Worker } from "node:worker_threads";
|
|
34
41
|
import { ProbeWorkerPool } from "./probe-worker-pool.js";
|
|
35
42
|
import { join, dirname, basename, delimiter, isAbsolute, relative, resolve as resolvePath } from "node:path";
|
|
36
43
|
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { isDeepStrictEqual } from "node:util";
|
|
37
45
|
import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
46
|
+
import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./kiro-migrate.js";
|
|
38
47
|
import { fleetLabel } from "./fleet-label.js";
|
|
39
48
|
import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
|
|
40
49
|
import { formatUpdateProgress } from "./update-progress.js";
|
|
@@ -53,7 +62,7 @@ const __dirname = dirname(__filename);
|
|
|
53
62
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
54
63
|
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
64
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
56
|
-
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
65
|
+
import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
|
|
57
66
|
import { EventLog } from "./event-log.js";
|
|
58
67
|
import { binaryProbe } from "./binary-probe.js";
|
|
59
68
|
import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
|
|
@@ -160,6 +169,7 @@ import { OwnerCssStore } from "./web-owner-css-store.js";
|
|
|
160
169
|
import { ThemeStore } from "./web-theme-store.js";
|
|
161
170
|
import { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
|
|
162
171
|
import { OfficeSkinStore } from "./web-office-skins.js";
|
|
172
|
+
import { OfficeActs } from "./web-office-acts.js";
|
|
163
173
|
import { WebAppearance } from "./web-appearance.js";
|
|
164
174
|
import { WebEffectLibrary } from "./web-effect-library.js";
|
|
165
175
|
import { WebEffectTrials } from "./web-effect-trials.js";
|
|
@@ -189,7 +199,7 @@ import { discordGeneralChannelId } from "./discord-general.js";
|
|
|
189
199
|
import { parseEnvText } from "./env-file.js";
|
|
190
200
|
import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
|
|
191
201
|
import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
|
|
192
|
-
import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
202
|
+
import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
|
|
193
203
|
import { LoginSession } from "./login-manager.js";
|
|
194
204
|
import { runOnLoginShell } from "./login-shell.js";
|
|
195
205
|
import { tightenInstanceDirs } from "./private-dir.js";
|
|
@@ -209,12 +219,16 @@ import { admitProfileLogin, LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOG
|
|
|
209
219
|
import { WebBackendLogins, dashboardSignInSupported } from "./web-backend-login.js";
|
|
210
220
|
import { runBeforeDeadline } from "./deadline.js";
|
|
211
221
|
import { LoginWindowLock } from "./login-window-lock.js";
|
|
222
|
+
import { BrowserBackendSignIns } from "./browser-backend-signins.js";
|
|
223
|
+
import { createStructuredSignInTransport } from "./structured-signin-transport.js";
|
|
224
|
+
import { ONBOARDING_BACKENDS, readPendingBackendSignIns } from "./backend-signin-pending.js";
|
|
225
|
+
import { structuredSignInSupported } from "./structured-signin.js";
|
|
212
226
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
213
227
|
import { setLocale, detectLocale, getLocale, t } from "./locale.js";
|
|
214
228
|
import { recentChatContext } from "./log-tail.js";
|
|
215
229
|
import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
|
|
216
230
|
import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
217
|
-
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
231
|
+
import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
|
|
218
232
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
219
233
|
import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
|
|
220
234
|
import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
|
|
@@ -272,6 +286,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
|
|
|
272
286
|
/** A self-restart is a whole service restart; 120s is the apply budget, not this. */
|
|
273
287
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
274
288
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
289
|
+
import { kiroHome } from "./backend/kiro-kas-store.js";
|
|
275
290
|
import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
276
291
|
import { carryClaudeConversation } from "./backend/claude-carry.js";
|
|
277
292
|
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
@@ -469,6 +484,10 @@ const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
|
|
|
469
484
|
const NEEDS_PAUSE_TTL_MS = 10_000;
|
|
470
485
|
const HANG_CALLBACK_PREFIX = "hang:";
|
|
471
486
|
const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
|
|
487
|
+
/** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
|
|
488
|
+
const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
|
|
489
|
+
/** Long enough for an admin to read the prompt; the move is judged again on the click. */
|
|
490
|
+
const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
|
|
472
491
|
/**
|
|
473
492
|
* The prompts the web dashboard also offers: the ones about an instance's own health, which a dashboard
|
|
474
493
|
* user — holding the full-fleet web credential — may answer exactly as a fleet admin on the platform may.
|
|
@@ -564,6 +583,7 @@ export const CLI_ENV_PROBE_DEADLINE_MS = 16_000;
|
|
|
564
583
|
* started ten CLIs together, saturated the CPU, and healthy starts then missed
|
|
565
584
|
* their startup budget — which used to cost the user their conversation. Cores
|
|
566
585
|
* bound how many can actually make progress; memory bounds how many fit.
|
|
586
|
+
* `cores` is what this process may use (cpuset and cgroup quota, #749), not the host's count.
|
|
567
587
|
*/
|
|
568
588
|
export function deriveSpawnConcurrency(freeMemMB, cores) {
|
|
569
589
|
const byMemory = Math.floor(freeMemMB / 300);
|
|
@@ -610,6 +630,7 @@ export class FleetManager {
|
|
|
610
630
|
workingRecovery = null;
|
|
611
631
|
workingRecoveryTimer = null;
|
|
612
632
|
workingRecoveryCause = "unknown_interruption";
|
|
633
|
+
workingRecoveryGroup = this.managerBootId;
|
|
613
634
|
recoveryEnabled(name) {
|
|
614
635
|
const own = this.fleetConfig?.instances[name]?.resume_interrupted_work;
|
|
615
636
|
return own ?? this.fleetConfig?.defaults?.resume_interrupted_work ?? true;
|
|
@@ -650,7 +671,16 @@ export class FleetManager {
|
|
|
650
671
|
const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
|
|
651
672
|
&& (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
|
|
652
673
|
.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.`));
|
|
674
|
+
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.`));
|
|
675
|
+
if (posted) {
|
|
676
|
+
const counts = { interrupted: rows.length, queued: rows.filter(row => row.outcome === "admitted").length,
|
|
677
|
+
excluded: rows.filter(row => row.outcome === "excluded").length, deferred: rows.filter(row => row.outcome === "deferred").length };
|
|
678
|
+
const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
|
|
679
|
+
this.logger.info({ group, ...counts, observedCause: "unknown_interruption", requestedAction }, "Interrupted work recovery summary queued for General");
|
|
680
|
+
this.eventLog?.insert(general, "working_state", { reason: "recovery_summary", group, ...counts,
|
|
681
|
+
observedCause: "unknown_interruption", requestedAction,
|
|
682
|
+
message: `Interrupted work summary queued for General: ${counts.interrupted} interrupted; ${counts.queued} queued, ${counts.excluded} excluded, ${counts.deferred} deferred.` });
|
|
683
|
+
}
|
|
654
684
|
},
|
|
655
685
|
log: (episode, outcome, reason) => {
|
|
656
686
|
const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
|
|
@@ -881,6 +911,8 @@ export class FleetManager {
|
|
|
881
911
|
pendingBackendSelects = new Map();
|
|
882
912
|
/** nonce → pending button prompt (hang restart, interactive assist, clean-exit restart). */
|
|
883
913
|
pendingNonceButtons = new Map();
|
|
914
|
+
/** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
|
|
915
|
+
pendingKiroMigrations = new Map();
|
|
884
916
|
/** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
|
|
885
917
|
needsYou = null;
|
|
886
918
|
/** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
|
|
@@ -1147,6 +1179,98 @@ export class FleetManager {
|
|
|
1147
1179
|
officeSkinWrites = new OfficeWriteLimiter(undefined, { perWindow: 10, windowMs: 60_000 });
|
|
1148
1180
|
/** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
|
|
1149
1181
|
officeWrites = new OfficeWriteLimiter();
|
|
1182
|
+
officeSharesInstance;
|
|
1183
|
+
/**
|
|
1184
|
+
* #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
|
|
1185
|
+
* their own (a manager and lease of its own: never the fleet's one tunnel lane nor the public link's preview tunnel).
|
|
1186
|
+
*/
|
|
1187
|
+
get officeShares() {
|
|
1188
|
+
return (this.officeSharesInstance ??= new OfficeShareService({
|
|
1189
|
+
source: {
|
|
1190
|
+
status: () => this.getUiStatus(),
|
|
1191
|
+
org: () => this.orgChart(),
|
|
1192
|
+
offices: () => this.officeStore.list(),
|
|
1193
|
+
acts: () => this.officeActs.live(),
|
|
1194
|
+
traffic: () => this.trafficSnapshot(),
|
|
1195
|
+
needs: () => this.needsYouItems().map(i => i.instance),
|
|
1196
|
+
hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
|
|
1197
|
+
skins: this.officeSkins,
|
|
1198
|
+
},
|
|
1199
|
+
manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
|
|
1200
|
+
ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
|
|
1201
|
+
provider: binary => new CloudflaredProvider({ binaryName: binary, protocol: this.fleetConfig?.web?.public_link?.protocol ?? "http2" }),
|
|
1202
|
+
config: () => this.fleetConfig?.web?.office_share,
|
|
1203
|
+
officeExists: id => this.officeStore.list().offices.some(o => o.id === id),
|
|
1204
|
+
ownerCurrent: owner => this.officeShareOwnerCurrent(owner),
|
|
1205
|
+
changed: () => this.emitSseEvent("office_shares", {}),
|
|
1206
|
+
log: (event, detail) => this.logger.info({ event, ...(detail ? { detail } : {}) }, "Office share link"),
|
|
1207
|
+
}));
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* #2636: a share made or ended — said in General, as a sign-in is (who, which office, for how long; never its URL).
|
|
1211
|
+
* `share` null with `count`: "Revoke all".
|
|
1212
|
+
*/
|
|
1213
|
+
officeShareNotice(event, who, share, count) {
|
|
1214
|
+
const office = !share ? "" : share.office === ALL_OFFICES ? t("office_share.all") : this.officeStore.list().offices.find(o => o.id === share.office)?.name ?? share.office;
|
|
1215
|
+
const minutes = share ? Math.round((share.expiresAt - share.createdAt) / 60_000) : 0;
|
|
1216
|
+
const text = event === "created" ? t("office_share.created_notice", who, office, minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`)
|
|
1217
|
+
: share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
|
|
1218
|
+
this.notifyFleetError(text, { throttle: false });
|
|
1219
|
+
}
|
|
1220
|
+
/** A share's owner: a person (`person:<adapter>:<user>`) still a fleet admin there; the host's own admin session stays. */
|
|
1221
|
+
officeShareOwnerCurrent(owner) {
|
|
1222
|
+
const m = /^person:([^:]+):(.+)$/.exec(owner);
|
|
1223
|
+
if (!m)
|
|
1224
|
+
return owner === "admin-session";
|
|
1225
|
+
return this.hasFleetAdmins(m[1]) && this.isFleetAdmin(m[2], m[1]);
|
|
1226
|
+
}
|
|
1227
|
+
officeActsInstance;
|
|
1228
|
+
/** #2578: the agents' acts in the Office view (office_view / office_set), memory only; every change is `office_acts`. */
|
|
1229
|
+
get officeActs() {
|
|
1230
|
+
return (this.officeActsInstance ??= new OfficeActs({
|
|
1231
|
+
offices: () => this.officeStore.list().offices,
|
|
1232
|
+
pack: id => this.officeSkins.pack(id),
|
|
1233
|
+
chart: () => { const c = this.orgChart(); return { names: Object.keys(c.instances), teams: c.teams }; },
|
|
1234
|
+
people: () => this.officePeople(),
|
|
1235
|
+
changed: acts => this.emitSseEvent("office_acts", { acts, now: Date.now() }),
|
|
1236
|
+
}));
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* #2578: each instance's state as the org chart shows it (fleet-org.js orgState, from the same fields the status
|
|
1240
|
+
* frame and the Needs-you list carry) and its display name (resolveInstanceIdentity, as getUiStatus) — read once.
|
|
1241
|
+
*/
|
|
1242
|
+
officePeople() {
|
|
1243
|
+
const needs = new Map();
|
|
1244
|
+
for (const item of this.needsYouItems())
|
|
1245
|
+
needs.set(item.instance, (needs.get(item.instance) ?? 0) + 1);
|
|
1246
|
+
const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
|
|
1247
|
+
let profiles = null;
|
|
1248
|
+
return {
|
|
1249
|
+
stateOf: name => {
|
|
1250
|
+
if (!this.isKnownInstance(name))
|
|
1251
|
+
return "missing";
|
|
1252
|
+
const p = this.instancePresentation(name), status = this.getInstanceStatus(name);
|
|
1253
|
+
if (p.state === "awaiting_input" || (needs.get(name) ?? 0) > 0)
|
|
1254
|
+
return "needs";
|
|
1255
|
+
if (p.startup_phase === "queued" || p.startup_phase === "starting")
|
|
1256
|
+
return p.startup_phase;
|
|
1257
|
+
if (status === "crashed" || status === "paused")
|
|
1258
|
+
return status;
|
|
1259
|
+
if (status !== "running")
|
|
1260
|
+
return "stopped";
|
|
1261
|
+
return p.execution_state === "stuck" || p.execution_state === "working" ? p.execution_state : "idle";
|
|
1262
|
+
},
|
|
1263
|
+
displayName: name => {
|
|
1264
|
+
try {
|
|
1265
|
+
profiles ??= profileIdentities(this.dataDir);
|
|
1266
|
+
return resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic: classicRooms.get(name), profile: profiles.get(name) }).display_name || undefined;
|
|
1267
|
+
}
|
|
1268
|
+
catch {
|
|
1269
|
+
return undefined;
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1150
1274
|
/** #1629: the theme tools, a person's reset and lock, and the one appearance rate limiter (shared with effects). */
|
|
1151
1275
|
get webAppearance() {
|
|
1152
1276
|
return (this.webAppearanceService ??= new WebAppearance(this.themeStore, () => this.fleetConfig?.web ?? null, () => { try {
|
|
@@ -1672,6 +1796,74 @@ export class FleetManager {
|
|
|
1672
1796
|
// No platform confirmation was reachable. Host inspection is still required.
|
|
1673
1797
|
this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
|
|
1674
1798
|
}
|
|
1799
|
+
kiroMigrateDeps() {
|
|
1800
|
+
return {
|
|
1801
|
+
agendHome: getAgendHome(),
|
|
1802
|
+
fleetConfig: () => this.fleetConfig,
|
|
1803
|
+
saveFleetConfig: () => this.saveFleetConfig(),
|
|
1804
|
+
status: name => this.getInstanceStatus(name),
|
|
1805
|
+
restart: name => this.restartSingleInstance(name, { explicit: true }),
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* #2598 B2, migrate_kiro_instance: judge the move, then ask in General for a fleet admin's Confirm. Nothing changes
|
|
1810
|
+
* here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
|
|
1811
|
+
*/
|
|
1812
|
+
async requestKiroMigration(name, startFresh) {
|
|
1813
|
+
const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
|
|
1814
|
+
if (!judged.ok)
|
|
1815
|
+
return judged;
|
|
1816
|
+
const general = this.findGeneralInstance();
|
|
1817
|
+
const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
|
|
1818
|
+
const adapter = general ? this.getAdapterForInstance(general) : undefined;
|
|
1819
|
+
const chatId = general ? this.getGroupIdForInstance(general) : undefined;
|
|
1820
|
+
const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
|
|
1821
|
+
if (!general || !adapterId || !adapter || !chatId)
|
|
1822
|
+
return { ok: false, error: t("kiro_migrate.no_general") };
|
|
1823
|
+
const { plan } = judged;
|
|
1824
|
+
try {
|
|
1825
|
+
const nonce = await this.postNonceButtonPromptOrThrow({
|
|
1826
|
+
prefix: KIRO_MIGRATE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name, adapter, adapterId, chatId,
|
|
1827
|
+
...(topic ? { threadId: topic } : {}),
|
|
1828
|
+
message: kiroMigrationPrompt(plan, this.getInstanceStatus(name) === "running"),
|
|
1829
|
+
choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
|
|
1830
|
+
expiredText: t("kiro_migrate.expired", name),
|
|
1831
|
+
timeoutMs: KIRO_MIGRATE_TIMEOUT_MS,
|
|
1832
|
+
});
|
|
1833
|
+
this.pendingKiroMigrations.set(nonce, plan);
|
|
1834
|
+
const expiry = setTimeout(() => this.pendingKiroMigrations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000);
|
|
1835
|
+
expiry.unref?.();
|
|
1836
|
+
}
|
|
1837
|
+
catch (err) {
|
|
1838
|
+
return { ok: false, error: `${t("kiro_migrate.no_general")} (${err.message})` };
|
|
1839
|
+
}
|
|
1840
|
+
return { ok: true, text: t("kiro_migrate.requested"), classic_conversation: plan.classicId };
|
|
1841
|
+
}
|
|
1842
|
+
/** A fleet admin's Confirm or Cancel on a kiro move. Any fleet admin (the default nonce authorization), in General. */
|
|
1843
|
+
async handleKiroMigrateCallback(data, adapterId, adapter) {
|
|
1844
|
+
const claimed = this.consumeNonceCallback(KIRO_MIGRATE_CALLBACK_PREFIX, /^kiro-migrate:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
|
|
1845
|
+
if (claimed === null)
|
|
1846
|
+
return false;
|
|
1847
|
+
if (claimed === "consumed")
|
|
1848
|
+
return true;
|
|
1849
|
+
const { entry, action } = claimed;
|
|
1850
|
+
const nonce = data.callbackData.slice(KIRO_MIGRATE_CALLBACK_PREFIX.length, KIRO_MIGRATE_CALLBACK_PREFIX.length + 32);
|
|
1851
|
+
const plan = this.pendingKiroMigrations.get(nonce);
|
|
1852
|
+
this.pendingKiroMigrations.delete(nonce);
|
|
1853
|
+
const messageId = entry.messageId ?? data.messageId;
|
|
1854
|
+
if (action === "cancel" || !plan) {
|
|
1855
|
+
await this.retireNonceButtons(entry, messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
|
|
1856
|
+
return true;
|
|
1857
|
+
}
|
|
1858
|
+
this.eventLog?.insert(plan.name, "kiro_migrate", { classicId: plan.classicId, userId: data.userId });
|
|
1859
|
+
await this.retireNonceButtons(entry, messageId, t("kiro_migrate.applying", plan.name));
|
|
1860
|
+
const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
|
|
1861
|
+
this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
|
|
1862
|
+
this.notifyFleetError(outcome.text);
|
|
1863
|
+
if (outcome.ok)
|
|
1864
|
+
this.notifyInstanceTopic(plan.name, outcome.text);
|
|
1865
|
+
return true;
|
|
1866
|
+
}
|
|
1675
1867
|
async handleSettingsChangeCallback(data, adapterId, adapter) {
|
|
1676
1868
|
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
1869
|
if (!result)
|
|
@@ -2218,7 +2410,7 @@ export class FleetManager {
|
|
|
2218
2410
|
if (reason === "cancelled")
|
|
2219
2411
|
outbox.workingRecovery.cancel(name, observation.configuration, observation.sessionId);
|
|
2220
2412
|
if (entry.prior)
|
|
2221
|
-
outbox.workingRecovery.discover(name, entry.prior);
|
|
2413
|
+
outbox.workingRecovery.discover(name, entry.prior, this.workingRecoveryGroup);
|
|
2222
2414
|
const state = entry.current.observation.executionState ?? "unknown";
|
|
2223
2415
|
const counts = { tasks: entry.current.references.filter(r => r.kind === "task").length,
|
|
2224
2416
|
owed: entry.current.references.filter(r => r.kind === "owed").length,
|
|
@@ -2525,7 +2717,7 @@ export class FleetManager {
|
|
|
2525
2717
|
const explicit = this.fleetConfig?.defaults?.startup?.concurrency;
|
|
2526
2718
|
if (explicit != null)
|
|
2527
2719
|
return Math.max(1, Math.min(20, explicit));
|
|
2528
|
-
return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)),
|
|
2720
|
+
return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)), effectiveCpuCount());
|
|
2529
2721
|
}
|
|
2530
2722
|
/** #1685: "elevated" or "critical" while host memory pressure slows or holds new agents; otherwise null (an
|
|
2531
2723
|
* unknown macOS reading — the advisory case — is unknown, so null too). */
|
|
@@ -2742,8 +2934,9 @@ export class FleetManager {
|
|
|
2742
2934
|
// the next tunnel and says so, it does not block the fleet.
|
|
2743
2935
|
this.announceToolPermissionsChange();
|
|
2744
2936
|
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
|
-
|
|
2937
|
+
// #1723: the public link's preview tunnel keeps its own lease beside it; a leftover one is reaped the same way (and
|
|
2938
|
+
// #2636's Office share tunnel's).
|
|
2939
|
+
for (const leaseDir of [this.dataDir, join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR), join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR)]) {
|
|
2747
2940
|
void reapStaleTunnel(leaseDir)
|
|
2748
2941
|
.then(outcome => {
|
|
2749
2942
|
if (outcome.kind === "manual")
|
|
@@ -3308,7 +3501,7 @@ export class FleetManager {
|
|
|
3308
3501
|
await data.respond(t("permission.denied"));
|
|
3309
3502
|
return;
|
|
3310
3503
|
}
|
|
3311
|
-
this.fleetConfig
|
|
3504
|
+
setNoticeMode(this.fleetConfig, "tips", mode === "on");
|
|
3312
3505
|
this.saveFleetConfig();
|
|
3313
3506
|
await data.respond(t(mode === "on" ? "tips.enabled" : "tips.disabled"));
|
|
3314
3507
|
return;
|
|
@@ -5098,6 +5291,7 @@ export class FleetManager {
|
|
|
5098
5291
|
}
|
|
5099
5292
|
classicBehaviorUpdate(instanceName) {
|
|
5100
5293
|
return {
|
|
5294
|
+
reply_triggers: this.classicChannels?.getReplyTriggersByInstance(instanceName, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS,
|
|
5101
5295
|
tool_progress: this.classicChannels?.getToolProgressByInstance(instanceName, this.fleetConfig?.defaults?.tool_progress) ?? "off",
|
|
5102
5296
|
reply_completion_guard: this.classicChannels?.getReplyCompletionGuardByInstance(instanceName, this.fleetConfig?.defaults?.reply_completion_guard) ?? true,
|
|
5103
5297
|
};
|
|
@@ -5396,36 +5590,40 @@ export class FleetManager {
|
|
|
5396
5590
|
* config file races. Stagger applies between outer lifecycle admissions.
|
|
5397
5591
|
* TODO: per-instance startup timeout (existing issue, not introduced here)
|
|
5398
5592
|
*/
|
|
5399
|
-
async startInstancesWithConcurrency(entries, topicMode, onReady) {
|
|
5593
|
+
async startInstancesWithConcurrency(entries, topicMode, onReady, classic = []) {
|
|
5400
5594
|
// Persisted pauses are intentionally preserved across fleet restarts. Filter
|
|
5401
5595
|
// them before grouping/staggering: startInstance() retains its own guard as
|
|
5402
5596
|
// a final backstop, but putting a no-op entry in this queue still consumes a
|
|
5403
5597
|
// full stagger slot for every distinct working directory.
|
|
5404
5598
|
const runnableEntries = entries.filter(([name]) => !this.lifecycle.isPaused(name));
|
|
5599
|
+
// ClassicBot rooms queue with the fleet instances, at the same pressure pacing (#2642 follow-up): they used to start
|
|
5600
|
+
// only after the whole fleet, the chat connections, topics and icons. A name the fleet also configures is the fleet's.
|
|
5601
|
+
const fleetNames = new Set(entries.map(([name]) => name));
|
|
5602
|
+
const runnableClassic = classic.filter(ch => !this.lifecycle.isPaused(ch.instanceName) && !fleetNames.has(ch.instanceName));
|
|
5405
5603
|
const pausedCount = entries.length - runnableEntries.length;
|
|
5406
5604
|
if (pausedCount > 0) {
|
|
5407
5605
|
this.logger.info({ pausedCount }, "Paused instances excluded from startup queue");
|
|
5408
5606
|
}
|
|
5409
|
-
if (runnableEntries.length === 0)
|
|
5607
|
+
if (runnableEntries.length === 0 && runnableClassic.length === 0)
|
|
5410
5608
|
return;
|
|
5411
5609
|
if (this.fleetConfig?.defaults?.startup?.concurrency == null) {
|
|
5412
5610
|
this.logger.info({
|
|
5413
5611
|
concurrency: this.spawnConcurrency(),
|
|
5414
5612
|
freeMemMB: Math.round(freemem() / (1024 * 1024)),
|
|
5415
|
-
totalInstances: runnableEntries.length,
|
|
5613
|
+
totalInstances: runnableEntries.length + runnableClassic.length,
|
|
5416
5614
|
}, "Adaptive startup concurrency");
|
|
5417
5615
|
}
|
|
5418
5616
|
const startedAt = performance.now();
|
|
5419
5617
|
let active = 0, peakConcurrentStarts = 0, readyInstances = 0;
|
|
5420
|
-
|
|
5618
|
+
const admit = (name, workingDirectory, start) => this.spawnGate.run({
|
|
5421
5619
|
instanceName: name,
|
|
5422
|
-
workingDirectory
|
|
5620
|
+
workingDirectory,
|
|
5423
5621
|
reason: "startup",
|
|
5424
5622
|
stage: "lifecycle",
|
|
5425
5623
|
}, async () => {
|
|
5426
5624
|
peakConcurrentStarts = Math.max(peakConcurrentStarts, ++active);
|
|
5427
5625
|
try {
|
|
5428
|
-
if (await
|
|
5626
|
+
if (await start()) {
|
|
5429
5627
|
readyInstances++;
|
|
5430
5628
|
onReady?.(name);
|
|
5431
5629
|
}
|
|
@@ -5433,9 +5631,13 @@ export class FleetManager {
|
|
|
5433
5631
|
finally {
|
|
5434
5632
|
active--;
|
|
5435
5633
|
}
|
|
5436
|
-
})
|
|
5634
|
+
});
|
|
5635
|
+
await Promise.all([
|
|
5636
|
+
...runnableEntries.map(([name, config]) => admit(name, config.working_directory, () => this.startInstanceUnattended(name, config, topicMode, "instance"))),
|
|
5637
|
+
...runnableClassic.map(ch => admit(ch.instanceName, join(getAgendHome(), "workspaces", ch.instanceName), () => this.startClassicInstanceUnattended(ch, "classic instance"))),
|
|
5638
|
+
]);
|
|
5437
5639
|
this.logger.info({
|
|
5438
|
-
totalInstances: runnableEntries.length, readyInstances,
|
|
5640
|
+
totalInstances: runnableEntries.length + runnableClassic.length, classicInstances: runnableClassic.length, readyInstances,
|
|
5439
5641
|
elapsedSeconds: Math.round((performance.now() - startedAt) / 100) / 10,
|
|
5440
5642
|
peakConcurrentStarts,
|
|
5441
5643
|
}, "Fleet startup batch completed");
|
|
@@ -6855,9 +7057,22 @@ export class FleetManager {
|
|
|
6855
7057
|
...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
|
|
6856
7058
|
}).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
|
|
6857
7059
|
// #2348: `explicit` — grants are the scope (default deny); false: the derived scope of #1920, until grants are saved.
|
|
6858
|
-
|
|
7060
|
+
// #2667: the ClassicBot rooms there are now, apart from fleet.yaml's instances (a grant may name either: guestReach),
|
|
7061
|
+
// with its display name as the sidebar resolves it (a profile alias, else the room's own), when it has one.
|
|
7062
|
+
const profiles = profileIdentities(this.dataDir);
|
|
7063
|
+
const rooms = (this.classicChannels?.getAll() ?? []).filter((r) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, r.instanceName))
|
|
7064
|
+
.map((r) => {
|
|
7065
|
+
const shown = resolveInstanceIdentity({ classic: r, profile: profiles.get(r.instanceName) }).display_name;
|
|
7066
|
+
return { name: r.instanceName, ...(shown && shown !== r.instanceName ? { display_name: shown } : {}) };
|
|
7067
|
+
})
|
|
7068
|
+
.sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
|
|
7069
|
+
return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), rooms, connections, guests,
|
|
6859
7070
|
audit: this.guestAudit() };
|
|
6860
7071
|
}
|
|
7072
|
+
/** #2667: the ClassicBot rooms' instance names now — what a settings save's validation knows besides fleet.yaml's instances. */
|
|
7073
|
+
classicRoomNames() {
|
|
7074
|
+
return (this.classicChannels?.getAll() ?? []).map((r) => r.instanceName);
|
|
7075
|
+
}
|
|
6861
7076
|
/**
|
|
6862
7077
|
* #2499 review: the guest audit an admin reads — the newest grant changes and admin-sent codes from the event log
|
|
6863
7078
|
* (`fleet` rows: no agent's Log shows them). Through guestOverview only: Settings' admin gate, never over the public
|
|
@@ -7312,7 +7527,8 @@ export class FleetManager {
|
|
|
7312
7527
|
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
7528
|
return;
|
|
7314
7529
|
}
|
|
7315
|
-
|
|
7530
|
+
// Never by DM (hmsg-3b8ac2af: Telegram cannot DM a user who has not started the bot); a legacy "dm" reads as on.
|
|
7531
|
+
if (!loginNoticeMode(this.fleetConfig?.web))
|
|
7316
7532
|
return;
|
|
7317
7533
|
// The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
|
|
7318
7534
|
// sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
|
|
@@ -7686,7 +7902,8 @@ export class FleetManager {
|
|
|
7686
7902
|
}
|
|
7687
7903
|
}
|
|
7688
7904
|
async startAllWithinDeadline(configPath, deadline) {
|
|
7689
|
-
this.
|
|
7905
|
+
if (!this.structuredLoginCleanupFailed)
|
|
7906
|
+
this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
|
|
7690
7907
|
this.loginController?.reopen();
|
|
7691
7908
|
const startupStartedAt = Date.now();
|
|
7692
7909
|
FleetManager.signalTarget = this;
|
|
@@ -7947,12 +8164,7 @@ export class FleetManager {
|
|
|
7947
8164
|
mkdirSync(generalDir, { recursive: true, mode: 0o700 });
|
|
7948
8165
|
const backendName = fleet.defaults.backend ?? "claude-code";
|
|
7949
8166
|
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
|
-
};
|
|
8167
|
+
fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
|
|
7956
8168
|
generalsCreated = true;
|
|
7957
8169
|
}
|
|
7958
8170
|
if (generalsCreated)
|
|
@@ -8098,9 +8310,11 @@ export class FleetManager {
|
|
|
8098
8310
|
this.rotateAllInstanceLogs();
|
|
8099
8311
|
}, 24 * 60 * 60_000);
|
|
8100
8312
|
this.logRotateTimer.unref?.();
|
|
8101
|
-
// Phase 2: Start remaining instances with staggered concurrency
|
|
8102
|
-
|
|
8103
|
-
|
|
8313
|
+
// Phase 2: Start remaining instances, and the ClassicBot rooms with them, with staggered concurrency. A room needs no
|
|
8314
|
+
// chat connection to spawn: its open-list is set above (reregisterClassicChannels) and inbound queues until it is up.
|
|
8315
|
+
const classicAtBoot = topicMode && (fleet.channel || fleet.channels?.length) ? this.classicChannels?.getAll() ?? [] : [];
|
|
8316
|
+
if (others.length > 0 || classicAtBoot.length > 0) {
|
|
8317
|
+
await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
|
|
8104
8318
|
}
|
|
8105
8319
|
if (topicMode && (fleet.channel || fleet.channels?.length)) {
|
|
8106
8320
|
await adapterStartup;
|
|
@@ -8142,22 +8356,7 @@ export class FleetManager {
|
|
|
8142
8356
|
// IPC is already wired by startInstancesWithConcurrency → startInstance →
|
|
8143
8357
|
// connectIpcToInstance. The previous 3s sleep + connectToInstances loop
|
|
8144
8358
|
// 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
|
-
}
|
|
8359
|
+
// The ClassicBot rooms started in Phase 2, with the fleet.
|
|
8161
8360
|
for (const name of Object.keys(fleet.instances)) {
|
|
8162
8361
|
this.startStatuslineWatcher(name);
|
|
8163
8362
|
}
|
|
@@ -8424,24 +8623,42 @@ export class FleetManager {
|
|
|
8424
8623
|
if (channelConfigs.length === 0)
|
|
8425
8624
|
return;
|
|
8426
8625
|
// Start ALL adapters in parallel — any single failure doesn't block others.
|
|
8427
|
-
const
|
|
8626
|
+
const settled = channelConfigs.map(() => false);
|
|
8627
|
+
// Each connection is recorded (connected, or retrying with its retry scheduled) as its own login settles.
|
|
8628
|
+
const recorded = channelConfigs.map((cfg, i) => (i === 0
|
|
8428
8629
|
? this.startSingleAdapter(fleet, cfg)
|
|
8429
|
-
: this.startAdditionalAdapter(cfg)))
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
}
|
|
8630
|
+
: this.startAdditionalAdapter(cfg)).then(() => ({ status: "fulfilled" }), (reason) => ({ status: "rejected", reason }))
|
|
8631
|
+
.then(result => { settled[i] = true; this.recordAdapterStartup(cfg, result); })
|
|
8632
|
+
.catch(err => this.logger.error({ err }, "Recording a chat connection's startup failed")));
|
|
8633
|
+
// #2642: bounded. Startup continues past a connection still logging in after this long (ClassicBot instances,
|
|
8634
|
+
// READY); that connection is recorded, or retried, when its login settles, and joins the fleet as it would anyway.
|
|
8635
|
+
let timer;
|
|
8636
|
+
const inTime = await Promise.race([
|
|
8637
|
+
Promise.all(recorded).then(() => true),
|
|
8638
|
+
new Promise(resolve => { timer = setTimeout(() => resolve(false), FleetManager.ADAPTER_STARTUP_WAIT_MS); timer.unref?.(); }),
|
|
8639
|
+
]).finally(() => clearTimeout(timer));
|
|
8640
|
+
if (!inTime) {
|
|
8641
|
+
this.logger.warn({
|
|
8642
|
+
pending: channelConfigs.filter((_, i) => !settled[i]).map(cfg => cfg.id ?? cfg.type),
|
|
8643
|
+
waitedMs: FleetManager.ADAPTER_STARTUP_WAIT_MS,
|
|
8644
|
+
}, "Chat connection still connecting — fleet startup continues without waiting for it");
|
|
8645
|
+
}
|
|
8646
|
+
}
|
|
8647
|
+
/** How long fleet startup waits for its chat connections before it continues without the slow ones (#2642). */
|
|
8648
|
+
static ADAPTER_STARTUP_WAIT_MS = 60_000;
|
|
8649
|
+
/** Track a connection's startup outcome; a failure gets the background retry. */
|
|
8650
|
+
recordAdapterStartup(cfg, result) {
|
|
8651
|
+
const adapterId = cfg.id ?? cfg.type;
|
|
8652
|
+
if (result.status === "fulfilled") {
|
|
8653
|
+
this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
|
|
8654
|
+
return;
|
|
8444
8655
|
}
|
|
8656
|
+
const err = result.reason;
|
|
8657
|
+
this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
|
|
8658
|
+
this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
|
|
8659
|
+
this.scheduleAdapterRetry(adapterId);
|
|
8660
|
+
// Notify admin via whichever adapter is already up
|
|
8661
|
+
this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
|
|
8445
8662
|
}
|
|
8446
8663
|
/** Keep trying while the fleet runs, with a five-minute cap even after a long outage. */
|
|
8447
8664
|
scheduleAdapterRetry(adapterId) {
|
|
@@ -11122,7 +11339,7 @@ export class FleetManager {
|
|
|
11122
11339
|
if (!ipc || !fleetRequestId)
|
|
11123
11340
|
return;
|
|
11124
11341
|
const type = String(msg.type);
|
|
11125
|
-
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) ? "fleet_web_theme_response"
|
|
11342
|
+
const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" ? "fleet_web_theme_response"
|
|
11126
11343
|
: type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
|
|
11127
11344
|
: type.startsWith("fleet_decision_") ? "fleet_decision_response"
|
|
11128
11345
|
: type === "fleet_task" ? "fleet_task_response"
|
|
@@ -11176,7 +11393,8 @@ export class FleetManager {
|
|
|
11176
11393
|
|| type === "fleet_get_web_appearance" || type === "fleet_preview_web_appearance"
|
|
11177
11394
|
|| type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
|
|
11178
11395
|
|| type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
|
|
11179
|
-
|| type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
|
|
11396
|
+
|| type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
|
|
11397
|
+
|| type === "fleet_office_view" || type === "fleet_office_set") {
|
|
11180
11398
|
this.handleWebTheme(name, msg);
|
|
11181
11399
|
return;
|
|
11182
11400
|
}
|
|
@@ -11197,7 +11415,8 @@ export class FleetManager {
|
|
|
11197
11415
|
const result = tool === "get_web_appearance" || tool === "preview_web_appearance"
|
|
11198
11416
|
? this.appearanceTools.handle(instanceName, tool, args)
|
|
11199
11417
|
: tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
|
|
11200
|
-
: this.
|
|
11418
|
+
: tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
|
|
11419
|
+
: this.webAppearance.handleTool(instanceName, tool, args);
|
|
11201
11420
|
ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
|
|
11202
11421
|
}
|
|
11203
11422
|
catch (err) {
|
|
@@ -11633,6 +11852,8 @@ export class FleetManager {
|
|
|
11633
11852
|
return this.appearanceTools.handle(instance, tool, args);
|
|
11634
11853
|
if (tool === "export_web_mod" || tool === "propose_web_mod")
|
|
11635
11854
|
return this.agentMods.handle(instance, tool, args);
|
|
11855
|
+
if (tool === "office_view" || tool === "office_set")
|
|
11856
|
+
return this.officeActs.handle(instance, tool, args); // #2578
|
|
11636
11857
|
return this.webAppearance.handleTool(instance, tool, args);
|
|
11637
11858
|
}
|
|
11638
11859
|
async handlePreviewEmojisHttp(instance, args) {
|
|
@@ -12067,6 +12288,24 @@ export class FleetManager {
|
|
|
12067
12288
|
return;
|
|
12068
12289
|
if (!this.savedFleetConfigSnapshot)
|
|
12069
12290
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
12291
|
+
normalizeNoticeDelivery(this.fleetConfig);
|
|
12292
|
+
normalizeOfficeStorage(this.fleetConfig);
|
|
12293
|
+
explicitPatches = explicitPatches.flatMap(patch => {
|
|
12294
|
+
const path = patch.path;
|
|
12295
|
+
if (path.at(-2) === "dashboard" && path.at(-1) === "office" && !patch.remove) {
|
|
12296
|
+
return [{ ...patch, path: [...path.slice(0, -2), "office_access"] }, { path, remove: true }];
|
|
12297
|
+
}
|
|
12298
|
+
if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.hasOwn(patch.value, "office")) {
|
|
12299
|
+
const { office, ...dashboard } = patch.value;
|
|
12300
|
+
return [{ ...patch, value: dashboard }, { path: [...path.slice(0, -1), "office_access"], value: office, remove: office === null }];
|
|
12301
|
+
}
|
|
12302
|
+
// #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
|
|
12303
|
+
if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
|
|
12304
|
+
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "login"], remove: true }];
|
|
12305
|
+
if (patch.path.join(".") === "defaults.tips" && patch.value === "dm")
|
|
12306
|
+
return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "tips"], remove: true }];
|
|
12307
|
+
return [patch];
|
|
12308
|
+
});
|
|
12070
12309
|
// Re-read immediately before patching so an unrelated concurrent/manual
|
|
12071
12310
|
// edit is retained. Invalid concurrent YAML is never overwritten.
|
|
12072
12311
|
const source = existsSync(this.configPath) ? readFileSync(this.configPath, "utf-8") : "{}\n";
|
|
@@ -12142,9 +12381,46 @@ export class FleetManager {
|
|
|
12142
12381
|
},
|
|
12143
12382
|
});
|
|
12144
12383
|
this.rawFleetConfig = loadRawFleetConfig(this.configPath);
|
|
12384
|
+
// The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
|
|
12385
|
+
// leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
|
|
12386
|
+
// Keep the FleetConfig object itself: Settings and other request contexts hold it.
|
|
12387
|
+
const resolvedInstances = loadFleetConfig(this.configPath).instances;
|
|
12388
|
+
for (const [name, resolved] of Object.entries(resolvedInstances)) {
|
|
12389
|
+
const current = this.fleetConfig.instances[name];
|
|
12390
|
+
if (!current) {
|
|
12391
|
+
this.fleetConfig.instances[name] = resolved;
|
|
12392
|
+
continue;
|
|
12393
|
+
}
|
|
12394
|
+
// Lifecycle rollback fences hold these object identities across save. Refresh their values, not their owner.
|
|
12395
|
+
for (const key of Object.keys(current))
|
|
12396
|
+
if (!Object.hasOwn(resolved, key))
|
|
12397
|
+
delete current[key];
|
|
12398
|
+
Object.assign(current, resolved);
|
|
12399
|
+
}
|
|
12145
12400
|
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
12401
|
+
this.refreshSavedDashboardPermissions();
|
|
12146
12402
|
this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
|
|
12147
12403
|
}
|
|
12404
|
+
/** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
|
|
12405
|
+
refreshSavedDashboardPermissions() {
|
|
12406
|
+
if (!this.fleetConfig)
|
|
12407
|
+
return;
|
|
12408
|
+
const classicNames = new Set(this.classicChannels?.getAll().map(ch => ch.instanceName) ?? []);
|
|
12409
|
+
for (const [name, daemon] of this.daemons) {
|
|
12410
|
+
const next = this.fleetConfig.instances[name] ?? (classicNames.has(name) ? getEffectiveInstanceDefaults(this.fleetConfig.defaults) : undefined);
|
|
12411
|
+
const previous = daemon.getConfigSnapshot?.();
|
|
12412
|
+
if (!next || !previous)
|
|
12413
|
+
continue;
|
|
12414
|
+
if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
|
|
12415
|
+
continue;
|
|
12416
|
+
// Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
|
|
12417
|
+
// detects that they require a restart. MCP menus also watch the raw file; this updates the next spawn's env.
|
|
12418
|
+
daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null });
|
|
12419
|
+
if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
|
|
12420
|
+
this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
|
|
12421
|
+
}
|
|
12422
|
+
}
|
|
12423
|
+
}
|
|
12148
12424
|
/** Why the config written to `candidate` must not replace fleet.yaml, or null: it does not load, or it adds errors. */
|
|
12149
12425
|
savedFleetConfigProblem(candidate) {
|
|
12150
12426
|
let after;
|
|
@@ -12173,7 +12449,7 @@ export class FleetManager {
|
|
|
12173
12449
|
logConfigWarningsAtStartup() {
|
|
12174
12450
|
let warnings = [];
|
|
12175
12451
|
try {
|
|
12176
|
-
warnings = validateFleetConfig(this.rawFleetConfig).warnings;
|
|
12452
|
+
warnings = validateFleetConfig(this.rawFleetConfig, { rooms: readClassicRoomNames(this.dataDir) }).warnings;
|
|
12177
12453
|
}
|
|
12178
12454
|
catch (err) {
|
|
12179
12455
|
this.logger.debug({ err }, "fleet.yaml validation failed (startup warnings)");
|
|
@@ -12182,7 +12458,44 @@ export class FleetManager {
|
|
|
12182
12458
|
this.logger.warn({ path: w.path }, `fleet.yaml: ${w.path ? `${w.path}: ` : ""}${w.message}`);
|
|
12183
12459
|
}
|
|
12184
12460
|
/** One-time upgrade migration; invalid YAML is never rewritten. */
|
|
12461
|
+
migrateNoticeDeliveryStorage() {
|
|
12462
|
+
if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
|
|
12463
|
+
|| this.fleetConfig.web?.notice_delivery !== undefined)
|
|
12464
|
+
&& validateFleetConfig(this.rawFleetConfig).valid) {
|
|
12465
|
+
try {
|
|
12466
|
+
this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
|
|
12467
|
+
this.saveFleetConfig();
|
|
12468
|
+
this.logger.info("Notices no longer go by DM: stored them as topic booleans and dropped the old DM routing");
|
|
12469
|
+
}
|
|
12470
|
+
catch (err) {
|
|
12471
|
+
this.logger.warn({ err }, "Could not migrate DM notice routing; keeping the original file");
|
|
12472
|
+
}
|
|
12473
|
+
}
|
|
12474
|
+
}
|
|
12475
|
+
migrateOfficeStorage() {
|
|
12476
|
+
if (!this.fleetConfig || !this.configPath || !hasLegacyOffice(this.rawFleetConfig) || !validateFleetConfig(this.rawFleetConfig).valid)
|
|
12477
|
+
return;
|
|
12478
|
+
try {
|
|
12479
|
+
this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
|
|
12480
|
+
const patches = [];
|
|
12481
|
+
const raw = this.rawFleetConfig;
|
|
12482
|
+
for (const [path, item] of [[["defaults"], raw.defaults], ...Object.entries(raw.instances ?? {}).map(([name, value]) => [["instances", name], value])]) {
|
|
12483
|
+
if (!item?.dashboard || !Object.hasOwn(item.dashboard, "office"))
|
|
12484
|
+
continue;
|
|
12485
|
+
patches.push({ path: [...path, "dashboard", "office"], remove: true });
|
|
12486
|
+
if (!Object.hasOwn(item, "office_access") && typeof item.dashboard.office === "boolean")
|
|
12487
|
+
patches.push({ path: [...path, "office_access"], value: item.dashboard.office });
|
|
12488
|
+
}
|
|
12489
|
+
this.saveFleetConfig(patches);
|
|
12490
|
+
this.logger.info("Stored Office access outside dashboard for downgrade compatibility");
|
|
12491
|
+
}
|
|
12492
|
+
catch (err) {
|
|
12493
|
+
this.logger.warn({ err }, "Could not migrate Office access; keeping the original file");
|
|
12494
|
+
}
|
|
12495
|
+
}
|
|
12185
12496
|
slimFleetConfigAtStartup() {
|
|
12497
|
+
this.migrateOfficeStorage();
|
|
12498
|
+
this.migrateNoticeDeliveryStorage();
|
|
12186
12499
|
const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
|
|
12187
12500
|
if (redundantPaths.length === 0)
|
|
12188
12501
|
return;
|
|
@@ -12405,6 +12718,7 @@ export class FleetManager {
|
|
|
12405
12718
|
this.effectToolsService?.forget(name);
|
|
12406
12719
|
this.mods.forgetInstance(name);
|
|
12407
12720
|
this.agentMods.forget(name);
|
|
12721
|
+
this.officeActsInstance?.forget(name);
|
|
12408
12722
|
}
|
|
12409
12723
|
catch (err) {
|
|
12410
12724
|
this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
|
|
@@ -14211,6 +14525,8 @@ export class FleetManager {
|
|
|
14211
14525
|
return true;
|
|
14212
14526
|
if (await this.handleSettingsChangeCallback(data, adapterId, adapter))
|
|
14213
14527
|
return true;
|
|
14528
|
+
if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
|
|
14529
|
+
return true;
|
|
14214
14530
|
if (await this.handleDashboardCallback(data, adapterId, adapter))
|
|
14215
14531
|
return true;
|
|
14216
14532
|
if (await this.handleClassicApproval(data, adapterId, adapter))
|
|
@@ -14331,6 +14647,7 @@ export class FleetManager {
|
|
|
14331
14647
|
const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
|
|
14332
14648
|
name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
|
|
14333
14649
|
claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
|
|
14650
|
+
kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
|
|
14334
14651
|
}));
|
|
14335
14652
|
const taken = new Set(fleet.map(i => i.name));
|
|
14336
14653
|
const classic = (this.classicChannels?.getAll() ?? []).filter(ch => !taken.has(ch.instanceName)).map(ch => ({
|
|
@@ -14340,6 +14657,7 @@ export class FleetManager {
|
|
|
14340
14657
|
ledgerPath: join(this.getInstanceDir(ch.instanceName), "cache-ledger.json"),
|
|
14341
14658
|
claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
|
|
14342
14659
|
profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14660
|
+
kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
|
|
14343
14661
|
}));
|
|
14344
14662
|
return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
|
|
14345
14663
|
}
|
|
@@ -14360,8 +14678,19 @@ export class FleetManager {
|
|
|
14360
14678
|
return this.credentialProfileOf(name, "claude-code") ?? "default";
|
|
14361
14679
|
if (backend === "codex")
|
|
14362
14680
|
return "unrecorded";
|
|
14681
|
+
if (backend === "kiro-cli")
|
|
14682
|
+
return this.credentialProfileOf(name, "kiro-cli") ?? "default";
|
|
14683
|
+
if (backend === "grok" || backend === "muse" || backend === "opencode" || backend === "antigravity")
|
|
14684
|
+
return "default";
|
|
14363
14685
|
return undefined;
|
|
14364
14686
|
}
|
|
14687
|
+
/** #2122 §10: a Kiro instance with a credential profile writes its classic store in that profile's home. */
|
|
14688
|
+
kiroDbPathOf(name, backend) {
|
|
14689
|
+
if (backend !== "kiro-cli")
|
|
14690
|
+
return undefined;
|
|
14691
|
+
const profile = this.credentialProfileOf(name, "kiro-cli");
|
|
14692
|
+
return profile ? join(credentialProfileStoreHome(getAgendHome(), "kiro-cli", profile), "data.sqlite3") : undefined;
|
|
14693
|
+
}
|
|
14365
14694
|
/** #2122: the fleet's day boundary zone — defaults.scheduler.default_timezone, else this host's. */
|
|
14366
14695
|
usageZone() {
|
|
14367
14696
|
return this.fleetConfig?.defaults?.scheduler?.default_timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
@@ -14404,6 +14733,19 @@ export class FleetManager {
|
|
|
14404
14733
|
rolloutsUnreadable: root => sharedRolloutIndex(root).unreadable,
|
|
14405
14734
|
metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
|
|
14406
14735
|
zone: () => this.usageZone(),
|
|
14736
|
+
// #2122 §10: Kiro's shared classic store (XDG_DATA_HOME, as kiro-cli resolves it) and its KIRO_HOME.
|
|
14737
|
+
kiroDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "kiro-cli", "data.sqlite3"),
|
|
14738
|
+
kiroHome: () => kiroHome(),
|
|
14739
|
+
// #2122 §10 step 3: grok keeps its sessions under ~/.grok (grok.ts reads them there too).
|
|
14740
|
+
grokSessionsDir: () => join(homedir(), ".grok", "sessions"),
|
|
14741
|
+
// #2122 §10 step 4: muse keeps sessions under $XDG_DATA_HOME/muse (else ~/.local/share/muse), as muse.ts reads them.
|
|
14742
|
+
// #2122 §10 step 5: OpenCode's database, under XDG_DATA_HOME as opencode resolves it.
|
|
14743
|
+
// #2122 §10 step 6: agy's conversations, under ~/.gemini as the antigravity backend reads its settings.
|
|
14744
|
+
antigravityDir: () => join(homedir(), ".gemini", "antigravity-cli", "conversations"),
|
|
14745
|
+
// The version AgEnD's CLI probe last recorded for agy: its cache file, whatever its age (never runs agy).
|
|
14746
|
+
antigravityVersion: () => this.lastRecordedCliVersion("antigravity"),
|
|
14747
|
+
opencodeDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "opencode", "opencode.db"),
|
|
14748
|
+
museSessionsDir: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "muse", "sessions"),
|
|
14407
14749
|
log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
|
|
14408
14750
|
});
|
|
14409
14751
|
}
|
|
@@ -14524,6 +14866,7 @@ export class FleetManager {
|
|
|
14524
14866
|
return this.deliveryOutbox.acknowledge(deliveryId, by);
|
|
14525
14867
|
},
|
|
14526
14868
|
tasks: () => this.needsYouTaskItems(),
|
|
14869
|
+
backendSignIns: () => this.needsYouBackendSignIns(),
|
|
14527
14870
|
taskAction: action => this.taskActionFromChat(action),
|
|
14528
14871
|
settings: () => ({
|
|
14529
14872
|
liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
|
|
@@ -16528,6 +16871,55 @@ export class FleetManager {
|
|
|
16528
16871
|
}
|
|
16529
16872
|
return this.loginController;
|
|
16530
16873
|
}
|
|
16874
|
+
browserBackendSignInsInstance = null;
|
|
16875
|
+
structuredLoginCleanupFailed = false;
|
|
16876
|
+
pendingBackendSince = new Map();
|
|
16877
|
+
browserSignInOwnerCurrent(owner) {
|
|
16878
|
+
const record = owner;
|
|
16879
|
+
return !this.shuttingDown && typeof record.handle === "string" && this.webSessions?.findByHandle(record.handle) === owner
|
|
16880
|
+
&& this.isLocalAdminSession(record.handle);
|
|
16881
|
+
}
|
|
16882
|
+
/** #2599: default-account controls share the exact writer reservation with chat/profile/install. */
|
|
16883
|
+
get browserBackendSignIns() {
|
|
16884
|
+
return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
|
|
16885
|
+
dataDir: this.dataDir, lock: this.loginWindow,
|
|
16886
|
+
current: owner => this.browserSignInOwnerCurrent(owner),
|
|
16887
|
+
installed: async () => ONBOARDING_BACKENDS.filter(backend => this.isCliInstalled(backend)),
|
|
16888
|
+
checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
|
|
16889
|
+
checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
|
|
16890
|
+
transport: () => createStructuredSignInTransport(),
|
|
16891
|
+
changed: (event, owner, launched) => {
|
|
16892
|
+
try {
|
|
16893
|
+
this.eventLog?.insert("login", "browser_backend_signin", event);
|
|
16894
|
+
}
|
|
16895
|
+
catch { /* safe metadata only */ }
|
|
16896
|
+
broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
|
|
16897
|
+
this.needsYou?.poke();
|
|
16898
|
+
if (!this.browserSignInOwnerCurrent(owner))
|
|
16899
|
+
return;
|
|
16900
|
+
if (event.phase === "starting") {
|
|
16901
|
+
const record = owner;
|
|
16902
|
+
this.notifyFleetError(t("login.web_dashboard_notice", event.backend, record.label, record.handle.slice(0, 8)));
|
|
16903
|
+
}
|
|
16904
|
+
if (event.phase === "done") {
|
|
16905
|
+
void this.backendAvailability.refresh([event.backend], { force: true }).catch(() => { });
|
|
16906
|
+
if (launched)
|
|
16907
|
+
void this.recoverBackendInstances(event.backend, undefined, null).catch(() => this.logger.warn({ backend: event.backend }, "Post sign-in recovery failed"));
|
|
16908
|
+
}
|
|
16909
|
+
},
|
|
16910
|
+
});
|
|
16911
|
+
}
|
|
16912
|
+
needsYouBackendSignIns() {
|
|
16913
|
+
const pending = readPendingBackendSignIns(this.dataDir).pending;
|
|
16914
|
+
for (const backend of this.pendingBackendSince?.keys() ?? [])
|
|
16915
|
+
if (!pending.includes(backend))
|
|
16916
|
+
this.pendingBackendSince.delete(backend);
|
|
16917
|
+
return pending.map(backend => {
|
|
16918
|
+
if (!this.pendingBackendSince.has(backend))
|
|
16919
|
+
this.pendingBackendSince.set(backend, Date.now());
|
|
16920
|
+
return { backend, since: this.pendingBackendSince.get(backend), browser: structuredSignInSupported(backend) };
|
|
16921
|
+
});
|
|
16922
|
+
}
|
|
16531
16923
|
/** #2233: sign-ins started from the web dashboard (local admin sessions), on the same controller as chat's. */
|
|
16532
16924
|
webBackendLoginsInstance = null;
|
|
16533
16925
|
get webBackendLogins() {
|
|
@@ -16880,6 +17272,14 @@ export class FleetManager {
|
|
|
16880
17272
|
// "cancelled" is the detail both legacy onDone handlers keep quiet about —
|
|
16881
17273
|
// a stopping fleet must not announce "login failed — fleet shutdown".
|
|
16882
17274
|
await Promise.all([
|
|
17275
|
+
bounded(this.browserBackendSignInsInstance?.close().then(confirmed => {
|
|
17276
|
+
if (confirmed)
|
|
17277
|
+
this.browserBackendSignInsInstance = null;
|
|
17278
|
+
else {
|
|
17279
|
+
this.structuredLoginCleanupFailed = true;
|
|
17280
|
+
this.logger.error("Structured login cleanup failed; sign-in admission remains closed until the host is restarted.");
|
|
17281
|
+
}
|
|
17282
|
+
}), "browser-controls"),
|
|
16883
17283
|
bounded(this.loginController?.shutdown(), "web-login"),
|
|
16884
17284
|
bounded(this.activeInstall?.session.cancel("cancelled"), "install"),
|
|
16885
17285
|
]);
|
|
@@ -16896,6 +17296,13 @@ export class FleetManager {
|
|
|
16896
17296
|
this.installHandoff = null;
|
|
16897
17297
|
return t("login.cancelled", backend);
|
|
16898
17298
|
}
|
|
17299
|
+
const browserLogin = this.browserBackendSignInsInstance, browserBackend = browserLogin?.activeBackend;
|
|
17300
|
+
if (browserLogin && browserBackend) {
|
|
17301
|
+
const confirmed = await browserLogin.close();
|
|
17302
|
+
if (confirmed)
|
|
17303
|
+
this.browserBackendSignInsInstance = null;
|
|
17304
|
+
return confirmed ? t("login.cancelled", browserBackend) : t("login.browser_cleanup_failed");
|
|
17305
|
+
}
|
|
16899
17306
|
if (this.loginController?.isActive())
|
|
16900
17307
|
return this.loginController.cancel();
|
|
16901
17308
|
return t("login.no_session");
|
|
@@ -17067,7 +17474,7 @@ export class FleetManager {
|
|
|
17067
17474
|
// session only judges that verdict; it does not read the output for URLs or prompts.
|
|
17068
17475
|
const flow = {
|
|
17069
17476
|
backend,
|
|
17070
|
-
command: info.install,
|
|
17477
|
+
command: info.unattended ?? info.install,
|
|
17071
17478
|
successPattern: /$^/,
|
|
17072
17479
|
timeoutMs: 10 * 60 * 1000,
|
|
17073
17480
|
};
|
|
@@ -18068,8 +18475,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18068
18475
|
// its ping in the reference, not the text). One hop only: a reply to a message of ours that itself answered a bot
|
|
18069
18476
|
// (or the sender) is a chain of bots answering each other — a mention still carries it on. A reply to our answer
|
|
18070
18477
|
// to a person is the ordinary handoff, and is addressed.
|
|
18071
|
-
const
|
|
18072
|
-
const
|
|
18478
|
+
const replyTriggers = this.classicChannels?.getReplyTriggers(channelId, msg.adapterId, this.fleetConfig?.defaults?.reply_triggers) ?? DEFAULT_REPLY_TRIGGERS;
|
|
18479
|
+
const repliesToUs = replyTriggers && !!adapterBotUserId && msg.replyToAuthorId === adapterBotUserId && msg.replyToChain !== true;
|
|
18480
|
+
// Snapshot/quoted content is context, never the forwarder's own mention.
|
|
18481
|
+
const addressText = msg.source === "discord" ? (msg.humanOriginalBody ?? text) : text;
|
|
18482
|
+
const isMentioned = (mentionTag && addressText.includes(mentionTag)) || repliesToUs;
|
|
18073
18483
|
if (!isMentioned) {
|
|
18074
18484
|
// Bare attachment (no @mention) — already saved above; just acknowledge.
|
|
18075
18485
|
if (saved) {
|
|
@@ -18404,6 +18814,19 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
18404
18814
|
catch { /* missing / stale / corrupt */ }
|
|
18405
18815
|
return null;
|
|
18406
18816
|
}
|
|
18817
|
+
/**
|
|
18818
|
+
* #2122 §10: the version the CLI probe last recorded for a backend — its cache file read whatever its age (the usage
|
|
18819
|
+
* reader's version gate needs the last known version, not a fresh one); null when none was recorded. Never a probe.
|
|
18820
|
+
*/
|
|
18821
|
+
lastRecordedCliVersion(backend) {
|
|
18822
|
+
try {
|
|
18823
|
+
const v = JSON.parse(readFileSync(this.cliEnvPath(backend), "utf-8")).version;
|
|
18824
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
18825
|
+
}
|
|
18826
|
+
catch {
|
|
18827
|
+
return null;
|
|
18828
|
+
}
|
|
18829
|
+
}
|
|
18407
18830
|
/** Return cached CLI versions immediately. Refresh is started after /sysinfo is sent. */
|
|
18408
18831
|
getBackendCliVersionSnapshot() {
|
|
18409
18832
|
const snapshot = {};
|
|
@@ -20341,8 +20764,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20341
20764
|
/** Signal-handler continuation, after successful graceful shutdown only. */
|
|
20342
20765
|
noteRequestedShutdown() {
|
|
20343
20766
|
const progress = readUpdateProgress(this.dataDir)?.progress;
|
|
20344
|
-
|
|
20345
|
-
|
|
20767
|
+
// Host CLI updates carry a generic marker with no chat progress target.
|
|
20768
|
+
const update = progress ? updateProgressOperation(progress) === "update"
|
|
20769
|
+
&& ["installed", "stopping", "starting"].includes(progress.stage) : isUpdateInProgress(this.dataDir);
|
|
20346
20770
|
this.recordWorkingRestartIntent(update ? "update" : "manual_restart");
|
|
20347
20771
|
}
|
|
20348
20772
|
recordWorkingRestartIntent(cause) {
|
|
@@ -20402,6 +20826,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20402
20826
|
const settingsControlStopped = settingsControl?.close();
|
|
20403
20827
|
this.publicLinkAutoOpen?.stop();
|
|
20404
20828
|
const publicStopped = this.publicWebLink?.close("fleet shutdown");
|
|
20829
|
+
const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
|
|
20405
20830
|
// #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
|
|
20406
20831
|
// adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
|
|
20407
20832
|
const needsStopped = this.stopNeedsYou();
|
|
@@ -20440,6 +20865,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20440
20865
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
20441
20866
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
20442
20867
|
await publicStopped;
|
|
20868
|
+
await sharesStopped?.catch(() => { });
|
|
20443
20869
|
await profileStopped;
|
|
20444
20870
|
await needsStopped;
|
|
20445
20871
|
await settingsControlStopped;
|
|
@@ -20792,6 +21218,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
20792
21218
|
// mark every row done and tell the user "changes applied".
|
|
20793
21219
|
return { rejected: why };
|
|
20794
21220
|
}
|
|
21221
|
+
this.migrateOfficeStorage();
|
|
21222
|
+
this.migrateNoticeDeliveryStorage();
|
|
20795
21223
|
if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
|
|
20796
21224
|
this.canvasWorkerSettingChanged();
|
|
20797
21225
|
}
|
|
@@ -22340,6 +22768,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22340
22768
|
this.logger.info("No instances to restart");
|
|
22341
22769
|
return;
|
|
22342
22770
|
}
|
|
22771
|
+
this.workingRecoveryGroup = randomUUID();
|
|
22343
22772
|
this.recordWorkingRestartIntent("manual_restart");
|
|
22344
22773
|
this.workingRecovery?.reset();
|
|
22345
22774
|
this.logger.info(`Graceful restart: waiting for ${instanceNames.length} instances to idle...`);
|
|
@@ -22415,29 +22844,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
22415
22844
|
}
|
|
22416
22845
|
// General is ready again; now its topic can own the live progress message.
|
|
22417
22846
|
await restartProgress.start(progressTarget);
|
|
22418
|
-
|
|
22419
|
-
|
|
22847
|
+
// The ClassicBot rooms (killed during orphan cleanup) restart with the fleet instances, in the same queue; their
|
|
22848
|
+
// open-lists are refreshed first so a room's inbound is never dropped while it starts.
|
|
22849
|
+
const restartClassic = topicMode ? this.classicChannels?.getAll() ?? [] : [];
|
|
22850
|
+
if (restartClassic.length > 0)
|
|
22851
|
+
this.reregisterClassicChannels();
|
|
22852
|
+
if (restartOthers.length > 0 || restartClassic.length > 0) {
|
|
22853
|
+
await this.startInstancesWithConcurrency(restartOthers, topicMode, () => restartProgress.markReady(), restartClassic);
|
|
22420
22854
|
}
|
|
22421
22855
|
if (topicMode) {
|
|
22422
22856
|
this.routing.rebuild(this.fleetConfig);
|
|
22423
22857
|
this.reregisterClassicChannels();
|
|
22424
22858
|
// 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
22859
|
for (const name of Object.keys(fleet.instances)) {
|
|
22442
22860
|
this.startStatuslineWatcher(name);
|
|
22443
22861
|
}
|