@songsid/agend 2.3.2-beta.8 → 2.3.2

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 (243) hide show
  1. package/README.md +6 -0
  2. package/README.zh-TW.md +6 -0
  3. package/dist/agent-cli.js +48 -0
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.js +6 -0
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-whats-new.js +3 -0
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.js +2 -0
  10. package/dist/auth-api.js.map +1 -1
  11. package/dist/backend/claude-code.js +23 -1
  12. package/dist/backend/claude-code.js.map +1 -1
  13. package/dist/backend/codex-session-lookup.d.ts +15 -0
  14. package/dist/backend/codex-session-lookup.js +58 -0
  15. package/dist/backend/codex-session-lookup.js.map +1 -1
  16. package/dist/backend/codex.js +15 -4
  17. package/dist/backend/codex.js.map +1 -1
  18. package/dist/backend/kiro-classic-archive.d.ts +57 -0
  19. package/dist/backend/kiro-classic-archive.js +622 -0
  20. package/dist/backend/kiro-classic-archive.js.map +1 -0
  21. package/dist/backend/kiro-classic-links.d.ts +24 -0
  22. package/dist/backend/kiro-classic-links.js +135 -0
  23. package/dist/backend/kiro-classic-links.js.map +1 -0
  24. package/dist/backend/kiro-identity.d.ts +100 -1
  25. package/dist/backend/kiro-identity.js +262 -31
  26. package/dist/backend/kiro-identity.js.map +1 -1
  27. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  28. package/dist/backend/kiro-migration-verify.js +52 -0
  29. package/dist/backend/kiro-migration-verify.js.map +1 -0
  30. package/dist/backend/kiro-reconcile-store.d.ts +27 -0
  31. package/dist/backend/kiro-reconcile-store.js +344 -0
  32. package/dist/backend/kiro-reconcile-store.js.map +1 -0
  33. package/dist/backend/kiro-v2-store-worker.js +35 -1
  34. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  35. package/dist/backend/kiro-v2-store.d.ts +31 -1
  36. package/dist/backend/kiro-v2-store.js +22 -12
  37. package/dist/backend/kiro-v2-store.js.map +1 -1
  38. package/dist/backend/kiro.d.ts +124 -1
  39. package/dist/backend/kiro.js +553 -29
  40. package/dist/backend/kiro.js.map +1 -1
  41. package/dist/backend/types.d.ts +102 -0
  42. package/dist/backend/types.js +9 -0
  43. package/dist/backend/types.js.map +1 -1
  44. package/dist/cache-service.d.ts +13 -1
  45. package/dist/cache-service.js +45 -2
  46. package/dist/cache-service.js.map +1 -1
  47. package/dist/channel/ipc-timeouts.js +3 -0
  48. package/dist/channel/ipc-timeouts.js.map +1 -1
  49. package/dist/channel/mcp-tools.js +40 -5
  50. package/dist/channel/mcp-tools.js.map +1 -1
  51. package/dist/channel/tool-router.js +2 -1
  52. package/dist/channel/tool-router.js.map +1 -1
  53. package/dist/classic-channel-manager.d.ts +6 -0
  54. package/dist/classic-channel-manager.js +25 -0
  55. package/dist/classic-channel-manager.js.map +1 -1
  56. package/dist/cli.js +50 -4
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config-validator.d.ts +7 -1
  59. package/dist/config-validator.js +17 -8
  60. package/dist/config-validator.js.map +1 -1
  61. package/dist/config.js +2 -0
  62. package/dist/config.js.map +1 -1
  63. package/dist/daemon.d.ts +66 -0
  64. package/dist/daemon.js +421 -19
  65. package/dist/daemon.js.map +1 -1
  66. package/dist/dashboard-permissions.d.ts +15 -4
  67. package/dist/dashboard-permissions.js +49 -23
  68. package/dist/dashboard-permissions.js.map +1 -1
  69. package/dist/doctor.js +2 -1
  70. package/dist/doctor.js.map +1 -1
  71. package/dist/fleet-control-audit.d.ts +1 -1
  72. package/dist/fleet-control-audit.js +3 -0
  73. package/dist/fleet-control-audit.js.map +1 -1
  74. package/dist/fleet-manager.d.ts +90 -3
  75. package/dist/fleet-manager.js +569 -41
  76. package/dist/fleet-manager.js.map +1 -1
  77. package/dist/forged-envelope-scan.d.ts +9 -0
  78. package/dist/forged-envelope-scan.js +30 -5
  79. package/dist/forged-envelope-scan.js.map +1 -1
  80. package/dist/general-instance.d.ts +24 -0
  81. package/dist/general-instance.js +53 -0
  82. package/dist/general-instance.js.map +1 -1
  83. package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
  84. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
  85. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  86. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
  87. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
  88. package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
  89. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
  90. package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
  91. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +60 -18
  92. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +60 -18
  93. package/dist/instance-config-impact.js +3 -2
  94. package/dist/instance-config-impact.js.map +1 -1
  95. package/dist/instance-lifecycle.d.ts +4 -0
  96. package/dist/instance-lifecycle.js +7 -0
  97. package/dist/instance-lifecycle.js.map +1 -1
  98. package/dist/kiro-migrate.d.ts +19 -2
  99. package/dist/kiro-migrate.js +133 -35
  100. package/dist/kiro-migrate.js.map +1 -1
  101. package/dist/kiro-reconcile.d.ts +21 -0
  102. package/dist/kiro-reconcile.js +88 -0
  103. package/dist/kiro-reconcile.js.map +1 -0
  104. package/dist/kiro-session-proof.d.ts +41 -0
  105. package/dist/kiro-session-proof.js +89 -0
  106. package/dist/kiro-session-proof.js.map +1 -0
  107. package/dist/kiro-tui-forensic.d.ts +19 -0
  108. package/dist/kiro-tui-forensic.js +124 -0
  109. package/dist/kiro-tui-forensic.js.map +1 -0
  110. package/dist/locale.js +198 -0
  111. package/dist/locale.js.map +1 -1
  112. package/dist/log-service.js +2 -2
  113. package/dist/log-service.js.map +1 -1
  114. package/dist/needs-you.d.ts +3 -1
  115. package/dist/needs-you.js +4 -2
  116. package/dist/needs-you.js.map +1 -1
  117. package/dist/office-levels.d.ts +64 -0
  118. package/dist/office-levels.js +66 -0
  119. package/dist/office-levels.js.map +1 -0
  120. package/dist/office-share-page.d.ts +3 -1
  121. package/dist/office-share-page.js +8 -3
  122. package/dist/office-share-page.js.map +1 -1
  123. package/dist/office-share-service.d.ts +6 -3
  124. package/dist/office-share-service.js +12 -8
  125. package/dist/office-share-service.js.map +1 -1
  126. package/dist/office-share.d.ts +80 -13
  127. package/dist/office-share.js +145 -25
  128. package/dist/office-share.js.map +1 -1
  129. package/dist/outbound-handlers.js +20 -1
  130. package/dist/outbound-handlers.js.map +1 -1
  131. package/dist/outbound-schemas.d.ts +47 -2
  132. package/dist/outbound-schemas.js +41 -2
  133. package/dist/outbound-schemas.js.map +1 -1
  134. package/dist/public-web-gateway.js +1 -1
  135. package/dist/public-web-gateway.js.map +1 -1
  136. package/dist/quickstart-api.d.ts +39 -1
  137. package/dist/quickstart-api.js +115 -17
  138. package/dist/quickstart-api.js.map +1 -1
  139. package/dist/remote-card-answer.d.ts +83 -0
  140. package/dist/remote-card-answer.js +208 -0
  141. package/dist/remote-card-answer.js.map +1 -0
  142. package/dist/remote-card-prompt.d.ts +38 -0
  143. package/dist/remote-card-prompt.js +86 -0
  144. package/dist/remote-card-prompt.js.map +1 -0
  145. package/dist/scheduler/task-board.d.ts +28 -6
  146. package/dist/scheduler/task-board.js +66 -11
  147. package/dist/scheduler/task-board.js.map +1 -1
  148. package/dist/service-installer.d.ts +13 -0
  149. package/dist/service-installer.js +15 -0
  150. package/dist/service-installer.js.map +1 -1
  151. package/dist/service-owner.d.ts +25 -0
  152. package/dist/service-owner.js +43 -2
  153. package/dist/service-owner.js.map +1 -1
  154. package/dist/settings-api.d.ts +2 -0
  155. package/dist/settings-api.js +18 -12
  156. package/dist/settings-api.js.map +1 -1
  157. package/dist/settings-change.js +10 -5
  158. package/dist/settings-change.js.map +1 -1
  159. package/dist/settings-effect.js +7 -4
  160. package/dist/settings-effect.js.map +1 -1
  161. package/dist/settings-instance-patch.js +1 -1
  162. package/dist/settings-instance-patch.js.map +1 -1
  163. package/dist/setup-form.d.ts +2 -2
  164. package/dist/setup-form.js +139 -93
  165. package/dist/setup-form.js.map +1 -1
  166. package/dist/setup-i18n.d.ts +25 -0
  167. package/dist/setup-i18n.js +226 -0
  168. package/dist/setup-i18n.js.map +1 -0
  169. package/dist/task-board-waker.js +7 -2
  170. package/dist/task-board-waker.js.map +1 -1
  171. package/dist/tool-permissions.js +18 -0
  172. package/dist/tool-permissions.js.map +1 -1
  173. package/dist/transcript-sources.d.ts +1 -0
  174. package/dist/transcript-sources.js +20 -3
  175. package/dist/transcript-sources.js.map +1 -1
  176. package/dist/types.d.ts +10 -0
  177. package/dist/ui/backend-signin.js +4 -3
  178. package/dist/ui/chat-store.js +3 -2
  179. package/dist/ui/chat-strings.js +2 -2
  180. package/dist/ui/fleet-office-card.js +114 -0
  181. package/dist/ui/fleet-office-editor.js +81 -9
  182. package/dist/ui/fleet-office-layout.js +87 -10
  183. package/dist/ui/fleet-office-levels.js +105 -0
  184. package/dist/ui/fleet-office-model.js +9 -1
  185. package/dist/ui/fleet-office-pack.d.ts +18 -3
  186. package/dist/ui/fleet-office-pack.js +217 -23
  187. package/dist/ui/fleet-office-skin.js +39 -3
  188. package/dist/ui/fleet-office-store.js +74 -3
  189. package/dist/ui/fleet-office-view.js +8 -0
  190. package/dist/ui/fleet-office-zones.js +102 -19
  191. package/dist/ui/fleet-office.js +323 -50
  192. package/dist/ui/fleet-org.js +2 -0
  193. package/dist/ui/fleet-task-card.js +2 -2
  194. package/dist/ui/fleet-task-common.js +30 -7
  195. package/dist/ui/fleet-task-work.js +2 -2
  196. package/dist/ui/fleet-tasks.js +10 -11
  197. package/dist/ui/office-share-dialog.js +31 -7
  198. package/dist/ui/office-share-page.js +50 -1
  199. package/dist/ui/panel-chat.js +14 -2
  200. package/dist/ui/panel-log.js +4 -4
  201. package/dist/ui/panel-needs.js +1 -1
  202. package/dist/ui/panel-settings.js +16 -8
  203. package/dist/ui/settings-discord.js +3 -2
  204. package/dist/ui/settings-guests.js +17 -7
  205. package/dist/ui/settings-model.js +17 -9
  206. package/dist/ui/settings-registry.js +1 -0
  207. package/dist/ui/settings-strings.js +16 -4
  208. package/dist/ui/settings-token.js +3 -2
  209. package/dist/ui/settings-wizard.js +25 -9
  210. package/dist/ui/shared/app-i18n.js +2 -2
  211. package/dist/ui/shared/app-stream.js +1 -1
  212. package/dist/ui/shared/app.css +74 -2
  213. package/dist/ui/shared/setup-host-text.js +107 -0
  214. package/dist/ui/structured-signin.js +89 -38
  215. package/dist/usage-rollup.d.ts +34 -3
  216. package/dist/usage-rollup.js +56 -3
  217. package/dist/usage-rollup.js.map +1 -1
  218. package/dist/web-api.d.ts +10 -1
  219. package/dist/web-api.js +207 -17
  220. package/dist/web-api.js.map +1 -1
  221. package/dist/web-auth.js +1 -1
  222. package/dist/web-auth.js.map +1 -1
  223. package/dist/web-office-acts.d.ts +5 -2
  224. package/dist/web-office-acts.js +9 -4
  225. package/dist/web-office-acts.js.map +1 -1
  226. package/dist/web-office-pack-read.d.ts +35 -0
  227. package/dist/web-office-pack-read.js +252 -0
  228. package/dist/web-office-pack-read.js.map +1 -0
  229. package/dist/web-office-pack-tools.d.ts +241 -0
  230. package/dist/web-office-pack-tools.js +785 -0
  231. package/dist/web-office-pack-tools.js.map +1 -0
  232. package/dist/web-office-skins.d.ts +68 -4
  233. package/dist/web-office-skins.js +324 -33
  234. package/dist/web-office-skins.js.map +1 -1
  235. package/dist/web-office-store.d.ts +47 -4
  236. package/dist/web-office-store.js +149 -15
  237. package/dist/web-office-store.js.map +1 -1
  238. package/dist/web-settings-patch.d.ts +3 -1
  239. package/dist/web-settings-patch.js +4 -1
  240. package/dist/web-settings-patch.js.map +1 -1
  241. package/dist/workflow-templates/default.md +1 -1
  242. package/package.json +2 -2
  243. package/templates/windows-shortcuts.ps1 +18 -17
