@songsid/agend 2.3.3-beta.1 → 2.3.3-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (355) hide show
  1. package/dist/agent-cli.js +13 -112
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +0 -1
  4. package/dist/agent-endpoint.js +1 -9
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.d.ts +3 -6
  7. package/dist/agent-whats-new.js +7 -18
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.d.ts +0 -7
  10. package/dist/auth-api.js +1 -15
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/authority/authority-state.js +1 -1
  13. package/dist/authority/authority-state.js.map +1 -1
  14. package/dist/authority/web-authority.d.ts +9 -4
  15. package/dist/authority/web-authority.js +10 -5
  16. package/dist/authority/web-authority.js.map +1 -1
  17. package/dist/backend/antigravity.js +6 -1
  18. package/dist/backend/antigravity.js.map +1 -1
  19. package/dist/backend/claude-code.d.ts +7 -0
  20. package/dist/backend/claude-code.js +34 -28
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/claude-transcript-errors.d.ts +18 -0
  23. package/dist/backend/claude-transcript-errors.js +19 -0
  24. package/dist/backend/claude-transcript-errors.js.map +1 -0
  25. package/dist/backend/codex-session-lookup.d.ts +16 -3
  26. package/dist/backend/codex-session-lookup.js +45 -11
  27. package/dist/backend/codex-session-lookup.js.map +1 -1
  28. package/dist/backend/codex.d.ts +16 -0
  29. package/dist/backend/codex.js +184 -35
  30. package/dist/backend/codex.js.map +1 -1
  31. package/dist/backend/grok.js +58 -12
  32. package/dist/backend/grok.js.map +1 -1
  33. package/dist/backend/kiro.js +2 -0
  34. package/dist/backend/kiro.js.map +1 -1
  35. package/dist/backend/muse.js +2 -1
  36. package/dist/backend/muse.js.map +1 -1
  37. package/dist/backend/types.d.ts +46 -0
  38. package/dist/backend/types.js.map +1 -1
  39. package/dist/bundled-skills.d.ts +4 -6
  40. package/dist/bundled-skills.js +10 -14
  41. package/dist/bundled-skills.js.map +1 -1
  42. package/dist/channel/adapters/discord.js +60 -17
  43. package/dist/channel/adapters/discord.js.map +1 -1
  44. package/dist/channel/mcp-server.js +5 -5
  45. package/dist/channel/mcp-server.js.map +1 -1
  46. package/dist/channel/mcp-tools.d.ts +1 -3
  47. package/dist/channel/mcp-tools.js +4 -58
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/classic-channel-manager.d.ts +4 -1
  50. package/dist/classic-channel-manager.js +21 -7
  51. package/dist/classic-channel-manager.js.map +1 -1
  52. package/dist/cli.js +21 -4
  53. package/dist/cli.js.map +1 -1
  54. package/dist/config-history.d.ts +50 -0
  55. package/dist/config-history.js +263 -0
  56. package/dist/config-history.js.map +1 -0
  57. package/dist/config-validator.js +7 -7
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +3 -0
  60. package/dist/config.js +11 -4
  61. package/dist/config.js.map +1 -1
  62. package/dist/daemon.d.ts +73 -3
  63. package/dist/daemon.js +334 -121
  64. package/dist/daemon.js.map +1 -1
  65. package/dist/daily-summary.d.ts +8 -1
  66. package/dist/daily-summary.js +8 -5
  67. package/dist/daily-summary.js.map +1 -1
  68. package/dist/dashboard-live.d.ts +1 -1
  69. package/dist/dashboard-live.js +3 -3
  70. package/dist/dashboard-live.js.map +1 -1
  71. package/dist/dashboard-permissions.d.ts +1 -1
  72. package/dist/dashboard-permissions.js +1 -3
  73. package/dist/dashboard-permissions.js.map +1 -1
  74. package/dist/delivery-outbox.d.ts +4 -0
  75. package/dist/delivery-outbox.js +12 -2
  76. package/dist/delivery-outbox.js.map +1 -1
  77. package/dist/doctor.d.ts +4 -0
  78. package/dist/doctor.js +16 -1
  79. package/dist/doctor.js.map +1 -1
  80. package/dist/fleet-level-config.js +1 -1
  81. package/dist/fleet-level-config.js.map +1 -1
  82. package/dist/fleet-manager.d.ts +70 -54
  83. package/dist/fleet-manager.js +257 -322
  84. package/dist/fleet-manager.js.map +1 -1
  85. package/dist/fleet-process-handlers.d.ts +5 -0
  86. package/dist/fleet-process-handlers.js +21 -0
  87. package/dist/fleet-process-handlers.js.map +1 -1
  88. package/dist/fleet-service-owner.d.ts +11 -0
  89. package/dist/fleet-service-owner.js +3 -0
  90. package/dist/fleet-service-owner.js.map +1 -0
  91. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
  92. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
  93. package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
  94. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +29 -67
  95. package/dist/instance-lifecycle.d.ts +19 -0
  96. package/dist/instance-lifecycle.js +52 -10
  97. package/dist/instance-lifecycle.js.map +1 -1
  98. package/dist/locale.js +92 -42
  99. package/dist/locale.js.map +1 -1
  100. package/dist/needs-you-hub.d.ts +1 -0
  101. package/dist/needs-you-hub.js +6 -3
  102. package/dist/needs-you-hub.js.map +1 -1
  103. package/dist/needs-you-live.d.ts +3 -1
  104. package/dist/needs-you-live.js +24 -2
  105. package/dist/needs-you-live.js.map +1 -1
  106. package/dist/needs-you.d.ts +13 -2
  107. package/dist/needs-you.js +11 -1
  108. package/dist/needs-you.js.map +1 -1
  109. package/dist/notify-socket.d.ts +10 -0
  110. package/dist/notify-socket.js +45 -0
  111. package/dist/notify-socket.js.map +1 -0
  112. package/dist/outbound-handlers.js +19 -7
  113. package/dist/outbound-handlers.js.map +1 -1
  114. package/dist/outbound-schemas.d.ts +0 -62
  115. package/dist/outbound-schemas.js +3 -42
  116. package/dist/outbound-schemas.js.map +1 -1
  117. package/dist/parked-notices.d.ts +87 -0
  118. package/dist/parked-notices.js +194 -0
  119. package/dist/parked-notices.js.map +1 -0
  120. package/dist/public-link-progress.d.ts +5 -2
  121. package/dist/public-link-progress.js +4 -3
  122. package/dist/public-link-progress.js.map +1 -1
  123. package/dist/public-web-gateway.js +0 -9
  124. package/dist/public-web-gateway.js.map +1 -1
  125. package/dist/public-web-link.d.ts +12 -0
  126. package/dist/public-web-link.js +29 -5
  127. package/dist/public-web-link.js.map +1 -1
  128. package/dist/quota-notice.d.ts +23 -0
  129. package/dist/quota-notice.js +22 -0
  130. package/dist/quota-notice.js.map +1 -0
  131. package/dist/scheduler/task-board.d.ts +0 -13
  132. package/dist/scheduler/task-board.js +0 -14
  133. package/dist/scheduler/task-board.js.map +1 -1
  134. package/dist/sd-notify.d.ts +10 -1
  135. package/dist/sd-notify.js +27 -2
  136. package/dist/sd-notify.js.map +1 -1
  137. package/dist/search/search-docs.d.ts +1 -1
  138. package/dist/search/search-docs.js +2 -2
  139. package/dist/search/search-docs.js.map +1 -1
  140. package/dist/settings-api.d.ts +16 -0
  141. package/dist/settings-api.js +55 -2
  142. package/dist/settings-api.js.map +1 -1
  143. package/dist/settings-change.js +3 -1
  144. package/dist/settings-change.js.map +1 -1
  145. package/dist/settings-confirmation.d.ts +17 -3
  146. package/dist/settings-confirmation.js +8 -5
  147. package/dist/settings-confirmation.js.map +1 -1
  148. package/dist/settings-http-confirmation.js +31 -1
  149. package/dist/settings-http-confirmation.js.map +1 -1
  150. package/dist/setup-wizard.d.ts +3 -0
  151. package/dist/setup-wizard.js +4 -2
  152. package/dist/setup-wizard.js.map +1 -1
  153. package/dist/task-board-waker.d.ts +4 -1
  154. package/dist/task-board-waker.js +13 -2
  155. package/dist/task-board-waker.js.map +1 -1
  156. package/dist/tool-permissions.js +3 -10
  157. package/dist/tool-permissions.js.map +1 -1
  158. package/dist/topic-commands.d.ts +27 -7
  159. package/dist/topic-commands.js +95 -36
  160. package/dist/topic-commands.js.map +1 -1
  161. package/dist/transcript-monitor.d.ts +7 -0
  162. package/dist/transcript-monitor.js +48 -3
  163. package/dist/transcript-monitor.js.map +1 -1
  164. package/dist/tunnel/cloudflared.d.ts +5 -0
  165. package/dist/tunnel/cloudflared.js +32 -1
  166. package/dist/tunnel/cloudflared.js.map +1 -1
  167. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  168. package/dist/tunnel/named-diagnostics.js +4 -1
  169. package/dist/tunnel/named-diagnostics.js.map +1 -1
  170. package/dist/tunnel/named-link-test.d.ts +90 -0
  171. package/dist/tunnel/named-link-test.js +270 -0
  172. package/dist/tunnel/named-link-test.js.map +1 -0
  173. package/dist/tunnel/purpose-lane.d.ts +3 -1
  174. package/dist/tunnel/purpose-lane.js +2 -0
  175. package/dist/tunnel/purpose-lane.js.map +1 -1
  176. package/dist/types.d.ts +8 -6
  177. package/dist/ui/chat-ambient.js +4 -4
  178. package/dist/ui/chat-commands.js +3 -2
  179. package/dist/ui/chat-store.js +4 -1
  180. package/dist/ui/chat-strings.js +12 -46
  181. package/dist/ui/chat-thread.js +51 -59
  182. package/dist/ui/fleet-office-skin.js +37 -14
  183. package/dist/ui/fleet-office.js +31 -7
  184. package/dist/ui/html-attachment.js +22 -8
  185. package/dist/ui/panel-chat.js +21 -50
  186. package/dist/ui/panel-needs.js +11 -9
  187. package/dist/ui/panel-settings.js +5 -3
  188. package/dist/ui/preview-panel.js +12 -21
  189. package/dist/ui/preview.js +10 -3
  190. package/dist/ui/search-dialog.js +30 -14
  191. package/dist/ui/settings-appearance.js +1 -3
  192. package/dist/ui/settings-confirm.js +18 -0
  193. package/dist/ui/settings-guests.js +13 -4
  194. package/dist/ui/settings-learn.js +1 -1
  195. package/dist/ui/settings-mods.js +5 -12
  196. package/dist/ui/settings-named-test.js +144 -0
  197. package/dist/ui/settings-registry.js +0 -1
  198. package/dist/ui/settings-strings.js +14 -14
  199. package/dist/ui/settings-web-authority.js +4 -3
  200. package/dist/ui/shared/app-i18n.js +15 -4
  201. package/dist/ui/shared/app-needs.js +5 -1
  202. package/dist/ui/shared/app-prefs.js +17 -17
  203. package/dist/ui/shared/app-shell.js +47 -9
  204. package/dist/ui/shared/app-store.js +6 -2
  205. package/dist/ui/shared/app.css +33 -39
  206. package/dist/ui/shared/app.js +5 -4
  207. package/dist/ui/shared/instance-nav.js +9 -9
  208. package/dist/ui/shared/instance-switch.js +8 -2
  209. package/dist/ui/shared/panel-view.js +22 -8
  210. package/dist/ui/shared/view-strings.js +6 -0
  211. package/dist/ui/shared/web-authorize.js +35 -13
  212. package/dist/usage/usage-api.d.ts +3 -0
  213. package/dist/usage/usage-api.js +5 -1
  214. package/dist/usage/usage-api.js.map +1 -1
  215. package/dist/view-api.d.ts +2 -0
  216. package/dist/view-api.js +64 -0
  217. package/dist/view-api.js.map +1 -1
  218. package/dist/web-api.d.ts +9 -17
  219. package/dist/web-api.js +257 -371
  220. package/dist/web-api.js.map +1 -1
  221. package/dist/web-appearance-tools.d.ts +0 -2
  222. package/dist/web-appearance-tools.js +2 -5
  223. package/dist/web-appearance-tools.js.map +1 -1
  224. package/dist/web-appearance-view.d.ts +0 -6
  225. package/dist/web-appearance-view.js +0 -1
  226. package/dist/web-appearance-view.js.map +1 -1
  227. package/dist/web-appearance.d.ts +14 -7
  228. package/dist/web-appearance.js +5 -6
  229. package/dist/web-appearance.js.map +1 -1
  230. package/dist/web-asset-parts.d.ts +1 -17
  231. package/dist/web-asset-parts.js +1 -15
  232. package/dist/web-asset-parts.js.map +1 -1
  233. package/dist/web-auth.js +1 -7
  234. package/dist/web-auth.js.map +1 -1
  235. package/dist/web-chat-history.d.ts +3 -7
  236. package/dist/web-chat-history.js +10 -17
  237. package/dist/web-chat-history.js.map +1 -1
  238. package/dist/web-commands.d.ts +9 -1
  239. package/dist/web-commands.js +13 -0
  240. package/dist/web-commands.js.map +1 -1
  241. package/dist/web-experimental.d.ts +2 -11
  242. package/dist/web-experimental.js +8 -36
  243. package/dist/web-experimental.js.map +1 -1
  244. package/dist/web-guest-access.d.ts +40 -2
  245. package/dist/web-guest-access.js +50 -14
  246. package/dist/web-guest-access.js.map +1 -1
  247. package/dist/web-mod-bundle.d.ts +2 -45
  248. package/dist/web-mod-bundle.js +15 -63
  249. package/dist/web-mod-bundle.js.map +1 -1
  250. package/dist/web-mod-store.d.ts +3 -48
  251. package/dist/web-mod-store.js +30 -97
  252. package/dist/web-mod-store.js.map +1 -1
  253. package/dist/web-prefs.d.ts +13 -0
  254. package/dist/web-prefs.js +38 -0
  255. package/dist/web-prefs.js.map +1 -1
  256. package/dist/web-preview.d.ts +6 -13
  257. package/dist/web-preview.js +12 -23
  258. package/dist/web-preview.js.map +1 -1
  259. package/dist/web-session.d.ts +11 -3
  260. package/dist/web-session.js +22 -11
  261. package/dist/web-session.js.map +1 -1
  262. package/dist/web-settings-patch.d.ts +1 -0
  263. package/dist/web-settings-patch.js +6 -3
  264. package/dist/web-settings-patch.js.map +1 -1
  265. package/dist/web-settings-tools.d.ts +0 -13
  266. package/dist/web-settings-tools.js +12 -84
  267. package/dist/web-settings-tools.js.map +1 -1
  268. package/launcher/agend-install.cjs +7 -1
  269. package/launcher/fleet-service-owner.cjs +150 -0
  270. package/package.json +1 -1
  271. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
  272. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -673
  273. package/dist/task-board-summary.d.ts +0 -101
  274. package/dist/task-board-summary.js +0 -177
  275. package/dist/task-board-summary.js.map +0 -1
  276. package/dist/ui/app-effects.js +0 -251
  277. package/dist/ui/chat-effect-posters.js +0 -42
  278. package/dist/ui/chat-effects.js +0 -167
  279. package/dist/ui/effect-trial-controller.js +0 -124
  280. package/dist/ui/effect-trial-offer.js +0 -2
  281. package/dist/ui/effect-trial-panel.js +0 -40
  282. package/dist/ui/effects-waker.js +0 -2
  283. package/dist/ui/settings-effects.js +0 -202
  284. package/dist/ui/settings-owner-effects.js +0 -82
  285. package/dist/ui/shared/effect-app-runtime.js +0 -299
  286. package/dist/ui/shared/effect-baseline.js +0 -64
  287. package/dist/ui/shared/effect-chat-runtime.js +0 -162
  288. package/dist/ui/shared/effect-cursor.js +0 -126
  289. package/dist/ui/shared/effect-display-metadata.js +0 -39
  290. package/dist/ui/shared/effect-frame.js +0 -146
  291. package/dist/ui/shared/effect-geometry.js +0 -137
  292. package/dist/ui/shared/effect-host-style.js +0 -10
  293. package/dist/ui/shared/effect-owner-regions.js +0 -74
  294. package/dist/ui/shared/effect-owner-source.js +0 -122
  295. package/dist/ui/shared/effect-policy.js +0 -151
  296. package/dist/ui/shared/effect-poster-reader.js +0 -98
  297. package/dist/ui/shared/effect-posters.js +0 -117
  298. package/dist/ui/shared/effect-profiler.js +0 -184
  299. package/dist/ui/shared/effect-profiles.js +0 -65
  300. package/dist/ui/shared/effect-registry.js +0 -545
  301. package/dist/ui/shared/effect-runtime-loader.js +0 -236
  302. package/dist/ui/shared/effect-safety.js +0 -153
  303. package/dist/ui/shared/effect-stock-snapshot.js +0 -43
  304. package/dist/ui/shared/effect-stock-trust.js +0 -217
  305. package/dist/ui/shared/effect-trial-shadow.js +0 -62
  306. package/dist/ui/shared/effect-trial-source.js +0 -103
  307. package/dist/ui/shared/effect-trial-stage.js +0 -170
  308. package/dist/ui/shared/effect-trial.css +0 -40
  309. package/dist/ui/shared/feature-flags.js +0 -127
  310. package/dist/ui/shared/owner-effect-prefs.js +0 -117
  311. package/dist/web-effect-bootstrap.d.ts +0 -12
  312. package/dist/web-effect-bootstrap.js +0 -49
  313. package/dist/web-effect-bootstrap.js.map +0 -1
  314. package/dist/web-effect-bundle.d.ts +0 -28
  315. package/dist/web-effect-bundle.js +0 -171
  316. package/dist/web-effect-bundle.js.map +0 -1
  317. package/dist/web-effect-canonical.d.ts +0 -8
  318. package/dist/web-effect-canonical.js +0 -27
  319. package/dist/web-effect-canonical.js.map +0 -1
  320. package/dist/web-effect-library.d.ts +0 -114
  321. package/dist/web-effect-library.js +0 -530
  322. package/dist/web-effect-library.js.map +0 -1
  323. package/dist/web-effect-playback.d.ts +0 -30
  324. package/dist/web-effect-playback.js +0 -71
  325. package/dist/web-effect-playback.js.map +0 -1
  326. package/dist/web-effect-poster.d.ts +0 -8
  327. package/dist/web-effect-poster.js +0 -52
  328. package/dist/web-effect-poster.js.map +0 -1
  329. package/dist/web-effect-profiles.d.ts +0 -118
  330. package/dist/web-effect-profiles.js +0 -181
  331. package/dist/web-effect-profiles.js.map +0 -1
  332. package/dist/web-effect-service.d.ts +0 -93
  333. package/dist/web-effect-service.js +0 -335
  334. package/dist/web-effect-service.js.map +0 -1
  335. package/dist/web-effect-tools.d.ts +0 -48
  336. package/dist/web-effect-tools.js +0 -129
  337. package/dist/web-effect-tools.js.map +0 -1
  338. package/dist/web-effect-trials.d.ts +0 -86
  339. package/dist/web-effect-trials.js +0 -204
  340. package/dist/web-effect-trials.js.map +0 -1
  341. package/dist/web-effect-upload.d.ts +0 -27
  342. package/dist/web-effect-upload.js +0 -185
  343. package/dist/web-effect-upload.js.map +0 -1
  344. package/dist/web-effect-worker.d.ts +0 -8
  345. package/dist/web-effect-worker.js +0 -170
  346. package/dist/web-effect-worker.js.map +0 -1
  347. package/dist/web-effect.d.ts +0 -103
  348. package/dist/web-effect.js +0 -153
  349. package/dist/web-effect.js.map +0 -1
  350. package/dist/web-experimental-http.d.ts +0 -2
  351. package/dist/web-experimental-http.js +0 -12
  352. package/dist/web-experimental-http.js.map +0 -1
  353. package/dist/web-mod-agent.d.ts +0 -50
  354. package/dist/web-mod-agent.js +0 -207
  355. package/dist/web-mod-agent.js.map +0 -1
