crewly 1.20.306 → 1.20.308

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 (126) hide show
  1. package/config/hooks/agent-status/report.sh +39 -1
  2. package/dist/backend/backend/src/constants.d.ts +120 -0
  3. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  4. package/dist/backend/backend/src/constants.js +120 -0
  5. package/dist/backend/backend/src/constants.js.map +1 -1
  6. package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.d.ts +2 -1
  7. package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.d.ts.map +1 -1
  8. package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.js +18 -1
  9. package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.js.map +1 -1
  10. package/dist/backend/backend/src/controllers/board/board.controller.d.ts +23 -0
  11. package/dist/backend/backend/src/controllers/board/board.controller.d.ts.map +1 -0
  12. package/dist/backend/backend/src/controllers/board/board.controller.js +48 -0
  13. package/dist/backend/backend/src/controllers/board/board.controller.js.map +1 -0
  14. package/dist/backend/backend/src/controllers/board/board.routes.d.ts +15 -0
  15. package/dist/backend/backend/src/controllers/board/board.routes.d.ts.map +1 -0
  16. package/dist/backend/backend/src/controllers/board/board.routes.js +21 -0
  17. package/dist/backend/backend/src/controllers/board/board.routes.js.map +1 -0
  18. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
  19. package/dist/backend/backend/src/controllers/browser/browser.controller.js +2 -0
  20. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  21. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/chat/chat.controller.js +5 -0
  23. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  25. package/dist/backend/backend/src/controllers/slack/slack.controller.js +3 -0
  26. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  27. package/dist/backend/backend/src/index.d.ts +18 -0
  28. package/dist/backend/backend/src/index.d.ts.map +1 -1
  29. package/dist/backend/backend/src/index.js +172 -2
  30. package/dist/backend/backend/src/index.js.map +1 -1
  31. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  32. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  33. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  34. package/dist/backend/backend/src/services/board/board-activity.normalizer.d.ts +102 -0
  35. package/dist/backend/backend/src/services/board/board-activity.normalizer.d.ts.map +1 -0
  36. package/dist/backend/backend/src/services/board/board-activity.normalizer.js +244 -0
  37. package/dist/backend/backend/src/services/board/board-activity.normalizer.js.map +1 -0
  38. package/dist/backend/backend/src/services/board/board-activity.tracker.d.ts +159 -0
  39. package/dist/backend/backend/src/services/board/board-activity.tracker.d.ts.map +1 -0
  40. package/dist/backend/backend/src/services/board/board-activity.tracker.js +346 -0
  41. package/dist/backend/backend/src/services/board/board-activity.tracker.js.map +1 -0
  42. package/dist/backend/backend/src/services/board/board-outcome-phrases.d.ts +43 -0
  43. package/dist/backend/backend/src/services/board/board-outcome-phrases.d.ts.map +1 -0
  44. package/dist/backend/backend/src/services/board/board-outcome-phrases.js +76 -0
  45. package/dist/backend/backend/src/services/board/board-outcome-phrases.js.map +1 -0
  46. package/dist/backend/backend/src/services/board/board-push.service.d.ts +132 -0
  47. package/dist/backend/backend/src/services/board/board-push.service.d.ts.map +1 -0
  48. package/dist/backend/backend/src/services/board/board-push.service.js +374 -0
  49. package/dist/backend/backend/src/services/board/board-push.service.js.map +1 -0
  50. package/dist/backend/backend/src/services/board/board-redact.d.ts +90 -0
  51. package/dist/backend/backend/src/services/board/board-redact.d.ts.map +1 -0
  52. package/dist/backend/backend/src/services/board/board-redact.js +359 -0
  53. package/dist/backend/backend/src/services/board/board-redact.js.map +1 -0
  54. package/dist/backend/backend/src/services/board/board-snapshot.builder.d.ts +183 -0
  55. package/dist/backend/backend/src/services/board/board-snapshot.builder.d.ts.map +1 -0
  56. package/dist/backend/backend/src/services/board/board-snapshot.builder.js +344 -0
  57. package/dist/backend/backend/src/services/board/board-snapshot.builder.js.map +1 -0
  58. package/dist/backend/backend/src/services/board/board-usage.d.ts +20 -0
  59. package/dist/backend/backend/src/services/board/board-usage.d.ts.map +1 -0
  60. package/dist/backend/backend/src/services/board/board-usage.js +82 -0
  61. package/dist/backend/backend/src/services/board/board-usage.js.map +1 -0
  62. package/dist/backend/backend/src/services/board/board.service.d.ts +60 -0
  63. package/dist/backend/backend/src/services/board/board.service.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/board/board.service.js +82 -0
  65. package/dist/backend/backend/src/services/board/board.service.js.map +1 -0
  66. package/dist/backend/backend/src/services/board/board.sources.d.ts +25 -0
  67. package/dist/backend/backend/src/services/board/board.sources.d.ts.map +1 -0
  68. package/dist/backend/backend/src/services/board/board.sources.js +300 -0
  69. package/dist/backend/backend/src/services/board/board.sources.js.map +1 -0
  70. package/dist/backend/backend/src/services/board/board.types.d.ts +122 -0
  71. package/dist/backend/backend/src/services/board/board.types.d.ts.map +1 -0
  72. package/dist/backend/backend/src/services/board/board.types.js +8 -0
  73. package/dist/backend/backend/src/services/board/board.types.js.map +1 -0
  74. package/dist/backend/backend/src/services/board/canvas/canvas-inbound.service.d.ts +122 -0
  75. package/dist/backend/backend/src/services/board/canvas/canvas-inbound.service.d.ts.map +1 -0
  76. package/dist/backend/backend/src/services/board/canvas/canvas-inbound.service.js +339 -0
  77. package/dist/backend/backend/src/services/board/canvas/canvas-inbound.service.js.map +1 -0
  78. package/dist/backend/backend/src/services/board/canvas/canvas-redact.d.ts +44 -0
  79. package/dist/backend/backend/src/services/board/canvas/canvas-redact.d.ts.map +1 -0
  80. package/dist/backend/backend/src/services/board/canvas/canvas-redact.js +161 -0
  81. package/dist/backend/backend/src/services/board/canvas/canvas-redact.js.map +1 -0
  82. package/dist/backend/backend/src/services/board/canvas/canvas-step.normalizer.d.ts +57 -0
  83. package/dist/backend/backend/src/services/board/canvas/canvas-step.normalizer.d.ts.map +1 -0
  84. package/dist/backend/backend/src/services/board/canvas/canvas-step.normalizer.js +164 -0
  85. package/dist/backend/backend/src/services/board/canvas/canvas-step.normalizer.js.map +1 -0
  86. package/dist/backend/backend/src/services/board/canvas/canvas-step.stream.d.ts +140 -0
  87. package/dist/backend/backend/src/services/board/canvas/canvas-step.stream.d.ts.map +1 -0
  88. package/dist/backend/backend/src/services/board/canvas/canvas-step.stream.js +300 -0
  89. package/dist/backend/backend/src/services/board/canvas/canvas-step.stream.js.map +1 -0
  90. package/dist/backend/backend/src/services/board/canvas/canvas-terminal.service.d.ts +99 -0
  91. package/dist/backend/backend/src/services/board/canvas/canvas-terminal.service.d.ts.map +1 -0
  92. package/dist/backend/backend/src/services/board/canvas/canvas-terminal.service.js +223 -0
  93. package/dist/backend/backend/src/services/board/canvas/canvas-terminal.service.js.map +1 -0
  94. package/dist/backend/backend/src/services/board/canvas/canvas-transcript.tail.d.ts +126 -0
  95. package/dist/backend/backend/src/services/board/canvas/canvas-transcript.tail.d.ts.map +1 -0
  96. package/dist/backend/backend/src/services/board/canvas/canvas-transcript.tail.js +385 -0
  97. package/dist/backend/backend/src/services/board/canvas/canvas-transcript.tail.js.map +1 -0
  98. package/dist/backend/backend/src/services/board/canvas/canvas-wiring.d.ts +103 -0
  99. package/dist/backend/backend/src/services/board/canvas/canvas-wiring.d.ts.map +1 -0
  100. package/dist/backend/backend/src/services/board/canvas/canvas-wiring.js +124 -0
  101. package/dist/backend/backend/src/services/board/canvas/canvas-wiring.js.map +1 -0
  102. package/dist/backend/backend/src/services/board/canvas/canvas.types.d.ts +88 -0
  103. package/dist/backend/backend/src/services/board/canvas/canvas.types.d.ts.map +1 -0
  104. package/dist/backend/backend/src/services/board/canvas/canvas.types.js +8 -0
  105. package/dist/backend/backend/src/services/board/canvas/canvas.types.js.map +1 -0
  106. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
  107. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
  108. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +1 -0
  109. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
  110. package/dist/backend/backend/src/services/google/google-reauth-notifier.service.d.ts +13 -0
  111. package/dist/backend/backend/src/services/google/google-reauth-notifier.service.d.ts.map +1 -1
  112. package/dist/backend/backend/src/services/google/google-reauth-notifier.service.js +15 -0
  113. package/dist/backend/backend/src/services/google/google-reauth-notifier.service.js.map +1 -1
  114. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  115. package/dist/backend/backend/src/services/slack/slack-initializer.js +2 -1
  116. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  117. package/dist/backend/build-info.json +2 -2
  118. package/dist/cli/backend/src/constants.d.ts +120 -0
  119. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  120. package/dist/cli/backend/src/constants.js +120 -0
  121. package/dist/cli/backend/src/constants.js.map +1 -1
  122. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
  123. package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
  124. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +1 -0
  125. package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
  126. package/package.json +1 -1
