@songsid/agend 2.3.2 → 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 (393) hide show
  1. package/dist/agent-cli-instructions.md +2 -2
  2. package/dist/agent-cli.js +13 -112
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.d.ts +0 -1
  5. package/dist/agent-endpoint.js +1 -9
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/agent-whats-new.d.ts +23 -5
  8. package/dist/agent-whats-new.js +58 -40
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.d.ts +0 -7
  11. package/dist/auth-api.js +1 -15
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/authority/authority-state.js +1 -1
  14. package/dist/authority/authority-state.js.map +1 -1
  15. package/dist/authority/web-authority.d.ts +9 -4
  16. package/dist/authority/web-authority.js +10 -5
  17. package/dist/authority/web-authority.js.map +1 -1
  18. package/dist/backend/antigravity.js +6 -1
  19. package/dist/backend/antigravity.js.map +1 -1
  20. package/dist/backend/claude-code.d.ts +20 -1
  21. package/dist/backend/claude-code.js +54 -29
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/claude-transcript-errors.d.ts +18 -0
  24. package/dist/backend/claude-transcript-errors.js +19 -0
  25. package/dist/backend/claude-transcript-errors.js.map +1 -0
  26. package/dist/backend/codex-session-lookup.d.ts +16 -3
  27. package/dist/backend/codex-session-lookup.js +45 -11
  28. package/dist/backend/codex-session-lookup.js.map +1 -1
  29. package/dist/backend/codex.d.ts +16 -0
  30. package/dist/backend/codex.js +184 -35
  31. package/dist/backend/codex.js.map +1 -1
  32. package/dist/backend/grok.js +58 -12
  33. package/dist/backend/grok.js.map +1 -1
  34. package/dist/backend/kiro-reconcile-store.js +8 -1
  35. package/dist/backend/kiro-reconcile-store.js.map +1 -1
  36. package/dist/backend/kiro.d.ts +0 -5
  37. package/dist/backend/kiro.js +2 -10
  38. package/dist/backend/kiro.js.map +1 -1
  39. package/dist/backend/muse.js +2 -1
  40. package/dist/backend/muse.js.map +1 -1
  41. package/dist/backend/types.d.ts +53 -4
  42. package/dist/backend/types.js.map +1 -1
  43. package/dist/bundled-skills.d.ts +4 -6
  44. package/dist/bundled-skills.js +10 -14
  45. package/dist/bundled-skills.js.map +1 -1
  46. package/dist/channel/adapters/discord.js +60 -17
  47. package/dist/channel/adapters/discord.js.map +1 -1
  48. package/dist/channel/mcp-server.js +5 -5
  49. package/dist/channel/mcp-server.js.map +1 -1
  50. package/dist/channel/mcp-tools.d.ts +1 -3
  51. package/dist/channel/mcp-tools.js +4 -58
  52. package/dist/channel/mcp-tools.js.map +1 -1
  53. package/dist/classic-channel-manager.d.ts +4 -1
  54. package/dist/classic-channel-manager.js +21 -7
  55. package/dist/classic-channel-manager.js.map +1 -1
  56. package/dist/cli.js +23 -6
  57. package/dist/cli.js.map +1 -1
  58. package/dist/config-history.d.ts +50 -0
  59. package/dist/config-history.js +263 -0
  60. package/dist/config-history.js.map +1 -0
  61. package/dist/config-validator.js +10 -8
  62. package/dist/config-validator.js.map +1 -1
  63. package/dist/config.d.ts +3 -0
  64. package/dist/config.js +11 -4
  65. package/dist/config.js.map +1 -1
  66. package/dist/cross-instance-envelope.d.ts +10 -5
  67. package/dist/cross-instance-envelope.js +20 -17
  68. package/dist/cross-instance-envelope.js.map +1 -1
  69. package/dist/daemon.d.ts +130 -64
  70. package/dist/daemon.js +603 -428
  71. package/dist/daemon.js.map +1 -1
  72. package/dist/daily-summary.d.ts +8 -1
  73. package/dist/daily-summary.js +8 -5
  74. package/dist/daily-summary.js.map +1 -1
  75. package/dist/dashboard-live.d.ts +1 -1
  76. package/dist/dashboard-live.js +3 -3
  77. package/dist/dashboard-live.js.map +1 -1
  78. package/dist/dashboard-permissions.d.ts +1 -1
  79. package/dist/dashboard-permissions.js +1 -3
  80. package/dist/dashboard-permissions.js.map +1 -1
  81. package/dist/delivery-outbox.d.ts +31 -22
  82. package/dist/delivery-outbox.js +119 -161
  83. package/dist/delivery-outbox.js.map +1 -1
  84. package/dist/delivery-outbox.test.js +10 -25
  85. package/dist/delivery-outbox.test.js.map +1 -1
  86. package/dist/doctor.d.ts +4 -0
  87. package/dist/doctor.js +16 -1
  88. package/dist/doctor.js.map +1 -1
  89. package/dist/fleet-level-config.js +1 -1
  90. package/dist/fleet-level-config.js.map +1 -1
  91. package/dist/fleet-manager.d.ts +74 -66
  92. package/dist/fleet-manager.js +285 -414
  93. package/dist/fleet-manager.js.map +1 -1
  94. package/dist/fleet-process-handlers.d.ts +5 -0
  95. package/dist/fleet-process-handlers.js +21 -0
  96. package/dist/fleet-process-handlers.js.map +1 -1
  97. package/dist/fleet-service-owner.d.ts +11 -0
  98. package/dist/fleet-service-owner.js +3 -0
  99. package/dist/fleet-service-owner.js.map +1 -0
  100. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
  101. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
  102. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  103. package/dist/general-knowledge/skills/session-management/SKILL.md +7 -1
  104. package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
  105. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +30 -68
  106. package/dist/instance-lifecycle.d.ts +19 -0
  107. package/dist/instance-lifecycle.js +73 -15
  108. package/dist/instance-lifecycle.js.map +1 -1
  109. package/dist/instructions.js +6 -7
  110. package/dist/instructions.js.map +1 -1
  111. package/dist/kiro-reconcile.js +3 -3
  112. package/dist/kiro-reconcile.js.map +1 -1
  113. package/dist/locale.js +96 -58
  114. package/dist/locale.js.map +1 -1
  115. package/dist/log-service.js +1 -1
  116. package/dist/log-service.js.map +1 -1
  117. package/dist/needs-you-hub.d.ts +1 -0
  118. package/dist/needs-you-hub.js +6 -3
  119. package/dist/needs-you-hub.js.map +1 -1
  120. package/dist/needs-you-live.d.ts +3 -1
  121. package/dist/needs-you-live.js +24 -2
  122. package/dist/needs-you-live.js.map +1 -1
  123. package/dist/needs-you.d.ts +13 -2
  124. package/dist/needs-you.js +11 -1
  125. package/dist/needs-you.js.map +1 -1
  126. package/dist/notify-socket.d.ts +10 -0
  127. package/dist/notify-socket.js +45 -0
  128. package/dist/notify-socket.js.map +1 -0
  129. package/dist/outbound-handlers.js +20 -8
  130. package/dist/outbound-handlers.js.map +1 -1
  131. package/dist/outbound-schemas.d.ts +0 -62
  132. package/dist/outbound-schemas.js +4 -43
  133. package/dist/outbound-schemas.js.map +1 -1
  134. package/dist/parked-notices.d.ts +87 -0
  135. package/dist/parked-notices.js +194 -0
  136. package/dist/parked-notices.js.map +1 -0
  137. package/dist/public-link-progress.d.ts +5 -2
  138. package/dist/public-link-progress.js +4 -3
  139. package/dist/public-link-progress.js.map +1 -1
  140. package/dist/public-web-gateway.js +0 -9
  141. package/dist/public-web-gateway.js.map +1 -1
  142. package/dist/public-web-link.d.ts +12 -0
  143. package/dist/public-web-link.js +29 -5
  144. package/dist/public-web-link.js.map +1 -1
  145. package/dist/quota-notice.d.ts +23 -0
  146. package/dist/quota-notice.js +22 -0
  147. package/dist/quota-notice.js.map +1 -0
  148. package/dist/scheduler/task-board.d.ts +17 -25
  149. package/dist/scheduler/task-board.js +29 -39
  150. package/dist/scheduler/task-board.js.map +1 -1
  151. package/dist/sd-notify.d.ts +23 -15
  152. package/dist/sd-notify.js +88 -29
  153. package/dist/sd-notify.js.map +1 -1
  154. package/dist/search/search-docs.d.ts +1 -1
  155. package/dist/search/search-docs.js +2 -2
  156. package/dist/search/search-docs.js.map +1 -1
  157. package/dist/settings-api.d.ts +16 -1
  158. package/dist/settings-api.js +56 -16
  159. package/dist/settings-api.js.map +1 -1
  160. package/dist/settings-change.js +3 -1
  161. package/dist/settings-change.js.map +1 -1
  162. package/dist/settings-confirmation.d.ts +17 -3
  163. package/dist/settings-confirmation.js +8 -5
  164. package/dist/settings-confirmation.js.map +1 -1
  165. package/dist/settings-effect.js +0 -2
  166. package/dist/settings-effect.js.map +1 -1
  167. package/dist/settings-http-confirmation.js +31 -1
  168. package/dist/settings-http-confirmation.js.map +1 -1
  169. package/dist/setup-wizard.d.ts +3 -0
  170. package/dist/setup-wizard.js +4 -2
  171. package/dist/setup-wizard.js.map +1 -1
  172. package/dist/task-board-waker.d.ts +43 -1
  173. package/dist/task-board-waker.js +121 -9
  174. package/dist/task-board-waker.js.map +1 -1
  175. package/dist/tmux-control.d.ts +1 -1
  176. package/dist/tmux-control.js +3 -2
  177. package/dist/tmux-control.js.map +1 -1
  178. package/dist/tmux-manager.js +18 -16
  179. package/dist/tmux-manager.js.map +1 -1
  180. package/dist/tmux-read.d.ts +2 -0
  181. package/dist/tmux-read.js +7 -1
  182. package/dist/tmux-read.js.map +1 -1
  183. package/dist/tool-permissions.js +3 -10
  184. package/dist/tool-permissions.js.map +1 -1
  185. package/dist/topic-commands.d.ts +29 -7
  186. package/dist/topic-commands.js +97 -37
  187. package/dist/topic-commands.js.map +1 -1
  188. package/dist/transcript-monitor.d.ts +7 -0
  189. package/dist/transcript-monitor.js +48 -3
  190. package/dist/transcript-monitor.js.map +1 -1
  191. package/dist/tunnel/cloudflared.d.ts +5 -0
  192. package/dist/tunnel/cloudflared.js +32 -1
  193. package/dist/tunnel/cloudflared.js.map +1 -1
  194. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  195. package/dist/tunnel/named-diagnostics.js +4 -1
  196. package/dist/tunnel/named-diagnostics.js.map +1 -1
  197. package/dist/tunnel/named-link-test.d.ts +90 -0
  198. package/dist/tunnel/named-link-test.js +270 -0
  199. package/dist/tunnel/named-link-test.js.map +1 -0
  200. package/dist/tunnel/purpose-lane.d.ts +3 -1
  201. package/dist/tunnel/purpose-lane.js +2 -0
  202. package/dist/tunnel/purpose-lane.js.map +1 -1
  203. package/dist/types.d.ts +9 -11
  204. package/dist/ui/chat-ambient.js +4 -4
  205. package/dist/ui/chat-commands.js +3 -2
  206. package/dist/ui/chat-store.js +4 -1
  207. package/dist/ui/chat-strings.js +12 -46
  208. package/dist/ui/chat-thread.js +51 -59
  209. package/dist/ui/fleet-office-skin.js +37 -14
  210. package/dist/ui/fleet-office.js +31 -7
  211. package/dist/ui/html-attachment.js +22 -8
  212. package/dist/ui/panel-chat.js +21 -50
  213. package/dist/ui/panel-log.js +2 -2
  214. package/dist/ui/panel-needs.js +11 -9
  215. package/dist/ui/panel-settings.js +5 -3
  216. package/dist/ui/preview-panel.js +12 -21
  217. package/dist/ui/preview.js +10 -3
  218. package/dist/ui/search-dialog.js +30 -14
  219. package/dist/ui/settings-appearance.js +3 -37
  220. package/dist/ui/settings-confirm.js +18 -0
  221. package/dist/ui/settings-guests.js +13 -4
  222. package/dist/ui/settings-learn.js +1 -1
  223. package/dist/ui/settings-mods.js +5 -12
  224. package/dist/ui/settings-named-test.js +144 -0
  225. package/dist/ui/settings-registry.js +0 -2
  226. package/dist/ui/settings-strings.js +16 -16
  227. package/dist/ui/settings-web-authority.js +4 -3
  228. package/dist/ui/shared/app-i18n.js +15 -4
  229. package/dist/ui/shared/app-needs.js +5 -1
  230. package/dist/ui/shared/app-prefs.js +17 -17
  231. package/dist/ui/shared/app-shell.js +47 -9
  232. package/dist/ui/shared/app-store.js +6 -2
  233. package/dist/ui/shared/app.css +33 -39
  234. package/dist/ui/shared/app.js +5 -4
  235. package/dist/ui/shared/instance-nav.js +9 -9
  236. package/dist/ui/shared/instance-switch.js +8 -2
  237. package/dist/ui/shared/panel-view.js +22 -8
  238. package/dist/ui/shared/view-strings.js +6 -0
  239. package/dist/ui/shared/web-authorize.js +35 -13
  240. package/dist/update-check.d.ts +13 -4
  241. package/dist/update-check.js +17 -4
  242. package/dist/update-check.js.map +1 -1
  243. package/dist/usage/usage-api.d.ts +3 -0
  244. package/dist/usage/usage-api.js +5 -1
  245. package/dist/usage/usage-api.js.map +1 -1
  246. package/dist/view-api.d.ts +2 -0
  247. package/dist/view-api.js +64 -0
  248. package/dist/view-api.js.map +1 -1
  249. package/dist/web-api.d.ts +9 -17
  250. package/dist/web-api.js +257 -371
  251. package/dist/web-api.js.map +1 -1
  252. package/dist/web-appearance-tools.d.ts +0 -2
  253. package/dist/web-appearance-tools.js +2 -5
  254. package/dist/web-appearance-tools.js.map +1 -1
  255. package/dist/web-appearance-view.d.ts +0 -6
  256. package/dist/web-appearance-view.js +0 -1
  257. package/dist/web-appearance-view.js.map +1 -1
  258. package/dist/web-appearance.d.ts +14 -7
  259. package/dist/web-appearance.js +5 -6
  260. package/dist/web-appearance.js.map +1 -1
  261. package/dist/web-asset-parts.d.ts +1 -17
  262. package/dist/web-asset-parts.js +1 -15
  263. package/dist/web-asset-parts.js.map +1 -1
  264. package/dist/web-auth.js +1 -7
  265. package/dist/web-auth.js.map +1 -1
  266. package/dist/web-chat-history.d.ts +3 -7
  267. package/dist/web-chat-history.js +10 -17
  268. package/dist/web-chat-history.js.map +1 -1
  269. package/dist/web-commands.d.ts +9 -1
  270. package/dist/web-commands.js +13 -0
  271. package/dist/web-commands.js.map +1 -1
  272. package/dist/web-experimental.d.ts +3 -8
  273. package/dist/web-experimental.js +9 -33
  274. package/dist/web-experimental.js.map +1 -1
  275. package/dist/web-guest-access.d.ts +40 -2
  276. package/dist/web-guest-access.js +50 -14
  277. package/dist/web-guest-access.js.map +1 -1
  278. package/dist/web-mod-bundle.d.ts +2 -45
  279. package/dist/web-mod-bundle.js +15 -63
  280. package/dist/web-mod-bundle.js.map +1 -1
  281. package/dist/web-mod-store.d.ts +3 -48
  282. package/dist/web-mod-store.js +30 -97
  283. package/dist/web-mod-store.js.map +1 -1
  284. package/dist/web-prefs.d.ts +13 -0
  285. package/dist/web-prefs.js +38 -0
  286. package/dist/web-prefs.js.map +1 -1
  287. package/dist/web-preview.d.ts +6 -13
  288. package/dist/web-preview.js +12 -23
  289. package/dist/web-preview.js.map +1 -1
  290. package/dist/web-session.d.ts +11 -3
  291. package/dist/web-session.js +22 -11
  292. package/dist/web-session.js.map +1 -1
  293. package/dist/web-settings-patch.d.ts +1 -1
  294. package/dist/web-settings-patch.js +9 -7
  295. package/dist/web-settings-patch.js.map +1 -1
  296. package/dist/web-settings-tools.d.ts +0 -13
  297. package/dist/web-settings-tools.js +12 -84
  298. package/dist/web-settings-tools.js.map +1 -1
  299. package/dist/workflow-templates/default.md +2 -2
  300. package/dist/working-recovery.d.ts +0 -2
  301. package/dist/working-recovery.js +2 -23
  302. package/dist/working-recovery.js.map +1 -1
  303. package/launcher/agend-install.cjs +7 -1
  304. package/launcher/fleet-service-owner.cjs +150 -0
  305. package/package.json +1 -1
  306. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
  307. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -673
  308. package/dist/task-board-summary.d.ts +0 -101
  309. package/dist/task-board-summary.js +0 -177
  310. package/dist/task-board-summary.js.map +0 -1
  311. package/dist/ui/app-effects.js +0 -251
  312. package/dist/ui/chat-effect-posters.js +0 -42
  313. package/dist/ui/chat-effects.js +0 -167
  314. package/dist/ui/effect-trial-controller.js +0 -124
  315. package/dist/ui/effect-trial-offer.js +0 -2
  316. package/dist/ui/effect-trial-panel.js +0 -40
  317. package/dist/ui/effects-waker.js +0 -2
  318. package/dist/ui/settings-effects.js +0 -202
  319. package/dist/ui/settings-owner-effects.js +0 -82
  320. package/dist/ui/shared/effect-app-runtime.js +0 -299
  321. package/dist/ui/shared/effect-baseline.js +0 -64
  322. package/dist/ui/shared/effect-chat-runtime.js +0 -162
  323. package/dist/ui/shared/effect-cursor.js +0 -126
  324. package/dist/ui/shared/effect-display-metadata.js +0 -39
  325. package/dist/ui/shared/effect-frame.js +0 -146
  326. package/dist/ui/shared/effect-geometry.js +0 -137
  327. package/dist/ui/shared/effect-host-style.js +0 -10
  328. package/dist/ui/shared/effect-owner-regions.js +0 -74
  329. package/dist/ui/shared/effect-owner-source.js +0 -122
  330. package/dist/ui/shared/effect-policy.js +0 -151
  331. package/dist/ui/shared/effect-poster-reader.js +0 -98
  332. package/dist/ui/shared/effect-posters.js +0 -117
  333. package/dist/ui/shared/effect-profiler.js +0 -184
  334. package/dist/ui/shared/effect-profiles.js +0 -65
  335. package/dist/ui/shared/effect-registry.js +0 -545
  336. package/dist/ui/shared/effect-runtime-loader.js +0 -236
  337. package/dist/ui/shared/effect-safety.js +0 -153
  338. package/dist/ui/shared/effect-stock-snapshot.js +0 -43
  339. package/dist/ui/shared/effect-stock-trust.js +0 -217
  340. package/dist/ui/shared/effect-trial-shadow.js +0 -62
  341. package/dist/ui/shared/effect-trial-source.js +0 -103
  342. package/dist/ui/shared/effect-trial-stage.js +0 -170
  343. package/dist/ui/shared/effect-trial.css +0 -40
  344. package/dist/ui/shared/feature-flags.js +0 -127
  345. package/dist/ui/shared/owner-effect-prefs.js +0 -117
  346. package/dist/upgrade-notices.d.ts +0 -17
  347. package/dist/upgrade-notices.js +0 -93
  348. package/dist/upgrade-notices.js.map +0 -1
  349. package/dist/web-effect-bootstrap.d.ts +0 -12
  350. package/dist/web-effect-bootstrap.js +0 -49
  351. package/dist/web-effect-bootstrap.js.map +0 -1
  352. package/dist/web-effect-bundle.d.ts +0 -28
  353. package/dist/web-effect-bundle.js +0 -171
  354. package/dist/web-effect-bundle.js.map +0 -1
  355. package/dist/web-effect-canonical.d.ts +0 -8
  356. package/dist/web-effect-canonical.js +0 -27
  357. package/dist/web-effect-canonical.js.map +0 -1
  358. package/dist/web-effect-library.d.ts +0 -114
  359. package/dist/web-effect-library.js +0 -530
  360. package/dist/web-effect-library.js.map +0 -1
  361. package/dist/web-effect-playback.d.ts +0 -30
  362. package/dist/web-effect-playback.js +0 -71
  363. package/dist/web-effect-playback.js.map +0 -1
  364. package/dist/web-effect-poster.d.ts +0 -8
  365. package/dist/web-effect-poster.js +0 -52
  366. package/dist/web-effect-poster.js.map +0 -1
  367. package/dist/web-effect-profiles.d.ts +0 -118
  368. package/dist/web-effect-profiles.js +0 -181
  369. package/dist/web-effect-profiles.js.map +0 -1
  370. package/dist/web-effect-service.d.ts +0 -93
  371. package/dist/web-effect-service.js +0 -335
  372. package/dist/web-effect-service.js.map +0 -1
  373. package/dist/web-effect-tools.d.ts +0 -48
  374. package/dist/web-effect-tools.js +0 -129
  375. package/dist/web-effect-tools.js.map +0 -1
  376. package/dist/web-effect-trials.d.ts +0 -86
  377. package/dist/web-effect-trials.js +0 -204
  378. package/dist/web-effect-trials.js.map +0 -1
  379. package/dist/web-effect-upload.d.ts +0 -27
  380. package/dist/web-effect-upload.js +0 -185
  381. package/dist/web-effect-upload.js.map +0 -1
  382. package/dist/web-effect-worker.d.ts +0 -8
  383. package/dist/web-effect-worker.js +0 -170
  384. package/dist/web-effect-worker.js.map +0 -1
  385. package/dist/web-effect.d.ts +0 -103
  386. package/dist/web-effect.js +0 -153
  387. package/dist/web-effect.js.map +0 -1
  388. package/dist/web-experimental-http.d.ts +0 -2
  389. package/dist/web-experimental-http.js +0 -12
  390. package/dist/web-experimental-http.js.map +0 -1
  391. package/dist/web-mod-agent.d.ts +0 -50
  392. package/dist/web-mod-agent.js +0 -207
  393. 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";