@@ -27,6 +27,7 @@ import { clearPublicLinkOwner, readPublicLinkOwner, writePublicLinkOwner } from
27
27
  import { PUBLIC_PREVIEW_FRAME_SRC, PUBLIC_PREVIEW_TUNNEL_DIR, PublicPreview, publicPreviewWanted } from "./public-preview.js";
28
28
  import { CloudflaredProvider } from "./tunnel/cloudflared.js";
29
29
  import { ensureCloudflared } from "./tunnel/cloudflared-install.js";
30
+ import { namedTestLane, runNamedLinkTest } from "./tunnel/named-link-test.js";
30
31
  import { OFFICE_SHARE_TUNNEL_DIR, OfficeShareService } from "./office-share-service.js";
31
32
  import { ALL_OFFICES, WHOLE_FLEET } from "./office-share.js";
32
33
  import { TunnelPurposeLane } from "./tunnel/purpose-lane.js";
@@ -67,7 +68,7 @@ const __dirname = dirname(__filename);
67
68
  const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
68
69
  import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
69
70
  import { isProbeableRouteTarget } from "./fleet-context.js";
70
- import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
71
+ import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
71
72
  import { EventLog } from "./event-log.js";
72
73
  import { binaryProbe } from "./binary-probe.js";
73
74
  import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
@@ -80,6 +81,7 @@ import { TmuxManager } from "./tmux-manager.js";
80
81
  import { AccessManager } from "./channel/access-manager.js";
81
82
  import { IpcClient } from "./channel/ipc-bridge.js";
82
83
  import { usesChannelAddress } from "./channel/mcp-tools.js";
84
+ import { ParkedNotices, PARKED_NOTICE_LEDGER_FILE } from "./parked-notices.js";
83
85
  import { formatFleetTime } from "./tz-utils.js";
84
86
  import { createAdapter } from "./channel/factory.js";
85
87
  import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
@@ -100,7 +102,6 @@ import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./schedul
100
102
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
101
103
  import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext, readStatuslineJson, statuslineModel } from "./topic-commands.js";
102
104
  import { DailySummary } from "./daily-summary.js";
103
- import { TaskBoardSummaryClock, buildSummary, isQuiet, renderSummary, summaryMarkFile, summaryTime } from "./task-board-summary.js";
104
105
  import { WebhookEmitter } from "./webhook-emitter.js";
105
106
  import { TmuxControlClient } from "./tmux-control.js";
106
107
  import { safeHandler } from "./safe-async.js";
@@ -177,13 +178,6 @@ import { OfficeSkinStore } from "./web-office-skins.js";
177
178
  import { OfficeActs } from "./web-office-acts.js";
178
179
  import { OFFICE_PACK_TOOLS, OfficePackTools } from "./web-office-pack-tools.js";
179
180
  import { WebAppearance } from "./web-appearance.js";
180
- import { WebEffectLibrary } from "./web-effect-library.js";
181
- import { WebEffectTrials } from "./web-effect-trials.js";
182
- import { WebEffectService } from "./web-effect-service.js";
183
- import { WebEffectProfiles } from "./web-effect-profiles.js";
184
- import { WebEffectTools, WEB_EFFECT_TOOLS } from "./web-effect-tools.js";
185
- import { resolveEffectParams } from "./web-effect.js";
186
- import { cw1EffectChecker } from "./web-mod-bundle.js";
187
181
  import { effectiveAppearanceLimits } from "./web-appearance-limits.js";
