crewly 1.20.211 → 1.20.213

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 (205) hide show
  1. package/config/roles/orchestrator/prompt.md +1 -0
  2. package/config/skills/agent/core/send-message/SKILL.md +9 -0
  3. package/config/skills/orchestrator/delegate-task/SKILL.md +9 -0
  4. package/config/skills/orchestrator/get-team-status/SKILL.md +9 -0
  5. package/config/skills/orchestrator/get-team-status/execute.sh +5 -1
  6. package/config/skills/orchestrator/send-message/SKILL.md +9 -0
  7. package/config/skills/orchestrator/start-agent/SKILL.md +9 -0
  8. package/config/skills/orchestrator/start-team/SKILL.md +9 -0
  9. package/config/skills/team-leader/assign-ticket/SKILL.md +9 -0
  10. package/config/skills/team-leader/delegate-task/SKILL.md +9 -0
  11. package/dist/backend/backend/src/constants.d.ts +25 -0
  12. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  13. package/dist/backend/backend/src/constants.js +25 -0
  14. package/dist/backend/backend/src/constants.js.map +1 -1
  15. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts +12 -0
  16. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
  17. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +36 -1
  18. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
  19. package/dist/backend/backend/src/controllers/request-types.d.ts +10 -1
  20. package/dist/backend/backend/src/controllers/request-types.d.ts.map +1 -1
  21. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts +0 -36
  22. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +41 -1
  24. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  25. package/dist/backend/backend/src/controllers/team/team-pause.controller.d.ts +59 -0
  26. package/dist/backend/backend/src/controllers/team/team-pause.controller.d.ts.map +1 -0
  27. package/dist/backend/backend/src/controllers/team/team-pause.controller.js +161 -0
  28. package/dist/backend/backend/src/controllers/team/team-pause.controller.js.map +1 -0
  29. package/dist/backend/backend/src/controllers/team/team.controller.d.ts +14 -0
  30. package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
  31. package/dist/backend/backend/src/controllers/team/team.controller.js +78 -5
  32. package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
  33. package/dist/backend/backend/src/controllers/team/team.routes.d.ts.map +1 -1
  34. package/dist/backend/backend/src/controllers/team/team.routes.js +7 -1
  35. package/dist/backend/backend/src/controllers/team/team.routes.js.map +1 -1
  36. package/dist/backend/backend/src/index.d.ts.map +1 -1
  37. package/dist/backend/backend/src/index.js +18 -1
  38. package/dist/backend/backend/src/index.js.map +1 -1
  39. package/dist/backend/backend/src/models/Team.d.ts +5 -1
  40. package/dist/backend/backend/src/models/Team.d.ts.map +1 -1
  41. package/dist/backend/backend/src/models/Team.js +8 -0
  42. package/dist/backend/backend/src/models/Team.js.map +1 -1
  43. package/dist/backend/backend/src/services/agent/owner-stopped.registry.d.ts +7 -2
  44. package/dist/backend/backend/src/services/agent/owner-stopped.registry.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/agent/owner-stopped.registry.js +9 -3
  46. package/dist/backend/backend/src/services/agent/owner-stopped.registry.js.map +1 -1
  47. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  48. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +14 -4
  49. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  50. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +3 -1
  52. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  53. package/dist/backend/backend/src/services/chat-v2/ticket-line.utils.d.ts +29 -0
  54. package/dist/backend/backend/src/services/chat-v2/ticket-line.utils.d.ts.map +1 -0
  55. package/dist/backend/backend/src/services/chat-v2/ticket-line.utils.js +48 -0
  56. package/dist/backend/backend/src/services/chat-v2/ticket-line.utils.js.map +1 -0
  57. package/dist/backend/backend/src/services/cloud/conversation-cloud-sync.service.d.ts.map +1 -1
  58. package/dist/backend/backend/src/services/cloud/conversation-cloud-sync.service.js +3 -1
  59. package/dist/backend/backend/src/services/cloud/conversation-cloud-sync.service.js.map +1 -1
  60. package/dist/backend/backend/src/services/core/storage.service.d.ts.map +1 -1
  61. package/dist/backend/backend/src/services/core/storage.service.js +6 -0
  62. package/dist/backend/backend/src/services/core/storage.service.js.map +1 -1
  63. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.d.ts.map +1 -1
  64. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.js +6 -0
  65. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.js.map +1 -1
  66. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +12 -0
  67. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
  68. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js +59 -0
  69. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.js.map +1 -1
  70. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.d.ts.map +1 -1
  71. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js +12 -6
  72. package/dist/backend/backend/src/services/project-tickets/ticket-autopilot.service.js.map +1 -1
  73. package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
  74. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +5 -0
  75. package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
  76. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  77. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +4 -0
  78. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  79. package/dist/backend/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
  80. package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js +22 -0
  81. package/dist/backend/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
  82. package/dist/backend/backend/src/services/slack/slack-channel-members.d.ts +27 -0
  83. package/dist/backend/backend/src/services/slack/slack-channel-members.d.ts.map +1 -0
  84. package/dist/backend/backend/src/services/slack/slack-channel-members.js +54 -0
  85. package/dist/backend/backend/src/services/slack/slack-channel-members.js.map +1 -0
  86. package/dist/backend/backend/src/services/slack/slack-directory.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/slack/slack-directory.service.js +7 -2
  88. package/dist/backend/backend/src/services/slack/slack-directory.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  90. package/dist/backend/backend/src/services/slack/slack-initializer.js +3 -0
  91. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  92. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +12 -1
  93. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  94. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +57 -16
  95. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  96. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +48 -0
  97. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +155 -4
  99. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  100. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +11 -0
  101. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/task-pool/task-pool.service.js +36 -1
  103. package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  104. package/dist/backend/backend/src/services/task-pool/untargeted-router.d.ts.map +1 -1
  105. package/dist/backend/backend/src/services/task-pool/untargeted-router.js +14 -6
  106. package/dist/backend/backend/src/services/task-pool/untargeted-router.js.map +1 -1
  107. package/dist/backend/backend/src/services/team/team-pause-command.d.ts +97 -0
  108. package/dist/backend/backend/src/services/team/team-pause-command.d.ts.map +1 -0
  109. package/dist/backend/backend/src/services/team/team-pause-command.js +177 -0
  110. package/dist/backend/backend/src/services/team/team-pause-command.js.map +1 -0
  111. package/dist/backend/backend/src/services/team/team-pause-notice.d.ts +43 -0
  112. package/dist/backend/backend/src/services/team/team-pause-notice.d.ts.map +1 -0
  113. package/dist/backend/backend/src/services/team/team-pause-notice.js +61 -0
  114. package/dist/backend/backend/src/services/team/team-pause-notice.js.map +1 -0
  115. package/dist/backend/backend/src/services/team/team-pause.registry.d.ts +167 -0
  116. package/dist/backend/backend/src/services/team/team-pause.registry.d.ts.map +1 -0
  117. package/dist/backend/backend/src/services/team/team-pause.registry.js +263 -0
  118. package/dist/backend/backend/src/services/team/team-pause.registry.js.map +1 -0
  119. package/dist/backend/backend/src/services/team/team-pause.service.d.ts +172 -0
  120. package/dist/backend/backend/src/services/team/team-pause.service.d.ts.map +1 -0
  121. package/dist/backend/backend/src/services/team/team-pause.service.js +254 -0
  122. package/dist/backend/backend/src/services/team/team-pause.service.js.map +1 -0
  123. package/dist/backend/backend/src/services/team/team-pause.wiring.d.ts +30 -0
  124. package/dist/backend/backend/src/services/team/team-pause.wiring.d.ts.map +1 -0
  125. package/dist/backend/backend/src/services/team/team-pause.wiring.js +48 -0
  126. package/dist/backend/backend/src/services/team/team-pause.wiring.js.map +1 -0
  127. package/dist/backend/backend/src/services/v3/agent-auto-claim.service.d.ts.map +1 -1
  128. package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js +11 -0
  129. package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js.map +1 -1
  130. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts +9 -0
  131. package/dist/backend/backend/src/services/v3/trigger-engine.service.d.ts.map +1 -1
  132. package/dist/backend/backend/src/services/v3/trigger-engine.service.js +27 -2
  133. package/dist/backend/backend/src/services/v3/trigger-engine.service.js.map +1 -1
  134. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
  135. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +7 -0
  136. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
  137. package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
  138. package/dist/backend/backend/src/services/workflow/cron-task.service.js +14 -0
  139. package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
  140. package/dist/backend/backend/src/services/workflow/message-scheduler.service.d.ts.map +1 -1
  141. package/dist/backend/backend/src/services/workflow/message-scheduler.service.js +5 -0
  142. package/dist/backend/backend/src/services/workflow/message-scheduler.service.js.map +1 -1
  143. package/dist/backend/backend/src/services/workflow/scheduler.service.d.ts.map +1 -1
  144. package/dist/backend/backend/src/services/workflow/scheduler.service.js +6 -0
  145. package/dist/backend/backend/src/services/workflow/scheduler.service.js.map +1 -1
  146. package/dist/backend/backend/src/types/index.d.ts +22 -0
  147. package/dist/backend/backend/src/types/index.d.ts.map +1 -1
  148. package/dist/backend/backend/src/types/index.js.map +1 -1
  149. package/dist/backend/build-info.json +2 -2
  150. package/dist/cli/backend/src/constants.d.ts +25 -0
  151. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  152. package/dist/cli/backend/src/constants.js +25 -0
  153. package/dist/cli/backend/src/constants.js.map +1 -1
  154. package/dist/cli/backend/src/models/Team.d.ts +5 -1
  155. package/dist/cli/backend/src/models/Team.d.ts.map +1 -1
  156. package/dist/cli/backend/src/models/Team.js +8 -0
  157. package/dist/cli/backend/src/models/Team.js.map +1 -1
  158. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  159. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +14 -4
  160. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  161. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  162. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +3 -1
  163. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  164. package/dist/cli/backend/src/services/chat-v2/ticket-line.utils.d.ts +29 -0
  165. package/dist/cli/backend/src/services/chat-v2/ticket-line.utils.d.ts.map +1 -0
  166. package/dist/cli/backend/src/services/chat-v2/ticket-line.utils.js +48 -0
  167. package/dist/cli/backend/src/services/chat-v2/ticket-line.utils.js.map +1 -0
  168. package/dist/cli/backend/src/services/core/storage.service.d.ts.map +1 -1
  169. package/dist/cli/backend/src/services/core/storage.service.js +6 -0
  170. package/dist/cli/backend/src/services/core/storage.service.js.map +1 -1
  171. package/dist/cli/backend/src/services/slack/slack-agent-dm.service.d.ts.map +1 -1
  172. package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js +22 -0
  173. package/dist/cli/backend/src/services/slack/slack-agent-dm.service.js.map +1 -1
  174. package/dist/cli/backend/src/services/slack/slack-directory.service.d.ts.map +1 -1
  175. package/dist/cli/backend/src/services/slack/slack-directory.service.js +7 -2
  176. package/dist/cli/backend/src/services/slack/slack-directory.service.js.map +1 -1
  177. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +12 -1
  178. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  179. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +57 -16
  180. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  181. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +48 -0
  182. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  183. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +155 -4
  184. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  185. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +11 -0
  186. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  187. package/dist/cli/backend/src/services/task-pool/task-pool.service.js +36 -1
  188. package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  189. package/dist/cli/backend/src/services/team/team-pause-notice.d.ts +43 -0
  190. package/dist/cli/backend/src/services/team/team-pause-notice.d.ts.map +1 -0
  191. package/dist/cli/backend/src/services/team/team-pause-notice.js +61 -0
  192. package/dist/cli/backend/src/services/team/team-pause-notice.js.map +1 -0
  193. package/dist/cli/backend/src/services/team/team-pause.registry.d.ts +167 -0
  194. package/dist/cli/backend/src/services/team/team-pause.registry.d.ts.map +1 -0
  195. package/dist/cli/backend/src/services/team/team-pause.registry.js +263 -0
  196. package/dist/cli/backend/src/services/team/team-pause.registry.js.map +1 -0
  197. package/dist/cli/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
  198. package/dist/cli/backend/src/services/workflow/cron-task.service.js +14 -0
  199. package/dist/cli/backend/src/services/workflow/cron-task.service.js.map +1 -1
  200. package/dist/cli/backend/src/types/index.d.ts +22 -0
  201. package/dist/cli/backend/src/types/index.d.ts.map +1 -1
  202. package/dist/cli/backend/src/types/index.js.map +1 -1
  203. package/frontend/dist/assets/{index-f64b333a.js → index-917db1b8.js} +279 -279
  204. package/frontend/dist/index.html +1 -1
  205. package/package.json +1 -1