@@ -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,6 +47,7 @@ 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";
@@ -83,7 +88,7 @@ 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";
@@ -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";
@@ -248,7 +255,7 @@ 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
257
  import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
251
- import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
258
+ import { GENERAL_NAME, GENERAL_PAUSE_ERROR, adoptGeneralByName, freeGeneralName, isGeneralInstance } from "./general-instance.js";
252
259
  import { buildOrgChart } from "./web-org.js";
253
260
  import { topMemoryHogs } from "./memory-hogs.js";
254
261
  import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
@@ -478,6 +485,13 @@ const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
478
485
  const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
479
486
  const BACKEND_SELECT_CALLBACK_PREFIX = "backend-select:";
480
487
  const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
488
+ /** #2675: a card a CLI shows in place of its composer (kiro V3's Question / MCP input), posted with its answers. */
489
+ const REMOTE_CARD_CALLBACK_PREFIX = "card-answer:";
490
+ 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)$/;
491
+ /** A posted card's buttons stay this long; the card itself waits in the pane for as long as it takes. */
492
+ const REMOTE_CARD_TIMEOUT_MS = 12 * 60 * 60_000;
493
+ /** A reply already taken as a card's answer is remembered this long, so another bot's copy of it is dropped (#2675). */
494
+ const REMOTE_CARD_TAKEN_TTL_MS = 10 * 60_000;
481
495
  const EXIT_RESTART_CALLBACK_PREFIX = "exit-restart:";
482
496
  const OOM_RETRY_CALLBACK_PREFIX = "oom-retry:";
483
497
  /** #1386: how long a paused instance's marker reason is reused before it is read again (transitions drop it at once). */
@@ -486,6 +500,11 @@ const HANG_CALLBACK_PREFIX = "hang:";
486
500
  const CLEAR_CONFIRM_CALLBACK_PREFIX = "clear-confirm:";
487
501
  /** #2598 B2: a kiro classic → TUI move's Confirm in General. "kiro-migrate:" (13) + 32 hex + ":confirm" (8) = 53 bytes. */
488
502
  const KIRO_MIGRATE_CALLBACK_PREFIX = "kiro-migrate:";
503
+ const KIRO_RECONCILE_CALLBACK_PREFIX = "kiro-reconcile:";
504
+ /** #2692: a fleet admin's Approve / Reject on an agent's pending skin-pack change, in General. */
505
+ const OFFICE_PACK_CALLBACK_PREFIX = "office-pack:";
506
+ /** #2692: the skin-pack tools' IPC types (answered as web theme tools). */
507
+ const OFFICE_PACK_IPC = new Set(OFFICE_PACK_TOOLS.map(tool => `fleet_${tool}`));
489
508
  /** Long enough for an admin to read the prompt; the move is judged again on the click. */
490
509
  const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
491
510
  /**
@@ -496,6 +515,7 @@ const KIRO_MIGRATE_TIMEOUT_MS = 10 * 60_000;
496
515
  */
497
516
  const WEB_MIRRORED_PROMPT_PREFIXES = new Set([
498
517
  HANG_CALLBACK_PREFIX, EXIT_RESTART_CALLBACK_PREFIX, OOM_RETRY_CALLBACK_PREFIX, INTERACTIVE_ASSIST_CALLBACK_PREFIX,
518
+ REMOTE_CARD_CALLBACK_PREFIX,
499
519
  ]);
500
520
  /**
501
521
  * Where a reply goes on a fleet with no chat platform (web track C4): it is "sent" by being shown in the
@@ -913,6 +933,8 @@ export class FleetManager {
913
933
  pendingNonceButtons = new Map();
914
934
  /** #2598 B2: the kiro moves waiting on a fleet admin's Confirm in General, by prompt nonce. */
915
935
  pendingKiroMigrations = new Map();
936
+ pendingKiroReconciliations = new Map();
937
+ kiroMigrationRuns = new Map();
916
938
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
917
939
  needsYou = null;