@@ -77,6 +77,8 @@ import { parseSlackThreadKey } from './services/slack/slack-thread-key.js';
77
77
  import { LIVENESS_MONITOR_CONSTANTS, INPUT_CIRCUIT_CONSTANTS, INPUT_BLOCKED_RETRY_CONSTANTS } from './constants.js';
78
78
  import { InputBlockedRetryService } from './services/messaging/input-blocked-retry.service.js';
79
79
  import { driveCapabilities } from './services/drive/drive-agent.service.js';
80
+ import { canvasCapabilities } from './services/board/canvas/canvas-inbound.service.js';
81
+ import { isBoardPushDisabled } from './services/board/board-push.service.js';
80
82
  import { teamMoveCapabilities } from './services/team-move/team-move-driver.service.js';
81
83
  import { SUB_AGENT_QUEUE_CONSTANTS, CHAT_CONTEXT_CONSTANTS, SAFE_RESTART, AUTO_UPDATE_CONSTANTS, PROCESS_EXIT_CODES, CLAUDE_STARTUP_CONSTANTS, WEB_CONSTANTS, TICKET_CONSTANTS, UNASSIGNED_ROUTE_CONSTANTS, CLOUD_TALK_CONSTANTS, STANDING_ANSWERS_CONSTANTS, TICKET_AUTOPILOT_CONSTANTS, TICKET_HYGIENE_CONSTANTS, EXPERIMENT_CONSTANTS, WORK_ITEM_DESTINATION_CONSTANTS, CODEX_USAGE_SYNC_CONSTANTS, ANTIGRAVITY_USAGE_SYNC_CONSTANTS, OWNER_AUTH_CONSTANTS, CREWLY_APPS_CONSTANTS, SLACK_AGENT_DM_CONSTANTS, BRIEFING_CONSTANTS, DRIVE_CONSTANTS, POOL_PRUNE_CONSTANTS } from './constants.js';
