@songsid/agend 2.3.2-beta.9 → 2.3.3-beta.1

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 (285) hide show
  1. package/README.md +6 -0
  2. package/README.zh-TW.md +6 -0
  3. package/dist/agent-cli-instructions.md +2 -2
  4. package/dist/agent-cli.js +48 -0
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +6 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +21 -0
  9. package/dist/agent-whats-new.js +53 -21
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/auth-api.js +2 -0
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +13 -1
  14. package/dist/backend/claude-code.js +43 -2
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex-session-lookup.d.ts +15 -0
  17. package/dist/backend/codex-session-lookup.js +58 -0
  18. package/dist/backend/codex-session-lookup.js.map +1 -1
  19. package/dist/backend/codex.js +15 -4
  20. package/dist/backend/codex.js.map +1 -1
  21. package/dist/backend/kiro-classic-archive.d.ts +57 -0
  22. package/dist/backend/kiro-classic-archive.js +622 -0
  23. package/dist/backend/kiro-classic-archive.js.map +1 -0
  24. package/dist/backend/kiro-classic-links.d.ts +24 -0
  25. package/dist/backend/kiro-classic-links.js +135 -0
  26. package/dist/backend/kiro-classic-links.js.map +1 -0
  27. package/dist/backend/kiro-identity.d.ts +100 -1
  28. package/dist/backend/kiro-identity.js +262 -31
  29. package/dist/backend/kiro-identity.js.map +1 -1
  30. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  31. package/dist/backend/kiro-migration-verify.js +52 -0
  32. package/dist/backend/kiro-migration-verify.js.map +1 -0
  33. package/dist/backend/kiro-reconcile-store.d.ts +27 -0
  34. package/dist/backend/kiro-reconcile-store.js +351 -0
  35. package/dist/backend/kiro-reconcile-store.js.map +1 -0
  36. package/dist/backend/kiro-v2-store-worker.js +35 -1
  37. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  38. package/dist/backend/kiro-v2-store.d.ts +31 -1
  39. package/dist/backend/kiro-v2-store.js +22 -12
  40. package/dist/backend/kiro-v2-store.js.map +1 -1
  41. package/dist/backend/kiro.d.ts +124 -6
  42. package/dist/backend/kiro.js +553 -39
  43. package/dist/backend/kiro.js.map +1 -1
  44. package/dist/backend/types.d.ts +109 -4
  45. package/dist/backend/types.js +9 -0
  46. package/dist/backend/types.js.map +1 -1
  47. package/dist/cache-service.d.ts +13 -1
  48. package/dist/cache-service.js +45 -2
  49. package/dist/cache-service.js.map +1 -1
  50. package/dist/channel/ipc-timeouts.js +3 -0
  51. package/dist/channel/ipc-timeouts.js.map +1 -1
  52. package/dist/channel/mcp-tools.js +40 -5
  53. package/dist/channel/mcp-tools.js.map +1 -1
  54. package/dist/channel/tool-router.js +2 -1
  55. package/dist/channel/tool-router.js.map +1 -1
  56. package/dist/classic-channel-manager.d.ts +6 -0
  57. package/dist/classic-channel-manager.js +25 -0
  58. package/dist/classic-channel-manager.js.map +1 -1
  59. package/dist/cli.js +52 -6
  60. package/dist/cli.js.map +1 -1
  61. package/dist/config-validator.d.ts +7 -1
  62. package/dist/config-validator.js +20 -9
  63. package/dist/config-validator.js.map +1 -1
  64. package/dist/config.js +2 -0
  65. package/dist/config.js.map +1 -1
  66. package/dist/cross-instance-envelope.d.ts +10 -5
  67. package/dist/cross-instance-envelope.js +20 -17
  68. package/dist/cross-instance-envelope.js.map +1 -1
  69. package/dist/daemon.d.ts +125 -63
  70. package/dist/daemon.js +694 -330
  71. package/dist/daemon.js.map +1 -1
  72. package/dist/dashboard-permissions.d.ts +15 -4
  73. package/dist/dashboard-permissions.js +49 -23
  74. package/dist/dashboard-permissions.js.map +1 -1
  75. package/dist/delivery-outbox.d.ts +27 -22
  76. package/dist/delivery-outbox.js +109 -161
  77. package/dist/delivery-outbox.js.map +1 -1
  78. package/dist/delivery-outbox.test.js +10 -25
  79. package/dist/delivery-outbox.test.js.map +1 -1
  80. package/dist/doctor.js +2 -1
  81. package/dist/doctor.js.map +1 -1
  82. package/dist/fleet-control-audit.d.ts +1 -1
  83. package/dist/fleet-control-audit.js +3 -0
  84. package/dist/fleet-control-audit.js.map +1 -1
  85. package/dist/fleet-manager.d.ts +94 -15
  86. package/dist/fleet-manager.js +597 -133
  87. package/dist/fleet-manager.js.map +1 -1
  88. package/dist/forged-envelope-scan.d.ts +9 -0
  89. package/dist/forged-envelope-scan.js +30 -5
  90. package/dist/forged-envelope-scan.js.map +1 -1
  91. package/dist/general-instance.d.ts +24 -0
  92. package/dist/general-instance.js +53 -0
  93. package/dist/general-instance.js.map +1 -1
  94. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
  95. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  96. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
  97. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  98. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
  99. package/dist/general-knowledge/skills/session-management/SKILL.md +2 -2
  100. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
  102. package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +57 -17
  104. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
  105. package/dist/instance-config-impact.js +3 -2
  106. package/dist/instance-config-impact.js.map +1 -1
  107. package/dist/instance-lifecycle.d.ts +4 -0
  108. package/dist/instance-lifecycle.js +28 -5
  109. package/dist/instance-lifecycle.js.map +1 -1
  110. package/dist/instructions.js +6 -7
  111. package/dist/instructions.js.map +1 -1
  112. package/dist/kiro-migrate.d.ts +19 -2
  113. package/dist/kiro-migrate.js +133 -35
  114. package/dist/kiro-migrate.js.map +1 -1
  115. package/dist/kiro-reconcile.d.ts +21 -0
  116. package/dist/kiro-reconcile.js +88 -0
  117. package/dist/kiro-reconcile.js.map +1 -0
  118. package/dist/kiro-session-proof.d.ts +41 -0
  119. package/dist/kiro-session-proof.js +89 -0
  120. package/dist/kiro-session-proof.js.map +1 -0
  121. package/dist/kiro-tui-forensic.d.ts +19 -0
  122. package/dist/kiro-tui-forensic.js +124 -0
  123. package/dist/kiro-tui-forensic.js.map +1 -0
  124. package/dist/locale.js +202 -16
  125. package/dist/locale.js.map +1 -1
  126. package/dist/log-service.js +3 -3
  127. package/dist/log-service.js.map +1 -1
  128. package/dist/needs-you.d.ts +3 -1
  129. package/dist/needs-you.js +4 -2
  130. package/dist/needs-you.js.map +1 -1
  131. package/dist/office-levels.d.ts +64 -0
  132. package/dist/office-levels.js +66 -0
  133. package/dist/office-levels.js.map +1 -0
  134. package/dist/office-share-page.d.ts +3 -1
  135. package/dist/office-share-page.js +8 -3
  136. package/dist/office-share-page.js.map +1 -1
  137. package/dist/office-share-service.d.ts +6 -3
  138. package/dist/office-share-service.js +12 -8
  139. package/dist/office-share-service.js.map +1 -1
  140. package/dist/office-share.d.ts +45 -8
  141. package/dist/office-share.js +75 -15
  142. package/dist/office-share.js.map +1 -1
  143. package/dist/outbound-handlers.js +21 -2
  144. package/dist/outbound-handlers.js.map +1 -1
  145. package/dist/outbound-schemas.d.ts +47 -2
  146. package/dist/outbound-schemas.js +42 -3
  147. package/dist/outbound-schemas.js.map +1 -1
  148. package/dist/public-web-gateway.js +1 -1
  149. package/dist/public-web-gateway.js.map +1 -1
  150. package/dist/quickstart-api.d.ts +39 -1
  151. package/dist/quickstart-api.js +115 -17
  152. package/dist/quickstart-api.js.map +1 -1
  153. package/dist/remote-card-answer.d.ts +83 -0
  154. package/dist/remote-card-answer.js +208 -0
  155. package/dist/remote-card-answer.js.map +1 -0
  156. package/dist/remote-card-prompt.d.ts +38 -0
  157. package/dist/remote-card-prompt.js +86 -0
  158. package/dist/remote-card-prompt.js.map +1 -0
  159. package/dist/scheduler/task-board.d.ts +44 -17
  160. package/dist/scheduler/task-board.js +95 -36
  161. package/dist/scheduler/task-board.js.map +1 -1
  162. package/dist/sd-notify.d.ts +13 -14
  163. package/dist/sd-notify.js +62 -28
  164. package/dist/sd-notify.js.map +1 -1
  165. package/dist/service-installer.d.ts +13 -0
  166. package/dist/service-installer.js +15 -0
  167. package/dist/service-installer.js.map +1 -1
  168. package/dist/service-owner.d.ts +25 -0
  169. package/dist/service-owner.js +43 -2
  170. package/dist/service-owner.js.map +1 -1
  171. package/dist/settings-api.d.ts +2 -1
  172. package/dist/settings-api.js +19 -26
  173. package/dist/settings-api.js.map +1 -1
  174. package/dist/settings-change.js +10 -5
  175. package/dist/settings-change.js.map +1 -1
  176. package/dist/settings-effect.js +7 -6
  177. package/dist/settings-effect.js.map +1 -1
  178. package/dist/settings-instance-patch.js +1 -1
  179. package/dist/settings-instance-patch.js.map +1 -1
  180. package/dist/setup-form.d.ts +2 -2
  181. package/dist/setup-form.js +139 -93
  182. package/dist/setup-form.js.map +1 -1
  183. package/dist/setup-i18n.d.ts +25 -0
  184. package/dist/setup-i18n.js +226 -0
  185. package/dist/setup-i18n.js.map +1 -0
  186. package/dist/task-board-waker.d.ts +40 -1
  187. package/dist/task-board-waker.js +115 -9
  188. package/dist/task-board-waker.js.map +1 -1
  189. package/dist/tmux-control.d.ts +1 -1
  190. package/dist/tmux-control.js +3 -2
  191. package/dist/tmux-control.js.map +1 -1
  192. package/dist/tmux-manager.js +18 -16
  193. package/dist/tmux-manager.js.map +1 -1
  194. package/dist/tmux-read.d.ts +2 -0
  195. package/dist/tmux-read.js +7 -1
  196. package/dist/tmux-read.js.map +1 -1
  197. package/dist/tool-permissions.js +18 -0
  198. package/dist/tool-permissions.js.map +1 -1
  199. package/dist/topic-commands.d.ts +2 -0
  200. package/dist/topic-commands.js +2 -1
  201. package/dist/topic-commands.js.map +1 -1
  202. package/dist/transcript-sources.d.ts +1 -0
  203. package/dist/transcript-sources.js +20 -3
  204. package/dist/transcript-sources.js.map +1 -1
  205. package/dist/types.d.ts +11 -5
  206. package/dist/ui/backend-signin.js +4 -3
  207. package/dist/ui/chat-store.js +3 -2
  208. package/dist/ui/chat-strings.js +2 -2
  209. package/dist/ui/fleet-office-card.js +114 -0
  210. package/dist/ui/fleet-office-editor.js +81 -9
  211. package/dist/ui/fleet-office-layout.js +56 -11
  212. package/dist/ui/fleet-office-levels.js +105 -0
  213. package/dist/ui/fleet-office-model.js +9 -1
  214. package/dist/ui/fleet-office-pack.d.ts +15 -3
  215. package/dist/ui/fleet-office-pack.js +177 -23
  216. package/dist/ui/fleet-office-skin.js +39 -3
  217. package/dist/ui/fleet-office-store.js +74 -3
  218. package/dist/ui/fleet-office-view.js +8 -0
  219. package/dist/ui/fleet-office-zones.js +102 -19
  220. package/dist/ui/fleet-office.js +308 -44
  221. package/dist/ui/fleet-org.js +2 -0
  222. package/dist/ui/fleet-task-card.js +2 -2
  223. package/dist/ui/fleet-task-common.js +30 -7
  224. package/dist/ui/fleet-task-work.js +2 -2
  225. package/dist/ui/fleet-tasks.js +10 -11
  226. package/dist/ui/office-share-dialog.js +31 -7
  227. package/dist/ui/office-share-page.js +1 -0
  228. package/dist/ui/panel-chat.js +14 -2
  229. package/dist/ui/panel-log.js +6 -6
  230. package/dist/ui/panel-needs.js +1 -1
  231. package/dist/ui/panel-settings.js +16 -8
  232. package/dist/ui/settings-appearance.js +3 -35
  233. package/dist/ui/settings-discord.js +3 -2
  234. package/dist/ui/settings-guests.js +17 -7
  235. package/dist/ui/settings-model.js +17 -9
  236. package/dist/ui/settings-registry.js +1 -1
  237. package/dist/ui/settings-strings.js +18 -6
  238. package/dist/ui/settings-token.js +3 -2
  239. package/dist/ui/settings-wizard.js +25 -9
  240. package/dist/ui/shared/app-i18n.js +2 -2
  241. package/dist/ui/shared/app-stream.js +1 -1
  242. package/dist/ui/shared/app.css +70 -2
  243. package/dist/ui/shared/setup-host-text.js +107 -0
  244. package/dist/ui/structured-signin.js +89 -38
  245. package/dist/update-check.d.ts +13 -4
  246. package/dist/update-check.js +17 -4
  247. package/dist/update-check.js.map +1 -1
  248. package/dist/usage-rollup.d.ts +34 -3
  249. package/dist/usage-rollup.js +56 -3
  250. package/dist/usage-rollup.js.map +1 -1
  251. package/dist/web-api.d.ts +10 -1
  252. package/dist/web-api.js +207 -17
  253. package/dist/web-api.js.map +1 -1
  254. package/dist/web-auth.js +1 -1
  255. package/dist/web-auth.js.map +1 -1
  256. package/dist/web-experimental.d.ts +6 -2
  257. package/dist/web-experimental.js +8 -4
  258. package/dist/web-experimental.js.map +1 -1
  259. package/dist/web-office-acts.d.ts +5 -2
  260. package/dist/web-office-acts.js +9 -4
  261. package/dist/web-office-acts.js.map +1 -1
  262. package/dist/web-office-pack-read.d.ts +35 -0
  263. package/dist/web-office-pack-read.js +252 -0
  264. package/dist/web-office-pack-read.js.map +1 -0
  265. package/dist/web-office-pack-tools.d.ts +241 -0
  266. package/dist/web-office-pack-tools.js +785 -0
  267. package/dist/web-office-pack-tools.js.map +1 -0
  268. package/dist/web-office-skins.d.ts +68 -4
  269. package/dist/web-office-skins.js +324 -33
  270. package/dist/web-office-skins.js.map +1 -1
  271. package/dist/web-office-store.d.ts +47 -4
  272. package/dist/web-office-store.js +124 -23
  273. package/dist/web-office-store.js.map +1 -1
  274. package/dist/web-settings-patch.d.ts +3 -2
  275. package/dist/web-settings-patch.js +7 -5
  276. package/dist/web-settings-patch.js.map +1 -1
  277. package/dist/workflow-templates/default.md +3 -3
  278. package/dist/working-recovery.d.ts +0 -2
  279. package/dist/working-recovery.js +2 -23
  280. package/dist/working-recovery.js.map +1 -1
  281. package/package.json +2 -2
  282. package/templates/windows-shortcuts.ps1 +18 -17
  283. package/dist/upgrade-notices.d.ts +0 -17
  284. package/dist/upgrade-notices.js +0 -93
  285. package/dist/upgrade-notices.js.map +0 -1