918
940
  /** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
@@ -925,6 +947,12 @@ export class FleetManager {
925
947
  * adapter emits — whatever fields its payload carries — can claim a dashboard click's authority.
926
948
  */
927
949
  webPromptClicks = new WeakSet();
950
+ /** #2675: a dashboard click's typed answer, for the click it came with. */
951
+ webPromptTexts = new WeakMap();
952
+ /** #2675: the nonce of each instance's posted card. */
953
+ remoteCardPosts = new Map();
954
+ /** #2675: replies taken as a card's answer (`<chatId>|<messageId>` → monotonic expiry), so another bot's copy is dropped. */
955
+ takenCardReplies = new Map();
928
956
  /** The web clicks consumeNonceCallback actually claimed (the others were refused or lost a race). */
929
957
  webPromptClaims = new WeakSet();
930
958
  // Model failover state
@@ -1195,6 +1223,7 @@ export class FleetManager {
1195
1223
  needs: () => this.needsYouItems().map(i => i.instance),
1196
1224
  hidden: name => crossInstanceVisibility(this.fleetConfig, name) === "hidden",
1197
1225
  skins: this.officeSkins,
1226
+ levels: () => this.officeLevelsSnapshot(),
1198
1227
  },
1199
1228
  manager: new ManagedTunnel({ dataDir: join(this.dataDir, OFFICE_SHARE_TUNNEL_DIR), log: () => this.logger.warn("Office share tunnel cleanup requires attention") }),
1200
1229
  ensure: signal => ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal }),
@@ -1211,7 +1240,7 @@ export class FleetManager {
1211
1240
  * `share` null with `count`: "Revoke all".
1212
1241
  */
1213
1242
  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;
1243
+ 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
1244
  const minutes = share ? Math.round((share.expiresAt - share.createdAt) / 60_000) : 0;
1216
1245
  const text = event === "created" ? t("office_share.created_notice", who, office, minutes < 60 ? `${minutes} min` : `${Math.round(minutes / 60)} h`)
1217
1246
  : share ? t("office_share.revoked_notice", who, office) : t("office_share.revoked_all_notice", who, count);
@@ -1235,6 +1264,98 @@ export class FleetManager {
1235
1264
  changed: acts => this.emitSseEvent("office_acts", { acts, now: Date.now() }),
1236
1265
  }));
1237
1266
  }
1267
+ officePackToolsInstance;
1268
+ /** #2692: the agents' skin-pack tools — their log, the admins' approval mode and the pending changes (memory only). */
1269
+ get officePackTools() {
1270
+ return (this.officePackToolsInstance ??= new OfficePackTools({
1271
+ skins: this.officeSkins, offices: this.officeStore,
1272
+ allowedAgents: () => this.officePackAgents(),
1273
+ notice: (text, pending) => { void this.officePackNotice(text, pending); },
1274
+ settled: (id, text) => this.settleOfficePackPrompt(id, text),
1275
+ changed: what => {
1276
+ if (what.skins !== undefined)
1277
+ this.emitSseEvent("office_skins", { rev: what.skins });
1278
+ if (what.offices !== undefined)
1279
+ this.emitSseEvent("offices", { rev: what.offices });
1280
+ if (what.changes)
1281
+ this.emitSseEvent("office_pack_changes", {});
1282
+ },
1283
+ logger: this.logger,
1284
+ }));
1285
+ }
1286
+ /** #2692: the instances whose `office_packs` feature is on (their own switch, else the defaults'): the Office page lists them. */
1287
+ officePackAgents() {
1288
+ const names = new Set([...Object.keys(this.fleetConfig?.instances ?? {}), ...(this.classicChannels?.getAll() ?? []).map(ch => ch.instanceName)]);
1289
+ return [...names].filter(name => dashboardRefusalIn(this.fleetConfig, name, "office_pack_upload") === null);
1290
+ }
1291
+ /** #2692: the nonce of each pending pack change's prompt in General (its buttons are retired when it is decided). */
1292
+ officePackPrompts = new Map();
1293
+ /**
1294
+ * #2692: tell the fleet's admins of an agent's pack change — one short message in General (notifyFleetError, never a
1295
+ * sign-in notice). A pending one comes with Approve / Reject (a fleet admin's, as a kiro move); where no General can
1296
+ * carry buttons it is the plain message, and the Office page decides it.
1297
+ */
1298
+ async officePackNotice(text, pending) {
1299
+ if (!pending) {
1300
+ this.notifyFleetError(text, { throttle: false });
1301
+ return;
1302
+ }
1303
+ const general = this.findGeneralInstance();
1304
+ const adapterId = general ? this.getInstanceAdapterId(general) : undefined;
1305
+ const adapter = general ? this.getAdapterForInstance(general) : undefined;
1306
+ const chatId = general ? this.getGroupIdForInstance(general) : undefined;
1307
+ const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
1308
+ if (general && adapterId && adapter && chatId) {
1309
+ try {
1310
+ const nonce = await this.postNonceButtonPromptOrThrow({
1311
+ prefix: OFFICE_PACK_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: pending.instance, adapter, adapterId, chatId,
1312
+ ...(topic ? { threadId: topic } : {}), message: text,
1313
+ choices: [{ action: "approve", label: t("office_packs.approve_button") }, { action: "reject", label: t("office_packs.reject_button") }],
1314
+ expiredText: t("office_packs.expired", ""), timeoutMs: Math.max(1_000, pending.expires_at - Date.now()),
1315
+ });
1316
+ if (this.officePackTools.pendingViews().some(v => v.id === pending.id))
1317
+ this.officePackPrompts.set(pending.id, nonce);
1318
+ else
1319
+ this.retireOfficePackNonce(nonce, t("office_packs.gone"));
1320
+ return;
1321
+ }
1322
+ catch (err) {
1323
+ this.logger.info({ err: err.message }, "Office pack approval prompt unavailable; the plain notice and the Office page remain");
1324
+ }
1325
+ }
1326
+ this.notifyFleetError(text, { throttle: false });
1327
+ }
1328
+ retireOfficePackNonce(nonce, text) {
1329
+ const entry = this.pendingNonceButtons.get(nonce);
1330
+ if (!entry || entry.prefix !== OFFICE_PACK_CALLBACK_PREFIX)
1331
+ return;
1332
+ this.pendingNonceButtons.delete(nonce);
1333
+ clearTimeout(entry.timer);
1334
+ void this.retireNonceButtons(entry, entry.messageId ?? "", text);
1335
+ }
1336
+ /** #2692: a pending change was decided (here or on the page) or expired: its prompt says so and loses its buttons. */
1337
+ settleOfficePackPrompt(id, text) {
1338
+ const nonce = this.officePackPrompts.get(id);
1339
+ this.officePackPrompts.delete(id);
1340
+ if (nonce)
1341
+ this.retireOfficePackNonce(nonce, text);
1342
+ }
1343
+ /** #2692: Approve / Reject on a pending pack change. Any fleet admin (the default nonce authorization), in General. */
1344
+ async handleOfficePackCallback(data, adapterId, adapter) {
1345
+ const claimed = this.consumeNonceCallback(OFFICE_PACK_CALLBACK_PREFIX, /^office-pack:([0-9a-f]{32}):(approve|reject)$/, data, adapterId, adapter);
1346
+ if (claimed === null)
1347
+ return false;
1348
+ if (claimed === "consumed")
1349
+ return true;
1350
+ const { entry, action } = claimed;
1351
+ const nonce = data.callbackData.slice(OFFICE_PACK_CALLBACK_PREFIX.length, OFFICE_PACK_CALLBACK_PREFIX.length + 32);
1352
+ const id = [...this.officePackPrompts].find(([, n]) => n === nonce)?.[0];
1353
+ if (id)
1354
+ this.officePackPrompts.delete(id);
1355
+ const decided = id ? this.officePackTools.decide(id, action, t("settings_confirm.admin_label", data.userId ?? "")) : null;
1356
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, decided?.text ?? t("office_packs.gone"));
1357
+ return true;
1358
+ }
1238
1359
  /**
1239
1360
  * #2578: each instance's state as the org chart shows it (fleet-org.js orgState, from the same fields the status
1240
1361
  * frame and the Needs-you list carry) and its display name (resolveInstanceIdentity, as getUiStatus) — read once.
@@ -1245,7 +1366,10 @@ export class FleetManager {
1245
1366
  needs.set(item.instance, (needs.get(item.instance) ?? 0) + 1);
1246
1367
  const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
1247
1368
  let profiles = null;
1369
+ // #2661: the levels as last worked out (never waits; none while off or not worked out yet).
1370
+ const levels = this.officeLevelsSnapshot();
1248
1371
  return {
1372
+ level: name => (levels && Object.prototype.hasOwnProperty.call(levels, name) ? levels[name].level : undefined),
1249
1373
  stateOf: name => {
1250
1374
  if (!this.isKnownInstance(name))
1251
1375
  return "missing";
@@ -1797,12 +1921,96 @@ export class FleetManager {
1797
1921
  this.logger.info({ id: view.id }, "Settings change requires local `agend settings confirm <id>`");
1798
1922
  }
1799
1923
  kiroMigrateDeps() {
1924
+ let token;
1925
+ const current = (name) => {
1926
+ const run = this.kiroMigrationRuns.get(name);
1927
+ return !!token && run?.token === token && !this.shuttingDown
1928
+ && (this.explicitStopGeneration.get(name) ?? 0) === run.stopGeneration;
1929
+ };
1800
1930
  return {
1801
- agendHome: getAgendHome(),
1802
- fleetConfig: () => this.fleetConfig,
1803
- saveFleetConfig: () => this.saveFleetConfig(),
1931
+ agendHome: getAgendHome(), fleetConfig: () => this.fleetConfig, saveFleetConfig: () => this.saveFleetConfig(),
1804
1932
  status: name => this.getInstanceStatus(name),
1805
- restart: name => this.restartSingleInstance(name, { explicit: true }),
1933
+ reserve: name => {
1934
+ if (this.kiroMigrationRuns.has(name) || this.restartsInFlight.has(name) || this.stopsInFlight.has(name))
1935
+ throw new Error(t("kiro_migrate.in_progress"));
1936
+ token = Symbol(name);
1937
+ this.kiroMigrationRuns.set(name, { token, stopGeneration: this.explicitStopGeneration.get(name) ?? 0 });
1938
+ return { current: () => current(name), release: () => {
1939
+ if (this.kiroMigrationRuns.get(name)?.token === token)
1940
+ this.kiroMigrationRuns.delete(name);
1941
+ this.scheduleDeliveryOutboxPump();
1942
+ } };
1943
+ },
1944
+ preflight: async (name, plan) => {
1945
+ const compat = getCachedKiroCliCompatibility(resolveBinary("kiro-cli"));
1946
+ if (!supportsVerifiedKiroMigration(compat))
1947
+ throw new Error(t("kiro_migrate.version_required"));
1948
+ const daemon = this.daemons.get(name);
1949
+ if (this.getInstanceStatus(name) === "running" && (!daemon || !await daemon.kiroMigrationIdle()))
1950
+ throw new Error(t("kiro_migrate.idle_required"));
1951
+ if (!current(name) || daemon && this.daemons.get(name) !== daemon)
1952
+ throw new Error(t("kiro_migrate.superseded"));
1953
+ const inst = this.fleetConfig?.instances[name];
1954
+ const model = daemon?.getModelOverride() ?? inst?.model ?? (plan.classicId ? readKiroV1SavedModel({ id: plan.classicId,
1955
+ workingDirectory: plan.workingDirectory, dbPath: kiroV1DbPath(plan.credentialProfile) }) : undefined);
1956
+ if (!model && plan.classicId)
1957
+ throw new Error(t("kiro_migrate.model_unknown"));
1958
+ return model ?? "";
1959
+ },
1960
+ restart: async (name) => {
1961
+ if (!current(name))
1962
+ throw new Error(t("kiro_migrate.superseded"));
1963
+ const run = this.kiroMigrationRuns.get(name);
1964
+ const expected = run.stopGeneration + 1;
1965
+ // This restart owns exactly one stop. A person's concurrent stop must not be hidden by the workflow.
1966
+ try {
1967
+ await this.restartSingleInstance(name, { explicit: true, kiroMigration: token });
1968
+ }
1969
+ finally {
1970
+ // Even a failed replacement owns its completed stop; retain authority to clean up/pause it.
1971
+ if (this.kiroMigrationRuns.get(name) === run && this.explicitStopGeneration.get(name) === expected)
1972
+ run.stopGeneration = expected;
1973
+ }
1974
+ if (this.explicitStopGeneration.get(name) !== expected)
1975
+ throw new Error(t("kiro_migrate.superseded"));
1976
+ },
1977
+ verify: async (name, id, model, archived) => {
1978
+ const daemon = this.daemons.get(name), deadline = performance.now() + 60_000;
1979
+ if (!daemon)
1980
+ throw new Error(t("kiro_migrate.verification_failed"));
1981
+ while (performance.now() < deadline) {
1982
+ if (!current(name) || this.daemons.get(name) !== daemon)
1983
+ throw new Error(t("kiro_migrate.superseded"));
1984
+ const observed = await daemon.kiroMigrationObservation();
1985
+ if (observed) {
1986
+ if (observed.sessionId !== id || observed.model !== model || resolvePath(observed.transcript) !== resolvePath(kiroV2SessionsDir(), id + ".jsonl"))
1987
+ throw new Error(t("kiro_migrate.verification_failed"));
1988
+ const inst = this.fleetConfig.instances[name];
1989
+ const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: kiroV2SessionsDir(), migration: {
1990
+ agendHome: getAgendHome(), instance: name, workingDirectory: inst.working_directory, credentialProfile: instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli"),
1991
+ sessionId: id, sessionsDir: kiroV2SessionsDir(), dbPath: kiroV1DbPath(instanceCredentialProfile(inst, this.fleetConfig?.defaults, "kiro-cli")),
1992
+ model, archived, outboxDbPath: join(this.dataDir, "delivery-outbox.db"), knownInstances: Object.keys(this.fleetConfig.instances),
1993
+ } });
1994
+ if (!current(name) || this.daemons.get(name) !== daemon)
1995
+ throw new Error(t("kiro_migrate.superseded"));
1996
+ if (reply.kind !== "ok" || !reply.verification)
1997
+ throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_migrate.verification_failed"));
1998
+ return reply.verification;
1999
+ }
2000
+ await new Promise(r => setTimeout(r, 500));
2001
+ }
2002
+ throw new Error(t("kiro_migrate.verification_failed"));
2003
+ },
2004
+ stop: async (name) => { try {
2005
+ await this.stopInstance(name);
2006
+ }
2007
+ finally {
2008
+ writePausedMarker(this.getInstanceDir(name), Date.now(), "error");
2009
+ } },
2010
+ readClassicStore: async (workingDirectory, credentialProfile) => {
2011
+ const dbPath = kiroV1DbPath(credentialProfile);
2012
+ return listKiroV1Sessions(workingDirectory, dbPath, await readKiroClassicLinks(workingDirectory, dbPath));
2013
+ },
1806
2014
  };
1807
2015
  }
1808
2016
  /**
@@ -1810,7 +2018,7 @@ export class FleetManager {
1810
2018
  * here; the move runs on the Confirm (handleKiroMigrateCallback), judged again then.
1811
2019
  */