@@ -24,6 +24,8 @@
24
24
  *
25
25
  * @module services/slack/slack-team-channel.service
26
26
  */
27
+ import { isSessionPaused, pausedTeamOfSession } from '../team/team-pause.registry.js';
28
+ import { pausedSlackNotice, pausedThreadNotices } from '../team/team-pause-notice.js';
27
29
  import { notePerson } from '../people/people-directory.service.js';
28
30
  import { dedicatedDecisionFor } from '../people/dedicated-agent.js';
29
31
  import { getTicketIntakeService } from '../v3/ticket-intake.service.js';
@@ -88,6 +90,7 @@ export function slackChannelNameFor(teamName, prefix = '') {
88
90
  * awake room member. Null when nobody is awake (Cloud's fallback decides).
89
91
  *
90
92
  * Cloud's delivery facts come first (specs/2026-10-04-room-delivery-audit.md):
93
+ * with `nobody-awake`, Cloud's fallback machine is the owner; otherwise
91
94
  * the responder Cloud names (`delivery.owner`) is the owner — Cloud pushes it
92
95
  * first and names another if it cannot be reached. Without one, the owner is
93
96
  * computed among the machines Cloud delivered to (`delivery.targets`) only,
@@ -102,6 +105,11 @@ export function roomOwnerInstance(room) {
102
105
  const delivery = room.delivery;
103
106
  if (delivery?.owner)
104
107
  return delivery.owner;
108
+ // Nobody awake anywhere: Cloud's one fallback machine wakes its lead and
109
+ // every other machine defers — never dispatching to agents that are awake
110
+ // only by its own count (review of crewly-services#34: two machines acted).
111
+ if (delivery?.rule === 'nobody-awake' && room.fallback?.instanceId)
112
+ return room.fallback.instanceId;
105
113
  const targets = delivery && Array.isArray(delivery.targets) && delivery.targets.length > 0 ? delivery.targets : null;
106
114
  const awakeMembers = room.members.filter((m) => m.awake && (!targets || targets.includes(m.instanceId)));
107
115
  const awake = [...new Set(awakeMembers.map((m) => m.instanceId))].sort();
@@ -351,6 +359,8 @@ export class SlackTeamChannelService {
351
359
  deps;
352
360
  storePath;
353
361
  store = null;
362
+ pruneTimer = null;
363
+ pruneInterval = null;
354
364
  loading = null;
355
365
  unsubscribeStorage = null;
356
366
  unsubscribeIdentity = null;
@@ -423,6 +433,94 @@ export class SlackTeamChannelService {
423
433
  this.logger.warn('Team channel reconcile failed (non-fatal)', { error: err instanceof Error ? err.message : String(err) });
424
434
  });
425
435
  void this.rejoinMappedChannels();
436
+ // Ad-hoc rosters only ever grew: an agent stayed a "member" of a room its
437
+ // bot had left (or was never in), and Cloud counted that machine as being
438
+ // in the room (2026-10-03 / 2026-10-04, #content-team). Check against
439
+ // Slack now and periodically.
440
+ if (this.deps.listChannelMembers) {
441
+ const run = () => void this.pruneAdhocMembers().catch((err) => this.logger.warn('Ad-hoc room prune failed (non-fatal)', { error: err instanceof Error ? err.message : String(err) }));
442
+ this.pruneTimer = setTimeout(run, SLACK_TEAM_CHANNEL_CONSTANTS.ADHOC_PRUNE_FIRST_DELAY_MS);
443
+ this.pruneInterval = setInterval(run, SLACK_TEAM_CHANNEL_CONSTANTS.ADHOC_PRUNE_INTERVAL_MS);
444
+ this.pruneTimer.unref?.();
445
+ this.pruneInterval.unref?.();
446
+ }
447
+ }
448
+ /**
449
+ * Drop ad-hoc room members whose bot is not in the Slack channel.
450
+ *
451
+ * Each room is read with its members' own bot tokens, one after another,
452
+ * until one can read it (`conversations.members`): members whose bot user
453
+ * is not in that list are dropped. A bot that gets `not_in_channel` /
454
+ * `channel_not_found` is itself not in the room and is dropped. A member
455
+ * with no bot of its own, or a read that fails otherwise (rate limit,
456
+ * network), is kept — nothing is dropped on a guess. A dropped agent comes
457
+ * back on its own the next time a copy arrives through its app.
458
+ *
459
+ * @returns Per room changed: the channel and the sessions dropped
460
+ */
461
+ async pruneAdhocMembers() {
462
+ const list = this.deps.listChannelMembers;
463
+ const identities = this.deps.identities;
464
+ if (!list || !identities)
465
+ return [];
466
+ await this.load();
467
+ await identities.load();
468
+ const changed = [];
469
+ // Who of a room's members is not in the Slack channel, by the bot ids
470
+ // and tokens the identity store holds right now.
471
+ const absent = async (mapping) => {
472
+ const bots = (mapping.members ?? []).map((session) => {
473
+ const installed = identities.getInstalled(session);
474
+ return { session, token: installed?.botToken ?? null, botUserId: installed?.botUserId ?? identities.get(session)?.botUserId ?? null };
475
+ });
476
+ const notInChannel = new Set();
477
+ let inChannel = null;
478
+ for (const bot of bots) {
479
+ if (!bot.token)
480
+ continue;
481
+ const res = await list(mapping.slackChannelId, bot.token);
482
+ if (res.ok) {
483
+ inChannel = new Set(res.members);
484
+ break;
485
+ }
486
+ if (res.error === 'not_in_channel' || res.error === 'channel_not_found')
487
+ notInChannel.add(bot.session);
488
+ }
489
+ const seen = inChannel;
490
+ return bots.filter((b) => notInChannel.has(b.session) || (seen !== null && !!b.botUserId && !seen.has(b.botUserId))).map((b) => b.session);
491
+ };
492
+ let refreshed = false;
493
+ for (const mapping of this.store?.mappings ?? []) {
494
+ if (!isAdhocMapping(mapping) || (mapping.members ?? []).length === 0)
495
+ continue;
496
+ let removed = await absent(mapping);
497
+ if (removed.length > 0 && this.deps.refreshIdentities) {
498
+ // A reinstalled app has a new bot user id and token: re-read them and
499
+ // check again before dropping anyone, so a reinstall does not flap
500
+ // the roster (review of crewly#1067).
501
+ if (!refreshed) {
502
+ refreshed = true;
503
+ await this.deps.refreshIdentities().catch((err) => this.logger.warn('Could not refresh agent identities before pruning', { error: err instanceof Error ? err.message : String(err) }));
504
+ }
505
+ const again = new Set(await absent(mapping));
506
+ removed = removed.filter((s) => again.has(s));
507
+ }
508
+ if (removed.length === 0)
509
+ continue;
510
+ mapping.members = (mapping.members ?? []).filter((m) => !removed.includes(m));
511
+ this.deps.chat.setHuddleMembers(mapping.chatChannelId, mapping.members);
512
+ changed.push({ slackChannelId: mapping.slackChannelId, removed });
513
+ this.logger.info('Ad-hoc room members dropped — their bots are not in the Slack channel', {
514
+ slackChannel: `#${mapping.slackChannelName}`,
515
+ removed,
516
+ remaining: mapping.members,
517
+ });
518
+ }
519
+ if (changed.length > 0) {
520
+ await this.save();
521
+ this.deps.onRoomsChanged?.();
522
+ }
523
+ return changed;
426
524
  }
427
525
  /**
428
526
  * Make sure the master bot is still a member of every channel we map.
@@ -565,6 +663,12 @@ export class SlackTeamChannelService {
565
663
  for (const pending of this.unanswered.values())
566
664
  clearTimeout(pending.timer);
567
665
  this.unanswered.clear();
666
+ if (this.pruneTimer)
667
+ clearTimeout(this.pruneTimer);
668
+ if (this.pruneInterval)
669
+ clearInterval(this.pruneInterval);
670
+ this.pruneTimer = null;
671
+ this.pruneInterval = null;
568
672
  }
569
673
  // -------------------------------------------------------------------------
570
674
  // Store
@@ -1098,6 +1202,40 @@ export class SlackTeamChannelService {
1098
1202
  }
1099
1203
  return declined;
1100
1204
  }
1205
+ /**
1206
+ * Take every @'d agent of a paused team off the list
1207
+ * (specs/2026-10-04-team-pause.md): it is not woken; the thread gets one
1208
+ * short auto-reply from that agent's bot, at most once per thread.
1209
+ *
1210
+ * @param message - Inbound channel message
1211
+ * @param mentions - Sessions it addressed
1212
+ * @param nameOf - Display name of a session
1213
+ * @returns The sessions taken off
1214
+ */
1215
+ async declinePausedMentions(message, mentions, nameOf) {
1216
+ const declined = [];
1217
+ for (const sessionName of mentions) {
1218
+ const paused = pausedTeamOfSession(sessionName);
1219
+ if (!paused)
1220
+ continue;
1221
+ declined.push(sessionName);
1222
+ const threadTs = message.threadTs || message.ts;
1223
+ if (!pausedThreadNotices.claim(message.channelId, threadTs, paused.teamId))
1224
+ continue;
1225
+ const botToken = this.deps.identities?.getInstalled(sessionName)?.botToken;
1226
+ await this.deps.slack
1227
+ .sendMessage({
1228
+ channelId: message.channelId,
1229
+ text: pausedSlackNotice(paused, nameOf(sessionName), { toAgent: !!message.authorAgentSession }),
1230
+ threadTs,
1231
+ ...(botToken ? { botToken } : {}),
1232
+ skipChatV2Mirror: true,
1233
+ })
1234
+ .catch((err) => this.logger.warn('Could not post the paused-team notice', { sessionName, error: err instanceof Error ? err.message : String(err) }));
1235
+ this.logger.info('Paused agent @-mentioned in Slack — not woken, thread told', { sessionName, team: paused.teamName });
1236
+ }
1237
+ return declined;
1238
+ }
1101
1239
  /**
1102
1240
  * Route an inbound Slack message that arrived in a mapped channel.
1103
1241
  *
@@ -1218,8 +1356,14 @@ export class SlackTeamChannelService {
1218
1356
  // posts a polite decline from its own bot and is taken off the list —
1219
1357
  // nothing is dispatched to it and it is not woken.
1220
1358
  const declinedDedicated = !message.authorAgentSession && message.userId && resolved.mentions.length > 0 ? await this.declineDedicatedMentions(message, resolved.mentions) : [];
1221
- const declinedOnly = declinedDedicated.length > 0 && resolved.mentions.every((sess) => declinedDedicated.includes(sess));
1222
- for (const sess of declinedDedicated)
1359
+ // Agents of a paused team (specs/2026-10-04-team-pause.md) are taken off
1360
+ // too: not woken, and the thread is told once.
1361
+ const declinedPaused = resolved.mentions.length > 0
1362
+ ? await this.declinePausedMentions(message, resolved.mentions.filter((sess) => !declinedDedicated.includes(sess)), (sess) => candidates.find((c) => c.sessionName === sess)?.name ?? sess)
1363
+ : [];
1364
+ const declinedAll = [...declinedDedicated, ...declinedPaused];
1365
+ const declinedOnly = declinedAll.length > 0 && resolved.mentions.every((sess) => declinedAll.includes(sess));
1366
+ for (const sess of declinedAll)
1223
1367
  resolved.mentions.splice(resolved.mentions.indexOf(sess), 1);
1224
1368
  const agentsMentionedViaCloud = [...new Set([...(message.mentionedAgentSessions ?? []), ...namedElsewhere])];
1225
1369
  const mentionedElsewhere = agentsMentionedViaCloud.filter((sess) => !isLocal(sess));
@@ -2191,7 +2335,8 @@ export class SlackTeamChannelService {
2191
2335
  const team = teams.find((t) => t.id === mapping.teamId) ?? null;
2192
2336
  const isLocal = (s) => this.deps.isLocalAgent?.(s) ?? true;
2193
2337
  const roster = team ? teamChannelMembers(team) : null;
2194
- const local = (roster ? roster.map((m) => m.sessionName) : (mapping.members ?? [])).filter(isLocal);
2338
+ // Never a paused team's agent (specs/2026-10-04-team-pause.md).
2339
+ const local = (roster ? roster.map((m) => m.sessionName) : (mapping.members ?? [])).filter((s) => isLocal(s) && !isSessionPaused(s));
2195
2340
  if (local.length === 0)
2196
2341
  return null;
2197
2342
  const turns = this.deps.chat.queryRecentTurnsForDispatch?.(mapping.chatChannelId, undefined, SLACK_TEAM_CHANNEL_CONSTANTS.ROOM_LAST_SPEAKER_SCAN) ?? [];
@@ -2388,7 +2533,13 @@ export class SlackTeamChannelService {
2388
2533
  const me = this.deps.resolveInstanceId ? await this.deps.resolveInstanceId().catch(() => null) : null;
2389
2534
  const isHere = (m) => me ? m.instanceId === me : (this.deps.isLocalAgent?.(localAgentSession(m.agentSession)) ?? false);
2390
2535
  const owner = me ? roomOwnerInstance(room) : null;
2391
- return !!owner && owner !== me && room.members.some((m) => !isHere(m) && m.awake);
2536
+ if (!owner || owner === me)
2537
+ return false;
2538
+ // Cloud named the machine (its responder, or its fallback when nobody is
2539
+ // awake): that machine has the message and acts; this one defers.
2540
+ if (room.delivery?.owner || room.delivery?.rule === 'nobody-awake')
2541
+ return true;
2542
+ return room.members.some((m) => !isHere(m) && m.awake);
2392
2543
  }
2393
2544
  /**
2394
2545
  * Hand a room message to one agent, wherever it runs.