@@ -1,4 +1,8 @@
1
- import { hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
1
+ import { supportsVerifiedKiroMigration, getCachedKiroCliCompatibility, kiroV1DbPath, kiroV2SessionsDir } from "./backend/kiro.js";
2
+ import { listKiroV1Sessions, readKiroClassicLinks } from "./backend/kiro-identity.js";
3
+ import { readKiroV1SavedModel } from "./backend/kiro-retired-model.js";
4
+ import { sharedKiroV2StoreLane } from "./backend/kiro-v2-store.js";
5
+ import { SEPARATE_FEATURE_KEYS, hasLegacyOffice, normalizeOfficeStorage } from "./dashboard-permissions.js";
2
6
  import { loginNoticeMode, normalizeNoticeDelivery, setNoticeMode } from "./notice-delivery.js";
3
7
  import { DEFAULT_REPLY_TRIGGERS } from "./reply-triggers.js";
4
8
  import { WorkingRecoveryCoordinator, WORKING_RECOVERY_KIND, buildWorkingRecoveryPrompt } from "./working-recovery.js";
@@ -24,7 +28,7 @@ import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, pub
24
28
  import { CloudflaredProvider } from "./tunnel/cloudflared.js";
25
29
  import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
26
30
  import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
27
- import { ALL_OFFICES } from "./office-share.js";
31
+ import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
28
32
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
29
33
  import { ManagedTunnel } from "./tunnel/manager.js";
30
34
  import { withinBudget } from "./monotonic-budget.js";
@@ -43,11 +47,12 @@ import { join, dirname, basename, delimiter, isAbsolute, relative, resolve as re
43
47
  import { fileURLToPath } from "node:url";
44
48
  import { isDeepStrictEqual } from "node:util";
45
49
  import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
50
+ import { planKiroReconciliation, applyKiroReconciliation, kiroReconciliationPrompt } from "./kiro-reconcile.js";
46
51
  import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./kiro-migrate.js";
47
52
  import { fleetLabel } from "./fleet-label.js";
48
53
  import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
49
54
  import { formatUpdateProgress } from "./update-progress.js";
50
- import { sdNotify, sdNotifyBlocking, setSdNotifyWarn } from "./sd-notify.js";
55
+ import { sdNotify, sdNotifyBlocking, setSdNotifyWarn, startSdWatchdog } from "./sd-notify.js";
51
56
  import { StartupMemoryDeadline, fleetStopConcurrency } from "./startup-memory-deadline.js";
52
57
  import { readFleetMemory } from "./process-memory.js";
53
58
  import { MemoryPressure } from "./memory-pressure.js";
@@ -83,13 +88,13 @@ import { createBackend } from "./backend/factory.js";
83
88
  import { readEffortMetadata } from "./backend/effort-metadata.js";
84
89
  import { codexEffort, flagEffort } from "./backend/launch-effort.js";
85
90
  import { codexMetadataHomes, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./backend/codex-metadata.js";
86
- import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError } from "./backend/types.js";
91
+ import { CLI_ENV_TTL_MS, isModelCompatible, noteResolvedBinary, resolveBinary, SYSINFO_BACKEND_IDS, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
87
92
  import { createLogger, rotateLogIfNeeded } from "./logger.js";
88
93
  import { processAttachments } from "./channel/attachment-handler.js";
89
94
  import { routeToolCall } from "./channel/tool-router.js";
90
95
  import { Scheduler } from "./scheduler/index.js";
91
96
  import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
92
- import { TaskBoardWaker } from "./task-board-waker.js";
97
+ import { TaskBoardWaker, namesCard } from "./task-board-waker.js";
93
98
  import { TaskBoardError, waitsForPerson, wrapUpGap } from "./scheduler/task-board.js";
94
99
  import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
95
100
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
@@ -170,6 +175,7 @@ import { ThemeStore } from "./web-theme-store.js";
170
175
  import { OfficeStore, OfficeWriteLimiter } from "./web-office-store.js";
171
176
  import { OfficeSkinStore } from "./web-office-skins.js";
172
177
  import { OfficeActs } from "./web-office-acts.js";
178
+ import { OFFICE_PACK_TOOLS, OfficePackTools } from "./web-office-pack-tools.js";
173
179
  import { WebAppearance } from "./web-appearance.js";
174
180
  import { WebEffectLibrary } from "./web-effect-library.js";
175
181
  import { WebEffectTrials } from "./web-effect-trials.js";
@@ -225,10 +231,11 @@ import { ONBOARDING_BACKENDS, readPendingBackendSignIns } from "./backend-signin
225
231
  import { structuredSignInSupported } from "./structured-signin.js";
226
232
  import { handleSettingsRequest } from "./settings-api.js";
227
233
  import { setLocale, detectLocale, getLocale, t } from "./locale.js";
234
+ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteCardPrompt, REMOTE_CARD_TEXT_MAX } from "./remote-card-prompt.js";
228
235
  import { recentChatContext } from "./log-tail.js";
229
236
  import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
230
237
  import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
231
- import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
238
+ import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
232
239
  import { assertExplicitInstanceRemoval } from "./instance-removal.js";
233
240
  import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
234
241
  import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
@@ -247,8 +254,7 @@ import { isSetupComplete, markSetupComplete } from "./setup-marker.js";
247
254
  import { manualCleanupMessage, reapStaleTunnel } from "./tunnel/lease.js";
248
255
  import { buildToolPermissionsNotice } from "./tool-permissions-notice.js";
249
256
  import { NeedsYouHub } from "./needs-you-hub.js";
250
- import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
251
- import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
257
+ import { GENERAL_NAME, GENERAL_PAUSE_ERROR, adoptGeneralByName, freeGeneralName, isGeneralInstance } from "./general-instance.js";
252
258
  import { buildOrgChart } from "./web-org.js";
253
259
  import { topMemoryHogs } from "./memory-hogs.js";
254
260
  import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
@@ -478,6 +484,13 @@ const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
478
484
  const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
479
485
  const BACKEND_SELECT_CALLBACK_PREFIX = "backend-select:";
480
486
  const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
487
+ /** #2675: a card a CLI shows in place of its composer (kiro V3's Question / MCP input), posted with its answers. */
488
+ const REMOTE_CARD_CALLBACK_PREFIX = "card-answer:";
489
+ const REMOTE_CARD_CALLBACK_RE = /^card-answer:([0-9a-f]{32}):(opt-\d{1,2}|sub-all|sub-\d{1,2}|back|skip|decline|cancel|text)$/;
490
+ /** A posted card's buttons stay this long; the card itself waits in the pane for as long as it takes. */
491
+ const REMOTE_CARD_TIMEOUT_MS = 12 * 60 * 60_000;
492
+ /** A reply already taken as a card's answer is remembered this long, so another bot's copy of it is dropped (#2675). */
493
+ const REMOTE_CARD_TAKEN_TTL_MS = 10 * 60_000;
481
494
  const EXIT_RESTART_CALLBACK_PREFIX = "exit-restart:";
482
495
  const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
483
496
  /** #1386: how long a paused instance's marker reason is reused before it is read again (transitions drop it at once). */
@@ -486,6 +499,11 @@ const HANG_CALLBACK_PREFIX = "hang:";
486
499
  const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
487
500
  /** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
488
501
  const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
502
+ const KIRO_RECONCILE_CALLBACK_PREFIX = "kiro-reconcile:";
503
+ /** #2692: a fleet admin's Approve / Reject on an agent's pending skin-pack change, in General. */
504
+ const OFFICE_PACK_CALLBACK_PREFIX = "office-pack:";
505
+ /** #2692: the skin-pack tools' IPC types (answered as web theme tools). */
506
+ const OFFICE_PACK_IPC = new Set(OFFICE_PACK_TOOLS.map(tool => `fleet_${tool}`));
489
507
  /** Long enough for an admin to read the prompt; the move is judged again on the click. */
490
508
  const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
491
509
  /**
@@ -496,6 +514,7 @@ const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
496
514
  */
497
515
  const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
498
516
  HANG_CALLBACK_PREFIX, EXIT_RESTART_CALLBACK_PREFIX, OOM_RETRY_CALLBACK_PREFIX, INTERACTIVE_ASSIST_CALLBACK_PREFIX,
517
+ REMOTE_CARD_CALLBACK_PREFIX,
499
518
  ]);