@@ -52,7 +53,7 @@ import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./ki
52
53
  import { fleetLabel } from "./fleet-label.js";
53
54
  import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
54
55
  import { formatUpdateProgress } from "./update-progress.js";
55
- import { sdNotify, sdNotifyBlocking, setSdNotifyWarn } from "./sd-notify.js";
56
+ import { sdNotify, sdNotifyBlocking, setSdNotifyWarn, startSdWatchdog } from "./sd-notify.js";
56
57
  import { StartupMemoryDeadline, fleetStopConcurrency } from "./startup-memory-deadline.js";
57
58
  import { readFleetMemory } from "./process-memory.js";
58
59
  import { MemoryPressure } from "./memory-pressure.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";
@@ -94,13 +96,12 @@ import { processAttachments } from "./channel/attachment-handler.js";
94
96
  import { routeToolCall } from "./channel/tool-router.js";
95
97
  import { Scheduler } from "./scheduler/index.js";
96
98
  import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
97
- import { TaskBoardWaker } from "./task-board-waker.js";
99
+ import { TaskBoardWaker, namesCard } from "./task-board-waker.js";
98
100
  import { TaskBoardError, waitsForPerson, wrapUpGap } from "./scheduler/task-board.js";
99
101
  import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
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";
@@ -254,20 +248,19 @@ import { isSetupComplete, markSetupComplete } from "./setup-marker.js";
254
248
  import { manualCleanupMessage, reapStaleTunnel } from "./tunnel/lease.js";