188
182
  import { WEB_NAME_FALLBACK, safeWebName, validateWebName, webNameInUse, webPersonKey } from "./web-profile.js";
189
183
  import { ReplyButtonStore, parseReplyButtons, replyButtonClickText, replyButtonsFallbackText, REPLY_BUTTON_PREFIX } from "./reply-buttons.js";
@@ -196,7 +190,6 @@ import { autoIdentityColor } from "./web-identity-color.js";
196
190
  import { AppearanceTools } from "./web-appearance-tools.js";
197
191
  import { cardVersion, resolveCard } from "./web-card.js";
198
192
  import { ModStore } from "./web-mod-store.js";
199
- import { AgentMods } from "./web-mod-agent.js";
200
193
  import { downloadStickerImage } from "./channel/sticker-download.js";
201
194
  import { WebImageCache, imageMimeForFile, WEB_IMAGE_MAX_AGE_MS, WEB_IMAGE_MAX_BYTES, WEB_IMAGE_RETRY_MS } from "./web-images.js";
202
195
  import { accentColor, avatarFor, ownBotConnections } from "./instance-avatar.js";
@@ -215,7 +208,7 @@ import { crossInstanceVisibility, runVisibilityCommand, xmsgVisibility } from ".
215
208
  import { SEARCH_RETENTION_DAYS, SearchIndex } from "./search/search-index.js";
216
209
  import { chatDoc, taskDoc, xmsgDoc } from "./search/search-docs.js";
217
210
  import { WebSettingsTools } from "./web-settings-tools.js";
218
- import { bindGuestRequest, guestRouteDecision } from "./web-guest-access.js";
211
+ import { GUEST_COMMAND_RUNS, GUEST_PREF_WRITES, GUEST_SEARCHES, bindGuestRequest, guestRouteDecision } from "./web-guest-access.js";
219
212
  import { GUEST_ROOM_TICK_MS, GuestRoomMembership, guestGroupRooms } from "./web-guest-rooms.js";
220
213
  import { Delegations, TrafficLog, trafficItem } from "./fleet-traffic.js";
221
214
  import { installedChannel, updateNoticeKey } from "./update-check.js";
@@ -235,6 +228,7 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
235
228
  import { recentChatContext } from "./log-tail.js";
236
229
  import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
237
230
  import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
231
+ import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
238
232
  import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
239
233
  import { assertExplicitInstanceRemoval } from "./instance-removal.js";
240
234
  import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
@@ -259,14 +253,14 @@ import { buildOrgChart } from "./web-org.js";
259
253
  import { topMemoryHogs } from "./memory-hogs.js";
260
254
  import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
261
255
  import { CacheService, cacheWindow } from "./cache-service.js";
262
- import { runWebCommand } from "./web-commands.js";
256
+ import { guestWebCommands, runWebCommand } from "./web-commands.js";
263
257
  import { CompactWatch, launchIdentity } from "./compact-watch.js";
264
258
  import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
265
259
  import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
266
260
  import { sharedRolloutIndex } from "./rollout-index.js";
267
261
  import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
268
262
  import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
269
- import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
263
+ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
270
264
  import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
271
265
  import { isWebPageNavigation } from "./web-shell-routes.js";
272
266
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
@@ -279,10 +273,8 @@ import { decisionListAnswer } from "./decision-list.js";
279
273
  import { CodeRequestLimiter, ReturningDevices } from "./web-returning-device.js";
280
274
  import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, LOOPBACK_HOST_NAMES, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
281
275
  import { createPreviewListener, normalizeOrigin, previewAvailability, previewSettings } from "./web-preview.js";
282
- import { canvasWorkerEnabled, isCanvasWorkerTool, CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
283
- import { isCanvasWorkerHttpPath } from "./web-experimental-http.js";
276
+ import { isCanvasWorkerTool, CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
284
277
  import { bundledSkillContents } from "./bundled-skills.js";
285
- import { createEffectBootstrap } from "./web-effect-bootstrap.js";
286
278
  import { fleetLevelDifferences, fleetLevelSignature, appendedConnections } from "./fleet-level-config.js";
287
279
  import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-restart-limit.js";
288
280
  import { SecretStore } from "./secret-store.js";
@@ -797,8 +789,6 @@ export class FleetManager {
797
789
  costGuard = null;
798
790
  statuslineWatcher;
799
791
  dailySummary = null;
800
- /** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
801
- taskBoardSummary = null;
802
792
  dailyTipScheduler = null;
803
793
  /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
804
794
  taskBoardWaker = null;
@@ -909,6 +899,12 @@ export class FleetManager {
909
899
  kiroMigrationRuns = new Map();
910
900
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
911
901
  needsYou = null;
902
+ configHistory = null;
903
+ configHistoryTimer = null;
904
+ loadedFleetSource = null;
905
+ getConfigHistory() {
906
+ return this.configHistory ??= new ConfigHistory(this.dataDir, () => this.needsYou?.poke(), message => this.logger.warn(message));
907
+ }
912
908
  /** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
913
909
  needsCrashedAt = new Map();
914
910
  /** Pause reason/time read from the marker, per paused instance; dropped on any attention change of it, and at most
@@ -984,11 +980,6 @@ export class FleetManager {
984
980
  collabInstances = new Set();
985
981
  // Health endpoint
986
982
  healthServer = null;
987
- /** #2118: independent of HTML preview availability/port; fixed bootstrap only, no effect resources. */
988
- effectBootstrap = createEffectBootstrap();
989
- /** Fixed trusted document coherence, independent of the HTML preview listener; never a resource grant. */
990
- get effectBoot() { return this.effectBootstrap.boot; }
991
- canvasWorkerEnabled() { return canvasWorkerEnabled(this.fleetConfig?.web); }
992
983
  healthServerDrain = null;
993
984
  /** #1306: the preview listener (health_port + 1 by default), and whether it is listening. */
994
985
  previewListener = null;
@@ -1167,7 +1158,7 @@ export class FleetManager {
1167
1158
  /** #1629: every instance's theme, placements, assets and lock (web-themes.json). */
1168
1159
  get themeStore() {
1169
1160
  // r4.1 Q3 (b): the card's limits are read from fleet.yaml at each upload (a change applies without a restart).
1170
- return (this.themeStoreInstance ??= new ThemeStore(this.dataDir, { limits: () => effectiveAppearanceLimits(this.fleetConfig?.web?.appearance).limits, effectsDropped: ids => this.effectLibrary.collectUnreferenced(ids) }));
1161
+ return (this.themeStoreInstance ??= new ThemeStore(this.dataDir, { limits: () => effectiveAppearanceLimits(this.fleetConfig?.web?.appearance).limits }));
1171
1162
  }
1172
1163
  officeStoreInstance;
1173
1164
  /** Fleet → Org chart → Office: the offices people made (web-offices.json, beside web-themes.json). */
@@ -1179,6 +1170,12 @@ export class FleetManager {
1179
1170
  officeSkinWrites = new OfficeWriteLimiter(undefined, { perWindow: 10, windowMs: 60_000 });
1180
1171
  /** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
1181
1172
  officeWrites = new OfficeWriteLimiter();
1173
+ /** #2855: a guest's preference saves, per account. */
1174
+ guestPrefWrites = new OfficeWriteLimiter(undefined, GUEST_PREF_WRITES);
1175
+ /** #2855: a guest's commands, per account. */
1176
+ guestCommandRuns = new OfficeWriteLimiter(undefined, GUEST_COMMAND_RUNS);
1177
+ /** #2855: a trusted guest's searches, per account. */
1178
+ guestSearches = new OfficeWriteLimiter(undefined, { perWindow: GUEST_SEARCHES.perWindow, windowMs: GUEST_SEARCHES.windowMs });
1182
1179
  officeSharesInstance;
1183
1180
  /**
1184
1181
  * #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
@@ -1374,96 +1371,16 @@ export class FleetManager {
1374
1371
  }
1375
1372
  catch { /* the next frame carries the rev */ } }));
1376
1373
  }
1377
- effectLibraryInstance;
1378
- effectTrialsInstance;
1379
- /** Lazy, single-owner storage. Register every host reference source before any operation can collect. */
1380
- get effectLibrary() {
1381
- if (!this.effectLibraryInstance) {
1382
- const library = new WebEffectLibrary(this.dataDir);
1383
- // Providers are lazy; publish only after every registration succeeds.
1384
- library.registerReferenceSource("themes", () => {
1385
- if (!this.themeStore.readable())
1386
- throw new Error("themes_unreadable");
1387
- return this.themeStore.placedEffects().flatMap(row => row.ids.map(id => ({ id, holder: `instance:${row.name}` })));
1388
- });
1389
- library.registerReferenceSource("prefs", () => this.webPrefs.ownerEffectReferences());
1390
- library.registerReferenceSource("mods", () => {
1391
- const mods = this.mods.list();
1392
- if (mods.unreadable)
1393
- throw new Error("mods_unreadable");
1394
- return mods.mods.flatMap(mod => mod.effects.map(effect => ({ id: effect.id, holder: `mod:${mod.id}` })));
1395
- });
1396
- this.effectTrialsInstance = new WebEffectTrials({ library, known: name => this.isKnownInstance(name) });
1397
- this.effectLibraryInstance = library;
1398
- }
1399
- return this.effectLibraryInstance;
1400
- }
1401
- /** Volatile Try offers and session-bound runs share this library. */
1402
- get effectTrials() { if (!this.effectTrialsInstance)
1403
- void this.effectLibrary; return this.effectTrialsInstance; }
1404
- effectProfilesInstance;
1405
- get effectProfiles() { return (this.effectProfilesInstance ??= new WebEffectProfiles(this.dataDir)); }
1406
- effectServiceInstance;
1407
- get effectService() {
1408
- return (this.effectServiceInstance ??= new WebEffectService({ library: this.effectLibrary, themes: this.themeStore, profiles: this.effectProfiles, prefs: this.webPrefs, trials: this.effectTrials,
1409
- known: name => this.isKnownInstance(name), changed: () => { try {
1410
- this.emitSseEvent("status", this.getUiStatus());
1411
- }
1412
- catch { /* next status carries revision */ } } }));
1413
- }
1414
- effectToolsService;
1415
- /** Caller identity is supplied only by the owned IPC connection / verified agent endpoint token. */
1416
- get effectTools() {
1417
- return (this.effectToolsService ??= new WebEffectTools({
1418
- library: this.effectLibrary, limiter: this.webAppearance.limiter,
1419
- authorize: (name, buckets) => !this.isKnownInstance(name) ? { error: "unknown_instance" } : this.webAppearance.authorizeEffect(name, buckets),
1420
- placed: name => this.themeStore.get(name).placements.effects,
1421
- place: (name, effects) => {
1422
- const before = this.themeStore.get(name).placements.effects;
1423
- const explicit = new Set(effects.filter(e => typeof e !== "string").map(e => e.id));
1424
- const w = this.themeStore.setPlacements(name, { effects }, "agent", what => {
1425
- // An unchanged human/private placement may be retained, never exposed or newly acquired by an agent.
1426
- const r = explicit.has(what.id) ? this.effectLibrary.read(what.id, { kind: "agent", id: name })
1427
- : this.effectLibrary.readReferenced(what.id, `instance:${name}`);
1428
- if (!r.ok)
1429
- return r.error;
1430
- const params = explicit.has(what.id) ? what.params : before.find(e => e.id === what.id)?.params;
1431
- return resolveEffectParams(r.entry.manifest, params ?? {}).ok ? null : "effect_params";
1432
- });
1433
- return w.ok ? { ok: true, rev: w.rev } : { ok: false, error: w.error };
1434
- },
1435
- changed: () => { try {
1436
- this.emitSseEvent("status", this.getUiStatus());
1437
- }
1438
- catch { /* later status carries placements */ } },
1439
- // A nonmutating own/shared offer is not an appearance write. Only an explicit non-public admin-session click may start it.
1440
- preview: async (instance, id) => {
1441
- const result = this.effectTrials.offer(instance, id);
1442
- if (!result.ok)
1443
- return { error: "not_found" };
1444
- const present = this.webChatHistory.list(instance).some(row => row.effectTrial?.offer === result.offer.offer);
1445
- if (!present && !this.publishEffectTrial(instance, result.offer))
1446
- return { error: "trial_unavailable" };
1447
- return { id, status: "offered", expiresAt: result.offer.expiresAt, message: "A trial card is in your web chat. A signed-in admin outside the public link can start it; nothing was applied." };
1448
- },
1449
- profiles: id => this.effectProfiles.read(id),
1450
- profileSummary: id => this.effectService.profileSummary(id),
1451
- profileSummaries: ids => this.effectService.profileSummaries(ids),
1452
- playbackSummary: (name, id) => this.effectService.playbackSummary(name, id),
1453
- }));
1454
- }
1455
1374
  appearanceToolsService;