82
84
  import { randomUUID } from 'crypto';
@@ -335,6 +337,11 @@ export class CrewlyServer {
335
337
  /** Gaps in this backend's life are told to the owner (crewly#1015 §12) */
336
338
  livenessMonitor = null;
337
339
  waitingItemsSync = null;
340
+ ownerBoard = null;
341
+ /** Canvas: follows the agents' transcripts for their own text lines (crewly#1247 v2) */
342
+ canvasTail = null;
343
+ /** Canvas: masked terminal streams the owner opened in the portal */
344
+ canvasTerminal = null;
338
345
  /** Epoch ms of the last shutdown signal acted on (dedups process-group delivery) */
339
346
  lastShutdownSignalAt = 0;
340
347
  /** Interrupted turns loaded at boot, resumed once their agents are back */
@@ -2958,6 +2965,78 @@ export class CrewlyServer {
2958
2965
  error: waitingErr instanceof Error ? waitingErr.message : String(waitingErr),
2959
2966
  });
2960
2967
  }
2968
+ // Crewly Canvas (crewly#1247 v2): the owner's actions from the portal
2969
+ // canvas (message an agent, stop it, open its masked terminal) arrive as
2970
+ // `canvas_action` pushes; the handler fetches each with this machine's
2971
+ // token and runs it. Starting it advertises `canvas_actions` to Cloud.
2972
+ try {
2973
+ // The board's kill switch also keeps the canvas actions off this machine
2974
+ if (isBoardPushDisabled())
2975
+ throw new Error('CREWLY_BOARD_PUSH=0');
2976
+ const [{ CanvasInboundService }, { CanvasTerminalService }, { createStopAgent, createSecretsProvider }, { CloudClientService }, { buildAgentRoster }, { intakeChatV2OwnerMessage }, { getTicketIntakeService }, { getBoardService }, { getApiToken }, { internalCredentialHeaders },] = await Promise.all([
2977
+ import('./services/board/canvas/canvas-inbound.service.js'),
2978
+ import('./services/board/canvas/canvas-terminal.service.js'),
2979
+ import('./services/board/canvas/canvas-wiring.js'),
2980
+ import('./services/cloud/cloud-client.service.js'),
2981
+ import('./services/cloud/agent-roster.utils.js'),
2982
+ import('./services/v3/ticket-channel-hooks.js'),
2983
+ import('./services/v3/ticket-intake.service.js'),
2984
+ import('./services/board/board.service.js'),
2985
+ import('./services/core/api-token.service.js'),
2986
+ import('./services/core/owner-auth.service.js'),
2987
+ ]);
2988
+ const cloudClient = CloudClientService.getInstance();
2989
+ const cloud = {
2990
+ getToken: () => cloudClient.getToken(),
2991
+ getCloudUrl: () => cloudClient.getCloudUrl(),
2992
+ tryRefreshToken: () => cloudClient.tryRefreshToken(),
2993
+ };
2994
+ const identity = async () => {
2995
+ const id = await DeviceIdentityService.getInstance().getOrCreateIdentity();
2996
+ return { instanceId: id.deviceId, deviceName: id.deviceName };
2997
+ };
2998
+ const terminal = new CanvasTerminalService({
2999
+ cloud,
3000
+ identity,
3001
+ capture: (session, lines) => getSessionBackendSync()?.captureOutput(session, lines) ?? null,
3002
+ exists: (session) => getSessionBackendSync()?.sessionExists(session) === true,
3003
+ secrets: createSecretsProvider(),
3004
+ });
3005
+ this.canvasTerminal = terminal;
3006
+ new CanvasInboundService({
3007
+ source: sync,
3008
+ cloud,
3009
+ chat: chatService,
3010
+ identity,
3011
+ deliver: async (channel, message) => {
3012
+ const toDispatch = await intakeChatV2OwnerMessage(getTicketIntakeService(), channel, message, CLOUD_TALK_CONSTANTS.INTAKE_ORIGIN);
3013
+ await chatDispatcher.dispatchMessage(channel, toDispatch);
3014
+ },
3015
+ agentExists: async (session) => buildAgentRoster(await this.storageService.getTeams()).some((a) => a.agentSession === session),
3016
+ stopAgent: createStopAgent({
3017
+ webPort: this.config.webPort,
3018
+ teams: () => this.storageService.getTeams(),
3019
+ headers: () => {
3020
+ let token = null;
3021
+ try {
3022
+ token = getApiToken();
3023
+ }
3024
+ catch {
3025
+ token = null;
3026
+ }
3027
+ return { ...(token ? { [API_SECURITY_CONSTANTS.TOKEN_HEADER]: token } : {}), ...internalCredentialHeaders('relay') };
3028
+ },
3029
+ }),
3030
+ terminal,
3031
+ // Someone is at the canvas: push live for a while
3032
+ onAction: () => getBoardService()?.push.noteViewer(),
3033
+ }).start();
3034
+ }
3035
+ catch (canvasErr) {
3036
+ this.logger.warn('Canvas action handler wiring skipped', {
3037
+ error: canvasErr instanceof Error ? canvasErr.message : String(canvasErr),
3038
+ });
3039
+ }
2961
3040
  // Mobile app: generic allowlisted REST passthrough over the same