255
249
  import { buildToolPermissionsNotice } from "./tool-permissions-notice.js";
256
250
  import { NeedsYouHub } from "./needs-you-hub.js";
257
- import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
258
251
  import { GENERAL_NAME, GENERAL_PAUSE_ERROR, adoptGeneralByName, freeGeneralName, isGeneralInstance } from "./general-instance.js";
259
252
  import { buildOrgChart } from "./web-org.js";
260
253
  import { topMemoryHogs } from "./memory-hogs.js";
261
254
  import { GENERAL_POINTING_REFUSALS, SNOWFLAKE, hasPlaceRefs, placeRef, webRoute, webTextLinks, placeSuffix, renderPlaceRefs, stripPlaceRefs, topicUrl, worldPlaceOf } from "./topic-link.js";
262
255
  import { CacheService, cacheWindow } from "./cache-service.js";
263
- import { runWebCommand } from "./web-commands.js";
256
+ import { guestWebCommands, runWebCommand } from "./web-commands.js";
264
257
  import { CompactWatch, launchIdentity } from "./compact-watch.js";
265
258
  import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./backend/antigravity.js";
266
259
  import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
267
260
  import { sharedRolloutIndex } from "./rollout-index.js";
268
261
  import { TASK_BOARD_ON_NOTICE, dashboardRefusalIn, taskBoardSwitchedOn } from "./dashboard-permissions.js";
269
262
  import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
270
- import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
263
+ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readSessionCookies, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
271
264
  import { bypassesWebGate, handleAuthRequest, serveGuestRefusedPage, serveSigninPage } from "./auth-api.js";
272
265
  import { isWebPageNavigation } from "./web-shell-routes.js";
273
266
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
@@ -280,10 +273,8 @@ import { decisionListAnswer } from "./decision-list.js";
280
273
  import { CodeRequestLimiter, ReturningDevices } from "./web-returning-device.js";
281
274
  import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, LOOPBACK_HOST_NAMES, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
282
275
  import { createPreviewListener, normalizeOrigin, previewAvailability, previewSettings } from "./web-preview.js";
283
- import { canvasWorkerEnabled, isCanvasWorkerTool, CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
284
- import { isCanvasWorkerHttpPath } from "./web-experimental-http.js";
276
+ import { isCanvasWorkerTool, CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
285
277
  import { bundledSkillContents } from "./bundled-skills.js";
286
- import { createEffectBootstrap } from "./web-effect-bootstrap.js";
287
278
  import { fleetLevelDifferences, fleetLevelSignature, appendedConnections } from "./fleet-level-config.js";
288
279
  import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-restart-limit.js";
289
280
  import { SecretStore } from "./secret-store.js";
@@ -612,7 +603,6 @@ export function deriveSpawnConcurrency(freeMemMB, cores) {
612
603
  /** #926: how often reply obligations are checked, the owner-reminder grace, and the default overdue window. */
613
604
  const REPLY_OBLIGATION_SWEEP_MS = 30_000;
614
605
  const REPLY_REMINDER_GRACE_MS = 60_000;
615
- const DEFAULT_REPLY_OVERDUE_MINUTES = 15;
616
606
  /**
617
607
  * The filter `list_emojis` and `list_stickers` take (#1226). Values may come from the agent CLI as strings, so a
618
608
  * limit is read as a number and the flags accept "true". No limit means all of them — never zero.
@@ -676,32 +666,6 @@ export class FleetManager {
676
666
  laneFree: name => !this.isInstanceRestarting(name) && !this.activeDurableTargets.has(name)
677
667
  && outbox.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending"]) === 0,
678
668
  admit: (episode, tasks, advisory) => outbox.admitWorkingRecovery(episode, buildWorkingRecoveryPrompt(tasks, this.workingRecoveryCause, advisory), advisory),
679
- summary: (group, rows) => {
680
- const general = this.findGeneralInstance();
681
- if (!general || !this.daemons.has(general))
682
- return;
683
- // Our own informational notice must not supersede General's recovery
684
- // as unrelated pending work before it crosses the input boundary.
685
- const generalEpisode = store.get(general);
686
- if (generalEpisode?.outcome === "admitted" && generalEpisode.deliveryId) {
687
- const state = outbox.get(generalEpisode.deliveryId)?.state;
688
- if (!state || !["delivered", "uncertain", "failed", "cancelled"].includes(state))
689
- return;
690
- }
691
- const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
692
- && (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
693
- .map(row => `${row.instance} (${row.reason})`);
694
- const posted = store.summary(group, () => outbox.admitSystemNotice(general, `working-recovery-summary:${group}`, `[system:working-recovery-summary] Observed cause: unknown interruption${this.workingRecoveryCause === "unknown_interruption" ? "" : `; prior requested action: ${this.workingRecoveryCause}`}. ${rows.length} interrupted instances; continuation queued ${rows.filter(r => r.outcome === "admitted").length}, excluded ${rows.filter(r => r.outcome === "excluded").length}, still deferred ${rows.filter(r => r.outcome === "deferred").length}. ${rows.filter(r => r.reason === "advisory_background_unknown").length ? `Advisory, background unknown: ${rows.filter(r => r.reason === "advisory_background_unknown").map(r => r.instance).join(", ")}. ` : ""}${deferredAdvisories.length ? `Deferred advisory candidates (background unknown; all other gates still apply): ${deferredAdvisories.join(", ")}. ` : ""}Details are in the Log. Deferred work remains eligible; no action or confirmation is requested.`));
695
- if (posted) {
696
- const counts = { interrupted: rows.length, queued: rows.filter(row => row.outcome === "admitted").length,
697
- excluded: rows.filter(row => row.outcome === "excluded").length, deferred: rows.filter(row => row.outcome === "deferred").length };
698
- const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
699
- this.logger.info({ group, ...counts, observedCause: "unknown_interruption", requestedAction }, "Interrupted work recovery summary queued for General");
700
- this.eventLog?.insert(general, "working_state", { reason: "recovery_summary", group, ...counts,
701
- observedCause: "unknown_interruption", requestedAction,
702
- message: `Interrupted work summary queued for General: ${counts.interrupted} interrupted; ${counts.queued} queued, ${counts.excluded} excluded, ${counts.deferred} deferred.` });
703
- }
704
- },
705
669
  log: (episode, outcome, reason) => {
706
670
  const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
707
671
  this.logger.info({ name: episode.instance, episode: episode.key, outcome, reason,
@@ -825,8 +789,6 @@ export class FleetManager {
825
789
  costGuard = null;
826
790
  statuslineWatcher;
827
791
  dailySummary = null;
828
- /** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
829
- taskBoardSummary = null;
830
792
  dailyTipScheduler = null;
831
793
  /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
832
794
  taskBoardWaker = null;
@@ -937,6 +899,12 @@ export class FleetManager {
937
899
  kiroMigrationRuns = new Map();
938
900
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
939
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
+ }
940
908
  /** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
941
909
  needsCrashedAt = new Map();
942
910
  /** Pause reason/time read from the marker, per paused instance; dropped on any attention change of it, and at most
@@ -1012,11 +980,6 @@ export class FleetManager {
1012
980
  collabInstances = new Set();
1013
981
  // Health endpoint
1014
982
  healthServer = null;
1015
- /** #2118: independent of HTML preview availability/port; fixed bootstrap only, no effect resources. */
1016
- effectBootstrap = createEffectBootstrap();
1017
- /** Fixed trusted document coherence, independent of the HTML preview listener; never a resource grant. */
1018
- get effectBoot() { return this.effectBootstrap.boot; }
1019
- canvasWorkerEnabled() { return canvasWorkerEnabled(this.fleetConfig?.web); }
1020
983
  healthServerDrain = null;
1021
984
  /** #1306: the preview listener (health_port + 1 by default), and whether it is listening. */
1022
985
  previewListener = null;
@@ -1195,7 +1158,7 @@ export class FleetManager {
1195
1158
  /** #1629: every instance's theme, placements, assets and lock (web-themes.json). */
1196
1159
  get themeStore() {
1197
1160
  // r4.1 Q3 (b): the card's limits are read from fleet.yaml at each upload (a change applies without a restart).
1198
- 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 }));
1199
1162
  }
1200
1163
  officeStoreInstance;
1201
1164
  /** Fleet → Org chart → Office: the offices people made (web-offices.json, beside web-themes.json). */
@@ -1207,6 +1170,12 @@ export class FleetManager {
1207
1170
  officeSkinWrites = new OfficeWriteLimiter(undefined, { perWindow: 10, windowMs: 60_000 });
1208
1171
  /** How often a person may save the offices (PUT /ui/offices); memory only, as the appearance limiter. */
1209
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 });
1210
1179
  officeSharesInstance;
1211
1180
  /**
1212
1181
  * #2636: Office share links — the live Office view for people outside the fleet, read-only, on a quick tunnel of
@@ -1402,96 +1371,16 @@ export class FleetManager {
1402
1371
  }
1403
1372
  catch { /* the next frame carries the rev */ } }));
1404
1373
  }
