crewly 1.20.356 → 1.20.359

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 (277) hide show
  1. package/config/constants.ts +11 -0
  2. package/dist/backend/backend/src/constants.d.ts +313 -17
  3. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  4. package/dist/backend/backend/src/constants.js +316 -17
  5. package/dist/backend/backend/src/constants.js.map +1 -1
  6. package/dist/backend/backend/src/index.d.ts +21 -0
  7. package/dist/backend/backend/src/index.d.ts.map +1 -1
  8. package/dist/backend/backend/src/index.js +232 -15
  9. package/dist/backend/backend/src/index.js.map +1 -1
  10. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts +10 -2
  11. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  12. package/dist/backend/backend/src/services/agent/agent-registration.service.js +27 -2
  13. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  14. package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts +34 -1
  15. package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
  16. package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js +69 -13
  17. package/dist/backend/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
  18. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts +8 -0
  19. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
  20. package/dist/backend/backend/src/services/agent/idle-detection.service.js +26 -2
  21. package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
  22. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +9 -0
  23. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  24. package/dist/backend/backend/src/services/agent/resource-mode.service.js +16 -0
  25. package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
  26. package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts +44 -3
  27. package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
  28. package/dist/backend/backend/src/services/agent/runtime-session-recovery.js +126 -11
  29. package/dist/backend/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
  30. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +2 -0
  31. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  32. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +9 -1
  33. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  34. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts +150 -0
  35. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.d.ts.map +1 -0
  36. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js +324 -0
  37. package/dist/backend/backend/src/services/memory/agent-memory-refresh.service.js.map +1 -0
  38. package/dist/backend/backend/src/services/memory/auto-learning.subscriber.d.ts +9 -7
  39. package/dist/backend/backend/src/services/memory/auto-learning.subscriber.d.ts.map +1 -1
  40. package/dist/backend/backend/src/services/memory/auto-learning.subscriber.js +19 -17
  41. package/dist/backend/backend/src/services/memory/auto-learning.subscriber.js.map +1 -1
  42. package/dist/backend/backend/src/services/memory/memory.service.d.ts.map +1 -1
  43. package/dist/backend/backend/src/services/memory/memory.service.js +11 -0
  44. package/dist/backend/backend/src/services/memory/memory.service.js.map +1 -1
  45. package/dist/backend/backend/src/services/memory/startup-briefing.service.d.ts +114 -0
  46. package/dist/backend/backend/src/services/memory/startup-briefing.service.d.ts.map +1 -0
  47. package/dist/backend/backend/src/services/memory/startup-briefing.service.js +345 -0
  48. package/dist/backend/backend/src/services/memory/startup-briefing.service.js.map +1 -0
  49. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +33 -1
  50. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js +69 -4
  52. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
  53. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.d.ts +24 -1
  54. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.d.ts.map +1 -1
  55. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.js +54 -3
  56. package/dist/backend/backend/src/services/messaging/owner-thread-sentinel.wiring.js.map +1 -1
  57. package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts +9 -0
  58. package/dist/backend/backend/src/services/orc/owner-completion-report.service.d.ts.map +1 -1
  59. package/dist/backend/backend/src/services/orc/owner-completion-report.service.js +16 -2
  60. package/dist/backend/backend/src/services/orc/owner-completion-report.service.js.map +1 -1
  61. package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.d.ts +30 -0
  62. package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.d.ts.map +1 -0
  63. package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.js +48 -0
  64. package/dist/backend/backend/src/services/project-tickets/ticket-prefixes.js.map +1 -0
  65. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +8 -0
  66. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +29 -8
  68. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  69. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.d.ts +27 -0
  70. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.d.ts.map +1 -1
  71. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.js +78 -24
  72. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.service.js.map +1 -1
  73. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.d.ts +23 -0
  74. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js +38 -1
  76. package/dist/backend/backend/src/services/runtime-fallback/runtime-fallback.wiring.js.map +1 -1
  77. package/dist/backend/backend/src/services/session/session-binding.d.ts +25 -2
  78. package/dist/backend/backend/src/services/session/session-binding.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/session/session-binding.js +32 -4
  80. package/dist/backend/backend/src/services/session/session-binding.js.map +1 -1
  81. package/dist/backend/backend/src/services/slack/agent-coordination.d.ts +119 -0
  82. package/dist/backend/backend/src/services/slack/agent-coordination.d.ts.map +1 -0
  83. package/dist/backend/backend/src/services/slack/agent-coordination.js +174 -0
  84. package/dist/backend/backend/src/services/slack/agent-coordination.js.map +1 -0
  85. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  86. package/dist/backend/backend/src/services/slack/slack-initializer.js +7 -0
  87. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  88. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +167 -0
  89. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  90. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +619 -9
  91. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  92. package/dist/backend/backend/src/services/slack/slack-thread-context.service.d.ts +22 -0
  93. package/dist/backend/backend/src/services/slack/slack-thread-context.service.d.ts.map +1 -1
  94. package/dist/backend/backend/src/services/slack/slack-thread-context.service.js +49 -12
  95. package/dist/backend/backend/src/services/slack/slack-thread-context.service.js.map +1 -1
  96. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +7 -0
  97. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +25 -4
  99. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  100. package/dist/backend/backend/src/services/team-move/team-move-chat.d.ts +21 -1
  101. package/dist/backend/backend/src/services/team-move/team-move-chat.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/team-move/team-move-chat.js +54 -2
  103. package/dist/backend/backend/src/services/team-move/team-move-chat.js.map +1 -1
  104. package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts +9 -4
  105. package/dist/backend/backend/src/services/team-move/team-move-exporter.d.ts.map +1 -1
  106. package/dist/backend/backend/src/services/team-move/team-move-exporter.js +32 -10
  107. package/dist/backend/backend/src/services/team-move/team-move-exporter.js.map +1 -1
  108. package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts +19 -0
  109. package/dist/backend/backend/src/services/team-move/team-move-importer.d.ts.map +1 -1
  110. package/dist/backend/backend/src/services/team-move/team-move-importer.js +78 -3
  111. package/dist/backend/backend/src/services/team-move/team-move-importer.js.map +1 -1
  112. package/dist/backend/backend/src/services/team-move/team-move-runtime.d.ts.map +1 -1
  113. package/dist/backend/backend/src/services/team-move/team-move-runtime.js +8 -0
  114. package/dist/backend/backend/src/services/team-move/team-move-runtime.js.map +1 -1
  115. package/dist/backend/backend/src/services/team-move/team-move-transcripts.d.ts +106 -0
  116. package/dist/backend/backend/src/services/team-move/team-move-transcripts.d.ts.map +1 -0
  117. package/dist/backend/backend/src/services/team-move/team-move-transcripts.js +212 -0
  118. package/dist/backend/backend/src/services/team-move/team-move-transcripts.js.map +1 -0
  119. package/dist/backend/backend/src/services/team-move/team-move.types.d.ts +20 -0
  120. package/dist/backend/backend/src/services/team-move/team-move.types.d.ts.map +1 -1
  121. package/dist/backend/backend/src/services/team-move/team-move.types.js.map +1 -1
  122. package/dist/backend/backend/src/services/v3/agent-auto-claim.service.d.ts +2 -2
  123. package/dist/backend/backend/src/services/v3/agent-auto-claim.service.d.ts.map +1 -1
  124. package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js +11 -4
  125. package/dist/backend/backend/src/services/v3/agent-auto-claim.service.js.map +1 -1
  126. package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.d.ts.map +1 -1
  127. package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.js +3 -0
  128. package/dist/backend/backend/src/services/wiki/wiki-backlinks.service.js.map +1 -1
  129. package/dist/backend/backend/src/services/wiki/wiki-bookkeep.service.d.ts.map +1 -1
  130. package/dist/backend/backend/src/services/wiki/wiki-bookkeep.service.js +4 -1
  131. package/dist/backend/backend/src/services/wiki/wiki-bookkeep.service.js.map +1 -1
  132. package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.d.ts.map +1 -1
  133. package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.js +4 -0
  134. package/dist/backend/backend/src/services/wiki/wiki-cleanup.service.js.map +1 -1
  135. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts +36 -5
  136. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
  137. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.js +75 -7
  138. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
  139. package/dist/backend/backend/src/services/wiki/wiki-lint.service.d.ts.map +1 -1
  140. package/dist/backend/backend/src/services/wiki/wiki-lint.service.js +3 -1
  141. package/dist/backend/backend/src/services/wiki/wiki-lint.service.js.map +1 -1
  142. package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.d.ts +141 -0
  143. package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.d.ts.map +1 -0
  144. package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.js +324 -0
  145. package/dist/backend/backend/src/services/wiki/wiki-log-compile.service.js.map +1 -0
  146. package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts +142 -0
  147. package/dist/backend/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -0
  148. package/dist/backend/backend/src/services/wiki/wiki-log-entries.js +222 -0
  149. package/dist/backend/backend/src/services/wiki/wiki-log-entries.js.map +1 -0
  150. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts +93 -0
  151. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.d.ts.map +1 -0
  152. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js +142 -0
  153. package/dist/backend/backend/src/services/wiki/wiki-log-rotation.service.js.map +1 -0
  154. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts +104 -0
  155. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.d.ts.map +1 -0
  156. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js +192 -0
  157. package/dist/backend/backend/src/services/wiki/wiki-maintenance.service.js.map +1 -0
  158. package/dist/backend/backend/src/services/wiki/wiki-query.service.d.ts +3 -2
  159. package/dist/backend/backend/src/services/wiki/wiki-query.service.d.ts.map +1 -1
  160. package/dist/backend/backend/src/services/wiki/wiki-query.service.js +23 -39
  161. package/dist/backend/backend/src/services/wiki/wiki-query.service.js.map +1 -1
  162. package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.d.ts +105 -0
  163. package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.d.ts.map +1 -0
  164. package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.js +195 -0
  165. package/dist/backend/backend/src/services/wiki/wiki-queue-drain.service.js.map +1 -0
  166. package/dist/backend/backend/src/services/wiki/wiki-recent.service.d.ts.map +1 -1
  167. package/dist/backend/backend/src/services/wiki/wiki-recent.service.js +3 -0
  168. package/dist/backend/backend/src/services/wiki/wiki-recent.service.js.map +1 -1
  169. package/dist/backend/backend/src/services/wiki/wiki-search.service.d.ts +28 -5
  170. package/dist/backend/backend/src/services/wiki/wiki-search.service.d.ts.map +1 -1
  171. package/dist/backend/backend/src/services/wiki/wiki-search.service.js +96 -25
  172. package/dist/backend/backend/src/services/wiki/wiki-search.service.js.map +1 -1
  173. package/dist/backend/backend/src/services/wiki/wiki-vault-walk.d.ts +11 -0
  174. package/dist/backend/backend/src/services/wiki/wiki-vault-walk.d.ts.map +1 -1
  175. package/dist/backend/backend/src/services/wiki/wiki-vault-walk.js +17 -1
  176. package/dist/backend/backend/src/services/wiki/wiki-vault-walk.js.map +1 -1
  177. package/dist/backend/backend/src/services/wiki/work-record-transcript.d.ts +64 -0
  178. package/dist/backend/backend/src/services/wiki/work-record-transcript.d.ts.map +1 -0
  179. package/dist/backend/backend/src/services/wiki/work-record-transcript.js +194 -0
  180. package/dist/backend/backend/src/services/wiki/work-record-transcript.js.map +1 -0
  181. package/dist/backend/backend/src/services/wiki/work-record.d.ts +239 -0
  182. package/dist/backend/backend/src/services/wiki/work-record.d.ts.map +1 -0
  183. package/dist/backend/backend/src/services/wiki/work-record.js +631 -0
  184. package/dist/backend/backend/src/services/wiki/work-record.js.map +1 -0
  185. package/dist/backend/backend/src/services/wiki/work-record.service.d.ts +209 -0
  186. package/dist/backend/backend/src/services/wiki/work-record.service.d.ts.map +1 -0
  187. package/dist/backend/backend/src/services/wiki/work-record.service.js +514 -0
  188. package/dist/backend/backend/src/services/wiki/work-record.service.js.map +1 -0
  189. package/dist/backend/build-info.json +2 -2
  190. package/dist/backend/config/constants.d.ts +11 -0
  191. package/dist/backend/config/constants.d.ts.map +1 -1
  192. package/dist/backend/config/constants.js +11 -0
  193. package/dist/backend/config/constants.js.map +1 -1
  194. package/dist/cli/backend/src/constants.d.ts +313 -17
  195. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  196. package/dist/cli/backend/src/constants.js +316 -17
  197. package/dist/cli/backend/src/constants.js.map +1 -1
  198. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts +10 -2
  199. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  200. package/dist/cli/backend/src/services/agent/agent-registration.service.js +27 -2
  201. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  202. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts +34 -1
  203. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.d.ts.map +1 -1
  204. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js +69 -13
  205. package/dist/cli/backend/src/services/agent/fresh-task-conversation.service.js.map +1 -1
  206. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +9 -0
  207. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  208. package/dist/cli/backend/src/services/agent/resource-mode.service.js +16 -0
  209. package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
  210. package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts +44 -3
  211. package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
  212. package/dist/cli/backend/src/services/agent/runtime-session-recovery.js +126 -11
  213. package/dist/cli/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
  214. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +2 -0
  215. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  216. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +9 -1
  217. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  218. package/dist/cli/backend/src/services/memory/memory.service.d.ts.map +1 -1
  219. package/dist/cli/backend/src/services/memory/memory.service.js +11 -0
  220. package/dist/cli/backend/src/services/memory/memory.service.js.map +1 -1
  221. package/dist/cli/backend/src/services/memory/startup-briefing.service.d.ts +114 -0
  222. package/dist/cli/backend/src/services/memory/startup-briefing.service.d.ts.map +1 -0
  223. package/dist/cli/backend/src/services/memory/startup-briefing.service.js +345 -0
  224. package/dist/cli/backend/src/services/memory/startup-briefing.service.js.map +1 -0
  225. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts +33 -1
  226. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.d.ts.map +1 -1
  227. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js +69 -4
  228. package/dist/cli/backend/src/services/messaging/owner-thread-sentinel.service.js.map +1 -1
  229. package/dist/cli/backend/src/services/slack/agent-coordination.d.ts +119 -0
  230. package/dist/cli/backend/src/services/slack/agent-coordination.d.ts.map +1 -0
  231. package/dist/cli/backend/src/services/slack/agent-coordination.js +174 -0
  232. package/dist/cli/backend/src/services/slack/agent-coordination.js.map +1 -0
  233. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +167 -0
  234. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  235. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +619 -9
  236. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  237. package/dist/cli/backend/src/services/slack/slack-thread-context.service.d.ts +22 -0
  238. package/dist/cli/backend/src/services/slack/slack-thread-context.service.d.ts.map +1 -1
  239. package/dist/cli/backend/src/services/slack/slack-thread-context.service.js +49 -12
  240. package/dist/cli/backend/src/services/slack/slack-thread-context.service.js.map +1 -1
  241. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +7 -0
  242. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  243. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +25 -4
  244. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  245. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts +36 -5
  246. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
  247. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.js +75 -7
  248. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
  249. package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts +142 -0
  250. package/dist/cli/backend/src/services/wiki/wiki-log-entries.d.ts.map +1 -0
  251. package/dist/cli/backend/src/services/wiki/wiki-log-entries.js +222 -0
  252. package/dist/cli/backend/src/services/wiki/wiki-log-entries.js.map +1 -0
  253. package/dist/cli/backend/src/services/wiki/wiki-recent.service.d.ts +51 -0
  254. package/dist/cli/backend/src/services/wiki/wiki-recent.service.d.ts.map +1 -0
  255. package/dist/cli/backend/src/services/wiki/wiki-recent.service.js +105 -0
  256. package/dist/cli/backend/src/services/wiki/wiki-recent.service.js.map +1 -0
  257. package/dist/cli/backend/src/services/wiki/wiki-search.service.d.ts +28 -5
  258. package/dist/cli/backend/src/services/wiki/wiki-search.service.d.ts.map +1 -1
  259. package/dist/cli/backend/src/services/wiki/wiki-search.service.js +96 -25
  260. package/dist/cli/backend/src/services/wiki/wiki-search.service.js.map +1 -1
  261. package/dist/cli/backend/src/services/wiki/wiki-vault-walk.d.ts +11 -0
  262. package/dist/cli/backend/src/services/wiki/wiki-vault-walk.d.ts.map +1 -1
  263. package/dist/cli/backend/src/services/wiki/wiki-vault-walk.js +17 -1
  264. package/dist/cli/backend/src/services/wiki/wiki-vault-walk.js.map +1 -1
  265. package/dist/cli/backend/src/services/wiki/work-record-transcript.d.ts +64 -0
  266. package/dist/cli/backend/src/services/wiki/work-record-transcript.d.ts.map +1 -0
  267. package/dist/cli/backend/src/services/wiki/work-record-transcript.js +194 -0
  268. package/dist/cli/backend/src/services/wiki/work-record-transcript.js.map +1 -0
  269. package/dist/cli/backend/src/services/wiki/work-record.d.ts +239 -0
  270. package/dist/cli/backend/src/services/wiki/work-record.d.ts.map +1 -0
  271. package/dist/cli/backend/src/services/wiki/work-record.js +631 -0
  272. package/dist/cli/backend/src/services/wiki/work-record.js.map +1 -0
  273. package/dist/cli/config/constants.d.ts +11 -0
  274. package/dist/cli/config/constants.d.ts.map +1 -1
  275. package/dist/cli/config/constants.js +11 -0
  276. package/dist/cli/config/constants.js.map +1 -1
  277. package/package.json +1 -1