2962
3041
  // relay (api_request → local HTTP → api_response). Non-fatal.
2963
3042
  try {
@@ -3872,6 +3951,9 @@ export class CrewlyServer {
3872
3951
  // "Waiting on you" (§F): tickets in 待验收 go to Crewly Cloud as text
3873
3952
  // snapshots, so the portal lists them for every machine.
3874
3953
  void this.startWaitingItemsSync();
3954
+ // Owner board (crewly#1247): outcomes + live activity, pushed to Cloud
3955
+ // every 60 s and on change. CREWLY_BOARD_PUSH=0 turns the push off.
3956
+ void this.startOwnerBoard();
3875
3957
  // V3-only as of spec 2026-05-06-task-management-v1-deprecation.md.
3876
3958
  // The legacy `TaskTrackingService.startAutoSync()` is gone — V3
3877
3959
  // task-pool reconciler owns lifecycle cleanup now.
@@ -5369,7 +5451,7 @@ export class CrewlyServer {
5369
5451
  // whether this machine takes `talk_message` — also for machines
5370
5452
  // without Slack, which never send the registry heartbeat.
5371
5453
  roster: async () => buildAgentRoster(await this.storageService.getTeams()),
5372
- capabilities: () => [...cloudTalkCapabilities(), ...waitingActionCapabilities(), ...talkTranscribeCapabilities(), ...driveCapabilities(), ...teamMoveCapabilities()],
5454
+ capabilities: () => [...cloudTalkCapabilities(), ...waitingActionCapabilities(), ...canvasCapabilities(), ...talkTranscribeCapabilities(), ...driveCapabilities(), ...teamMoveCapabilities()],
5373
5455
  onNewMessage: (listener) => {
5374
5456
  chat.on('chat_message', listener);
5375
5457
  return () => chat.off('chat_message', listener);
@@ -5441,7 +5523,7 @@ export class CrewlyServer {
5441
5523
  agentNames: async () => new Map(buildAgentRoster(await this.storageService.getTeams())
5442
5524
  .filter((a) => a.displayName)
5443
5525
  .map((a) => [a.agentSession, a.displayName])),
5444
- capabilities: () => [...cloudTalkCapabilities(), ...waitingActionCapabilities(), ...talkTranscribeCapabilities(), ...driveCapabilities(), ...teamMoveCapabilities()],
5526
+ capabilities: () => [...cloudTalkCapabilities(), ...waitingActionCapabilities(), ...canvasCapabilities(), ...talkTranscribeCapabilities(), ...driveCapabilities(), ...teamMoveCapabilities()],
5445
5527
  onTicketChange: (listener) => RequestService.getInstance().onChange(() => listener()),
5446
5528
  });
5447
5529
  setWaitingItemsSyncService(service);
@@ -5454,6 +5536,91 @@ export class CrewlyServer {
5454
5536
  });
5455
5537
  }
5456
5538
  }