1405
- effectLibraryInstance;
1406
- effectTrialsInstance;
1407
- /** Lazy, single-owner storage. Register every host reference source before any operation can collect. */
1408
- get effectLibrary() {
1409
- if (!this.effectLibraryInstance) {
1410
- const library = new WebEffectLibrary(this.dataDir);
1411
- // Providers are lazy; publish only after every registration succeeds.
1412
- library.registerReferenceSource("themes", () => {
1413
- if (!this.themeStore.readable())
1414
- throw new Error("themes_unreadable");
1415
- return this.themeStore.placedEffects().flatMap(row => row.ids.map(id => ({ id, holder: `instance:${row.name}` })));
1416
- });
1417
- library.registerReferenceSource("prefs", () => this.webPrefs.ownerEffectReferences());
1418
- library.registerReferenceSource("mods", () => {
1419
- const mods = this.mods.list();
1420
- if (mods.unreadable)
1421
- throw new Error("mods_unreadable");
1422
- return mods.mods.flatMap(mod => mod.effects.map(effect => ({ id: effect.id, holder: `mod:${mod.id}` })));
1423
- });
1424
- this.effectTrialsInstance = new WebEffectTrials({ library, known: name => this.isKnownInstance(name) });
1425
- this.effectLibraryInstance = library;
1426
- }
1427
- return this.effectLibraryInstance;
1428
- }
1429
- /** Volatile Try offers and session-bound runs share this library. */
1430
- get effectTrials() { if (!this.effectTrialsInstance)
1431
- void this.effectLibrary; return this.effectTrialsInstance; }
1432
- effectProfilesInstance;
1433
- get effectProfiles() { return (this.effectProfilesInstance ??= new WebEffectProfiles(this.dataDir)); }
1434
- effectServiceInstance;
1435
- get effectService() {
1436
- return (this.effectServiceInstance ??= new WebEffectService({ library: this.effectLibrary, themes: this.themeStore, profiles: this.effectProfiles, prefs: this.webPrefs, trials: this.effectTrials,
1437
- known: name => this.isKnownInstance(name), changed: () => { try {
1438
- this.emitSseEvent("status", this.getUiStatus());
1439
- }
1440
- catch { /* next status carries revision */ } } }));
1441
- }
1442
- effectToolsService;
1443
- /** Caller identity is supplied only by the owned IPC connection / verified agent endpoint token. */
1444
- get effectTools() {
1445
- return (this.effectToolsService ??= new WebEffectTools({
1446
- library: this.effectLibrary, limiter: this.webAppearance.limiter,
1447
- authorize: (name, buckets) => !this.isKnownInstance(name) ? { error: "unknown_instance" } : this.webAppearance.authorizeEffect(name, buckets),
1448
- placed: name => this.themeStore.get(name).placements.effects,
1449
- place: (name, effects) => {
1450
- const before = this.themeStore.get(name).placements.effects;
1451
- const explicit = new Set(effects.filter(e => typeof e !== "string").map(e => e.id));
1452
- const w = this.themeStore.setPlacements(name, { effects }, "agent", what => {
1453
- // An unchanged human/private placement may be retained, never exposed or newly acquired by an agent.
1454
- const r = explicit.has(what.id) ? this.effectLibrary.read(what.id, { kind: "agent", id: name })
1455
- : this.effectLibrary.readReferenced(what.id, `instance:${name}`);
1456
- if (!r.ok)
1457
- return r.error;
1458
- const params = explicit.has(what.id) ? what.params : before.find(e => e.id === what.id)?.params;
1459
- return resolveEffectParams(r.entry.manifest, params ?? {}).ok ? null : "effect_params";
1460
- });
1461
- return w.ok ? { ok: true, rev: w.rev } : { ok: false, error: w.error };
1462
- },
1463
- changed: () => { try {
1464
- this.emitSseEvent("status", this.getUiStatus());
1465
- }
1466
- catch { /* later status carries placements */ } },
1467
- // A nonmutating own/shared offer is not an appearance write. Only an explicit non-public admin-session click may start it.
1468
- preview: async (instance, id) => {
1469
- const result = this.effectTrials.offer(instance, id);
1470
- if (!result.ok)
1471
- return { error: "not_found" };
1472
- const present = this.webChatHistory.list(instance).some(row => row.effectTrial?.offer === result.offer.offer);
1473
- if (!present && !this.publishEffectTrial(instance, result.offer))
1474
- return { error: "trial_unavailable" };
1475
- 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." };
1476
- },
1477
- profiles: id => this.effectProfiles.read(id),
1478
- profileSummary: id => this.effectService.profileSummary(id),
1479
- profileSummaries: ids => this.effectService.profileSummaries(ids),
1480
- playbackSummary: (name, id) => this.effectService.playbackSummary(name, id),
1481
- }));
1482
- }
1483
1374
  appearanceToolsService;
1484
1375
  /** #1629 r4 §1: get_web_appearance / preview_web_appearance — the caller's own look, resolved as the page resolves it. */
1485
1376
  get appearanceTools() {
1486
1377
  return (this.appearanceToolsService ??= new AppearanceTools({
1487
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1488
1378
  frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
1489
1379
  theme: (name) => {
1490
1380
  const r = this.themeStore.get(name);
1491
1381
  return { theme: r.theme, locked: r.locked, widgets: r.placements.widgets, ambient: r.placements.ambient,
1492
1382
  glyphAsset: (id) => this.themeStore.asset(name, id) };
1493
1383
  },
1494
- 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) })) : [],
1495
1384
  card: (name) => resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card),
1496
1385
  // #1958: the host's identity-colour settings, as the page reads them (an unreadable prefs file: the defaults).
1497
1386
  colorPrefs: () => { try {
@@ -1520,49 +1409,17 @@ export class FleetManager {
1520
1409
  return Object.hasOwn(this.fleetConfig?.instances ?? {}, name) || (this.classicChannels?.getAll() ?? []).some((ch) => ch.instanceName === name);
1521
1410
  }
1522
1411
  modStore;
1523
- /**
1524
- * #1629 r4-5 / B2: installed mods use the same validated library and scoped holder claims as direct uploads.
1525
- */
1412
+ /** #1629 r4-5: installed mods (#2799: a bundle that carries Canvas Worker effects is refused). */
1526
1413
  get mods() {
1527
1414
  return (this.modStore ??= new ModStore(this.dataDir, {
1528
1415
  themes: this.themeStore,
1529
1416
  prefs: this.webPrefs,
1530
1417
  ownerCss: this.ownerCss,
1531
- effects: this.effectLibrary,
1532
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1533
- context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits(), effects: cw1EffectChecker() }),
1418
+ context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits() }),
1534
1419
  isKnownInstance: (name) => this.isKnownInstance(name),
1535
1420
  prefsIdentity: OPERATOR,
1536
1421
  }));
1537
1422
  }
1538
- agentModsService;
1539
- /** #1629 r4-5b: export_web_mod / propose_web_mod — the caller's own look as a file, and a proposal for a person. */
1540
- get agentMods() {
1541
- return (this.agentModsService ??= new AgentMods({
1542
- themes: this.themeStore, mods: this.mods, limiter: this.webAppearance.limiter,
1543
- webConfig: () => this.fleetConfig?.web ?? null,
1544
- // The mod store's own: a proposal is checked exactly as the person's import of it will be.
1545
- context: () => this.mods.checkContext(),
1546
- 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; },
1547
- notice: (instance, text) => { try {
1548
- this.emitSseEvent("message", { instance, sender: instance, role: "status", text, ts: new Date().toISOString() });
1549
- }
1550
- catch { /* the page reads it from Settings too */ } },
1551
- changed: () => { try {
1552
- this.emitSseEvent("status", this.getUiStatus());
1553
- }
1554
- catch { /* the next frame */ } },
1555
- }));
1556
- }
1557
- /**
1558
- * #1629 r4-5c: an effect as this person may carry it away in an export — their own claim, or one a person shared —
1559
- * read with their principal (Sol's ACL). Being able to view or run a placed effect never grants packaging someone
1560
- * else's private claim: the library's reference read is runtime access, not an export path. Null: not theirs.
1561
- */
1562
- modPlacedEffect(person, id) {
1563
- const r = this.effectLibrary.read(id, { kind: "human", id: person });
1564
- return r.ok ? { name: r.entry.manifest.name, files: r.entry.files } : null;
1565
- }
1566
1423
  /** #1629 r4-5: an instance's card text as every viewer sees it (a person's fields over the agent's). */
1567
1424
  modCardText(name) {
1568
1425
  const card = resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card);
@@ -1585,8 +1442,7 @@ export class FleetManager {
1585
1442
  */
1586
1443
  get webSettings() {
1587
1444
  return (this.webSettingsToolsService ??= new WebSettingsTools({
1588
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1589
- prefs: this.webPrefs, ownerCss: this.ownerCss, mods: this.mods, effects: this.effectService,
1445
+ prefs: this.webPrefs, ownerCss: this.ownerCss,
1590
1446
  instances: () => [...Object.keys(this.fleetConfig?.instances ?? {}),
1591
1447
  ...(this.classicChannels?.getAll() ?? []).map((ch) => ch.instanceName).filter((n) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, n))],
1592
1448
  colour: (name) => personIdentityColour(this.dataDir, name),
@@ -1595,7 +1451,6 @@ export class FleetManager {
1595
1451
  bannerFocus: (name) => personBannerFocus(this.dataDir, name),
1596
1452
  setBannerFocus: (name, focus) => { const w = setPersonBannerFocus(this.dataDir, name, focus); if (w.ok)
1597
1453
  this.identityChanged(); return w; },
1598
- placements: (name) => this.themeStore.get(name).placements.effects.map((e) => (typeof e === "string" ? { id: e } : e)),
1599
1454
  ambient: (name) => Object.fromEntries(Object.entries(this.themeStore.get(name).placements.ambient).flatMap(([k, a]) => a ? [[k, { spec: a.spec, by: a.by }]] : [])),
1600
1455
  setAmbient: (name, spec, which) => this.webAppearance.humanSetAmbient(name, spec, which),
1601
1456
  announce: () => { try {
@@ -1909,7 +1764,7 @@ export class FleetManager {
1909
1764
  retire();
1910
1765
  continue;
1911
1766
  }
1912
- if (store.attachPrompt(view.id, retire))
1767
+ if (store.attachPrompt(view.id, retire, { platform: adapter.type, connection: adapterId }))
1913
1768
  return;
1914
1769
  }
1915
1770
  catch {
@@ -2436,12 +2291,10 @@ export class FleetManager {
2436
2291
  const notice = silent
2437
2292
  ? t(`delivery.fleet_${row.state}_silent_schedule`, row.targetInstance, row.operationId, row.deliveryId, row.lastError ?? "unknown")
2438
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.
2439
2296
  this.logger.error({ deliveryId: row.deliveryId, operationId: row.operationId, target: row.targetInstance, state: row.state,
2440
2297
  cause: row.lastError, ...(silent ? { silentSchedule: true } : {}) }, notice);
2441
- // A self-scheduled silent heartbeat stays log-only, including the
2442
- // operator alert; another source still receives the normal notice.
2443
- if (!silent || row.sourceInstance !== row.targetInstance)
2444
- this.notifyFleetError(notice);
2445
2298
  }
2446
2299
  }
2447
2300
  });
@@ -2473,8 +2326,9 @@ export class FleetManager {
2473
2326
  if (event.count)
2474
2327
  this.needsYou?.poke(); // #1386: rows that expired failed — they wait on someone now
2475
2328
  this.wakeCoordinator?.kick();
2329
+ // #2804: the expired rows are Needs-you items now; the count is logged, not posted to General.
2476
2330
  if (event.count)
2477
- 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));
2478
2332
  });
2479
2333
  this.scheduleDeliveryOutboxPump();
2480
2334
  }
