@songsid/agend 2.3.0-beta.22 → 2.3.0-beta.24

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 (298) 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 +1 -0
  6. package/dist/agent-endpoint.js +4 -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 +54 -21
  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 +3 -1
  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 +11 -10
  31. package/dist/bug-report-cli.js.map +1 -1
  32. package/dist/bug-report.js +11 -10
  33. package/dist/bug-report.js.map +1 -1
  34. package/dist/bundled-skills.d.ts +7 -3
  35. package/dist/bundled-skills.js +16 -7
  36. package/dist/bundled-skills.js.map +1 -1
  37. package/dist/channel/adapters/discord.js +3 -3
  38. package/dist/channel/adapters/discord.js.map +1 -1
  39. package/dist/channel/mcp-server.js +12 -4
  40. package/dist/channel/mcp-server.js.map +1 -1
  41. package/dist/channel/mcp-tools.d.ts +3 -1
  42. package/dist/channel/mcp-tools.js +36 -0
  43. package/dist/channel/mcp-tools.js.map +1 -1
  44. package/dist/child-process.d.ts +21 -0
  45. package/dist/child-process.js +54 -0
  46. package/dist/child-process.js.map +1 -0
  47. package/dist/cli.js +188 -204
  48. package/dist/cli.js.map +1 -1
  49. package/dist/command-table.d.ts +1 -1
  50. package/dist/command-table.js +1 -1
  51. package/dist/command-table.js.map +1 -1
  52. package/dist/config-validator.js +20 -1
  53. package/dist/config-validator.js.map +1 -1
  54. package/dist/config.js +3 -1
  55. package/dist/config.js.map +1 -1
  56. package/dist/daemon.d.ts +8 -0
  57. package/dist/daemon.js +31 -8
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.js +1 -1
  60. package/dist/doctor.d.ts +5 -0
  61. package/dist/doctor.js +42 -0
  62. package/dist/doctor.js.map +1 -1
  63. package/dist/export-import.js +4 -3
  64. package/dist/export-import.js.map +1 -1
  65. package/dist/fleet-level-config.d.ts +1 -1
  66. package/dist/fleet-level-config.js +2 -0
  67. package/dist/fleet-level-config.js.map +1 -1
  68. package/dist/fleet-manager.d.ts +45 -0
  69. package/dist/fleet-manager.js +324 -36
  70. package/dist/fleet-manager.js.map +1 -1
  71. package/dist/full-restart.d.ts +5 -5
  72. package/dist/full-restart.js +5 -5
  73. package/dist/full-restart.js.map +1 -1
  74. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +48 -10
  75. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  76. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
  77. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  78. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  79. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  80. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +17 -4
  81. package/dist/general-knowledge/steering/core-rules.md +1 -1
  82. package/dist/instance-config-impact.js +10 -3
  83. package/dist/instance-config-impact.js.map +1 -1
  84. package/dist/instance-lifecycle.js +4 -4
  85. package/dist/locale.js +296 -4
  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-schemas.d.ts +3 -3
  101. package/dist/public-link-progress.d.ts +12 -1
  102. package/dist/public-link-progress.js +5 -10
  103. package/dist/public-link-progress.js.map +1 -1
  104. package/dist/public-tunnel-config.d.ts +5 -0
  105. package/dist/public-tunnel-config.js +45 -1
  106. package/dist/public-tunnel-config.js.map +1 -1
  107. package/dist/public-web-link.js +6 -5
  108. package/dist/public-web-link.js.map +1 -1
  109. package/dist/quickstart.js +15 -9
  110. package/dist/quickstart.js.map +1 -1
  111. package/dist/resource-report.js +1 -1
  112. package/dist/scheduler/task-board.d.ts +41 -1
  113. package/dist/scheduler/task-board.js +152 -5
  114. package/dist/scheduler/task-board.js.map +1 -1
  115. package/dist/sd-notify.js +2 -1
  116. package/dist/sd-notify.js.map +1 -1
  117. package/dist/service-installer.d.ts +69 -0
  118. package/dist/service-installer.js +182 -0
  119. package/dist/service-installer.js.map +1 -1
  120. package/dist/service-owner.d.ts +44 -0
  121. package/dist/service-owner.js +48 -0
  122. package/dist/service-owner.js.map +1 -0
  123. package/dist/service-plan.d.ts +8 -0
  124. package/dist/service-plan.js +13 -0
  125. package/dist/service-plan.js.map +1 -1
  126. package/dist/settings-api.d.ts +4 -0
  127. package/dist/settings-api.js +21 -3
  128. package/dist/settings-api.js.map +1 -1
  129. package/dist/settings-change.js +88 -4
  130. package/dist/settings-change.js.map +1 -1
  131. package/dist/settings-confirmation.d.ts +2 -0
  132. package/dist/settings-confirmation.js +14 -8
  133. package/dist/settings-confirmation.js.map +1 -1
  134. package/dist/settings-control.js +2 -1
  135. package/dist/settings-control.js.map +1 -1
  136. package/dist/settings-effect.js +2 -0
  137. package/dist/settings-effect.js.map +1 -1
  138. package/dist/settings-http-confirmation.d.ts +2 -0
  139. package/dist/settings-http-confirmation.js +3 -2
  140. package/dist/settings-http-confirmation.js.map +1 -1
  141. package/dist/settings-request-capability.d.ts +4 -1
  142. package/dist/settings-request-capability.js +11 -3
  143. package/dist/settings-request-capability.js.map +1 -1
  144. package/dist/setup-auth.d.ts +1 -0
  145. package/dist/setup-auth.js +2 -1
  146. package/dist/setup-auth.js.map +1 -1
  147. package/dist/setup-host.d.ts +1 -0
  148. package/dist/setup-host.js +7 -3
  149. package/dist/setup-host.js.map +1 -1
  150. package/dist/setup-wizard.js +1 -1
  151. package/dist/setup-wizard.js.map +1 -1
  152. package/dist/task-board-summary.d.ts +101 -0
  153. package/dist/task-board-summary.js +177 -0
  154. package/dist/task-board-summary.js.map +1 -0
  155. package/dist/task-board-waker.d.ts +8 -1
  156. package/dist/task-board-waker.js +15 -6
  157. package/dist/task-board-waker.js.map +1 -1
  158. package/dist/tmux-control.js +1 -1
  159. package/dist/tmux-control.js.map +1 -1
  160. package/dist/tmux-exec-helper.d.ts +13 -3
  161. package/dist/tmux-exec-helper.js +34 -29
  162. package/dist/tmux-exec-helper.js.map +1 -1
  163. package/dist/tmux-manager.js +1 -1
  164. package/dist/tmux-read.js +1 -1
  165. package/dist/tmux-read.js.map +1 -1
  166. package/dist/topic-commands.d.ts +3 -0
  167. package/dist/topic-commands.js +29 -18
  168. package/dist/topic-commands.js.map +1 -1
  169. package/dist/transcript-monitor.js +5 -0
  170. package/dist/transcript-monitor.js.map +1 -1
  171. package/dist/transcript-sources.d.ts +2 -0
  172. package/dist/transcript-sources.js +4 -0
  173. package/dist/transcript-sources.js.map +1 -1
  174. package/dist/tunnel/cloudflared-install.js +3 -1
  175. package/dist/tunnel/cloudflared-install.js.map +1 -1
  176. package/dist/tunnel/cloudflared.d.ts +1 -1
  177. package/dist/tunnel/cloudflared.js +51 -10
  178. package/dist/tunnel/cloudflared.js.map +1 -1
  179. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  180. package/dist/tunnel/named-diagnostics.js +52 -0
  181. package/dist/tunnel/named-diagnostics.js.map +1 -0
  182. package/dist/tunnel/types.d.ts +2 -1
  183. package/dist/tunnel/types.js.map +1 -1
  184. package/dist/types.d.ts +9 -0
  185. package/dist/ui/app-effects.js +10 -4
  186. package/dist/ui/chat-strings.js +3 -0
  187. package/dist/ui/chat-thread.js +14 -7
  188. package/dist/ui/effect-trial-offer.js +2 -31
  189. package/dist/ui/effects-waker.js +2 -70
  190. package/dist/ui/fleet-task-card.js +5 -5
  191. package/dist/ui/fleet-task-common.js +43 -9
  192. package/dist/ui/fleet-task-work.js +107 -0
  193. package/dist/ui/fleet-tasks.js +46 -12
  194. package/dist/ui/image-lightbox.js +39 -1
  195. package/dist/ui/image-zoom.js +242 -0
  196. package/dist/ui/panel-chat.js +24 -13
  197. package/dist/ui/panel-details.js +2 -0
  198. package/dist/ui/panel-fleet.js +6 -2
  199. package/dist/ui/panel-needs.js +21 -6
  200. package/dist/ui/panel-settings.js +35 -13
  201. package/dist/ui/preview-panel.js +2 -1
  202. package/dist/ui/search-dialog.js +2 -1
  203. package/dist/ui/settings-appearance.js +44 -10
  204. package/dist/ui/settings-dialogs.js +33 -17
  205. package/dist/ui/settings-discord.js +6 -2
  206. package/dist/ui/settings-guests.js +2 -1
  207. package/dist/ui/settings-model.js +2 -11
  208. package/dist/ui/settings-mods.js +8 -3
  209. package/dist/ui/settings-strings.js +32 -0
  210. package/dist/ui/settings-token.js +3 -1
  211. package/dist/ui/settings-web-authority.js +6 -3
  212. package/dist/ui/settings-wizard.js +13 -7
  213. package/dist/ui/shared/app-i18n.js +2 -2
  214. package/dist/ui/shared/app-needs.js +2 -2
  215. package/dist/ui/shared/app-store.js +2 -2
  216. package/dist/ui/shared/app.css +93 -32
  217. package/dist/ui/shared/app.js +5 -1
  218. package/dist/ui/shared/effect-app-runtime.js +7 -5
  219. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  220. package/dist/ui/shared/effect-cursor.js +3 -2
  221. package/dist/ui/shared/effect-display-metadata.js +39 -0
  222. package/dist/ui/shared/effect-registry.js +4 -2
  223. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  224. package/dist/ui/shared/effect-stock-trust.js +5 -5
  225. package/dist/ui/shared/feature-flags.js +127 -0
  226. package/dist/ui/shared/identity-color.js +6 -3
  227. package/dist/ui/shared/instance-nav.js +3 -2
  228. package/dist/ui/shared/panel-view.js +14 -29
  229. package/dist/ui/shared/text-undo.js +210 -0
  230. package/dist/ui/shared/ui-dialog.js +4 -3
  231. package/dist/ui/shared/ui-icons.js +2 -0
  232. package/dist/ui/shared/usage-dialog.js +29 -8
  233. package/dist/ui/shared/view-strings.js +2 -4
  234. package/dist/ui/web-terminal/terminal.html +10 -10
  235. package/dist/ui/web-terminal/terminal.js +58 -12
  236. package/dist/update-dispatch.js +1 -1
  237. package/dist/update-launch.d.ts +1 -1
  238. package/dist/update-launch.js +1 -2
  239. package/dist/update-launch.js.map +1 -1
  240. package/dist/update-preflight.js +1 -1
  241. package/dist/usage/providers.js +1 -1
  242. package/dist/usage/providers.js.map +1 -1
  243. package/dist/view-api.js +1 -1
  244. package/dist/web-api.d.ts +6 -0
  245. package/dist/web-api.js +163 -21
  246. package/dist/web-api.js.map +1 -1
  247. package/dist/web-app-icon-worker.js +2 -1
  248. package/dist/web-app-icon-worker.js.map +1 -1
  249. package/dist/web-app-icon.d.ts +10 -8
  250. package/dist/web-app-icon.js +14 -13
  251. package/dist/web-app-icon.js.map +1 -1
  252. package/dist/web-appearance-tools.d.ts +1 -0
  253. package/dist/web-appearance-tools.js +7 -3
  254. package/dist/web-appearance-tools.js.map +1 -1
  255. package/dist/web-appearance.d.ts +4 -10
  256. package/dist/web-appearance.js +4 -3
  257. package/dist/web-appearance.js.map +1 -1
  258. package/dist/web-auth.d.ts +8 -4
  259. package/dist/web-auth.js +28 -17
  260. package/dist/web-auth.js.map +1 -1
  261. package/dist/web-chat-history.d.ts +14 -0
  262. package/dist/web-chat-history.js +26 -1
  263. package/dist/web-chat-history.js.map +1 -1
  264. package/dist/web-cookie-names.d.ts +5 -0
  265. package/dist/web-cookie-names.js +12 -0
  266. package/dist/web-cookie-names.js.map +1 -0
  267. package/dist/web-experimental-http.d.ts +2 -0
  268. package/dist/web-experimental-http.js +12 -0
  269. package/dist/web-experimental-http.js.map +1 -0
  270. package/dist/web-experimental.d.ts +11 -0
  271. package/dist/web-experimental.js +49 -0
  272. package/dist/web-experimental.js.map +1 -0
  273. package/dist/web-guest-access.d.ts +1 -0
  274. package/dist/web-guest-access.js +4 -2
  275. package/dist/web-guest-access.js.map +1 -1
  276. package/dist/web-mod-store.d.ts +2 -0
  277. package/dist/web-mod-store.js +10 -0
  278. package/dist/web-mod-store.js.map +1 -1
  279. package/dist/web-png.d.ts +4 -0
  280. package/dist/web-png.js +10 -2
  281. package/dist/web-png.js.map +1 -1
  282. package/dist/web-preview.d.ts +2 -0
  283. package/dist/web-preview.js +2 -0
  284. package/dist/web-preview.js.map +1 -1
  285. package/dist/web-returning-device.d.ts +4 -2
  286. package/dist/web-returning-device.js +12 -5
  287. package/dist/web-returning-device.js.map +1 -1
  288. package/dist/web-settings-patch.d.ts +2 -1
  289. package/dist/web-settings-patch.js +5 -1
  290. package/dist/web-settings-patch.js.map +1 -1
  291. package/dist/web-settings-tools.d.ts +1 -0
  292. package/dist/web-settings-tools.js +14 -10
  293. package/dist/web-settings-tools.js.map +1 -1
  294. package/dist/web-terminal.js +1 -1
  295. package/dist/web-theme-store.d.ts +2 -2
  296. package/dist/web-theme-store.js +6 -3
  297. package/dist/web-theme-store.js.map +1 -1
  298. package/package.json +11 -11
