@songsid/agend 2.3.2-beta.4 → 2.3.2-beta.6

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.
Files changed (197) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/antigravity-usage-scan.d.ts +58 -0
  3. package/dist/antigravity-usage-scan.js +257 -0
  4. package/dist/antigravity-usage-scan.js.map +1 -0
  5. package/dist/antigravity-usage-worker.d.ts +1 -0
  6. package/dist/antigravity-usage-worker.js +10 -0
  7. package/dist/antigravity-usage-worker.js.map +1 -0
  8. package/dist/backend/kiro-identity.d.ts +45 -0
  9. package/dist/backend/kiro-identity.js +74 -0
  10. package/dist/backend/kiro-identity.js.map +1 -1
  11. package/dist/backend/kiro.d.ts +15 -0
  12. package/dist/backend/kiro.js +42 -5
  13. package/dist/backend/kiro.js.map +1 -1
  14. package/dist/backend/types.d.ts +6 -0
  15. package/dist/backend/types.js.map +1 -1
  16. package/dist/backend-signin-guide.d.ts +1 -0
  17. package/dist/backend-signin-guide.js +9 -0
  18. package/dist/backend-signin-guide.js.map +1 -0
  19. package/dist/backend-signin-pending.d.ts +10 -0
  20. package/dist/backend-signin-pending.js +44 -0
  21. package/dist/backend-signin-pending.js.map +1 -0
  22. package/dist/browser-backend-signins.d.ts +56 -0
  23. package/dist/browser-backend-signins.js +189 -0
  24. package/dist/browser-backend-signins.js.map +1 -0
  25. package/dist/cache-ledger.d.ts +38 -2
  26. package/dist/cache-ledger.js +133 -8
  27. package/dist/cache-ledger.js.map +1 -1
  28. package/dist/cache-prices.d.ts +2 -1
  29. package/dist/cache-prices.js +5 -2
  30. package/dist/cache-prices.js.map +1 -1
  31. package/dist/cache-service.d.ts +68 -0
  32. package/dist/cache-service.js +355 -13
  33. package/dist/cache-service.js.map +1 -1
  34. package/dist/channel/adapters/discord.d.ts +11 -0
  35. package/dist/channel/adapters/discord.js +50 -8
  36. package/dist/channel/adapters/discord.js.map +1 -1
  37. package/dist/channel/mcp-tools.js +3 -1
  38. package/dist/channel/mcp-tools.js.map +1 -1
  39. package/dist/cli.js +21 -2
  40. package/dist/cli.js.map +1 -1
  41. package/dist/config-validator.js +25 -3
  42. package/dist/config-validator.js.map +1 -1
  43. package/dist/config.d.ts +19 -0
  44. package/dist/config.js +25 -0
  45. package/dist/config.js.map +1 -1
  46. package/dist/cpu-quota.d.ts +23 -0
  47. package/dist/cpu-quota.js +86 -0
  48. package/dist/cpu-quota.js.map +1 -0
  49. package/dist/daemon.js +6 -3
  50. package/dist/daemon.js.map +1 -1
  51. package/dist/fleet-manager.d.ts +56 -5
  52. package/dist/fleet-manager.js +387 -144
  53. package/dist/fleet-manager.js.map +1 -1
  54. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  55. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  56. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +20 -4
  57. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +20 -4
  58. package/dist/instance-lifecycle.d.ts +9 -0
  59. package/dist/instance-lifecycle.js +27 -1
  60. package/dist/instance-lifecycle.js.map +1 -1
  61. package/dist/kiro-db-reader.d.ts +5 -0
  62. package/dist/kiro-db-reader.js +18 -0
  63. package/dist/kiro-db-reader.js.map +1 -1
  64. package/dist/kiro-engine-status.d.ts +2 -0
  65. package/dist/kiro-engine-status.js +9 -0
  66. package/dist/kiro-engine-status.js.map +1 -1
  67. package/dist/kiro-migrate.d.ts +49 -0
  68. package/dist/kiro-migrate.js +105 -0
  69. package/dist/kiro-migrate.js.map +1 -0
  70. package/dist/kiro-transcript-lane.d.ts +2 -0
  71. package/dist/kiro-transcript-lane.js.map +1 -1
  72. package/dist/kiro-transcript-worker.js +1 -0
  73. package/dist/kiro-transcript-worker.js.map +1 -1
  74. package/dist/kiro-usage-run.d.ts +7 -0
  75. package/dist/kiro-usage-run.js +39 -0
  76. package/dist/kiro-usage-run.js.map +1 -0
  77. package/dist/kiro-usage-scan.d.ts +49 -0
  78. package/dist/kiro-usage-scan.js +223 -0
  79. package/dist/kiro-usage-scan.js.map +1 -0
  80. package/dist/kiro-usage-worker.d.ts +1 -0
  81. package/dist/kiro-usage-worker.js +10 -0
  82. package/dist/kiro-usage-worker.js.map +1 -0
  83. package/dist/kiro-usage.d.ts +58 -0
  84. package/dist/kiro-usage.js +310 -0
  85. package/dist/kiro-usage.js.map +1 -0
  86. package/dist/locale.js +78 -2
  87. package/dist/locale.js.map +1 -1
  88. package/dist/log-service.js +1 -1
  89. package/dist/log-service.js.map +1 -1
  90. package/dist/login-url.d.ts +4 -0
  91. package/dist/login-url.js +22 -0
  92. package/dist/login-url.js.map +1 -0
  93. package/dist/needs-you-hub.d.ts +2 -1
  94. package/dist/needs-you-hub.js +1 -1
  95. package/dist/needs-you-hub.js.map +1 -1
  96. package/dist/needs-you.d.ts +8 -2
  97. package/dist/needs-you.js +6 -0
  98. package/dist/needs-you.js.map +1 -1
  99. package/dist/notice-delivery.d.ts +7 -6
  100. package/dist/notice-delivery.js +16 -18
  101. package/dist/notice-delivery.js.map +1 -1
  102. package/dist/office-share-page.d.ts +13 -0
  103. package/dist/office-share-page.js +82 -0
  104. package/dist/office-share-page.js.map +1 -0
  105. package/dist/office-share-service.d.ts +89 -0
  106. package/dist/office-share-service.js +172 -0
  107. package/dist/office-share-service.js.map +1 -0
  108. package/dist/office-share.d.ts +228 -0
  109. package/dist/office-share.js +489 -0
  110. package/dist/office-share.js.map +1 -0
  111. package/dist/opencode-usage-scan.d.ts +28 -0
  112. package/dist/opencode-usage-scan.js +92 -0
  113. package/dist/opencode-usage-scan.js.map +1 -0
  114. package/dist/opencode-usage-worker.d.ts +1 -0
  115. package/dist/opencode-usage-worker.js +10 -0
  116. package/dist/opencode-usage-worker.js.map +1 -0
  117. package/dist/outbound-handlers.js +22 -2
  118. package/dist/outbound-handlers.js.map +1 -1
  119. package/dist/outbound-schemas.d.ts +4 -0
  120. package/dist/outbound-schemas.js +4 -0
  121. package/dist/outbound-schemas.js.map +1 -1
  122. package/dist/public-web-gateway.js +6 -0
  123. package/dist/public-web-gateway.js.map +1 -1
  124. package/dist/quickstart-api.js +13 -2
  125. package/dist/quickstart-api.js.map +1 -1
  126. package/dist/quickstart.js +2 -3
  127. package/dist/quickstart.js.map +1 -1
  128. package/dist/settings-api.js +1 -1
  129. package/dist/settings-api.js.map +1 -1
  130. package/dist/setup-form.d.ts +1 -1
  131. package/dist/setup-form.js +7 -1
  132. package/dist/setup-form.js.map +1 -1
  133. package/dist/setup-host.d.ts +9 -0
  134. package/dist/setup-host.js +125 -7
  135. package/dist/setup-host.js.map +1 -1
  136. package/dist/setup-signin-assets.d.ts +2 -0
  137. package/dist/setup-signin-assets.js +6 -0
  138. package/dist/setup-signin-assets.js.map +1 -0
  139. package/dist/setup-wizard.js +2 -2
  140. package/dist/setup-wizard.js.map +1 -1
  141. package/dist/structured-signin-transport.d.ts +8 -0
  142. package/dist/structured-signin-transport.js +79 -0
  143. package/dist/structured-signin-transport.js.map +1 -0
  144. package/dist/structured-signin.d.ts +131 -0
  145. package/dist/structured-signin.js +388 -0
  146. package/dist/structured-signin.js.map +1 -0
  147. package/dist/tool-permissions.js +2 -0
  148. package/dist/tool-permissions.js.map +1 -1
  149. package/dist/topic-commands.js +4 -1
  150. package/dist/topic-commands.js.map +1 -1
  151. package/dist/transcript-sources.d.ts +26 -2
  152. package/dist/transcript-sources.js +87 -40
  153. package/dist/transcript-sources.js.map +1 -1
  154. package/dist/types.d.ts +15 -5
  155. package/dist/ui/backend-signin.js +63 -10
  156. package/dist/ui/fleet-office-pack.js +7 -2
  157. package/dist/ui/fleet-office-skin.js +5 -4
  158. package/dist/ui/fleet-office-zones.js +17 -6
  159. package/dist/ui/fleet-office.js +20 -10
  160. package/dist/ui/fleet-usage.js +19 -15
  161. package/dist/ui/office-share-dialog.js +145 -0
  162. package/dist/ui/office-share-page.js +116 -0
  163. package/dist/ui/panel-log.js +2 -2
  164. package/dist/ui/panel-needs.js +4 -4
  165. package/dist/ui/panel-settings.js +12 -8
  166. package/dist/ui/settings-registry.js +2 -2
  167. package/dist/ui/settings-strings.js +6 -6
  168. package/dist/ui/shared/app-i18n.js +2 -2
  169. package/dist/ui/shared/app.css +29 -0
  170. package/dist/ui/structured-signin.js +115 -0
  171. package/dist/usage-report.d.ts +20 -0
  172. package/dist/usage-report.js +54 -17
  173. package/dist/usage-report.js.map +1 -1
  174. package/dist/usage-rollup.d.ts +9 -1
  175. package/dist/usage-rollup.js +20 -4
  176. package/dist/usage-rollup.js.map +1 -1
  177. package/dist/web-api.d.ts +15 -0
  178. package/dist/web-api.js +190 -0
  179. package/dist/web-api.js.map +1 -1
  180. package/dist/web-auth.js +1 -1
  181. package/dist/web-auth.js.map +1 -1
  182. package/dist/web-backend-login.d.ts +1 -7
  183. package/dist/web-backend-login.js +2 -24
  184. package/dist/web-backend-login.js.map +1 -1
  185. package/dist/web-office-skins.d.ts +2 -1
  186. package/dist/web-office-skins.js +2 -1
  187. package/dist/web-office-skins.js.map +1 -1
  188. package/dist/web-settings-patch.js +1 -1
  189. package/dist/web-settings-patch.js.map +1 -1
  190. package/dist/working-recovery.d.ts +2 -2
  191. package/dist/working-recovery.js +43 -8
  192. package/dist/working-recovery.js.map +1 -1
  193. package/dist/working-state-journal.d.ts +2 -0
  194. package/dist/working-state-journal.js +16 -1
  195. package/dist/working-state-journal.js.map +1 -1
  196. package/package.json +2 -1
  197. package/templates/windows-shortcuts.ps1 +7 -5