1456
1375
  /** #1629 r4 §1: get_web_appearance / preview_web_appearance — the caller's own look, resolved as the page resolves it. */
1457
1376
  get appearanceTools() {
1458
1377
  return (this.appearanceToolsService ??= new AppearanceTools({
1459
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1460
1378
  frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
1461
1379
  theme: (name) => {
1462
1380
  const r = this.themeStore.get(name);
1463
1381
  return { theme: r.theme, locked: r.locked, widgets: r.placements.widgets, ambient: r.placements.ambient,
1464
1382
  glyphAsset: (id) => this.themeStore.asset(name, id) };
1465
1383
  },
1466
- effects: name => this.themeStore.readable() ? this.themeStore.get(name).placements.effects.map(p => ({ id: p.id, by: p.by, ...(p.params ? { params: p.params } : {}), playback: this.effectService.playbackSummary(name, p.id) })) : [],
1467
1384
  card: (name) => resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card),
1468
1385
  // #1958: the host's identity-colour settings, as the page reads them (an unreadable prefs file: the defaults).
1469
1386
  colorPrefs: () => { try {
@@ -1492,49 +1409,17 @@ export class FleetManager {
1492
1409
  return Object.hasOwn(this.fleetConfig?.instances ?? {}, name) || (this.classicChannels?.getAll() ?? []).some((ch) => ch.instanceName === name);
1493
1410
  }
1494
1411
  modStore;
1495
- /**
1496
- * #1629 r4-5 / B2: installed mods use the same validated library and scoped holder claims as direct uploads.
1497
- */
1412
+ /** #1629 r4-5: installed mods (#2799: a bundle that carries Canvas Worker effects is refused). */
1498
1413
  get mods() {
1499
1414
  return (this.modStore ??= new ModStore(this.dataDir, {
1500
1415
  themes: this.themeStore,
1501
1416
  prefs: this.webPrefs,
1502
1417
  ownerCss: this.ownerCss,
1503
- effects: this.effectLibrary,
1504
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1505
- context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits(), effects: cw1EffectChecker() }),
1418
+ context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits() }),
1506
1419
  isKnownInstance: (name) => this.isKnownInstance(name),
1507
1420
  prefsIdentity: OPERATOR,
1508
1421
  }));
1509
1422
  }
1510
- agentModsService;
1511
- /** #1629 r4-5b: export_web_mod / propose_web_mod — the caller's own look as a file, and a proposal for a person. */
1512
- get agentMods() {
1513
- return (this.agentModsService ??= new AgentMods({
1514
- themes: this.themeStore, mods: this.mods, limiter: this.webAppearance.limiter,
1515
- webConfig: () => this.fleetConfig?.web ?? null,
1516
- // The mod store's own: a proposal is checked exactly as the person's import of it will be.
1517
- context: () => this.mods.checkContext(),
1518
- readOwnEffect: (instance, id) => { const r = this.effectLibrary.read(id, { kind: "agent", id: instance }); return r.ok ? { name: r.entry.manifest.name, files: r.entry.files } : null; },
1519
- notice: (instance, text) => { try {
1520
- this.emitSseEvent("message", { instance, sender: instance, role: "status", text, ts: new Date().toISOString() });
1521
- }
1522
- catch { /* the page reads it from Settings too */ } },
1523
- changed: () => { try {
1524
- this.emitSseEvent("status", this.getUiStatus());
1525
- }
1526
- catch { /* the next frame */ } },
1527
- }));
1528
- }
1529
- /**
1530
- * #1629 r4-5c: an effect as this person may carry it away in an export — their own claim, or one a person shared —
1531
- * read with their principal (Sol's ACL). Being able to view or run a placed effect never grants packaging someone
1532
- * else's private claim: the library's reference read is runtime access, not an export path. Null: not theirs.
1533
- */
1534
- modPlacedEffect(person, id) {
1535
- const r = this.effectLibrary.read(id, { kind: "human", id: person });
1536
- return r.ok ? { name: r.entry.manifest.name, files: r.entry.files } : null;
1537
- }
1538
1423
  /** #1629 r4-5: an instance's card text as every viewer sees it (a person's fields over the agent's). */
1539
1424
  modCardText(name) {
1540
1425
  const card = resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card);
@@ -1557,8 +1442,7 @@ export class FleetManager {
1557
1442
  */
1558
1443
  get webSettings() {
1559
1444
  return (this.webSettingsToolsService ??= new WebSettingsTools({
1560
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1561
- prefs: this.webPrefs, ownerCss: this.ownerCss, mods: this.mods, effects: this.effectService,
1445
+ prefs: this.webPrefs, ownerCss: this.ownerCss,
1562
1446
  instances: () => [...Object.keys(this.fleetConfig?.instances ?? {}),
1563
1447
  ...(this.classicChannels?.getAll() ?? []).map((ch) => ch.instanceName).filter((n) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, n))],
1564
1448
  colour: (name) => personIdentityColour(this.dataDir, name),
@@ -1567,7 +1451,6 @@ export class FleetManager {
1567
1451
  bannerFocus: (name) => personBannerFocus(this.dataDir, name),
1568
1452
  setBannerFocus: (name, focus) => { const w = setPersonBannerFocus(this.dataDir, name, focus); if (w.ok)
1569
1453
  this.identityChanged(); return w; },
1570
- placements: (name) => this.themeStore.get(name).placements.effects.map((e) => (typeof e === "string" ? { id: e } : e)),
1571
1454
  ambient: (name) => Object.fromEntries(Object.entries(this.themeStore.get(name).placements.ambient).flatMap(([k, a]) => a ? [[k, { spec: a.spec, by: a.by }]] : [])),
1572
1455
  setAmbient: (name, spec, which) => this.webAppearance.humanSetAmbient(name, spec, which),
1573
1456
  announce: () => { try {
@@ -1881,7 +1764,7 @@ export class FleetManager {
1881
1764
  retire();
1882
1765
  continue;
1883
1766
  }
1884
- if (store.attachPrompt(view.id, retire))
1767
+ if (store.attachPrompt(view.id, retire, { platform: adapter.type, connection: adapterId }))
1885
1768
  return;
1886
1769
  }
1887
1770
  catch {
@@ -2408,12 +2291,10 @@ export class FleetManager {
2408
2291
  const notice = silent
2409
2292
  ? t(`delivery.fleet_${row.state}_silent_schedule`, row.targetInstance, row.operationId, row.deliveryId, row.lastError ?? "unknown")
2410
2293
  : t(`delivery.fleet_${row.state}`, row.targetInstance, row.operationId, row.deliveryId);
2294
+ // #2804: log-only. The person sees a failed or unconfirmed delivery as a Needs-you item (with Acknowledge),
2295
+ // not as a separate General post; the sending agent still gets its delivery-outcome notice.
2411
2296
  this.logger.error({ deliveryId: row.deliveryId, operationId: row.operationId, target: row.targetInstance, state: row.state,
2412
2297
  cause: row.lastError, ...(silent ? { silentSchedule: true } : {}) }, notice);
2413
- // A self-scheduled silent heartbeat stays log-only, including the
2414
- // operator alert; another source still receives the normal notice.
2415
- if (!silent || row.sourceInstance !== row.targetInstance)
2416
- this.notifyFleetError(notice);
2417
2298
  }
2418
2299
  }
2419
2300
  });
@@ -2445,8 +2326,9 @@ export class FleetManager {
2445
2326
  if (event.count)
2446
2327
  this.needsYou?.poke(); // #1386: rows that expired failed — they wait on someone now
2447
2328
  this.wakeCoordinator?.kick();
2329
+ // #2804: the expired rows are Needs-you items now; the count is logged, not posted to General.
2448
2330
  if (event.count)
2449
- this.notifyFleetError(t("delivery.expired", event.count, event.uncertain ?? 0));
2331
+ this.logger.warn({ count: event.count, uncertain: event.uncertain ?? 0 }, t("delivery.expired", event.count, event.uncertain ?? 0));
2450
2332
  });
2451
2333
  this.scheduleDeliveryOutboxPump();
2452
2334
  }
@@ -2931,7 +2813,9 @@ export class FleetManager {
2931
2813
  if ((current?.state === "delivering" || current?.state === "submission_started")
2932
2814
  && current.targetDaemonBootId === claimed.targetDaemonBootId
2933
2815
  && current.managerBootId === claimed.managerBootId) {
2934
- this.notifyFleetError(t("delivery.lane_waiting", claimed.targetInstance, Math.round(DURABLE_DELIVERY_LANE_ALERT_MS / 60_000), deliveryId));
2816
+ // #2804: logged, not posted. A delivery still in flight needs nobody yet; if it ends failed or unconfirmed it
2817
+ // becomes a Needs-you item.
2818
+ this.logger.warn({ deliveryId, target: claimed.targetInstance }, t("delivery.lane_waiting", claimed.targetInstance, Math.round(DURABLE_DELIVERY_LANE_ALERT_MS / 60_000), deliveryId));
2935
2819
  }
2936
2820
  }, DURABLE_DELIVERY_LANE_ALERT_MS);
2937
2821
  alertTimer.unref?.();
@@ -3103,7 +2987,8 @@ export class FleetManager {
3103
2987
  // fleet.yaml, sent SIGHUP, and got no reaction and no explanation.
3104
2988
  this.logger.error({ err }, "SIGHUP config reload failed");
3105
2989
  const message = err instanceof Error ? err.message : String(err);
3106
- this.notifyFleetError(t("fleet.reload_failed", message));
2990
+ if (!(err instanceof ConfigSyntaxError))
2991
+ this.notifyFleetError(t("fleet.reload_failed", message));
3107
2992
  });
3108
2993
  }
3109
2994
  /**
@@ -3196,6 +3081,7 @@ export class FleetManager {
3196
3081
  if (this.reloadPending)
3197
3082
  this.scheduleReconcile();
3198
3083
  void this.sweepOrphanedCancelButtons();
3084
+ void this.parkedNotices.sweep();
3199
3085
  }
3200
3086
  /**
3201
3087
  * We are the post-update fleet: the update is over by definition. Clearing it here (rather than in the update
@@ -3339,19 +3225,19 @@ export class FleetManager {
3339
3225
  };
3340
3226
  }
3341
3227
  /** Load fleet.yaml and build routing table */