500
519
  /**
501
520
  * Where a reply goes on a fleet with no chat platform (web track C4): it is "sent" by being shown in the
@@ -592,7 +611,6 @@ export function deriveSpawnConcurrency(freeMemMB, cores) {
592
611
  /** #926: how often reply obligations are checked, the owner-reminder grace, and the default overdue window. */
593
612
  const REPLY_OBLIGATION_SWEEP_MS = 30_000;
594
613
  const REPLY_REMINDER_GRACE_MS = 60_000;
595
- const DEFAULT_REPLY_OVERDUE_MINUTES = 15;
596
614
  /**
597
615
  * The filter `list_emojis` and `list_stickers` take (#1226). Values may come from the agent CLI as strings, so a
598
616
  * limit is read as a number and the flags accept "true". No limit means all of them — never zero.
@@ -656,32 +674,6 @@ export class FleetManager {
656
674
  laneFree: name => !this.isInstanceRestarting(name) && !this.activeDurableTargets.has(name)
657
675
  && outbox.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending"]) === 0,
658
676
  admit: (episode, tasks, advisory) => outbox.admitWorkingRecovery(episode, buildWorkingRecoveryPrompt(tasks, this.workingRecoveryCause, advisory), advisory),
659
- summary: (group, rows) => {
660
- const general = this.findGeneralInstance();
661
- if (!general || !this.daemons.has(general))
662
- return;
663
- // Our own informational notice must not supersede General's recovery
664
- // as unrelated pending work before it crosses the input boundary.
665
- const generalEpisode = store.get(general);
666
- if (generalEpisode?.outcome === "admitted" && generalEpisode.deliveryId) {
667
- const state = outbox.get(generalEpisode.deliveryId)?.state;
668
- if (!state || !["delivered", "uncertain", "failed", "cancelled"].includes(state))
669
- return;
670
- }
671
- const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
672
- && (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
673
- .map(row => `${row.instance} (${row.reason})`);
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
- }
684
- },
685
677
  log: (episode, outcome, reason) => {
686
678
  const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
687
679
  this.logger.info({ name: episode.instance, episode: episode.key, outcome, reason,
@@ -913,6 +905,8 @@ export class FleetManager {
913
905
  pendingNonceButtons = new Map();
914
906
  /** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
915
907
  pendingKiroMigrations = new Map();
908
+ pendingKiroReconciliations = new Map();
909
+ kiroMigrationRuns = new Map();
916
910
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
917
911
  needsYou = null;
918
912
  /** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
@@ -925,6 +919,12 @@ export class FleetManager {
925
919
  * adapter emits — whatever fields its payload carries — can claim a dashboard click's authority.
926
920
  */
927
921
  webPromptClicks = new WeakSet();
922
+ /** #2675: a dashboard click's typed answer, for the click it came with. */
923
+ webPromptTexts = new WeakMap();
924
+ /** #2675: the nonce of each instance's posted card. */
925
+ remoteCardPosts = new Map();
926
+ /** #2675: replies taken as a card's answer (`<chatId>|<messageId>` → monotonic expiry), so another bot's copy is dropped. */
927
+ takenCardReplies = new Map();
928
928
  /** The web clicks consumeNonceCallback actually claimed (the others were refused or lost a race). */
929
929
  webPromptClaims = new WeakSet();
930
930
  // Model failover state
@@ -1195,6 +1195,7 @@ export class FleetManager {
1195
1195
  needs: () => this.needsYouItems().map(i => i.instance),
1196
1196
  hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
1197
1197
  skins: this.officeSkins,
1198
+ levels: () => this.officeLevelsSnapshot(),
1198
1199
  },
1199
1200
  manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
1200
1201
  ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