5539
+ /**
5540
+ * Start the owner board: the local `GET /api/board` always works; the push
5541
+ * to Crewly Cloud runs while the machine is signed in. Never throws.
5542
+ */
5543
+ async startOwnerBoard() {
5544
+ try {
5545
+ const [{ BoardService, setBoardService }, { createProductionBoardSources }, { CloudClientService }, { getBoardActivity }] = await Promise.all([
5546
+ import('./services/board/board.service.js'),
5547
+ import('./services/board/board.sources.js'),
5548
+ import('./services/cloud/cloud-client.service.js'),
5549
+ import('./services/board/board-activity.tracker.js'),
5550
+ ]);
5551
+ const cloud = CloudClientService.getInstance();
5552
+ // Project roots turn file paths into repo-relative ones
5553
+ getBoardActivity().setRootsResolver((session) => this.boardProjectRoots(session));
5554
+ const service = new BoardService({
5555
+ sources: createProductionBoardSources({ storage: this.storageService }),
5556
+ cloud: { getToken: () => cloud.getToken(), getCloudUrl: () => cloud.getCloudUrl(), tryRefreshToken: () => cloud.tryRefreshToken() },
5557
+ });
5558
+ setBoardService(service);
5559
+ service.start();
5560
+ this.ownerBoard = service;
5561
+ await this.startCanvasTail(service);
5562
+ }
5563
+ catch (error) {
5564
+ this.logger.warn('Owner board not started (non-fatal)', { error: error instanceof Error ? error.message : String(error) });
5565
+ }
5566
+ }
5567
+ /**
5568
+ * Canvas (crewly#1247 v2): follow the agents' transcripts for their own
5569
+ * short text lines (and Codex's tool calls). Off with the board push.
5570
+ */
5571
+ async startCanvasTail(service) {
5572
+ try {
5573
+ if (service.push.disabled)
5574
+ return;
5575
+ const [{ TranscriptTailService }, { createTranscriptLocator, createSecretsProvider }, recovery] = await Promise.all([
5576
+ import('./services/board/canvas/canvas-transcript.tail.js'),
5577
+ import('./services/board/canvas/canvas-wiring.js'),
5578
+ import('./services/agent/runtime-session-recovery.js'),
5579
+ ]);
5580
+ const { findCodexRollout } = await import('./services/monitoring/codex-rollout-sync.service.js');
5581
+ const persistence = getSessionStatePersistence();
5582
+ const locate = createTranscriptLocator({
5583
+ meta: (session) => {
5584
+ try {
5585
+ return persistence.getSessionMetadata(session);
5586
+ }
5587
+ catch {
5588
+ return undefined;
5589
+ }
5590
+ },
5591
+ claudeHomes: (meta) => [meta.env?.CLAUDE_CONFIG_DIR, recovery.defaultClaudeHome()].filter((h) => typeof h === 'string' && h.length > 0),
5592
+ findClaude: recovery.findClaudeTranscript,
5593
+ codexHome: recovery.defaultCodexHome,
5594
+ findCodex: findCodexRollout,
5595
+ });
5596
+ this.canvasTail = new TranscriptTailService({
5597
+ steps: service.tracker.steps,
5598
+ sessions: () => service.builder.knownSessions(),
5599
+ locate,
5600
+ roots: (session) => this.boardProjectRoots(session),
5601
+ secrets: createSecretsProvider(),
5602
+ isLive: () => service.push.isLive(),
5603
+ });
5604
+ this.canvasTail.start();
5605
+ }
5606
+ catch (error) {
5607
+ this.logger.warn('Canvas transcript tail not started (non-fatal)', { error: error instanceof Error ? error.message : String(error) });
5608
+ }
5609
+ }
5610
+ /** Cached project paths for the board's relative file paths. */
5611
+ boardRootsCache = { at: 0, roots: [] };
5612
+ boardProjectRoots(_session) {
5613
+ if (Date.now() - this.boardRootsCache.at > 60_000) {
5614
+ this.boardRootsCache.at = Date.now();
5615
+ void this.storageService
5616
+ .getProjects()
5617
+ .then((projects) => {
5618
+ this.boardRootsCache.roots = projects.map((p) => p.path).filter(Boolean);
5619
+ })
5620
+ .catch(() => undefined);
5621
+ }
5622
+ return this.boardRootsCache.roots;
5623
+ }
5457
5624
  /**
5458
5625
  * Load interrupted turns left by the previous shutdown (fresh ones only).
5459
5626
  */
@@ -6761,6 +6928,9 @@ export class CrewlyServer {
6761
6928
  this.cloudDisconnectNotice?.stop();
6762
6929
  this.conversationCloudSync?.stop();
6763
6930
  this.waitingItemsSync?.stop();
6931
+ this.ownerBoard?.stop();
6932
+ this.canvasTail?.stop();
6933
+ this.canvasTerminal?.closeAll();
6764
6934
  // Safe restart: stop delivering, wait for agents mid-turn, persist the rest.
6765
6935
  // Runs before the force-exit timer below, which only bounds the teardown.
6766
6936
  await this.drainInFlightTurns(options.reason ?? 'shutdown', options.drain !== false);