3342
- loadConfig(configPath) {
3228
+ loadConfig(configPath, options = {}) {
3343
3229
  this.configPath = configPath;
3344
- const source = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "{}\n";
3345
- this.rawFleetDocument = parseDocument(source, { keepSourceTokens: true });
3346
- if (this.rawFleetDocument.errors.length > 0) {
3347
- throw new Error(`Invalid fleet.yaml: ${this.rawFleetDocument.errors[0].message}`);
3348
- }
3349
- const raw = loadRawFleetConfig(configPath);
3350
- const loaded = loadFleetConfig(configPath);
3230
+ const history = this.getConfigHistory();
3231
+ history.track("fleet.yaml", configPath);
3232
+ const selected = existsSync(configPath) ? history.read("fleet.yaml", configPath, options.startup) : { source: "{}\n", fallback: false };
3233
+ this.rawFleetDocument = parseDocument(selected.source, { keepSourceTokens: true });
3234
+ const raw = loadRawFleetConfigSource(selected.source);
3235
+ const loaded = loadFleetConfigSource(selected.source);
3351
3236
  this.assertProviderSecretEnvKeys(loaded);
3352
3237
  this.rawFleetConfig = raw;
3353
3238
  this.settingsRuntimeRevision++;
3354
3239
  this.fleetConfig = loaded;
3240
+ this.loadedFleetSource = selected;
3355
3241
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
3356
3242
  this.warnAboutRemovedLoginMode(loaded);
3357
3243
  this.enforceGuestSwitch();
@@ -3360,6 +3246,8 @@ export class FleetManager {
3360
3246
  // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
3361
3247
  process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
3362
3248
  process.env.AGEND_FLEET_CONFIG_PATH = resolvePath(configPath);
3249
+ if (options.remember !== false && existsSync(configPath))
3250
+ history.accepted("fleet.yaml", configPath, selected);
3363
3251
  return this.fleetConfig;
3364
3252
  }
3365
3253
  relayLoginModeWarned = false;
@@ -6644,10 +6532,12 @@ export class FleetManager {
6644
6532
  dataDir: this.dataDir,
6645
6533
  onWarn: message => this.logger.warn(message),
6646
6534
  // #1620: fleet.yaml's web.session.local, read at every check — a Settings save or a reload applies at once
6647
- // (shortening ends the sessions it now outlives). The public link's policy stays fixed.
6535
+ // (shortening ends the sessions it now outlives). The public link's own cap stays fixed.
6648
6536
  localPolicy: () => localSessionPolicy(this.fleetConfig?.web?.session?.local),
6649
6537
  // #1920 §5.4: a guest's, from web.guests.session (never longer than the surface's own).
6650
6538
  guestPolicy: () => guestSessionPolicy(this.fleetConfig?.web?.guests?.session),
6539
+ // A session over the temporary link ends with the link (its TTL, at most 8 h on a quick tunnel).
6540
+ exposureEnd: id => this.publicWebLink?.endOf(id) ?? null,
6651
6541
  });
6652
6542
  this.enforceGuestSwitch(); // #1920: guests off while the fleet was down
6653
6543
  }
@@ -6696,6 +6586,32 @@ export class FleetManager {
6696
6586
  return this.previewTunnel ??= new ManagedTunnel({ dataDir: join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR),
6697
6587
  log: () => this.logger.warn("Public preview tunnel cleanup requires attention") });
6698
6588
  }
