@songsid/agend 2.3.0-beta.22 → 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 (256) 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 +8 -2
  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/kiro.d.ts +9 -1
  21. package/dist/backend/kiro.js +102 -1
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/opencode-permission.js +1 -1
  24. package/dist/backend/opencode-permission.js.map +1 -1
  25. package/dist/backend/opencode.js +2 -1
  26. package/dist/backend/opencode.js.map +1 -1
  27. package/dist/binary-probe.js +1 -1
  28. package/dist/bug-report-cli.js +1 -1
  29. package/dist/bundled-skills.d.ts +7 -3
  30. package/dist/bundled-skills.js +16 -7
  31. package/dist/bundled-skills.js.map +1 -1
  32. package/dist/channel/adapters/discord.js +3 -3
  33. package/dist/channel/adapters/discord.js.map +1 -1
  34. package/dist/channel/mcp-server.js +12 -4
  35. package/dist/channel/mcp-server.js.map +1 -1
  36. package/dist/channel/mcp-tools.d.ts +3 -1
  37. package/dist/channel/mcp-tools.js +36 -0
  38. package/dist/channel/mcp-tools.js.map +1 -1
  39. package/dist/child-process.d.ts +21 -0
  40. package/dist/child-process.js +54 -0
  41. package/dist/child-process.js.map +1 -0
  42. package/dist/cli.js +167 -182
  43. package/dist/cli.js.map +1 -1
  44. package/dist/command-table.d.ts +1 -1
  45. package/dist/command-table.js +1 -1
  46. package/dist/command-table.js.map +1 -1
  47. package/dist/config-validator.js +20 -1
  48. package/dist/config-validator.js.map +1 -1
  49. package/dist/config.js +3 -1
  50. package/dist/config.js.map +1 -1
  51. package/dist/daemon.d.ts +8 -0
  52. package/dist/daemon.js +31 -8
  53. package/dist/daemon.js.map +1 -1
  54. package/dist/darwin-memory.js +1 -1
  55. package/dist/doctor.d.ts +5 -0
  56. package/dist/doctor.js +42 -0
  57. package/dist/doctor.js.map +1 -1
  58. package/dist/export-import.js +1 -1
  59. package/dist/export-import.js.map +1 -1
  60. package/dist/fleet-level-config.d.ts +1 -1
  61. package/dist/fleet-level-config.js +2 -0
  62. package/dist/fleet-level-config.js.map +1 -1
  63. package/dist/fleet-manager.d.ts +45 -0
  64. package/dist/fleet-manager.js +301 -21
  65. package/dist/fleet-manager.js.map +1 -1
  66. package/dist/full-restart.d.ts +5 -5
  67. package/dist/full-restart.js +5 -5
  68. package/dist/full-restart.js.map +1 -1
  69. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  70. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
  71. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  72. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  73. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  74. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +16 -3
  75. package/dist/general-knowledge/steering/core-rules.md +1 -1
  76. package/dist/instance-config-impact.js +10 -3
  77. package/dist/instance-config-impact.js.map +1 -1
  78. package/dist/instance-lifecycle.js +4 -4
  79. package/dist/locale.js +118 -4
  80. package/dist/locale.js.map +1 -1
  81. package/dist/login-flows.js +1 -1
  82. package/dist/login-shell.js +3 -1
  83. package/dist/login-shell.js.map +1 -1
  84. package/dist/ls-rows.js +1 -1
  85. package/dist/needs-you-hub.d.ts +98 -1
  86. package/dist/needs-you-hub.js +313 -44
  87. package/dist/needs-you-hub.js.map +1 -1
  88. package/dist/needs-you-live.d.ts +5 -2
  89. package/dist/needs-you-live.js +6 -3
  90. package/dist/needs-you-live.js.map +1 -1
  91. package/dist/needs-you.d.ts +58 -3
  92. package/dist/needs-you.js +36 -3
  93. package/dist/needs-you.js.map +1 -1
  94. package/dist/outbound-schemas.d.ts +3 -3
  95. package/dist/public-link-progress.d.ts +12 -1
  96. package/dist/public-link-progress.js +5 -10
  97. package/dist/public-link-progress.js.map +1 -1
  98. package/dist/public-tunnel-config.d.ts +5 -0
  99. package/dist/public-tunnel-config.js +45 -1
  100. package/dist/public-tunnel-config.js.map +1 -1
  101. package/dist/public-web-link.js +6 -5
  102. package/dist/public-web-link.js.map +1 -1
  103. package/dist/quickstart.js +14 -8
  104. package/dist/quickstart.js.map +1 -1
  105. package/dist/resource-report.js +1 -1
  106. package/dist/scheduler/task-board.d.ts +41 -1
  107. package/dist/scheduler/task-board.js +150 -4
  108. package/dist/scheduler/task-board.js.map +1 -1
  109. package/dist/sd-notify.js +2 -1
  110. package/dist/sd-notify.js.map +1 -1
  111. package/dist/service-installer.d.ts +69 -0
  112. package/dist/service-installer.js +181 -0
  113. package/dist/service-installer.js.map +1 -1
  114. package/dist/service-owner.d.ts +44 -0
  115. package/dist/service-owner.js +48 -0
  116. package/dist/service-owner.js.map +1 -0
  117. package/dist/settings-api.d.ts +4 -0
  118. package/dist/settings-api.js +21 -3
  119. package/dist/settings-api.js.map +1 -1
  120. package/dist/settings-change.js +85 -2
  121. package/dist/settings-change.js.map +1 -1
  122. package/dist/settings-effect.js +2 -0
  123. package/dist/settings-effect.js.map +1 -1
  124. package/dist/settings-http-confirmation.d.ts +2 -0
  125. package/dist/settings-http-confirmation.js +3 -2
  126. package/dist/settings-http-confirmation.js.map +1 -1
  127. package/dist/settings-request-capability.d.ts +4 -1
  128. package/dist/settings-request-capability.js +11 -3
  129. package/dist/settings-request-capability.js.map +1 -1
  130. package/dist/setup-host.js +1 -1
  131. package/dist/setup-wizard.js +1 -1
  132. package/dist/setup-wizard.js.map +1 -1
  133. package/dist/task-board-summary.d.ts +101 -0
  134. package/dist/task-board-summary.js +177 -0
  135. package/dist/task-board-summary.js.map +1 -0
  136. package/dist/task-board-waker.d.ts +8 -1
  137. package/dist/task-board-waker.js +15 -6
  138. package/dist/task-board-waker.js.map +1 -1
  139. package/dist/tmux-control.js +1 -1
  140. package/dist/tmux-control.js.map +1 -1
  141. package/dist/tmux-exec-helper.d.ts +5 -1
  142. package/dist/tmux-exec-helper.js +13 -21
  143. package/dist/tmux-exec-helper.js.map +1 -1
  144. package/dist/tmux-manager.js +1 -1
  145. package/dist/tmux-read.js +1 -1
  146. package/dist/tmux-read.js.map +1 -1
  147. package/dist/topic-commands.d.ts +3 -0
  148. package/dist/topic-commands.js +29 -18
  149. package/dist/topic-commands.js.map +1 -1
  150. package/dist/transcript-monitor.js +5 -0
  151. package/dist/transcript-monitor.js.map +1 -1
  152. package/dist/transcript-sources.d.ts +2 -0
  153. package/dist/transcript-sources.js +4 -0
  154. package/dist/transcript-sources.js.map +1 -1
  155. package/dist/tunnel/cloudflared-install.js +3 -1
  156. package/dist/tunnel/cloudflared-install.js.map +1 -1
  157. package/dist/tunnel/cloudflared.d.ts +1 -1
  158. package/dist/tunnel/cloudflared.js +51 -10
  159. package/dist/tunnel/cloudflared.js.map +1 -1
  160. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  161. package/dist/tunnel/named-diagnostics.js +52 -0
  162. package/dist/tunnel/named-diagnostics.js.map +1 -0
  163. package/dist/tunnel/types.d.ts +2 -1
  164. package/dist/tunnel/types.js.map +1 -1
  165. package/dist/types.d.ts +9 -0
  166. package/dist/ui/app-effects.js +10 -4
  167. package/dist/ui/chat-strings.js +3 -0
  168. package/dist/ui/chat-thread.js +14 -7
  169. package/dist/ui/effect-trial-offer.js +2 -31
  170. package/dist/ui/effects-waker.js +2 -70
  171. package/dist/ui/fleet-task-card.js +5 -5
  172. package/dist/ui/fleet-task-common.js +41 -7
  173. package/dist/ui/fleet-task-work.js +107 -0
  174. package/dist/ui/fleet-tasks.js +46 -12
  175. package/dist/ui/image-lightbox.js +39 -1
  176. package/dist/ui/image-zoom.js +242 -0
  177. package/dist/ui/panel-chat.js +24 -13
  178. package/dist/ui/panel-details.js +2 -0
  179. package/dist/ui/panel-fleet.js +6 -2
  180. package/dist/ui/panel-needs.js +21 -6
  181. package/dist/ui/panel-settings.js +35 -13
  182. package/dist/ui/preview-panel.js +2 -1
  183. package/dist/ui/search-dialog.js +2 -1
  184. package/dist/ui/settings-appearance.js +33 -8
  185. package/dist/ui/settings-dialogs.js +33 -17
  186. package/dist/ui/settings-discord.js +6 -2
  187. package/dist/ui/settings-guests.js +2 -1
  188. package/dist/ui/settings-model.js +2 -11
  189. package/dist/ui/settings-mods.js +6 -3
  190. package/dist/ui/settings-strings.js +2 -0
  191. package/dist/ui/settings-token.js +3 -1
  192. package/dist/ui/settings-web-authority.js +6 -3
  193. package/dist/ui/settings-wizard.js +13 -7
  194. package/dist/ui/shared/app-i18n.js +2 -2
  195. package/dist/ui/shared/app-needs.js +2 -2
  196. package/dist/ui/shared/app-store.js +2 -2
  197. package/dist/ui/shared/app.css +80 -9
  198. package/dist/ui/shared/app.js +5 -1
  199. package/dist/ui/shared/effect-app-runtime.js +7 -5
  200. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  201. package/dist/ui/shared/effect-cursor.js +3 -2
  202. package/dist/ui/shared/effect-display-metadata.js +39 -0
  203. package/dist/ui/shared/effect-registry.js +4 -2
  204. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  205. package/dist/ui/shared/effect-stock-trust.js +1 -1
  206. package/dist/ui/shared/feature-flags.js +127 -0
  207. package/dist/ui/shared/identity-color.js +6 -3
  208. package/dist/ui/shared/instance-nav.js +3 -2
  209. package/dist/ui/shared/text-undo.js +210 -0
  210. package/dist/ui/shared/ui-dialog.js +4 -3
  211. package/dist/ui/shared/ui-icons.js +2 -0
  212. package/dist/update-dispatch.js +1 -1
  213. package/dist/update-launch.d.ts +1 -1
  214. package/dist/update-launch.js +1 -2
  215. package/dist/update-launch.js.map +1 -1
  216. package/dist/usage/providers.js +1 -1
  217. package/dist/usage/providers.js.map +1 -1
  218. package/dist/view-api.js +1 -1
  219. package/dist/web-api.d.ts +6 -0
  220. package/dist/web-api.js +152 -14
  221. package/dist/web-api.js.map +1 -1
  222. package/dist/web-appearance-tools.d.ts +1 -0
  223. package/dist/web-appearance-tools.js +7 -3
  224. package/dist/web-appearance-tools.js.map +1 -1
  225. package/dist/web-appearance.d.ts +4 -10
  226. package/dist/web-appearance.js +4 -3
  227. package/dist/web-appearance.js.map +1 -1
  228. package/dist/web-chat-history.d.ts +14 -0
  229. package/dist/web-chat-history.js +26 -1
  230. package/dist/web-chat-history.js.map +1 -1
  231. package/dist/web-experimental-http.d.ts +2 -0
  232. package/dist/web-experimental-http.js +12 -0
  233. package/dist/web-experimental-http.js.map +1 -0
  234. package/dist/web-experimental.d.ts +11 -0
  235. package/dist/web-experimental.js +49 -0
  236. package/dist/web-experimental.js.map +1 -0
  237. package/dist/web-guest-access.d.ts +1 -0
  238. package/dist/web-guest-access.js +4 -2
  239. package/dist/web-guest-access.js.map +1 -1
  240. package/dist/web-mod-store.d.ts +2 -0
  241. package/dist/web-mod-store.js +10 -0
  242. package/dist/web-mod-store.js.map +1 -1
  243. package/dist/web-preview.d.ts +2 -0
  244. package/dist/web-preview.js +2 -0
  245. package/dist/web-preview.js.map +1 -1
  246. package/dist/web-settings-patch.d.ts +2 -1
  247. package/dist/web-settings-patch.js +5 -1
  248. package/dist/web-settings-patch.js.map +1 -1
  249. package/dist/web-settings-tools.d.ts +1 -0
  250. package/dist/web-settings-tools.js +14 -10
  251. package/dist/web-settings-tools.js.map +1 -1
  252. package/dist/web-terminal.js +1 -1
  253. package/dist/web-theme-store.d.ts +2 -2
  254. package/dist/web-theme-store.js +6 -3
  255. package/dist/web-theme-store.js.map +1 -1
  256. package/package.json +1 -1
