@songsid/agend 2.3.0-beta.21 → 2.3.0-beta.23

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 (300) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/agent-cli.js +4 -2
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +9 -0
  6. package/dist/agent-endpoint.js +15 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +5 -0
  9. package/dist/agent-whats-new.js +16 -1
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/auth-api.d.ts +7 -0
  12. package/dist/auth-api.js +10 -3
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/codex-package.d.ts +30 -0
  15. package/dist/backend/codex-package.js +133 -0
  16. package/dist/backend/codex-package.js.map +1 -0
  17. package/dist/backend/codex.d.ts +2 -0
  18. package/dist/backend/codex.js +9 -2
  19. package/dist/backend/codex.js.map +1 -1
  20. package/dist/backend/grok.js +56 -3
  21. package/dist/backend/grok.js.map +1 -1
  22. package/dist/backend/kiro.d.ts +9 -1
  23. package/dist/backend/kiro.js +102 -1
  24. package/dist/backend/kiro.js.map +1 -1
  25. package/dist/backend/opencode-permission.js +1 -1
  26. package/dist/backend/opencode-permission.js.map +1 -1
  27. package/dist/backend/opencode.js +2 -1
  28. package/dist/backend/opencode.js.map +1 -1
  29. package/dist/binary-probe.js +1 -1
  30. package/dist/bug-report-cli.js +1 -1
  31. package/dist/bundled-skills.d.ts +7 -3
  32. package/dist/bundled-skills.js +16 -7
  33. package/dist/bundled-skills.js.map +1 -1
  34. package/dist/channel/adapters/discord.js +3 -3
  35. package/dist/channel/adapters/discord.js.map +1 -1
  36. package/dist/channel/mcp-server.js +12 -4
  37. package/dist/channel/mcp-server.js.map +1 -1
  38. package/dist/channel/mcp-tools.d.ts +3 -1
  39. package/dist/channel/mcp-tools.js +38 -2
  40. package/dist/channel/mcp-tools.js.map +1 -1
  41. package/dist/child-process.d.ts +21 -0
  42. package/dist/child-process.js +54 -0
  43. package/dist/child-process.js.map +1 -0
  44. package/dist/cli.js +167 -182
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +1 -1
  47. package/dist/command-table.js +1 -1
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +28 -2
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.d.ts +2 -2
  52. package/dist/config.js +16 -2
  53. package/dist/config.js.map +1 -1
  54. package/dist/daemon.d.ts +14 -0
  55. package/dist/daemon.js +45 -8
  56. package/dist/daemon.js.map +1 -1
  57. package/dist/darwin-memory.js +1 -1
  58. package/dist/doctor.d.ts +5 -0
  59. package/dist/doctor.js +42 -0
  60. package/dist/doctor.js.map +1 -1
  61. package/dist/export-import.js +1 -1
  62. package/dist/export-import.js.map +1 -1
  63. package/dist/fleet-level-config.d.ts +1 -1
  64. package/dist/fleet-level-config.js +2 -0
  65. package/dist/fleet-level-config.js.map +1 -1
  66. package/dist/fleet-manager.d.ts +73 -2
  67. package/dist/fleet-manager.js +400 -33
  68. package/dist/fleet-manager.js.map +1 -1
  69. package/dist/full-restart.d.ts +5 -5
  70. package/dist/full-restart.js +5 -5
  71. package/dist/full-restart.js.map +1 -1
  72. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  73. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +8 -9
  74. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  75. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  76. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  77. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +19 -4
  78. package/dist/general-knowledge/steering/core-rules.md +1 -1
  79. package/dist/instance-config-impact.js +13 -3
  80. package/dist/instance-config-impact.js.map +1 -1
  81. package/dist/instance-lifecycle.js +6 -6
  82. package/dist/instance-lifecycle.js.map +1 -1
  83. package/dist/instructions.js +2 -1
  84. package/dist/instructions.js.map +1 -1
  85. package/dist/locale.js +137 -10
  86. package/dist/locale.js.map +1 -1
  87. package/dist/login-flows.js +1 -1
  88. package/dist/login-shell.js +3 -1
  89. package/dist/login-shell.js.map +1 -1
  90. package/dist/ls-rows.js +1 -1
  91. package/dist/needs-you-hub.d.ts +98 -1
  92. package/dist/needs-you-hub.js +313 -44
  93. package/dist/needs-you-hub.js.map +1 -1
  94. package/dist/needs-you-live.d.ts +5 -2
  95. package/dist/needs-you-live.js +6 -3
  96. package/dist/needs-you-live.js.map +1 -1
  97. package/dist/needs-you.d.ts +58 -3
  98. package/dist/needs-you.js +36 -3
  99. package/dist/needs-you.js.map +1 -1
  100. package/dist/outbound-handlers.js +25 -10
  101. package/dist/outbound-handlers.js.map +1 -1
  102. package/dist/outbound-schemas.d.ts +3 -3
  103. package/dist/provider-secret-registry.d.ts +5 -3
  104. package/dist/provider-secret-registry.js +8 -2
  105. package/dist/provider-secret-registry.js.map +1 -1
  106. package/dist/public-link-progress.d.ts +12 -1
  107. package/dist/public-link-progress.js +5 -10
  108. package/dist/public-link-progress.js.map +1 -1
  109. package/dist/public-tunnel-config.d.ts +11 -0
  110. package/dist/public-tunnel-config.js +68 -0
  111. package/dist/public-tunnel-config.js.map +1 -0
  112. package/dist/public-web-gateway.d.ts +2 -0
  113. package/dist/public-web-gateway.js +3 -2
  114. package/dist/public-web-gateway.js.map +1 -1
  115. package/dist/public-web-link.d.ts +13 -2
  116. package/dist/public-web-link.js +37 -10
  117. package/dist/public-web-link.js.map +1 -1
  118. package/dist/quickstart.js +14 -8
  119. package/dist/quickstart.js.map +1 -1
  120. package/dist/resource-report.js +1 -1
  121. package/dist/scheduler/task-board.d.ts +41 -1
  122. package/dist/scheduler/task-board.js +150 -4
  123. package/dist/scheduler/task-board.js.map +1 -1
  124. package/dist/sd-notify.js +2 -1
  125. package/dist/sd-notify.js.map +1 -1
  126. package/dist/secret-store.d.ts +2 -0
  127. package/dist/secret-store.js +21 -0
  128. package/dist/secret-store.js.map +1 -1
  129. package/dist/service-installer.d.ts +69 -0
  130. package/dist/service-installer.js +181 -0
  131. package/dist/service-installer.js.map +1 -1
  132. package/dist/service-owner.d.ts +44 -0
  133. package/dist/service-owner.js +48 -0
  134. package/dist/service-owner.js.map +1 -0
  135. package/dist/settings-api.d.ts +5 -0
  136. package/dist/settings-api.js +32 -10
  137. package/dist/settings-api.js.map +1 -1
  138. package/dist/settings-change.js +86 -3
  139. package/dist/settings-change.js.map +1 -1
  140. package/dist/settings-effect.js +6 -1
  141. package/dist/settings-effect.js.map +1 -1
  142. package/dist/settings-http-confirmation.d.ts +2 -0
  143. package/dist/settings-http-confirmation.js +4 -3
  144. package/dist/settings-http-confirmation.js.map +1 -1
  145. package/dist/settings-request-capability.d.ts +4 -1
  146. package/dist/settings-request-capability.js +11 -3
  147. package/dist/settings-request-capability.js.map +1 -1
  148. package/dist/setup-host.js +1 -1
  149. package/dist/setup-wizard.js +1 -1
  150. package/dist/setup-wizard.js.map +1 -1
  151. package/dist/task-board-summary.d.ts +101 -0
  152. package/dist/task-board-summary.js +177 -0
  153. package/dist/task-board-summary.js.map +1 -0
  154. package/dist/task-board-waker.d.ts +8 -1
  155. package/dist/task-board-waker.js +15 -6
  156. package/dist/task-board-waker.js.map +1 -1
  157. package/dist/tmux-control.js +1 -1
  158. package/dist/tmux-control.js.map +1 -1
  159. package/dist/tmux-exec-helper.d.ts +5 -1
  160. package/dist/tmux-exec-helper.js +13 -21
  161. package/dist/tmux-exec-helper.js.map +1 -1
  162. package/dist/tmux-manager.js +1 -1
  163. package/dist/tmux-read.js +1 -1
  164. package/dist/tmux-read.js.map +1 -1
  165. package/dist/topic-commands.d.ts +3 -0
  166. package/dist/topic-commands.js +29 -18
  167. package/dist/topic-commands.js.map +1 -1
  168. package/dist/transcript-monitor.js +5 -0
  169. package/dist/transcript-monitor.js.map +1 -1
  170. package/dist/transcript-sources.d.ts +2 -0
  171. package/dist/transcript-sources.js +4 -0
  172. package/dist/transcript-sources.js.map +1 -1
  173. package/dist/tunnel/cloudflared-install.js +3 -1
  174. package/dist/tunnel/cloudflared-install.js.map +1 -1
  175. package/dist/tunnel/cloudflared.d.ts +7 -16
  176. package/dist/tunnel/cloudflared.js +65 -13
  177. package/dist/tunnel/cloudflared.js.map +1 -1
  178. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  179. package/dist/tunnel/named-diagnostics.js +52 -0
  180. package/dist/tunnel/named-diagnostics.js.map +1 -0
  181. package/dist/tunnel/types.d.ts +2 -1
  182. package/dist/tunnel/types.js.map +1 -1
  183. package/dist/types.d.ts +12 -0
  184. package/dist/ui/app-effects.js +10 -4
  185. package/dist/ui/app.html +2 -0
  186. package/dist/ui/chat-render.js +39 -0
  187. package/dist/ui/chat-store.js +8 -0
  188. package/dist/ui/chat-strings.js +5 -2
  189. package/dist/ui/chat-thread.js +15 -8
  190. package/dist/ui/effect-trial-offer.js +2 -31
  191. package/dist/ui/effects-waker.js +2 -70
  192. package/dist/ui/fleet-canvas.js +14 -6
  193. package/dist/ui/fleet-route-scene.js +86 -21
  194. package/dist/ui/fleet-task-card.js +5 -5
  195. package/dist/ui/fleet-task-common.js +41 -7
  196. package/dist/ui/fleet-task-work.js +107 -0
  197. package/dist/ui/fleet-tasks.js +46 -12
  198. package/dist/ui/fleet-traffic.js +57 -10
  199. package/dist/ui/icons/icon-192.png +0 -0
  200. package/dist/ui/icons/icon-512.png +0 -0
  201. package/dist/ui/image-lightbox.js +39 -1
  202. package/dist/ui/image-zoom.js +242 -0
  203. package/dist/ui/panel-chat.js +24 -13
  204. package/dist/ui/panel-details.js +2 -0
  205. package/dist/ui/panel-fleet.js +6 -2
  206. package/dist/ui/panel-needs.js +21 -6
  207. package/dist/ui/panel-settings.js +90 -15
  208. package/dist/ui/preview-panel.js +2 -1
  209. package/dist/ui/search-dialog.js +2 -1
  210. package/dist/ui/settings-appearance.js +159 -9
  211. package/dist/ui/settings-dialogs.js +33 -17
  212. package/dist/ui/settings-discord.js +6 -2
  213. package/dist/ui/settings-guests.js +2 -1
  214. package/dist/ui/settings-learn.js +1 -0
  215. package/dist/ui/settings-model.js +2 -11
  216. package/dist/ui/settings-mods.js +6 -3
  217. package/dist/ui/settings-strings.js +80 -0
  218. package/dist/ui/settings-token.js +3 -1
  219. package/dist/ui/settings-web-authority.js +6 -3
  220. package/dist/ui/settings-wizard.js +13 -7
  221. package/dist/ui/shared/app-i18n.js +2 -2
  222. package/dist/ui/shared/app-needs.js +2 -2
  223. package/dist/ui/shared/app-store.js +2 -2
  224. package/dist/ui/shared/app-stream.js +2 -1
  225. package/dist/ui/shared/app.css +125 -28
  226. package/dist/ui/shared/app.js +5 -1
  227. package/dist/ui/shared/effect-app-runtime.js +7 -5
  228. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  229. package/dist/ui/shared/effect-cursor.js +3 -2
  230. package/dist/ui/shared/effect-display-metadata.js +39 -0
  231. package/dist/ui/shared/effect-registry.js +4 -2
  232. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  233. package/dist/ui/shared/effect-stock-trust.js +1 -1
  234. package/dist/ui/shared/feature-flags.js +127 -0
  235. package/dist/ui/shared/identity-color.js +6 -3
  236. package/dist/ui/shared/instance-nav.js +3 -2
  237. package/dist/ui/shared/text-undo.js +210 -0
  238. package/dist/ui/shared/ui-dialog.js +4 -3
  239. package/dist/ui/shared/ui-icons.js +2 -0
  240. package/dist/ui/signin.html +2 -0
  241. package/dist/update-dispatch.js +1 -1
  242. package/dist/update-launch.d.ts +1 -1
  243. package/dist/update-launch.js +1 -2
  244. package/dist/update-launch.js.map +1 -1
  245. package/dist/usage/providers.js +1 -1
  246. package/dist/usage/providers.js.map +1 -1
  247. package/dist/view-api.js +1 -1
  248. package/dist/web-api.d.ts +12 -2
  249. package/dist/web-api.js +266 -18
  250. package/dist/web-api.js.map +1 -1
  251. package/dist/web-app-icon-worker.d.ts +1 -0
  252. package/dist/web-app-icon-worker.js +15 -0
  253. package/dist/web-app-icon-worker.js.map +1 -0
  254. package/dist/web-app-icon.d.ts +107 -0
  255. package/dist/web-app-icon.js +256 -0
  256. package/dist/web-app-icon.js.map +1 -0
  257. package/dist/web-appearance-tools.d.ts +1 -0
  258. package/dist/web-appearance-tools.js +7 -3
  259. package/dist/web-appearance-tools.js.map +1 -1
  260. package/dist/web-appearance.d.ts +4 -10
  261. package/dist/web-appearance.js +4 -3
  262. package/dist/web-appearance.js.map +1 -1
  263. package/dist/web-chat-history.d.ts +37 -0
  264. package/dist/web-chat-history.js +57 -2
  265. package/dist/web-chat-history.js.map +1 -1
  266. package/dist/web-experimental-http.d.ts +2 -0
  267. package/dist/web-experimental-http.js +12 -0
  268. package/dist/web-experimental-http.js.map +1 -0
  269. package/dist/web-experimental.d.ts +11 -0
  270. package/dist/web-experimental.js +49 -0
  271. package/dist/web-experimental.js.map +1 -0
  272. package/dist/web-guest-access.d.ts +1 -0
  273. package/dist/web-guest-access.js +11 -3
  274. package/dist/web-guest-access.js.map +1 -1
  275. package/dist/web-icons.d.ts +33 -1
  276. package/dist/web-icons.js +83 -11
  277. package/dist/web-icons.js.map +1 -1
  278. package/dist/web-mod-store.d.ts +2 -0
  279. package/dist/web-mod-store.js +10 -0
  280. package/dist/web-mod-store.js.map +1 -1
  281. package/dist/web-png.d.ts +29 -0
  282. package/dist/web-png.js +321 -0
  283. package/dist/web-png.js.map +1 -0
  284. package/dist/web-preview.d.ts +2 -0
  285. package/dist/web-preview.js +2 -0
  286. package/dist/web-preview.js.map +1 -1
  287. package/dist/web-reactions.d.ts +34 -0
  288. package/dist/web-reactions.js +47 -0
  289. package/dist/web-reactions.js.map +1 -0
  290. package/dist/web-settings-patch.d.ts +2 -1
  291. package/dist/web-settings-patch.js +5 -1
  292. package/dist/web-settings-patch.js.map +1 -1
  293. package/dist/web-settings-tools.d.ts +1 -0
  294. package/dist/web-settings-tools.js +14 -10
  295. package/dist/web-settings-tools.js.map +1 -1
  296. package/dist/web-terminal.js +1 -1
  297. package/dist/web-theme-store.d.ts +2 -2
  298. package/dist/web-theme-store.js +6 -3
  299. package/dist/web-theme-store.js.map +1 -1
  300. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