1812
2020
  async requestKiroMigration(name, startFresh) {
1813
- const judged = planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
2021
+ const judged = await planKiroMigration(this.kiroMigrateDeps(), name, startFresh);
1814
2022
  if (!judged.ok)
1815
2023
  return judged;
1816
2024
  const general = this.findGeneralInstance();
@@ -1860,8 +2068,77 @@ export class FleetManager {
1860
2068
  const outcome = await applyKiroMigration(this.kiroMigrateDeps(), plan);
1861
2069
  this.logger.info({ name: plan.name, ok: outcome.ok, classicId: plan.classicId }, "kiro classic → TUI move");
1862
2070
  this.notifyFleetError(outcome.text);
1863
- if (outcome.ok)
1864
- this.notifyInstanceTopic(plan.name, outcome.text);
2071
+ this.notifyInstanceTopic(plan.name, outcome.text);
2072
+ return true;
2073
+ }
2074
+ kiroReconcileDeps() {
2075
+ return { ...this.kiroMigrateDeps(), store: async (input, plan, actor, rollbackReceipt) => {
2076
+ const reply = await sharedKiroV2StoreLane.read({ keys: [], sessionsDir: input.sessionsDir, reconciliation: { input, plan, actor, rollbackReceipt } });
2077
+ if (reply.kind !== "ok" || !reply.reconciliation)
2078
+ throw new Error(reply.kind === "unreadable" ? reply.detail : t("kiro_reconcile.changed"));
2079
+ return reply.reconciliation;
2080
+ } };
2081
+ }
2082
+ async requestKiroReconciliation(name, keep, archive, rollbackReceipt) {
2083
+ try {
2084
+ if (!this.eventLog)
2085
+ return { ok: false, error: t("kiro_reconcile.audit_failed") };
2086
+ const plan = await planKiroReconciliation(this.kiroReconcileDeps(), name, keep, archive, rollbackReceipt);
2087
+ const general = this.findGeneralInstance(), adapterId = general ? this.getInstanceAdapterId(general) : undefined;
2088
+ const adapter = general ? this.getAdapterForInstance(general) : undefined, chatId = general ? this.getGroupIdForInstance(general) : undefined;
2089
+ const topic = general ? String(this.fleetConfig?.instances[general]?.topic_id ?? "") : "";
2090
+ if (!general || !adapterId || !adapter || !chatId)
2091
+ return { ok: false, error: t("kiro_migrate.no_general") };
2092
+ const nonce = await this.postNonceButtonPromptOrThrow({ prefix: KIRO_RECONCILE_CALLBACK_PREFIX, alertType: "clear_confirm", instanceName: name,
2093
+ adapter, adapterId, chatId, ...(topic ? { threadId: topic } : {}), message: kiroReconciliationPrompt(plan),
2094
+ choices: [{ action: "confirm", label: t("kiro_migrate.confirm_button") }, { action: "cancel", label: t("kiro_migrate.cancel_button") }],
2095
+ expiredText: t("kiro_migrate.expired", name), timeoutMs: KIRO_MIGRATE_TIMEOUT_MS });
2096
+ this.pendingKiroReconciliations.set(nonce, plan);
2097
+ setTimeout(() => this.pendingKiroReconciliations.delete(nonce), KIRO_MIGRATE_TIMEOUT_MS + 60_000).unref?.();
2098
+ this.eventLog?.insert(name, "kiro_reconcile", { phase: "requested", keep, archive, rollbackReceipt, message: kiroReconciliationPrompt(plan) });
2099
+ return { ok: true, text: t("kiro_migrate.requested") };
2100
+ }
2101
+ catch (error) {
2102
+ return { ok: false, error: error instanceof SyntaxError ? "Malformed reconciliation data" : error.message };
2103
+ }
2104
+ }
2105
+ async handleKiroReconcileCallback(data, adapterId, adapter) {
2106
+ const claimed = this.consumeNonceCallback(KIRO_RECONCILE_CALLBACK_PREFIX, /^kiro-reconcile:([0-9a-f]{32}):(confirm|cancel)$/, data, adapterId, adapter);
2107
+ if (claimed === null)
2108
+ return false;
2109
+ if (claimed === "consumed")
2110
+ return true;
2111
+ const { entry, action } = claimed;
2112
+ const nonce = data.callbackData.slice(KIRO_RECONCILE_CALLBACK_PREFIX.length, KIRO_RECONCILE_CALLBACK_PREFIX.length + 32);
2113
+ const plan = this.pendingKiroReconciliations.get(nonce);
2114
+ this.pendingKiroReconciliations.delete(nonce);
2115
+ if (action === "cancel" || !plan) {
2116
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t(plan ? "kiro_migrate.cancelled" : "kiro_migrate.expired", entry.instanceName));
2117
+ this.eventLog?.insert(entry.instanceName, "kiro_reconcile", { phase: "cancelled_or_expired", userId: data.userId });
2118
+ return true;
2119
+ }
2120
+ let outcome;
2121
+ try {
2122
+ if (!this.eventLog)
2123
+ throw new Error(t("kiro_reconcile.audit_failed"));
2124
+ this.eventLog.insert(plan.name, "kiro_reconcile", { phase: "confirmed", userId: data.userId, keep: plan.input.sessionId, archive: plan.input.archiveId });
2125
+ }
2126
+ catch {
2127
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.audit_failed"));
2128
+ this.notifyFleetError(t("kiro_reconcile.audit_failed"));
2129
+ return true;
2130
+ }
2131
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("kiro_reconcile.applying", plan.name));
2132
+ outcome = await applyKiroReconciliation(this.kiroReconcileDeps(), plan, `${adapterId}:${data.userId}`);
2133
+ try {
2134
+ this.eventLog.insert(plan.name, "kiro_reconcile", { phase: outcome.ok ? "completed" : "failed", userId: data.userId, receipt: outcome.receipt, message: outcome.text });
2135
+ }
2136
+ catch {
2137
+ this.logger.error({ name: plan.name, receipt: outcome.receipt }, "Kiro reconciliation final Log entry failed; confirmed intent remains in the audit and archive");
2138
+ }
2139
+ this.logger.info({ name: plan.name, ok: outcome.ok, receipt: outcome.receipt }, "Kiro explicit reconciliation");
2140
+ this.notifyFleetError(outcome.text);
2141
+ this.notifyInstanceTopic(plan.name, outcome.text);
1865
2142
  return true;
