@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.
Files changed (266) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/kiro-identity.d.ts +64 -0
  15. package/dist/backend/kiro-identity.js +101 -0
  16. package/dist/backend/kiro-identity.js.map +1 -1
  17. package/dist/backend/kiro-retired-model.d.ts +62 -0
  18. package/dist/backend/kiro-retired-model.js +160 -0
  19. package/dist/backend/kiro-retired-model.js.map +1 -0
  20. package/dist/backend/kiro.d.ts +71 -2
  21. package/dist/backend/kiro.js +352 -11
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/types.d.ts +59 -0
  24. package/dist/backend/types.js.map +1 -1
  25. package/dist/backend-signin-guide.d.ts +1 -0
  26. package/dist/backend-signin-guide.js +9 -0
  27. package/dist/backend-signin-guide.js.map +1 -0
  28. package/dist/backend-signin-pending.d.ts +10 -0
  29. package/dist/backend-signin-pending.js +44 -0
  30. package/dist/backend-signin-pending.js.map +1 -0
  31. package/dist/browser-backend-signins.d.ts +56 -0
  32. package/dist/browser-backend-signins.js +189 -0
  33. package/dist/browser-backend-signins.js.map +1 -0
  34. package/dist/cache-ledger.d.ts +38 -2
  35. package/dist/cache-ledger.js +133 -8
  36. package/dist/cache-ledger.js.map +1 -1
  37. package/dist/cache-prices.d.ts +2 -1
  38. package/dist/cache-prices.js +5 -2
  39. package/dist/cache-prices.js.map +1 -1
  40. package/dist/cache-service.d.ts +68 -0
  41. package/dist/cache-service.js +355 -13
  42. package/dist/cache-service.js.map +1 -1
  43. package/dist/channel/adapters/discord.d.ts +11 -0
  44. package/dist/channel/adapters/discord.js +71 -14
  45. package/dist/channel/adapters/discord.js.map +1 -1
  46. package/dist/channel/mcp-tools.js +29 -2
  47. package/dist/channel/mcp-tools.js.map +1 -1
  48. package/dist/channel/tool-router.js +2 -1
  49. package/dist/channel/tool-router.js.map +1 -1
  50. package/dist/classic-channel-manager.d.ts +11 -0
  51. package/dist/classic-channel-manager.js +39 -1
  52. package/dist/classic-channel-manager.js.map +1 -1
  53. package/dist/cli.js +21 -2
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config-validator.d.ts +7 -1
  56. package/dist/config-validator.js +53 -10
  57. package/dist/config-validator.js.map +1 -1
  58. package/dist/config.d.ts +19 -0
  59. package/dist/config.js +31 -1
  60. package/dist/config.js.map +1 -1
  61. package/dist/cpu-quota.d.ts +23 -0
  62. package/dist/cpu-quota.js +86 -0
  63. package/dist/cpu-quota.js.map +1 -0
  64. package/dist/daemon.d.ts +23 -0
  65. package/dist/daemon.js +106 -7
  66. package/dist/daemon.js.map +1 -1
  67. package/dist/dashboard-permissions.d.ts +20 -6
  68. package/dist/dashboard-permissions.js +58 -9
  69. package/dist/dashboard-permissions.js.map +1 -1
  70. package/dist/doctor.js +2 -1
  71. package/dist/doctor.js.map +1 -1
  72. package/dist/fleet-manager.d.ts +72 -0
  73. package/dist/fleet-manager.js +507 -89
  74. package/dist/fleet-manager.js.map +1 -1
  75. package/dist/forged-envelope-scan.d.ts +9 -0
  76. package/dist/forged-envelope-scan.js +30 -5
  77. package/dist/forged-envelope-scan.js.map +1 -1
  78. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  79. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  80. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
  81. package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
  82. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +37 -9
  83. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +44 -11
  84. package/dist/instance-config-impact.js +10 -5
  85. package/dist/instance-config-impact.js.map +1 -1
  86. package/dist/instance-lifecycle.d.ts +19 -0
  87. package/dist/instance-lifecycle.js +55 -12
  88. package/dist/instance-lifecycle.js.map +1 -1
  89. package/dist/kiro-db-reader.d.ts +5 -0
  90. package/dist/kiro-db-reader.js +18 -0
  91. package/dist/kiro-db-reader.js.map +1 -1
  92. package/dist/kiro-engine-status.d.ts +2 -0
  93. package/dist/kiro-engine-status.js +9 -0
  94. package/dist/kiro-engine-status.js.map +1 -1
  95. package/dist/kiro-migrate.d.ts +49 -0
  96. package/dist/kiro-migrate.js +105 -0
  97. package/dist/kiro-migrate.js.map +1 -0
  98. package/dist/kiro-transcript-lane.d.ts +2 -0
  99. package/dist/kiro-transcript-lane.js.map +1 -1
  100. package/dist/kiro-transcript-worker.js +1 -0
  101. package/dist/kiro-transcript-worker.js.map +1 -1
  102. package/dist/kiro-tui-forensic.d.ts +7 -0
  103. package/dist/kiro-tui-forensic.js +110 -0
  104. package/dist/kiro-tui-forensic.js.map +1 -0
  105. package/dist/kiro-usage-run.d.ts +7 -0
  106. package/dist/kiro-usage-run.js +39 -0
  107. package/dist/kiro-usage-run.js.map +1 -0
  108. package/dist/kiro-usage-scan.d.ts +49 -0
  109. package/dist/kiro-usage-scan.js +223 -0
  110. package/dist/kiro-usage-scan.js.map +1 -0
  111. package/dist/kiro-usage-worker.d.ts +1 -0
  112. package/dist/kiro-usage-worker.js +10 -0
  113. package/dist/kiro-usage-worker.js.map +1 -0
  114. package/dist/kiro-usage.d.ts +58 -0
  115. package/dist/kiro-usage.js +310 -0
  116. package/dist/kiro-usage.js.map +1 -0
  117. package/dist/locale.js +88 -8
  118. package/dist/locale.js.map +1 -1
  119. package/dist/log-service.js +1 -1
  120. package/dist/log-service.js.map +1 -1
  121. package/dist/login-url.d.ts +4 -0
  122. package/dist/login-url.js +22 -0
  123. package/dist/login-url.js.map +1 -0
  124. package/dist/needs-you-hub.d.ts +2 -1
  125. package/dist/needs-you-hub.js +1 -1
  126. package/dist/needs-you-hub.js.map +1 -1
  127. package/dist/needs-you.d.ts +8 -2
  128. package/dist/needs-you.js +6 -0
  129. package/dist/needs-you.js.map +1 -1
  130. package/dist/notice-delivery.d.ts +16 -0
  131. package/dist/notice-delivery.js +35 -0
  132. package/dist/notice-delivery.js.map +1 -0
  133. package/dist/office-share-page.d.ts +16 -0
  134. package/dist/office-share-page.js +85 -0
  135. package/dist/office-share-page.js.map +1 -0
  136. package/dist/office-share-service.d.ts +91 -0
  137. package/dist/office-share-service.js +176 -0
  138. package/dist/office-share-service.js.map +1 -0
  139. package/dist/office-share.d.ts +258 -0
  140. package/dist/office-share.js +549 -0
  141. package/dist/office-share.js.map +1 -0
  142. package/dist/opencode-usage-scan.d.ts +28 -0
  143. package/dist/opencode-usage-scan.js +92 -0
  144. package/dist/opencode-usage-scan.js.map +1 -0
  145. package/dist/opencode-usage-worker.d.ts +1 -0
  146. package/dist/opencode-usage-worker.js +10 -0
  147. package/dist/opencode-usage-worker.js.map +1 -0
  148. package/dist/outbound-handlers.js +22 -2
  149. package/dist/outbound-handlers.js.map +1 -1
  150. package/dist/outbound-schemas.d.ts +26 -2
  151. package/dist/outbound-schemas.js +27 -2
  152. package/dist/outbound-schemas.js.map +1 -1
  153. package/dist/public-web-gateway.js +7 -1
  154. package/dist/public-web-gateway.js.map +1 -1
  155. package/dist/quickstart-api.js +13 -2
  156. package/dist/quickstart-api.js.map +1 -1
  157. package/dist/quickstart.js +2 -1
  158. package/dist/quickstart.js.map +1 -1
  159. package/dist/remote-card-answer.d.ts +83 -0
  160. package/dist/remote-card-answer.js +208 -0
  161. package/dist/remote-card-answer.js.map +1 -0
  162. package/dist/reply-triggers.d.ts +2 -0
  163. package/dist/reply-triggers.js +3 -0
  164. package/dist/reply-triggers.js.map +1 -0
  165. package/dist/settings-api.d.ts +2 -0
  166. package/dist/settings-api.js +41 -16
  167. package/dist/settings-api.js.map +1 -1
  168. package/dist/settings-change.js +17 -14
  169. package/dist/settings-change.js.map +1 -1
  170. package/dist/settings-effect.js +7 -1
  171. package/dist/settings-effect.js.map +1 -1
  172. package/dist/settings-instance-patch.js +4 -1
  173. package/dist/settings-instance-patch.js.map +1 -1
  174. package/dist/setup-form.d.ts +1 -1
  175. package/dist/setup-form.js +55 -6
  176. package/dist/setup-form.js.map +1 -1
  177. package/dist/setup-host.d.ts +9 -0
  178. package/dist/setup-host.js +127 -6
  179. package/dist/setup-host.js.map +1 -1
  180. package/dist/setup-signin-assets.d.ts +2 -0
  181. package/dist/setup-signin-assets.js +6 -0
  182. package/dist/setup-signin-assets.js.map +1 -0
  183. package/dist/setup-wizard.js +2 -0
  184. package/dist/setup-wizard.js.map +1 -1
  185. package/dist/structured-signin-transport.d.ts +8 -0
  186. package/dist/structured-signin-transport.js +79 -0
  187. package/dist/structured-signin-transport.js.map +1 -0
  188. package/dist/structured-signin.d.ts +131 -0
  189. package/dist/structured-signin.js +388 -0
  190. package/dist/structured-signin.js.map +1 -0
  191. package/dist/tool-permissions.js +12 -0
  192. package/dist/tool-permissions.js.map +1 -1
  193. package/dist/topic-commands.js +6 -2
  194. package/dist/topic-commands.js.map +1 -1
  195. package/dist/transcript-sources.d.ts +26 -2
  196. package/dist/transcript-sources.js +103 -40
  197. package/dist/transcript-sources.js.map +1 -1
  198. package/dist/turn-reply-guard.d.ts +1 -1
  199. package/dist/turn-reply-guard.js +1 -1
  200. package/dist/turn-reply-guard.js.map +1 -1
  201. package/dist/types.d.ts +28 -8
  202. package/dist/ui/backend-signin.js +63 -10
  203. package/dist/ui/chat-strings.js +2 -2
  204. package/dist/ui/fleet-office-editor.js +14 -9
  205. package/dist/ui/fleet-office-layout.js +70 -32
  206. package/dist/ui/fleet-office-model.js +6 -3
  207. package/dist/ui/fleet-office-overview.js +167 -0
  208. package/dist/ui/fleet-office-pack.d.ts +7 -2
  209. package/dist/ui/fleet-office-pack.js +119 -15
  210. package/dist/ui/fleet-office-skin.js +98 -23
  211. package/dist/ui/fleet-office-store.js +39 -5
  212. package/dist/ui/fleet-office-view.js +5 -4
  213. package/dist/ui/fleet-office-zones.js +172 -12
  214. package/dist/ui/fleet-office.js +547 -186
  215. package/dist/ui/fleet-usage.js +19 -15
  216. package/dist/ui/office-share-dialog.js +145 -0
  217. package/dist/ui/office-share-page.js +164 -0
  218. package/dist/ui/panel-chat.js +8 -1
  219. package/dist/ui/panel-log.js +2 -2
  220. package/dist/ui/panel-needs.js +4 -4
  221. package/dist/ui/panel-settings.js +37 -11
  222. package/dist/ui/settings-dialogs.js +8 -1
  223. package/dist/ui/settings-guests.js +17 -7
  224. package/dist/ui/settings-model.js +14 -6
  225. package/dist/ui/settings-registry.js +4 -0
  226. package/dist/ui/settings-strings.js +16 -2
  227. package/dist/ui/shared/app-i18n.js +4 -2
  228. package/dist/ui/shared/app-shell.js +14 -6
  229. package/dist/ui/shared/app-stream.js +2 -1
  230. package/dist/ui/shared/app.css +67 -2
  231. package/dist/ui/shared/office/cozy.png +0 -0
  232. package/dist/ui/shared/side-panel.js +95 -0
  233. package/dist/ui/structured-signin.js +115 -0
  234. package/dist/usage-report.d.ts +20 -0
  235. package/dist/usage-report.js +54 -17
  236. package/dist/usage-report.js.map +1 -1
  237. package/dist/usage-rollup.d.ts +9 -1
  238. package/dist/usage-rollup.js +20 -4
  239. package/dist/usage-rollup.js.map +1 -1
  240. package/dist/web-api.d.ts +18 -0
  241. package/dist/web-api.js +201 -0
  242. package/dist/web-api.js.map +1 -1
  243. package/dist/web-auth.js +1 -1
  244. package/dist/web-auth.js.map +1 -1
  245. package/dist/web-backend-login.d.ts +1 -7
  246. package/dist/web-backend-login.js +2 -24
  247. package/dist/web-backend-login.js.map +1 -1
  248. package/dist/web-office-acts.d.ts +173 -0
  249. package/dist/web-office-acts.js +285 -0
  250. package/dist/web-office-acts.js.map +1 -0
  251. package/dist/web-office-skins.d.ts +3 -2
  252. package/dist/web-office-skins.js +3 -2
  253. package/dist/web-office-skins.js.map +1 -1
  254. package/dist/web-office-store.js +40 -7
  255. package/dist/web-office-store.js.map +1 -1
  256. package/dist/web-settings-patch.d.ts +2 -1
  257. package/dist/web-settings-patch.js +4 -1
  258. package/dist/web-settings-patch.js.map +1 -1
  259. package/dist/working-recovery.d.ts +2 -2
  260. package/dist/working-recovery.js +43 -8
  261. package/dist/working-recovery.js.map +1 -1
  262. package/dist/working-state-journal.d.ts +2 -0
  263. package/dist/working-state-journal.js +16 -1
  264. package/dist/working-state-journal.js.map +1 -1
  265. package/package.json +3 -2
  266. package/templates/windows-shortcuts.ps1 +155 -140