1
2
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
2
3
  import { SettingsBaselines } from "./settings-baseline.js";
3
4
  import { SettingsConfirmationStore } from "./settings-confirmation.js";
@@ -8,7 +9,8 @@ import { performance } from "node:perf_hooks";
8
9
  import { withSpawnHoldReply } from "./spawn-hold-reply.js";
9
10
  import { gatewayRequestContext } from "./web-request-context.js";
10
11
  import { createPublicWebGateway, PUBLIC_GATEWAY_RECEIVE_MS } from "./public-web-gateway.js";
11
- import { handleWebIconRequest } from "./web-icons.js";
12
+ import { handleWebIconRequest, useAppIconStore } from "./web-icons.js";
13
+ import { AppIconStore } from "./web-app-icon.js";
12
14
  import { trackWebServer } from "./web-server-drain.js";
13
15
  import { renderPublicLinkProgress, ThrottledMessageEditor } from "./public-link-progress.js";
14
16
  import { PublicWebLink, publicLinkSettings } from "./public-web-link.js";
@@ -75,10 +77,12 @@ import { routeToolCall } from "./channel/tool-router.js";
75
77
  import { Scheduler } from "./scheduler/index.js";
76
78
  import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
77
79
  import { TaskBoardWaker } from "./task-board-waker.js";