@@ -1,7 +1,7 @@
1
- import { PUBLIC_TUNNEL_TOKEN_ENV, validPublicTunnelToken } from "./public-tunnel-config.js";
1
+ import { PUBLIC_TUNNEL_TOKEN_ENV, PUBLIC_TUNNEL_TOKEN_HINT, normalizePublicTunnelToken } from "./public-tunnel-config.js";
2
2
  import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
3
3
  import { SettingsBaselines } from "./settings-baseline.js";
4
- import { SettingsConfirmationStore } from "./settings-confirmation.js";
4
+ import { SettingsConfirmationStore, settingsSourceLabel } from "./settings-confirmation.js";
5
5
  import { SettingsHttpConfirmation } from "./settings-http-confirmation.js";
6
6
  import { SettingsControlServer } from "./settings-control.js";
7
7
  import { SettingsExecution, settingsRevision, noteSettingsWrite, settingsFileResource, assertSettingsLease, trySettingsLease, waitSettingsLease, settingsUndo, undoSettingsPaths } from "./settings-transaction.js";
@@ -77,10 +77,12 @@ import { routeToolCall } from "./channel/tool-router.js";
77
77
  import { Scheduler } from "./scheduler/index.js";
78
78
  import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
79
79
  import { TaskBoardWaker } from "./task-board-waker.js";