6589
+ namedLinkTestRunning = false;
6590
+ /**
6591
+ * #2906 (the user, 2026-11-01): Settings → Access → Test connection, on the fixed-address values as filled in (the
6592
+ * typed connector token, else the saved one). One at a time; the result is closed step codes only — the token is
6593
+ * never logged or returned. Admin only, never over the public link (settings-api.ts).
6594
+ */
6595
+ async testNamedLink(input) {
6596
+ if (this.namedLinkTestRunning)
6597
+ return { busy: true };
6598
+ this.namedLinkTestRunning = true;
6599
+ try {
6600
+ const typed = typeof input.token === "string" && input.token.trim() ? input.token : null;
6601
+ const result = await runNamedLinkTest({ hostname: input.hostname, port: input.port, protocol: input.protocol, token: typed ?? process.env[PUBLIC_TUNNEL_TOKEN_ENV] ?? null }, {
6602
+ healthPort: this.fleetConfig?.health_port ?? 19280,
6603
+ ensureBinary: async (signal) => (await ensureCloudflared({ dataDir: this.dataDir, pinnedOnly: true, signal })).path,
6604
+ liveMarker: values => this.publicWebLink?.liveNamedMarker(values) ?? null,
6605
+ reserve: namedTestLane(this.getTunnelLane()),
6606
+ current: input.current,
6607
+ });
6608
+ this.logger[result.cleanup ? "warn" : "info"]({ steps: result.steps.map(s => `${s.step}:${s.code}`), typedToken: !!typed, ...(result.cleanup ? { cleanup: result.cleanup } : {}) }, result.cleanup ? "Named link test: the test connector's stop could not be proven; the tunnel lane stays held" : "Named link test");
6609
+ return result;
6610
+ }
6611
+ finally {
6612
+ this.namedLinkTestRunning = false;
6613
+ }
6614
+ }
6699
6615
  getTunnelLane() {
6700
6616
  return this.tunnelPurposeLane ??= new TunnelPurposeLane(new ManagedTunnel({ dataDir: this.dataDir,
6701
6617
  log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
@@ -7071,7 +6987,8 @@ export class FleetManager {
7071
6987
  : "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
7072
6988
  + (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : ""));
7073
6989
  const choices = [{ action: "local", label: t("dashboard.local_signin") },
7074
- ...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
6990
+ ...(publicLinkSettings(this.fleetConfig?.web).allowed
6991
+ ? [{ action: "public", label: t(publicLinkNamedHostname(this.fleetConfig?.web) ? "dashboard.public_open_named" : "dashboard.public_open") }] : []),
7075
6992
  ...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
7076
6993
  const posted = await this.postNonceButtonPrompt({
7077
6994
  prefix: "dashboard:", alertType: "login", instanceName: "dashboard", adapter, adapterId: owner.adapterId,
@@ -7291,7 +7208,7 @@ export class FleetManager {
7291
7208
  return { ...a, name: badge.name, platform: badge.platform, scope: [...this.guestScope(owner)].sort(), reach: [...this.guestReach(owner)].sort(),
7292
7209
  devices: devices.get(k) ?? 0, narrow: own(cfg?.narrow, `${a.adapterId}:${a.userId}`) ?? null,
7293
7210
  // #2348: the grant as fleet.yaml has it (null: none), and the last code an admin sent from Settings.
7294
- grant: grant ? { ...(grant.name ? { name: grant.name } : {}), instances: [...grant.instances] } : null,
7211
+ grant: grant ? { ...(grant.name ? { name: grant.name } : {}), instances: [...grant.instances], ...(grant.level === "trusted" ? { level: "trusted" } : {}) } : null,
7295
7212
  ...(sent ? { code: { at: sent.at, outcome: sent.outcome.kind, ...(sent.outcome.kind === "sent" ? { expiresAt: sent.at + sent.outcome.expiresInMs } : {}),
7296
7213
  ...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
7297
7214
  }).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
@@ -7339,7 +7256,7 @@ export class FleetManager {
7339
7256
  const when = Date.parse(r.created_at.includes("T") ? r.created_at : `${r.created_at.replace(" ", "T")}Z`);
7340
7257
  return r.event_type === "guest_grant_changed"
7341
7258
  ? [{ kind: "grant", at: when, account, name, via: p.via === "confirmed" ? "confirmed" : "direct", added: names(p.added), removed: names(p.removed),
7342
- endedSessions: typeof p.endedSessions === "number" ? p.endedSessions : 0 }]
7259
+ endedSessions: typeof p.endedSessions === "number" ? p.endedSessions : 0, ...(p.level === "chat" || p.level === "trusted" ? { level: p.level } : {}) }]
7343
7260
  : [{ kind: "code", at: when, account, name, outcome: typeof p.outcome === "string" ? p.outcome : "unknown" }];
7344
7261
  });
7345
7262
  }
@@ -7378,7 +7295,7 @@ export class FleetManager {
7378
7295
  * confirmed in General, the agents added and taken away, and the sessions a removal ended. Never message content.
7379
7296
  */
7380
7297
  noteGuestGrantChange(change) {
7381
- this.logger.info({ account: change.account, via: change.via, added: change.added.length, removed: change.removed.length }, "Guest access changed in Settings");
7298
+ this.logger.info({ account: change.account, via: change.via, added: change.added.length, removed: change.removed.length, ...(change.level ? { level: change.level } : {}) }, "Guest access changed in Settings");
7382
7299
  try {
7383
7300
  this.eventLog?.insert("fleet", "guest_grant_changed", { ...change });
7384
7301
  }
@@ -7463,6 +7380,8 @@ export class FleetManager {
7463
7380
  * #1920 §5.4: a guest's own `/web revoke` — its sessions and its outstanding code; nobody else's (an admin's revokes
7464
7381
  * everyone's, as before).
7465
7382
  */
7383
+ /** #2855: the admin removed a guest — its own preferences (theme, language, favourites, sort, filter) go too. */
7384
+ forgetGuestPrefs(owner) { this.webPrefs.forgetGuest(webPersonKey(owner)); }
7466
7385
  revokeGuestWebSessions(owner) {
7467
7386
  this.initializeWebSessions();
7468
7387
  const key = `${owner.adapterId}\u0000${owner.userId}`;
@@ -8169,7 +8088,7 @@ export class FleetManager {
8169
8088
  this.tmuxExecHelperTimer ??= setInterval(() => tmuxExecHelper.maintain(), 5_000);
8170
8089
  this.tmuxExecHelperTimer.unref?.();
8171
8090
  this.rotateFleetLogs();
8172
- const fleet = this.loadConfig(configPath);
8091
+ const fleet = this.loadConfig(configPath, { startup: true });
8173
8092
  this.slimFleetConfigAtStartup();
8174
8093
  this.logConfigWarningsAtStartup();
8175
8094
  setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
@@ -8247,7 +8166,7 @@ export class FleetManager {
8247
8166
  // migrates legacy single-bot entries and names without a suffix. Classic
8248
8167
  // routing does NOT go through the routing engine (single-key, can't hold two
8249
8168
  // bots in one channel) — it resolves per-bot via getInstanceByChannel.
8250
- this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger);
8169
+ this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger, this.getConfigHistory());
8251
8170
  const classicAdapters = fleet.channels?.length ? fleet.channels : (fleet.channel ? [fleet.channel] : []);
8252
8171
  this.classicChannels.configureAdapters(classicAdapters);
8253
8172
  // The unrecoverable-id report is deliberately NOT sent here: no adapter
@@ -8265,6 +8184,9 @@ export class FleetManager {
8265
8184
  // rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
8266
8185
  this.restoreWebChat(fleet);
8267
8186
  this.startSearchIndex(fleet);
8187
+ // Parse-only source monitoring; it does not apply hand-edited Fleet settings.
8188
+ this.configHistoryTimer = setInterval(() => this.configHistory?.check(), 1_000);
8189
+ this.configHistoryTimer.unref?.();
8268
8190
  // Poll classicBot.yaml for external changes every 30s
8269
8191
  this.classicReloadTimer = setInterval(() => {
8270
8192
  // Attributes the synchronous part, up to the reload's first await (#1235).
@@ -8296,7 +8218,8 @@ export class FleetManager {
8296
8218
  ...fleet.defaults.daily_summary,
8297
8219
  };
8298
8220
  this.dailySummary = new DailySummary(summaryConfig, costGuardConfig.timezone, (text) => {
8299
- this.postDailySummary(text);
8221
+ if (text !== null)
8222
+ this.postDailySummary(text);
8300
8223
  // Rotate classic channel chat logs daily
8301
8224
  this.classicChannels?.rotateLogs();
8302
8225
  this.rotateInboxes();
@@ -8314,17 +8237,6 @@ export class FleetManager {
8314
8237
  return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
8315
8238
  });
8316
8239
  this.dailySummary.start();
8317
- // #2246 step 3: the task board's daily summary — its own time (defaults.task_board.summary_at, read at every look),
8318
- // in the fleet's zone; a summary missed while the fleet was down goes out when it is back.
8319
- this.taskBoardSummary = new TaskBoardSummaryClock({
8320
- at: () => summaryTime(this.fleetConfig?.defaults?.task_board?.summary_at) ?? null,
8321
- zone: () => this.usageZone(),
8322
- now: () => new Date(),
8323
- mark: summaryMarkFile(join(this.dataDir, "task-board-summary.json")),
8324
- send: (since, until) => this.postTaskBoardSummary(since, until),
8325
- failed: (err) => this.logger.warn({ err }, "The task board's daily summary failed; the next minute tries again"),
8326
- });
8327
- this.taskBoardSummary.start();
8328
8240
  // Rotate classic channel chat logs daily (piggyback on daily summary timer)
8329
8241
  this.classicChannels?.rotateLogs();
8330
8242
  this.rotateInboxes();
@@ -8413,7 +8325,7 @@ export class FleetManager {
8413
8325
  fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
8414
8326
  generalsCreated = true;
8415
8327
  }
8416
- if (generalsCreated)
8328
+ if (generalsCreated && !this.configHistory?.isFallback(this.configPath))
8417
8329
  this.saveFleetConfig();
8418
8330
  if (topicMode && (fleet.channel || fleet.channels?.length)) {
8419
8331
  const schedulerConfig = {
@@ -8589,7 +8501,7 @@ export class FleetManager {
8589
8501
  fixedGeneral = true;
8590
8502
  }
8591
8503
  }
8592
- if (fixedGeneral)
8504
+ if (fixedGeneral && !this.configHistory?.isFallback(this.configPath))
8593
8505
  this.saveFleetConfig();
8594
8506
  // Auto-create topics AFTER adapter is ready (needs adapter.createTopic)
8595
8507
  await this.topicCommands.autoCreateTopics();
@@ -10363,6 +10275,11 @@ export class FleetManager {
10363
10275
  }
10364
10276
  return;
10365
10277
  }
10278
+ // #2870 (the user's choice): in a private chat, a command from someone ClassicBot does not serve here — not an
10279
+ // allowed user, not a ClassicBot admin, not a fleet admin, no room in this chat — gets no answer at all. /start
10280
+ // above still asks for access; a guest's /web is handled before it.
10281
+ if (isPrivateChat && text.startsWith("/") && this.isClassicPrivateStranger(msg.userId, chatId, msg.adapterId))
10282
+ return;
10366
10283
  // Handle /stop command
10367
10284
  if (text === "/stop" || text.startsWith("/stop ")) {
10368
10285
  if (!this.classicChannels.isAdmin(msg.userId)) {
@@ -10837,8 +10754,8 @@ export class FleetManager {
10837
10754
  }
10838
10755
  }
10839
10756
  checkToolPermission(sink, instanceName, tool) {
10840
- if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
10841
- return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
10757
+ if (isCanvasWorkerTool(tool))
10758
+ return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }; // #2799: removed
10842
10759
  const profile = resolveToolSet(this.fleetConfig?.instances[instanceName], instanceName);
10843
10760
  const allowed = mayUseTool(profile, tool);
10844
10761
  if (!allowed) {
@@ -11571,19 +11488,6 @@ export class FleetManager {
11571
11488
  }
11572
11489
  dispatchTypedIpc(name, msg) {
11573
11490
  const type = String(msg.type);
11574
- if (type.startsWith("fleet_") && WEB_EFFECT_TOOLS.has(type.slice(6))) {
11575
- const ipc = this.instanceIpcClients.get(name), fleetRequestId = msg.fleetRequestId;
11576
- if (!ipc || typeof fleetRequestId !== "string")
11577
- return;
11578
- void this.handleWebThemeHttp(name, type.slice(6), (msg.payload ?? {})).then(result => {
11579
- if (this.instanceIpcClients.get(name) === ipc)
11580
- ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
11581
- }, err => {
11582
- if (this.instanceIpcClients.get(name) === ipc)
11583
- ipc.send({ type: "fleet_web_theme_response", fleetRequestId, error: err instanceof Error ? err.message : "Effect operation failed" });
11584
- }).catch(err => this.logger.debug({ err }, "Effect tool response could not be written"));
11585
- return;
11586
- }
11587
11491
  if (type.startsWith("fleet_schedule_")) {
11588
11492
  this.handleScheduleCrud(name, msg);
11589
11493
  return;
@@ -11637,10 +11541,9 @@ export class FleetManager {
11637
11541
  const args = (msg.payload ?? {});
11638
11542
  const result = tool === "get_web_appearance" || tool === "preview_web_appearance"
11639
11543
  ? this.appearanceTools.handle(instanceName, tool, args)
11640
- : tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
11641
- : tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
11642
- : OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
11643
- : this.webAppearance.handleTool(instanceName, tool, args);
11544
+ : tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
11545
+ : OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
11546
+ : this.webAppearance.handleTool(instanceName, tool, args);
11644
11547
  ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
11645
11548
  }
11646
11549
  catch (err) {
@@ -12065,17 +11968,10 @@ export class FleetManager {
12065
11968
  }
12066
11969
  /** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
12067
11970
  async handleWebThemeHttp(instance, tool, args) {
12068
- if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
12069
- return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
12070
- if (WEB_EFFECT_TOOLS.has(tool)) {
12071
- if (!this.isKnownInstance(instance))
12072
- return { error: "unknown_instance" };
12073
- return this.effectTools.handle(instance, tool, args);
12074
- }
11971
+ if (isCanvasWorkerTool(tool))
11972
+ return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }; // #2799: removed
12075
11973
  if (tool === "get_web_appearance" || tool === "preview_web_appearance")
12076
11974
  return this.appearanceTools.handle(instance, tool, args);
12077
- if (tool === "export_web_mod" || tool === "propose_web_mod")
12078
- return this.agentMods.handle(instance, tool, args);
12079
11975
  if (tool === "office_view" || tool === "office_set")
12080
11976
  return this.officeActs.handle(instance, tool, args); // #2578
12081
11977
  if (OFFICE_PACK_IPC.has(`fleet_${tool}`))
@@ -12616,6 +12512,7 @@ export class FleetManager {
12616
12512
  },
12617
12513
  });
12618
12514
  this.rawFleetConfig = loadRawFleetConfig(this.configPath);
12515
+ this.configHistory?.accepted("fleet.yaml", this.configPath, { source: output, fallback: false });
12619
12516
  // The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
12620
12517
  // leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
12621
12518
  // Keep the FleetConfig object itself: Settings and other request contexts hold it.
@@ -12694,6 +12591,8 @@ export class FleetManager {
12694
12591
  }
12695
12592
  /** One-time upgrade migration; invalid YAML is never rewritten. */
12696
12593
  migrateNoticeDeliveryStorage() {
12594
+ if (this.configHistory?.isFallback(this.configPath))
12595
+ return;
12697
12596
  if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
12698
12597
  || this.fleetConfig.web?.notice_delivery !== undefined)
12699
12598
  && validateFleetConfig(this.rawFleetConfig).valid) {
@@ -12708,6 +12607,8 @@ export class FleetManager {
12708
12607
  }
12709
12608
  }
12710
12609
  migrateOfficeStorage() {
12610
+ if (this.configHistory?.isFallback(this.configPath))
12611
+ return;
12711
12612
  if (!this.fleetConfig || !this.configPath || !hasLegacyOffice(this.rawFleetConfig) || !validateFleetConfig(this.rawFleetConfig).valid)
12712
12613
  return;
12713
12614
  try {
@@ -12731,6 +12632,8 @@ export class FleetManager {
12731
12632
  }
12732
12633
  }
12733
12634
  slimFleetConfigAtStartup() {
12635
+ if (this.configHistory?.isFallback(this.configPath))
12636
+ return;
12734
12637
  this.migrateOfficeStorage();
12735
12638
  this.migrateNoticeDeliveryStorage();
12736
12639
  const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
@@ -12952,9 +12855,7 @@ export class FleetManager {
12952
12855
  this.themeStore.forget(name);
12953
12856
  this.webAppearance.forget(name);
12954
12857
  this.appearanceTools.forget(name);
12955
- this.effectToolsService?.forget(name);
12956
12858
  this.mods.forgetInstance(name);
12957
- this.agentMods.forget(name);
12958
12859
  this.officeActsInstance?.forget(name);
12959
12860
  this.officePackToolsInstance?.forget(name);
12960
12861
  }
@@ -14312,51 +14213,6 @@ export class FleetManager {
14312
14213
  }
14313
14214
  return { chatId, opts: {} };
14314
14215
  }
14315
- /**
14316
- * #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
14317
- * gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
14318
- * one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
14319
- * nothing. A web-only fleet: everything, in the web chat.
14320
- */
14321
- postTaskBoardSummary(since, until) {
14322
- const board = this.scheduler?.db.board;
14323
- if (!board)
14324
- return;
14325
- const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
14326
- const projects = board.listProjects(true);
14327
- const stats = board.projectStats(this.dispatcherGone);
14328
- const names = new Map(projects.map((p) => [p.id, p.name]));
14329
- const configured = this.getPrimaryAdapterId();
14330
- const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
14331
- const worldOf = (assignee) => {
14332
- const owner = assignee ? this.getInstanceAdapterId(assignee) : undefined;
14333
- return owner !== undefined && this.worlds.has(owner) ? owner : primary;
14334
- };
14335
- const zone = this.usageZone();
14336
- for (const world of this.worlds.size ? [...this.worlds.keys()] : [undefined]) {
14337
- const summary = buildSummary(read, since, until, { inChat: (assignee) => worldOf(assignee) === world, projects, stats });
14338
- if (isQuiet(summary))
14339
- continue;
14340
- const text = renderSummary(summary, { zone, t: (key, ...args) => t(key, ...args), projectOf: (c) => (c.projectId === null ? null : names.get(c.projectId) ?? null) });
14341
- const cards = [...new Set([...summary.review, ...summary.blocked, ...summary.done, ...summary.created].map((c) => c.code))];
14342
- if (world !== undefined) {
14343
- const adapter = this.worlds.get(world)?.adapter;
14344
- const target = this.fleetNoticeTarget(world);
14345
- if (adapter && target) {
14346
- adapter.sendText(target.chatId, text, target.opts).catch(e => this.logger.warn({ err: e, world }, "Failed to send the task board's daily summary"));
14347
- }
14348
- else {
14349
- this.logger.warn({ world }, "The task board's daily summary has no postable target — set a General topic or channel.options.general_channel_id");
14350
- }
14351
- }
14352
- for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
14353
- if (cfg.general_topic !== true || (world !== undefined && this.getInstanceAdapterId(name) !== world))
14354
- continue;
14355
- // #2341 review: with the cards it names, so the public link withholds it while one of them is hidden there.
14356
- this.emitSseEvent("message", { instance: name, sender: name, role: "status", text, ts: new Date().toISOString(), cards });
14357
- }
14358
- }
14359
- }
14360
14216
  /**
14361
14217
  * Post the daily summary where fleet-wide notices go.
14362
14218
  *
@@ -14452,11 +14308,22 @@ export class FleetManager {
14452
14308
  * would have to make this async and change every caller.
14453
14309
  */
14454
14310
  notifyInstanceTopic(instanceName, text, extraOpts) {
14311
+ const send = this.sendToInstanceTopic(instanceName, text, extraOpts);
14312
+ if (!send)
14313
+ return false;
14314
+ send.sent.catch(e => this.logger.warn({ err: e, instanceName }, send.failure));
14315
+ return true;
14316
+ }
14317
+ /**
14318
+ * notifyInstanceTopic's routing, handing back the send itself — and which world sent it — for a caller that must
14319
+ * edit or delete the message later (#2897). Null when there is no adapter or no postable target (already logged).
14320
+ */
14321
+ sendToInstanceTopic(instanceName, text, extraOpts) {
14455
14322
  const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
14456
14323
  if (!adapter) {
14457
14324
  // Early startup: adapters are created well after the fleet object exists.
14458
14325
  this.logger.warn({ instanceName }, "No adapter yet — instance topic notification not sent");
14459
- return false;
14326
+ return null;
14460
14327
  }
14461
14328
  const channelCfg = this.getChannelConfig(this.getInstanceAdapterId(instanceName));
14462
14329
  const groupId = channelCfg?.group_id;
@@ -14465,17 +14332,13 @@ export class FleetManager {
14465
14332
  const worldId = this.getInstanceAdapterId(instanceName);
14466
14333
  if (threadId != null && groupId) {
14467
14334
  const out = this.renderPlaces(worldId, text, { threadId: String(threadId), ...extraOpts });
14468
- adapter.sendText(String(groupId), out.text, out.opts)
14469
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
14470
- return true;
14335
+ return { adapterId: worldId, sent: adapter.sendText(String(groupId), out.text, out.opts), failure: "Failed to send instance topic notification" };
14471
14336
  }
14472
14337
  // Classic instance: find its channelId from the classic manager
14473
14338
  const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
14474
14339
  if (classicChatId) {
14475
14340
  // #1990: a ClassicBot room's readers need not be in the fleet's group, so its notices name places as plain text.
14476
- adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts)
14477
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
14478
- return true;
14341
+ return { adapterId: worldId, sent: adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts), failure: "Failed to send classic notification" };
14479
14342
  }
14480
14343
  // Fallback: the instance has neither a topic nor a classic channel, so post
14481
14344
  // where fleet-wide notices go. Not the bare group id: on Discord that is a
@@ -14483,12 +14346,67 @@ export class FleetManager {
14483
14346
  const target = this.fleetNoticeTarget(worldId);
14484
14347
  if (target) {
14485
14348
  const out = this.renderPlaces(worldId, text, { ...target.opts, ...extraOpts });
14486
- adapter.sendText(target.chatId, out.text, out.opts)
14487
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
14488
- return true;
14349
+ return { adapterId: worldId, sent: adapter.sendText(target.chatId, out.text, out.opts), failure: "Failed to send notification (no topic)" };
14489
14350
  }
14490
14351
  this.logger.warn({ instanceName }, "No postable target — instance topic notification not sent");
14491
- return false;
14352
+ return null;
14353
+ }
14354
+ // ── Parked-dialog notices (#2897) ──
14355
+ parkedNoticesStore = null;
14356
+ get parkedNotices() {
14357
+ this.parkedNoticesStore ??= new ParkedNotices({
14358
+ file: join(this.dataDir, PARKED_NOTICE_LEDGER_FILE),
14359
+ logger: this.logger,
14360
+ post: (instanceName, place, text) => this.postParkedNotice(instanceName, place, text),
14361
+ adapterFor: adapterId => this.worlds.get(adapterId)?.adapter
14362
+ ?? (adapterId === this.getPrimaryAdapterId() ? this.adapter ?? undefined : undefined),
14363
+ resolvedText: () => t("fleet.dialog_parked_resolved"),
14364
+ // A ClassicBot room names places as plain text (#1990), as its post did.
14365
+ render: (row, text) => row.place === "topic" && this.classicChannels?.getChannelIdByInstance(row.instanceName) === row.chatId
14366
+ ? stripPlaceRefs(text) : this.renderPlaces(row.adapterId, text).text,
14367
+ });
14368
+ return this.parkedNoticesStore;
14369
+ }
14370
+ /**
14371
+ * A held CLI dialog's notices: the instance-topic one and the General one, each edited in place while the
14372
+ * dialog stays (one live message per place, never a pile), and taken back by resolveParkedNotice.
14373
+ */
14374
+ showParkedNotice(instanceName, topicText, generalText) {
14375
+ if (topicText !== null)
14376
+ void this.parkedNotices.show(instanceName, "topic", topicText);
14377
+ if (generalText !== null)
14378
+ void this.parkedNotices.show(instanceName, "general", generalText);
14379
+ }
14380
+ /** The held dialog is resolved, or the instance stopped or restarted: delete its notices (no-op when none). */
14381
+ resolveParkedNotice(instanceName) {
14382
+ void this.parkedNotices.resolve(instanceName);
14383
+ }
14384
+ async postParkedNotice(instanceName, place, text) {
14385
+ let send;
14386
+ if (place === "topic") {
14387
+ send = this.sendToInstanceTopic(instanceName, text);
14388
+ }
14389
+ else {
14390
+ // Where notifyFleetError posts, without its text throttle: the ledger is what keeps this to one message.
14391
+ const general = Object.entries(this.fleetConfig?.instances ?? {})
14392
+ .find(([, config]) => config.general_topic === true)?.[0];
14393
+ const target = general ? null : this.fleetNoticeTarget();
14394
+ if (general)
14395
+ send = this.sendToInstanceTopic(general, text);
14396
+ else if (this.adapter && target) {
14397
+ const out = this.renderPlaces(this.getPrimaryAdapterId(), text, target.opts);
14398
+ send = { adapterId: this.getPrimaryAdapterId(), sent: this.adapter.sendText(target.chatId, out.text, out.opts) };
14399
+ }
14400
+ else
14401
+ send = null;
14402
+ }
14403
+ if (!send)
14404
+ return null;
14405
+ const sent = await send.sent;
14406
+ const adapterId = send.adapterId ?? this.getPrimaryAdapterId();
14407
+ if (!adapterId)
14408
+ return null;
14409
+ return { adapterId, chatId: sent.chatId, ...(sent.threadId !== undefined ? { threadId: sent.threadId } : {}), messageId: sent.messageId };
14492
14410
  }
14493
14411
  // ── Nonce-armed button prompts (hang / assist / exit / clear) ──
14494
14412
  //
@@ -15066,6 +14984,7 @@ export class FleetManager {
15066
14984
  pauseReason, pausedAt,
15067
14985
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
15068
14986
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
14987
+ usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
15069
14988
  });
15070
14989
  }
15071
14990
  return out;
@@ -15139,6 +15058,9 @@ export class FleetManager {
15139
15058
  },
15140
15059
  tasks: () => this.needsYouTaskItems(),
15141
15060
  backendSignIns: () => this.needsYouBackendSignIns(),
15061
+ configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
15062
+ ...issue, detail: configIssueText(issue, t), owner: this.worlds.keys().next().value,
15063
+ })),
15142
15064
  taskAction: action => this.taskActionFromChat(action),
15143
15065
  settings: () => ({
15144
15066
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
@@ -15888,6 +15810,16 @@ export class FleetManager {
15888
15810
  return result;
15889
15811
  }
15890
15812
  /** #1269: an instance's chat command from the web chat (web-commands.ts); `publicLink`: a gateway request. */
15813
+ /** #2855: the table scope of an instance the web chat may command (a fleet instance or a ClassicBot room), else null. */
15814
+ webCommandScope(instance) {
15815
+ // Own entries only: a name like "__proto__" or "constructor" must not be found on the prototype (#1476 review).
15816
+ const instances = this.fleetConfig?.instances;
15817
+ if (instances && Object.prototype.hasOwnProperty.call(instances, instance))
15818
+ return isGeneralInstance(this.fleetConfig, instance) ? "general" : "fleet";
15819
+ return this.classicChannels?.getChannelIdByInstance(instance) !== undefined ? "classic" : null;
15820
+ }
15821
+ /** #2855: the platform an instance's channel is on, for the command table's cell (a web-only fleet: Discord's). */
15822
+ commandPlatform(instance) { return this.getAdapterForInstance(instance)?.type === "telegram" ? "telegram" : "discord"; }
15891
15823
  webCommand(input, opts) {
15892
15824
  const webMeta = (instance) => {
15893
15825
  const topicId = this.fleetConfig?.instances[instance]?.topic_id;
@@ -15896,6 +15828,7 @@ export class FleetManager {
15896
15828
  chatId: this.getGroupIdForInstance(instance) || "", messageId: newWebMessageId(),
15897
15829
  username: opts.person?.name ?? WEB_NAME_FALLBACK, userId: opts.person?.userId ?? WEB_NAME_FALLBACK,
15898
15830
  threadId: topicId != null ? String(topicId) : undefined, adapterId: this.getAdapterForInstance(instance)?.id ?? "", source: "web", humanOrigin: webHumanOrigin(true, opts.authority),
15831
+ ...(opts.guest ? { webGuest: true } : {}), // #2855: a guest's /steer or /btw reaches the agent as a guest's
15899
15832
  };
15900
15833
  };
15901
15834
  const choicesFromCache = (instance) => {
@@ -15907,13 +15840,8 @@ export class FleetManager {
15907
15840
  return { current, options: this.withCurrentModel(cached.models, current).map(o => ({ id: o.id, label: this.modelChoiceLabel(o, current) })) };
15908
15841
  };
15909
15842
  return runWebCommand({
15910
- // Own entries only: a name like "__proto__" or "constructor" must not be found on the prototype (#1476 review).
15911
- scope: (instance) => {
15912
- const instances = this.fleetConfig?.instances;
15913
- if (instances && Object.prototype.hasOwnProperty.call(instances, instance))
15914
- return isGeneralInstance(this.fleetConfig, instance) ? "general" : "fleet";
15915
- return this.classicChannels?.getChannelIdByInstance(instance) !== undefined ? "classic" : null;
15916
- },
15843
+ scope: (instance) => this.webCommandScope(instance),
15844
+ platform: (instance) => this.commandPlatform(instance),
15917
15845
  ctx: (instance) => this.topicCommands.getCtxText(instance),
15918
15846
  // Board 003648ac: a watch for the paste, armed when the daemon says it was written (raw_paste_written); one
15919
15847
  // for a command that was not handed over is dropped.
@@ -17590,6 +17518,13 @@ export class FleetManager {
17590
17518
  lines.push(t(admin === "added" ? "classic.approve_done_admin" : "classic.approve_done_admin_already", adminUserId ?? ""));
17591
17519
  }
17592
17520
  this.notifyInstanceTopic(entry.instanceName, lines.join("\n"));
17521
+ // #2870: the chat just allowed — and the new ClassicBot admin's own chat — get the ClassicBot menu now, not at the
17522
+ // bot's next start (Telegram only; the menu is per chat there).
17523
+ const reply = entry.classicReplyTo;
17524
+ if (reply && reply.adapter.type === "telegram" && scope !== "guild") {
17525
+ for (const chat of new Set([groupId, ...(adminUserId ? [adminUserId] : [])]))
17526
+ void this.topicCommands.addClassicMenuChat(reply.adapterId, chat);
17527
+ }
17593
17528
  await this.notifyClassicRequester(entry, "classic.request_allowed");
17594
17529
  }
17595
17530
  async notifyClassicRequester(entry, key) {
@@ -18189,14 +18124,6 @@ export class FleetManager {
18189
18124
  const room = this.classicChannels?.getAll().find(c => c.instanceName === name);
18190
18125
  return room ? `room:${room.channelId}:${room.adapterId ?? ""}` : null;
18191
18126
  }
18192
- /** The only Try-card publisher: no arbitrary SSE sender/role can manufacture this metadata. */
18193
- publishEffectTrial(instance, offer) {
18194
- const row = this.webChatHistory.recordEffectTrial(instance, offer, new Date().toISOString());
18195
- if (!row)
18196
- return false;
18197
- broadcastSseEvent(this.sseClients, "message", row, err => this.logger.debug({ err }, "SSE client write failed; evicting"), this.webChatHistory.cursorOf(row));
18198
- return true;
18199
- }
18200
18127
  /** Push an SSE event to all connected Web UI clients. */
18201
18128
  emitSseEvent(event, data) {
18202
18129
  // #1386: a prompt opened or closed, or an instance's state moved — "Needs you" may have changed.
@@ -18504,27 +18431,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
18504
18431
  this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
18505
18432
  this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
18506
18433
  }
18507
- /** Update only AgEnD-managed skill copies; no launch or restart is needed for an experimental switch. */
18508
- canvasWorkerSettingChanged() {
18509
- this.refreshExperimentalSkills();
18510
- this.emitSseEvent("status", this.getUiStatus());
18511
- }
18512
- refreshExperimentalSkills() {
18513
- for (const [name, daemon] of this.daemons) {
18514
- try {
18515
- const cfg = daemon.getConfigSnapshot();
18516
- const backend = cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
18517
- const workDir = this.resolveKnowledgeWorkDir(cfg.working_directory, backend, name);
18518
- const classic = this.classicChannels?.getAll().some(ch => ch.instanceName === name) === true;
18519
- if (classic && !insideManagedWorkspaces(workDir))
18520
- continue;
18521
- this.syncRoleSkills(workDir, backend, classic ? "classic" : cfg.general_topic ? "general" : "worker");
18522
- }
18523
- catch (err) {
18524
- this.logger.warn({ name, err }, "Could not refresh experimental skill visibility");
18525
- }
18526
- }
18527
- }
18528
18434
  /** Publish only the bundled skills eligible for an instance role. */
18529
18435
  syncRoleSkills(workDir, backend, role, knowledgeDir) {
18530
18436
  const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
@@ -18729,7 +18635,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
18729
18635
  continue;
18730
18636
  }
18731
18637
  managed.push(name);
18732
- const newContent = bundledSkillContents(join(srcSkills, name), this.canvasWorkerEnabled());
18638
+ const newContent = bundledSkillContents(join(srcSkills, name));
18733
18639
  if (refuse(dest))
18734
18640
  continue;
18735
18641
  try {
@@ -20887,6 +20793,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20887
20793
  return (adapterId ? generals.find(([name, cfg]) => (cfg.channel_id ?? this.getInstanceAdapterId(name)) === adapterId) : undefined)?.[0]
20888
20794
  ?? generals[0]?.[0];
20889
20795
  }
20796
+ /**
20797
+ * #2870: someone ClassicBot does not serve in this private chat: not on allowed_users, not a ClassicBot admin, not a
20798
+ * fleet admin of this bot, and no ClassicBot room here. Their commands other than /start get no answer.
20799
+ */
20800
+ isClassicPrivateStranger(userId, chatId, adapterId) {
20801
+ const classic = this.classicChannels;
20802
+ if (!classic)
20803
+ return false;
20804
+ return !classic.isAdmin(userId) && !classic.isUserAllowed(userId) && !this.isFleetAdmin(userId, adapterId ?? "")
20805
+ && !classic.isClassicChannel(chatId, adapterId);
20806
+ }
20890
20807
  /** Return a user-facing blocker without mutating ClassicBot state. */
20891
20808
  validateClassicStart(channelId, userId, guildId, adapterId) {
20892
20809
  const classic = this.classicChannels;
@@ -21169,7 +21086,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21169
21086
  try {
21170
21087
  this.themeStore.forget(ch.instanceName);
21171
21088
  this.webAppearance.limiter.forget(ch.instanceName);
21172
- this.effectToolsService?.forget(ch.instanceName);
21173
21089
  }
21174
21090
  catch (err) {
21175
21091
  this.logger.warn({ err: err.message, instanceName: ch.instanceName }, "Dropping a removed room's web theme failed");
@@ -21278,6 +21194,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21278
21194
  const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
21279
21195
  // #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
21280
21196
  // adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
21197
+ if (this.configHistoryTimer)
21198
+ clearInterval(this.configHistoryTimer);
21199
+ this.configHistoryTimer = null;
21281
21200
  const needsStopped = this.stopNeedsYou();
21282
21201
  // #1490 P3: stop() now returns a Promise; await it so the final
21283
21202
  // ledger flush completes before process.exit(0) in doStopAll's caller.
@@ -21333,7 +21252,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21333
21252
  this.clearStatuslineWatchers();
21334
21253
  this.costGuard?.stop();
21335
21254
  this.dailySummary?.stop();
21336
- this.taskBoardSummary?.stop();
21337
21255
  this.dailyTipScheduler?.stop();
21338
21256
  this.dailyTipScheduler = null;
21339
21257
  if (this.updateCheckTimer) {
@@ -21389,6 +21307,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21389
21307
  for (const timer of this.cancelButtonIdleRetireTimers.values())
21390
21308
  clearTimeout(timer);
21391
21309
  this.cancelButtonIdleRetireTimers.clear();
21310
+ // #2897: notices still being retired stay in the ledger; the next fleet's sweep finishes them.
21311
+ this.parkedNoticesStore?.dispose();
21392
21312
  if (this.topicCleanupTimer) {
21393
21313
  clearInterval(this.topicCleanupTimer);
21394
21314
  this.topicCleanupTimer = null;
@@ -21624,14 +21544,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21624
21544
  const previousRawConfig = this.rawFleetConfig;
21625
21545
  const previousRawDocument = this.rawFleetDocument;
21626
21546
  const previousSavedSnapshot = this.savedFleetConfigSnapshot;
21547
+ const previousSource = this.loadedFleetSource;
21627
21548
  try {
21628
- this.loadConfig(this.configPath);
21549
+ this.loadConfig(this.configPath, { remember: false });
21629
21550
  }
21630
21551
  catch (err) {
21631
21552
  this.fleetConfig = oldConfig;
21632
21553
  this.rawFleetConfig = previousRawConfig;
21633
21554
  this.rawFleetDocument = previousRawDocument;
21634
21555
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
21556
+ this.loadedFleetSource = previousSource;
21635
21557
  throw err;
21636
21558
  }
21637
21559
  const validation = validateFleetConfig(this.rawFleetConfig);
@@ -21647,6 +21569,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21647
21569
  this.rawFleetConfig = previousRawConfig;
21648
21570
  this.rawFleetDocument = previousRawDocument;
21649
21571
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
21572
+ this.loadedFleetSource = previousSource;
21650
21573
  this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
21651
21574
  this.logger.error({
21652
21575
  oldCount,
@@ -21667,11 +21590,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21667
21590
  // mark every row done and tell the user "changes applied".
21668
21591
  return { rejected: why };
21669
21592
  }
21593
+ if (this.loadedFleetSource)
21594
+ this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
21670
21595
  this.migrateOfficeStorage();
21671
21596
  this.migrateNoticeDeliveryStorage();
21672
- if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
21673
- this.canvasWorkerSettingChanged();
21674
- }
21675
21597
  // Classic behavior settings share the fleet defaults but are not entries
21676
21598
  // in fleet.yaml. Snapshot the old effective chain before reloading the
21677
21599
  // Classic file so SIGHUP can hot-apply either source without waiting for
@@ -21881,7 +21803,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21881
21803
  * the panel shows the mismatch instead of a button.
21882
21804
  */
21883
21805
  checkStartupSignatureConsistency() {
21884
- if (!this.configPath) {
21806
+ if (!this.configPath || this.configHistory?.isFallback(this.configPath)) {
21885
21807
  this.fleetSignatureMismatch = null;
21886
21808
  return;
21887
21809
  }
@@ -23538,6 +23460,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23538
23460
  }
23539
23461
  return scope.size ? scope : NO_GUEST_SCOPE;
23540
23462
  }
23463
+ /** #2855: whether a request carries a guest's live session (its cookie; checked without counting as activity). */
23464
+ presentsGuestSession(req) {
23465
+ if (!this.webToken || !this.webSessions)
23466
+ return false;
23467
+ const epoch = tokenEpoch(this.webToken);
23468
+ return readSessionCookies(req, this.webToken).some(id => this.webSessions.authenticate(id, epoch, { touch: false })?.tier === "guest");
23469
+ }
23470
+ /** #2855: whether the admin made this guest Trusted — only an explicit grant can (web.guests.grants[account].level). */
23471
+ guestTrusted(owner) {
23472
+ const guests = this.fleetConfig?.web?.guests;
23473
+ if (guests?.enabled !== true || !guests.grants)
23474
+ return false;
23475
+ const key = `${owner.adapterId}:${owner.userId}`;
23476
+ return Object.hasOwn(guests.grants, key) && guests.grants[key]?.level === "trusted";
23477
+ }
23541
23478
  /**
23542
23479
  * #1920 (#2104 review): what an account reaches before an admin's narrowing — its connection's topics (an open one's
23543
23480
  * named agents), its own Telegram private room, its verified group rooms. Narrowing can only take from this set, so it
@@ -23716,13 +23653,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23716
23653
  return;
23717
23654
  }
23718
23655
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
23719
- if (!this.canvasWorkerEnabled() && isCanvasWorkerHttpPath(requestPath)) {
23720
- res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
23721
- res.end(JSON.stringify({ error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }));
23722
- return;
23723
- }
23724
- if (this.effectBootstrap.handle(req, res))
23725
- return;
23726
23656
  // Branding is public, after Host admission: automatic favicon requests
23727
23657
  // must not become noisy web-token 401s in the browser console.
23728
23658
  // #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
@@ -23730,8 +23660,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23730
23660
  if (handleWebIconRequest(req, res, requestPath))
23731
23661
  return;
23732
23662
  // Public: the health probe, /agent (instance-token auth of its own), the
23733
- // sign-in surface, and /view's reads unless web.view_access says otherwise.
23734
- if (bypassesWebGate(req, requestPath, this.fleetConfig, p => !gatewayRequestContext(req) && (isViewPath(p) || isUsagePath(p)))) {
23663
+ // sign-in surface, and /view's reads unless web.view_access says otherwise (#2855: never a guest's, which go
23664
+ // through the guest gate and its limits on either listener).
23665
+ if (bypassesWebGate(req, requestPath, this.fleetConfig, p => !gatewayRequestContext(req) && (isViewPath(p) || isUsagePath(p)) && !this.presentsGuestSession(req))) {
23735
23666
  // fall through to the handlers below
23736
23667
  }
23737
23668
  else {
@@ -23797,7 +23728,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23797
23728
  streamOpened: () => { void this.refreshGuestRooms(owner, { mode: "force" }); },
23798
23729
  history: () => this.webChatHistory,
23799
23730
  // #1920 §5.1: guests chat unless the host made them read-only (web.guests.chat: false).
23800
- chat: () => this.fleetConfig?.web?.guests?.chat !== false });
23731
+ chat: () => this.fleetConfig?.web?.guests?.chat !== false,
23732
+ // #2855: the admin's level for this guest, read at each request (a change applies at once).
23733
+ trusted: () => this.guestTrusted(owner) });
23801
23734
  }
23802
23735
  if (decision.via === "session" && this.webToken) {
23803
23736
  const upgrade = sessionCookieUpgrade(req, this.webToken, decision.session);
@@ -24203,7 +24136,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24203
24136
  return;
24204
24137
  }
24205
24138
  const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
24206
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
24207
24139
  // #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
24208
24140
  extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; } });
24209
24141
  this.previewListener = listener;
@@ -24380,8 +24312,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24380
24312
  catch {
24381
24313
  return {};
24382
24314
  } })(),
24383
- // #2118: only this Chat's placement content; guest projection drops global revisions.
24384
- effect_chat_v: this.canvasWorkerEnabled() ? createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32) : null,
24385
24315
  // #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
24386
24316
  color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
24387
24317
  ...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
@@ -24392,6 +24322,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24392
24322
  tags: identity.tags,
24393
24323
  teams: instanceTeams(this.fleetConfig?.teams, name), // #2172: the sidebar groups a team's members under it
24394
24324
  last_reply: this.webChatHistory.lastAgentReply(name) ?? undefined, // #2179: its last reply to a person (ISO)
24325
+ // #2855: its last reply that reached the channel — the only reply time a guest's sidebar gets (guestStatus).
24326
+ last_reply_channel: this.webChatHistory.lastAgentReply(name, { channel: true }) ?? undefined,
24327
+ // #2855: the web commands a guest may run here — the table's "anyone" cells for this channel (guestWebCommands);
24328
+ // only while guests are on (no guest reads it otherwise).
24329
+ ...(this.fleetConfig?.web?.guests?.enabled === true
24330
+ ? { guest_commands: (() => { const scope = this.webCommandScope(name); return scope ? guestWebCommands(scope, this.commandPlatform(name)) : []; })() } : {}),
24395
24331
  status: this.getInstanceStatus(name),
24396
24332
  // `state` (presentation: may be awaiting_input) and `execution_state` (working / idle / stuck, or null —
24397
24333
  // what the dashboard's activity events carry) come from instancePresentation (#1212).
@@ -24408,7 +24344,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24408
24344
  };
24409
24345
  });
24410
24346
  return {
24411
- canvas_worker: this.canvasWorkerEnabled(),
24412
24347
  instances,
24413
24348
  startup: this.startupDashboard?.snapshot() ?? null,
24414
24349
  publicLink: this.getPublicWebStatus(),