80
+ import { TaskBoardError, wrapUpGap } from "./scheduler/task-board.js";
78
81
  import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
79
82
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
80
83
  import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext, readStatuslineJson, statuslineModel } from "./topic-commands.js";
81
84
  import { DailySummary } from "./daily-summary.js";
85
+ import { TaskBoardSummaryClock, buildSummary, isQuiet, renderSummary, summaryMarkFile, summaryTime } from "./task-board-summary.js";
82
86
  import { WebhookEmitter } from "./webhook-emitter.js";
83
87
  import { TmuxControlClient } from "./tmux-control.js";
84
88
  import { safeHandler } from "./safe-async.js";
@@ -143,6 +147,7 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
143
147
  import { handleWebRequest, broadcastSseEvent, broadcastToGuests, SSE_HEARTBEAT_MS } from "./web-api.js";
144
148
  import { LogService } from "./log-service.js";
145
149
  import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId, webChatFrom } from "./web-chat-history.js";
150
+ import { WEB_ECHO_COPIES_MAX, WEB_MIRROR_TIMEOUT_MS, webReactionEmoji, webReactionProblem } from "./web-reactions.js";
146
151
  import { WebChatDiskStore } from "./web-chat-store.js";
147
152
  import { SeenUsers, resolveMentions } from "./web-mentions.js";
148
153
  import { OPERATOR, WebPrefsStore } from "./web-prefs.js";
@@ -244,13 +249,16 @@ import { decisionListAnswer } from "./decision-list.js";
244
249
  import { CodeRequestLimiter, ReturningDevices } from "./web-returning-device.js";
245
250
  import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, LOOPBACK_HOST_NAMES, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
246
251
  import { createPreviewListener, normalizeOrigin, previewAvailability, previewSettings } from "./web-preview.js";
252
+ import { canvasWorkerEnabled, isCanvasWorkerTool, CANVAS_WORKER_EXPERIMENTAL_MESSAGE } from "./web-experimental.js";
253
+ import { isCanvasWorkerHttpPath } from "./web-experimental-http.js";
254
+ import { bundledSkillContents } from "./bundled-skills.js";
247
255
  import { createEffectBootstrap } from "./web-effect-bootstrap.js";
248
256
  import { fleetLevelDifferences, fleetLevelSignature, appendedConnections } from "./fleet-level-config.js";
249
257
  import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-restart-limit.js";
250
258
  import { SecretStore } from "./secret-store.js";
251
259
  import { opaqueId, safeSecretError, SECRET_CHALLENGE_TTL_MS, } from "./connection-secrets.js";
252
260
  import { verifyDiscordToken, verifyTelegramToken } from "./provider-probe.js";
253
- import { PROVIDER_SECRET_SPECS, providerSecretSpec, providerRegistryEnvKeys, isReservedProviderEnvKey, verifyProviderSecret, } from "./provider-secret-registry.js";
261
+ import { PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC, providerSecretSpec, providerRegistryEnvKeys, isReservedProviderEnvKey, verifyProviderSecret, } from "./provider-secret-registry.js";
254
262
  /** A self-restart is a whole service restart; 120s is the apply budget, not this. */
255
263
  const SELF_RESTART_DEADLINE_MS = 300_000;
256
264
  import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
@@ -260,6 +268,7 @@ import { mergeBackendOptions, profileKey, profileName, profileOf } from "./class
260
268
  import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
261
269
  import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
262
270
  import { launchFullRestartHelper } from "./full-restart.js";
271
+ import { fullReloadExitCode } from "./service-owner.js";
263
272
  import { SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE } from "./service-restart-selection.js";
264
273
  import { collectRedundantInstanceDefaultPaths } from "./fleet-yaml-slim.js";
265
274
  import { StormWindow } from "./storm-window.js";