@@ -2604,27 +2458,8 @@ export class FleetManager {
2604
2458
  const daemon = this.daemons.get(name);
2605
2459
  if (daemon?.bootId === daemonBootId)
2606
2460
  this.listenForWorkingState(name, daemon);
2607
- const outbox = this.deliveryOutbox;
2608
- if (!outbox)
2609
- return;
2610
- for (const prior of outbox.getUnansweredAccepted(name, daemonBootId)) {
2611
- try {
2612
- outbox.admitPostRestartOutcomeNotice(prior, daemonBootId);
2613
- }
2614
- catch (err) {
2615
- this.logger.error({ err, source: name, operationId: prior.operationId }, "Could not persist post-restart delivery outcome notice");
2616
- }
2617
- }
2618
- // #926: a restart kills the turn that was working on a request, and a
2619
- // resumed CLI does not pick it back up. Remind the owner once per restart.
2620
- try {
2621
- const now = new Date();
2622
- this.expireReplyObligations(now);
2623
- outbox.remindReplyObligations(name, { reason: "restart", graceMs: 0, now, since: now, restartId: daemonBootId });
2624
- }
2625
- catch (err) {
2626
- this.logger.error({ err, owner: name }, "Could not persist the post-restart reply reminder");
2627
- }
2461
+ // #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
2462
+ // request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
2628
2463
  this.scheduleDeliveryOutboxPump();
2629
2464
  }
2630
2465
  workingStateListeners;
@@ -2708,9 +2543,8 @@ export class FleetManager {
2708
2543
  }
2709
2544
  }
2710
2545
  /**
2711
- * #926: the two reply-obligation safety nets. Idle owners with an unanswered
2712
- * request get one reminder (after the grace); requesters get one overdue
2713
- * notice when the owner is not working and nothing came back in time.
2546
+ * #926: idle owners with an unanswered request get one reminder (after the grace). #2802: the only reply notice —
2547
+ * requesters are no longer told about overdue or expired asks (they can check delivery_status / describe_instance).
2714
2548
  */
2715
2549
  sweepReplyObligations(now = new Date()) {
2716
2550
  const outbox = this.deliveryOutbox;
@@ -2721,10 +2555,8 @@ export class FleetManager {
2721
2555
  this.expireReplyObligations(now);
2722
2556
  for (const name of this.daemons.keys()) {
2723
2557
  if (stateOf(name) === "idle")
2724
- outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS, reason: "turn-ended" });
2558
+ outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS });
2725
2559
  }
2726
- const minutes = this.fleetConfig?.defaults?.reply_overdue_minutes ?? DEFAULT_REPLY_OVERDUE_MINUTES;
2727
- outbox.notifyOverdueReplyObligations({ now, overdueMs: minutes * 60_000, ownerIdle: name => stateOf(name) !== "working" });
2728
2560
  }
2729
2561
  catch (err) {
2730
2562
  this.logger.error({ err }, "Reply-obligation sweep failed");
@@ -2981,7 +2813,9 @@ export class FleetManager {
2981
2813
  if ((current?.state === "delivering" || current?.state === "submission_started")
2982
2814
  && current.targetDaemonBootId === claimed.targetDaemonBootId
2983
2815
  && current.managerBootId === claimed.managerBootId) {
2984
- 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));
2985
2819
  }
2986
2820
  }, DURABLE_DELIVERY_LANE_ALERT_MS);
2987
2821
  alertTimer.unref?.();
@@ -3153,7 +2987,8 @@ export class FleetManager {
3153
2987
  // fleet.yaml, sent SIGHUP, and got no reaction and no explanation.
3154
2988
  this.logger.error({ err }, "SIGHUP config reload failed");
3155
2989
  const message = err instanceof Error ? err.message : String(err);
3156
- this.notifyFleetError(t("fleet.reload_failed", message));
2990
+ if (!(err instanceof ConfigSyntaxError))
2991
+ this.notifyFleetError(t("fleet.reload_failed", message));
3157
2992
  });
3158
2993
  }
3159
2994
  /**
@@ -3246,6 +3081,7 @@ export class FleetManager {
3246
3081
  if (this.reloadPending)
3247
3082
  this.scheduleReconcile();
3248
3083
  void this.sweepOrphanedCancelButtons();
3084
+ void this.parkedNotices.sweep();
3249
3085
  }
3250
3086
  /**
3251
3087
  * We are the post-update fleet: the update is over by definition. Clearing it here (rather than in the update
@@ -3389,19 +3225,19 @@ export class FleetManager {
3389
3225
  };
3390
3226
  }
3391
3227
  /** Load fleet.yaml and build routing table */
3392
- loadConfig(configPath) {
3228
+ loadConfig(configPath, options = {}) {
3393
3229
  this.configPath = configPath;
3394
- const source = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "{}\n";
3395
- this.rawFleetDocument = parseDocument(source, { keepSourceTokens: true });
3396
- if (this.rawFleetDocument.errors.length > 0) {
3397
- throw new Error(`Invalid fleet.yaml: ${this.rawFleetDocument.errors[0].message}`);
3398
- }
3399
- const raw = loadRawFleetConfig(configPath);
3400
- 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);
3401
3236
  this.assertProviderSecretEnvKeys(loaded);
3402
3237
  this.rawFleetConfig = raw;
3403
3238
  this.settingsRuntimeRevision++;
3404
3239
  this.fleetConfig = loaded;
3240
+ this.loadedFleetSource = selected;
3405
3241
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
3406
3242
  this.warnAboutRemovedLoginMode(loaded);
3407
3243
  this.enforceGuestSwitch();
@@ -3410,6 +3246,8 @@ export class FleetManager {
3410
3246
  // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
3411
3247
  process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
3412
3248
  process.env.AGEND_FLEET_CONFIG_PATH = resolvePath(configPath);
3249
+ if (options.remember !== false && existsSync(configPath))
3250
+ history.accepted("fleet.yaml", configPath, selected);
3413
3251
  return this.fleetConfig;
3414
3252
  }
3415
3253
  relayLoginModeWarned = false;
@@ -6694,10 +6532,12 @@ export class FleetManager {
6694
6532
  dataDir: this.dataDir,
6695
6533
  onWarn: message => this.logger.warn(message),
6696
6534
  // #1620: fleet.yaml's web.session.local, read at every check — a Settings save or a reload applies at once
6697
- // (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.
6698
6536
  localPolicy: () => localSessionPolicy(this.fleetConfig?.web?.session?.local),
6699
6537
  // #1920 §5.4: a guest's, from web.guests.session (never longer than the surface's own).
6700
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,
6701
6541
  });
6702
6542
  this.enforceGuestSwitch(); // #1920: guests off while the fleet was down
6703
6543
  }
@@ -6746,6 +6586,32 @@ export class FleetManager {
6746
6586
  return this.previewTunnel ??= new ManagedTunnel({ dataDir: join(this.dataDir, PUBLIC_PREVIEW_TUNNEL_DIR),
6747
6587
  log: () => this.logger.warn("Public preview tunnel cleanup requires attention") });
6748
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
+ }
6749
6615
  getTunnelLane() {
6750
6616
  return this.tunnelPurposeLane ??= new TunnelPurposeLane(new ManagedTunnel({ dataDir: this.dataDir,
6751
6617
  log: () => this.logger.warn("Managed tunnel cleanup requires attention") }));
@@ -7121,7 +6987,8 @@ export class FleetManager {
7121
6987
  : "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
7122
6988
  + (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : ""));
7123
6989
  const choices = [{ action: "local", label: t("dashboard.local_signin") },
7124
- ...(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") }] : []),
7125
6992
  ...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
7126
6993
  const posted = await this.postNonceButtonPrompt({
7127
6994
  prefix: "dashboard:", alertType: "login", instanceName: "dashboard", adapter, adapterId: owner.adapterId,
@@ -7341,7 +7208,7 @@ export class FleetManager {
7341
7208
  return { ...a, name: badge.name, platform: badge.platform, scope: [...this.guestScope(owner)].sort(), reach: [...this.guestReach(owner)].sort(),
7342
7209
  devices: devices.get(k) ?? 0, narrow: own(cfg?.narrow, `${a.adapterId}:${a.userId}`) ?? null,
7343
7210
  // #2348: the grant as fleet.yaml has it (null: none), and the last code an admin sent from Settings.
7344
- 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,
7345
7212
  ...(sent ? { code: { at: sent.at, outcome: sent.outcome.kind, ...(sent.outcome.kind === "sent" ? { expiresAt: sent.at + sent.outcome.expiresInMs } : {}),
7346
7213
  ...(sent.outcome.kind === "dm_failed" ? { reason: sent.outcome.reason, platform: sent.outcome.platform, ...(sent.outcome.startUrl ? { startUrl: sent.outcome.startUrl } : {}) } : {}) } } : {}) };
7347
7214
  }).sort((x, y) => (y.lastSeen ?? -1) - (x.lastSeen ?? -1));
@@ -7389,7 +7256,7 @@ export class FleetManager {
7389
7256
  const when = Date.parse(r.created_at.includes("T") ? r.created_at : `${r.created_at.replace(" ", "T")}Z`);
7390
7257
  return r.event_type === "guest_grant_changed"
7391
7258
  ? [{ kind: "grant", at: when, account, name, via: p.via === "confirmed" ? "confirmed" : "direct", added: names(p.added), removed: names(p.removed),
7392
- 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 } : {}) }]
7393
7260
  : [{ kind: "code", at: when, account, name, outcome: typeof p.outcome === "string" ? p.outcome : "unknown" }];
7394
7261
  });
7395
7262
  }