1866
2143
  }
1867
2144
  async handleSettingsChangeCallback(data, adapterId, adapter) {
@@ -5441,7 +5718,10 @@ export class FleetManager {
5441
5718
  */
5442
5719
  resumePaused = false,
5443
5720
  /** Phase 2a: the caller's transition, for a start made from inside a restart or wake. */
5444
- transition, execution) {
5721
+ transition, execution, kiroMigration) {
5722
+ const migration = this.kiroMigrationRuns?.get(name);
5723
+ if (migration && migration.token !== kiroMigration)
5724
+ throw new Error(t("kiro_migrate.in_progress"));
5445
5725
  execution?.assert();
5446
5726
  if (this.sessionOomStopped(name)) {
5447
5727
  if (resumePaused)
@@ -5937,6 +6217,11 @@ export class FleetManager {
5937
6217
  stopOnUnsupportedCli(name, err, what) {
5938
6218
  if (!(err instanceof UnsupportedCliError))
5939
6219
  return false;
6220
+ if (err instanceof ResumeBlockedError) {
6221
+ this.logger.error({ name, reason: err.message }, `Not starting ${what}: retained session needs store repair`);
6222
+ void this.notifyFleetError(t("fleet.resume_blocked", name, err.message, err.remedy));
6223
+ return true;
6224
+ }
5940
6225
  this.logger.error({ name, reason: err.message }, `Not starting ${what}: the installed CLI cannot run it as configured`);
5941
6226
  this.queueUnsupportedCliNotice(name, err.message);
5942
6227
  this.queueKiroIncompatNotice(name, err.message);
@@ -6136,6 +6421,9 @@ export class FleetManager {
6136
6421
  }
6137
6422
  /** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
6138
6423
  async restartSingleInstance(name, opts) {
6424
+ const migration = this.kiroMigrationRuns?.get(name);
6425
+ if (migration && migration.token !== opts?.kiroMigration)
6426
+ throw new Error(t("kiro_migrate.in_progress"));
6139
6427
  if (opts?.recoveryCurrent && !opts.recoveryCurrent())
6140
6428
  throw new Error(t("exit.expired", name));
6141
6429
  const retry = opts?.explicit ? opts.sessionOomRetry ?? captureSessionOomRetry(this.getInstanceDir(name)) : undefined;
@@ -6196,7 +6484,7 @@ export class FleetManager {
6196
6484
  return run;
6197
6485
  }
6198
6486
  isInstanceRestarting(name) {
6199
- return this.restartsInFlight.has(name);
6487
+ return this.restartsInFlight.has(name) || this.kiroMigrationRuns?.has(name) === true;
6200
6488
  }
6201
6489
  /** InstanceLifecycleContext: an explicit wake joins the restart in flight. */
6202
6490
  restartInFlight(name) {
@@ -6301,7 +6589,8 @@ export class FleetManager {
6301
6589
  return { backend, profile: credentialHomeSpec(backend) ? instanceCredentialProfile(config, this.fleetConfig?.defaults, backend) : null };
6302
6590
  });
6303
6591
  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));
6592
+ await this.startAfterRestart(name, wasPaused, pausedAt, pauseReason, () => opts?.kiroMigration ? this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition, undefined, opts.kiroMigration)
6593
+ : this.startInstance(name, config, topicMode ?? false, "fleet-topic", false, transition));
6305
6594
  this.stormWindow.markRecovered(name);
6306
6595
  return;
6307
6596
  }
@@ -7057,9 +7346,22 @@ export class FleetManager {
7057
7346
  ...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
7058
7347
  }).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
7059
7348
  // #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,
7349
+ // #2667: the ClassicBot rooms there are now, apart from fleet.yaml's instances (a grant may name either: guestReach),
7350
+ // with its display name as the sidebar resolves it (a profile alias, else the room's own), when it has one.
7351
+ const profiles = profileIdentities(this.dataDir);
7352
+ const rooms = (this.classicChannels?.getAll() ?? []).filter((r) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, r.instanceName))
7353
+ .map((r) => {
7354
+ const shown = resolveInstanceIdentity({ classic: r, profile: profiles.get(r.instanceName) }).display_name;
7355
+ return { name: r.instanceName, ...(shown && shown !== r.instanceName ? { display_name: shown } : {}) };
7356
+ })
7357
+ .sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0));
7358
+ return { enabled: cfg?.enabled === true, chat: cfg?.chat !== false, explicit: cfg?.grants !== undefined, instances: Object.keys(this.fleetConfig?.instances ?? {}).sort(), rooms, connections, guests,
7061
7359
  audit: this.guestAudit() };
7062
7360
  }