@@ -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, cpus, homedir } from "node:os";
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)), cpus().length);
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
- 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)]) {
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.defaults.tips = mode === "on";
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
- await Promise.all(runnableEntries.map(([name, config]) => this.spawnGate.run({
5618
+ const admit = (name, workingDirectory, start) => this.spawnGate.run({
5421
5619
  instanceName: name,
5422
- workingDirectory: config.working_directory,
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 this.startInstanceUnattended(name, config, topicMode, "instance")) {
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
- return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), connections, guests,
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
- if (this.fleetConfig?.web?.notify_login === false)
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.loginWindow.reopen(); // a stopAll → startAll restart must accept login windows again
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
- if (others.length > 0) {
8103
- await this.startInstancesWithConcurrency(others, topicMode, () => startupProgress.markReady());
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
- // Start classic channel instances (parallel, concurrency 3)
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 results = await Promise.allSettled(channelConfigs.map((cfg, i) => i === 0
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
- // Track state + schedule background retry for failures.
8431
- for (let i = 0; i < channelConfigs.length; i++) {
8432
- const adapterId = channelConfigs[i].id ?? channelConfigs[i].type;
8433
- if (results[i].status === "fulfilled") {
8434
- this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
8435
- }
8436
- else {
8437
- const err = results[i].reason;
8438
- this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
8439
- this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
8440
- this.scheduleAdapterRetry(adapterId);
8441
- // Notify admin via whichever adapter is already up
8442
- this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
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.webAppearance.handleTool(instanceName, tool, args);
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 repliesToUs = !!adapterBotUserId && msg.replyToAuthorId === adapterBotUserId && msg.replyToChain !== true;
18072
- const isMentioned = (mentionTag && text.includes(mentionTag)) || repliesToUs;
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
- const update = progress && updateProgressOperation(progress) === "update"
20345
- && ["installed", "stopping", "starting"].includes(progress.stage);
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
- if (restartOthers.length > 0) {
22419
- await this.startInstancesWithConcurrency(restartOthers, topicMode, () => restartProgress.markReady());
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
  }