@@ -7428,7 +7295,7 @@ export class FleetManager {
7428
7295
  * confirmed in General, the agents added and taken away, and the sessions a removal ended. Never message content.
7429
7296
  */
7430
7297
  noteGuestGrantChange(change) {
7431
- 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");
7432
7299
  try {
7433
7300
  this.eventLog?.insert("fleet", "guest_grant_changed", { ...change });
7434
7301
  }
@@ -7513,6 +7380,8 @@ export class FleetManager {
7513
7380
  * #1920 §5.4: a guest's own `/web revoke` — its sessions and its outstanding code; nobody else's (an admin's revokes
7514
7381
  * everyone's, as before).
7515
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)); }
7516
7385
  revokeGuestWebSessions(owner) {
7517
7386
  this.initializeWebSessions();
7518
7387
  const key = `${owner.adapterId}\u0000${owner.userId}`;
@@ -8170,7 +8039,7 @@ export class FleetManager {
8170
8039
  setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
8171
8040
  if (this.watchdogTimer)
8172
8041
  clearInterval(this.watchdogTimer);
8173
- this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
8042
+ this.watchdogTimer = startSdWatchdog();
8174
8043
  this.eventLoopWatch?.stop();
8175
8044
  this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
8176
8045
  try {
@@ -8219,7 +8088,7 @@ export class FleetManager {
8219
8088
  this.tmuxExecHelperTimer ??= setInterval(() => tmuxExecHelper.maintain(), 5_000);
8220
8089
  this.tmuxExecHelperTimer.unref?.();
8221
8090
  this.rotateFleetLogs();
8222
- const fleet = this.loadConfig(configPath);
8091
+ const fleet = this.loadConfig(configPath, { startup: true });
8223
8092
  this.slimFleetConfigAtStartup();
8224
8093
  this.logConfigWarningsAtStartup();
8225
8094
  setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
@@ -8297,7 +8166,7 @@ export class FleetManager {
8297
8166
  // migrates legacy single-bot entries and names without a suffix. Classic
8298
8167
  // routing does NOT go through the routing engine (single-key, can't hold two
8299
8168
  // bots in one channel) — it resolves per-bot via getInstanceByChannel.
8300
- this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger);
8169
+ this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger, this.getConfigHistory());
8301
8170
  const classicAdapters = fleet.channels?.length ? fleet.channels : (fleet.channel ? [fleet.channel] : []);
8302
8171
  this.classicChannels.configureAdapters(classicAdapters);
8303
8172
  // The unrecoverable-id report is deliberately NOT sent here: no adapter
@@ -8315,6 +8184,9 @@ export class FleetManager {
8315
8184
  // rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
8316
8185
  this.restoreWebChat(fleet);
8317
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?.();
8318
8190
  // Poll classicBot.yaml for external changes every 30s
8319
8191
  this.classicReloadTimer = setInterval(() => {
8320
8192
  // Attributes the synchronous part, up to the reload's first await (#1235).
@@ -8346,7 +8218,8 @@ export class FleetManager {
8346
8218
  ...fleet.defaults.daily_summary,
8347
8219
  };
8348
8220
  this.dailySummary = new DailySummary(summaryConfig, costGuardConfig.timezone, (text) => {
8349
- this.postDailySummary(text);
8221
+ if (text !== null)
8222
+ this.postDailySummary(text);
8350
8223
  // Rotate classic channel chat logs daily
8351
8224
  this.classicChannels?.rotateLogs();
8352
8225
  this.rotateInboxes();
@@ -8364,17 +8237,6 @@ export class FleetManager {
8364
8237
  return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
8365
8238
  });
8366
8239
  this.dailySummary.start();
8367
- // #2246 step 3: the task board's daily summary — its own time (defaults.task_board.summary_at, read at every look),
8368
- // in the fleet's zone; a summary missed while the fleet was down goes out when it is back.
8369
- this.taskBoardSummary = new TaskBoardSummaryClock({
8370
- at: () => summaryTime(this.fleetConfig?.defaults?.task_board?.summary_at) ?? null,
8371
- zone: () => this.usageZone(),
8372
- now: () => new Date(),
8373
- mark: summaryMarkFile(join(this.dataDir, "task-board-summary.json")),
8374
- send: (since, until) => this.postTaskBoardSummary(since, until),
8375
- failed: (err) => this.logger.warn({ err }, "The task board's daily summary failed; the next minute tries again"),
8376
- });
8377
- this.taskBoardSummary.start();
8378
8240
  // Rotate classic channel chat logs daily (piggyback on daily summary timer)
8379
8241
  this.classicChannels?.rotateLogs();
8380
8242
  this.rotateInboxes();
@@ -8463,7 +8325,7 @@ export class FleetManager {
8463
8325
  fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
8464
8326
  generalsCreated = true;
8465
8327
  }
8466
- if (generalsCreated)
8328
+ if (generalsCreated && !this.configHistory?.isFallback(this.configPath))
8467
8329
  this.saveFleetConfig();
8468
8330
  if (topicMode && (fleet.channel || fleet.channels?.length)) {
8469
8331
  const schedulerConfig = {
@@ -8639,7 +8501,7 @@ export class FleetManager {
8639
8501
  fixedGeneral = true;
8640
8502
  }
8641
8503
  }
8642
- if (fixedGeneral)
8504
+ if (fixedGeneral && !this.configHistory?.isFallback(this.configPath))
8643
8505
  this.saveFleetConfig();
8644
8506
  // Auto-create topics AFTER adapter is ready (needs adapter.createTopic)
8645
8507
  await this.topicCommands.autoCreateTopics();
@@ -8734,8 +8596,6 @@ export class FleetManager {
8734
8596
  }
8735
8597
  await this.sendFleetStartCompletionFallback(String(fleet.channel.group_id), text, generalThreadId != null ? String(generalThreadId) : undefined);
8736
8598
  }
8737
- // After "fleet ready", so the notice is not the first thing people see of a restart.
8738
- void this.announceWebChatOnce(agendVersion);
8739
8599
  // #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
8740
8600
  this.startPublicLinkAutoOpen();
8741
8601
  }
@@ -10415,6 +10275,11 @@ export class FleetManager {
10415
10275
  }
10416
10276
  return;
10417
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;
10418
10283
  // Handle /stop command
10419
10284
  if (text === "/stop" || text.startsWith("/stop ")) {
10420
10285
  if (!this.classicChannels.isAdmin(msg.userId)) {
@@ -10870,34 +10735,6 @@ export class FleetManager {
10870
10735
  * their fleet is organised. Both stay theirs — this only makes sure they are
10871
10736
  * not discovered by an agent failing at three in the morning.
10872
10737
  */
10873
- /**
10874
- * #1366: the first time a fleet runs 2.2 or later, tell each chat platform's General — once — that its agents can
10875
- * now be talked to from a browser. Claimed in upgrade-notices.json before the send, released if the send fails.
10876
- * Platforms with nowhere to post fleet notices (no group, or a Discord fleet with no General channel) are skipped
10877
- * and not recorded, so they are told once they have one.
10878
- */
10879
- async announceWebChatOnce(version) {
10880
- if (!hasWebChat(version))
10881
- return;
10882
- const path = upgradeNoticesPath(this.dataDir);
10883
- for (const [adapterId, world] of this.worlds) {
10884
- if (this.shuttingDown)
10885
- return;
10886
- const target = this.fleetNoticeTarget(adapterId);
10887
- if (!target)
10888
- continue;
10889
- if (!claimNotice(path, WEB_CHAT_NOTICE, adapterId))
10890
- continue;
10891
- try {
10892
- await world.adapter.sendText(target.chatId, t("upgrade.web_chat"), target.opts);
10893
- this.logger.info({ adapterId, notice: WEB_CHAT_NOTICE }, "Announced web chat in General");
10894
- }
10895
- catch (err) {
10896
- const released = releaseNotice(path, WEB_CHAT_NOTICE, adapterId);
10897
- this.logger.warn({ err, adapterId, released }, "Could not announce web chat in General — will try at the next start");
10898
- }
10899
- }
10900
- }
10901
10738
  announceToolPermissionsChange() {
10902
10739
  try {
10903
10740
  const thirtyDaysAgo = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 19).replace("T", " ");
@@ -10917,8 +10754,8 @@ export class FleetManager {
10917
10754
  }
10918
10755
  }
10919
10756
  checkToolPermission(sink, instanceName, tool) {
10920
- if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
10921
- return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
10757
+ if (isCanvasWorkerTool(tool))
10758
+ return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }; // #2799: removed
10922
10759
  const profile = resolveToolSet(this.fleetConfig?.instances[instanceName], instanceName);
10923
10760
  const allowed = mayUseTool(profile, tool);
10924
10761
  if (!allowed) {
@@ -11651,19 +11488,6 @@ export class FleetManager {
11651
11488
  }
11652
11489
  dispatchTypedIpc(name, msg) {
11653
11490
  const type = String(msg.type);
11654
- if (type.startsWith("fleet_") && WEB_EFFECT_TOOLS.has(type.slice(6))) {
11655
- const ipc = this.instanceIpcClients.get(name), fleetRequestId = msg.fleetRequestId;
11656
- if (!ipc || typeof fleetRequestId !== "string")
11657
- return;
11658
- void this.handleWebThemeHttp(name, type.slice(6), (msg.payload ?? {})).then(result => {
11659
- if (this.instanceIpcClients.get(name) === ipc)
11660
- ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
11661
- }, err => {
11662
- if (this.instanceIpcClients.get(name) === ipc)
11663
- ipc.send({ type: "fleet_web_theme_response", fleetRequestId, error: err instanceof Error ? err.message : "Effect operation failed" });
11664
- }).catch(err => this.logger.debug({ err }, "Effect tool response could not be written"));
11665
- return;
11666
- }
11667
11491
  if (type.startsWith("fleet_schedule_")) {
11668
11492
  this.handleScheduleCrud(name, msg);
11669
11493
  return;
@@ -11717,10 +11541,9 @@ export class FleetManager {
11717
11541
  const args = (msg.payload ?? {});
11718
11542
  const result = tool === "get_web_appearance" || tool === "preview_web_appearance"
11719
11543
  ? this.appearanceTools.handle(instanceName, tool, args)
11720
- : tool === "export_web_mod" || tool === "propose_web_mod" ? this.agentMods.handle(instanceName, tool, args)
11721
- : tool === "office_view" || tool === "office_set" ? this.officeActs.handle(instanceName, tool, args)
11722
- : OFFICE_PACK_IPC.has(`fleet_${tool}`) ? this.officePackTools.handle(instanceName, tool, args)
11723
- : 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);
11724
11547
  ipc.send({ type: "fleet_web_theme_response", fleetRequestId, result });
11725
11548
  }
11726
11549
  catch (err) {
@@ -12145,17 +11968,10 @@ export class FleetManager {
12145
11968
  }
12146
11969
  /** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
12147
11970
  async handleWebThemeHttp(instance, tool, args) {
12148
- if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
12149
- return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
12150
- if (WEB_EFFECT_TOOLS.has(tool)) {
12151
- if (!this.isKnownInstance(instance))
12152
- return { error: "unknown_instance" };
12153
- return this.effectTools.handle(instance, tool, args);
12154
- }
11971
+ if (isCanvasWorkerTool(tool))
11972
+ return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }; // #2799: removed
12155
11973
  if (tool === "get_web_appearance" || tool === "preview_web_appearance")
12156
11974
  return this.appearanceTools.handle(instance, tool, args);
12157
- if (tool === "export_web_mod" || tool === "propose_web_mod")
12158
- return this.agentMods.handle(instance, tool, args);
12159
11975
  if (tool === "office_view" || tool === "office_set")
12160
11976
  return this.officeActs.handle(instance, tool, args); // #2578
12161
11977
  if (OFFICE_PACK_IPC.has(`fleet_${tool}`))
@@ -12696,6 +12512,7 @@ export class FleetManager {
12696
12512
  },
12697
12513
  });
12698
12514
  this.rawFleetConfig = loadRawFleetConfig(this.configPath);
12515
+ this.configHistory?.accepted("fleet.yaml", this.configPath, { source: output, fallback: false });
12699
12516
  // The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
12700
12517
  // leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
12701
12518
  // Keep the FleetConfig object itself: Settings and other request contexts hold it.
@@ -12774,6 +12591,8 @@ export class FleetManager {
12774
12591
  }
12775
12592
  /** One-time upgrade migration; invalid YAML is never rewritten. */
12776
12593
  migrateNoticeDeliveryStorage() {
12594
+ if (this.configHistory?.isFallback(this.configPath))
12595
+ return;
12777
12596
  if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
12778
12597
  || this.fleetConfig.web?.notice_delivery !== undefined)
12779
12598
  && validateFleetConfig(this.rawFleetConfig).valid) {
@@ -12788,6 +12607,8 @@ export class FleetManager {
12788
12607
  }
12789
12608
  }
12790
12609
  migrateOfficeStorage() {
12610
+ if (this.configHistory?.isFallback(this.configPath))
12611
+ return;
12791
12612
  if (!this.fleetConfig || !this.configPath || !hasLegacyOffice(this.rawFleetConfig) || !validateFleetConfig(this.rawFleetConfig).valid)
12792
12613
  return;
12793
12614
  try {
@@ -12811,6 +12632,8 @@ export class FleetManager {
12811
12632
  }
12812
12633
  }
12813
12634
  slimFleetConfigAtStartup() {
12635
+ if (this.configHistory?.isFallback(this.configPath))
12636
+ return;
12814
12637
  this.migrateOfficeStorage();
12815
12638
  this.migrateNoticeDeliveryStorage();
12816
12639
  const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
@@ -13032,9 +12855,7 @@ export class FleetManager {
13032
12855
  this.themeStore.forget(name);
13033
12856
  this.webAppearance.forget(name);
13034
12857
  this.appearanceTools.forget(name);
13035
- this.effectToolsService?.forget(name);
13036
12858
  this.mods.forgetInstance(name);
13037
- this.agentMods.forget(name);
13038
12859
  this.officeActsInstance?.forget(name);
13039
12860
  this.officePackToolsInstance?.forget(name);
13040
12861
  }
@@ -14392,51 +14213,6 @@ export class FleetManager {
14392
14213
  }
14393
14214
  return { chatId, opts: {} };
14394
14215
  }
14395
- /**
14396
- * #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
14397
- * gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
14398
- * one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
14399
- * nothing. A web-only fleet: everything, in the web chat.
14400
- */
14401
- postTaskBoardSummary(since, until) {
14402
- const board = this.scheduler?.db.board;
14403
- if (!board)
14404
- return;
14405
- const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
14406
- const projects = board.listProjects(true);
14407
- const stats = board.projectStats(this.dispatcherGone);
14408
- const names = new Map(projects.map((p) => [p.id, p.name]));
14409
- const configured = this.getPrimaryAdapterId();
14410
- const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
14411
- const worldOf = (assignee) => {
14412
- const owner = assignee ? this.getInstanceAdapterId(assignee) : undefined;
14413
- return owner !== undefined && this.worlds.has(owner) ? owner : primary;
14414
- };
14415
- const zone = this.usageZone();
14416
- for (const world of this.worlds.size ? [...this.worlds.keys()] : [undefined]) {
14417
- const summary = buildSummary(read, since, until, { inChat: (assignee) => worldOf(assignee) === world, projects, stats });
14418
- if (isQuiet(summary))
14419
- continue;
14420
- const text = renderSummary(summary, { zone, t: (key, ...args) => t(key, ...args), projectOf: (c) => (c.projectId === null ? null : names.get(c.projectId) ?? null) });
14421
- const cards = [...new Set([...summary.review, ...summary.blocked, ...summary.done, ...summary.created].map((c) => c.code))];
14422
- if (world !== undefined) {
14423
- const adapter = this.worlds.get(world)?.adapter;
14424
- const target = this.fleetNoticeTarget(world);
14425
- if (adapter && target) {
14426
- adapter.sendText(target.chatId, text, target.opts).catch(e => this.logger.warn({ err: e, world }, "Failed to send the task board's daily summary"));
14427
- }
14428
- else {
14429
- this.logger.warn({ world }, "The task board's daily summary has no postable target — set a General topic or channel.options.general_channel_id");
14430
- }
14431
- }
14432
- for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
14433
- if (cfg.general_topic !== true || (world !== undefined && this.getInstanceAdapterId(name) !== world))
14434
- continue;
14435
- // #2341 review: with the cards it names, so the public link withholds it while one of them is hidden there.
14436
- this.emitSseEvent("message", { instance: name, sender: name, role: "status", text, ts: new Date().toISOString(), cards });
14437
- }
14438
- }
14439
- }
14440
14216
  /**
14441
14217
  * Post the daily summary where fleet-wide notices go.
14442
14218
  *
@@ -14532,11 +14308,22 @@ export class FleetManager {
14532
14308
  * would have to make this async and change every caller.
14533
14309
  */
14534
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) {
14535
14322
  const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
14536
14323
  if (!adapter) {
14537
14324
  // Early startup: adapters are created well after the fleet object exists.
14538
14325
  this.logger.warn({ instanceName }, "No adapter yet — instance topic notification not sent");
14539
- return false;
14326
+ return null;
14540
14327
  }
14541
14328
  const channelCfg = this.getChannelConfig(this.getInstanceAdapterId(instanceName));
14542
14329
  const groupId = channelCfg?.group_id;
@@ -14545,17 +14332,13 @@ export class FleetManager {
14545
14332
  const worldId = this.getInstanceAdapterId(instanceName);
14546
14333
  if (threadId != null && groupId) {
14547
14334
  const out = this.renderPlaces(worldId, text, { threadId: String(threadId), ...extraOpts });
14548
- adapter.sendText(String(groupId), out.text, out.opts)
14549
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
14550
- return true;
14335
+ return { adapterId: worldId, sent: adapter.sendText(String(groupId), out.text, out.opts), failure: "Failed to send instance topic notification" };
14551
14336
  }
14552
14337
  // Classic instance: find its channelId from the classic manager
14553
14338
  const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
14554
14339
  if (classicChatId) {
14555
14340
  // #1990: a ClassicBot room's readers need not be in the fleet's group, so its notices name places as plain text.
14556
- adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts)
14557
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
14558
- return true;
14341
+ return { adapterId: worldId, sent: adapter.sendText(classicChatId, stripPlaceRefs(text), extraOpts), failure: "Failed to send classic notification" };
14559
14342
  }
14560
14343
  // Fallback: the instance has neither a topic nor a classic channel, so post
14561
14344
  // where fleet-wide notices go. Not the bare group id: on Discord that is a
@@ -14563,12 +14346,67 @@ export class FleetManager {
14563
14346
  const target = this.fleetNoticeTarget(worldId);
14564
14347
  if (target) {
14565
14348
  const out = this.renderPlaces(worldId, text, { ...target.opts, ...extraOpts });
14566
- adapter.sendText(target.chatId, out.text, out.opts)
14567
- .catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
14568
- return true;
14349
+ return { adapterId: worldId, sent: adapter.sendText(target.chatId, out.text, out.opts), failure: "Failed to send notification (no topic)" };
14569
14350
  }
14570
14351
  this.logger.warn({ instanceName }, "No postable target — instance topic notification not sent");
14571
- 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 };
14572
14410
  }
14573
14411
  // ── Nonce-armed button prompts (hang / assist / exit / clear) ──
14574
14412
  //
@@ -15146,6 +14984,7 @@ export class FleetManager {
15146
14984
  pauseReason, pausedAt,
15147
14985
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
15148
14986
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
14987
+ usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
15149
14988
  });
15150
14989
  }
15151
14990
  return out;
@@ -15219,6 +15058,9 @@ export class FleetManager {
15219
15058
  },
15220
15059
  tasks: () => this.needsYouTaskItems(),
15221
15060
  backendSignIns: () => this.needsYouBackendSignIns(),
15061
+ configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
15062
+ ...issue, detail: configIssueText(issue, t), owner: this.worlds.keys().next().value,
15063
+ })),
15222
15064
  taskAction: action => this.taskActionFromChat(action),
15223
15065
  settings: () => ({
15224
15066
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
@@ -15968,6 +15810,16 @@ export class FleetManager {
15968
15810
  return result;
15969
15811
  }
15970
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"; }
15971
15823
  webCommand(input, opts) {
15972
15824
  const webMeta = (instance) => {
15973
15825
  const topicId = this.fleetConfig?.instances[instance]?.topic_id;
@@ -15976,6 +15828,7 @@ export class FleetManager {
15976
15828
  chatId: this.getGroupIdForInstance(instance) || "", messageId: newWebMessageId(),
15977
15829
  username: opts.person?.name ?? WEB_NAME_FALLBACK, userId: opts.person?.userId ?? WEB_NAME_FALLBACK,
15978
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
15979
15832
  };
15980
15833
  };
15981
15834
  const choicesFromCache = (instance) => {
@@ -15987,13 +15840,8 @@ export class FleetManager {
15987
15840
  return { current, options: this.withCurrentModel(cached.models, current).map(o => ({ id: o.id, label: this.modelChoiceLabel(o, current) })) };
15988
15841
  };
15989
15842
  return runWebCommand({
15990
- // Own entries only: a name like "__proto__" or "constructor" must not be found on the prototype (#1476 review).
15991
- scope: (instance) => {
15992
- const instances = this.fleetConfig?.instances;
15993
- if (instances && Object.prototype.hasOwnProperty.call(instances, instance))
15994
- return isGeneralInstance(this.fleetConfig, instance) ? "general" : "fleet";
15995
- return this.classicChannels?.getChannelIdByInstance(instance) !== undefined ? "classic" : null;
15996
- },
15843
+ scope: (instance) => this.webCommandScope(instance),
15844
+ platform: (instance) => this.commandPlatform(instance),
15997
15845
  ctx: (instance) => this.topicCommands.getCtxText(instance),
15998
15846
  // Board 003648ac: a watch for the paste, armed when the daemon says it was written (raw_paste_written); one
15999
15847
  // for a command that was not handed over is dropped.
@@ -17670,6 +17518,13 @@ export class FleetManager {
17670
17518
  lines.push(t(admin === "added" ? "classic.approve_done_admin" : "classic.approve_done_admin_already", adminUserId ?? ""));
17671
17519
  }
17672
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
+ }
17673
17528
  await this.notifyClassicRequester(entry, "classic.request_allowed");
17674
17529
  }
17675
17530
  async notifyClassicRequester(entry, key) {
@@ -18269,14 +18124,6 @@ export class FleetManager {
18269
18124
  const room = this.classicChannels?.getAll().find(c => c.instanceName === name);
18270
18125
  return room ? `room:${room.channelId}:${room.adapterId ?? ""}` : null;
18271
18126
  }
18272
- /** The only Try-card publisher: no arbitrary SSE sender/role can manufacture this metadata. */
18273
- publishEffectTrial(instance, offer) {
18274
- const row = this.webChatHistory.recordEffectTrial(instance, offer, new Date().toISOString());
18275
- if (!row)
18276
- return false;
18277
- broadcastSseEvent(this.sseClients, "message", row, err => this.logger.debug({ err }, "SSE client write failed; evicting"), this.webChatHistory.cursorOf(row));
18278
- return true;
18279
- }
18280
18127
  /** Push an SSE event to all connected Web UI clients. */
18281
18128
  emitSseEvent(event, data) {
18282
18129
  // #1386: a prompt opened or closed, or an instance's state moved — "Needs you" may have changed.
@@ -18321,8 +18168,8 @@ export class FleetManager {
18321
18168
  * The task board's live side: every committed change goes to the open pages (never to a guest) — the board's new
18322
18169
  * revision and the card numbers it touched; not who touched them (the page does not need it, and a public-link page
18323
18170
  * must not learn a hidden instance's name from it). Its news goes to the instances whose cards someone else changed:
18324
- * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken
18325
- * (task-board-waker.ts).
18171
+ * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken, or
18172
+ * withdrawn once its cards changed again (#2801; task-board-waker.ts).
18326
18173
  */
18327
18174
  attachTaskBoard(db) {
18328
18175
  this.taskBoardWaker?.stop();
@@ -18331,6 +18178,19 @@ export class FleetManager {
18331
18178
  const waker = this.taskBoardWaker = new TaskBoardWaker({
18332
18179
  deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
18333
18180
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
18181
+ // #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
18182
+ messagedAbout: (from, to, codes, sinceMs) => {
18183
+ const outbox = this.deliveryOutbox;
18184
+ if (!outbox?.isOpen)
18185
+ return false;
18186
+ return outbox.recentTextsFrom(to, from, new Date(sinceMs).toISOString()).some((text) => codes.some((code) => namesCard(text, code)));
18187
+ },
18188
+ // #2801: a notice still waiting whose cards have all changed again is not delivered.
18189
+ withdraw: (prefix, stale, reason) => {
18190
+ const outbox = this.deliveryOutbox;
18191
+ return outbox?.isOpen ? outbox.withdrawWaitingNotices(prefix, stale, reason).length : 0;
18192
+ },
18193
+ changedAt: (uuid) => db.board.lastChangeAt(uuid),
18334
18194
  holds: (wake) => db.board.wakeHolds(wake),
18335
18195
  card: (uuid) => db.board.findCard(uuid),
18336
18196
  // #2352: the card's Needs you buttons are in this instance's own channel (it is the card's lane).
@@ -18347,12 +18207,13 @@ export class FleetManager {
18347
18207
  },
18348
18208
  log: (level, message, extra) => this.logger[level](extra ?? {}, message),
18349
18209
  });
18350
- db.board.onChange(({ rev, ids, projects, wakes }) => {
18210
+ db.board.onChange(({ rev, ids, projects, wakes, changed }) => {
18351
18211
  // Refresh both old and new owners on reassignment; wakes are not an ownership index.
18352
18212
  if (ids.length)
18353
18213
  for (const name of this.daemons.keys())
18354
18214
  this.observeWorkingState(name, "tasks");
18355
18215
  this.emitSseEvent("tasks", { rev, ids, projects });
18216
+ waker.changed(changed);
18356
18217
  waker.note(wakes);
18357
18218
  // step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
18358
18219
  if (ids.length || projects)
@@ -18570,27 +18431,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
18570
18431
  this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
18571
18432
  this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
18572
18433
  }
18573
- /** Update only AgEnD-managed skill copies; no launch or restart is needed for an experimental switch. */
18574
- canvasWorkerSettingChanged() {
18575
- this.refreshExperimentalSkills();
18576
- this.emitSseEvent("status", this.getUiStatus());
18577
- }
18578
- refreshExperimentalSkills() {
18579
- for (const [name, daemon] of this.daemons) {
18580
- try {
18581
- const cfg = daemon.getConfigSnapshot();
18582
- const backend = cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
18583
- const workDir = this.resolveKnowledgeWorkDir(cfg.working_directory, backend, name);
18584
- const classic = this.classicChannels?.getAll().some(ch => ch.instanceName === name) === true;
18585
- if (classic && !insideManagedWorkspaces(workDir))
18586
- continue;
18587
- this.syncRoleSkills(workDir, backend, classic ? "classic" : cfg.general_topic ? "general" : "worker");
18588
- }
18589
- catch (err) {
18590
- this.logger.warn({ name, err }, "Could not refresh experimental skill visibility");
18591
- }
18592
- }
18593
- }
18594
18434
  /** Publish only the bundled skills eligible for an instance role. */
18595
18435
  syncRoleSkills(workDir, backend, role, knowledgeDir) {
18596
18436
  const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
@@ -18795,7 +18635,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
18795
18635
  continue;
18796
18636
  }
18797
18637
  managed.push(name);
18798
- const newContent = bundledSkillContents(join(srcSkills, name), this.canvasWorkerEnabled());
18638
+ const newContent = bundledSkillContents(join(srcSkills, name));
18799
18639
  if (refuse(dest))
18800
18640
  continue;
18801
18641
  try {
@@ -20953,6 +20793,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20953
20793
  return (adapterId ? generals.find(([name, cfg]) => (cfg.channel_id ?? this.getInstanceAdapterId(name)) === adapterId) : undefined)?.[0]
20954
20794
  ?? generals[0]?.[0];
20955
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
+ }
20956
20807
  /** Return a user-facing blocker without mutating ClassicBot state. */
20957
20808
  validateClassicStart(channelId, userId, guildId, adapterId) {
20958
20809
  const classic = this.classicChannels;
@@ -21235,7 +21086,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21235
21086
  try {
21236
21087
  this.themeStore.forget(ch.instanceName);
21237
21088
  this.webAppearance.limiter.forget(ch.instanceName);
21238
- this.effectToolsService?.forget(ch.instanceName);
21239
21089
  }
21240
21090
  catch (err) {
21241
21091
  this.logger.warn({ err: err.message, instanceName: ch.instanceName }, "Dropping a removed room's web theme failed");
@@ -21344,6 +21194,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21344
21194
  const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
21345
21195
  // #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
21346
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;
21347
21200
  const needsStopped = this.stopNeedsYou();
21348
21201
  // #1490 P3: stop() now returns a Promise; await it so the final
21349
21202
  // ledger flush completes before process.exit(0) in doStopAll's caller.
@@ -21399,7 +21252,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21399
21252
  this.clearStatuslineWatchers();
21400
21253
  this.costGuard?.stop();
21401
21254
  this.dailySummary?.stop();
21402
- this.taskBoardSummary?.stop();
21403
21255
  this.dailyTipScheduler?.stop();
21404
21256
  this.dailyTipScheduler = null;
21405
21257
  if (this.updateCheckTimer) {
@@ -21455,6 +21307,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21455
21307
  for (const timer of this.cancelButtonIdleRetireTimers.values())
21456
21308
  clearTimeout(timer);
21457
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();
21458
21312
  if (this.topicCleanupTimer) {
21459
21313
  clearInterval(this.topicCleanupTimer);
21460
21314
  this.topicCleanupTimer = null;
@@ -21690,14 +21544,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21690
21544
  const previousRawConfig = this.rawFleetConfig;
21691
21545
  const previousRawDocument = this.rawFleetDocument;
21692
21546
  const previousSavedSnapshot = this.savedFleetConfigSnapshot;
21547
+ const previousSource = this.loadedFleetSource;
21693
21548
  try {
21694
- this.loadConfig(this.configPath);
21549
+ this.loadConfig(this.configPath, { remember: false });
21695
21550
  }
21696
21551
  catch (err) {
21697
21552
  this.fleetConfig = oldConfig;
21698
21553
  this.rawFleetConfig = previousRawConfig;
21699
21554
  this.rawFleetDocument = previousRawDocument;
21700
21555
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
21556
+ this.loadedFleetSource = previousSource;
21701
21557
  throw err;
21702
21558
  }
21703
21559
  const validation = validateFleetConfig(this.rawFleetConfig);
@@ -21713,6 +21569,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21713
21569
  this.rawFleetConfig = previousRawConfig;
21714
21570
  this.rawFleetDocument = previousRawDocument;
21715
21571
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
21572
+ this.loadedFleetSource = previousSource;
21716
21573
  this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
21717
21574
  this.logger.error({
21718
21575
  oldCount,
@@ -21733,11 +21590,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21733
21590
  // mark every row done and tell the user "changes applied".
21734
21591
  return { rejected: why };
21735
21592
  }
21593
+ if (this.loadedFleetSource)
21594
+ this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
21736
21595
  this.migrateOfficeStorage();
21737
21596
  this.migrateNoticeDeliveryStorage();
21738
- if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
21739
- this.canvasWorkerSettingChanged();
21740
- }
21741
21597
  // Classic behavior settings share the fleet defaults but are not entries
21742
21598
  // in fleet.yaml. Snapshot the old effective chain before reloading the
21743
21599
  // Classic file so SIGHUP can hot-apply either source without waiting for
@@ -21947,7 +21803,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21947
21803
  * the panel shows the mismatch instead of a button.
21948
21804
  */
21949
21805
  checkStartupSignatureConsistency() {
21950
- if (!this.configPath) {
21806
+ if (!this.configPath || this.configHistory?.isFallback(this.configPath)) {
21951
21807
  this.fleetSignatureMismatch = null;
21952
21808
  return;
21953
21809
  }
@@ -23506,7 +23362,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23506
23362
  // From the install's own channel tag, this is what /update installs — even a stable the release pointed the
23507
23363
  // tag at (2.2.0 on @alpha / @beta): the plain /update text, never "/update stays on … use --stable" (#1928).
23508
23364
  const key = fromChannel ? "update.available_current" : updateNoticeKey(currentVersion, target);
23509
- this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
23365
+ // #2775: null = a beta install and a stable target; beta users are never told to switch.
23366
+ if (key)
23367
+ this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
23510
23368
  }
23511
23369
  }
23512
23370
  }
@@ -23602,6 +23460,21 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23602
23460
  }
23603
23461
  return scope.size ? scope : NO_GUEST_SCOPE;
23604
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
+ }
23605
23478
  /**
23606
23479
  * #1920 (#2104 review): what an account reaches before an admin's narrowing — its connection's topics (an open one's
23607
23480
  * named agents), its own Telegram private room, its verified group rooms. Narrowing can only take from this set, so it
@@ -23780,13 +23653,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23780
23653
  return;
23781
23654
  }
23782
23655
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
23783
- if (!this.canvasWorkerEnabled() && isCanvasWorkerHttpPath(requestPath)) {
23784
- res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
23785
- res.end(JSON.stringify({ error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }));
23786
- return;
23787
- }
23788
- if (this.effectBootstrap.handle(req, res))
23789
- return;
23790
23656
  // Branding is public, after Host admission: automatic favicon requests
23791
23657
  // must not become noisy web-token 401s in the browser console.
23792
23658
  // #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
@@ -23794,8 +23660,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23794
23660
  if (handleWebIconRequest(req, res, requestPath))
23795
23661
  return;
23796
23662
  // Public: the health probe, /agent (instance-token auth of its own), the
23797
- // sign-in surface, and /view's reads unless web.view_access says otherwise.
23798
- 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))) {
23799
23666
  // fall through to the handlers below
23800
23667
  }
23801
23668
  else {
@@ -23861,7 +23728,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23861
23728
  streamOpened: () => { void this.refreshGuestRooms(owner, { mode: "force" }); },
23862
23729
  history: () => this.webChatHistory,
23863
23730
  // #1920 §5.1: guests chat unless the host made them read-only (web.guests.chat: false).
23864
- 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) });
23865
23734
  }
23866
23735
  if (decision.via === "session" && this.webToken) {
23867
23736
  const upgrade = sessionCookieUpgrade(req, this.webToken, decision.session);
@@ -24267,7 +24136,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24267
24136
  return;
24268
24137
  }
24269
24138
  const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
24270
- canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
24271
24139
  // #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
24272
24140
  extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; } });
24273
24141
  this.previewListener = listener;
@@ -24444,8 +24312,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24444
24312
  catch {
24445
24313
  return {};
24446
24314
  } })(),
24447
- // #2118: only this Chat's placement content; guest projection drops global revisions.
24448
- effect_chat_v: this.canvasWorkerEnabled() ? createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32) : null,
24449
24315
  // #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
24450
24316
  color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
24451
24317
  ...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
@@ -24456,6 +24322,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24456
24322
  tags: identity.tags,
24457
24323
  teams: instanceTeams(this.fleetConfig?.teams, name), // #2172: the sidebar groups a team's members under it
24458
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)) : []; })() } : {}),
24459
24331
  status: this.getInstanceStatus(name),
24460
24332
  // `state` (presentation: may be awaiting_input) and `execution_state` (working / idle / stuck, or null —
24461
24333
  // what the dashboard's activity events carry) come from instancePresentation (#1212).
@@ -24472,7 +24344,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
24472
24344
  };
24473
24345
  });
24474
24346
  return {
24475
- canvas_worker: this.canvasWorkerEnabled(),
24476
24347
  instances,
24477
24348
  startup: this.startupDashboard?.snapshot() ?? null,
24478
24349
  publicLink: this.getPublicWebStatus(),