@@ -55,6 +55,8 @@ import { resolveSlackMentions, extractNativeMentionIds, leadingNameMention } fro
55
55
  import { toSlackMrkdwn } from './slack-mrkdwn.js';
56
56
  import { renderSlackThreadContext } from './slack-thread-context.service.js';
57
57
  import { trackSlackDelivery, SLACK_DELIVERY_METADATA_KEY } from './slack-outbound-delivery.js';
58
+ import { classifyAgentAddressing, stripTicketIds, CoordinationDedupe } from './agent-coordination.js';
59
+ import { AGENT_COORDINATION_CONSTANTS } from '../../constants.js';
58
60
  const EMPTY_STORE = {
59
61
  version: 1,
60
62
  autoCreate: true,
@@ -458,6 +460,17 @@ export class SlackTeamChannelService {
458
460
  * guessing.
459
461
  */
460
462
  rootPosts = new Map();
463
+ /** Near-identical agent-to-agent repeats (crewly#1340). */
464
+ coordinationDedupe = new CoordinationDedupe();
465
+ /**
466
+ * `chatChannelId:helper` → the agent that last asked it something here, and
467
+ * when — so a reply the helper cannot post in Slack goes back to the asker.
468
+ */
469
+ askedBy = new Map();
470
+ /** `slackChannelId:YYYY-MM-DD` → Slack ts of that day's cross-machine coordination thread. */
471
+ coordinationThreads = new Map();
472
+ /** Ticket prefixes, cached. */
473
+ ticketPrefixCache = null;
461
474
  constructor(deps) {
462
475
  this.deps = deps;
463
476
  this.storePath =
@@ -485,6 +498,7 @@ export class SlackTeamChannelService {
485
498
  void this.inviteInstalledEverywhere(record.agentSession);
486
499
  }) ?? null;
487
500
  this.started = true;
501
+ await this.healMissingChatChannels();
488
502
  await this.restoreUnanswered();
489
503
  this.logger.info('Slack team channels started', {
490
504
  mappings: this.store?.mappings.length ?? 0,
@@ -512,6 +526,57 @@ export class SlackTeamChannelService {
512
526
  this.pruneInterval.unref?.();
513
527
  }
514
528
  }
529
+ /**
530
+ * Give every mapping a chat channel that exists. A mapping whose
531
+ * `chatChannelId` is not in the chat store (a team moved here without its
532
+ * room, a wiped chat.db) made every Slack turn reach its agents with no
533
+ * local thread history and `GET …/messages` answer 404 (crewly#1344,
534
+ * #air-steamfun). A fresh huddle with the known roster takes its place.
535
+ *
536
+ * @returns Mappings re-pointed
537
+ */
538
+ async healMissingChatChannels() {
539
+ const store = await this.load();
540
+ let healed = 0;
541
+ for (const mapping of store.mappings) {
542
+ let exists = false;
543
+ try {
544
+ exists = this.deps.chat.getChannelForBridge(mapping.chatChannelId) !== null;
545
+ }
546
+ catch {
547
+ exists = true; // the store cannot say: leave the mapping alone
548
+ }
549
+ if (exists)
550
+ continue;
551
+ try {
552
+ const team = (await this.deps.storage.getTeams()).find((t) => t.id === mapping.teamId);
553
+ const roster = mapping.members && mapping.members.length > 0 ? mapping.members : team ? teamChannelMembers(team).map((m) => m.sessionName) : [];
554
+ const huddle = this.deps.chat.createHuddle({
555
+ name: `#${mapping.slackChannelName}`,
556
+ purpose: team ? `Slack team channel for ${team.name}` : `Slack channel #${mapping.slackChannelName}`,
557
+ memberSessions: roster.length > 0 ? roster : [`team:${mapping.teamId}`],
558
+ principal: { userId: 'system', source: 'oss' },
559
+ });
560
+ this.logger.warn('Slack channel mapping pointed at a chat channel that does not exist here — created a new one', {
561
+ slackChannel: `#${mapping.slackChannelName}`,
562
+ teamId: mapping.teamId,
563
+ missing: mapping.chatChannelId,
564
+ chatChannelId: huddle.id,
565
+ });
566
+ mapping.chatChannelId = huddle.id;
567
+ healed += 1;
568
+ }
569
+ catch (err) {
570
+ this.logger.warn('Could not recreate the chat channel behind a Slack mapping', {
571
+ slackChannel: `#${mapping.slackChannelName}`,
572
+ error: err instanceof Error ? err.message : String(err),
573
+ });
574
+ }
575
+ }
576
+ if (healed > 0)
577
+ await this.save();
578
+ return healed;
579
+ }
515
580
  /**
516
581
  * Drop ad-hoc room members whose bot is not in the Slack channel.
517
582
  *
@@ -1878,10 +1943,19 @@ export class SlackTeamChannelService {
1878
1943
  ...(bareMentionNote ? { bareMentionNote } : {}),
1879
1944
  ...(slackContext
1880
1945
  ? {
1881
- slackContextFor: (session) => renderSlackThreadContext(slackContext, {
1882
- botUserId: this.deps.identities?.get(session)?.botUserId,
1883
- name: members.find((m) => m.sessionName === session)?.name,
1884
- }),
1946
+ slackContextFor: (session) => {
1947
+ const block = renderSlackThreadContext(slackContext, {
1948
+ botUserId: this.deps.identities?.get(session)?.botUserId,
1949
+ name: members.find((m) => m.sessionName === session)?.name,
1950
+ });
1951
+ // Where the whole thread lives on this machine, the way the
1952
+ // orchestrator path names its thread file (crewly#1344): the
1953
+ // block is a clipped window, the chat thread is the record.
1954
+ const localThread = threadId ?? persisted.threadId;
1955
+ return block && localThread
1956
+ ? `${block}\n[Full thread: chat channel ${mapping.chatChannelId}, thread ${localThread} — read it with search-chat when the lines above are not enough]`
1957
+ : block;
1958
+ },
1885
1959
  }
1886
1960
  : {}),
1887
1961
  });
@@ -3397,6 +3471,7 @@ export class SlackTeamChannelService {
3397
3471
  */
3398
3472
  async mirrorOutboundDetailed(dto) {
3399
3473
  const skipped = { attempted: false, delivery: null };
3474
+ let mappedForFailure = null;
3400
3475
  try {
3401
3476
  // Every reason a reply does not reach Slack is logged. Until now all
3402
3477
  // four were silent `return false`, so an agent could answer, be told
@@ -3440,6 +3515,15 @@ export class SlackTeamChannelService {
3440
3515
  const mapping = this.findByChatChannelId(dto.channelId);
3441
3516
  if (!mapping)
3442
3517
  return skip('channel-not-mapped-to-slack');
3518
+ mappedForFailure = mapping;
3519
+ // Agent-to-agent coordination (crewly#1340). Agents on this machine
3520
+ // talking to each other stay inside Crewly: the owner had 20+ unread
3521
+ // top-level posts in 90 minutes from three agents on his own Mac
3522
+ // (2026-10-16, #content-team). Decided before the Slack check, since
3523
+ // inside Crewly needs no Slack.
3524
+ const coordination = await this.coordinate(mapping, dto);
3525
+ if (coordination.handled)
3526
+ return skip(coordination.reason);
3443
3527
  if (!this.deps.slack.isConnected())
3444
3528
  return skip('slack-not-connected');
3445
3529
  // A reply under a top-level post that is still being sent waits for
@@ -3450,15 +3534,28 @@ export class SlackTeamChannelService {
3450
3534
  const resolvedTs = this.resolveOutboundThreadTs(mapping, dto);
3451
3535
  if (resolvedTs === null)
3452
3536
  return skip('thread-key-names-another-channel');
3453
- const threadTs = resolvedTs;
3454
- if (!threadTs)
3455
- return await this.postTopLevel(mapping, dto);
3537
+ // Agents on other machines can only be reached through Slack: in the
3538
+ // room's coordination thread of the day, not as a new top-level post.
3539
+ const threadTs = resolvedTs || (coordination.addressing === 'cross-machine' ? this.coordinationThreadTs(mapping) : undefined);
3540
+ if (!threadTs) {
3541
+ const posted = await this.postTopLevel(mapping, dto);
3542
+ if (coordination.addressing === 'cross-machine')
3543
+ this.rememberCoordinationThread(mapping, dto);
3544
+ return posted;
3545
+ }
3546
+ if (!resolvedTs) {
3547
+ this.logger.info('Agent-to-agent message across machines posted in the room\'s coordination thread', {
3548
+ slackChannel: mapping.slackChannelName,
3549
+ sender: dto.senderId,
3550
+ threadTs,
3551
+ });
3552
+ }
3456
3553
  const member = await this.findPostingMember(mapping, dto.senderId);
3457
3554
  // A real bot user (Cloud-provisioned identity) beats the cosmetic
3458
3555
  // username/icon override.
3459
3556
  const installed = this.deps.identities?.getInstalled(dto.senderId) ?? null;
3460
3557
  const identity = installed ? { botToken: installed.botToken } : slackIdentityFor(member, dto.senderId);
3461
- const text = await this.linkAgentMentions(toSlackMrkdwn(dto.content), mapping.slackChannelId);
3558
+ const text = await this.linkAgentMentions(toSlackMrkdwn(await this.withoutTicketIds(dto.content)), mapping.slackChannelId);
3462
3559
  if (this.deps.typing) {
3463
3560
  const typingKey = { agentSession: dto.senderId, slackChannelId: mapping.slackChannelId, ...(threadTs ? { threadTs } : {}) };
3464
3561
  const typingIdentity = installed
@@ -3506,6 +3603,14 @@ export class SlackTeamChannelService {
3506
3603
  }
3507
3604
  catch (err) {
3508
3605
  const error = err instanceof Error ? err.message : String(err);
3606
+ // A helper whose bot is not in this Slack channel (Sam in #content-team,
3607
+ // `channel_not_found` on every post, 2026-10-16) does not fail and is
3608
+ // not retried: its reply goes to the agent that asked it, who is in the
3609
+ // room and posts the update (crewly#1340).
3610
+ if (mappedForFailure && dto.senderType === 'agent' && isNotInChannelError(err)) {
3611
+ if (await this.handBackUnpostable(mappedForFailure, dto, describeSlackError(err).code))
3612
+ return skipped;
3613
+ }
3509
3614
  this.logger.warn('Outbound mirror to Slack failed', {
3510
3615
  channelId: dto.channelId,
3511
3616
  sender: dto.senderId,
@@ -3520,6 +3625,476 @@ export class SlackTeamChannelService {
3520
3625
  };
3521
3626
  }
3522
3627
  }
3628
+ // -------------------------------------------------------------------------
3629
+ // Agent-to-agent coordination (crewly#1340)
3630
+ // -------------------------------------------------------------------------
3631
+ /**
3632
+ * Decide what happens to an agent's room post that addresses other agents.
3633
+ *
3634
+ * The model is a company group chat: whoever the room asked is its one
3635
+ * voice; it asks its helpers privately and posts one update when their
3636
+ * result is in. So a post that @'s only agents on this machine — no
3637
+ * person, no name nobody knows, not inside a thread a person is in — is
3638
+ * delivered inside Crewly (the same dispatcher prompt the Slack round-trip
3639
+ * produced) and never posted to Slack. A reply in such an internal thread
3640
+ * stays internal too, and goes to the thread's other agents when it @'s
3641
+ * nobody. Everything else is left to the Slack path; posts that reach an
3642
+ * agent on another machine are threaded there.
3643
+ *
3644
+ * Until now a local colleague heard such a post only through the Slack
3645
+ * round-trip (its bot copy came back in and was dispatched from a
3646
+ * transient `slack-echo-…` turn), so every one of them was a Slack post —
3647
+ * and a colleague whose bot is not in the channel could not answer: its
3648
+ * reply named that transient thread and was refused.
3649
+ *
3650
+ * @param mapping - The room
3651
+ * @param dto - The agent's post
3652
+ * @returns Handled here (with the reason it is not posted), or the addressing for the Slack path
3653
+ */
3654
+ async coordinate(mapping, dto) {
3655
+ const unchanged = { handled: false, addressing: 'other' };
3656
+ if (!this.deps.isLocalAgent)
3657
+ return unchanged;
3658
+ try {
3659
+ const root = dto.threadId ? this.deps.chat.getMessageForBridge(dto.threadId) : null;
3660
+ const internalThread = typeof root?.metadata?.[AGENT_COORDINATION_CONSTANTS.INTERNAL_METADATA_KEY] === 'string';
3661
+ // A thread a person started or spoke in is theirs to read: agents
3662
+ // answering there stay visible.
3663
+ if (dto.threadId && !internalThread && this.personInThread(dto.channelId, dto.threadId, root))
3664
+ return unchanged;
3665
+ // Nothing @'d and no internal thread to continue: nothing to decide.
3666
+ if (!internalThread && !(dto.content ?? '').includes('@'))
3667
+ return unchanged;
3668
+ const addressing = await this.agentAddressing(dto);
3669
+ let kind = addressing.kind;
3670
+ let recipients = addressing.local;
3671
+ const addressesNobody = addressing.local.length === 0 && addressing.remote.length === 0 && addressing.others.length === 0 && !addressing.addressesPeople;
3672
+ if (internalThread && root && addressesNobody) {
3673
+ recipients = this.internalThreadAgents(root, dto.senderId);
3674
+ kind = recipients.length > 0 ? 'local-only' : 'other';
3675
+ }
3676
+ if (kind !== 'local-only')
3677
+ return { handled: false, addressing: kind };
3678
+ if (!(await this.roomHears(mapping, recipients)))
3679
+ return unchanged;
3680
+ // "Working on it" from a colleague to a colleague: nobody needs it.
3681
+ if (isInterim(dto))
3682
+ return { handled: true, reason: 'agent-to-agent-local-interim' };
3683
+ const now = (this.deps.now?.() ?? new Date()).getTime();
3684
+ if (this.coordinationDedupe.isRepeat(mapping.chatChannelId, dto.senderId, recipients, dto.content, now)) {
3685
+ this.markInternal(dto, 'agent-to-agent-duplicate');
3686
+ this.logger.info('Agent-to-agent message dropped — the same agent sent these colleagues the same message recently', {
3687
+ slackChannel: mapping.slackChannelName,
3688
+ sender: dto.senderId,
3689
+ recipients,
3690
+ messageId: dto.id,
3691
+ });
3692
+ return { handled: true, reason: 'agent-to-agent-duplicate' };
3693
+ }
3694
+ const delivered = await this.deliverInternally(mapping, dto, recipients, 'agent-to-agent-local');
3695
+ // No dispatcher (start-up): Slack is the only way left — never lost.
3696
+ if (!delivered)
3697
+ return unchanged;
3698
+ return { handled: true, reason: 'agent-to-agent-local' };
3699
+ }
3700
+ catch (err) {
3701
+ this.logger.warn('Agent-to-agent routing failed — posting to Slack as before', {
3702
+ messageId: dto.id,
3703
+ error: err instanceof Error ? err.message : String(err),
3704
+ });
3705
+ return unchanged;
3706
+ }
3707
+ }
3708
+ /**
3709
+ * Whom an agent post @'s: agents on this machine, agents on other machines
3710
+ * (Cloud's room roster and the directory), and anything else — a person,
3711
+ * a broadcast, a name nobody knows (each of which keeps the post in Slack).
3712
+ *
3713
+ * @param dto - The agent's post
3714
+ * @returns The resolved names and their class
3715
+ */
3716
+ async agentAddressing(dto) {
3717
+ const text = dto.content ?? '';
3718
+ const teams = await this.deps.storage.getTeams();
3719
+ if (this.deps.identities)
3720
+ await this.deps.identities.load();
3721
+ // The room's own members first: with two local agents of one name
3722
+ // ("Ella" of two teams), the one in this room is meant.
3723
+ const roster = new Set((this.deps.chat.getChannelForBridge(dto.channelId)?.members ?? []).map((m) => m.sessionName));
3724
+ const candidates = teams
3725
+ .flatMap((t) => teamChannelMembers(t))
3726
+ .sort((a, b) => Number(roster.has(b.sessionName)) - Number(roster.has(a.sessionName)))
3727
+ .map((m) => ({ name: m.name, sessionName: m.sessionName, botUserId: this.deps.identities?.get(m.sessionName)?.botUserId }));
3728
+ const resolved = resolveSlackMentions(text, candidates);
3729
+ const isLocal = (s) => this.deps.isLocalAgent?.(s) ?? false;
3730
+ const local = resolved.mentions.filter((s) => s !== dto.senderId && isLocal(s));
3731
+ const others = resolved.mentions.filter((s) => s !== dto.senderId && !isLocal(s));
3732
+ const remote = [];
3733
+ const localBots = new Set(candidates.map((c) => c.botUserId).filter((id) => !!id));
3734
+ const nativeOthers = extractNativeMentionIds(text).filter((id) => !localBots.has(id));
3735
+ if (resolved.unknown.length > 0 || nativeOthers.length > 0) {
3736
+ const elsewhere = await this.agentsElsewhere(dto.channelId);
3737
+ for (const u of resolved.unknown) {
3738
+ const key = u.token.toLowerCase();
3739
+ const hit = this.humanNames.has(key) ? undefined : elsewhere.byName.get(key);
3740
+ if (hit)
3741
+ remote.push(hit);
3742
+ else
3743
+ others.push(u.token);
3744
+ }
3745
+ for (const id of nativeOthers) {
3746
+ const hit = elsewhere.byBot.get(id);
3747
+ if (hit)
3748
+ remote.push(hit);
3749
+ else
3750
+ others.push(id);
3751
+ }
3752
+ }
3753
+ // "Steve, …" with no @ is still addressed to Steve.
3754
+ const ownerName = this.ownerName ?? (await this.ownerDisplayName());
3755
+ const people = [ownerName, ownerName.split(/\s+/u)[0], ...this.humanNames.keys()].filter((n) => n && n !== 'Owner').map((name) => ({ name }));
3756
+ const addressesPeople = SLACK_BROADCAST_MENTION_RE.test(text) || (people.length > 0 && !!leadingNameMention(text, people));
3757
+ return {
3758
+ kind: classifyAgentAddressing({ localAgents: local, remoteAgents: remote, others, addressesPeople }),
3759
+ local: [...new Set(local)],
3760
+ remote: [...new Set(remote)],
3761
+ others,
3762
+ addressesPeople,
3763
+ };
3764
+ }
3765
+ /**
3766
+ * Agents of this account on other machines that may be in a room, by
3767
+ * (lower-case) name and bot id: Cloud's last room roster for the channel,
3768
+ * then the directory (cached). Never throws.
3769
+ *
3770
+ * @param chatChannelId - The room's chat channel
3771
+ * @returns Name → session, bot id → session
3772
+ */
3773
+ async agentsElsewhere(chatChannelId) {
3774
+ const byName = new Map();
3775
+ const byBot = new Map();
3776
+ const isLocal = (s) => this.deps.isLocalAgent?.(localAgentSession(s)) ?? false;
3777
+ const addName = (name, session) => {
3778
+ const full = (name ?? '').trim().toLowerCase();
3779
+ if (!full)
3780
+ return;
3781
+ if (!byName.has(full))
3782
+ byName.set(full, session);
3783
+ const bare = full.replace(/\s*\([^)]*\)\s*$/u, '').trim();
3784
+ if (bare && !byName.has(bare))
3785
+ byName.set(bare, session);
3786
+ };
3787
+ const mapping = this.findByChatChannelId(chatChannelId);
3788
+ for (const m of (mapping ? this.lastRooms.get(mapping.slackChannelId)?.members : undefined) ?? []) {
3789
+ if (!isLocal(m.agentSession))
3790
+ addName(m.displayName, m.agentSession);
3791
+ }
3792
+ try {
3793
+ const directory = getSlackDirectoryService();
3794
+ const entries = directory
3795
+ ? await withinMs(directory.list(mapping?.slackChannelId), AGENT_COORDINATION_CONSTANTS.DIRECTORY_LOOKUP_MS, [])
3796
+ : [];
3797
+ for (const e of entries) {
3798
+ if (e.kind !== 'agent' || e.source === 'this-machine' || !e.agentSession || isLocal(e.agentSession))
3799
+ continue;
3800
+ addName(e.name, e.agentSession);
3801
+ if (e.botUserId)
3802
+ byBot.set(e.botUserId, e.agentSession);
3803
+ }
3804
+ }
3805
+ catch {
3806
+ /* the room roster alone */
3807
+ }
3808
+ return { byName, byBot };
3809
+ }
3810
+ /**
3811
+ * Whether a person started a chat thread or spoke in it.
3812
+ *
3813
+ * @param chatChannelId - The room's chat channel
3814
+ * @param threadId - Thread root id
3815
+ * @param root - The root row, when known
3816
+ * @returns True when a person is part of the thread
3817
+ */
3818
+ personInThread(chatChannelId, threadId, root) {
3819
+ const isPerson = (m) => m.senderType === 'user' && !isAgentAuthoredRow(m);
3820
+ if (!root)
3821
+ return true;
3822
+ if (isPerson(root))
3823
+ return true;
3824
+ const rows = this.deps.chat.listThreadForBridge?.(chatChannelId, threadId) ?? [];
3825
+ return rows.some(isPerson);
3826
+ }
3827
+ /**
3828
+ * The agents of an internal coordination thread other than the sender:
3829
+ * whoever wrote in it and whoever it @'d.
3830
+ *
3831
+ * @param root - The thread root
3832
+ * @param sender - The agent posting now
3833
+ * @returns Local agent sessions
3834
+ */
3835
+ internalThreadAgents(root, sender) {
3836
+ const rows = [root, ...(this.deps.chat.listThreadForBridge?.(root.channelId, root.id) ?? []).filter((m) => m.id !== root.id)];
3837
+ const out = new Set();
3838
+ for (const m of rows) {
3839
+ const author = m.senderType === 'agent' ? m.senderId : agentAuthorSession(m);
3840
+ if (author)
3841
+ out.add(author);
3842
+ for (const s of m.mentions ?? [])
3843
+ out.add(s);
3844
+ }
3845
+ out.delete(sender);
3846
+ return [...out].filter((s) => this.deps.isLocalAgent?.(s) ?? false);
3847
+ }
3848
+ /**
3849
+ * Make sure every recipient is on the room's roster, so the dispatcher
3850
+ * delivers to it: an ad-hoc room grows by an explicit @ (as it does for a
3851
+ * Slack message); a team channel does not, so a colleague of another team
3852
+ * keeps today's path (its own bot hears the Slack post).
3853
+ *
3854
+ * @param mapping - The room
3855
+ * @param recipients - Agents to deliver to
3856
+ * @returns True when all of them are on the roster
3857
+ */
3858
+ async roomHears(mapping, recipients) {
3859
+ if (recipients.length === 0)
3860
+ return false;
3861
+ const room = this.deps.chat.getChannelForBridge(mapping.chatChannelId);
3862
+ if (!room || room.archivedAt)
3863
+ return false;
3864
+ const roster = new Set((room.members ?? []).map((m) => m.sessionName));
3865
+ const missing = recipients.filter((s) => !roster.has(s));
3866
+ if (missing.length === 0)
3867
+ return true;
3868
+ if (!isAdhocMapping(mapping))
3869
+ return false;
3870
+ const next = [...new Set([...(mapping.members ?? []), ...roster, ...missing])];
3871
+ mapping.members = next;
3872
+ if (mapping.excluded) {
3873
+ mapping.excluded = mapping.excluded.filter((s) => !next.includes(s));
3874
+ if (mapping.excluded.length === 0)
3875
+ delete mapping.excluded;
3876
+ }
3877
+ this.deps.chat.setHuddleMembers(mapping.chatChannelId, next);
3878
+ await this.save();
3879
+ this.deps.onRoomsChanged?.();
3880
+ return true;
3881
+ }
3882
+ /**
3883
+ * Remember on the chat row that it stayed inside Crewly, so replies in its
3884
+ * thread stay inside too (and a later reader knows why Slack never had it).
3885
+ *
3886
+ * @param dto - The agent's post
3887
+ * @param reason - Why
3888
+ */
3889
+ markInternal(dto, reason) {
3890
+ const patch = { [AGENT_COORDINATION_CONSTANTS.INTERNAL_METADATA_KEY]: reason };
3891
+ try {
3892
+ this.deps.chat.updateMessageMetadata?.(dto.id, patch);
3893
+ }
3894
+ catch {
3895
+ /* routing hint only */
3896
+ }
3897
+ dto.metadata = { ...(dto.metadata ?? {}), ...patch };
3898
+ }
3899
+ /**
3900
+ * Deliver an agent's post to colleagues on this machine without Slack:
3901
+ * the dispatcher gets it as a colleague's turn — exactly what the Slack
3902
+ * round-trip produced — but threaded on the real chat row, so their
3903
+ * replies find it (the transient `slack-echo-…` id of the round-trip did
3904
+ * not exist, and those replies were refused).
3905
+ *
3906
+ * @param mapping - The room
3907
+ * @param dto - The agent's post
3908
+ * @param recipients - Local agents it is for
3909
+ * @param reason - Logged and stored on the row
3910
+ * @param note - A line put before the text (a hand-back)
3911
+ * @returns False when there is no dispatcher to deliver with
3912
+ */
3913
+ async deliverInternally(mapping, dto, recipients, reason, note) {
3914
+ const dispatcher = this.deps.getDispatcher();
3915
+ const channel = this.deps.chat.getChannelForBridge(mapping.chatChannelId);
3916
+ if (!dispatcher || !channel)
3917
+ return false;
3918
+ this.markInternal(dto, reason);
3919
+ const name = (await this.findPostingMember(mapping, dto.senderId))?.name ?? dto.senderId;
3920
+ const turn = {
3921
+ ...dto,
3922
+ senderType: 'user',
3923
+ senderId: `${name} (agent)`,
3924
+ content: note ? `${note}\n\n${dto.content}` : dto.content,
3925
+ mentions: [...recipients],
3926
+ metadata: {
3927
+ ...(dto.metadata ?? {}),
3928
+ [OWNER_EVIDENCE_METADATA.REMOTE_AGENT_SESSION]: dto.senderId,
3929
+ [AGENT_COORDINATION_CONSTANTS.INTERNAL_METADATA_KEY]: reason,
3930
+ },
3931
+ };
3932
+ const now = (this.deps.now?.() ?? new Date()).getTime();
3933
+ for (const r of recipients)
3934
+ this.noteAskedBy(mapping.chatChannelId, r, dto.senderId, now);
3935
+ const result = await dispatcher.dispatchMessage(channel, turn, {
3936
+ threadId: dto.threadId ?? dto.id,
3937
+ replyVia: 'reply-channel',
3938
+ excludeSessions: [dto.senderId],
3939
+ });
3940
+ const log = result.dispatched ? this.logger.info.bind(this.logger) : this.logger.warn.bind(this.logger);
3941
+ log('Agent-to-agent message delivered inside Crewly — not posted to Slack', {
3942
+ reason,
3943
+ slackChannel: mapping.slackChannelName,
3944
+ sender: dto.senderId,
3945
+ recipients,
3946
+ dispatched: result.dispatched,
3947
+ messageId: dto.id,
3948
+ });
3949
+ return true;
3950
+ }
3951
+ /**
3952
+ * Remember who asked a helper something in a room.
3953
+ *
3954
+ * @param chatChannelId - The room
3955
+ * @param helper - The agent asked
3956
+ * @param asker - The agent that asked
3957
+ * @param now - Time (ms)
3958
+ */
3959
+ noteAskedBy(chatChannelId, helper, asker, now) {
3960
+ if (helper === asker)
3961
+ return;
3962
+ const key = `${chatChannelId}:${helper}`;
3963
+ this.askedBy.delete(key);
3964
+ this.askedBy.set(key, { asker, at: now });
3965
+ if (this.askedBy.size > AGENT_COORDINATION_CONSTANTS.ASKED_BY_MAX) {
3966
+ const oldest = this.askedBy.keys().next().value;
3967
+ if (oldest !== undefined)
3968
+ this.askedBy.delete(oldest);
3969
+ }
3970
+ }
3971
+ /**
3972
+ * A helper's reply its bot could not post in the room (not a member of the
3973
+ * Slack channel): handed to the agent that asked it — the thread root's
3974
+ * author, else the agent that last @'d it here, else the room's lead —
3975
+ * who is in the room and posts the update. Logged either way.
3976
+ *
3977
+ * @param mapping - The room
3978
+ * @param dto - The helper's reply
3979
+ * @param code - Slack's error code
3980
+ * @returns True when someone here got it
3981
+ */
3982
+ async handBackUnpostable(mapping, dto, code) {
3983
+ try {
3984
+ const isLocal = (s) => !!s && s !== dto.senderId && (this.deps.isLocalAgent?.(s) ?? false);
3985
+ const root = dto.threadId ? this.deps.chat.getMessageForBridge(dto.threadId) : null;
3986
+ const rootAuthor = root ? (root.senderType === 'agent' ? root.senderId : agentAuthorSession(root)) : null;
3987
+ const now = (this.deps.now?.() ?? new Date()).getTime();
3988
+ const asked = this.askedBy.get(`${mapping.chatChannelId}:${dto.senderId}`);
3989
+ const recentAsker = asked && now - asked.at <= AGENT_COORDINATION_CONSTANTS.ASKED_BY_TTL_MS ? asked.asker : null;
3990
+ let requester = null;
3991
+ let source = '';
3992
+ if (isLocal(rootAuthor)) {
3993
+ requester = rootAuthor;
3994
+ source = 'thread-root';
3995
+ }
3996
+ else if (isLocal(recentAsker)) {
3997
+ requester = recentAsker;
3998
+ source = 'last-asker';
3999
+ }
4000
+ else {
4001
+ const lead = await this.localRoomLead(mapping, dto.threadId);
4002
+ if (isLocal(lead)) {
4003
+ requester = lead;
4004
+ source = 'room-lead';
4005
+ }
4006
+ }
4007
+ if (!requester) {
4008
+ this.logger.warn('Helper cannot post in this Slack room and nobody here asked it — reply not handed on', {
4009
+ slackChannel: mapping.slackChannelName,
4010
+ sender: dto.senderId,
4011
+ code,
4012
+ });
4013
+ return false;
4014
+ }
4015
+ if (source === 'room-lead') {
4016
+ this.logger.warn('Helper cannot post in this Slack room and who asked it is unknown — handing its reply to the room lead', {
4017
+ slackChannel: mapping.slackChannelName,
4018
+ sender: dto.senderId,
4019
+ lead: requester,
4020
+ });
4021
+ }
4022
+ if (!(await this.roomHears(mapping, [requester])))
4023
+ return false;
4024
+ const name = (await this.findPostingMember(mapping, dto.senderId))?.name ?? dto.senderId;
4025
+ const note = `[Sent to you only: ${name} cannot post in #${mapping.slackChannelName}. You are the voice of this room — put what matters into your own update there.]`;
4026
+ const ok = await this.deliverInternally(mapping, dto, [requester], 'helper-not-in-slack-channel', note);
4027
+ if (ok) {
4028
+ this.logger.info('Helper reply handed to the agent that asked it (its bot is not in the Slack channel)', {
4029
+ slackChannel: mapping.slackChannelName,
4030
+ sender: dto.senderId,
4031
+ requester,
4032
+ source,
4033
+ code,
4034
+ });
4035
+ }
4036
+ return ok;
4037
+ }
4038
+ catch (err) {
4039
+ this.logger.warn('Could not hand a helper reply back to the agent that asked it', {
4040
+ sender: dto.senderId,
4041
+ error: err instanceof Error ? err.message : String(err),
4042
+ });
4043
+ return false;
4044
+ }
4045
+ }
4046
+ /**
4047
+ * The Slack ts of the room's cross-machine coordination thread today
4048
+ * (UTC), if one was started.
4049
+ *
4050
+ * @param mapping - The room
4051
+ * @returns Thread ts, or undefined
4052
+ */
4053
+ coordinationThreadTs(mapping) {
4054
+ return this.coordinationThreads.get(this.coordinationKey(mapping));
4055
+ }
4056
+ /**
4057
+ * Make a top-level cross-machine post the room's coordination thread for
4058
+ * today: later ones go under it.
4059
+ *
4060
+ * @param mapping - The room
4061
+ * @param dto - The post (its Slack ts was recorded by recordSlackRoot)
4062
+ */
4063
+ rememberCoordinationThread(mapping, dto) {
4064
+ const ts = dto.metadata?.slackThreadTs;
4065
+ if (typeof ts !== 'string' || !ts)
4066
+ return;
4067
+ this.coordinationThreads.set(this.coordinationKey(mapping), ts);
4068
+ if (this.coordinationThreads.size > AGENT_COORDINATION_CONSTANTS.COORDINATION_THREADS_MAX) {
4069
+ const oldest = this.coordinationThreads.keys().next().value;
4070
+ if (oldest !== undefined)
4071
+ this.coordinationThreads.delete(oldest);
4072
+ }
4073
+ }
4074
+ /** `slackChannelId:YYYY-MM-DD` (UTC). */
4075
+ coordinationKey(mapping) {
4076
+ return `${mapping.slackChannelId}:${(this.deps.now?.() ?? new Date()).toISOString().slice(0, 10)}`;
4077
+ }
4078
+ /**
4079
+ * An agent's text without internal ticket ids (tickets are the harness's
4080
+ * bookkeeping; the owner never sees their ids). Never throws.
4081
+ *
4082
+ * @param text - The agent's text
4083
+ * @returns The text for Slack
4084
+ */
4085
+ async withoutTicketIds(text) {
4086
+ try {
4087
+ const now = Date.now();
4088
+ if (!this.ticketPrefixCache || now - this.ticketPrefixCache.at > AGENT_COORDINATION_CONSTANTS.TICKET_PREFIX_CACHE_MS) {
4089
+ const prefixes = this.deps.ticketPrefixes ? await this.deps.ticketPrefixes() : [];
4090
+ this.ticketPrefixCache = { at: now, prefixes: [AGENT_COORDINATION_CONSTANTS.REQUEST_TICKET_PREFIX, ...prefixes] };
4091
+ }
4092
+ return stripTicketIds(text, this.ticketPrefixCache.prefixes);
4093
+ }
4094
+ catch {
4095
+ return text;
4096
+ }
4097
+ }
3523
4098
  /**
3524
4099
  * Store where a mirrored reply landed on its chat message, in the shape
3525
4100
  * inbound messages use, so the Slack side can be audited from our store.
@@ -3661,7 +4236,7 @@ export class SlackTeamChannelService {
3661
4236
  const member = await this.findPostingMember(mapping, dto.senderId);
3662
4237
  const installed = this.deps.identities?.getInstalled(dto.senderId) ?? null;
3663
4238
  const identity = installed ? { botToken: installed.botToken } : slackIdentityFor(member, dto.senderId);
3664
- const text = await this.linkAgentMentions(toSlackMrkdwn(dto.content), mapping.slackChannelId);
4239
+ const text = await this.linkAgentMentions(toSlackMrkdwn(await this.withoutTicketIds(dto.content)), mapping.slackChannelId);
3665
4240
  const ts = await this.deps.slack.sendMessage({
3666
4241
  channelId: mapping.slackChannelId,
3667
4242
  text,
@@ -4798,6 +5373,41 @@ export function isOwnerTypedInCrewly(dto) {
4798
5373
  return false;
4799
5374
  return typeof md.slackTs !== 'string' && typeof md.slackChannelId !== 'string';
4800
5375
  }
5376
+ /**
5377
+ * Whether a chat row was written by an agent (as a colleague's user turn).
5378
+ *
5379
+ * @param dto - A chat row
5380
+ * @returns True for an agent-authored row
5381
+ */
5382
+ function isAgentAuthoredRow(dto) {
5383
+ return agentAuthorSession(dto) !== null;
5384
+ }
5385
+ /**
5386
+ * The agent that wrote a user-turn row (a colleague on this or another
5387
+ * machine), or null for a person's row.
5388
+ *
5389
+ * @param dto - A chat row
5390
+ * @returns Agent session, or null
5391
+ */
5392
+ function agentAuthorSession(dto) {
5393
+ const md = dto.metadata ?? {};
5394
+ const author = md[OWNER_EVIDENCE_METADATA.AUTHOR_AGENT_SESSION] ?? md[OWNER_EVIDENCE_METADATA.REMOTE_AGENT_SESSION];
5395
+ return typeof author === 'string' && author ? author : null;
5396
+ }
5397
+ /**
5398
+ * Whether Slack refused a post because the posting bot is not in the
5399
+ * channel (`not_in_channel`, or `channel_not_found` for a private channel
5400
+ * it cannot see).
5401
+ *
5402
+ * @param err - The thrown error
5403
+ * @returns True for "this bot cannot post here"
5404
+ */
5405
+ function isNotInChannelError(err) {
5406
+ const code = describeSlackError(err).code;
5407
+ if (code === 'not_in_channel' || code === 'channel_not_found')
5408
+ return true;
5409
+ return /\b(?:not_in_channel|channel_not_found)\b/.test(err instanceof Error ? err.message : String(err));
5410
+ }
4801
5411
  /** Type guard for persisted mapping rows. */
4802
5412
  /**
4803
5413
  * A promise's value, or `fallback` when it takes longer than `ms`.