7361
+ /** #2667: the ClassicBot rooms' instance names now — what a settings save's validation knows besides fleet.yaml's instances. */
7362
+ classicRoomNames() {
7363
+ return (this.classicChannels?.getAll() ?? []).map((r) => r.instanceName);
7364
+ }
7063
7365
  /**
7064
7366
  * #2499 review: the guest audit an admin reads — the newest grant changes and admin-sent codes from the event log
7065
7367
  * (`fleet` rows: no agent's Log shows them). Through guestOverview only: Settings' admin gate, never over the public
@@ -8082,8 +8384,8 @@ export class FleetManager {
8082
8384
  const channelConfigs = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
8083
8385
  this.warnUnboundGeneralChannelIds(fleet);
8084
8386
  const primaryAdapterId = channelConfigs[0] ? (channelConfigs[0].id ?? channelConfigs[0].type) : undefined;
8387
+ let generalsCreated = adoptGeneralByName(fleet, this.logger, dir => insideManagedWorkspaces(dir) || resolvePath(dir) === join(getAgendHome(), GENERAL_NAME));
8085
8388
  const generalInstances = Object.entries(fleet.instances).filter(([, inst]) => inst.general_topic === true);
8086
- let generalsCreated = false;
8087
8389
  // Collect unbound generals (no channel_id set) for auto-assignment
8088
8390
  const unboundGenerals = generalInstances.filter(([, inst]) => !inst.channel_id);
8089
8391
  // Track which adapters still need a general
@@ -8143,9 +8445,16 @@ export class FleetManager {
8143
8445
  }
8144
8446
  // Phase 3: Create new generals for any remaining adapters
8145
8447
  for (const need of needsGeneral) {
8146
- const name = channelConfigs.length > 1 ? `general-${need.adapterId}` : "general";
8147
- if (fleet.instances[name])
8448
+ let name = channelConfigs.length > 1 ? `general-${need.adapterId}` : GENERAL_NAME;
8449
+ if (fleet.instances[name]?.general_topic)
8148
8450
  continue;
8451
+ if (fleet.instances[name]) {
8452
+ // M-2069: the name is an agent's that is not the General (adoptGeneralByName left it: it works in a project
8453
+ // of the user's) — the fleet still gets its General, under a free name, never none.
8454
+ const taken = name;
8455
+ name = freeGeneralName(fleet.instances, taken);
8456
+ 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");
8457
+ }
8149
8458
  this.logger.warn({ adapter: need.adapterId, name }, "No general instance for adapter — auto-creating");
8150
8459
  const generalDir = join(getAgendHome(), name);
8151
8460
  mkdirSync(generalDir, { recursive: true, mode: 0o700 });
@@ -9935,6 +10244,10 @@ export class FleetManager {
9935
10244
  }))
9936
10245
  return;
9937
10246
  }
10247
+ // #2675: a reply to a posted question card is its typed answer (judged by who may talk to that instance), not a
10248
+ // message for the instance — which could not take one while its card is up anyway.
10249
+ if (await this.takeRemoteCardReply(msg))
10250
+ return;
9938
10251
  // Access control — classic channels are open to all, others require an allowed
9939
10252
  // user. This runs before the dedup claim, and is judged by the adapter that
9940
10253
  // owns the topic rather than the one that happened to receive the message:
@@ -11326,7 +11639,7 @@ export class FleetManager {
11326
11639
  if (!ipc || !fleetRequestId)
11327
11640
  return;
11328
11641
  const type = String(msg.type);
11329
- const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" ? "fleet_web_theme_response"
11642
+ const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) || type === "fleet_office_view" || type === "fleet_office_set" || OFFICE_PACK_IPC.has(type) ? "fleet_web_theme_response"
11330
11643
  : type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
11331
11644
  : type.startsWith("fleet_decision_") ? "fleet_decision_response"
11332
11645
  : type === "fleet_task" ? "fleet_task_response"
@@ -11381,7 +11694,10 @@ export class FleetManager {
11381
11694
  || type === "fleet_set_web_widget" || type === "fleet_remove_web_widget"
11382
11695
  || type === "fleet_set_web_ambient" || type === "fleet_clear_web_ambient"
11383
11696
  || type === "fleet_export_web_mod" || type === "fleet_propose_web_mod"
11384
- || type === "fleet_office_view" || type === "fleet_office_set") {
11697
+ || type === "fleet_office_view" || type === "fleet_office_set"
11698
+ // #2692: the skin packs (OFFICE_PACK_IPC), answered as web theme tools.
11699
+ || type === "fleet_office_pack_check" || type === "fleet_office_pack_upload" || type === "fleet_office_pack_delete"
11700
+ || type === "fleet_office_pack_restore" || type === "fleet_office_pack_list" || type === "fleet_office_set_style") {
11385
11701
  this.handleWebTheme(name, msg);
11386
11702
  return;
11387
11703
  }
@@ -11403,7 +11719,8 @@ export class FleetManager {
11403
11719
  ? this.appearanceTools.handle(instanceName, tool, args)
11404
11720
  : tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
11405
11721
  : tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
11406
- : this.webAppearance.handleTool(instanceName, tool, args);
11722
+ : OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
11723
+ : this.webAppearance.handleTool(instanceName, tool, args);
11407
11724
  ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
11408
11725
  }
11409
11726
  catch (err) {
@@ -11841,6 +12158,8 @@ export class FleetManager {
11841
12158
  return this.agentMods.handle(instance, tool, args);
11842
12159
  if (tool === "office_view" || tool === "office_set")
11843
12160
  return this.officeActs.handle(instance, tool, args); // #2578
12161
+ if (OFFICE_PACK_IPC.has(`fleet_${tool}`))
12162
+ return this.officePackTools.handle(instance, tool, args); // #2692
11844
12163
  return this.webAppearance.handleTool(instance, tool, args);
11845
12164
  }
11846
12165
  async handlePreviewEmojisHttp(instance, args) {
@@ -12279,12 +12598,21 @@ export class FleetManager {
12279
12598
  normalizeOfficeStorage(this.fleetConfig);
12280
12599
  explicitPatches = explicitPatches.flatMap(patch => {
12281
12600
  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 }];
12601
+ // #2601 (and #2692's office_packs): a separately kept feature is written under its own key, never in `dashboard`.
12602
+ const separate = path.at(-2) === "dashboard" ? SEPARATE_FEATURE_KEYS[path.at(-1)] : undefined;
12603
+ if (separate && !patch.remove) {
12604
+ return [{ ...patch, path: [...path.slice(0, -2), separate] }, { path, remove: true }];
12605
+ }
12606
+ if (path.at(-1) === "dashboard" && patch.value && typeof patch.value === "object" && Object.keys(SEPARATE_FEATURE_KEYS).some(f => Object.hasOwn(patch.value, f))) {
12607
+ const dashboard = { ...patch.value }, moved = [];
12608
+ for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
12609
+ if (!Object.hasOwn(dashboard, feature))
12610
+ continue;
12611
+ const value = dashboard[feature];
12612
+ delete dashboard[feature];
12613
+ moved.push({ path: [...path.slice(0, -1), key], value, remove: value === null });
12614
+ }
12615
+ return [{ ...patch, value: dashboard }, ...moved];
12288
12616
  }
12289
12617
  // #2581's DM choice was withdrawn: an old string is just "on", posted in the topic, and its route is dropped.
12290
12618
  if (patch.path.join(".") === "web.notify_login" && patch.value === "dm")
@@ -12398,11 +12726,11 @@ export class FleetManager {
12398
12726
  const previous = daemon.getConfigSnapshot?.();
12399
12727
  if (!next || !previous)
12400
12728
  continue;
12401
- if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access)
12729
+ if (isDeepStrictEqual(previous.dashboard, next.dashboard) && previous.office_access === next.office_access && previous.office_pack_access === next.office_pack_access)
12402
12730
  continue;
12403
12731
  // Only permissions are adopted here. Cold fields retain the live daemon's old snapshot, so reconcile still
12404
12732
  // 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 });
12733
+ daemon.applyConfigUpdate({ dashboard: next.dashboard ?? null, office_access: next.office_access ?? null, office_pack_access: next.office_pack_access ?? null });
12406
12734
  if (taskBoardSwitchedOn(previous.dashboard, next.dashboard)) {
12407
12735
  this.admitTaskBoardNotice(name, TASK_BOARD_ON_NOTICE, `task-board-on:${name}:${Date.now()}`);
12408
12736
  }
@@ -12436,7 +12764,7 @@ export class FleetManager {
12436
12764
  logConfigWarningsAtStartup() {
12437
12765
  let warnings = [];
12438
12766
  try {
12439
- warnings = validateFleetConfig(this.rawFleetConfig).warnings;
12767
+ warnings = validateFleetConfig(this.rawFleetConfig, { rooms: readClassicRoomNames(this.dataDir) }).warnings;
12440
12768
  }
12441
12769
  catch (err) {
12442
12770
  this.logger.debug({ err }, "fleet.yaml validation failed (startup warnings)");
@@ -12467,11 +12795,13 @@ export class FleetManager {
12467
12795
  const patches = [];
12468
12796
  const raw = this.rawFleetConfig;
12469
12797
  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 });
12798
+ for (const [feature, key] of Object.entries(SEPARATE_FEATURE_KEYS)) {
12799
+ if (!item?.dashboard || !Object.hasOwn(item.dashboard, feature))
12800
+ continue;
12801
+ patches.push({ path: [...path, "dashboard", feature], remove: true });
12802
+ if (!Object.hasOwn(item, key) && typeof item.dashboard[feature] === "boolean")
12803
+ patches.push({ path: [...path, key], value: item.dashboard[feature] });
12804
+ }
12475
12805
  }
12476
12806
  this.saveFleetConfig(patches);
12477
12807
  this.logger.info("Stored Office access outside dashboard for downgrade compatibility");
@@ -12706,6 +13036,7 @@ export class FleetManager {
12706
13036
  this.mods.forgetInstance(name);
12707
13037
  this.agentMods.forget(name);
12708
13038
  this.officeActsInstance?.forget(name);
13039
+ this.officePackToolsInstance?.forget(name);
12709
13040
  }
12710
13041
  catch (err) {
12711
13042
  this.logger.warn({ err: err.message, name }, "Dropping a removed instance's web theme failed");
@@ -14073,7 +14404,7 @@ export class FleetManager {
14073
14404
  return;
14074
14405
  const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
14075
14406
  const projects = board.listProjects(true);
14076
- const stats = board.projectStats();
14407
+ const stats = board.projectStats(this.dispatcherGone);
14077
14408
  const names = new Map(projects.map((p) => [p.id, p.name]));
14078
14409
  const configured = this.getPrimaryAdapterId();
14079
14410
  const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
@@ -14337,7 +14668,7 @@ export class FleetManager {
14337
14668
  entry.web = {
14338
14669
  text: web.text,
14339
14670
  ...(links ? { links } : {}),
14340
- actions: opts.choices.map(c => ({ id: c.action, label: c.label })),
14671
+ actions: [...opts.choices.map(c => ({ id: c.action, label: c.label })), ...(opts.webInput ? [{ id: opts.webInput.action, label: opts.webInput.label, input: true }] : [])],
14341
14672
  expiresAt: Date.now() + (opts.timeoutMs ?? NONCE_BUTTON_TIMEOUT_MS),
14342
14673
  };
14343
14674
  this.emitSseEvent("prompt", { instance: entry.instanceName, nonce, ...entry.web });
@@ -14514,6 +14845,10 @@ export class FleetManager {
14514
14845
  return true;
14515
14846
  if (await this.handleKiroMigrateCallback(data, adapterId, adapter))
14516
14847
  return true;
14848
+ if (await this.handleKiroReconcileCallback(data, adapterId, adapter))
14849
+ return true;
14850
+ if (await this.handleOfficePackCallback(data, adapterId, adapter))
14851
+ return true;
14517
14852
  if (await this.handleDashboardCallback(data, adapterId, adapter))
14518
14853
  return true;
14519
14854
  if (await this.handleClassicApproval(data, adapterId, adapter))
@@ -14530,6 +14865,8 @@ export class FleetManager {
14530
14865
  return true;
14531
14866
  if (await this.handleInteractivePromptAssist(data, adapterId, adapter))
14532
14867
  return true;
14868
+ if (await this.handleRemoteCardClick(data, adapterId, adapter))
14869
+ return true;
14533
14870
  if (await this.handleClassicBackendSelection(data))
14534
14871
  return true;
14535
14872
  if (await this.handleModelSelection(data, adapterId))
@@ -14706,6 +15043,34 @@ export class FleetManager {
14706
15043
  messages: messages.filter(m => keep(m.instance)),
14707
15044
  });
14708
15045
  }
15046
+ /** #2661: the Office levels as last worked out, and the work in flight (officeLevels). */
15047
+ officeLevelsMemo = null;
15048
+ officeLevelsRun = null;
15049
+ /**
15050
+ * #2661: every instance's Office level (office-levels.ts) — worked out by the cache service from the usage store, at
15051
+ * most once per OFFICE_LEVELS_TTL_MS however many pages and share links ask (one run at a time). Null when
15052
+ * web.office_levels is false.
15053
+ */
15054
+ async officeLevels() {
15055
+ if (this.fleetConfig?.web?.office_levels === false)
15056
+ return null;
15057
+ const { OFFICE_LEVELS_TTL_MS } = await import("./office-levels.js");
15058
+ const memo = this.officeLevelsMemo;
15059
+ if (memo && performance.now() - memo.at < OFFICE_LEVELS_TTL_MS)
15060
+ return memo.levels;
15061
+ this.officeLevelsRun ??= this.cacheServiceFor().officeLevels(Date.now()).then((levels) => { this.officeLevelsMemo = { at: performance.now(), levels }; return levels; }).finally(() => { this.officeLevelsRun = null; });
15062
+ return this.officeLevelsRun;
15063
+ }
15064
+ /**
15065
+ * #2661: the levels as last worked out, for a synchronous reader (a share link's gateway): never waits; a stale or
15066
+ * missing set starts the next run. Null when web.office_levels is false or none has been worked out yet.
15067
+ */
15068
+ officeLevelsSnapshot() {
15069
+ if (this.fleetConfig?.web?.office_levels === false)
15070
+ return null;
15071
+ void this.officeLevels().catch(() => { });
15072
+ return this.officeLevelsMemo?.levels ?? null;
15073
+ }
14709
15074
  /** #2001: get_cache_report's — the same report, as last worked out (reused for a minute, until the next pass). */