@@ -1211,7 +1212,7 @@ export class FleetManager {
1211
1212
  * `share` null with `count`: "Revoke all".
1212
1213
  */
1213
1214
  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 office = !share ? "" : share.office === ALL_OFFICES ? t("office_share.all") : share.office === WHOLE_FLEET ? t("office_share.whole") : this.officeStore.list().offices.find(o => o.id === share.office)?.name ?? share.office;
1215
1216
  const minutes = share ? Math.round((share.expiresAt - share.createdAt) / 60_000) : 0;
1216
1217
  const text = event === "created" ? t("office_share.created_notice", who, office, minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`)
1217
1218
  : share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
@@ -1235,6 +1236,98 @@ export class FleetManager {
1235
1236
  changed: acts => this.emitSseEvent("office_acts", { acts, now: Date.now() }),
1236
1237
  }));
1237
1238
  }
1239
+ officePackToolsInstance;
1240
+ /** #2692: the agents' skin-pack tools — their log, the admins' approval mode and the pending changes (memory only). */
1241
+ get officePackTools() {
1242
+ return (this.officePackToolsInstance ??= new OfficePackTools({
1243
+ skins: this.officeSkins, offices: this.officeStore,
1244
+ allowedAgents: () => this.officePackAgents(),
1245
+ notice: (text, pending) => { void this.officePackNotice(text, pending); },
1246
+ settled: (id, text) => this.settleOfficePackPrompt(id, text),
1247
+ changed: what => {
1248
+ if (what.skins !== undefined)
1249
+ this.emitSseEvent("office_skins", { rev: what.skins });
1250
+ if (what.offices !== undefined)
1251
+ this.emitSseEvent("offices", { rev: what.offices });
1252
+ if (what.changes)
1253
+ this.emitSseEvent("office_pack_changes", {});
1254
+ },
1255
+ logger: this.logger,
1256
+ }));
1257
+ }
1258
+ /** #2692: the instances whose `office_packs` feature is on (their own switch, else the defaults'): the Office page lists them. */
1259
+ officePackAgents() {
1260
+ const names = new Set([...Object.keys(this.fleetConfig?.instances ?? {}), ...(this.classicChannels?.getAll() ?? []).map(ch => ch.instanceName)]);
1261
+ return [...names].filter(name => dashboardRefusalIn(this.fleetConfig, name, "office_pack_upload") === null);
1262
+ }
1263
+ /** #2692: the nonce of each pending pack change's prompt in General (its buttons are retired when it is decided). */
1264
+ officePackPrompts = new Map();
1265
+ /**
1266
+ * #2692: tell the fleet's admins of an agent's pack change — one short message in General (notifyFleetError, never a
1267
+ * sign-in notice). A pending one comes with Approve / Reject (a fleet admin's, as a kiro move); where no General can
1268
+ * carry buttons it is the plain message, and the Office page decides it.
1269
+ */
1270
+ async officePackNotice(text, pending) {
1271
+ if (!pending) {
1272
+ this.notifyFleetError(text, { throttle: false });
1273
+ return;
1274
+ }
1275
+ const general = this.findGeneralInstance();
1276
+ const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
1277
+ const adapter = general ? this.getAdapterForInstance(general) : undefined;
1278
+ const chatId = general ? this.getGroupIdForInstance(general) : undefined;
1279
+ const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
1280
+ if (general && adapterId && adapter && chatId) {
1281
+ try {
1282
+ const nonce = await this.postNonceButtonPromptOrThrow({
1283
+ prefix: OFFICE_PACK_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: pending.instance, adapter, adapterId, chatId,
1284
+ ...(topic ? { threadId: topic } : {}), message: text,
1285
+ choices: [{ action: "approve", label: t("office_packs.approve_button") }, { action: "reject", label: t("office_packs.reject_button") }],
1286
+ expiredText: t("office_packs.expired", ""), timeoutMs: Math.max(1_000, pending.expires_at - Date.now()),
1287
+ });
1288
+ if (this.officePackTools.pendingViews().some(v => v.id === pending.id))
1289
+ this.officePackPrompts.set(pending.id, nonce);
1290
+ else
1291
+ this.retireOfficePackNonce(nonce, t("office_packs.gone"));
1292
+ return;
1293
+ }
1294
+ catch (err) {
1295
+ this.logger.info({ err: err.message }, "Office pack approval prompt unavailable; the plain notice and the Office page remain");
1296
+ }
1297
+ }
1298
+ this.notifyFleetError(text, { throttle: false });
1299
+ }
1300
+ retireOfficePackNonce(nonce, text) {
1301
+ const entry = this.pendingNonceButtons.get(nonce);
1302
+ if (!entry || entry.prefix !== OFFICE_PACK_CALLBACK_PREFIX)
1303
+ return;
1304
+ this.pendingNonceButtons.delete(nonce);
1305
+ clearTimeout(entry.timer);
1306
+ void this.retireNonceButtons(entry, entry.messageId ?? "", text);
1307
+ }
1308
+ /** #2692: a pending change was decided (here or on the page) or expired: its prompt says so and loses its buttons. */
1309
+ settleOfficePackPrompt(id, text) {
1310
+ const nonce = this.officePackPrompts.get(id);
1311
+ this.officePackPrompts.delete(id);
1312
+ if (nonce)
1313
+ this.retireOfficePackNonce(nonce, text);
1314
+ }
1315
+ /** #2692: Approve / Reject on a pending pack change. Any fleet admin (the default nonce authorization), in General. */
1316
+ async handleOfficePackCallback(data, adapterId, adapter) {
1317
+ const claimed = this.consumeNonceCallback(OFFICE_PACK_CALLBACK_PREFIX, /^office-pack:([0-9a-f]{32}):(approve|reject)$/, data, adapterId, adapter);
1318
+ if (claimed === null)
1319
+ return false;
1320
+ if (claimed === "consumed")
1321
+ return true;
1322
+ const { entry, action } = claimed;
1323
+ const nonce = data.callbackData.slice(OFFICE_PACK_CALLBACK_PREFIX.length, OFFICE_PACK_CALLBACK_PREFIX.length + 32);
1324
+ const id = [...this.officePackPrompts].find(([, n]) => n === nonce)?.[0];
1325
+ if (id)
1326
+ this.officePackPrompts.delete(id);
1327
+ const decided = id ? this.officePackTools.decide(id, action, t("settings_confirm.admin_label", data.userId ?? "")) : null;
1328
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, decided?.text ?? t("office_packs.gone"));
1329
+ return true;
1330
+ }
1238
1331
  /**
1239
1332
  * #2578: each instance's state as the org chart shows it (fleet-org.js orgState, from the same fields the status
1240
1333
  * frame and the Needs-you list carry) and its display name (resolveInstanceIdentity, as getUiStatus) — read once.
@@ -1245,7 +1338,10 @@ export class FleetManager {
1245
1338
  needs.set(item.instance, (needs.get(item.instance) ?? 0) + 1);
1246
1339
  const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
1247
1340
  let profiles = null;
1341
+ // #2661: the levels as last worked out (never waits; none while off or not worked out yet).
1342
+ const levels = this.officeLevelsSnapshot();
1248
1343
  return {
1344
+ level: name => (levels && Object.prototype.hasOwnProperty.call(levels, name) ? levels[name].level : undefined),
1249
1345
  stateOf: name => {
1250
1346
  if (!this.isKnownInstance(name))
1251
1347
  return "missing";
@@ -1797,12 +1893,96 @@ export class FleetManager {
1797
1893
  this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
1798
1894
  }
1799
1895
  kiroMigrateDeps() {
1896
+ let token;
1897
+ const current = (name) => {
1898
+ const run = this.kiroMigrationRuns.get(name);
1899
+ return !!token && run?.token === token && !this.shuttingDown
1900
+ && (this.explicitStopGeneration.get(name) ?? 0) === run.stopGeneration;
1901
+ };
1800
1902
  return {
1801
- agendHome: getAgendHome(),
1802
- fleetConfig: () => this.fleetConfig,
1803
- saveFleetConfig: () => this.saveFleetConfig(),
1903
+ agendHome: getAgendHome(), fleetConfig: () => this.fleetConfig, saveFleetConfig: () => this.saveFleetConfig(),
1804
1904
  status: name => this.getInstanceStatus(name),
1805
- restart: name => this.restartSingleInstance(name, { explicit: true }),
1905
+ reserve: name => {
1906
+ if (this.kiroMigrationRuns.has(name) || this.restartsInFlight.has(name) || this.stopsInFlight.has(name))
1907
+ throw new Error(t("kiro_migrate.in_progress"));
1908
+ token = Symbol(name);
1909
+ this.kiroMigrationRuns.set(name, { token, stopGeneration: this.explicitStopGeneration.get(name) ?? 0 });
1910
+ return { current: () => current(name), release: () => {
1911
+ if (this.kiroMigrationRuns.get(name)?.token === token)
1912
+ this.kiroMigrationRuns.delete(name);
1913
+ this.scheduleDeliveryOutboxPump();
1914
+ } };
1915
+ },
1916
+ preflight: async (name, plan) => {
1917
+ const compat = getCachedKiroCliCompatibility(resolveBinary("kiro-cli"));
1918
+ if (!supportsVerifiedKiroMigration(compat))
1919
+ throw new Error(t("kiro_migrate.version_required"));
1920
+ const daemon = this.daemons.get(name);
1921
+ if (this.getInstanceStatus(name) === "running" && (!daemon || !await daemon.kiroMigrationIdle()))
1922
+ throw new Error(t("kiro_migrate.idle_required"));
1923
+ if (!current(name) || daemon && this.daemons.get(name) !== daemon)
1924
+ throw new Error(t("kiro_migrate.superseded"));
1925
+ const inst = this.fleetConfig?.instances[name];
1926
+ const model = daemon?.getModelOverride() ?? inst?.model ?? (plan.classicId ? readKiroV1SavedModel({ id: plan.classicId,
1927
+ workingDirectory: plan.workingDirectory, dbPath: kiroV1DbPath(plan.credentialProfile) }) : undefined);
1928
+ if (!model && plan.classicId)
1929
+ throw new Error(t("kiro_migrate.model_unknown"));
1930
+ return model ?? "";
1931
+ },
1932
+ restart: async (name) => {
1933
+ if (!current(name))
1934
+ throw new Error(t("kiro_migrate.superseded"));
1935
+ const run = this.kiroMigrationRuns.get(name);
1936
+ const expected = run.stopGeneration + 1;
1937
+ // This restart owns exactly one stop. A person's concurrent stop must not be hidden by the workflow.
1938
+ try {
1939
+ await this.restartSingleInstance(name, { explicit: true, kiroMigration: token });
1940
+ }
1941
+ finally {
1942
+ // Even a failed replacement owns its completed stop; retain authority to clean up/pause it.
1943
+ if (this.kiroMigrationRuns.get(name) === run && this.explicitStopGeneration.get(name) === expected)
1944
+ run.stopGeneration = expected;
1945
+ }
1946
+ if (this.explicitStopGeneration.get(name) !== expected)
1947
+ throw new Error(t("kiro_migrate.superseded"));
1948
+ },
1949
+ verify: async (name, id, model, archived) => {
1950
+ const daemon = this.daemons.get(name), deadline = performance.now() + 60_000;
1951
+ if (!daemon)
1952
+ throw new Error(t("kiro_migrate.verification_failed"));
1953
+ while (performance.now() < deadline) {
1954
+ if (!current(name) || this.daemons.get(name) !== daemon)
1955
+ throw new Error(t("kiro_migrate.superseded"));
1956
+ const observed = await daemon.kiroMigrationObservation();
1957
+ if (observed) {
1958
+ if (observed.sessionId !== id || observed.model !== model || resolvePath(observed.transcript) !== resolvePath(kiroV2SessionsDir(), id + ".jsonl"))
1959
+ throw new Error(t("kiro_migrate.verification_failed"));
1960
+ const inst = this.fleetConfig.instances[name];
1961
+ const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: kiroV2SessionsDir(), migration: {
1962
+ agendHome: getAgendHome(), instance: name, workingDirectory: inst.working_directory, credentialProfile: instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli"),
1963
+ sessionId: id, sessionsDir: kiroV2SessionsDir(), dbPath: kiroV1DbPath(instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli")),
1964
+ model, archived, outboxDbPath: join(this.dataDir, "delivery-outbox.db"), knownInstances: Object.keys(this.fleetConfig.instances),
1965
+ } });
1966
+ if (!current(name) || this.daemons.get(name) !== daemon)
1967
+ throw new Error(t("kiro_migrate.superseded"));
1968
+ if (reply.kind !== "ok" || !reply.verification)
1969
+ throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_migrate.verification_failed"));
1970
+ return reply.verification;
1971
+ }
1972
+ await new Promise(r => setTimeout(r, 500));
1973
+ }
1974
+ throw new Error(t("kiro_migrate.verification_failed"));
1975
+ },
1976
+ stop: async (name) => { try {
1977
+ await this.stopInstance(name);
1978
+ }
1979
+ finally {
1980
+ writePausedMarker(this.getInstanceDir(name), Date.now(), "error");
1981
+ } },
1982
+ readClassicStore: async (workingDirectory, credentialProfile) => {
1983
+ const dbPath = kiroV1DbPath(credentialProfile);
1984
+ return listKiroV1Sessions(workingDirectory, dbPath, await readKiroClassicLinks(workingDirectory, dbPath));
1985
+ },
1806
1986
  };
1807
1987
  }
1808
1988
  /**
@@ -1810,7 +1990,7 @@ export class FleetManager {
1810
1990
  * here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
1811
1991
  */
1812
1992
  async requestKiroMigration(name, startFresh) {
1813
- const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
1993
+ const judged = await planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
1814
1994
  if (!judged.ok)
1815
1995
  return judged;
1816
1996
  const general = this.findGeneralInstance();
@@ -1860,8 +2040,77 @@ export class FleetManager {
1860
2040
  const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
1861
2041
  this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
1862
2042
  this.notifyFleetError(outcome.text);
1863
- if (outcome.ok)
1864
- this.notifyInstanceTopic(plan.name, outcome.text);
2043
+ this.notifyInstanceTopic(plan.name, outcome.text);
2044
+ return true;
2045
+ }
2046
+ kiroReconcileDeps() {
2047
+ return { ...this.kiroMigrateDeps(), store: async (input, plan, actor, rollbackReceipt) => {
2048
+ const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: input.sessionsDir, reconciliation: { input, plan, actor, rollbackReceipt } });
2049
+ if (reply.kind !== "ok" || !reply.reconciliation)
2050
+ throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_reconcile.changed"));
2051
+ return reply.reconciliation;
2052
+ } };
2053
+ }
2054
+ async requestKiroReconciliation(name, keep, archive, rollbackReceipt) {
2055
+ try {
2056
+ if (!this.eventLog)
2057
+ return { ok: false, error: t("kiro_reconcile.audit_failed") };
2058
+ const plan = await planKiroReconciliation(this.kiroReconcileDeps(), name, keep, archive, rollbackReceipt);
2059
+ const general = this.findGeneralInstance(), adapterId = general ? this.getInstanceAdapterId(general) : undefined;
2060
+ const adapter = general ? this.getAdapterForInstance(general) : undefined, chatId = general ? this.getGroupIdForInstance(general) : undefined;
2061
+ const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
2062
+ if (!general || !adapterId || !adapter || !chatId)
2063
+ return { ok: false, error: t("kiro_migrate.no_general") };
2064
+ const nonce = await this.postNonceButtonPromptOrThrow({ prefix: KIRO_RECONCILE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name,
2065
+ adapter, adapterId, chatId, ...(topic ? { threadId: topic } : {}), message: kiroReconciliationPrompt(plan),
2066
+ choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
2067
+ expiredText: t("kiro_migrate.expired", name), timeoutMs: KIRO_MIGRATE_TIMEOUT_MS });
2068
+ this.pendingKiroReconciliations.set(nonce, plan);
2069
+ setTimeout(() => this.pendingKiroReconciliations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000).unref?.();
2070
+ this.eventLog?.insert(name, "kiro_reconcile", { phase: "requested", keep, archive, rollbackReceipt, message: kiroReconciliationPrompt(plan) });
2071
+ return { ok: true, text: t("kiro_migrate.requested") };
2072
+ }
2073
+ catch (error) {
2074
+ return { ok: false, error: error instanceof SyntaxError ? "Malformed reconciliation data" : error.message };
2075
+ }
2076
+ }
2077
+ async handleKiroReconcileCallback(data, adapterId, adapter) {
2078
+ const claimed = this.consumeNonceCallback(KIRO_RECONCILE_CALLBACK_PREFIX, /^kiro-reconcile:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
2079
+ if (claimed === null)
2080
+ return false;
2081
+ if (claimed === "consumed")
2082
+ return true;
2083
+ const { entry, action } = claimed;
2084
+ const nonce = data.callbackData.slice(KIRO_RECONCILE_CALLBACK_PREFIX.length, KIRO_RECONCILE_CALLBACK_PREFIX.length + 32);
2085
+ const plan = this.pendingKiroReconciliations.get(nonce);
2086
+ this.pendingKiroReconciliations.delete(nonce);
2087
+ if (action === "cancel" || !plan) {
2088
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
2089
+ this.eventLog?.insert(entry.instanceName, "kiro_reconcile", { phase: "cancelled_or_expired", userId: data.userId });
2090
+ return true;
2091
+ }
2092
+ let outcome;
2093
+ try {
2094
+ if (!this.eventLog)
2095
+ throw new Error(t("kiro_reconcile.audit_failed"));
2096
+ this.eventLog.insert(plan.name, "kiro_reconcile", { phase: "confirmed", userId: data.userId, keep: plan.input.sessionId, archive: plan.input.archiveId });
2097
+ }
2098
+ catch {
2099
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.audit_failed"));
2100
+ this.notifyFleetError(t("kiro_reconcile.audit_failed"));
2101
+ return true;
2102
+ }
2103
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.applying", plan.name));
2104
+ outcome = await applyKiroReconciliation(this.kiroReconcileDeps(), plan, `${adapterId}:${data.userId}`);
2105
+ try {
2106
+ this.eventLog.insert(plan.name, "kiro_reconcile", { phase: outcome.ok ? "completed" : "failed", userId: data.userId, receipt: outcome.receipt, message: outcome.text });
2107
+ }
2108
+ catch {
2109
+ this.logger.error({ name: plan.name, receipt: outcome.receipt }, "Kiro reconciliation final Log entry failed; confirmed intent remains in the audit and archive");
2110
+ }
2111
+ this.logger.info({ name: plan.name, ok: outcome.ok, receipt: outcome.receipt }, "Kiro explicit reconciliation");
2112
+ this.notifyFleetError(outcome.text);
2113
+ this.notifyInstanceTopic(plan.name, outcome.text);
1865
2114
  return true;
1866
2115
  }
1867
2116
  async handleSettingsChangeCallback(data, adapterId, adapter) {
@@ -2327,27 +2576,8 @@ export class FleetManager {
2327
2576
  const daemon = this.daemons.get(name);
2328
2577
  if (daemon?.bootId === daemonBootId)
2329
2578
  this.listenForWorkingState(name, daemon);
2330
- const outbox = this.deliveryOutbox;
2331
- if (!outbox)
2332
- return;
2333
- for (const prior of outbox.getUnansweredAccepted(name, daemonBootId)) {
2334
- try {
2335
- outbox.admitPostRestartOutcomeNotice(prior, daemonBootId);
2336
- }
2337
- catch (err) {
2338
- this.logger.error({ err, source: name, operationId: prior.operationId }, "Could not persist post-restart delivery outcome notice");
2339
- }
2340
- }
2341
- // #926: a restart kills the turn that was working on a request, and a
2342
- // resumed CLI does not pick it back up. Remind the owner once per restart.
2343
- try {
2344
- const now = new Date();
2345
- this.expireReplyObligations(now);
2346
- outbox.remindReplyObligations(name, { reason: "restart", graceMs: 0, now, since: now, restartId: daemonBootId });
2347
- }
2348
- catch (err) {
2349
- this.logger.error({ err, owner: name }, "Could not persist the post-restart reply reminder");
2350
- }
2579
+ // #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
2580
+ // request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
2351
2581
  this.scheduleDeliveryOutboxPump();
2352
2582
  }
2353
2583
  workingStateListeners;
@@ -2431,9 +2661,8 @@ export class FleetManager {
2431
2661
  }
2432
2662
  }
2433
2663
  /**
2434
- * #926: the two reply-obligation safety nets. Idle owners with an unanswered
2435
- * request get one reminder (after the grace); requesters get one overdue
2436
- * notice when the owner is not working and nothing came back in time.
2664
+ * #926: idle owners with an unanswered request get one reminder (after the grace). #2802: the only reply notice —
2665
+ * requesters are no longer told about overdue or expired asks (they can check delivery_status / describe_instance).
2437
2666
  */
2438
2667
  sweepReplyObligations(now = new Date()) {
2439
2668
  const outbox = this.deliveryOutbox;
@@ -2444,10 +2673,8 @@ export class FleetManager {
2444
2673
  this.expireReplyObligations(now);
2445
2674
  for (const name of this.daemons.keys()) {
2446
2675
  if (stateOf(name) === "idle")
2447
- outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS, reason: "turn-ended" });
2676
+ outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS });
2448
2677
  }
2449
- const minutes = this.fleetConfig?.defaults?.reply_overdue_minutes ?? DEFAULT_REPLY_OVERDUE_MINUTES;
2450
- outbox.notifyOverdueReplyObligations({ now, overdueMs: minutes * 60_000, ownerIdle: name => stateOf(name) !== "working" });
2451
2678
  }
2452
2679
  catch (err) {
2453
2680
  this.logger.error({ err }, "Reply-obligation sweep failed");
@@ -5441,7 +5668,10 @@ export class FleetManager {
5441
5668
  */
5442
5669
  resumePaused = false,
5443
5670
  /** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
5444
- transition, execution) {
5671
+ transition, execution, kiroMigration) {
5672
+ const migration = this.kiroMigrationRuns?.get(name);
5673
+ if (migration && migration.token !== kiroMigration)
5674
+ throw new Error(t("kiro_migrate.in_progress"));
5445
5675
  execution?.assert();
5446
5676
  if (this.sessionOomStopped(name)) {
5447
5677
  if (resumePaused)
@@ -5937,6 +6167,11 @@ export class FleetManager {
5937
6167
  stopOnUnsupportedCli(name, err, what) {
5938
6168
  if (!(err instanceof UnsupportedCliError))
5939
6169
  return false;
6170
+ if (err instanceof ResumeBlockedError) {
6171
+ this.logger.error({ name, reason: err.message }, `Not starting ${what}: retained session needs store repair`);
6172
+ void this.notifyFleetError(t("fleet.resume_blocked", name, err.message, err.remedy));
6173
+ return true;
6174
+ }
5940
6175
  this.logger.error({ name, reason: err.message }, `Not starting ${what}: the installed CLI cannot run it as configured`);
5941
6176
  this.queueUnsupportedCliNotice(name, err.message);
5942
6177
  this.queueKiroIncompatNotice(name, err.message);
@@ -6136,6 +6371,9 @@ export class FleetManager {
6136
6371
  }
6137
6372
  /** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
6138
6373
  async restartSingleInstance(name, opts) {
6374
+ const migration = this.kiroMigrationRuns?.get(name);
6375
+ if (migration && migration.token !== opts?.kiroMigration)
6376
+ throw new Error(t("kiro_migrate.in_progress"));
6139
6377
  if (opts?.recoveryCurrent && !opts.recoveryCurrent())
6140
6378
  throw new Error(t("exit.expired", name));
6141
6379
  const retry = opts?.explicit ? opts.sessionOomRetry ?? captureSessionOomRetry(this.getInstanceDir(name)) : undefined;
@@ -6196,7 +6434,7 @@ export class FleetManager {
6196
6434
  return run;
6197
6435
  }
6198
6436
  isInstanceRestarting(name) {
6199
- return this.restartsInFlight.has(name);
6437
+ return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
6200
6438
  }
6201
6439
  /** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
6202
6440
  restartInFlight(name) {
@@ -6301,7 +6539,8 @@ export class FleetManager {
6301
6539
  return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, this.fleetConfig?.defaults, backend) : null };
6302
6540
  });
6303
6541
  const topicMode = this.fleetConfig?.channel?.mode === "topic";
6304
- await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
6542
+ await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => opts?.kiroMigration ? this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition, undefined, opts.kiroMigration)
6543
+ : this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
6305
6544
  this.stormWindow.markRecovered(name);
6306
6545
  return;
6307
6546
  }
@@ -7057,9 +7296,22 @@ export class FleetManager {
7057
7296
  ...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
7058
7297
  }).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
7059
7298
  // #2348: `explicit` — grants are the scope (default deny); false: the derived scope of #1920, until grants are saved.
7060
- return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), connections, guests,
7299
+ // #2667: the ClassicBot rooms there are now, apart from fleet.yaml's instances (a grant may name either: guestReach),
7300
+ // with its display name as the sidebar resolves it (a profile alias, else the room's own), when it has one.
7301
+ const profiles = profileIdentities(this.dataDir);
7302
+ const rooms = (this.classicChannels?.getAll() ?? []).filter((r) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, r.instanceName))
7303
+ .map((r) => {
7304
+ const shown = resolveInstanceIdentity({ classic: r, profile: profiles.get(r.instanceName) }).display_name;
7305
+ return { name: r.instanceName, ...(shown && shown !== r.instanceName ? { display_name: shown } : {}) };
7306
+ })
7307
+ .sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
7308
+ return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), rooms, connections, guests,
7061
7309
  audit: this.guestAudit() };
7062
7310
  }
7311
+ /** #2667: the ClassicBot rooms' instance names now — what a settings save's validation knows besides fleet.yaml's instances. */
7312
+ classicRoomNames() {
7313
+ return (this.classicChannels?.getAll() ?? []).map((r) => r.instanceName);
7314
+ }
7063
7315
  /**
7064
7316
  * #2499 review: the guest audit an admin reads — the newest grant changes and admin-sent codes from the event log
7065
7317
  * (`fleet` rows: no agent's Log shows them). Through guestOverview only: Settings' admin gate, never over the public
@@ -7868,7 +8120,7 @@ export class FleetManager {
7868
8120
  setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
7869
8121
  if (this.watchdogTimer)
7870
8122
  clearInterval(this.watchdogTimer);
7871
- this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
8123
+ this.watchdogTimer = startSdWatchdog();
7872
8124
  this.eventLoopWatch?.stop();
7873
8125
  this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
7874
8126
  try {
@@ -8082,8 +8334,8 @@ export class FleetManager {
8082
8334
  const channelConfigs = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
8083
8335
  this.warnUnboundGeneralChannelIds(fleet);
8084
8336
  const primaryAdapterId = channelConfigs[0] ? (channelConfigs[0].id ?? channelConfigs[0].type) : undefined;
8337
+ let generalsCreated = adoptGeneralByName(fleet, this.logger, dir => insideManagedWorkspaces(dir) || resolvePath(dir) === join(getAgendHome(), GENERAL_NAME));
8085
8338
  const generalInstances = Object.entries(fleet.instances).filter(([, inst]) => inst.general_topic === true);
8086
- let generalsCreated = false;
8087
8339
  // Collect unbound generals (no channel_id set) for auto-assignment
8088
8340
  const unboundGenerals = generalInstances.filter(([, inst]) => !inst.channel_id);
8089
8341
  // Track which adapters still need a general
@@ -8143,9 +8395,16 @@ export class FleetManager {
8143
8395
  }
8144
8396
  // Phase 3: Create new generals for any remaining adapters
8145
8397
  for (const need of needsGeneral) {
8146
- const name = channelConfigs.length > 1 ? `general-${need.adapterId}` : "general";
8147
- if (fleet.instances[name])
8398
+ let name = channelConfigs.length > 1 ? `general-${need.adapterId}` : GENERAL_NAME;
8399
+ if (fleet.instances[name]?.general_topic)
8148
8400
  continue;
8401
+ if (fleet.instances[name]) {
8402
+ // M-2069: the name is an agent's that is not the General (adoptGeneralByName left it: it works in a project
8403
+ // of the user's) — the fleet still gets its General, under a free name, never none.
8404
+ const taken = name;
8405
+ name = freeGeneralName(fleet.instances, taken);
8406
+ this.logger.warn({ taken, name }, "An agent is named like the General but is not one — creating the General under a free name; rename that agent to clear this");
8407
+ }
8149
8408
  this.logger.warn({ adapter: need.adapterId, name }, "No general instance for adapter — auto-creating");
8150
8409
  const generalDir = join(getAgendHome(), name);
8151
8410
  mkdirSync(generalDir, { recursive: true, mode: 0o700 });
@@ -8425,8 +8684,6 @@ export class FleetManager {
8425
8684
  }
8426
8685
  await this.sendFleetStartCompletionFallback(String(fleet.channel.group_id), text, generalThreadId != null ? String(generalThreadId) : undefined);
8427
8686
  }
8428
- // After "fleet ready", so the notice is not the first thing people see of a restart.
8429
- void this.announceWebChatOnce(agendVersion);
8430
8687
  // #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
8431
8688
  this.startPublicLinkAutoOpen();
8432
8689
  }
@@ -9935,6 +10192,10 @@ export class FleetManager {
9935
10192
  }))
9936
10193
  return;
9937
10194
  }
10195
+ // #2675: a reply to a posted question card is its typed answer (judged by who may talk to that instance), not a
10196
+ // message for the instance — which could not take one while its card is up anyway.
10197
+ if (await this.takeRemoteCardReply(msg))
10198
+ return;
9938
10199
  // Access control — classic channels are open to all, others require an allowed
9939
10200
  // user. This runs before the dedup claim, and is judged by the adapter that
9940
10201
  // owns the topic rather than the one that happened to receive the message:
@@ -10557,34 +10818,6 @@ export class FleetManager {
10557
10818
  * their fleet is organised. Both stay theirs — this only makes sure they are
10558
10819
  * not discovered by an agent failing at three in the morning.
10559
10820
  */
10560
- /**
10561
- * #1366: the first time a fleet runs 2.2 or later, tell each chat platform's General — once — that its agents can
10562
- * now be talked to from a browser. Claimed in upgrade-notices.json before the send, released if the send fails.
10563
- * Platforms with nowhere to post fleet notices (no group, or a Discord fleet with no General channel) are skipped
10564
- * and not recorded, so they are told once they have one.
10565
- */
10566
- async announceWebChatOnce(version) {
10567
- if (!hasWebChat(version))
10568
- return;
10569
- const path = upgradeNoticesPath(this.dataDir);
10570
- for (const [adapterId, world] of this.worlds) {
10571
- if (this.shuttingDown)
10572
- return;
10573
- const target = this.fleetNoticeTarget(adapterId);
10574
- if (!target)
10575
- continue;
10576
- if (!claimNotice(path, WEB_CHAT_NOTICE, adapterId))
10577
- continue;
10578
- try {
10579
- await world.adapter.sendText(target.chatId, t("upgrade.web_chat"), target.opts);
10580
- this.logger.info({ adapterId, notice: WEB_CHAT_NOTICE }, "Announced web chat in General");
10581
- }
10582
- catch (err) {
10583
- const released = releaseNotice(path, WEB_CHAT_NOTICE, adapterId);
10584
- this.logger.warn({ err, adapterId, released }, "Could not announce web chat in General — will try at the next start");
10585
- }
10586
- }
10587
- }
10588
10821
  announceToolPermissionsChange() {
10589
10822
  try {
10590
10823
  const thirtyDaysAgo = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 19).replace("T", " ");
@@ -11326,7 +11559,7 @@ export class FleetManager {
11326
11559
  if (!ipc || !fleetRequestId)
11327
11560
  return;
11328
11561
  const type = String(msg.type);
11329
- const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" ? "fleet_web_theme_response"
11562
+ const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" || OFFICE_PACK_IPC.has(type) ? "fleet_web_theme_response"
11330
11563
  : type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
11331
11564
  : type.startsWith("fleet_decision_") ? "fleet_decision_response"
11332
11565
  : type === "fleet_task" ? "fleet_task_response"
@@ -11381,7 +11614,10 @@ export class FleetManager {
11381
11614
  || type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
11382
11615
  || type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
11383
11616
  || type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
11384
- || type === "fleet_office_view" || type === "fleet_office_set") {
11617
+ || type === "fleet_office_view" || type === "fleet_office_set"
11618
+ // #2692: the skin packs (OFFICE_PACK_IPC), answered as web theme tools.
11619
+ || type === "fleet_office_pack_check" || type === "fleet_office_pack_upload" || type === "fleet_office_pack_delete"
11620
+ || type === "fleet_office_pack_restore" || type === "fleet_office_pack_list" || type === "fleet_office_set_style") {
11385
11621
  this.handleWebTheme(name, msg);
11386
11622
  return;
11387
11623
  }
@@ -11403,7 +11639,8 @@ export class FleetManager {
11403
11639
  ? this.appearanceTools.handle(instanceName, tool, args)
11404
11640
  : tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
11405
11641
  : tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
11406
- : this.webAppearance.handleTool(instanceName, tool, args);
11642
+ : OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
11643
+ : this.webAppearance.handleTool(instanceName, tool, args);
11407
11644
  ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
11408
11645
  }
11409
11646
  catch (err) {
@@ -11841,6 +12078,8 @@ export class FleetManager {
11841
12078
  return this.agentMods.handle(instance, tool, args);
11842
12079
  if (tool === "office_view" || tool === "office_set")
11843
12080
  return this.officeActs.handle(instance, tool, args); // #2578
12081
+ if (OFFICE_PACK_IPC.has(`fleet_${tool}`))
12082
+ return this.officePackTools.handle(instance, tool, args); // #2692
11844
12083
  return this.webAppearance.handleTool(instance, tool, args);
11845
12084
  }
11846
12085
  async handlePreviewEmojisHttp(instance, args) {
@@ -12279,12 +12518,21 @@ export class FleetManager {
12279
12518
  normalizeOfficeStorage(this.fleetConfig);
12280
12519
  explicitPatches = explicitPatches.flatMap(patch => {
12281
12520
  const path = patch.path;
12282
- if (path.at(-2) === "dashboard" && path.at(-1) === "office" && !patch.remove) {
12283
- return [{ ...patch, path: [...path.slice(0, -2), "office_access"] }, { path, remove: true }];
12284
- }
12285
- if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.hasOwn(patch.value, "office")) {
12286
- const { office, ...dashboard } = patch.value;
12287
- return [{ ...patch, value: dashboard }, { path: [...path.slice(0, -1), "office_access"], value: office, remove: office === null }];
12521
+ // #2601 (and #2692's office_packs): a separately kept feature is written under its own key, never in `dashboard`.
12522
+ const separate = path.at(-2) === "dashboard" ? SEPARATE_FEATURE_KEYS[path.at(-1)] : undefined;
12523
+ if (separate && !patch.remove) {
12524
+ return [{ ...patch, path: [...path.slice(0, -2), separate] }, { path, remove: true }];
12525
+ }
12526
+ if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.keys(SEPARATE_FEATURE_KEYS).some(f => Object.hasOwn(patch.value, f))) {
12527
+ const dashboard = { ...patch.value }, moved = [];
12528
+ for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
12529
+ if (!Object.hasOwn(dashboard, feature))
12530
+ continue;
12531
+ const value = dashboard[feature];
12532
+ delete dashboard[feature];
12533
+ moved.push({ path: [...path.slice(0, -1), key], value, remove: value === null });
12534
+ }
12535
+ return [{ ...patch, value: dashboard }, ...moved];
12288
12536
  }
12289
12537
  // #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
12290
12538
  if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
@@ -12398,11 +12646,11 @@ export class FleetManager {
12398
12646
  const previous = daemon.getConfigSnapshot?.();
12399
12647
  if (!next || !previous)
12400
12648
  continue;
12401
- if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
12649
+ if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access && previous.office_pack_access === next.office_pack_access)
12402
12650
  continue;
12403
12651
  // Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
12404
12652
  // 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 });
12653
+ daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null, office_pack_access: next.office_pack_access ?? null });
12406
12654
  if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
12407
12655
  this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
12408
12656
  }
@@ -12436,7 +12684,7 @@ export class FleetManager {
12436
12684
  logConfigWarningsAtStartup() {
12437
12685
  let warnings = [];
12438
12686
  try {
12439
- warnings = validateFleetConfig(this.rawFleetConfig).warnings;
12687
+ warnings = validateFleetConfig(this.rawFleetConfig, { rooms: readClassicRoomNames(this.dataDir) }).warnings;
12440
12688
  }
12441
12689
  catch (err) {
12442
12690
  this.logger.debug({ err }, "fleet.yaml validation failed (startup warnings)");
@@ -12467,11 +12715,13 @@ export class FleetManager {
12467
12715
  const patches = [];
12468
12716
  const raw = this.rawFleetConfig;
12469
12717
  for (const [path, item] of [[["defaults"], raw.defaults], ...Object.entries(raw.instances ?? {}).map(([name, value]) => [["instances", name], value])]) {
12470
- if (!item?.dashboard || !Object.hasOwn(item.dashboard, "office"))
12471
- continue;
12472
- patches.push({ path: [...path, "dashboard", "office"], remove: true });
12473
- if (!Object.hasOwn(item, "office_access") && typeof item.dashboard.office === "boolean")
12474
- patches.push({ path: [...path, "office_access"], value: item.dashboard.office });
12718
+ for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
12719
+ if (!item?.dashboard || !Object.hasOwn(item.dashboard, feature))
12720
+ continue;
12721
+ patches.push({ path: [...path, "dashboard", feature], remove: true });
12722
+ if (!Object.hasOwn(item, key) && typeof item.dashboard[feature] === "boolean")
12723
+ patches.push({ path: [...path, key], value: item.dashboard[feature] });
12724
+ }
12475
12725
  }
12476
12726
  this.saveFleetConfig(patches);
12477
12727
  this.logger.info("Stored Office access outside dashboard for downgrade compatibility");
@@ -12706,6 +12956,7 @@ export class FleetManager {
12706
12956
  this.mods.forgetInstance(name);
12707
12957
  this.agentMods.forget(name);
12708
12958
  this.officeActsInstance?.forget(name);
12959
+ this.officePackToolsInstance?.forget(name);
12709
12960
  }
12710
12961
  catch (err) {
12711
12962
  this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
@@ -14073,7 +14324,7 @@ export class FleetManager {
14073
14324
  return;
14074
14325
  const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
14075
14326
  const projects = board.listProjects(true);
14076
- const stats = board.projectStats();
14327
+ const stats = board.projectStats(this.dispatcherGone);
14077
14328
  const names = new Map(projects.map((p) => [p.id, p.name]));
14078
14329
  const configured = this.getPrimaryAdapterId();
14079
14330
  const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
@@ -14337,7 +14588,7 @@ export class FleetManager {
14337
14588
  entry.web = {
14338
14589
  text: web.text,
14339
14590
  ...(links ? { links } : {}),
14340
- actions: opts.choices.map(c => ({ id: c.action, label: c.label })),
14591
+ actions: [...opts.choices.map(c => ({ id: c.action, label: c.label })), ...(opts.webInput ? [{ id: opts.webInput.action, label: opts.webInput.label, input: true }] : [])],
14341
14592
  expiresAt: Date.now() + (opts.timeoutMs ?? NONCE_BUTTON_TIMEOUT_MS),
14342
14593
  };
14343
14594
  this.emitSseEvent("prompt", { instance: entry.instanceName, nonce, ...entry.web });
@@ -14514,6 +14765,10 @@ export class FleetManager {
14514
14765
  return true;
14515
14766
  if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
14516
14767
  return true;
14768
+ if (await this.handleKiroReconcileCallback(data, adapterId, adapter))
14769
+ return true;
14770
+ if (await this.handleOfficePackCallback(data, adapterId, adapter))
14771
+ return true;
14517
14772
  if (await this.handleDashboardCallback(data, adapterId, adapter))
14518
14773
  return true;
14519
14774
  if (await this.handleClassicApproval(data, adapterId, adapter))
@@ -14530,6 +14785,8 @@ export class FleetManager {
14530
14785
  return true;
14531
14786
  if (await this.handleInteractivePromptAssist(data, adapterId, adapter))
14532
14787
  return true;
14788
+ if (await this.handleRemoteCardClick(data, adapterId, adapter))
14789
+ return true;
14533
14790
  if (await this.handleClassicBackendSelection(data))
14534
14791
  return true;
14535
14792
  if (await this.handleModelSelection(data, adapterId))
@@ -14706,6 +14963,34 @@ export class FleetManager {
14706
14963
  messages: messages.filter(m => keep(m.instance)),
14707
14964
  });
14708
14965
  }
14966
+ /** #2661: the Office levels as last worked out, and the work in flight (officeLevels). */
14967
+ officeLevelsMemo = null;
14968
+ officeLevelsRun = null;
14969
+ /**
14970
+ * #2661: every instance's Office level (office-levels.ts) — worked out by the cache service from the usage store, at
14971
+ * most once per OFFICE_LEVELS_TTL_MS however many pages and share links ask (one run at a time). Null when
14972
+ * web.office_levels is false.
14973
+ */
14974
+ async officeLevels() {
14975
+ if (this.fleetConfig?.web?.office_levels === false)
14976
+ return null;
14977
+ const { OFFICE_LEVELS_TTL_MS } = await import("./office-levels.js");
14978
+ const memo = this.officeLevelsMemo;
14979
+ if (memo && performance.now() - memo.at < OFFICE_LEVELS_TTL_MS)
14980
+ return memo.levels;
14981
+ this.officeLevelsRun ??= this.cacheServiceFor().officeLevels(Date.now()).then((levels) => { this.officeLevelsMemo = { at: performance.now(), levels }; return levels; }).finally(() => { this.officeLevelsRun = null; });
14982
+ return this.officeLevelsRun;
14983
+ }
14984
+ /**
14985
+ * #2661: the levels as last worked out, for a synchronous reader (a share link's gateway): never waits; a stale or
14986
+ * missing set starts the next run. Null when web.office_levels is false or none has been worked out yet.
14987
+ */
14988
+ officeLevelsSnapshot() {
14989
+ if (this.fleetConfig?.web?.office_levels === false)
14990
+ return null;
14991
+ void this.officeLevels().catch(() => { });
14992
+ return this.officeLevelsMemo?.levels ?? null;
14993
+ }
14709
14994
  /** #2001: get_cache_report's — the same report, as last worked out (reused for a minute, until the next pass). */
14710
14995
  cacheReportPublished(window) {
14711
14996
  return this.cacheServiceFor().published(cacheWindow(window));
@@ -15003,8 +15288,12 @@ export class FleetManager {
15003
15288
  const buttons = this.replyButtons();
15004
15289
  return buttons ? buttons.clickWeb(instance, id, index, voter) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
15005
15290
  }
15006
- async clickWebPrompt(instance, nonce, action) {
15007
- if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z-]{0,23}$/.test(action))
15291
+ async clickWebPrompt(instance, nonce, action, text) {
15292
+ if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z0-9-]{0,23}$/.test(action))
15293
+ return { status: 400, error: "Malformed prompt answer" };
15294
+ // #2675: only an action the page shows a text box for takes text, and it must say something.
15295
+ const input = this.pendingNonceButtons.get(nonce)?.web?.actions.find(a => a.id === action)?.input === true;
15296
+ if (input !== (text !== undefined) || (input && (!text.trim() || text.length > REMOTE_CARD_TEXT_MAX)))
15008
15297
  return { status: 400, error: "Malformed prompt answer" };
15009
15298
  const entry = this.pendingNonceButtons.get(nonce);
15010
15299
  // Unknown is the same answer as answered or expired: the page drops the buttons either way.
@@ -15026,6 +15315,8 @@ export class FleetManager {
15026
15315
  notice = n; },
15027
15316
  };
15028
15317
  this.webPromptClicks.add(data);
15318
+ if (text !== undefined)
15319
+ this.webPromptTexts.set(data, text);
15029
15320
  await this.dispatchAdapterCallback(data, entry.adapterId, entry.adapter);
15030
15321
  if (this.webPromptClaims.has(data))
15031
15322
  return { status: 200 };
@@ -16083,6 +16374,163 @@ export class FleetManager {
16083
16374
  }
16084
16375
  return true;
16085
16376
  }
16377
+ // ── #2675: a card a CLI shows in place of its composer, answered from the chat ─────────────────────────────────
16378
+ /**
16379
+ * Where an instance's card is put to people: its own place — its topic, or its ClassicBot room — or, on a fleet with
16380
+ * no chat platform, the dashboard. Never General: the answer is the instance's conversation.
16381
+ */
16382
+ remoteCardPlace(instanceName) {
16383
+ const web = this.webOnlyPromptPlace();
16384
+ if (web)
16385
+ return web;
16386
+ const room = this.classicChannels?.getChannelIdByInstance(instanceName);
16387
+ if (room) {
16388
+ const adapterId = this.classicChannels?.getAdapterIdByInstance(instanceName) ?? this.getPrimaryAdapterId();
16389
+ const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
16390
+ return adapter && adapterId ? { adapter, adapterId, chatId: room } : null;
16391
+ }
16392
+ const adapterId = this.getInstanceAdapterId(instanceName);
16393
+ const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
16394
+ const chatId = this.getGroupIdForInstance(instanceName);
16395
+ const topicId = this.fleetConfig?.instances[instanceName]?.topic_id;
16396
+ if (!adapter || !adapterId || !chatId || topicId == null)
16397
+ return null;
16398
+ return { adapter, adapterId, chatId, threadId: String(topicId) };
16399
+ }
16400
+ /**
16401
+ * The daemon saw a card held still on `instanceName`'s screen (remote_card): put it to the people who talk to that
16402
+ * instance, with a button per answer. A card already posted for the instance is retired first (the daemon said it
16403
+ * was gone; this is the fallback).
16404
+ */
16405
+ async postRemoteCard(instanceName, card) {
16406
+ this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
16407
+ const place = this.remoteCardPlace(instanceName);
16408
+ if (!place) {
16409
+ this.logger.warn({ instanceName }, "A card is waiting on screen but the instance has no place to post it — the parked notice stands");
16410
+ return;
16411
+ }
16412
+ const prompt = remoteCardPrompt(instanceName, card);
16413
+ const nonce = await this.postNonceButtonPrompt({
16414
+ prefix: REMOTE_CARD_CALLBACK_PREFIX, alertType: "interactive_prompt", instanceName, ...place,
16415
+ message: prompt.message, choices: prompt.choices, webInput: prompt.webInput,
16416
+ expiredText: t("card.expired", instanceName), timeoutMs: REMOTE_CARD_TIMEOUT_MS,
16417
+ extra: { remoteCard: { identity: card.identity, kind: card.kind, options: card.kind === "question" ? card.options.map(o => ({ n: o.n, title: o.title })) : [] } },
16418
+ });
16419
+ if (nonce)
16420
+ this.remoteCardPosts.set(instanceName, nonce);
16421
+ }
16422
+ /**
16423
+ * The card is gone from the screen (remote_card_gone, `identity`; null: whichever is posted): its buttons collapse to
16424
+ * `text` if nobody answered them here.
16425
+ */
16426
+ retireRemoteCard(instanceName, identity, text = t("card.answered_in_pane")) {
16427
+ const nonce = this.remoteCardPosts.get(instanceName);
16428
+ if (!nonce)
16429
+ return;
16430
+ const entry = this.pendingNonceButtons.get(nonce);
16431
+ if (entry && identity !== null && entry.remoteCard?.identity !== identity)
16432
+ return;
16433
+ this.remoteCardPosts.delete(instanceName);
16434
+ if (!entry)
16435
+ return;
16436
+ this.pendingNonceButtons.delete(nonce);
16437
+ if (entry.timer)
16438
+ clearTimeout(entry.timer);
16439
+ void this.retireNonceButtons(entry, entry.messageId ?? "", text);
16440
+ }
16441
+ /** Who may answer an instance's card: whoever may already talk to it there (a ClassicBot room is open to all). */
16442
+ mayAnswerRemoteCard(userId, entry) {
16443
+ if (this.classicChannels?.getChannelIdByInstance(entry.instanceName) === entry.chatId)
16444
+ return true;
16445
+ const am = this.worlds.get(entry.adapterId)?.accessManager ?? this.accessManager;
16446
+ return !am || am.isAllowed(userId);
16447
+ }
16448
+ /** A click on a card's button (platform or dashboard). */
16449
+ async handleRemoteCardClick(data, callbackAdapterId, receivingAdapter) {
16450
+ if (!data.callbackData.startsWith(REMOTE_CARD_CALLBACK_PREFIX))
16451
+ return false;
16452
+ // A typed answer from the dashboard must come with its text; refused before the claim, so the buttons stay.
16453
+ const match = REMOTE_CARD_CALLBACK_RE.exec(data.callbackData);
16454
+ if (match?.[2] === "text" && remoteCardAction("text", this.webPromptTexts.get(data)) === null) {
16455
+ data.ack?.(t("buttons.stale_notice"));
16456
+ return true;
16457
+ }
16458
+ const claimed = this.consumeNonceCallback(REMOTE_CARD_CALLBACK_PREFIX, REMOTE_CARD_CALLBACK_RE, data, callbackAdapterId, receivingAdapter, undefined, (userId, entry) => this.mayAnswerRemoteCard(userId, entry));
16459
+ if (claimed === null)
16460
+ return false;
16461
+ if (claimed === "consumed")
16462
+ return true;
16463
+ const action = remoteCardAction(claimed.action, this.webPromptTexts.get(data));
16464
+ const who = this.webPromptClicks.has(data) ? t("card.someone_on_dashboard") : (data.username ?? data.userId ?? "?");
16465
+ await this.answerRemoteCardFor(claimed.entry, action, who, data.messageId, data.editClicked);
16466
+ return true;
16467
+ }
16468
+ /**
16469
+ * An inbound message that replies to a posted question card, by someone who may answer it: its text is the typed
16470
+ * answer, and the message goes no further (true). Another bot's copy of a reply already taken is dropped (true).
16471
+ */
16472
+ async takeRemoteCardReply(msg) {
16473
+ if (msg.isBotMessage || !msg.replyTo)
16474
+ return false;
16475
+ const now = performance.now();
16476
+ for (const [key, until] of this.takenCardReplies)
16477
+ if (until <= now)
16478
+ this.takenCardReplies.delete(key);
16479
+ const key = `${msg.chatId}|${msg.messageId}`;
16480
+ if (this.takenCardReplies.has(key))
16481
+ return true;
16482
+ for (const [nonce, entry] of this.pendingNonceButtons) {
16483
+ if (entry.prefix !== REMOTE_CARD_CALLBACK_PREFIX || entry.remoteCard?.kind !== "question")
16484
+ continue;
16485
+ // By place, whichever of the fleet's bots received the reply (a sibling's copy can arrive first, #2680 review), on
16486
+ // the post's own platform; who may answer is judged by the post's world (mayAnswerRemoteCard), as Needs you does.
16487
+ if (entry.chatId !== msg.chatId || entry.messageId !== msg.replyTo || entry.adapter.type !== msg.source)
16488
+ continue;
16489
+ if (!msg.userId || !this.mayAnswerRemoteCard(msg.userId, entry))
16490
+ return false; // not theirs to answer: it is just a message
16491
+ const action = remoteCardAction("text", msg.text ?? "");
16492
+ if (!action)
16493
+ return false;
16494
+ this.pendingNonceButtons.delete(nonce);
16495
+ if (entry.timer)
16496
+ clearTimeout(entry.timer);
16497
+ this.webPromptGone(entry);
16498
+ this.takenCardReplies.set(key, now + REMOTE_CARD_TAKEN_TTL_MS);
16499
+ await this.answerRemoteCardFor(entry, action, msg.username ?? msg.userId, entry.messageId ?? msg.replyTo);
16500
+ return true;
16501
+ }
16502
+ return false;
16503
+ }
16504
+ /**
16505
+ * Key a claimed answer into the instance's card (the daemon checks it is still that card and walks a verified
16506
+ * cursor), then collapse the post to what happened. When the card is still waiting and was not answered — refused
16507
+ * or stopped part-way — it is posted again, so it can still be answered from here.
16508
+ */
16509
+ async answerRemoteCardFor(entry, action, who, messageId, editClicked) {
16510
+ if (this.remoteCardPosts.get(entry.instanceName) === entry.nonce)
16511
+ this.remoteCardPosts.delete(entry.instanceName);
16512
+ const daemon = this.daemons.get(entry.instanceName);
16513
+ const card = entry.remoteCard;
16514
+ let outcome;
16515
+ if (!card || !action)
16516
+ outcome = { outcome: "refused", reason: "malformed" };
16517
+ else if (!daemon?.answerRemoteCard)
16518
+ outcome = { outcome: "stale" };
16519
+ else {
16520
+ try {
16521
+ outcome = await daemon.answerRemoteCard(card.identity, action);
16522
+ }
16523
+ catch (err) {
16524
+ this.logger.warn({ err: err?.message, instanceName: entry.instanceName }, "Answering a card from the chat failed");
16525
+ outcome = { outcome: "stopped", step: "error" };
16526
+ }
16527
+ }
16528
+ this.eventLog?.insert(entry.instanceName, "remote_card_answer", { action: action?.kind ?? null, outcome: outcome.outcome });
16529
+ const choice = card && action ? remoteCardChoiceLabel(card, action) : "?";
16530
+ await this.retireNonceButtons(entry, messageId, remoteCardOutcomeText(entry.instanceName, who, choice, outcome), editClicked);
16531
+ if (outcome.outcome === "refused" || outcome.outcome === "stopped")
16532
+ daemon?.repostRemoteCard?.();
16533
+ }
16086
16534
  // ── Cancel button ────────────────────────────────────────────────────
16087
16535
  // Sent after delivering a user message to an instance; clicking it (or
16088
16536
  // /cancel) sends Escape to the instance's pane to interrupt generation.
@@ -17793,8 +18241,8 @@ export class FleetManager {
17793
18241
  * The task board's live side: every committed change goes to the open pages (never to a guest) — the board's new
17794
18242
  * revision and the card numbers it touched; not who touched them (the page does not need it, and a public-link page
17795
18243
  * must not learn a hidden instance's name from it). Its news goes to the instances whose cards someone else changed:
17796
- * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken
17797
- * (task-board-waker.ts).
18244
+ * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken, or
18245
+ * withdrawn once its cards changed again (#2801; task-board-waker.ts).
17798
18246
  */
17799
18247
  attachTaskBoard(db) {
17800
18248
  this.taskBoardWaker?.stop();
@@ -17803,6 +18251,19 @@ export class FleetManager {
17803
18251
  const waker = this.taskBoardWaker = new TaskBoardWaker({
17804
18252
  deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
17805
18253
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
18254
+ // #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
18255
+ messagedAbout: (from, to, codes, sinceMs) => {
18256
+ const outbox = this.deliveryOutbox;
18257
+ if (!outbox?.isOpen)
18258
+ return false;
18259
+ return outbox.recentTextsFrom(to, from, new Date(sinceMs).toISOString()).some((text) => codes.some((code) => namesCard(text, code)));
18260
+ },
18261
+ // #2801: a notice still waiting whose cards have all changed again is not delivered.
18262
+ withdraw: (prefix, stale, reason) => {
18263
+ const outbox = this.deliveryOutbox;
18264
+ return outbox?.isOpen ? outbox.withdrawWaitingNotices(prefix, stale, reason).length : 0;
18265
+ },
18266
+ changedAt: (uuid) => db.board.lastChangeAt(uuid),
17806
18267
  holds: (wake) => db.board.wakeHolds(wake),
17807
18268
  card: (uuid) => db.board.findCard(uuid),
17808
18269
  // #2352: the card's Needs you buttons are in this instance's own channel (it is the card's lane).
@@ -17819,12 +18280,13 @@ export class FleetManager {
17819
18280
  },
17820
18281
  log: (level, message, extra) => this.logger[level](extra ?? {}, message),
17821
18282
  });
17822
- db.board.onChange(({ rev, ids, projects, wakes }) => {
18283
+ db.board.onChange(({ rev, ids, projects, wakes, changed }) => {
17823
18284
  // Refresh both old and new owners on reassignment; wakes are not an ownership index.
17824
18285
  if (ids.length)
17825
18286
  for (const name of this.daemons.keys())
17826
18287
  this.observeWorkingState(name, "tasks");
17827
18288
  this.emitSseEvent("tasks", { rev, ids, projects });
18289
+ waker.changed(changed);
17828
18290
  waker.note(wakes);
17829
18291
  // step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
17830
18292
  if (ids.length || projects)
@@ -22978,7 +23440,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22978
23440
  // From the install's own channel tag, this is what /update installs — even a stable the release pointed the
22979
23441
  // tag at (2.2.0 on @alpha / @beta): the plain /update text, never "/update stays on … use --stable" (#1928).
22980
23442
  const key = fromChannel ? "update.available_current" : updateNoticeKey(currentVersion, target);
22981
- this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
23443
+ // #2775: null = a beta install and a stable target; beta users are never told to switch.
23444
+ if (key)
23445
+ this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
22982
23446
  }
22983
23447
  }
22984
23448
  }