@@ -670,6 +679,8 @@ export class FleetManager {
670
679
  costGuard = null;
671
680
  statuslineWatcher;
672
681
  dailySummary = null;
682
+ /** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
683
+ taskBoardSummary = null;
673
684
  dailyTipScheduler = null;
674
685
  /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
675
686
  taskBoardWaker = null;
@@ -846,6 +857,7 @@ export class FleetManager {
846
857
  effectBootstrap = createEffectBootstrap();
847
858
  /** Fixed trusted document coherence, independent of the HTML preview listener; never a resource grant. */
848
859
  get effectBoot() { return this.effectBootstrap.boot; }
860
+ canvasWorkerEnabled() { return canvasWorkerEnabled(this.fleetConfig?.web); }
849
861
  healthServerDrain = null;
850
862
  /** #1306: the preview listener (health_port + 1 by default), and whether it is listening. */
851
863
  previewListener = null;
@@ -1092,6 +1104,7 @@ export class FleetManager {
1092
1104
  /** #1629 r4 §1: get_web_appearance / preview_web_appearance — the caller's own look, resolved as the page resolves it. */
1093
1105
  get appearanceTools() {
1094
1106
  return (this.appearanceToolsService ??= new AppearanceTools({
1107
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1095
1108
  frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
1096
1109
  theme: (name) => {
1097
1110
  const r = this.themeStore.get(name);
@@ -1136,6 +1149,7 @@ export class FleetManager {
1136
1149
  prefs: this.webPrefs,
1137
1150
  ownerCss: this.ownerCss,
1138
1151
  effects: this.effectLibrary,
1152
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1139
1153
  context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits(), effects: cw1EffectChecker() }),
1140
1154
  isKnownInstance: (name) => this.isKnownInstance(name),
1141
1155
  prefsIdentity: OPERATOR,
@@ -1174,6 +1188,11 @@ export class FleetManager {
1174
1188
  const card = resolveCard(name, humanCards(this.dataDir).get(name), this.themeStore.get(name).theme.card);
1175
1189
  return { ...(card.tagline ? { tagline: card.tagline.text } : {}), ...(card.bio ? { bio: card.bio.text } : {}) };
1176
1190
  }
1191
+ appIconStore;
1192
+ /** #2308: the person's own app icon (Settings → Web appearance), under the state dir; none set: the bundled one. */
1193
+ get appIcon() {
1194
+ return (this.appIconStore ??= new AppIconStore(this.dataDir));
1195
+ }
1177
1196
  ownerCssStore;
1178
1197
  /** #1629 r4 §3: each person's own CSS; which version is in force is their `css.active` in the web preferences. */
1179
1198
  get ownerCss() {
@@ -1186,6 +1205,7 @@ export class FleetManager {
1186
1205
  */
1187
1206
  get webSettings() {
1188
1207
  return (this.webSettingsToolsService ??= new WebSettingsTools({
1208
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1189
1209
  prefs: this.webPrefs, ownerCss: this.ownerCss, mods: this.mods, effects: this.effectService,
1190
1210
  instances: () => [...Object.keys(this.fleetConfig?.instances ?? {}),
1191
1211
  ...(this.classicChannels?.getAll() ?? []).map((ch) => ch.instanceName).filter((n) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, n))],
@@ -1303,6 +1323,7 @@ export class FleetManager {
1303
1323
  return null;
1304
1324
  const record = auth.session, sessions = this.webSessions, exposure = gatewayRequestContext(req);
1305
1325
  return { id: record.idHash, label: record.label, source: exposure ? "public_link" : "web_session",
1326
+ localAdmin: () => record.tier === "admin" && record.surface === "local" && !exposure,
1306
1327
  current: () => current() && sessions === this.webSessions && !!this.webToken
1307
1328
  && sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
1308
1329
  }, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
@@ -2651,6 +2672,10 @@ export class FleetManager {
2651
2672
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
2652
2673
  this.warnAboutRemovedLoginMode(loaded);
2653
2674
  this.enforceGuestSwitch();
2675
+ // Child tool/guidance readers must follow this fleet's actual config, including --config.
2676
+ // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
2677
+ process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
2678
+ process.env.AGEND_FLEET_CONFIG_PATH = resolvePath(configPath);
2654
2679
  return this.fleetConfig;
2655
2680
  }
2656
2681
  relayLoginModeWarned = false;
@@ -2830,8 +2855,17 @@ export class FleetManager {
2830
2855
  this.logger.info(`Received SIGUSR1, initiating full restart (process reload)... ${describeSignalSource(this.dataDir, "SIGUSR1")}`);
2831
2856
  this.gracefulShutdownForReload()
2832
2857
  .then(() => {
2833
- this.logger.info("Full restart: shutdown complete, exiting for reload");
2834
- process.exit(0);
2858
+ // #2356: under an AgEnD systemd unit a clean exit is a stop (Restart=on-failure); exit so the unit comes back.
2859
+ let ownCgroup = null;
2860
+ try {
2861
+ ownCgroup = readFileSync("/proc/self/cgroup", "utf-8");
2862
+ }
2863
+ catch { /* not Linux */ }
2864
+ const code = fullReloadExitCode(ownCgroup);
2865
+ this.logger.info(code === 0
2866
+ ? "Full restart: shutdown complete, exiting for reload"
2867
+ : `Full restart: shutdown complete, exiting ${code} so the systemd unit restarts the fleet (#2356)`);
2868
+ process.exit(code);
2835
2869
  })
2836
2870
  .catch(err => {
2837
2871
  this.logger.error({ err }, "Full restart: graceful shutdown failed");
@@ -2850,7 +2884,7 @@ export class FleetManager {
2850
2884
  const chatId = String(this.getChannelConfig(adapterId)?.group_id ?? data.channelId);
2851
2885
  this.beginUpdateProgress(adapter, chatId, data.channelId, messageId);
2852
2886
  }
2853
- const { spawn } = await import("node:child_process");
2887
+ const { spawn } = await import("./child-process.js");
2854
2888
  // Plain `agend update`: the CLI picks the channel from the version it is about to replace (a beta install
2855
2889
  // stays on beta). Deciding here, from the package.json next to THIS code, read a source checkout's 1.22.0 as
2856
2890
  // "not a beta" and sent a beta install to @latest. #1450 C5: the INSTALLED agend, verified, by absolute path.
@@ -3792,12 +3826,61 @@ export class FleetManager {
3792
3826
  * #1920 §5.2: a web message's channel copy was sent — it is now what the channel saw. Kept so (and on disk), and sent
3793
3827
  * to the guests' streams only: an admin's page already has it, and nothing about it changed for them.
3794
3828
  */
3795
- webMessageInChannel(instance, messageId) {
3829
+ webMessageInChannel(instance, messageId, copies = []) {
3830
+ // #2305: the copies' own message ids, so an instance's reaction to the web message is put on them too.
3831
+ if (copies.length)
3832
+ this.rememberWebEchoCopies(messageId, copies);
3796
3833
  const m = this.webChatHistory.markChannel(instance, messageId);
3797
3834
  if (m)
3798
3835
  broadcastToGuests(this.sseClients, "message", m, (err) => this.logger.debug({ err }, "SSE client write failed; evicting"));
3799
3836
  }
3800
- async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK) {
3837
+ /** #2305: a web message's channel copies (topic, ClassicBot rooms), newest WEB_ECHO_COPIES_MAX, this process only. */
3838
+ webEchoCopies = new Map();
3839
+ rememberWebEchoCopies(messageId, copies) {
3840
+ const list = [...(this.webEchoCopies.get(messageId) ?? []), ...copies.filter(c => c && typeof c.adapter?.react === "function" && c.messageId)];
3841
+ this.webEchoCopies.delete(messageId);
3842
+ this.webEchoCopies.set(messageId, list);
3843
+ while (this.webEchoCopies.size > WEB_ECHO_COPIES_MAX)
3844
+ this.webEchoCopies.delete(this.webEchoCopies.keys().next().value);
3845
+ }
3846
+ /**
3847
+ * #2305: an instance's reaction to a web chat message (`messageId`, the id it was handed): its minimal reply, as on
3848
+ * the platforms. Kept on the message's row (and on disk), shown live (`reactions` on the stream), and put on the
3849
+ * message's channel copies when this process sent them — in the background, each within WEB_MIRROR_TIMEOUT_MS, so a
3850
+ * slow or rate-limited platform never holds the tool's answer (best effort: a copy's failure or timeout is logged,
3851
+ * never the tool's). `mirrored`: how many copies took it, when that is known.
3852
+ * The tool's errors: an emoji outside the web set or one of the instance's delivery marks (web-reactions.ts), or a
3853
+ * message the chat does not have. An emoji the message already has is a success that changes nothing.
3854
+ */
3855
+ async reactToWebMessage(instance, messageId, raw) {
3856
+ const problem = webReactionProblem(raw, this.statusEmojiAvoidList(instance));
3857
+ if (problem)
3858
+ return { ok: false, error: problem };
3859
+ const emoji = webReactionEmoji(raw);
3860
+ const r = this.webChatHistory.addReaction(instance, messageId, emoji);
3861
+ if (!r)
3862
+ return { ok: false, error: `react: ${messageId} is not a message in ${instance}'s web chat now (it left the history, or it is from before AgEnD restarted) — answer with reply instead` };
3863
+ if (!r.added)
3864
+ return { ok: true, mirrored: Promise.resolve(0) };
3865
+ this.emitSseEvent("reactions", { instance, boot: r.message.boot, id: r.message.id, reactions: r.message.reactions ?? [] });
3866
+ const mirrorOne = (copy) => {
3867
+ let timer;
3868
+ const timeout = new Promise((_, reject) => {
3869
+ timer = setTimeout(() => reject(new Error(`no answer in ${WEB_MIRROR_TIMEOUT_MS} ms`)), WEB_MIRROR_TIMEOUT_MS);
3870
+ timer.unref?.();
3871
+ });
3872
+ return Promise.race([Promise.resolve().then(() => copy.adapter.react(copy.chatId, copy.messageId, reactionForm(copy.adapter.type, emoji), copy.threadId)), timeout])
3873
+ .then(() => 1, (err) => {
3874
+ this.logger.warn({ err, instance, adapter: copy.adapter.type }, "Reaction on a web message's channel copy failed (shown on the web)");
3875
+ return 0;
3876
+ })
3877
+ .finally(() => clearTimeout(timer));
3878
+ };
3879
+ const mirrored = Promise.all((this.webEchoCopies.get(messageId) ?? []).map(mirrorOne)).then(n => n.reduce((a, b) => a + b, 0));
3880
+ return { ok: true, mirrored };
3881
+ }
3882
+ /** #2305: `copies` collects each room's copy (its message id), for a reaction to the web message. */
3883
+ async sendClassicWebEcho(instance, text, sender = WEB_NAME_FALLBACK, copies) {
3801
3884
  const guard = this.webChannelEchoGuards.get(instance);
3802
3885
  const handled = new Set();
3803
3886
  let posted = 0;
@@ -3827,7 +3910,9 @@ export class FleetManager {
3827
3910
  try {
3828
3911
  if (!adapter?.sendText)
3829
3912
  continue;
3830
- await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
3913
+ const sent = await adapter.sendText(entry.channelId, text, { format: "text", allowedMentions: { parse: [] } });
3914
+ if (copies && sent && typeof sent.messageId === "string" && sent.messageId)
3915
+ copies.push({ adapter, chatId: entry.channelId, messageId: sent.messageId });
3831
3916
  ClassicChannelManager.logMessage(instance, sender, text, new Date());
3832
3917
  posted++;
3833
3918
  }
@@ -4245,6 +4330,18 @@ export class FleetManager {
4245
4330
  async dispatchSlashCore(slash, adapterId, adapter) {
4246
4331
  // #1569: an alias (/web) is its command (/dashboard) from here on — the door, the table and the handler see one name.
4247
4332
  const data = { ...slash, command: canonicalCommand(slash.command) };
4333
+ // #2359: /dashboard stays listed; prefix its reply with the deprecation notice, then run /web's handler.
4334
+ const noteDeprecatedDashboard = () => {
4335
+ if (slash.command !== "dashboard")
4336
+ return;
4337
+ const prefix = `${t("dashboard.deprecated")}\n\n`;
4338
+ const respond = data.respond;
4339
+ data.respond = async (text) => respond(prefix + text);
4340
+ if (data.respondButtons) {
4341
+ const respondButtons = data.respondButtons;
4342
+ data.respondButtons = async (text, choices) => respondButtons(prefix + text, choices);
4343
+ }
4344
+ };
4248
4345
  const door = this.slashDoor(data, adapterId);
4249
4346
  if ("refusal" in door) {
4250
4347
  await data.respond(t(door.refusal)).catch(() => { });
@@ -4255,6 +4352,7 @@ export class FleetManager {
4255
4352
  // — before the table, which keeps /web for fleet admins. Per account: it touches no instance binding.
4256
4353
  // Decided synchronously, so an admin's /web still reaches the table with no await after the door (#1399).
4257
4354
  if (data.command === "dashboard" && this.isGuestWebAsk(adapterId, data.userId)) {
4355
+ noteDeprecatedDashboard();
4258
4356
  await this.guestWebCommand({ adapterId, userId: data.userId, username: data.username,
4259
4357
  arg: String(data.options?.action ?? "").trim().toLowerCase(), replyHere: text => data.respond(text), replyPrivately: text => data.respond(text) });
4260
4358
  return;
@@ -4283,6 +4381,7 @@ export class FleetManager {
4283
4381
  await data.respond(reply).catch(() => { });
4284
4382
  return;
4285
4383
  }
4384
+ noteDeprecatedDashboard();
4286
4385
  if (data.command === "start") {
4287
4386
  await this.handleClassicStartSlash(data, adapterId);
4288
4387
  }
@@ -5866,7 +5965,7 @@ export class FleetManager {
5866
5965
  dataDir: this.dataDir, web: () => this.fleetConfig?.web,
5867
5966
  permitted: owner => this.publicOwnerCurrent(owner),
5868
5967
  reserve: id => this.getTunnelLane().reserve("dashboard", id),
5869
- createGateway: (exposureId, current, open, failed) => createPublicWebGateway({ exposureId, isCurrent: current, isOpen: open, onError: failed,
5968
+ createGateway: (exposureId, current, open, failed, named) => createPublicWebGateway({ ...named, exposureId, isCurrent: current, isOpen: open, onError: failed,
5870
5969
  dispatch: (req, res) => this.dispatchWebHttp(req, res, this.fleetConfig?.health_port ?? 19280) }),
5871
5970
  revoke: id => {
5872
5971
  this.webLoginCodes?.revokeAudience(id);
@@ -7099,6 +7198,17 @@ export class FleetManager {
7099
7198
  return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
7100
7199
  });
7101
7200
  this.dailySummary.start();
7201
+ // #2246 step 3: the task board's daily summary — its own time (defaults.task_board.summary_at, read at every look),
7202
+ // in the fleet's zone; a summary missed while the fleet was down goes out when it is back.
7203
+ this.taskBoardSummary = new TaskBoardSummaryClock({
7204
+ at: () => summaryTime(this.fleetConfig?.defaults?.task_board?.summary_at) ?? null,
7205
+ zone: () => this.usageZone(),
7206
+ now: () => new Date(),
7207
+ mark: summaryMarkFile(join(this.dataDir, "task-board-summary.json")),
7208
+ send: (since, until) => this.postTaskBoardSummary(since, until),
7209
+ failed: (err) => this.logger.warn({ err }, "The task board's daily summary failed; the next minute tries again"),
7210
+ });
7211
+ this.taskBoardSummary.start();
7102
7212
  // Rotate classic channel chat logs daily (piggyback on daily summary timer)
7103
7213
  this.classicChannels?.rotateLogs();
7104
7214
  this.rotateInboxes();
@@ -7493,7 +7603,8 @@ export class FleetManager {
7493
7603
  }, 60 * 60 * 1000);
7494
7604
  const onRestart = () => this.onGracefulRestartSignal(() => process.once("SIGUSR2", onRestart));
7495
7605
  process.once("SIGUSR2", onRestart);
7496
- // SIGUSR1: full process reload (graceful stop → exit → CLI restarts)
7606
+ // SIGUSR1: full process reload (graceful stop → exit). Nothing in this AgEnD sends it any more (#2356); an older
7607
+ // CLI or a hand-sent signal still may, and the exit code then lets the owning manager start the next fleet.
7497
7608
  process.once("SIGUSR1", () => this.onFullRestartSignal());
7498
7609
  // A SIGHUP may arrive after the PID/general is available but before the
7499
7610
  // rest of startup finishes. Replay one coalesced reload only after all
@@ -8384,7 +8495,7 @@ export class FleetManager {
8384
8495
  // means we cannot verify the pane, which is treated as dead (the same
8385
8496
  // conclusion the old code reached only by throwing).
8386
8497
  try {
8387
- const { execFile } = await import("node:child_process");
8498
+ const { execFile } = await import("./child-process.js");
8388
8499
  const { promisify } = await import("node:util");
8389
8500
  const { getTmuxSocketName } = await import("./paths.js");
8390
8501
  // Honour socket isolation: without -L this queried the user's default
@@ -8934,6 +9045,22 @@ export class FleetManager {
8934
9045
  this.logger.debug({ adapterId: msg.adapterId, threadId: threadId ?? null, messageId: msg.messageId, reason: drop }, "Inbound message dropped before the dedup claim — key not consumed");
8935
9046
  return;
8936
9047
  }
9048
+ // #2246 step 3 (#2341 review r2): a reply to a Needs you Send back / Answer prompt is the card's reason or answer, not
9049
+ // input for an instance — judged by the PROMPT's world (its admins, its instance, its room as it is now; the hub's
9050
+ // handleReply), before the room's generic access below: in a shared group the room may be another bot's, whose
9051
+ // policy has no say over that world's prompt. Only that exact reply, by the admin who clicked, while the prompt
9052
+ // stands. Taken once: another bot's copy of a taken reply is dropped here. All else flows on.
9053
+ if (!msg.isBotMessage && msg.replyTo && this.needsYou) {
9054
+ if (this.needsYou.wasTakenReply(msg.chatId, msg.messageId)) {
9055
+ this.logger.debug({ adapterId: msg.adapterId, messageId: msg.messageId }, "Another bot's copy of a Needs you answer already taken — dropped");
9056
+ return;
9057
+ }
9058
+ if (this.needsYou.handleReply({
9059
+ adapterId: msg.adapterId, chatId: msg.chatId, ...(threadId !== undefined ? { threadId } : {}), replyTo: msg.replyTo,
9060
+ messageId: msg.messageId, userId: msg.userId, ...(msg.username ? { username: msg.username } : {}), text: msg.text ?? "",
9061
+ }))
9062
+ return;
9063
+ }
8937
9064
  // Access control — classic channels are open to all, others require an allowed
8938
9065
  // user. This runs before the dedup claim, and is judged by the adapter that
8939
9066
  // owns the topic rather than the one that happened to receive the message:
@@ -9044,6 +9171,10 @@ export class FleetManager {
9044
9171
  // #1920 step 4b-3: /web typed in a ClassicBot room (a group: `/web@ThisBot`) by someone who is not a fleet admin,
9045
9172
  // while guest sign-in is on: their guest code, privately — as anywhere else. An admin's goes on as before.
9046
9173
  if (/^\/(?:web|dashboard)(?:\s|$)/i.test(text) && msgAdapter && this.isGuestWebAsk(msg.adapterId, msg.userId)) {
9174
+ // #2359: this door must not skip the deprecation notice TopicCommands shows on /dashboard.
9175
+ if (/^\/dashboard(?:\s|$)/i.test(text)) {
9176
+ await msgAdapter.sendText(chatId, t("dashboard.deprecated")).catch(() => { });
9177
+ }
9047
9178
  await this.guestWebCommand({ adapterId: msg.adapterId, userId: msg.userId, username: msg.username,
9048
9179
  arg: text.replace(/^\/(?:web|dashboard)/i, "").trim().toLowerCase(), replyHere: body => msgAdapter.sendText(chatId, body) });
9049
9180
  return;
@@ -9594,6 +9725,8 @@ export class FleetManager {
9594
9725
  }
9595
9726
  }
9596
9727
  checkToolPermission(sink, instanceName, tool) {
9728
+ if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
9729
+ return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
9597
9730
  const profile = resolveToolSet(this.fleetConfig?.instances[instanceName], instanceName);
9598
9731
  const allowed = mayUseTool(profile, tool);
9599
9732
  if (!allowed) {
@@ -9649,6 +9782,17 @@ export class FleetManager {
9649
9782
  });
9650
9783
  return;
9651
9784
  }
9785
+ // #2305: a reaction to a web chat message is the web chat's own (any fleet, web-only included): never an adapter's.
9786
+ if (tool === "react" && typeof args.message_id === "string" && isWebMessageId(args.message_id)) {
9787
+ // The socket's own instance, never a session name the caller wrote itself (as checkToolPermission: it was not
9788
+ // identified) — a channel tool's call from a daemon carries none anyway (#2310 review).
9789
+ const r = await this.reactToWebMessage(instanceName, args.message_id, args.emoji);
9790
+ if (r.ok)
9791
+ respond("ok");
9792
+ else
9793
+ respond(null, r.error);
9794
+ return;
9795
+ }
9652
9796
  // A fleet with no chat platform at all is driven from the web dashboard alone: a reply has nowhere
9653
9797
  // else to go, and "retry shortly" would have the agent retry forever. It goes to the web chat.
9654
9798
  const webOnlyReply = tool === "reply" && this.worlds.size === 0 && this.isWebOnlyFleet();
@@ -10267,13 +10411,14 @@ export class FleetManager {
10267
10411
  if (!ipc || !fleetRequestId)
10268
10412
  return;
10269
10413
  const type = String(msg.type);
10270
- const responseType = type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
10271
- : type.startsWith("fleet_decision_") ? "fleet_decision_response"
10272
- : type === "fleet_task" ? "fleet_task_response"
10273
- : type === "fleet_set_display_name" ? "fleet_display_name_response"
10274
- : type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis"
10275
- || type === "fleet_list_stickers" || type === "fleet_preview_stickers" ? "fleet_persona_emoji_response"
10276
- : "fleet_description_response";
10414
+ const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) ? "fleet_web_theme_response"
10415
+ : type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
10416
+ : type.startsWith("fleet_decision_") ? "fleet_decision_response"
10417
+ : type === "fleet_task" ? "fleet_task_response"
10418
+ : type === "fleet_set_display_name" ? "fleet_display_name_response"
10419
+ : type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis"
10420
+ || type === "fleet_list_stickers" || type === "fleet_preview_stickers" ? "fleet_persona_emoji_response"
10421
+ : "fleet_description_response";
10277
10422
  ipc.send({ type: responseType, fleetRequestId, error: message });
10278
10423
  }
10279
10424
  dispatchTypedIpc(name, msg) {
@@ -10766,6 +10911,8 @@ export class FleetManager {
10766
10911
  }
10767
10912
  /** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
10768
10913
  async handleWebThemeHttp(instance, tool, args) {
10914
+ if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
10915
+ return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
10769
10916
  if (WEB_EFFECT_TOOLS.has(tool)) {
10770
10917
  if (!this.isKnownInstance(instance))
10771
10918
  return { error: "unknown_instance" };
@@ -12658,7 +12805,7 @@ export class FleetManager {
12658
12805
  const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
12659
12806
  const backend = this.fleetConfig?.defaults?.backend || "claude-code";
12660
12807
  try {
12661
- const { execFile } = await import("node:child_process");
12808
+ const { execFile } = await import("./child-process.js");
12662
12809
  const { promisify } = await import("node:util");
12663
12810
  // execFile with an argv array — no shell, so the backend name cannot be
12664
12811
  // interpreted as a command even if config is malformed.
@@ -12884,6 +13031,51 @@ export class FleetManager {
12884
13031
  }
12885
13032
  return { chatId, opts: {} };
12886
13033
  }
13034
+ /**
13035
+ * #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
13036
+ * gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
13037
+ * one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
13038
+ * nothing. A web-only fleet: everything, in the web chat.
13039
+ */
13040
+ postTaskBoardSummary(since, until) {
13041
+ const board = this.scheduler?.db.board;
13042
+ if (!board)
13043
+ return;
13044
+ const read = board.summaryRead(since.toISOString(), until.toISOString());
13045
+ const projects = board.listProjects(true);
13046
+ const stats = board.projectStats();
13047
+ const names = new Map(projects.map((p) => [p.id, p.name]));
13048
+ const configured = this.getPrimaryAdapterId();
13049
+ const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
13050
+ const worldOf = (assignee) => {
13051
+ const owner = assignee ? this.getInstanceAdapterId(assignee) : undefined;
13052
+ return owner !== undefined && this.worlds.has(owner) ? owner : primary;
13053
+ };
13054
+ const zone = this.usageZone();
13055
+ for (const world of this.worlds.size ? [...this.worlds.keys()] : [undefined]) {
13056
+ const summary = buildSummary(read, since, until, { inChat: (assignee) => worldOf(assignee) === world, projects, stats });
13057
+ if (isQuiet(summary))
13058
+ continue;
13059
+ const text = renderSummary(summary, { zone, t: (key, ...args) => t(key, ...args), projectOf: (c) => (c.projectId === null ? null : names.get(c.projectId) ?? null) });
13060
+ const cards = [...new Set([...summary.review, ...summary.blocked, ...summary.done, ...summary.created].map((c) => c.code))];
13061
+ if (world !== undefined) {
13062
+ const adapter = this.worlds.get(world)?.adapter;
13063
+ const target = this.fleetNoticeTarget(world);
13064
+ if (adapter && target) {
13065
+ adapter.sendText(target.chatId, text, target.opts).catch(e => this.logger.warn({ err: e, world }, "Failed to send the task board's daily summary"));
13066
+ }
13067
+ else {
13068
+ this.logger.warn({ world }, "The task board's daily summary has no postable target — set a General topic or channel.options.general_channel_id");
13069
+ }
13070
+ }
13071
+ for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
13072
+ if (cfg.general_topic !== true || (world !== undefined && this.getInstanceAdapterId(name) !== world))
13073
+ continue;
13074
+ // #2341 review: with the cards it names, so the public link withholds it while one of them is hidden there.
13075
+ this.emitSseEvent("message", { instance: name, sender: name, role: "status", text, ts: new Date().toISOString(), cards });
13076
+ }
13077
+ }
13078
+ }
12887
13079
  /**
12888
13080
  * Post the daily summary where fleet-wide notices go.
12889
13081
  *
@@ -13592,6 +13784,8 @@ export class FleetManager {
13592
13784
  return String(topic);
13593
13785
  return this.classicChannels?.getChannelIdByInstance(instance) ?? undefined;
13594
13786
  },
13787
+ laneTarget: instance => this.instanceChannelTarget(instance),
13788
+ taskRoute: code => this.needsYouRoute(code),
13595
13789
  isFleetAdmin: (userId, world) => this.isFleetAdmin(userId, world),
13596
13790
  fleetAdmins: world => (this.getChannelConfig(world)?.access?.allowed_users ?? []).map(String),
13597
13791
  emitSse: (event, data) => this.emitSseEvent(event, data),
@@ -13600,6 +13794,8 @@ export class FleetManager {
13600
13794
  throw new Error("the delivery outbox is not open");
13601
13795
  return this.deliveryOutbox.acknowledge(deliveryId, by);
13602
13796
  },
13797
+ tasks: () => this.needsYouTasks(),
13798
+ taskAction: action => this.taskActionFromChat(action),
13603
13799
  settings: () => ({
13604
13800
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
13605
13801
  dm: this.fleetConfig?.needs_you?.dm === true,
@@ -13610,6 +13806,98 @@ export class FleetManager {
13610
13806
  });
13611
13807
  this.needsYou.start();
13612
13808
  }
13809
+ /**
13810
+ * #2352: where an instance's own notices go — its topic in its world's group, or its ClassicBot room — as a Needs you
13811
+ * lane's place; null when it has neither (or its world has no group).
13812
+ */
13813
+ instanceChannelTarget(instance) {
13814
+ const cfg = this.fleetConfig?.instances && Object.hasOwn(this.fleetConfig.instances, instance) ? this.fleetConfig.instances[instance] : undefined;
13815
+ if (!cfg)
13816
+ return null;
13817
+ const groupId = this.getChannelConfig(this.getInstanceAdapterId(instance))?.group_id;
13818
+ if (cfg.topic_id != null && groupId != null)
13819
+ return { chatId: String(groupId), threadId: String(cfg.topic_id) };
13820
+ const classic = this.classicChannels?.getChannelIdByInstance(instance);
13821
+ return classic ? { chatId: classic } : null;
13822
+ }
13823
+ /**
13824
+ * #2352: the instance whose own channel lists a waiting card — the instance that handed it out, else its project's
13825
+ * lead — when that channel can be used now (a fleet instance of a live world, with a topic or room). undefined: its
13826
+ * assignee's world's General, as before.
13827
+ */
13828
+ needsYouLane(card, leads) {
13829
+ const usable = (name) => {
13830
+ if (!name || !this.fleetConfig?.instances || !Object.hasOwn(this.fleetConfig.instances, name))
13831
+ return false;
13832
+ const world = this.getInstanceAdapterId(name);
13833
+ return world !== undefined && this.worlds.has(world) && this.instanceChannelTarget(name) !== null;
13834
+ };
13835
+ const dispatcher = card.dispatcher?.kind === "instance" && card.dispatcher.name !== card.assignee ? card.dispatcher.name : null;
13836
+ if (dispatcher)
13837
+ return usable(dispatcher) ? dispatcher : undefined;
13838
+ const lead = card.projectId != null ? leads.get(card.projectId) : null;
13839
+ return usable(lead) ? lead : undefined;
13840
+ }
13841
+ /**
13842
+ * #2355 review: where a card's Needs you buttons belong now — read from the board and the fleet at the moment of a click
13843
+ * or a reply, never from the last render. null: it no longer waits for the person.
13844
+ */
13845
+ needsYouRoute(code) {
13846
+ const board = this.scheduler?.db.board;
13847
+ const card = board?.findCard(code);
13848
+ if (!board || !card || !(card.status === "blocked" || (card.status === "review" && card.needsReview)))
13849
+ return null;
13850
+ const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
13851
+ const lane = this.needsYouLane(card, leads);
13852
+ return lane ? { lane } : {};
13853
+ }
13854
+ /**
13855
+ * #2246 step 3: the task board's cards that wait for the person, for Needs you — under each card's assignee, listed in
13856
+ * its lane's channel (#2352: the instance that handed it out, else the project's lead), else in General.
13857
+ */
13858
+ needsYouTasks() {
13859
+ const board = this.scheduler?.db.board;
13860
+ if (!board)
13861
+ return [];
13862
+ const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
13863
+ return board.waitingForPerson().map(({ card, since }) => {
13864
+ const gap = card.status === "review" ? wrapUpGap(card) : null;
13865
+ const lane = this.needsYouLane(card, leads);
13866
+ return {
13867
+ uuid: card.uuid, code: card.code, title: card.title, status: card.status, version: card.version,
13868
+ assignee: card.assignee, since: Date.parse(since) || 0, ...(gap ? { gap } : {}), ...(lane ? { lane } : {}),
13869
+ note: card.status === "blocked" ? card.blockedReason : gap ? t("needs.task_gap", [
13870
+ gap.unchecked ? t("needs.task_gap_unchecked", gap.unchecked) : "", gap.noResult ? t("needs.task_gap_no_result") : "",
13871
+ ].filter(Boolean).join(t("needs.task_gap_join"))) : "",
13872
+ };
13873
+ });
13874
+ }
13875
+ /**
13876
+ * #2246 step 3: the person's move on a card from a chat's Needs you list — a fleet admin of that world (checked by the
13877
+ * hub), acting as the person, through the board's rules, at the version the button showed.
13878
+ */
13879
+ taskActionFromChat(action) {
13880
+ const board = this.scheduler?.db.board;
13881
+ if (!board)
13882
+ return { ok: false, message: t("needs.task_board_closed") };
13883
+ const person = { kind: "person", name: action.by.username || `${action.by.platform}:${action.by.userId}` };
13884
+ try {
13885
+ if (action.verb === "confirm")
13886
+ board.moveCard(person, action.code, "done", { version: action.version });
13887
+ else if (action.verb === "back")
13888
+ board.moveCard(person, action.code, "claimed", { version: action.version, reason: action.text });
13889
+ else
13890
+ board.moveCard(person, action.code, "claimed", { version: action.version, comment: action.text });
13891
+ const done = { confirm: "needs.task_confirmed", back: "needs.task_sent_back", answer: "needs.task_answered" }[action.verb];
13892
+ this.logger.info({ code: action.code, verb: action.verb, by: `${action.by.platform}:${action.by.userId}` }, "Task board: the person moved a card from the chat");
13893
+ return { ok: true, message: t(done, action.code) };
13894
+ }
13895
+ catch (err) {
13896
+ if (err instanceof TaskBoardError && err.code === "conflict")
13897
+ return { ok: false, message: t("needs.task_changed", action.code) };
13898
+ return { ok: false, message: t("needs.task_refused", action.code, err.message) };
13899
+ }
13900
+ }
13613
13901
  /** The prompts open on the dashboard right now (a page that loads after one was posted asks for them). */
13614
13902
  listWebPrompts() {
13615
13903
  const open = [];
@@ -16268,6 +16556,7 @@ export class FleetManager {
16268
16556
  channel: m.channel,
16269
16557
  from: m.from,
16270
16558
  replyTo: m.replyTo,
16559
+ cards: m.cards,
16271
16560
  });
16272
16561
  broadcastSseEvent(this.sseClients, event, recorded, onError, this.webChatHistory.cursorOf(recorded));
16273
16562
  this.searchIndex?.record(chatDoc(recorded));
@@ -16299,11 +16588,25 @@ export class FleetManager {
16299
16588
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
16300
16589
  holds: (wake) => db.board.wakeHolds(wake),
16301
16590
  card: (uuid) => db.board.findCard(uuid),
16591
+ // #2352: the card's Needs you buttons are in this instance's own channel (it is the card's lane).
16592
+ buttonsHere: (instance, uuid) => {
16593
+ // #2355 review: no live message, no buttons — the old words then.
16594
+ if (this.fleetConfig?.needs_you?.live_message === false)
16595
+ return false;
16596
+ const card = db.board.findCard(uuid);
16597
+ if (!card)
16598
+ return false;
16599
+ const leads = new Map(db.board.listProjects(true).map((p) => [p.id, p.leadInstance]));
16600
+ return this.needsYouLane(card, leads) === instance;
16601
+ },
16302
16602
  log: (level, message, extra) => this.logger[level](extra ?? {}, message),
16303
16603
  });
16304
16604
  db.board.onChange(({ rev, ids, projects, wakes }) => {
16305
16605
  this.emitSseEvent("tasks", { rev, ids, projects });
16306
16606
  waker.note(wakes);
16607
+ // step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
16608
+ if (ids.length || projects)
16609
+ this.needsYou?.poke();
16307
16610
  });
16308
16611
  }
16309
16612
  /** A task board notice, as AgEnD's own (not a person's words): through the delivery outbox, held until delivered. */
@@ -16517,6 +16820,27 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16517
16820
  this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
16518
16821
  this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
16519
16822
  }
16823
+ /** Update only AgEnD-managed skill copies; no launch or restart is needed for an experimental switch. */
16824
+ canvasWorkerSettingChanged() {
16825
+ this.refreshExperimentalSkills();
16826
+ this.emitSseEvent("status", this.getUiStatus());
16827
+ }
16828
+ refreshExperimentalSkills() {
16829
+ for (const [name, daemon] of this.daemons) {
16830
+ try {
16831
+ const cfg = daemon.getConfigSnapshot();
16832
+ const backend = cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
16833
+ const workDir = this.resolveKnowledgeWorkDir(cfg.working_directory, backend, name);
16834
+ const classic = this.classicChannels?.getAll().some(ch => ch.instanceName === name) === true;
16835
+ if (classic && !insideManagedWorkspaces(workDir))
16836
+ continue;
16837
+ this.syncRoleSkills(workDir, backend, classic ? "classic" : cfg.general_topic ? "general" : "worker");
16838
+ }
16839
+ catch (err) {
16840
+ this.logger.warn({ name, err }, "Could not refresh experimental skill visibility");
16841
+ }
16842
+ }
16843
+ }
16520
16844
  /** Publish only the bundled skills eligible for an instance role. */
16521
16845
  syncRoleSkills(workDir, backend, role, knowledgeDir) {
16522
16846
  const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
@@ -16681,7 +17005,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16681
17005
  continue;
16682
17006
  }
16683
17007
  managed.push(name);
16684
- const newContent = readFileSync(join(srcSkills, name, "SKILL.md"), "utf-8");
17008
+ const newContent = bundledSkillContents(join(srcSkills, name), this.canvasWorkerEnabled());
16685
17009
  if (refuse(dest))
16686
17010
  continue;
16687
17011
  try {
@@ -19204,6 +19528,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19204
19528
  this.clearStatuslineWatchers();
19205
19529
  this.costGuard?.stop();
19206
19530
  this.dailySummary?.stop();
19531
+ this.taskBoardSummary?.stop();
19207
19532
  this.dailyTipScheduler?.stop();
19208
19533
  this.dailyTipScheduler = null;
19209
19534
  if (this.updateCheckTimer) {
@@ -19285,6 +19610,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19285
19610
  await this.retirePendingNoncePrompts();
19286
19611
  this.topicArchiver.stop();
19287
19612
  this.taskBoardWaker?.stop();
19613
+ this.appIconStore?.close();
19288
19614
  this.scheduler?.shutdown();
19289
19615
  // What it was sent is written first (bounded: search-index.ts close()).
19290
19616
  const search = this.searchIndex;
@@ -19539,6 +19865,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19539
19865
  // mark every row done and tell the user "changes applied".
19540
19866
  return { rejected: why };
19541
19867
  }
19868
+ if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
19869
+ this.canvasWorkerSettingChanged();
19870
+ }
19542
19871
  // Classic behavior settings share the fleet defaults but are not entries
19543
19872
  // in fleet.yaml. Snapshot the old effective chain before reloading the
19544
19873
  // Classic file so SIGHUP can hot-apply either source without waiting for
@@ -19788,6 +20117,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19788
20117
  return false;
19789
20118
  }
19790
20119
  /** Non-null when startup found the running config and fleet.yaml disagreeing. */
20120
+ /**
20121
+ * #2346 (settings schema): whether a connection added now would start at once — what canHotAddConnections would decide
20122
+ * for the current config with one more connection appended: another one is running, and nothing else this process took
20123
+ * at startup has changed since (a pending locale or access change, say, needs AgEnD restarted anyway).
20124
+ */
20125
+ connectionsStartLive() {
20126
+ if (!this.fleetConfig)
20127
+ return false;
20128
+ const current = this.fleetConfig.channels ?? (this.fleetConfig.channel ? [this.fleetConfig.channel] : []);
20129
+ const probe = { id: "\u0000probe", type: "telegram", mode: "topic", bot_token_env: "\u0000PROBE" };
20130
+ const next = { ...this.fleetConfig, channels: [...current, probe] };
20131
+ delete next.channel;
20132
+ return this.canHotAddConnections(next);
20133
+ }
19791
20134
  fleetSignatureMismatchKeys() {
19792
20135
  return this.fleetSignatureMismatch;
19793
20136
  }
@@ -20006,12 +20349,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20006
20349
  return this.fleetConfig?.web?.provider_secrets === true;
20007
20350
  }
20008
20351
  listProviderSecrets() {
20009
- return PROVIDER_SECRET_SPECS.map(spec => ({
20352
+ return [...PROVIDER_SECRET_SPECS, PUBLIC_TUNNEL_SECRET_SPEC].map(spec => ({
20010
20353
  id: spec.id,
20011
20354
  display_name: spec.displayName,
20012
20355
  kind: spec.kind,
20013
20356
  token_present: !!process.env[spec.envKey],
20014
- verifier: spec.verifier ? "available" : "unsupported",
20357
+ verifier: spec.kind === "tunnel_token" ? "local_format" : spec.verifier ? "available" : "unsupported",
20015
20358
  activation: spec.activation,
20016
20359
  stale_consumers: this.providerSecretStaleConsumers(spec.envKey),
20017
20360
  }));
@@ -20021,7 +20364,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20021
20364
  // a child process's private environment safely, so report the conservative
20022
20365
  // set of already-running children; the UI can then say "restart these"
20023
20366
  // rather than claiming an existing process reloaded.
20024
- if (envKey === "GROQ_API_KEY")
20367
+ if (envKey === "GROQ_API_KEY" || envKey === PUBLIC_TUNNEL_TOKEN_ENV)
20025
20368
  return [];
20026
20369
  return [...this.children.keys()].sort();
20027
20370
  }
@@ -20039,10 +20382,13 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20039
20382
  if (!spec || !this.providerSecretEnvAllowed(spec.envKey)) {
20040
20383
  return { ok: false, status: "invalid", error: "provider secret is not configured" };
20041
20384
  }
20042
- if (!spec.verifier)
20385
+ const tunnel = spec.kind === "tunnel_token";
20386
+ if (!tunnel && !spec.verifier)
20043
20387
  return { ok: false, status: "unsupported_verifier", error: "this provider has no supported verifier" };
20044
- if (!input.secret || input.secret.length > 4096 || /[\r\n\0]/.test(input.secret) || /[^\x20-\x7e]/.test(input.secret)) {
20045
- return { ok: false, status: "invalid", error: "secret is invalid" };
20388
+ const secret = tunnel && !input.clear ? normalizePublicTunnelToken(input.secret) : input.secret;
20389
+ if (input.clear && (!tunnel || input.secret !== "")
20390
+ || !input.clear && (!secret || secret.length > 4096 || /[\r\n\0]/.test(secret) || /[^\x20-\x7e]/.test(secret))) {
20391
+ return { ok: false, status: "invalid", error: tunnel ? PUBLIC_TUNNEL_TOKEN_HINT : "secret is invalid" };
20046
20392
  }
20047
20393
  const challengeKey = `${input.sessionBinding}:api_key:${spec.id}:${spec.envKey}:${input.idempotencyKey}`;
20048
20394
  const existingId = this.providerSecretChallengesByKey.get(challengeKey);
@@ -20052,7 +20398,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20052
20398
  }
20053
20399
  if (existingId)
20054
20400
  this.providerSecretChallengesByKey.delete(challengeKey);
20055
- const result = await verifyProviderSecret(spec, input.secret, this.providerSecretHttpClient);
20401
+ const result = tunnel ? { ok: true } : await verifyProviderSecret(spec, input.secret, this.providerSecretHttpClient);
20056
20402
  if (!result.ok) {
20057
20403
  // Do not log provider detail: the HTTP verifier already redacted it and
20058
20404
  // this endpoint has no need to disclose whether a key was close to valid.
@@ -20070,7 +20416,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20070
20416
  operation: "provider-secret.apply",
20071
20417
  idempotencyKey: input.idempotencyKey,
20072
20418
  expiresAt, deadline: performance.now() + SECRET_CHALLENGE_TTL_MS,
20073
- secret: input.secret,
20419
+ secret: secret,
20074
20420
  };
20075
20421
  this.providerSecretChallenges.set(challenge.id, challenge);
20076
20422
  this.providerSecretChallengesByKey.set(challengeKey, challenge.id);
@@ -20140,6 +20486,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20140
20486
  async runProviderSecretApply(job, secret, execution) {
20141
20487
  const spec = providerSecretSpec(job.specId);
20142
20488
  const allowed = new Set([
20489
+ PUBLIC_TUNNEL_TOKEN_ENV,
20143
20490
  ...PROVIDER_SECRET_SPECS.map(item => item.envKey),
20144
20491
  ...(this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : [])).map(channel => channel.bot_token_env),
20145
20492
  ]);
@@ -20153,7 +20500,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20153
20500
  // Construct inside the transaction: symlink/permission refusal must
20154
20501
  // settle the job as a safe failure, not escape the queued microtask.
20155
20502
  store = new SecretStore(join(this.dataDir, ".env"), allowed, { owner: execution?.owner });
20156
- const write = () => { before = store.write(job.envKey, secret); wrote = true; process.env[job.envKey] = secret; };
20503
+ const write = () => {
20504
+ before = spec.kind === "tunnel_token" && !secret ? store.clear(job.envKey) : store.write(job.envKey, secret);
20505
+ wrote = true;
20506
+ if (secret)
20507
+ process.env[job.envKey] = secret;
20508
+ else
20509
+ delete process.env[job.envKey];
20510
+ };
20157
20511
  if (execution)
20158
20512
  execution.mutate(write);
20159
20513
  else
@@ -20175,12 +20529,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20175
20529
  job.finishedAt = Date.now();
20176
20530
  }
20177
20531
  catch (err) {
20178
- const safe = safeSecretError(err, secret);
20532
+ const safe = spec?.kind === "tunnel_token" ? "connector token change failed" : safeSecretError(err, secret);
20179
20533
  this.logger.warn({ specId: job.specId, reason: safe }, "Provider API-key apply failed");
20180
20534
  try {
20181
20535
  // Memory and disk have separate receipts: a host-edited file must
20182
20536
  // remain, but cannot strand our owned process value or hook snapshot.
20183
- if (wrote && process.env[job.envKey] === secret) {
20537
+ if (wrote && (process.env[job.envKey] ?? "") === secret) {
20184
20538
  if (previousProcessValue === undefined)
20185
20539
  delete process.env[job.envKey];
20186
20540
  else
@@ -20220,6 +20574,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20220
20574
  * hot activation contract explicit and gives tests a failure seam; no generic
20221
20575
  * SIGHUP or caller-provided hook is ever executed. */
20222
20576
  async runProviderSecretReloadHook(hookId, _next, _previous) {
20577
+ if (hookId === "cloudflare.public_link") {
20578
+ await this.publicWebLink?.close("connector token changed");
20579
+ return;
20580
+ }
20223
20581
  if (hookId !== "groq.voice")
20224
20582
  throw new Error("unknown provider secret reload hook");
20225
20583
  const before = this.providerSecretHotSnapshots.get(hookId);
@@ -21202,7 +21560,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21202
21560
  // up to 15s each, and on a beta build BOTH ran — 30s with no WATCHDOG ping,
21203
21561
  // past WatchdogSec's half-interval and enough for systemd to SIGABRT the fleet
21204
21562
  // for a background version check.
21205
- const { execFile } = await import("node:child_process");
21563
+ const { execFile } = await import("./child-process.js");
21206
21564
  const { promisify } = await import("node:util");
21207
21565
  const execFileP = promisify(execFile);
21208
21566
  const npmVersion = async (spec) => {
@@ -21494,10 +21852,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21494
21852
  return;
21495
21853
  }
21496
21854
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
21855
+ if (!this.canvasWorkerEnabled() && isCanvasWorkerHttpPath(requestPath)) {
21856
+ res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
21857
+ res.end(JSON.stringify({ error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }));
21858
+ return;
21859
+ }
21497
21860
  if (this.effectBootstrap.handle(req, res))
21498
21861
  return;
21499
21862
  // Branding is public, after Host admission: automatic favicon requests
21500
21863
  // must not become noisy web-token 401s in the browser console.
21864
+ // #2308: the icon routes and the pages' icon links follow the app icon set in Settings.
21865
+ useAppIconStore(this.appIcon);
21501
21866
  if (handleWebIconRequest(req, res, requestPath))
21502
21867
  return;
21503
21868
  // Public: the health probe, /agent (instance-token auth of its own), the
@@ -21941,6 +22306,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21941
22306
  return;
21942
22307
  }
21943
22308
  const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
22309
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
21944
22310
  // #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
21945
22311
  extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; } });
21946
22312
  this.previewListener = listener;
@@ -22118,7 +22484,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22118
22484
  return {};
22119
22485
  } })(),
22120
22486
  // #2118: only this Chat's placement content; guest projection drops global revisions.
22121
- effect_chat_v: createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32),
22487
+ effect_chat_v: this.canvasWorkerEnabled() ? createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32) : null,
22122
22488
  // #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
22123
22489
  color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
22124
22490
  ...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
@@ -22145,6 +22511,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22145
22511
  };
22146
22512
  });
22147
22513
  return {
22514
+ canvas_worker: this.canvasWorkerEnabled(),
22148
22515
  instances,
22149
22516
  publicLink: this.getPublicWebStatus(),
22150
22517
  uptime: Math.floor((Date.now() - this.startedAt) / 1000),