14710
15075
  cacheReportPublished(window) {
14711
15076
  return this.cacheServiceFor().published(cacheWindow(window));
@@ -15003,8 +15368,12 @@ export class FleetManager {
15003
15368
  const buttons = this.replyButtons();
15004
15369
  return buttons ? buttons.clickWeb(instance, id, index, voter) : Promise.resolve({ status: 409, error: t("reply_buttons.closed") });
15005
15370
  }
15006
- async clickWebPrompt(instance, nonce, action) {
15007
- if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z-]{0,23}$/.test(action))
15371
+ async clickWebPrompt(instance, nonce, action, text) {
15372
+ if (!/^[0-9a-f]{32}$/.test(nonce) || !/^[a-z][a-z0-9-]{0,23}$/.test(action))
15373
+ return { status: 400, error: "Malformed prompt answer" };
15374
+ // #2675: only an action the page shows a text box for takes text, and it must say something.
15375
+ const input = this.pendingNonceButtons.get(nonce)?.web?.actions.find(a => a.id === action)?.input === true;
15376
+ if (input !== (text !== undefined) || (input && (!text.trim() || text.length > REMOTE_CARD_TEXT_MAX)))
15008
15377
  return { status: 400, error: "Malformed prompt answer" };
15009
15378
  const entry = this.pendingNonceButtons.get(nonce);
15010
15379
  // Unknown is the same answer as answered or expired: the page drops the buttons either way.
@@ -15026,6 +15395,8 @@ export class FleetManager {
15026
15395
  notice = n; },
15027
15396
  };
15028
15397
  this.webPromptClicks.add(data);
15398
+ if (text !== undefined)
15399
+ this.webPromptTexts.set(data, text);
15029
15400
  await this.dispatchAdapterCallback(data, entry.adapterId, entry.adapter);
15030
15401
  if (this.webPromptClaims.has(data))
15031
15402
  return { status: 200 };
@@ -16083,6 +16454,163 @@ export class FleetManager {
16083
16454
  }
16084
16455
  return true;
16085
16456
  }