80
+ import { TaskBoardError, wrapUpGap } from "./scheduler/task-board.js";
80
81
  import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
81
82
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
82
83
  import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext, readStatuslineJson, statuslineModel } from "./topic-commands.js";
83
84
  import { DailySummary } from "./daily-summary.js";
85
+ import { TaskBoardSummaryClock, buildSummary, isQuiet, renderSummary, summaryMarkFile, summaryTime } from "./task-board-summary.js";
84
86
  import { WebhookEmitter } from "./webhook-emitter.js";
85
87
  import { TmuxControlClient } from "./tmux-control.js";
86
88
  import { safeHandler } from "./safe-async.js";
@@ -234,7 +236,7 @@ import { agyLegacyAgentsFile, danglingSymlink, migrateAgyAgentsFile } from "./ba
234
236
  import { claudeConfigDirFor, claudeProjectKey } from "./backend/claude-code.js";
235
237
  import { sharedRolloutIndex } from "./rollout-index.js";
236
238
  import { mayUseTool, resolveToolSet, scheduleOpRefusal, toolForIpcType, toolRefusedMessage, } from "./tool-permissions.js";
237
- import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken } from "./web-auth.js";
239
+ import { authorizeSession, decideWebGate, loadOrCreateWebToken, readWebToken, sessionCookieUpgrade } from "./web-auth.js";
238
240
  import { bypassesWebGate, handleAuthRequest, serveSigninPage } from "./auth-api.js";
239
241
  import { isWebPageNavigation } from "./web-shell-routes.js";
240
242
  import { guestSessionPolicy, localSessionPolicy, tokenEpoch, WebSessionStore } from "./web-session.js";
@@ -247,6 +249,9 @@ import { decisionListAnswer } from "./decision-list.js";
247
249
  import { CodeRequestLimiter, ReturningDevices } from "./web-returning-device.js";
248
250
  import { allowedHostNames, applyWebSecurityHeaders, hostnameOf, isHostAllowed, LOOPBACK_HOST_NAMES, WEB_HOST_REJECTED_MESSAGE } from "./web-host-guard.js";
249
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";
250
255
  import { createEffectBootstrap } from "./web-effect-bootstrap.js";
251
256
  import { fleetLevelDifferences, fleetLevelSignature, appendedConnections } from "./fleet-level-config.js";
252
257
  import { checkSelfRestartAllowance, recordSelfRestartAttempt } from "./self-restart-limit.js";
@@ -263,6 +268,7 @@ import { mergeBackendOptions, profileKey, profileName, profileOf } from "./class
263
268
  import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
264
269
  import { cleanupWatchVerdict, formatRestartProgressCompletion, RestartProgress, } from "./restart-progress.js";
265
270
  import { launchFullRestartHelper } from "./full-restart.js";
271
+ import { fullReloadExitCode } from "./service-owner.js";
266
272
  import { SYSTEMD_RESTART_INDETERMINATE_EXIT_CODE } from "./service-restart-selection.js";
267
273
  import { collectRedundantInstanceDefaultPaths } from "./fleet-yaml-slim.js";
268
274
  import { StormWindow } from "./storm-window.js";