@@ -1,4 +1,4 @@
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
4
  import { SettingsConfirmationStore } from "./settings-confirmation.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";
@@ -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(),
@@ -2659,6 +2672,10 @@ export class FleetManager {
2659
2672
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
2660
2673
  this.warnAboutRemovedLoginMode(loaded);
2661
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);
2662
2679
  return this.fleetConfig;
2663
2680
  }
2664
2681
  relayLoginModeWarned = false;
@@ -2838,8 +2855,17 @@ export class FleetManager {
2838
2855
  this.logger.info(`Received SIGUSR1, initiating full restart (process reload)... ${describeSignalSource(this.dataDir, "SIGUSR1")}`);
2839
2856
  this.gracefulShutdownForReload()
2840
2857
  .then(() => {
2841
- this.logger.info("Full restart: shutdown complete, exiting for reload");
2842
- 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);
2843
2869
  })
2844
2870
  .catch(err => {
2845
2871
  this.logger.error({ err }, "Full restart: graceful shutdown failed");
@@ -2858,7 +2884,7 @@ export class FleetManager {
2858
2884
  const chatId = String(this.getChannelConfig(adapterId)?.group_id ?? data.channelId);
2859
2885
  this.beginUpdateProgress(adapter, chatId, data.channelId, messageId);
2860
2886
  }
2861
- const { spawn } = await import("node:child_process");
2887
+ const { spawn } = await import("./child-process.js");
2862
2888
  // Plain `agend update`: the CLI picks the channel from the version it is about to replace (a beta install
2863
2889
  // stays on beta). Deciding here, from the package.json next to THIS code, read a source checkout's 1.22.0 as
2864
2890
  // "not a beta" and sent a beta install to @latest. #1450 C5: the INSTALLED agend, verified, by absolute path.
@@ -4304,6 +4330,18 @@ export class FleetManager {
4304
4330
  async dispatchSlashCore(slash, adapterId, adapter) {
4305
4331
  // #1569: an alias (/web) is its command (/dashboard) from here on — the door, the table and the handler see one name.
4306
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
+ };
4307
4345
  const door = this.slashDoor(data, adapterId);
4308
4346
  if ("refusal" in door) {
4309
4347
  await data.respond(t(door.refusal)).catch(() => { });
@@ -4314,6 +4352,7 @@ export class FleetManager {
4314
4352
  // — before the table, which keeps /web for fleet admins. Per account: it touches no instance binding.
4315
4353
  // Decided synchronously, so an admin's /web still reaches the table with no await after the door (#1399).
4316
4354
  if (data.command === "dashboard" && this.isGuestWebAsk(adapterId, data.userId)) {
4355
+ noteDeprecatedDashboard();
4317
4356
  await this.guestWebCommand({ adapterId, userId: data.userId, username: data.username,
4318
4357
  arg: String(data.options?.action ?? "").trim().toLowerCase(), replyHere: text => data.respond(text), replyPrivately: text => data.respond(text) });
4319
4358
  return;
@@ -4342,6 +4381,7 @@ export class FleetManager {
4342
4381
  await data.respond(reply).catch(() => { });
4343
4382
  return;
4344
4383
  }
4384
+ noteDeprecatedDashboard();
4345
4385
  if (data.command === "start") {
4346
4386
  await this.handleClassicStartSlash(data, adapterId);
4347
4387
  }
@@ -7158,6 +7198,17 @@ export class FleetManager {
7158
7198
  return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
7159
7199
  });
7160
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();
7161
7212
  // Rotate classic channel chat logs daily (piggyback on daily summary timer)
7162
7213
  this.classicChannels?.rotateLogs();
7163
7214
  this.rotateInboxes();
@@ -7552,7 +7603,8 @@ export class FleetManager {
7552
7603
  }, 60 * 60 * 1000);
7553
7604
  const onRestart = () => this.onGracefulRestartSignal(() => process.once("SIGUSR2", onRestart));
7554
7605
  process.once("SIGUSR2", onRestart);
7555
- // 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.
7556
7608
  process.once("SIGUSR1", () => this.onFullRestartSignal());
7557
7609
  // A SIGHUP may arrive after the PID/general is available but before the
7558
7610
  // rest of startup finishes. Replay one coalesced reload only after all
@@ -8443,7 +8495,7 @@ export class FleetManager {
8443
8495
  // means we cannot verify the pane, which is treated as dead (the same
8444
8496
  // conclusion the old code reached only by throwing).
8445
8497
  try {
8446
- const { execFile } = await import("node:child_process");
8498
+ const { execFile } = await import("./child-process.js");
8447
8499
  const { promisify } = await import("node:util");
8448
8500
  const { getTmuxSocketName } = await import("./paths.js");
8449
8501
  // Honour socket isolation: without -L this queried the user's default
@@ -8993,6 +9045,22 @@ export class FleetManager {
8993
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");
8994
9046
  return;
8995
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
+ }
8996
9064
  // Access control — classic channels are open to all, others require an allowed
8997
9065
  // user. This runs before the dedup claim, and is judged by the adapter that
8998
9066
  // owns the topic rather than the one that happened to receive the message:
@@ -9103,6 +9171,10 @@ export class FleetManager {
9103
9171
  // #1920 step 4b-3: /web typed in a ClassicBot room (a group: `/web@ThisBot`) by someone who is not a fleet admin,
9104
9172
  // while guest sign-in is on: their guest code, privately — as anywhere else. An admin's goes on as before.
9105
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
+ }
9106
9178
  await this.guestWebCommand({ adapterId: msg.adapterId, userId: msg.userId, username: msg.username,
9107
9179
  arg: text.replace(/^\/(?:web|dashboard)/i, "").trim().toLowerCase(), replyHere: body => msgAdapter.sendText(chatId, body) });
9108
9180
  return;
@@ -9653,6 +9725,8 @@ export class FleetManager {
9653
9725
  }
9654
9726
  }
9655
9727
  checkToolPermission(sink, instanceName, tool) {
9728
+ if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
9729
+ return { allowed: false, message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
9656
9730
  const profile = resolveToolSet(this.fleetConfig?.instances[instanceName], instanceName);
9657
9731
  const allowed = mayUseTool(profile, tool);
9658
9732
  if (!allowed) {
@@ -10337,13 +10411,14 @@ export class FleetManager {
10337
10411
  if (!ipc || !fleetRequestId)
10338
10412
  return;
10339
10413
  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";
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";
10347
10422
  ipc.send({ type: responseType, fleetRequestId, error: message });
10348
10423
  }
10349
10424
  dispatchTypedIpc(name, msg) {
@@ -10836,6 +10911,8 @@ export class FleetManager {
10836
10911
  }
10837
10912
  /** #1629: `agend-agent theme …` — the same service as the tools; `instance` is the token's. */
10838
10913
  async handleWebThemeHttp(instance, tool, args) {
10914
+ if (isCanvasWorkerTool(tool) && !this.canvasWorkerEnabled())
10915
+ return { error: "canvas_worker_experimental", message: CANVAS_WORKER_EXPERIMENTAL_MESSAGE };
10839
10916
  if (WEB_EFFECT_TOOLS.has(tool)) {
10840
10917
  if (!this.isKnownInstance(instance))
10841
10918
  return { error: "unknown_instance" };
@@ -12728,7 +12805,7 @@ export class FleetManager {
12728
12805
  const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
12729
12806
  const backend = this.fleetConfig?.defaults?.backend || "claude-code";
12730
12807
  try {
12731
- const { execFile } = await import("node:child_process");
12808
+ const { execFile } = await import("./child-process.js");
12732
12809
  const { promisify } = await import("node:util");
12733
12810
  // execFile with an argv array — no shell, so the backend name cannot be
12734
12811
  // interpreted as a command even if config is malformed.
@@ -12954,6 +13031,51 @@ export class FleetManager {
12954
13031
  }
12955
13032
  return { chatId, opts: {} };
12956
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
+ }
12957
13079
  /**
12958
13080
  * Post the daily summary where fleet-wide notices go.
12959
13081
  *
@@ -13662,6 +13784,8 @@ export class FleetManager {
13662
13784
  return String(topic);
13663
13785
  return this.classicChannels?.getChannelIdByInstance(instance) ?? undefined;
13664
13786
  },
13787
+ laneTarget: instance => this.instanceChannelTarget(instance),
13788
+ taskRoute: code => this.needsYouRoute(code),
13665
13789
  isFleetAdmin: (userId, world) => this.isFleetAdmin(userId, world),
13666
13790
  fleetAdmins: world => (this.getChannelConfig(world)?.access?.allowed_users ?? []).map(String),
13667
13791
  emitSse: (event, data) => this.emitSseEvent(event, data),
@@ -13670,6 +13794,8 @@ export class FleetManager {
13670
13794
  throw new Error("the delivery outbox is not open");
13671
13795
  return this.deliveryOutbox.acknowledge(deliveryId, by);
13672
13796
  },
13797
+ tasks: () => this.needsYouTasks(),
13798
+ taskAction: action => this.taskActionFromChat(action),
13673
13799
  settings: () => ({
13674
13800
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
13675
13801
  dm: this.fleetConfig?.needs_you?.dm === true,
@@ -13680,6 +13806,98 @@ export class FleetManager {
13680
13806
  });
13681
13807
  this.needsYou.start();
13682
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
+ }
13683
13901
  /** The prompts open on the dashboard right now (a page that loads after one was posted asks for them). */
13684
13902
  listWebPrompts() {
13685
13903
  const open = [];
@@ -16338,6 +16556,7 @@ export class FleetManager {
16338
16556
  channel: m.channel,
16339
16557
  from: m.from,
16340
16558
  replyTo: m.replyTo,
16559
+ cards: m.cards,
16341
16560
  });
16342
16561
  broadcastSseEvent(this.sseClients, event, recorded, onError, this.webChatHistory.cursorOf(recorded));
16343
16562
  this.searchIndex?.record(chatDoc(recorded));
@@ -16369,11 +16588,25 @@ export class FleetManager {
16369
16588
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
16370
16589
  holds: (wake) => db.board.wakeHolds(wake),
16371
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
+ },
16372
16602
  log: (level, message, extra) => this.logger[level](extra ?? {}, message),
16373
16603
  });
16374
16604
  db.board.onChange(({ rev, ids, projects, wakes }) => {
16375
16605
  this.emitSseEvent("tasks", { rev, ids, projects });
16376
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();
16377
16610
  });
16378
16611
  }
16379
16612
  /** A task board notice, as AgEnD's own (not a person's words): through the delivery outbox, held until delivered. */
@@ -16587,6 +16820,27 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16587
16820
  this.syncRoleSkills(workDir, backend, "general", knowledgeDir);
16588
16821
  this.logger.debug({ knowledgeDir, workDir, backend }, "Synced general knowledge files");
16589
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
+ }
16590
16844
  /** Publish only the bundled skills eligible for an instance role. */
16591
16845
  syncRoleSkills(workDir, backend, role, knowledgeDir) {
16592
16846
  const skillSegments = FleetManager.SKILLS_DIR_SEGMENTS[backend];
@@ -16751,7 +17005,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
16751
17005
  continue;
16752
17006
  }
16753
17007
  managed.push(name);
16754
- const newContent = readFileSync(join(srcSkills, name, "SKILL.md"), "utf-8");
17008
+ const newContent = bundledSkillContents(join(srcSkills, name), this.canvasWorkerEnabled());
16755
17009
  if (refuse(dest))
16756
17010
  continue;
16757
17011
  try {
@@ -19274,6 +19528,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19274
19528
  this.clearStatuslineWatchers();
19275
19529
  this.costGuard?.stop();
19276
19530
  this.dailySummary?.stop();
19531
+ this.taskBoardSummary?.stop();
19277
19532
  this.dailyTipScheduler?.stop();
19278
19533
  this.dailyTipScheduler = null;
19279
19534
  if (this.updateCheckTimer) {
@@ -19610,6 +19865,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19610
19865
  // mark every row done and tell the user "changes applied".
19611
19866
  return { rejected: why };
19612
19867
  }
19868
+ if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
19869
+ this.canvasWorkerSettingChanged();
19870
+ }
19613
19871
  // Classic behavior settings share the fleet defaults but are not entries
19614
19872
  // in fleet.yaml. Snapshot the old effective chain before reloading the
19615
19873
  // Classic file so SIGHUP can hot-apply either source without waiting for
@@ -19859,6 +20117,20 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19859
20117
  return false;
19860
20118
  }
19861
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
+ }
19862
20134
  fleetSignatureMismatchKeys() {
19863
20135
  return this.fleetSignatureMismatch;
19864
20136
  }
@@ -20113,9 +20385,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20113
20385
  const tunnel = spec.kind === "tunnel_token";
20114
20386
  if (!tunnel && !spec.verifier)
20115
20387
  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" };
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" };
20119
20392
  }
20120
20393
  const challengeKey = `${input.sessionBinding}:api_key:${spec.id}:${spec.envKey}:${input.idempotencyKey}`;
20121
20394
  const existingId = this.providerSecretChallengesByKey.get(challengeKey);
@@ -20143,7 +20416,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
20143
20416
  operation: "provider-secret.apply",
20144
20417
  idempotencyKey: input.idempotencyKey,
20145
20418
  expiresAt, deadline: performance.now() + SECRET_CHALLENGE_TTL_MS,
20146
- secret: input.secret,
20419
+ secret: secret,
20147
20420
  };
20148
20421
  this.providerSecretChallenges.set(challenge.id, challenge);
20149
20422
  this.providerSecretChallengesByKey.set(challengeKey, challenge.id);
@@ -21287,7 +21560,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21287
21560
  // up to 15s each, and on a beta build BOTH ran — 30s with no WATCHDOG ping,
21288
21561
  // past WatchdogSec's half-interval and enough for systemd to SIGABRT the fleet
21289
21562
  // for a background version check.
21290
- const { execFile } = await import("node:child_process");
21563
+ const { execFile } = await import("./child-process.js");
21291
21564
  const { promisify } = await import("node:util");
21292
21565
  const execFileP = promisify(execFile);
21293
21566
  const npmVersion = async (spec) => {
@@ -21579,6 +21852,11 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21579
21852
  return;
21580
21853
  }
21581
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
+ }
21582
21860
  if (this.effectBootstrap.handle(req, res))
21583
21861
  return;
21584
21862
  // Branding is public, after Host admission: automatic favicon requests
@@ -22028,6 +22306,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22028
22306
  return;
22029
22307
  }
22030
22308
  const listener = createPreviewListener({ settings, healthPort: boundPort, config: this.fleetConfig,
22309
+ canvasWorkerEnabled: () => this.canvasWorkerEnabled(),
22031
22310
  // #1723: with a web.preview_origin, the open public link's dashboard frames this listener's preview too.
22032
22311
  extraOrigins: () => { const o = settings.origin ? this.publicWebLink?.openDashboardOrigin() : null; return o ? [o] : []; } });
22033
22312
  this.previewListener = listener;
@@ -22205,7 +22484,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22205
22484
  return {};
22206
22485
  } })(),
22207
22486
  // #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),
22487
+ effect_chat_v: this.canvasWorkerEnabled() ? createHash("sha256").update(JSON.stringify(this.themeStore.get(name).placements.effects)).digest("hex").slice(0, 32) : null,
22209
22488
  // #1958: the identity colour's tiers (the page resolves them: the agent's only while it shows agents' themes).
22210
22489
  color: { ...(colors.get(name)?.color ? { person: colors.get(name).color } : {}), ...(image.agentColor ? { agent: image.agentColor } : {}),
22211
22490
  ...(image.roleColor ? { role: image.roleColor } : {}), auto: autoIdentityColor(name) },
@@ -22232,6 +22511,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
22232
22511
  };
22233
22512
  });
22234
22513
  return {
22514
+ canvas_worker: this.canvasWorkerEnabled(),
22235
22515
  instances,
22236
22516
  publicLink: this.getPublicWebStatus(),
22237
22517
  uptime: Math.floor((Date.now() - this.startedAt) / 1000),