16457
+ // ── #2675: a card a CLI shows in place of its composer, answered from the chat ─────────────────────────────────
16458
+ /**
16459
+ * Where an instance's card is put to people: its own place — its topic, or its ClassicBot room — or, on a fleet with
16460
+ * no chat platform, the dashboard. Never General: the answer is the instance's conversation.
16461
+ */
16462
+ remoteCardPlace(instanceName) {
16463
+ const web = this.webOnlyPromptPlace();
16464
+ if (web)
16465
+ return web;
16466
+ const room = this.classicChannels?.getChannelIdByInstance(instanceName);
16467
+ if (room) {
16468
+ const adapterId = this.classicChannels?.getAdapterIdByInstance(instanceName) ?? this.getPrimaryAdapterId();
16469
+ const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
16470
+ return adapter && adapterId ? { adapter, adapterId, chatId: room } : null;
16471
+ }
16472
+ const adapterId = this.getInstanceAdapterId(instanceName);
16473
+ const adapter = adapterId ? this.worlds.get(adapterId)?.adapter : undefined;
16474
+ const chatId = this.getGroupIdForInstance(instanceName);
16475
+ const topicId = this.fleetConfig?.instances[instanceName]?.topic_id;
16476
+ if (!adapter || !adapterId || !chatId || topicId == null)
16477
+ return null;
16478
+ return { adapter, adapterId, chatId, threadId: String(topicId) };
16479
+ }
16480
+ /**
16481
+ * The daemon saw a card held still on `instanceName`'s screen (remote_card): put it to the people who talk to that
16482
+ * instance, with a button per answer. A card already posted for the instance is retired first (the daemon said it
16483
+ * was gone; this is the fallback).
16484
+ */
16485
+ async postRemoteCard(instanceName, card) {
16486
+ this.retireRemoteCard(instanceName, null, t("card.answered_in_pane"));
16487
+ const place = this.remoteCardPlace(instanceName);
16488
+ if (!place) {
16489
+ this.logger.warn({ instanceName }, "A card is waiting on screen but the instance has no place to post it — the parked notice stands");
16490
+ return;
16491
+ }
16492
+ const prompt = remoteCardPrompt(instanceName, card);
16493
+ const nonce = await this.postNonceButtonPrompt({
16494
+ prefix: REMOTE_CARD_CALLBACK_PREFIX, alertType: "interactive_prompt", instanceName, ...place,
16495
+ message: prompt.message, choices: prompt.choices, webInput: prompt.webInput,
16496
+ expiredText: t("card.expired", instanceName), timeoutMs: REMOTE_CARD_TIMEOUT_MS,
16497
+ extra: { remoteCard: { identity: card.identity, kind: card.kind, options: card.kind === "question" ? card.options.map(o => ({ n: o.n, title: o.title })) : [] } },
16498
+ });
16499
+ if (nonce)
16500
+ this.remoteCardPosts.set(instanceName, nonce);
16501
+ }
16502
+ /**
16503
+ * The card is gone from the screen (remote_card_gone, `identity`; null: whichever is posted): its buttons collapse to
16504
+ * `text` if nobody answered them here.
16505
+ */
16506
+ retireRemoteCard(instanceName, identity, text = t("card.answered_in_pane")) {
16507
+ const nonce = this.remoteCardPosts.get(instanceName);
16508
+ if (!nonce)
16509
+ return;
16510
+ const entry = this.pendingNonceButtons.get(nonce);
16511
+ if (entry && identity !== null && entry.remoteCard?.identity !== identity)
16512
+ return;
16513
+ this.remoteCardPosts.delete(instanceName);
16514
+ if (!entry)
16515
+ return;
16516
+ this.pendingNonceButtons.delete(nonce);
16517
+ if (entry.timer)
16518
+ clearTimeout(entry.timer);
16519
+ void this.retireNonceButtons(entry, entry.messageId ?? "", text);
16520
+ }
16521
+ /** Who may answer an instance's card: whoever may already talk to it there (a ClassicBot room is open to all). */
16522
+ mayAnswerRemoteCard(userId, entry) {
16523
+ if (this.classicChannels?.getChannelIdByInstance(entry.instanceName) === entry.chatId)
16524
+ return true;
16525
+ const am = this.worlds.get(entry.adapterId)?.accessManager ?? this.accessManager;
16526
+ return !am || am.isAllowed(userId);
16527
+ }
16528
+ /** A click on a card's button (platform or dashboard). */
16529
+ async handleRemoteCardClick(data, callbackAdapterId, receivingAdapter) {
16530
+ if (!data.callbackData.startsWith(REMOTE_CARD_CALLBACK_PREFIX))
16531
+ return false;
16532
+ // A typed answer from the dashboard must come with its text; refused before the claim, so the buttons stay.
16533
+ const match = REMOTE_CARD_CALLBACK_RE.exec(data.callbackData);
16534
+ if (match?.[2] === "text" && remoteCardAction("text", this.webPromptTexts.get(data)) === null) {
16535
+ data.ack?.(t("buttons.stale_notice"));
16536
+ return true;
16537
+ }
16538
+ const claimed = this.consumeNonceCallback(REMOTE_CARD_CALLBACK_PREFIX, REMOTE_CARD_CALLBACK_RE, data, callbackAdapterId, receivingAdapter, undefined, (userId, entry) => this.mayAnswerRemoteCard(userId, entry));
16539
+ if (claimed === null)
16540
+ return false;
16541
+ if (claimed === "consumed")
16542
+ return true;
16543
+ const action = remoteCardAction(claimed.action, this.webPromptTexts.get(data));
16544
+ const who = this.webPromptClicks.has(data) ? t("card.someone_on_dashboard") : (data.username ?? data.userId ?? "?");
16545
+ await this.answerRemoteCardFor(claimed.entry, action, who, data.messageId, data.editClicked);
16546
+ return true;
16547
+ }
16548
+ /**
16549
+ * An inbound message that replies to a posted question card, by someone who may answer it: its text is the typed
16550
+ * answer, and the message goes no further (true). Another bot's copy of a reply already taken is dropped (true).
16551
+ */
16552
+ async takeRemoteCardReply(msg) {
16553
+ if (msg.isBotMessage || !msg.replyTo)
16554
+ return false;
16555
+ const now = performance.now();
16556
+ for (const [key, until] of this.takenCardReplies)
16557
+ if (until <= now)
16558
+ this.takenCardReplies.delete(key);
16559
+ const key = `${msg.chatId}|${msg.messageId}`;
16560
+ if (this.takenCardReplies.has(key))
16561
+ return true;
16562
+ for (const [nonce, entry] of this.pendingNonceButtons) {
16563
+ if (entry.prefix !== REMOTE_CARD_CALLBACK_PREFIX || entry.remoteCard?.kind !== "question")
16564
+ continue;
16565
+ // By place, whichever of the fleet's bots received the reply (a sibling's copy can arrive first, #2680 review), on
16566
+ // the post's own platform; who may answer is judged by the post's world (mayAnswerRemoteCard), as Needs you does.
16567
+ if (entry.chatId !== msg.chatId || entry.messageId !== msg.replyTo || entry.adapter.type !== msg.source)
16568
+ continue;
16569
+ if (!msg.userId || !this.mayAnswerRemoteCard(msg.userId, entry))
16570
+ return false; // not theirs to answer: it is just a message
16571
+ const action = remoteCardAction("text", msg.text ?? "");
16572
+ if (!action)
16573
+ return false;
16574
+ this.pendingNonceButtons.delete(nonce);
16575
+ if (entry.timer)
16576
+ clearTimeout(entry.timer);
16577
+ this.webPromptGone(entry);
16578
+ this.takenCardReplies.set(key, now + REMOTE_CARD_TAKEN_TTL_MS);
16579
+ await this.answerRemoteCardFor(entry, action, msg.username ?? msg.userId, entry.messageId ?? msg.replyTo);
16580
+ return true;
16581
+ }
16582
+ return false;
16583
+ }
16584
+ /**
16585
+ * Key a claimed answer into the instance's card (the daemon checks it is still that card and walks a verified
16586
+ * cursor), then collapse the post to what happened. When the card is still waiting and was not answered — refused
16587
+ * or stopped part-way — it is posted again, so it can still be answered from here.
16588
+ */
16589
+ async answerRemoteCardFor(entry, action, who, messageId, editClicked) {
16590
+ if (this.remoteCardPosts.get(entry.instanceName) === entry.nonce)
16591
+ this.remoteCardPosts.delete(entry.instanceName);
16592
+ const daemon = this.daemons.get(entry.instanceName);
16593
+ const card = entry.remoteCard;
16594
+ let outcome;
16595
+ if (!card || !action)
16596
+ outcome = { outcome: "refused", reason: "malformed" };
16597
+ else if (!daemon?.answerRemoteCard)
16598
+ outcome = { outcome: "stale" };
16599
+ else {
16600
+ try {
16601
+ outcome = await daemon.answerRemoteCard(card.identity, action);
16602
+ }
16603
+ catch (err) {
16604
+ this.logger.warn({ err: err?.message, instanceName: entry.instanceName }, "Answering a card from the chat failed");
16605
+ outcome = { outcome: "stopped", step: "error" };
16606
+ }
16607
+ }
16608
+ this.eventLog?.insert(entry.instanceName, "remote_card_answer", { action: action?.kind ?? null, outcome: outcome.outcome });
16609
+ const choice = card && action ? remoteCardChoiceLabel(card, action) : "?";
16610
+ await this.retireNonceButtons(entry, messageId, remoteCardOutcomeText(entry.instanceName, who, choice, outcome), editClicked);
16611
+ if (outcome.outcome === "refused" || outcome.outcome === "stopped")
16612
+ daemon?.repostRemoteCard?.();
16613
+ }
16086
16614
  // ── Cancel button ────────────────────────────────────────────────────
16087
16615
  // Sent after delivering a user message to an instance; clicking it (or
16088
16616
  // /cancel) sends Escape to the instance's pane to interrupt generation.