@@ -673,6 +679,8 @@ export class FleetManager {
673
679
  costGuard = null;
674
680
  statuslineWatcher;
675
681
  dailySummary = null;
682
+ /** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
683
+ taskBoardSummary = null;
676
684
  dailyTipScheduler = null;
677
685
  /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
678
686
  taskBoardWaker = null;
@@ -849,6 +857,7 @@ export class FleetManager {
849
857
  effectBootstrap = createEffectBootstrap();
850
858
  /** Fixed trusted document coherence, independent of the HTML preview listener; never a resource grant. */
851
859
  get effectBoot() { return this.effectBootstrap.boot; }
860
+ canvasWorkerEnabled() { return canvasWorkerEnabled(this.fleetConfig?.web); }
852
861
  healthServerDrain = null;
853
862
  /** #1306: the preview listener (health_port + 1 by default), and whether it is listening. */
854
863
  previewListener = null;
@@ -1095,6 +1104,7 @@ export class FleetManager {
1095
1104
  /** #1629 r4 §1: get_web_appearance / preview_web_appearance — the caller's own look, resolved as the page resolves it. */
1096
1105
  get appearanceTools() {
1097
1106
  return (this.appearanceToolsService ??= new AppearanceTools({
1107
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1098
1108
  frame: (name) => (this.getUiStatus().instances.find(i => i.name === name) ?? { name }),
1099
1109
  theme: (name) => {
1100
1110
  const r = this.themeStore.get(name);
@@ -1139,6 +1149,7 @@ export class FleetManager {
1139
1149
  prefs: this.webPrefs,
1140
1150
  ownerCss: this.ownerCss,
1141
1151
  effects: this.effectLibrary,
1152
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1142
1153
  context: () => ({ agendVersion: agendVersion(), limits: this.themeStore.limits(), effects: cw1EffectChecker() }),
1143
1154
  isKnownInstance: (name) => this.isKnownInstance(name),
1144
1155
  prefsIdentity: OPERATOR,
@@ -1194,6 +1205,7 @@ export class FleetManager {
1194
1205
  */
1195
1206
  get webSettings() {
1196
1207
  return (this.webSettingsToolsService ??= new WebSettingsTools({
1208
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
1197
1209
  prefs: this.webPrefs, ownerCss: this.ownerCss, mods: this.mods, effects: this.effectService,
1198
1210
  instances: () => [...Object.keys(this.fleetConfig?.instances ?? {}),
1199
1211
  ...(this.classicChannels?.getAll() ?? []).map((ch) => ch.instanceName).filter((n) => !Object.hasOwn(this.fleetConfig?.instances ?? {}, n))],
@@ -1311,6 +1323,7 @@ export class FleetManager {
1311
1323
  return null;
1312
1324
  const record = auth.session, sessions = this.webSessions, exposure = gatewayRequestContext(req);
1313
1325
  return { id: record.idHash, label: record.label, source: exposure ? "public_link" : "web_session",
1326
+ localAdmin: () => record.tier === "admin" && record.surface === "local" && !exposure,
1314
1327
  current: () => current() && sessions === this.webSessions && !!this.webToken
1315
1328
  && sessions.isCurrent(record, tokenEpoch(this.webToken)) && (!exposure || exposure.isCurrent()) };
1316
1329
  }, baseline: () => baselines.read(), snapshot: () => baselines.snapshot(),
@@ -1370,7 +1383,7 @@ export class FleetManager {
1370
1383
  return null;
1371
1384
  const name = this.webPerson({ adapterId: actor.adapter_id, userId: actor.user_id }).name;
1372
1385
  return {
1373
- label: `${name} (web-verified)`,
1386
+ label: t("settings_confirm.web_verified_label", name),
1374
1387
  // admin() asks chat.current() too; a General a card could go to must still be there (else the host CLI's).
1375
1388
  current: () => !!verified() && chat.admin(actor.user_id) && this.settingsNoticeGenerals(affected, chat.platform).length > 0,
1376
1389
  done: view => this.settingsWebConfirmedNotice(view, affected, name, chat.platform),
@@ -1397,7 +1410,8 @@ export class FleetManager {
1397
1410
  */
1398
1411
  async settingsWebConfirmedNotice(view, affected, who, platform) {
1399
1412
  const outcome = view.outcome ? view.outcome.message : view.state;
1400
- const text = `✅ Settings change confirmed from the web by ${who} (web-verified)\nSource: ${view.source}\nRequester: ${view.requested_by}\n${view.summary.join("\n")}\nOutcome: ${outcome}`;
1413
+ const text = [t("settings_confirm.web_confirmed", who), t("settings_confirm.source", settingsSourceLabel(view.source)),
1414
+ t("settings_confirm.requester", view.requested_by), ...view.summary, t("settings_confirm.outcome", outcome)].join("\n");
1401
1415
  for (const { adapterId, adapter, chatId, topic } of this.settingsNoticeGenerals(affected, platform)) {
1402
1416
  try {
1403
1417
  await adapter.sendText(chatId, text, { threadId: topic, allowedMentions: { parse: [] } });
@@ -1479,9 +1493,10 @@ export class FleetManager {
1479
1493
  try {
1480
1494
  const posted = this.postNonceButtonPromptOrThrow({ prefix: "settings-confirm:", alertType: "clear_confirm",
1481
1495
  instanceName: name, adapterId, adapter, chatId, threadId: topic,
1482
- message: `🔒 Settings change awaiting fleet-admin confirmation\nSource: ${view.source}\nRequester: ${view.requested_by}\n${view.summary.join("\n")}`,
1483
- choices: [{ action: "confirm", label: "Confirm" }, { action: "reject", label: "Reject" }],
1484
- expiredText: "Settings confirmation is no longer pending.", timeoutMs: view.remaining_ms,
1496
+ message: [t("settings_confirm.prompt_title"), t("settings_confirm.source", settingsSourceLabel(view.source)),
1497
+ t("settings_confirm.requester", view.requested_by), ...view.summary].join("\n"),
1498
+ choices: [{ action: "confirm", label: t("settings_confirm.confirm_button") }, { action: "reject", label: t("settings_confirm.reject_button") }],
1499
+ expiredText: t("settings_confirm.no_longer_pending"), timeoutMs: view.remaining_ms,
1485
1500
  extra: { pendingChangeId: view.id, confirmationCurrent: current, confirmationAdmin: userId => current() && authority.admin(userId) },
1486
1501
  });
1487
1502
  void posted.then(nonce => {
@@ -1492,7 +1507,7 @@ export class FleetManager {
1492
1507
  return;
1493
1508
  this.pendingNonceButtons.delete(nonce);
1494
1509
  clearTimeout(entry.timer);
1495
- void this.retireNonceButtons(entry, entry.messageId ?? "", "Settings confirmation delivery expired.");
1510
+ void this.retireNonceButtons(entry, entry.messageId ?? "", t("settings_confirm.delivery_expired"));
1496
1511
  }, () => { });
1497
1512
  const nonce = await withinBudget(posted, postingDeadline);
1498
1513
  attached = true;
@@ -1502,7 +1517,7 @@ export class FleetManager {
1502
1517
  return;
1503
1518
  this.pendingNonceButtons.delete(nonce);
1504
1519
  clearTimeout(entry.timer);
1505
- void this.retireNonceButtons(entry, entry.messageId ?? "", "Settings confirmation is no longer pending.");
1520
+ void this.retireNonceButtons(entry, entry.messageId ?? "", t("settings_confirm.no_longer_pending"));
1506
1521
  };
1507
1522
  if (!current()) {
1508
1523
  retire();
@@ -1528,14 +1543,14 @@ export class FleetManager {
1528
1543
  const { entry, action } = result;
1529
1544
  try {
1530
1545
  const view = await this.settingsGate().store.decide(entry.pendingChangeId, action, {
1531
- label: `fleet admin ${data.userId}`,
1546
+ label: t("settings_confirm.admin_label", data.userId ?? ""),
1532
1547
  current: () => entry.confirmationCurrent?.() === true && this.getAdapterForInstance(entry.instanceName) === entry.adapter
1533
1548
  && !!data.userId && entry.confirmationAdmin?.(data.userId) === true,
1534
1549
  });
1535
- await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, view.outcome?.message ?? "Settings change is applying.");
1550
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, view.outcome?.message ?? t("settings_confirm.applying"));
1536
1551
  }
1537
1552
  catch {
1538
- await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, "Settings confirmation rejected or expired.");
1553
+ await this.retireNonceButtons(entry, entry.messageId ?? data.messageId, t("settings_confirm.rejected_or_expired"));
1539
1554
  }
1540
1555
  return true;
1541
1556
  }
@@ -2659,6 +2674,10 @@ export class FleetManager {
2659
2674
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
2660
2675
  this.warnAboutRemovedLoginMode(loaded);
2661
2676
  this.enforceGuestSwitch();
2677
+ // Child tool/guidance readers must follow this fleet's actual config, including --config.
2678
+ // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
2679
+ process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
2680
+ process.env.AGEND_FLEET_CONFIG_PATH = resolvePath(configPath);
2662
2681
  return this.fleetConfig;
2663
2682
  }
2664
2683
  relayLoginModeWarned = false;
@@ -2838,8 +2857,17 @@ export class FleetManager {
2838
2857
  this.logger.info(`Received SIGUSR1, initiating full restart (process reload)... ${describeSignalSource(this.dataDir, "SIGUSR1")}`);
2839
2858
  this.gracefulShutdownForReload()
2840
2859
  .then(() => {
2841
- this.logger.info("Full restart: shutdown complete, exiting for reload");
2842
- process.exit(0);
2860
+ // #2356: under an AgEnD systemd unit a clean exit is a stop (Restart=on-failure); exit so the unit comes back.
2861
+ let ownCgroup = null;
2862
+ try {
2863
+ ownCgroup = readFileSync("/proc/self/cgroup", "utf-8");
2864
+ }
2865
+ catch { /* not Linux */ }
2866
+ const code = fullReloadExitCode(ownCgroup);
2867
+ this.logger.info(code === 0
2868
+ ? "Full restart: shutdown complete, exiting for reload"
2869
+ : `Full restart: shutdown complete, exiting ${code} so the systemd unit restarts the fleet (#2356)`);
2870
+ process.exit(code);
2843
2871
  })
2844
2872
  .catch(err => {
2845
2873
  this.logger.error({ err }, "Full restart: graceful shutdown failed");
@@ -2858,7 +2886,7 @@ export class FleetManager {
2858
2886
  const chatId = String(this.getChannelConfig(adapterId)?.group_id ?? data.channelId);
2859
2887
  this.beginUpdateProgress(adapter, chatId, data.channelId, messageId);
2860
2888
  }
2861
- const { spawn } = await import("node:child_process");
2889
+ const { spawn } = await import("./child-process.js");
2862
2890
  // Plain `agend update`: the CLI picks the channel from the version it is about to replace (a beta install
2863
2891
  // stays on beta). Deciding here, from the package.json next to THIS code, read a source checkout's 1.22.0 as
2864
2892
  // "not a beta" and sent a beta install to @latest. #1450 C5: the INSTALLED agend, verified, by absolute path.
@@ -4304,6 +4332,18 @@ export class FleetManager {
4304
4332
  async dispatchSlashCore(slash, adapterId, adapter) {
4305
4333
  // #1569: an alias (/web) is its command (/dashboard) from here on — the door, the table and the handler see one name.
4306
4334
  const data = { ...slash, command: canonicalCommand(slash.command) };
4335
+ // #2359: /dashboard stays listed; prefix its reply with the deprecation notice, then run /web's handler.
4336
+ const noteDeprecatedDashboard = () => {
4337
+ if (slash.command !== "dashboard")
4338
+ return;
4339
+ const prefix = `${t("dashboard.deprecated")}\n\n`;
4340
+ const respond = data.respond;
4341
+ data.respond = async (text) => respond(prefix + text);
4342
+ if (data.respondButtons) {
4343
+ const respondButtons = data.respondButtons;
4344
+ data.respondButtons = async (text, choices) => respondButtons(prefix + text, choices);
4345
+ }
4346
+ };
4307
4347
  const door = this.slashDoor(data, adapterId);
4308
4348
  if ("refusal" in door) {
4309
4349
  await data.respond(t(door.refusal)).catch(() => { });
@@ -4314,6 +4354,7 @@ export class FleetManager {
4314
4354
  // — before the table, which keeps /web for fleet admins. Per account: it touches no instance binding.
4315
4355
  // Decided synchronously, so an admin's /web still reaches the table with no await after the door (#1399).
4316
4356
  if (data.command === "dashboard" && this.isGuestWebAsk(adapterId, data.userId)) {
4357
+ noteDeprecatedDashboard();
4317
4358
  await this.guestWebCommand({ adapterId, userId: data.userId, username: data.username,
4318
4359
  arg: String(data.options?.action ?? "").trim().toLowerCase(), replyHere: text => data.respond(text), replyPrivately: text => data.respond(text) });
4319
4360
  return;
@@ -4342,6 +4383,7 @@ export class FleetManager {
4342
4383
  await data.respond(reply).catch(() => { });
4343
4384
  return;
4344
4385
  }
4386
+ noteDeprecatedDashboard();
4345
4387
  if (data.command === "start") {
4346
4388
  await this.handleClassicStartSlash(data, adapterId);
4347
4389
  }
@@ -7158,6 +7200,17 @@ export class FleetManager {
7158
7200
  return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
7159
7201
  });
7160
7202
  this.dailySummary.start();
7203
+ // #2246 step 3: the task board's daily summary — its own time (defaults.task_board.summary_at, read at every look),
7204
+ // in the fleet's zone; a summary missed while the fleet was down goes out when it is back.
7205
+ this.taskBoardSummary = new TaskBoardSummaryClock({
7206
+ at: () => summaryTime(this.fleetConfig?.defaults?.task_board?.summary_at) ?? null,
7207
+ zone: () => this.usageZone(),
7208
+ now: () => new Date(),
7209
+ mark: summaryMarkFile(join(this.dataDir, "task-board-summary.json")),
7210
+ send: (since, until) => this.postTaskBoardSummary(since, until),
7211
+ failed: (err) => this.logger.warn({ err }, "The task board's daily summary failed; the next minute tries again"),
7212
+ });
7213
+ this.taskBoardSummary.start();
7161
7214
  // Rotate classic channel chat logs daily (piggyback on daily summary timer)
7162
7215
  this.classicChannels?.rotateLogs();
7163
7216
  this.rotateInboxes();
@@ -7552,7 +7605,8 @@ export class FleetManager {
7552
7605
  }, 60 * 60 * 1000);
7553
7606
  const onRestart = () => this.onGracefulRestartSignal(() => process.once("SIGUSR2", onRestart));
7554
7607
  process.once("SIGUSR2", onRestart);
7555
- // SIGUSR1: full process reload (graceful stop → exit → CLI restarts)
7608
+ // SIGUSR1: full process reload (graceful stop → exit). Nothing in this AgEnD sends it any more (#2356); an older
7609
+ // CLI or a hand-sent signal still may, and the exit code then lets the owning manager start the next fleet.
7556
7610
  process.once("SIGUSR1", () => this.onFullRestartSignal());
7557
7611
  // A SIGHUP may arrive after the PID/general is available but before the
7558
7612
  // rest of startup finishes. Replay one coalesced reload only after all
@@ -8443,7 +8497,7 @@ export class FleetManager {
8443
8497
  // means we cannot verify the pane, which is treated as dead (the same
8444
8498
  // conclusion the old code reached only by throwing).
8445
8499
  try {
8446
- const { execFile } = await import("node:child_process");
8500
+ const { execFile } = await import("./child-process.js");
8447
8501
  const { promisify } = await import("node:util");
8448
8502
  const { getTmuxSocketName } = await import("./paths.js");
8449
8503
  // Honour socket isolation: without -L this queried the user's default
@@ -8993,6 +9047,22 @@ export class FleetManager {
8993
9047
  this.logger.debug({ adapterId: msg.adapterId, threadId: threadId ?? null, messageId: msg.messageId, reason: drop }, "Inbound message dropped before the dedup claim — key not consumed");
8994
9048
  return;
8995
9049
  }
9050
+ // #2246 step 3 (#2341 review r2): a reply to a Needs you Send back / Answer prompt is the card's reason or answer, not
9051
+ // input for an instance — judged by the PROMPT's world (its admins, its instance, its room as it is now; the hub's
9052
+ // handleReply), before the room's generic access below: in a shared group the room may be another bot's, whose
9053
+ // policy has no say over that world's prompt. Only that exact reply, by the admin who clicked, while the prompt
9054
+ // stands. Taken once: another bot's copy of a taken reply is dropped here. All else flows on.
9055
+ if (!msg.isBotMessage && msg.replyTo && this.needsYou) {
9056
+ if (this.needsYou.wasTakenReply(msg.chatId, msg.messageId)) {
9057
+ this.logger.debug({ adapterId: msg.adapterId, messageId: msg.messageId }, "Another bot's copy of a Needs you answer already taken — dropped");
9058
+ return;
9059
+ }
9060
+ if (this.needsYou.handleReply({
9061
+ adapterId: msg.adapterId, chatId: msg.chatId, ...(threadId !== undefined ? { threadId } : {}), replyTo: msg.replyTo,
9062
+ messageId: msg.messageId, userId: msg.userId, ...(msg.username ? { username: msg.username } : {}), text: msg.text ?? "",
9063
+ }))
9064
+ return;
9065
+ }
8996
9066
  // Access control — classic channels are open to all, others require an allowed
8997
9067
  // user. This runs before the dedup claim, and is judged by the adapter that
8998
9068
  // owns the topic rather than the one that happened to receive the message:
@@ -9103,6 +9173,10 @@ export class FleetManager {
9103
9173
  // #1920 step 4b-3: /web typed in a ClassicBot room (a group: `/web@ThisBot`) by someone who is not a fleet admin,
9104
9174
  // while guest sign-in is on: their guest code, privately — as anywhere else. An admin's goes on as before.
9105
9175
  if (/^\/(?:web|dashboard)(?:\s|$)/i.test(text) && msgAdapter && this.isGuestWebAsk(msg.adapterId, msg.userId)) {
9176
+ // #2359: this door must not skip the deprecation notice TopicCommands shows on /dashboard.
9177
+ if (/^\/dashboard(?:\s|$)/i.test(text)) {
9178
+ await msgAdapter.sendText(chatId, t("dashboard.deprecated")).catch(() => { });
9179
+ }
9106
9180
  await this.guestWebCommand({ adapterId: msg.adapterId, userId: msg.userId, username: msg.username,
9107
9181
  arg: text.replace(/^\/(?:web|dashboard)/i, "").trim().toLowerCase(), replyHere: body => msgAdapter.sendText(chatId, body) });
9108
9182
  return;
@@ -9653,6 +9727,8 @@ export class FleetManager {
9653
9727
  }
9654
9728
  }
9655
9729
  checkToolPermission(sink, instanceName, tool) {
9730
+ if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
9731
+ return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
9656
9732
  const profile = resolveToolSet(this.fleetConfig?.instances[instanceName], instanceName);
9657
9733
  const allowed = mayUseTool(profile, tool);
9658
9734
  if (!allowed) {
@@ -10337,13 +10413,14 @@ export class FleetManager {
10337
10413
  if (!ipc || !fleetRequestId)
10338
10414
  return;
10339
10415
  const type = String(msg.type);
10340
- const responseType = type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
10341
- : type.startsWith("fleet_decision_") ? "fleet_decision_response"
10342
- : type === "fleet_task" ? "fleet_task_response"
10343
- : type === "fleet_set_display_name" ? "fleet_display_name_response"
10344
- : type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis"
10345
- || type === "fleet_list_stickers" || type === "fleet_preview_stickers" ? "fleet_persona_emoji_response"
10346
- : "fleet_description_response";
10416
+ const responseType = isCanvasWorkerTool(type.replace(/^fleet_/, "")) ? "fleet_web_theme_response"
10417
+ : type.startsWith("fleet_schedule_") ? "fleet_schedule_response"
10418
+ : type.startsWith("fleet_decision_") ? "fleet_decision_response"
10419
+ : type === "fleet_task" ? "fleet_task_response"
10420
+ : type === "fleet_set_display_name" ? "fleet_display_name_response"
10421
+ : type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis"
10422
+ || type === "fleet_list_stickers" || type === "fleet_preview_stickers" ? "fleet_persona_emoji_response"
10423
+ : "fleet_description_response";
10347
10424
  ipc.send({ type: responseType, fleetRequestId, error: message });
10348
10425
  }
10349
10426
  dispatchTypedIpc(name, msg) {
@@ -10836,6 +10913,8 @@ export class FleetManager {
10836
10913
  }
10837
10914
  /** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
10838
10915
  async handleWebThemeHttp(instance, tool, args) {
10916
+ if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
10917
+ return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
10839
10918
  if (WEB_EFFECT_TOOLS.has(tool)) {
10840
10919
  if (!this.isKnownInstance(instance))
10841
10920
  return { error: "unknown_instance" };
@@ -12728,7 +12807,7 @@ export class FleetManager {
12728
12807
  const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
12729
12808
  const backend = this.fleetConfig?.defaults?.backend || "claude-code";
12730
12809
  try {
12731
- const { execFile } = await import("node:child_process");
12810
+ const { execFile } = await import("./child-process.js");
12732
12811
  const { promisify } = await import("node:util");
12733
12812
  // execFile with an argv array — no shell, so the backend name cannot be
12734
12813
  // interpreted as a command even if config is malformed.
@@ -12954,6 +13033,51 @@ export class FleetManager {
12954
13033
  }
12955
13034
  return { chatId, opts: {} };
12956
13035
  }
13036
+ /**
13037
+ * #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
13038
+ * gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
13039
+ * one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
13040
+ * nothing. A web-only fleet: everything, in the web chat.
13041
+ */
13042
+ postTaskBoardSummary(since, until) {
13043
+ const board = this.scheduler?.db.board;
13044
+ if (!board)
13045
+ return;
13046
+ const read = board.summaryRead(since.toISOString(), until.toISOString());
13047
+ const projects = board.listProjects(true);
13048
+ const stats = board.projectStats();
13049
+ const names = new Map(projects.map((p) => [p.id, p.name]));
13050
+ const configured = this.getPrimaryAdapterId();
13051
+ const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
13052
+ const worldOf = (assignee) => {
13053
+ const owner = assignee ? this.getInstanceAdapterId(assignee) : undefined;
13054
+ return owner !== undefined && this.worlds.has(owner) ? owner : primary;
13055
+ };
13056
+ const zone = this.usageZone();
13057
+ for (const world of this.worlds.size ? [...this.worlds.keys()] : [undefined]) {
13058
+ const summary = buildSummary(read, since, until, { inChat: (assignee) => worldOf(assignee) === world, projects, stats });
13059
+ if (isQuiet(summary))
13060
+ continue;
13061
+ const text = renderSummary(summary, { zone, t: (key, ...args) => t(key, ...args), projectOf: (c) => (c.projectId === null ? null : names.get(c.projectId) ?? null) });
13062
+ const cards = [...new Set([...summary.review, ...summary.blocked, ...summary.done, ...summary.created].map((c) => c.code))];
13063
+ if (world !== undefined) {
13064
+ const adapter = this.worlds.get(world)?.adapter;
13065
+ const target = this.fleetNoticeTarget(world);
13066
+ if (adapter && target) {
13067
+ adapter.sendText(target.chatId, text, target.opts).catch(e => this.logger.warn({ err: e, world }, "Failed to send the task board's daily summary"));
13068
+ }
13069
+ else {
13070
+ this.logger.warn({ world }, "The task board's daily summary has no postable target — set a General topic or channel.options.general_channel_id");
13071
+ }
13072
+ }
13073
+ for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
13074
+ if (cfg.general_topic !== true || (world !== undefined && this.getInstanceAdapterId(name) !== world))
13075
+ continue;
13076
+ // #2341 review: with the cards it names, so the public link withholds it while one of them is hidden there.
13077
+ this.emitSseEvent("message", { instance: name, sender: name, role: "status", text, ts: new Date().toISOString(), cards });
13078
+ }
13079
+ }
13080
+ }
12957
13081
  /**
12958
13082
  * Post the daily summary where fleet-wide notices go.
12959
13083
  *
@@ -13662,6 +13786,8 @@ export class FleetManager {
13662
13786
  return String(topic);
13663
13787
  return this.classicChannels?.getChannelIdByInstance(instance) ?? undefined;
13664
13788
  },
13789
+ laneTarget: instance => this.instanceChannelTarget(instance),
13790
+ taskRoute: code => this.needsYouRoute(code),
13665
13791
  isFleetAdmin: (userId, world) => this.isFleetAdmin(userId, world),
13666
13792
  fleetAdmins: world => (this.getChannelConfig(world)?.access?.allowed_users ?? []).map(String),
13667
13793
  emitSse: (event, data) => this.emitSseEvent(event, data),
@@ -13670,6 +13796,8 @@ export class FleetManager {
13670
13796
  throw new Error("the delivery outbox is not open");
13671
13797
  return this.deliveryOutbox.acknowledge(deliveryId, by);
13672
13798
  },
13799
+ tasks: () => this.needsYouTasks(),
13800
+ taskAction: action => this.taskActionFromChat(action),
13673
13801
  settings: () => ({
13674
13802
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
13675
13803
  dm: this.fleetConfig?.needs_you?.dm === true,
@@ -13680,6 +13808,98 @@ export class FleetManager {
13680
13808
  });
13681
13809
  this.needsYou.start();
13682
13810
  }
13811
+ /**
13812
+ * #2352: where an instance's own notices go — its topic in its world's group, or its ClassicBot room — as a Needs you
13813
+ * lane's place; null when it has neither (or its world has no group).
13814
+ */
13815
+ instanceChannelTarget(instance) {
13816
+ const cfg = this.fleetConfig?.instances && Object.hasOwn(this.fleetConfig.instances, instance) ? this.fleetConfig.instances[instance] : undefined;
13817
+ if (!cfg)
13818
+ return null;
13819
+ const groupId = this.getChannelConfig(this.getInstanceAdapterId(instance))?.group_id;
13820
+ if (cfg.topic_id != null && groupId != null)
13821
+ return { chatId: String(groupId), threadId: String(cfg.topic_id) };
13822
+ const classic = this.classicChannels?.getChannelIdByInstance(instance);
13823
+ return classic ? { chatId: classic } : null;
13824
+ }
13825
+ /**
13826
+ * #2352: the instance whose own channel lists a waiting card — the instance that handed it out, else its project's
13827
+ * lead — when that channel can be used now (a fleet instance of a live world, with a topic or room). undefined: its
13828
+ * assignee's world's General, as before.
13829
+ */
13830
+ needsYouLane(card, leads) {
13831
+ const usable = (name) => {
13832
+ if (!name || !this.fleetConfig?.instances || !Object.hasOwn(this.fleetConfig.instances, name))
13833
+ return false;
13834
+ const world = this.getInstanceAdapterId(name);
13835
+ return world !== undefined && this.worlds.has(world) && this.instanceChannelTarget(name) !== null;
13836
+ };
13837
+ const dispatcher = card.dispatcher?.kind === "instance" && card.dispatcher.name !== card.assignee ? card.dispatcher.name : null;
13838
+ if (dispatcher)
13839
+ return usable(dispatcher) ? dispatcher : undefined;
13840
+ const lead = card.projectId != null ? leads.get(card.projectId) : null;
13841
+ return usable(lead) ? lead : undefined;
13842
+ }
13843
+ /**
13844
+ * #2355 review: where a card's Needs you buttons belong now — read from the board and the fleet at the moment of a click
13845
+ * or a reply, never from the last render. null: it no longer waits for the person.
13846
+ */
13847
+ needsYouRoute(code) {
13848
+ const board = this.scheduler?.db.board;
13849
+ const card = board?.findCard(code);
13850
+ if (!board || !card || !(card.status === "blocked" || (card.status === "review" && card.needsReview)))
13851
+ return null;
13852
+ const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
13853
+ const lane = this.needsYouLane(card, leads);
13854
+ return lane ? { lane } : {};
13855
+ }
13856
+ /**
13857
+ * #2246 step 3: the task board's cards that wait for the person, for Needs you — under each card's assignee, listed in
13858
+ * its lane's channel (#2352: the instance that handed it out, else the project's lead), else in General.
13859
+ */
13860
+ needsYouTasks() {
13861
+ const board = this.scheduler?.db.board;
13862
+ if (!board)
13863
+ return [];
13864
+ const leads = new Map(board.listProjects(true).map((p) => [p.id, p.leadInstance]));
13865
+ return board.waitingForPerson().map(({ card, since }) => {
13866
+ const gap = card.status === "review" ? wrapUpGap(card) : null;
13867
+ const lane = this.needsYouLane(card, leads);
13868
+ return {
13869
+ uuid: card.uuid, code: card.code, title: card.title, status: card.status, version: card.version,
13870
+ assignee: card.assignee, since: Date.parse(since) || 0, ...(gap ? { gap } : {}), ...(lane ? { lane } : {}),
13871
+ note: card.status === "blocked" ? card.blockedReason : gap ? t("needs.task_gap", [
13872
+ gap.unchecked ? t("needs.task_gap_unchecked", gap.unchecked) : "", gap.noResult ? t("needs.task_gap_no_result") : "",
13873
+ ].filter(Boolean).join(t("needs.task_gap_join"))) : "",
13874
+ };
13875
+ });
13876
+ }
13877
+ /**
13878
+ * #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
13879
+ * hub), acting as the person, through the board's rules, at the version the button showed.
13880
+ */
13881
+ taskActionFromChat(action) {
13882
+ const board = this.scheduler?.db.board;
13883
+ if (!board)
13884
+ return { ok: false, message: t("needs.task_board_closed") };
13885
+ const person = { kind: "person", name: action.by.username || `${action.by.platform}:${action.by.userId}` };
13886
+ try {
13887
+ if (action.verb === "confirm")
13888
+ board.moveCard(person, action.code, "done", { version: action.version });
13889
+ else if (action.verb === "back")
13890
+ board.moveCard(person, action.code, "claimed", { version: action.version, reason: action.text });
13891
+ else
13892
+ board.moveCard(person, action.code, "claimed", { version: action.version, comment: action.text });
13893
+ const done = { confirm: "needs.task_confirmed", back: "needs.task_sent_back", answer: "needs.task_answered" }[action.verb];
13894
+ 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");
13895
+ return { ok: true, message: t(done, action.code) };
13896
+ }
13897
+ catch (err) {
13898
+ if (err instanceof TaskBoardError && err.code === "conflict")
13899
+ return { ok: false, message: t("needs.task_changed", action.code) };
13900
+ return { ok: false, message: t("needs.task_refused", action.code, err.message) };
13901
+ }
13902
+ }
13683
13903
  /** The prompts open on the dashboard right now (a page that loads after one was posted asks for them). */
13684
13904
  listWebPrompts() {
13685
13905
  const open = [];
@@ -16338,6 +16558,7 @@ export class FleetManager {
16338
16558
  channel: m.channel,
16339
16559
  from: m.from,
16340
16560
  replyTo: m.replyTo,
16561
+ cards: m.cards,
16341
16562
  });
16342
16563
  broadcastSseEvent(this.sseClients, event, recorded, onError, this.webChatHistory.cursorOf(recorded));
16343
16564
  this.searchIndex?.record(chatDoc(recorded));
@@ -16369,11 +16590,25 @@ export class FleetManager {
16369
16590
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
16370
16591
  holds: (wake) => db.board.wakeHolds(wake),
16371
16592
  card: (uuid) => db.board.findCard(uuid),
16593
+ // #2352: the card's Needs you buttons are in this instance's own channel (it is the card's lane).
16594
+ buttonsHere: (instance, uuid) => {
16595
+ // #2355 review: no live message, no buttons — the old words then.
16596
+ if (this.fleetConfig?.needs_you?.live_message === false)
16597
+ return false;
16598
+ const card = db.board.findCard(uuid);
16599
+ if (!card)
16600
+ return false;
16601
+ const leads = new Map(db.board.listProjects(true).map((p) => [p.id, p.leadInstance]));
16602
+ return this.needsYouLane(card, leads) === instance;
16603
+ },
16372
16604
  log: (level, message, extra) => this.logger[level](extra ?? {}, message),
16373
16605
  });
16374
16606
  db.board.onChange(({ rev, ids, projects, wakes }) => {
16375
16607
  this.emitSseEvent("tasks", { rev, ids, projects });
16376
16608
  waker.note(wakes);
16609
+ // step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
16610
+ if (ids.length || projects)
16611
+ this.needsYou?.poke();
16377
16612
  });
16378
16613
  }
16379
16614
  /** A task board notice, as AgEnD's own (not a person's words): through the delivery outbox, held until delivered. */
@@ -16587,6 +16822,27 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16587
16822
  this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
16588
16823
  this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
16589
16824
  }
16825
+ /** Update only AgEnD-managed skill copies; no launch or restart is needed for an experimental switch. */
16826
+ canvasWorkerSettingChanged() {
16827
+ this.refreshExperimentalSkills();
16828
+ this.emitSseEvent("status", this.getUiStatus());
16829
+ }
16830
+ refreshExperimentalSkills() {
16831
+ for (const [name, daemon] of this.daemons) {
16832
+ try {
16833
+ const cfg = daemon.getConfigSnapshot();
16834
+ const backend = cfg.backend ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
16835
+ const workDir = this.resolveKnowledgeWorkDir(cfg.working_directory, backend, name);
16836
+ const classic = this.classicChannels?.getAll().some(ch => ch.instanceName === name) === true;
16837
+ if (classic && !insideManagedWorkspaces(workDir))
16838
+ continue;
16839
+ this.syncRoleSkills(workDir, backend, classic ? "classic" : cfg.general_topic ? "general" : "worker");
16840
+ }
16841
+ catch (err) {
16842
+ this.logger.warn({ name, err }, "Could not refresh experimental skill visibility");
16843
+ }
16844
+ }
16845
+ }
16590
16846
  /** Publish only the bundled skills eligible for an instance role. */
16591
16847
  syncRoleSkills(workDir, backend, role, knowledgeDir) {
16592
16848
  const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
@@ -16751,7 +17007,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16751
17007
  continue;
16752
17008
  }
16753
17009
  managed.push(name);
16754
- const newContent = readFileSync(join(srcSkills, name, "SKILL.md"), "utf-8");
17010
+ const newContent = bundledSkillContents(join(srcSkills, name), this.canvasWorkerEnabled());
16755
17011
  if (refuse(dest))
16756
17012
  continue;
16757
17013
  try {
@@ -19192,14 +19448,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19192
19448
  */
19193
19449
  stopAll() {
19194
19450
  this.stopAllInFlight ??= this.doStopAll().finally(() => {
19195
- this.stopAllInFlight = null;
19196
19451
  // #1956: after the stop's own tmux work; anything later goes direct. A later startAll starts it again.
19197
19452
  if (this.tmuxExecHelperTimer) {
19198
19453
  clearInterval(this.tmuxExecHelperTimer);
19199
19454
  this.tmuxExecHelperTimer = null;
19200
19455
  }
19201
- tmuxExecHelper.stop();
19202
- });
19456
+ // #2364: and the stop waits (bounded) for the helper to leave on its own, before the caller exits the process.
19457
+ return tmuxExecHelper.stop();
19458
+ }).finally(() => { this.stopAllInFlight = null; });
19203
19459
  return this.stopAllInFlight;
19204
19460
  }
19205
19461
  tmuxExecHelperTimer = null;
@@ -19274,6 +19530,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19274
19530
  this.clearStatuslineWatchers();
19275
19531
  this.costGuard?.stop();
19276
19532
  this.dailySummary?.stop();
19533
+ this.taskBoardSummary?.stop();
19277
19534
  this.dailyTipScheduler?.stop();
19278
19535
  this.dailyTipScheduler = null;
19279
19536
  if (this.updateCheckTimer) {
@@ -19610,6 +19867,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19610
19867
  // mark every row done and tell the user "changes applied".
19611
19868
  return { rejected: why };
19612
19869
  }
19870
+ if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
19871
+ this.canvasWorkerSettingChanged();
19872
+ }
19613
19873
  // Classic behavior settings share the fleet defaults but are not entries
19614
19874
  // in fleet.yaml. Snapshot the old effective chain before reloading the
19615
19875
  // Classic file so SIGHUP can hot-apply either source without waiting for
@@ -19859,6 +20119,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19859
20119
  return false;
19860
20120
  }
19861
20121
  /** Non-null when startup found the running config and fleet.yaml disagreeing. */
20122
+ /**
20123
+ * #2346 (settings schema): whether a connection added now would start at once — what canHotAddConnections would decide
20124
+ * for the current config with one more connection appended: another one is running, and nothing else this process took
20125
+ * at startup has changed since (a pending locale or access change, say, needs AgEnD restarted anyway).
20126
+ */
20127
+ connectionsStartLive() {
20128
+ if (!this.fleetConfig)
20129
+ return false;
20130
+ const current = this.fleetConfig.channels ?? (this.fleetConfig.channel ? [this.fleetConfig.channel] : []);
20131
+ const probe = { id: "\u0000probe", type: "telegram", mode: "topic", bot_token_env: "\u0000PROBE" };
20132
+ const next = { ...this.fleetConfig, channels: [...current, probe] };
20133
+ delete next.channel;
20134
+ return this.canHotAddConnections(next);
20135
+ }
19862
20136
  fleetSignatureMismatchKeys() {
19863
20137
  return this.fleetSignatureMismatch;
19864
20138
  }
@@ -20113,9 +20387,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20113
20387
  const tunnel = spec.kind === "tunnel_token";
20114
20388
  if (!tunnel && !spec.verifier)
20115
20389
  return { ok: false, status: "unsupported_verifier", error: "this provider has no supported verifier" };
20116
- if (input.clear && (!tunnel || input.secret !== "") || tunnel && !(input.clear === true && input.secret === "" || validPublicTunnelToken(input.secret))
20117
- || !input.clear && (!input.secret || input.secret.length > 4096 || /[\r\n\0]/.test(input.secret) || /[^\x20-\x7e]/.test(input.secret))) {
20118
- return { ok: false, status: "invalid", error: "secret is invalid" };
20390
+ const secret = tunnel && !input.clear ? normalizePublicTunnelToken(input.secret) : input.secret;
20391
+ if (input.clear && (!tunnel || input.secret !== "")
20392
+ || !input.clear && (!secret || secret.length > 4096 || /[\r\n\0]/.test(secret) || /[^\x20-\x7e]/.test(secret))) {
20393
+ return { ok: false, status: "invalid", error: tunnel ? PUBLIC_TUNNEL_TOKEN_HINT : "secret is invalid" };
20119
20394
  }
20120
20395
  const challengeKey = `${input.sessionBinding}:api_key:${spec.id}:${spec.envKey}:${input.idempotencyKey}`;
20121
20396
  const existingId = this.providerSecretChallengesByKey.get(challengeKey);
@@ -20143,7 +20418,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20143
20418
  operation: "provider-secret.apply",
20144
20419
  idempotencyKey: input.idempotencyKey,
20145
20420
  expiresAt, deadline: performance.now() + SECRET_CHALLENGE_TTL_MS,
20146
- secret: input.secret,
20421
+ secret: secret,
20147
20422
  };
20148
20423
  this.providerSecretChallenges.set(challenge.id, challenge);
20149
20424
  this.providerSecretChallengesByKey.set(challengeKey, challenge.id);
@@ -21287,7 +21562,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21287
21562
  // up to 15s each, and on a beta build BOTH ran — 30s with no WATCHDOG ping,
21288
21563
  // past WatchdogSec's half-interval and enough for systemd to SIGABRT the fleet
21289
21564
  // for a background version check.
21290
- const { execFile } = await import("node:child_process");
21565
+ const { execFile } = await import("./child-process.js");
21291
21566
  const { promisify } = await import("node:util");
21292
21567
  const execFileP = promisify(execFile);
21293
21568
  const npmVersion = async (spec) => {
@@ -21579,6 +21854,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21579
21854
  return;
21580
21855
  }
21581
21856
  const requestPath = new URL(req.url ?? "/", `http://localhost:${port}`).pathname;
21857
+ if (!this.canvasWorkerEnabled() && isCanvasWorkerHttpPath(requestPath)) {
21858
+ res.writeHead(403, { "Content-Type": "application/json", "Cache-Control": "no-store" });
21859
+ res.end(JSON.stringify({ error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE }));
21860
+ return;
21861
+ }
21582
21862
  if (this.effectBootstrap.handle(req, res))
21583
21863
  return;
21584
21864
  // Branding is public, after Host admission: automatic favicon requests
@@ -21647,6 +21927,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21647
21927
  // #1920 §5.1: guests chat unless the host made them read-only (web.guests.chat: false).
21648
21928
  chat: () => this.fleetConfig?.web?.guests?.chat !== false });
21649
21929
  }
21930
+ if (decision.via === "session" && this.webToken) {
21931
+ const upgrade = sessionCookieUpgrade(req, this.webToken, decision.session);
21932
+ // No cookie header is set before this gate; auth routes own their headers separately.
21933
+ if (upgrade)
21934
+ res.setHeader("Set-Cookie", upgrade);
21935
+ }
21650
21936
  }
21651
21937
  if (req.method === "GET" && req.url === "/health") {
21652
21938
  const health = this.getFleetHealth();
@@ -22028,6 +22314,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22028
22314
  return;
22029
22315
  }
22030
22316
  const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
22317
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
22031
22318
  // #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
22032
22319
  extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; } });
22033
22320
  this.previewListener = listener;
@@ -22205,7 +22492,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22205
22492
  return {};
22206
22493
  } })(),
22207
22494
  // #2118: only this Chat's placement content; guest projection drops global revisions.
22208
- effect_chat_v: createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32),
22495
+ effect_chat_v: this.canvasWorkerEnabled() ? createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32) : null,
22209
22496
  // #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
22210
22497
  color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
22211
22498
  ...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
@@ -22232,6 +22519,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22232
22519
  };
22233
22520
  });
22234
22521
  return {
22522
+ canvas_worker: this.canvasWorkerEnabled(),
22235
22523
  instances,
22236
22524
  publicLink: this.getPublicWebStatus(),
22237
22525
  uptime: Math.floor((Date.now() - this.startedAt) / 1000),