@@ -1,5 +1,5 @@
1
1
  import { hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
2
- import { loginNoticeMode, tipNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
2
+ import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
3
3
  import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
4
4
  import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
5
5
  import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
@@ -22,6 +22,9 @@ import { PublicLinkAutoOpen } from "./public-link-auto-open.js";
22
22
  import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from "./public-link-owner.js";
23
23
  import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
24
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";
25
28
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
26
29
  import { ManagedTunnel } from "./tunnel/manager.js";
27
30
  import { withinBudget } from "./monotonic-budget.js";
@@ -31,13 +34,16 @@ import { ProfileControlServer } from "./profile-control.js";
31
34
  import { existsSync, readFileSync, mkdirSync, writeFileSync, unlinkSync, rmSync, readdirSync, renameSync, copyFileSync, chmodSync, statSync, accessSync, realpathSync, lstatSync, constants as fsConstants, closeSync } from "node:fs";
32
35
  import { atomicWriteFileSync } from "./atomic-write.js";
33
36
  import { createHash, randomBytes, randomUUID } from "node:crypto";
34
- import { freemem, totalmem, cpus, homedir } from "node:os";
37
+ import { freemem, totalmem, homedir } from "node:os";
38
+ import { effectiveCpuCount } from "./cpu-quota.js";
35
39
  import { createServer } from "node:http";
36
40
  import { Worker } from "node:worker_threads";
37
41
  import { ProbeWorkerPool } from "./probe-worker-pool.js";
38
42
  import { join, dirname, basename, delimiter, isAbsolute, relative, resolve as resolvePath } from "node:path";
39
43
  import { fileURLToPath } from "node:url";
44
+ import { isDeepStrictEqual } from "node:util";
40
45
  import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
46
+ import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./kiro-migrate.js";
41
47
  import { fleetLabel } from "./fleet-label.js";
42
48
  import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
43
49
  import { formatUpdateProgress } from "./update-progress.js";
@@ -56,7 +62,7 @@ const __dirname = dirname(__filename);
56
62
  const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
57
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";
58
64
  import { isProbeableRouteTarget } from "./fleet-context.js";
59
- 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";
60
66
  import { EventLog } from "./event-log.js";
61
67
  import { binaryProbe } from "./binary-probe.js";
62
68
  import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
@@ -193,7 +199,7 @@ import { discordGeneralChannelId } from "./discord-general.js";
193
199
  import { parseEnvText } from "./env-file.js";
194
200
  import { isDisallowedIntentsError, isRejectedTokenError } from "./discord-permissions.js";
195
201
  import { filterUsageProviders, formatDiscordUsageActivity, getUsageSnapshot, handleUsageRequest, isUsagePath, usageProviderIdForBackend } from "./usage/usage-api.js";
196
- import { LOGIN_FLOWS, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
202
+ import { LOGIN_FLOWS, checkAuthStatus, LOGIN_BACKEND_ALIASES } from "./login-flows.js";
197
203
  import { LoginSession } from "./login-manager.js";
198
204
  import { runOnLoginShell } from "./login-shell.js";
199
205
  import { tightenInstanceDirs } from "./private-dir.js";
@@ -213,6 +219,10 @@ import { admitProfileLogin, LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOG
213
219
  import { WebBackendLogins, dashboardSignInSupported } from "./web-backend-login.js";
214
220
  import { runBeforeDeadline } from "./deadline.js";
215
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";
216
226
  import { handleSettingsRequest } from "./settings-api.js";
217
227
  import { setLocale, detectLocale, getLocale, t } from "./locale.js";
218
228
  import { recentChatContext } from "./log-tail.js";
@@ -276,6 +286,7 @@ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, p
276
286
  /** A self-restart is a whole service restart; 120s is the apply budget, not this. */
277
287
  const SELF_RESTART_DEADLINE_MS = 300_000;
278
288
  import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
289
+ import { kiroHome } from "./backend/kiro-kas-store.js";
279
290
  import { CHAT_PROFILE_LOGIN_BACKENDS, credentialHomeSpec, credentialProfileHome, credentialProfileLogin, credentialProfileLoginCommand, credentialProfileStoreHome, credentialSwitchCarriesConversation, credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
280
291
  import { carryClaudeConversation } from "./backend/claude-carry.js";
281
292
  import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
@@ -473,6 +484,10 @@ const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
473
484
  const NEEDS_PAUSE_TTL_MS = 10_000;
474
485
  const HANG_CALLBACK_PREFIX = "hang:";
475
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;
476
491
  /**
477
492
  * The prompts the web dashboard also offers: the ones about an instance's own health, which a dashboard
478
493
  * user — holding the full-fleet web credential — may answer exactly as a fleet admin on the platform may.
@@ -568,6 +583,7 @@ export const CLI_ENV_PROBE_DEADLINE_MS = 16_000;
568
583
  * started ten CLIs together, saturated the CPU, and healthy starts then missed
569
584
  * their startup budget — which used to cost the user their conversation. Cores
570
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.
571
587
  */
572
588
  export function deriveSpawnConcurrency(freeMemMB, cores) {
573
589
  const byMemory = Math.floor(freeMemMB / 300);
@@ -614,6 +630,7 @@ export class FleetManager {
614
630
  workingRecovery = null;
615
631
  workingRecoveryTimer = null;
616
632
  workingRecoveryCause = "unknown_interruption";
633
+ workingRecoveryGroup = this.managerBootId;
617
634
  recoveryEnabled(name) {
618
635
  const own = this.fleetConfig?.instances[name]?.resume_interrupted_work;
619
636
  return own ?? this.fleetConfig?.defaults?.resume_interrupted_work ?? true;
@@ -654,7 +671,16 @@ export class FleetManager {
654
671
  const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
655
672
  && (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
656
673
  .map(row => `${row.instance} (${row.reason})`);
657
- 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
+ }
658
684
  },
659
685
  log: (episode, outcome, reason) => {
660
686
  const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
@@ -885,6 +911,8 @@ export class FleetManager {
885
911
  pendingBackendSelects = new Map();
886
912
  /** nonce → pending button prompt (hang restart, interactive assist, clean-exit restart). */
887
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();
888
916
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
889
917
  needsYou = null;
890
918
  /** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
@@ -1151,6 +1179,51 @@ export class FleetManager {
1151
1179
  officeSkinWrites = new OfficeWriteLimiter(undefined, { perWindow: 10, windowMs: 60_000 });
1152
1180
  /** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
1153
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
+ }
1154
1227
  officeActsInstance;
1155
1228
  /** #2578: the agents' acts in the Office view (office_view / office_set), memory only; every change is `office_acts`. */
1156
1229
  get officeActs() {
@@ -1723,6 +1796,74 @@ export class FleetManager {
1723
1796
  // No platform confirmation was reachable. Host inspection is still required.
1724
1797
  this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
1725
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
+ }
1726
1867
  async handleSettingsChangeCallback(data, adapterId, adapter) {
1727
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);
1728
1869
  if (!result)
@@ -2269,7 +2410,7 @@ export class FleetManager {
2269
2410
  if (reason === "cancelled")
2270
2411
  outbox.workingRecovery.cancel(name, observation.configuration, observation.sessionId);
2271
2412
  if (entry.prior)
2272
- outbox.workingRecovery.discover(name, entry.prior);
2413
+ outbox.workingRecovery.discover(name, entry.prior, this.workingRecoveryGroup);
2273
2414
  const state = entry.current.observation.executionState ?? "unknown";
2274
2415
  const counts = { tasks: entry.current.references.filter(r => r.kind === "task").length,
2275
2416
  owed: entry.current.references.filter(r => r.kind === "owed").length,
@@ -2576,7 +2717,7 @@ export class FleetManager {
2576
2717
  const explicit = this.fleetConfig?.defaults?.startup?.concurrency;
2577
2718
  if (explicit != null)
2578
2719
  return Math.max(1, Math.min(20, explicit));
2579
- return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)), cpus().length);
2720
+ return deriveSpawnConcurrency(Math.round(freemem() / (1024 * 1024)), effectiveCpuCount());
2580
2721
  }
2581
2722
  /** #1685: "elevated" or "critical" while host memory pressure slows or holds new agents; otherwise null (an
2582
2723
  * unknown macOS reading — the advisory case — is unknown, so null too). */
@@ -2793,8 +2934,9 @@ export class FleetManager {
2793
2934
  // the next tunnel and says so, it does not block the fleet.
2794
2935
  this.announceToolPermissionsChange();
2795
2936
  this.tightenInstanceDirectories();
2796
- // #1723: the public link's preview tunnel keeps its own lease beside it; a leftover one is reaped the same way.
2797
- for (const leaseDir of [this.dataDir, join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR)]) {
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)]) {
2798
2940
  void reapStaleTunnel(leaseDir)
2799
2941
  .then(outcome => {
2800
2942
  if (outcome.kind === "manual")
@@ -5448,36 +5590,40 @@ export class FleetManager {
5448
5590
  * config file races. Stagger applies between outer lifecycle admissions.
5449
5591
  * TODO: per-instance startup timeout (existing issue, not introduced here)
5450
5592
  */
5451
- async startInstancesWithConcurrency(entries, topicMode, onReady) {
5593
+ async startInstancesWithConcurrency(entries, topicMode, onReady, classic = []) {
5452
5594
  // Persisted pauses are intentionally preserved across fleet restarts. Filter
5453
5595
  // them before grouping/staggering: startInstance() retains its own guard as
5454
5596
  // a final backstop, but putting a no-op entry in this queue still consumes a
5455
5597
  // full stagger slot for every distinct working directory.
5456
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));
5457
5603
  const pausedCount = entries.length - runnableEntries.length;
5458
5604
  if (pausedCount > 0) {
5459
5605
  this.logger.info({ pausedCount }, "Paused instances excluded from startup queue");
5460
5606
  }
5461
- if (runnableEntries.length === 0)
5607
+ if (runnableEntries.length === 0 && runnableClassic.length === 0)
5462
5608
  return;
5463
5609
  if (this.fleetConfig?.defaults?.startup?.concurrency == null) {
5464
5610
  this.logger.info({
5465
5611
  concurrency: this.spawnConcurrency(),
5466
5612
  freeMemMB: Math.round(freemem() / (1024 * 1024)),
5467
- totalInstances: runnableEntries.length,
5613
+ totalInstances: runnableEntries.length + runnableClassic.length,
5468
5614
  }, "Adaptive startup concurrency");
5469
5615
  }
5470
5616
  const startedAt = performance.now();
5471
5617
  let active = 0, peakConcurrentStarts = 0, readyInstances = 0;
5472
- await Promise.all(runnableEntries.map(([name, config]) => this.spawnGate.run({
5618
+ const admit = (name, workingDirectory, start) => this.spawnGate.run({
5473
5619
  instanceName: name,
5474
- workingDirectory: config.working_directory,
5620
+ workingDirectory,
5475
5621
  reason: "startup",
5476
5622
  stage: "lifecycle",
5477
5623
  }, async () => {
5478
5624
  peakConcurrentStarts = Math.max(peakConcurrentStarts, ++active);
5479
5625
  try {
5480
- if (await this.startInstanceUnattended(name, config, topicMode, "instance")) {
5626
+ if (await start()) {
5481
5627
  readyInstances++;
5482
5628
  onReady?.(name);
5483
5629
  }
@@ -5485,9 +5631,13 @@ export class FleetManager {
5485
5631
  finally {
5486
5632
  active--;
5487
5633
  }
5488
- })));
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
+ ]);
5489
5639
  this.logger.info({
5490
- totalInstances: runnableEntries.length, readyInstances,
5640
+ totalInstances: runnableEntries.length + runnableClassic.length, classicInstances: runnableClassic.length, readyInstances,
5491
5641
  elapsedSeconds: Math.round((performance.now() - startedAt) / 100) / 10,
5492
5642
  peakConcurrentStarts,
5493
5643
  }, "Fleet startup batch completed");
@@ -6498,33 +6648,11 @@ export class FleetManager {
6498
6648
  getPublicWebStatus() {
6499
6649
  return this.publicWebLink?.status() ?? { state: "closed" };
6500
6650
  }
6501
- /** Notices use the first explicit admin of the owning connection; never a guest/paired account. */
6502
- noticeAdmin(adapterId) {
6503
- return this.adminListOf(adapterId)?.find(id => id.length > 0);
6504
- }
6505
- /** Best-effort private delivery. The caller retains its existing topic fallback and authority fences. */
6506
- async tryNoticeDm(adapter, userId, text) {
6507
- if (!userId || !adapter?.sendDirect)
6508
- return false;
6509
- try {
6510
- await withinBudget(adapter.sendDirect(userId, text, { disablePreview: true }), performance.now() + 2_000);
6511
- return true;
6512
- }
6513
- catch {
6514
- this.logger.info({ adapterId: adapter.id }, "Fleet notice DM failed; falling back to the topic");
6515
- return false;
6516
- }
6517
- }
6518
6651
  async confirmPublicWebLogin(info, current) {
6519
6652
  if (!current() || !this.publicOwnerCurrent(info.owner))
6520
6653
  throw new Error("public owner unavailable");
6521
6654
  const adapter = info.owner.binding;
6522
- const text = t("web.public_login_notice", info.label, info.handle.slice(0, 8));
6523
- if (loginNoticeMode(this.fleetConfig?.web) !== "dm" || !await this.tryNoticeDm(adapter, info.owner.userId, text)) {
6524
- if (!current() || !this.publicOwnerCurrent(info.owner))
6525
- throw new Error("public owner changed");
6526
- await adapter.sendText(info.owner.chatId, text, { threadId: info.owner.threadId, allowedMentions: { parse: [] } });
6527
- }
6655
+ await adapter.sendText(info.owner.chatId, t("web.public_login_notice", info.label, info.handle.slice(0, 8)), { threadId: info.owner.threadId, allowedMentions: { parse: [] } });
6528
6656
  if (!current() || !this.publicOwnerCurrent(info.owner))
6529
6657
  throw new Error("public owner changed");
6530
6658
  }
@@ -6542,12 +6670,7 @@ export class FleetManager {
6542
6670
  if (!open)
6543
6671
  throw new Error("guest public sign-in unavailable");
6544
6672
  const adapter = open.owner.binding;
6545
- const text = t("web.guest_public_login_notice", this.webPerson(info.account).name, info.label, this.guestScope(info.account).size, info.handle.slice(0, 8));
6546
- if (loginNoticeMode(this.fleetConfig?.web) !== "dm" || !await this.tryNoticeDm(adapter, open.owner.userId, text)) {
6547
- if (!admitted())
6548
- throw new Error("guest public sign-in changed");
6549
- await adapter.sendText(open.owner.chatId, text, { threadId: open.owner.threadId, allowedMentions: { parse: [] } });
6550
- }
6673
+ await adapter.sendText(open.owner.chatId, t("web.guest_public_login_notice", this.webPerson(info.account).name, info.label, this.guestScope(info.account).size, info.handle.slice(0, 8)), { threadId: open.owner.threadId, allowedMentions: { parse: [] } });
6551
6674
  if (!admitted())
6552
6675
  throw new Error("guest public sign-in changed");
6553
6676
  }
@@ -7385,26 +7508,18 @@ export class FleetManager {
7385
7508
  }
7386
7509
  /** Called by the sign-in endpoint: a login the operator did not make should be visible to them. */
7387
7510
  onWebLogin(info) {
7388
- // Keep the existing false policy: guest and public-link security notices remain mandatory.
7389
- const guest = info.tier === "guest" && info.owner;
7390
- if (!guest && this.fleetConfig?.web?.notify_login === false)
7391
- return;
7392
- const text = guest
7393
- ? t("web.guest_login_notice", this.webPerson(info.owner).name, info.label, this.guestScope(info.owner).size, info.handle.slice(0, 8))
7394
- : t("web.login_notice", info.label, info.surface, info.handle.slice(0, 8));
7395
- if (loginNoticeMode(this.fleetConfig?.web) === "dm") {
7396
- const adapterId = info.owner?.adapterId;
7397
- const adapter = adapterId ? this.adapters.get(adapterId) : undefined;
7398
- // A local guest's own account must never receive the admin's security notice.
7399
- const userId = guest ? (adapterId ? this.noticeAdmin(adapterId) : undefined)
7400
- : info.owner && this.isFleetAdmin(info.owner.userId, adapterId) ? info.owner.userId : undefined;
7401
- void this.tryNoticeDm(adapter, userId, text).then(sent => { if (!sent)
7402
- this.notifyFleetError(text); })
7403
- .catch(() => this.logger.warn("Web sign-in notice could not be delivered"));
7511
+ // #1920 §2.3: a guest's sign-in is always said in General (web.notify_login is the operator's own sign-ins): who,
7512
+ // and how many agents they now reach — admins stay aware of everyone who is not one of them.
7513
+ if (info.tier === "guest" && info.owner) {
7514
+ this.notifyFleetError(t("web.guest_login_notice", this.webPerson(info.owner).name, info.label, this.guestScope(info.owner).size, info.handle.slice(0, 8)));
7404
7515
  return;
7405
7516
  }
7406
- // Exactly the old topic path, including the session-handle throttle key.
7407
- this.notifyFleetError(text);
7517
+ // Never by DM (hmsg-3b8ac2af: Telegram cannot DM a user who has not started the bot); a legacy "dm" reads as on.
7518
+ if (!loginNoticeMode(this.fleetConfig?.web))
7519
+ return;
7520
+ // The session handle makes each notice distinct: notifyFleetError throttles by text, and a second
7521
+ // sign-in from the same kind of browser is exactly the one the operator most needs to hear about.
7522
+ this.notifyFleetError(t("web.login_notice", info.label, info.surface, info.handle.slice(0, 8)));
7408
7523
  }
7409
7524
  getDashboardAccess() {
7410
7525
  return { ready: this.healthServerListening, token: this.webToken };
@@ -7774,7 +7889,8 @@ export class FleetManager {
7774
7889
  }
7775
7890
  }
7776
7891
  async startAllWithinDeadline(configPath, deadline) {
7777
- this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
7892
+ if (!this.structuredLoginCleanupFailed)
7893
+ this.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
7778
7894
  this.loginController?.reopen();
7779
7895
  const startupStartedAt = Date.now();
7780
7896
  FleetManager.signalTarget = this;
@@ -8035,12 +8151,7 @@ export class FleetManager {
8035
8151
  mkdirSync(generalDir, { recursive: true, mode: 0o700 });
8036
8152
  const backendName = fleet.defaults.backend ?? "claude-code";
8037
8153
  this.ensureGeneralInstructions(generalDir, backendName, name);
8038
- fleet.instances[name] = {
8039
- ...DEFAULT_INSTANCE_CONFIG,
8040
- working_directory: generalDir,
8041
- general_topic: true,
8042
- channel_id: need.adapterId,
8043
- };
8154
+ fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
8044
8155
  generalsCreated = true;
8045
8156
  }
8046
8157
  if (generalsCreated)
@@ -8186,9 +8297,11 @@ export class FleetManager {
8186
8297
  this.rotateAllInstanceLogs();
8187
8298
  }, 24 * 60 * 60_000);
8188
8299
  this.logRotateTimer.unref?.();
8189
- // Phase 2: Start remaining instances with staggered concurrency
8190
- if (others.length > 0) {
8191
- await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady());
8300
+ // Phase 2: Start remaining instances, and the ClassicBot rooms with them, with staggered concurrency. A room needs no
8301
+ // chat connection to spawn: its open-list is set above (reregisterClassicChannels) and inbound queues until it is up.
8302
+ const classicAtBoot = topicMode && (fleet.channel || fleet.channels?.length) ? this.classicChannels?.getAll() ?? [] : [];
8303
+ if (others.length > 0 || classicAtBoot.length > 0) {
8304
+ await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady(), classicAtBoot);
8192
8305
  }
8193
8306
  if (topicMode && (fleet.channel || fleet.channels?.length)) {
8194
8307
  await adapterStartup;
@@ -8230,22 +8343,7 @@ export class FleetManager {
8230
8343
  // IPC is already wired by startInstancesWithConcurrency → startInstance →
8231
8344
  // connectIpcToInstance. The previous 3s sleep + connectToInstances loop
8232
8345
  // was redundant.
8233
- // Start classic channel instances (parallel, concurrency 3)
8234
- if (this.classicChannels) {
8235
- const fleetBackend = this.fleetConfig?.defaults?.backend;
8236
- const channels = this.classicChannels.getAll()
8237
- .filter(ch => !this.lifecycle.isPaused(ch.instanceName));
8238
- const concurrency = 3;
8239
- let idx = 0;
8240
- while (idx < channels.length) {
8241
- const batch = channels.slice(idx, idx + concurrency);
8242
- await Promise.allSettled(batch.map(async (ch) => {
8243
- if (await this.startClassicInstanceUnattended(ch, "classic instance"))
8244
- startupProgress.markReady();
8245
- }));
8246
- idx += concurrency;
8247
- }
8248
- }
8346
+ // The ClassicBot rooms started in Phase 2, with the fleet.
8249
8347
  for (const name of Object.keys(fleet.instances)) {
8250
8348
  this.startStatuslineWatcher(name);
8251
8349
  }
@@ -8512,25 +8610,43 @@ export class FleetManager {
8512
8610
  if (channelConfigs.length === 0)
8513
8611
  return;
8514
8612
  // Start ALL adapters in parallel — any single failure doesn't block others.
8515
- const results = await Promise.allSettled(channelConfigs.map((cfg, i) => i === 0
8613
+ const settled = channelConfigs.map(() => false);
8614
+ // Each connection is recorded (connected, or retrying with its retry scheduled) as its own login settles.
8615
+ const recorded = channelConfigs.map((cfg, i) => (i === 0
8516
8616
  ? this.startSingleAdapter(fleet, cfg)
8517
- : this.startAdditionalAdapter(cfg)));
8518
- // Track state + schedule background retry for failures.
8519
- for (let i = 0; i < channelConfigs.length; i++) {
8520
- const adapterId = channelConfigs[i].id ?? channelConfigs[i].type;
8521
- if (results[i].status === "fulfilled") {
8522
- this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
8523
- }
8524
- else {
8525
- const err = results[i].reason;
8526
- this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
8527
- this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
8528
- this.scheduleAdapterRetry(adapterId);
8529
- // Notify admin via whichever adapter is already up
8530
- this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
8531
- }
8617
+ : this.startAdditionalAdapter(cfg)).then(() => ({ status: "fulfilled" }), (reason) => ({ status: "rejected", reason }))
8618
+ .then(result => { settled[i] = true; this.recordAdapterStartup(cfg, result); })
8619
+ .catch(err => this.logger.error({ err }, "Recording a chat connection's startup failed")));
8620
+ // #2642: bounded. Startup continues past a connection still logging in after this long (ClassicBot instances,
8621
+ // READY); that connection is recorded, or retried, when its login settles, and joins the fleet as it would anyway.
8622
+ let timer;
8623
+ const inTime = await Promise.race([
8624
+ Promise.all(recorded).then(() => true),
8625
+ new Promise(resolve => { timer = setTimeout(() => resolve(false), FleetManager.ADAPTER_STARTUP_WAIT_MS); timer.unref?.(); }),
8626
+ ]).finally(() => clearTimeout(timer));
8627
+ if (!inTime) {
8628
+ this.logger.warn({
8629
+ pending: channelConfigs.filter((_, i) => !settled[i]).map(cfg => cfg.id ?? cfg.type),
8630
+ waitedMs: FleetManager.ADAPTER_STARTUP_WAIT_MS,
8631
+ }, "Chat connection still connecting — fleet startup continues without waiting for it");
8532
8632
  }
8533
8633
  }
8634
+ /** How long fleet startup waits for its chat connections before it continues without the slow ones (#2642). */
8635
+ static ADAPTER_STARTUP_WAIT_MS = 60_000;
8636
+ /** Track a connection's startup outcome; a failure gets the background retry. */
8637
+ recordAdapterStartup(cfg, result) {
8638
+ const adapterId = cfg.id ?? cfg.type;
8639
+ if (result.status === "fulfilled") {
8640
+ this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
8641
+ return;
8642
+ }
8643
+ const err = result.reason;
8644
+ this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
8645
+ this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
8646
+ this.scheduleAdapterRetry(adapterId);
8647
+ // Notify admin via whichever adapter is already up
8648
+ this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
8649
+ }
8534
8650
  /** Keep trying while the fleet runs, with a five-minute cap even after a long outage. */
8535
8651
  scheduleAdapterRetry(adapterId) {
8536
8652
  const INITIAL_DELAY_MS = 5_000;
@@ -12170,10 +12286,11 @@ export class FleetManager {
12170
12286
  const { office, ...dashboard } = patch.value;
12171
12287
  return [{ ...patch, value: dashboard }, { path: [...path.slice(0, -1), "office_access"], value: office, remove: office === null }];
12172
12288
  }
12289
+ // #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
12173
12290
  if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
12174
- return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "login"], value: "dm" }];
12291
+ return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "login"], remove: true }];
12175
12292
  if (patch.path.join(".") === "defaults.tips" && patch.value === "dm")
12176
- return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "tips"], value: "dm" }];
12293
+ return [{ ...patch, value: true }, { path: ["web", "notice_delivery", "tips"], remove: true }];
12177
12294
  return [patch];
12178
12295
  });
12179
12296
  // Re-read immediately before patching so an unrelated concurrent/manual
@@ -12251,9 +12368,46 @@ export class FleetManager {
12251
12368
  },
12252
12369
  });
12253
12370
  this.rawFleetConfig = loadRawFleetConfig(this.configPath);
12371
+ // The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
12372
+ // leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
12373
+ // Keep the FleetConfig object itself: Settings and other request contexts hold it.
12374
+ const resolvedInstances = loadFleetConfig(this.configPath).instances;
12375
+ for (const [name, resolved] of Object.entries(resolvedInstances)) {
12376
+ const current = this.fleetConfig.instances[name];
12377
+ if (!current) {
12378
+ this.fleetConfig.instances[name] = resolved;
12379
+ continue;
12380
+ }
12381
+ // Lifecycle rollback fences hold these object identities across save. Refresh their values, not their owner.
12382
+ for (const key of Object.keys(current))
12383
+ if (!Object.hasOwn(resolved, key))
12384
+ delete current[key];
12385
+ Object.assign(current, resolved);
12386
+ }
12254
12387
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
12388
+ this.refreshSavedDashboardPermissions();
12255
12389
  this.logger.info({ path: this.configPath, strippedDefaults: redundantPaths.length }, "Saved fleet config (lossless patch)");
12256
12390
  }
12391
+ /** A permission tightening is effective when its save returns, independently of the later apply job or IPC. */
12392
+ refreshSavedDashboardPermissions() {
12393
+ if (!this.fleetConfig)
12394
+ return;
12395
+ const classicNames = new Set(this.classicChannels?.getAll().map(ch => ch.instanceName) ?? []);
12396
+ for (const [name, daemon] of this.daemons) {
12397
+ const next = this.fleetConfig.instances[name] ?? (classicNames.has(name) ? getEffectiveInstanceDefaults(this.fleetConfig.defaults) : undefined);
12398
+ const previous = daemon.getConfigSnapshot?.();
12399
+ if (!next || !previous)
12400
+ continue;
12401
+ if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
12402
+ continue;
12403
+ // Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
12404
+ // detects that they require a restart. MCP menus also watch the raw file; this updates the next spawn's env.
12405
+ daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null });
12406
+ if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
12407
+ this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
12408
+ }
12409
+ }
12410
+ }
12257
12411
  /** Why the config written to `candidate` must not replace fleet.yaml, or null: it does not load, or it adds errors. */
12258
12412
  savedFleetConfigProblem(candidate) {
12259
12413
  let after;
@@ -12292,12 +12446,13 @@ export class FleetManager {
12292
12446
  }
12293
12447
  /** One-time upgrade migration; invalid YAML is never rewritten. */
12294
12448
  migrateNoticeDeliveryStorage() {
12295
- if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm")
12449
+ if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
12450
+ || this.fleetConfig.web?.notice_delivery !== undefined)
12296
12451
  && validateFleetConfig(this.rawFleetConfig).valid) {
12297
12452
  try {
12298
12453
  this.writeFleetConfigBackup(readFileSync(this.configPath, "utf8"));
12299
12454
  this.saveFleetConfig();
12300
- this.logger.info("Stored DM notice routing with downgrade-safe boolean fields");
12455
+ this.logger.info("Notices no longer go by DM: stored them as topic booleans and dropped the old DM routing");
12301
12456
  }
12302
12457
  catch (err) {
12303
12458
  this.logger.warn({ err }, "Could not migrate DM notice routing; keeping the original file");
@@ -14357,6 +14512,8 @@ export class FleetManager {
14357
14512
  return true;
14358
14513
  if (await this.handleSettingsChangeCallback(data, adapterId, adapter))
14359
14514
  return true;
14515
+ if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
14516
+ return true;
14360
14517
  if (await this.handleDashboardCallback(data, adapterId, adapter))
14361
14518
  return true;
14362
14519
  if (await this.handleClassicApproval(data, adapterId, adapter))
@@ -14477,6 +14634,7 @@ export class FleetManager {
14477
14634
  const fleet = Object.entries(this.fleetConfig?.instances ?? {}).map(([name, cfg]) => ({
14478
14635
  name, backend: this.backendNameOf(name), workingDirectory: cfg.working_directory, ledgerPath: join(this.getInstanceDir(name), "cache-ledger.json"),
14479
14636
  claudeConfigDir: this.claudeConfigDirOf(name), profile: this.usageProfileOf(name, this.backendNameOf(name)),
14637
+ kiroDbPath: this.kiroDbPathOf(name, this.backendNameOf(name)),
14480
14638
  }));
14481
14639
  const taken = new Set(fleet.map(i => i.name));
14482
14640
  const classic = (this.classicChannels?.getAll() ?? []).filter(ch => !taken.has(ch.instanceName)).map(ch => ({
@@ -14486,6 +14644,7 @@ export class FleetManager {
14486
14644
  ledgerPath: join(this.getInstanceDir(ch.instanceName), "cache-ledger.json"),
14487
14645
  claudeConfigDir: this.claudeConfigDirOf(ch.instanceName),
14488
14646
  profile: this.usageProfileOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
14647
+ kiroDbPath: this.kiroDbPathOf(ch.instanceName, this.classicChannels?.getBackendByInstance(ch.instanceName, this.fleetConfig?.defaults?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code"),
14489
14648
  }));
14490
14649
  return [...fleet, ...new Map(classic.map(c => [c.name, c])).values()];
14491
14650
  }
@@ -14506,8 +14665,19 @@ export class FleetManager {
14506
14665
  return this.credentialProfileOf(name, "claude-code") ?? "default";
14507
14666
  if (backend === "codex")
14508
14667
  return "unrecorded";
14668
+ if (backend === "kiro-cli")
14669
+ return this.credentialProfileOf(name, "kiro-cli") ?? "default";
14670
+ if (backend === "grok" || backend === "muse" || backend === "opencode" || backend === "antigravity")
14671
+ return "default";
14509
14672
  return undefined;
14510
14673
  }
14674
+ /** #2122 §10: a Kiro instance with a credential profile writes its classic store in that profile's home. */
14675
+ kiroDbPathOf(name, backend) {
14676
+ if (backend !== "kiro-cli")
14677
+ return undefined;
14678
+ const profile = this.credentialProfileOf(name, "kiro-cli");
14679
+ return profile ? join(credentialProfileStoreHome(getAgendHome(), "kiro-cli", profile), "data.sqlite3") : undefined;
14680
+ }
14511
14681
  /** #2122: the fleet's day boundary zone — defaults.scheduler.default_timezone, else this host's. */
14512
14682
  usageZone() {
14513
14683
  return this.fleetConfig?.defaults?.scheduler?.default_timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -14550,6 +14720,19 @@ export class FleetManager {
14550
14720
  rolloutsUnreadable: root => sharedRolloutIndex(root).unreadable,
14551
14721
  metaPath: join(this.dataDir, "cache-codex-rollouts.json"),
14552
14722
  zone: () => this.usageZone(),
14723
+ // #2122 §10: Kiro's shared classic store (XDG_DATA_HOME, as kiro-cli resolves it) and its KIRO_HOME.
14724
+ kiroDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "kiro-cli", "data.sqlite3"),
14725
+ kiroHome: () => kiroHome(),
14726
+ // #2122 §10 step 3: grok keeps its sessions under ~/.grok (grok.ts reads them there too).
14727
+ grokSessionsDir: () => join(homedir(), ".grok", "sessions"),
14728
+ // #2122 §10 step 4: muse keeps sessions under $XDG_DATA_HOME/muse (else ~/.local/share/muse), as muse.ts reads them.
14729
+ // #2122 §10 step 5: OpenCode's database, under XDG_DATA_HOME as opencode resolves it.
14730
+ // #2122 §10 step 6: agy's conversations, under ~/.gemini as the antigravity backend reads its settings.
14731
+ antigravityDir: () => join(homedir(), ".gemini", "antigravity-cli", "conversations"),
14732
+ // The version AgEnD's CLI probe last recorded for agy: its cache file, whatever its age (never runs agy).
14733
+ antigravityVersion: () => this.lastRecordedCliVersion("antigravity"),
14734
+ opencodeDbPath: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "opencode", "opencode.db"),
14735
+ museSessionsDir: () => join(process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share"), "muse", "sessions"),
14553
14736
  log: (level, msg, extra) => this.logger[level](extra ?? {}, msg),
14554
14737
  });
14555
14738
  }
@@ -14670,6 +14853,7 @@ export class FleetManager {
14670
14853
  return this.deliveryOutbox.acknowledge(deliveryId, by);
14671
14854
  },
14672
14855
  tasks: () => this.needsYouTaskItems(),
14856
+ backendSignIns: () => this.needsYouBackendSignIns(),
14673
14857
  taskAction: action => this.taskActionFromChat(action),
14674
14858
  settings: () => ({
14675
14859
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
@@ -14928,14 +15112,14 @@ export class FleetManager {
14928
15112
  }
14929
15113
  }
14930
15114
  /** Post one fresh nonce-armed tip in a known General channel. */
14931
- async promptTip(generalName, adapter, chatId, threadId, directUserId) {
15115
+ async promptTip(generalName, adapter, chatId, threadId) {
14932
15116
  const state = this.readTipState();
14933
15117
  if (!state)
14934
15118
  return "unavailable";
14935
15119
  if (!state.advancedUnlocked
14936
15120
  && visibleTipLevels(true).has("advanced")
14937
15121
  && canUnlockAdvancedTips(state.dismissed)) {
14938
- return await this.promptAdvancedTipUnlock(generalName, adapter, chatId, threadId, directUserId)
15122
+ return await this.promptAdvancedTipUnlock(generalName, adapter, chatId, threadId)
14939
15123
  ? "posted"
14940
15124
  : "unavailable";
14941
15125
  }
@@ -14951,7 +15135,6 @@ export class FleetManager {
14951
15135
  chatId,
14952
15136
  threadId,
14953
15137
  message: this.formatTip(tip),
14954
- ...(directUserId ? { deliver: (choices) => withinBudget(adapter.sendDirect(directUserId, this.formatTip(tip), { choices, disablePreview: true }), performance.now() + 5_000) } : {}),
14955
15138
  choices: [
14956
15139
  { action: "dismiss", label: t("tips.dismiss") },
14957
15140
  { action: "confused", label: t("tips.confused") },
@@ -14963,7 +15146,7 @@ export class FleetManager {
14963
15146
  });
14964
15147
  return nonce ? "posted" : "unavailable";
14965
15148
  }
14966
- async promptAdvancedTipUnlock(generalName, adapter, chatId, threadId, directUserId) {
15149
+ async promptAdvancedTipUnlock(generalName, adapter, chatId, threadId) {
14967
15150
  const nonce = await this.postNonceButtonPrompt({
14968
15151
  prefix: TIP_UNLOCK_CALLBACK_PREFIX,
14969
15152
  alertType: "tip",
@@ -14973,7 +15156,6 @@ export class FleetManager {
14973
15156
  chatId,
14974
15157
  threadId,
14975
15158
  message: t("tips.advanced.unlock_prompt"),
14976
- ...(directUserId ? { deliver: (choices) => withinBudget(adapter.sendDirect(directUserId, t("tips.advanced.unlock_prompt"), { choices, disablePreview: true }), performance.now() + 5_000) } : {}),
14977
15159
  choices: [{ action: "unlock", label: t("tips.advanced.unlock") }],
14978
15160
  expiredText: t("tips.advanced.expired"),
14979
15161
  // No allowAnyUser: unlocking changes a persistent setting, which typed and slash `/tips advanced on` reserve for a
@@ -14993,15 +15175,6 @@ export class FleetManager {
14993
15175
  const topicId = this.fleetConfig?.instances[generalName]?.topic_id;
14994
15176
  if (!adapter || !chatId)
14995
15177
  return;
14996
- if (this.fleetConfig && tipNoticeMode(this.fleetConfig) === "dm" && adapter.sendDirect) {
14997
- const userId = this.noticeAdmin(adapter.id);
14998
- if (userId) {
14999
- const result = await this.promptTip(generalName, adapter, userId, undefined, userId);
15000
- if (result !== "unavailable")
15001
- return;
15002
- // The failed prompt disarmed its nonce; a new topic prompt is safe.
15003
- }
15004
- }
15005
15178
  const result = await this.promptTip(generalName, adapter, chatId, topicId != null ? String(topicId) : undefined);
15006
15179
  if (result === "unavailable") {
15007
15180
  this.logger.warn({ generalName }, "Daily tip could not be posted");
@@ -16685,6 +16858,55 @@ export class FleetManager {
16685
16858
  }
16686
16859
  return this.loginController;
16687
16860
  }
16861
+ browserBackendSignInsInstance = null;
16862
+ structuredLoginCleanupFailed = false;
16863
+ pendingBackendSince = new Map();
16864
+ browserSignInOwnerCurrent(owner) {
16865
+ const record = owner;
16866
+ return !this.shuttingDown && typeof record.handle === "string" && this.webSessions?.findByHandle(record.handle) === owner
16867
+ && this.isLocalAdminSession(record.handle);
16868
+ }
16869
+ /** #2599: default-account controls share the exact writer reservation with chat/profile/install. */
16870
+ get browserBackendSignIns() {
16871
+ return this.browserBackendSignInsInstance ??= new BrowserBackendSignIns({
16872
+ dataDir: this.dataDir, lock: this.loginWindow,
16873
+ current: owner => this.browserSignInOwnerCurrent(owner),
16874
+ installed: async () => ONBOARDING_BACKENDS.filter(backend => this.isCliInstalled(backend)),
16875
+ checkAuth: backend => checkAuthStatus(LOGIN_FLOWS[backend].authCheck),
16876
+ checkConsoleAuth: () => checkAuthStatus(LOGIN_FLOWS.antigravity.authCheck),
16877
+ transport: () => createStructuredSignInTransport(),
16878
+ changed: (event, owner, launched) => {
16879
+ try {
16880
+ this.eventLog?.insert("login", "browser_backend_signin", event);
16881
+ }
16882
+ catch { /* safe metadata only */ }
16883
+ broadcastSseEvent(this.sseClients, "backend_login", {}, err => this.logger.debug({ err }, "backend_login event failed"));
16884
+ this.needsYou?.poke();
16885
+ if (!this.browserSignInOwnerCurrent(owner))
16886
+ return;
16887
+ if (event.phase === "starting") {
16888
+ const record = owner;
16889
+ this.notifyFleetError(t("login.web_dashboard_notice", event.backend, record.label, record.handle.slice(0, 8)));
16890
+ }
16891
+ if (event.phase === "done") {
16892
+ void this.backendAvailability.refresh([event.backend], { force: true }).catch(() => { });
16893
+ if (launched)
16894
+ void this.recoverBackendInstances(event.backend, undefined, null).catch(() => this.logger.warn({ backend: event.backend }, "Post sign-in recovery failed"));
16895
+ }
16896
+ },
16897
+ });
16898
+ }
16899
+ needsYouBackendSignIns() {
16900
+ const pending = readPendingBackendSignIns(this.dataDir).pending;
16901
+ for (const backend of this.pendingBackendSince?.keys() ?? [])
16902
+ if (!pending.includes(backend))
16903
+ this.pendingBackendSince.delete(backend);
16904
+ return pending.map(backend => {
16905
+ if (!this.pendingBackendSince.has(backend))
16906
+ this.pendingBackendSince.set(backend, Date.now());
16907
+ return { backend, since: this.pendingBackendSince.get(backend), browser: structuredSignInSupported(backend) };
16908
+ });
16909
+ }
16688
16910
  /** #2233: sign-ins started from the web dashboard (local admin sessions), on the same controller as chat's. */
16689
16911
  webBackendLoginsInstance = null;
16690
16912
  get webBackendLogins() {
@@ -17037,6 +17259,14 @@ export class FleetManager {
17037
17259
  // "cancelled" is the detail both legacy onDone handlers keep quiet about —
17038
17260
  // a stopping fleet must not announce "login failed — fleet shutdown".
17039
17261
  await Promise.all([
17262
+ bounded(this.browserBackendSignInsInstance?.close().then(confirmed => {
17263
+ if (confirmed)
17264
+ this.browserBackendSignInsInstance = null;
17265
+ else {
17266
+ this.structuredLoginCleanupFailed = true;
17267
+ this.logger.error("Structured login cleanup failed; sign-in admission remains closed until the host is restarted.");
17268
+ }
17269
+ }), "browser-controls"),
17040
17270
  bounded(this.loginController?.shutdown(), "web-login"),
17041
17271
  bounded(this.activeInstall?.session.cancel("cancelled"), "install"),
17042
17272
  ]);
@@ -17053,6 +17283,13 @@ export class FleetManager {
17053
17283
  this.installHandoff = null;
17054
17284
  return t("login.cancelled", backend);
17055
17285
  }
17286
+ const browserLogin = this.browserBackendSignInsInstance, browserBackend = browserLogin?.activeBackend;
17287
+ if (browserLogin && browserBackend) {
17288
+ const confirmed = await browserLogin.close();
17289
+ if (confirmed)
17290
+ this.browserBackendSignInsInstance = null;
17291
+ return confirmed ? t("login.cancelled", browserBackend) : t("login.browser_cleanup_failed");
17292
+ }
17056
17293
  if (this.loginController?.isActive())
17057
17294
  return this.loginController.cancel();
17058
17295
  return t("login.no_session");
@@ -18564,6 +18801,19 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
18564
18801
  catch { /* missing / stale / corrupt */ }
18565
18802
  return null;
18566
18803
  }
18804
+ /**
18805
+ * #2122 §10: the version the CLI probe last recorded for a backend — its cache file read whatever its age (the usage
18806
+ * reader's version gate needs the last known version, not a fresh one); null when none was recorded. Never a probe.
18807
+ */
18808
+ lastRecordedCliVersion(backend) {
18809
+ try {
18810
+ const v = JSON.parse(readFileSync(this.cliEnvPath(backend), "utf-8")).version;
18811
+ return typeof v === "string" && v.trim() ? v.trim() : null;
18812
+ }
18813
+ catch {
18814
+ return null;
18815
+ }
18816
+ }
18567
18817
  /** Return cached CLI versions immediately. Refresh is started after /sysinfo is sent. */
18568
18818
  getBackendCliVersionSnapshot() {
18569
18819
  const snapshot = {};
@@ -20501,8 +20751,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20501
20751
  /** Signal-handler continuation, after successful graceful shutdown only. */
20502
20752
  noteRequestedShutdown() {
20503
20753
  const progress = readUpdateProgress(this.dataDir)?.progress;
20504
- const update = progress && updateProgressOperation(progress) === "update"
20505
- && ["installed", "stopping", "starting"].includes(progress.stage);
20754
+ // Host CLI updates carry a generic marker with no chat progress target.
20755
+ const update = progress ? updateProgressOperation(progress) === "update"
20756
+ && ["installed", "stopping", "starting"].includes(progress.stage) : isUpdateInProgress(this.dataDir);
20506
20757
  this.recordWorkingRestartIntent(update ? "update" : "manual_restart");
20507
20758
  }
20508
20759
  recordWorkingRestartIntent(cause) {
@@ -20562,6 +20813,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20562
20813
  const settingsControlStopped = settingsControl?.close();
20563
20814
  this.publicLinkAutoOpen?.stop();
20564
20815
  const publicStopped = this.publicWebLink?.close("fleet shutdown");
20816
+ const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
20565
20817
  // #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
20566
20818
  // adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
20567
20819
  const needsStopped = this.stopNeedsYou();
@@ -20600,6 +20852,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20600
20852
  // timer and HTTP listener living in THIS process: without an explicit
20601
20853
  // shutdown it would outlive us as an owner-less login CLI (sol B3).
20602
20854
  await publicStopped;
20855
+ await sharesStopped?.catch(() => { });
20603
20856
  await profileStopped;
20604
20857
  await needsStopped;
20605
20858
  await settingsControlStopped;
@@ -22502,6 +22755,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22502
22755
  this.logger.info("No instances to restart");
22503
22756
  return;
22504
22757
  }
22758
+ this.workingRecoveryGroup = randomUUID();
22505
22759
  this.recordWorkingRestartIntent("manual_restart");
22506
22760
  this.workingRecovery?.reset();
22507
22761
  this.logger.info(`Graceful restart: waiting for ${instanceNames.length} instances to idle...`);
@@ -22577,29 +22831,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22577
22831
  }
22578
22832
  // General is ready again; now its topic can own the live progress message.
22579
22833
  await restartProgress.start(progressTarget);
22580
- if (restartOthers.length > 0) {
22581
- await this.startInstancesWithConcurrency(restartOthers, topicMode, () => restartProgress.markReady());
22834
+ // The ClassicBot rooms (killed during orphan cleanup) restart with the fleet instances, in the same queue; their
22835
+ // open-lists are refreshed first so a room's inbound is never dropped while it starts.
22836
+ const restartClassic = topicMode ? this.classicChannels?.getAll() ?? [] : [];
22837
+ if (restartClassic.length > 0)
22838
+ this.reregisterClassicChannels();
22839
+ if (restartOthers.length > 0 || restartClassic.length > 0) {
22840
+ await this.startInstancesWithConcurrency(restartOthers, topicMode, () => restartProgress.markReady(), restartClassic);
22582
22841
  }
22583
22842
  if (topicMode) {
22584
22843
  this.routing.rebuild(this.fleetConfig);
22585
22844
  this.reregisterClassicChannels();
22586
22845
  // startInstance already calls connectIpcToInstance, no need for connectToInstances here
22587
- // Restart classic channel instances (killed during orphan cleanup)
22588
- if (this.classicChannels) {
22589
- const fleetBackend = this.fleetConfig?.defaults?.backend;
22590
- const channels = this.classicChannels.getAll()
22591
- .filter(ch => !this.lifecycle.isPaused(ch.instanceName));
22592
- const concurrency = 3;
22593
- let idx = 0;
22594
- while (idx < channels.length) {
22595
- const batch = channels.slice(idx, idx + concurrency);
22596
- await Promise.allSettled(batch.map(async (ch) => {
22597
- if (await this.startClassicInstanceUnattended(ch, "classic instance"))
22598
- restartProgress.markReady();
22599
- }));
22600
- idx += concurrency;
22601
- }
22602
- }
22603
22846
  for (const name of Object.keys(fleet.instances)) {
22604
22847
  this.startStatuslineWatcher(name);
22605
22848
  }