crewly 1.20.365 → 1.20.366

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 (136) hide show
  1. package/config/skills/agent/core/attach-file/SKILL.md +7 -0
  2. package/config/skills/agent/core/attach-file/execute.sh +8 -3
  3. package/config/skills/agent/core/complete-task/SKILL.md +4 -2
  4. package/config/skills/agent/core/complete-task/execute.sh +9 -1
  5. package/config/skills/agent/core/complete-task/explicit-id.test.sh +13 -2
  6. package/config/skills/agent/core/create-task/SKILL.md +2 -0
  7. package/config/skills/agent/core/create-task/execute.sh +27 -1
  8. package/config/skills/agent/core/create-task/execute.test.sh +67 -0
  9. package/config/skills/agent/core/handoff-task/execute.sh +12 -5
  10. package/config/skills/agent/core/handoff-task/explicit-id.test.sh +70 -0
  11. package/config/skills/agent/core/reply-channel/SKILL.md +12 -5
  12. package/config/skills/agent/core/reply-channel/execute.sh +17 -2
  13. package/config/skills/agent/core/reply-channel/execute.test.sh +15 -2
  14. package/config/skills/agent/core/slack-post/SKILL.md +3 -3
  15. package/config/skills/orchestrator/delegate-task/SKILL.md +1 -1
  16. package/config/skills/orchestrator/reply-slack/execute.sh +6 -2
  17. package/config/skills/orchestrator/send-pdf-to-slack/SKILL.md +2 -1
  18. package/config/skills/orchestrator/send-pdf-to-slack/execute.sh +10 -2
  19. package/config/skills/registry.json +6 -6
  20. package/config/skills/team-leader/delegate-task/SKILL.md +1 -1
  21. package/config/skills/team-leader/delegate-task/execute.sh +6 -0
  22. package/dist/backend/backend/src/constants.d.ts +26 -0
  23. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  24. package/dist/backend/backend/src/constants.js +26 -0
  25. package/dist/backend/backend/src/constants.js.map +1 -1
  26. package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.d.ts.map +1 -1
  27. package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.js +7 -3
  28. package/dist/backend/backend/src/controllers/agent-hooks/agent-hooks.controller.js.map +1 -1
  29. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts +12 -0
  30. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  31. package/dist/backend/backend/src/controllers/chat/chat.controller.js +56 -7
  32. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  33. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts +9 -0
  34. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
  35. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +111 -11
  36. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
  37. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
  38. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +52 -4
  39. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
  40. package/dist/backend/backend/src/controllers/slack/slack.controller.d.ts.map +1 -1
  41. package/dist/backend/backend/src/controllers/slack/slack.controller.js +58 -4
  42. package/dist/backend/backend/src/controllers/slack/slack.controller.js.map +1 -1
  43. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.d.ts.map +1 -1
  44. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js +36 -2
  45. package/dist/backend/backend/src/controllers/task-pool/task-pool.controller.js.map +1 -1
  46. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +16 -10
  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 +19 -11
  49. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  50. package/dist/backend/backend/src/services/messaging/agent-message-envelope.d.ts +8 -0
  51. package/dist/backend/backend/src/services/messaging/agent-message-envelope.d.ts.map +1 -1
  52. package/dist/backend/backend/src/services/messaging/agent-message-envelope.js +10 -1
  53. package/dist/backend/backend/src/services/messaging/agent-message-envelope.js.map +1 -1
  54. package/dist/backend/backend/src/services/messaging/owner-hook-message.d.ts +62 -20
  55. package/dist/backend/backend/src/services/messaging/owner-hook-message.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/messaging/owner-hook-message.js +178 -39
  57. package/dist/backend/backend/src/services/messaging/owner-hook-message.js.map +1 -1
  58. package/dist/backend/backend/src/services/orc/agent-mention-post-guard.d.ts +8 -5
  59. package/dist/backend/backend/src/services/orc/agent-mention-post-guard.d.ts.map +1 -1
  60. package/dist/backend/backend/src/services/orc/agent-mention-post-guard.js +13 -10
  61. package/dist/backend/backend/src/services/orc/agent-mention-post-guard.js.map +1 -1
  62. package/dist/backend/backend/src/services/orc/message-route.d.ts +129 -0
  63. package/dist/backend/backend/src/services/orc/message-route.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/orc/message-route.js +197 -0
  65. package/dist/backend/backend/src/services/orc/message-route.js.map +1 -0
  66. package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts +49 -6
  67. package/dist/backend/backend/src/services/orc/reply-destination-resolver.d.ts.map +1 -1
  68. package/dist/backend/backend/src/services/orc/reply-destination-resolver.js +284 -69
  69. package/dist/backend/backend/src/services/orc/reply-destination-resolver.js.map +1 -1
  70. package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts +64 -8
  71. package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts.map +1 -1
  72. package/dist/backend/backend/src/services/orc/reply-destination.wiring.js +215 -100
  73. package/dist/backend/backend/src/services/orc/reply-destination.wiring.js.map +1 -1
  74. package/dist/backend/backend/src/services/orc/triggered-by.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/orc/triggered-by.js +9 -0
  76. package/dist/backend/backend/src/services/orc/triggered-by.js.map +1 -1
  77. package/dist/backend/backend/src/services/orc/work-item-destination.d.ts +2 -1
  78. package/dist/backend/backend/src/services/orc/work-item-destination.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/orc/work-item-destination.js +1 -1
  80. package/dist/backend/backend/src/services/orc/work-item-destination.js.map +1 -1
  81. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts +10 -1
  82. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js +25 -4
  84. package/dist/backend/backend/src/services/orc/work-item-destination.wiring.js.map +1 -1
  85. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts +1 -0
  86. package/dist/backend/backend/src/services/project-tickets/project-ticket-workflow.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts +1 -1
  88. package/dist/backend/backend/src/services/slack/slack-agent-post.service.d.ts.map +1 -1
  89. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js +20 -13
  90. package/dist/backend/backend/src/services/slack/slack-agent-post.service.js.map +1 -1
  91. package/dist/backend/backend/src/services/slack/slack.service.d.ts +20 -1
  92. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  93. package/dist/backend/backend/src/services/slack/slack.service.js +58 -3
  94. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  95. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts +31 -0
  96. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  97. package/dist/backend/backend/src/services/task-pool/task-pool.service.js +62 -5
  98. package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  99. package/dist/backend/backend/src/services/trace/turn-trigger.d.ts +34 -1
  100. package/dist/backend/backend/src/services/trace/turn-trigger.d.ts.map +1 -1
  101. package/dist/backend/backend/src/services/trace/turn-trigger.js +74 -9
  102. package/dist/backend/backend/src/services/trace/turn-trigger.js.map +1 -1
  103. package/dist/backend/build-info.json +2 -2
  104. package/dist/cli/backend/src/constants.d.ts +26 -0
  105. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  106. package/dist/cli/backend/src/constants.js +26 -0
  107. package/dist/cli/backend/src/constants.js.map +1 -1
  108. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +16 -10
  109. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  110. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +19 -11
  111. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  112. package/dist/cli/backend/src/services/messaging/agent-message-envelope.d.ts +8 -0
  113. package/dist/cli/backend/src/services/messaging/agent-message-envelope.d.ts.map +1 -1
  114. package/dist/cli/backend/src/services/messaging/agent-message-envelope.js +10 -1
  115. package/dist/cli/backend/src/services/messaging/agent-message-envelope.js.map +1 -1
  116. package/dist/cli/backend/src/services/orc/message-route.d.ts +129 -0
  117. package/dist/cli/backend/src/services/orc/message-route.d.ts.map +1 -0
  118. package/dist/cli/backend/src/services/orc/message-route.js +197 -0
  119. package/dist/cli/backend/src/services/orc/message-route.js.map +1 -0
  120. package/dist/cli/backend/src/services/orc/work-item-destination.d.ts +2 -1
  121. package/dist/cli/backend/src/services/orc/work-item-destination.d.ts.map +1 -1
  122. package/dist/cli/backend/src/services/orc/work-item-destination.js +1 -1
  123. package/dist/cli/backend/src/services/orc/work-item-destination.js.map +1 -1
  124. package/dist/cli/backend/src/services/slack/slack.service.d.ts +20 -1
  125. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  126. package/dist/cli/backend/src/services/slack/slack.service.js +58 -3
  127. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  128. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts +31 -0
  129. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  130. package/dist/cli/backend/src/services/task-pool/task-pool.service.js +62 -5
  131. package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  132. package/dist/cli/backend/src/services/trace/turn-trigger.d.ts +34 -1
  133. package/dist/cli/backend/src/services/trace/turn-trigger.d.ts.map +1 -1
  134. package/dist/cli/backend/src/services/trace/turn-trigger.js +74 -9
  135. package/dist/cli/backend/src/services/trace/turn-trigger.js.map +1 -1
  136. package/package.json +1 -1
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { type ReplyDestination, type ReplyHints, type ReplyReference, type ReplyResolverDeps } from './reply-destination-resolver.js';
10
10
  import { type WorkDestinationDeps } from './work-item-destination.wiring.js';
11
+ import { type MessageRoute, type RouteCheckOutcome } from './message-route.js';
11
12
  import { type TurnTrigger } from '../trace/turn-trigger.js';
12
13
  /** Forget recent deliveries (tests). */
13
14
  export declare function resetReplyDeliveryGuardForTesting(): void;
@@ -51,6 +52,18 @@ export interface ReplyDeliveryDeps {
51
52
  turnTrigger?: (session: string) => TurnTrigger | null;
52
53
  /** The owner wrote to the agent in this conversation recently (its post there is an answer). Omitted: no. */
53
54
  ownerAskedRecently?: (session: string, conversationId: string) => boolean;
55
+ /**
56
+ * The owner wrote to the agent in this conversation earlier in its working
57
+ * window: a post there answers that, not the agent's newest owner message
58
+ * elsewhere (R8: Pia's #flopost answer said "Triggered by your message in
59
+ * #awesome-videos"). Omitted: no.
60
+ */
61
+ ownerAskedEarlier?: (session: string, conversationId: string) => boolean;
62
+ /**
63
+ * The latest trigger that names what the agent answers (turn-trigger.ts
64
+ * `routedTrigger`), for the route check. Omitted: no check.
65
+ */
66
+ routedTrigger?: (session: string) => TurnTrigger | null;
54
67
  /** The session's current run trace id. Omitted: none. */
55
68
  currentTrace?: (session: string) => string | null;
56
69
  /** A conversation as the owner knows it (`#content-team`, `the DM with Ella`). Omitted: unnamed. */
@@ -96,19 +109,41 @@ export interface DeliverReplyInput {
96
109
  * Resolve and deliver an agent's message. Never guesses: a destination that
97
110
  * does not take the message is reported as an error with the command to run.
98
111
  *
99
- * When the chosen place refuses the message, only these real places are
100
- * tried next (each logged), never "the latest thread" or the global current
101
- * conversation:
102
- * - a hint that did not take it → resolution without the hints;
103
- * - the current work's owner conversation → a new top-level post in the
104
- * team channel, opened with the work's topic;
105
- * - the current work's Slack place → the owner's turn origin, else the DM.
112
+ * When the chosen place refuses the message, only the SAME place is tried
113
+ * by another way (each logged): the Slack thread a delegate was handed, or
114
+ * the Slack thread a chat thread with no row stands for. It is never moved
115
+ * to another place — not a new top-level post, not the owner DM, not the
116
+ * agent's last owner conversation (message-routing audit 2026-10-18, R2):
117
+ * the agent gets the error and the command that works.
106
118
  *
107
119
  * @param input - Agent, text, references, hints
108
120
  * @param deps - Collaborators (default: the real ones)
109
121
  * @returns Where it landed, or an English error
110
122
  */
111
123
  export declare function deliverReply(input: DeliverReplyInput, deps?: ReplyDeliveryDeps): Promise<ReplyDelivery>;
124
+ /**
125
+ * Counts of the route check since boot.
126
+ *
127
+ * @returns match / mismatch / unknown counts
128
+ */
129
+ export declare function getRouteCheckStats(): Readonly<Record<RouteCheckOutcome, number>>;
130
+ /** Zero the route check counts (tests). */
131
+ export declare function resetRouteCheckStatsForTesting(): void;
132
+ /**
133
+ * Compare where a delivered post landed with the place of the request it
134
+ * answers (the route of the agent's latest routed input), and log a
135
+ * mismatch. A post the agent addressed by an explicit reference answers that
136
+ * reference and is not checked; an internal message to an agent has no
137
+ * Slack place. Never throws, never blocks: it is a regression signal.
138
+ *
139
+ * @param input - The delivered input
140
+ * @param delivery - Where it landed
141
+ * @param deps - Collaborators (default: the real ones)
142
+ * @returns The outcome, or null when not checked
143
+ */
144
+ export declare function checkLandedRoute(input: DeliverReplyInput, delivery: Extract<ReplyDelivery, {
145
+ ok: true;
146
+ }>, deps?: ReplyDeliveryDeps): Promise<RouteCheckOutcome | null>;
112
147
  /**
113
148
  * Provenance for a post an agent makes straight into a conversation
114
149
  * (`reply-channel`, `POST /chat/channels/:id/messages`), outside
@@ -124,10 +159,23 @@ export declare function provenanceForAgentPost(input: {
124
159
  conversationId: string;
125
160
  content: string;
126
161
  interim?: boolean;
127
- }, deps?: Pick<ReplyDeliveryDeps, 'resolver' | 'turnTrigger' | 'ownerAskedRecently' | 'currentTrace' | 'conversationPlace'>): Promise<{
162
+ }, deps?: Pick<ReplyDeliveryDeps, 'resolver' | 'turnTrigger' | 'ownerAskedRecently' | 'ownerAskedEarlier' | 'currentTrace' | 'conversationPlace'>): Promise<{
128
163
  content: string;
129
164
  triggeredBy?: Record<string, unknown>;
130
165
  }>;
166
+ /**
167
+ * Whether a chat thread is agent-to-agent coordination kept inside Crewly
168
+ * (crewly#1340): the message, or the root of its thread, carries
169
+ * `agentCoordinationInternal`. `reply-channel` keeps a post in such a thread
170
+ * internal; a file must not go to the Slack thread key stored on that
171
+ * message either (Pia's v3/v4 videos for Ella landed in Steve's thread,
172
+ * 2026-10-18).
173
+ *
174
+ * @param threadId - A chat message id (anything else is not internal)
175
+ * @param deps - Collaborators (default: the real ones)
176
+ * @returns Why the thread is internal, or null
177
+ */
178
+ export declare function internalCoordinationThread(threadId: unknown, deps?: ReplyDeliveryDeps): Promise<string | null>;
131
179
  /**
132
180
  * The Slack place (channel + thread) the resolver gives, without posting —
133
181
  * for files and the `[DONE]` notice. Ticket threads that do not exist yet
@@ -175,5 +223,13 @@ interface ChatChannelLookup {
175
223
  * @returns The place, or null when unknown
176
224
  */
177
225
  export declare function chatConversationPlace(chat: ChatChannelLookup | null, conversationId: string): string | null;
226
+ /**
227
+ * The route an agent's own messages carry onward: what it is answering now
228
+ * (its latest routed input; message-route.ts). Never throws.
229
+ *
230
+ * @param session - Agent session
231
+ * @returns The route, or null when its input names none
232
+ */
233
+ export declare function currentRouteOf(session: string): Promise<MessageRoute | null>;
178
234
  export {};
179
235
  //# sourceMappingURL=reply-destination.wiring.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reply-destination.wiring.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/orc/reply-destination.wiring.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reply-destination.wiring.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/orc/reply-destination.wiring.ts"],"names":[],"mappings":"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"}
@@ -14,7 +14,8 @@ import { LoggerService } from '../core/logger.service.js';
14
14
  import { AgentPromptReferenceService } from './agent-prompt-reference.service.js';
15
15
  import { fixCommand, latestInputOf, resolveReplyDestination, } from './reply-destination-resolver.js';
16
16
  import { defaultWorkDestinationDeps, deliverToWorkDestination } from './work-item-destination.wiring.js';
17
- import { currentWorkItemOf, shortTopic, withTopicLine } from './work-item-destination.js';
17
+ import { originOfWorkItem } from './work-item-destination.js';
18
+ import { comparePostPlace, placeOfOwnerRoute, routeOfTrigger } from './message-route.js';
18
19
  import { isSlackDm } from './orc-reply-route.service.js';
19
20
  import { traceOutboundReply } from '../trace/trace-recorder.js';
20
21
  import { stripTraceMarkers } from '../trace/trace-markers.js';
@@ -35,13 +36,12 @@ const logger = LoggerService.getInstance().createComponentLogger('ReplyDestinati
35
36
  * Resolve and deliver an agent's message. Never guesses: a destination that
36
37
  * does not take the message is reported as an error with the command to run.
37
38
  *
38
- * When the chosen place refuses the message, only these real places are
39
- * tried next (each logged), never "the latest thread" or the global current
40
- * conversation:
41
- * - a hint that did not take it → resolution without the hints;
42
- * - the current work's owner conversation → a new top-level post in the
43
- * team channel, opened with the work's topic;
44
- * - the current work's Slack place → the owner's turn origin, else the DM.
39
+ * When the chosen place refuses the message, only the SAME place is tried
40
+ * by another way (each logged): the Slack thread a delegate was handed, or
41
+ * the Slack thread a chat thread with no row stands for. It is never moved
42
+ * to another place — not a new top-level post, not the owner DM, not the
43
+ * agent's last owner conversation (message-routing audit 2026-10-18, R2):
44
+ * the agent gets the error and the command that works.
45
45
  *
46
46
  * @param input - Agent, text, references, hints
47
47
  * @param deps - Collaborators (default: the real ones)
@@ -71,8 +71,95 @@ export async function deliverReply(input, deps) {
71
71
  recentDeliveries.set(dupKey, { at: Date.now(), delivery: result });
72
72
  }
73
73
  traceOutboundReply(clean, result);
74
+ if (result.ok)
75
+ await checkLandedRoute(clean, result, deps).catch(() => undefined);
74
76
  return result;
75
77
  }
78
+ /** Route check outcomes since boot (the regression check of the routing invariant). */
79
+ const routeCheckStats = { match: 0, mismatch: 0, unknown: 0 };
80
+ /**
81
+ * Counts of the route check since boot.
82
+ *
83
+ * @returns match / mismatch / unknown counts
84
+ */
85
+ export function getRouteCheckStats() {
86
+ return { ...routeCheckStats };
87
+ }
88
+ /** Zero the route check counts (tests). */
89
+ export function resetRouteCheckStatsForTesting() {
90
+ routeCheckStats.match = 0;
91
+ routeCheckStats.mismatch = 0;
92
+ routeCheckStats.unknown = 0;
93
+ }
94
+ /**
95
+ * Compare where a delivered post landed with the place of the request it
96
+ * answers (the route of the agent's latest routed input), and log a
97
+ * mismatch. A post the agent addressed by an explicit reference answers that
98
+ * reference and is not checked; an internal message to an agent has no
99
+ * Slack place. Never throws, never blocks: it is a regression signal.
100
+ *
101
+ * @param input - The delivered input
102
+ * @param delivery - Where it landed
103
+ * @param deps - Collaborators (default: the real ones)
104
+ * @returns The outcome, or null when not checked
105
+ */
106
+ export async function checkLandedRoute(input, delivery, deps) {
107
+ const dest = delivery.destination;
108
+ if (REFERENCE_SOURCES.has(dest.source) || dest.source === 'prompt' || delivery.agentSession)
109
+ return null;
110
+ const d = deps ?? (await defaultReplyDeliveryDeps());
111
+ const trigger = d.routedTrigger?.(input.session) ?? null;
112
+ const route = routeOfTrigger(trigger, d.resolver.turnOrigin(input.session));
113
+ if (!route)
114
+ return null;
115
+ const expected = await expectedPlaceOf(route, d);
116
+ const landed = {
117
+ ...(delivery.conversationId ? { conversationId: delivery.conversationId } : {}),
118
+ ...(dest.kind === 'conversation' && dest.thread ? { thread: dest.thread } : {}),
119
+ ...(delivery.slackChannelId ? { slackChannelId: delivery.slackChannelId } : {}),
120
+ ...(delivery.threadTs ? { threadTs: delivery.threadTs } : {}),
121
+ };
122
+ const outcome = expected === 'internal' ? 'mismatch' : comparePostPlace(expected, landed);
123
+ routeCheckStats[outcome] += 1;
124
+ if (outcome === 'mismatch') {
125
+ logger.warn('[RouteCheck] Post landed outside the thread of the request it answers', {
126
+ session: input.session,
127
+ route,
128
+ expected: expected === 'internal' ? 'internal (agent-to-agent)' : expected,
129
+ landed,
130
+ source: dest.source,
131
+ mismatches: routeCheckStats.mismatch,
132
+ });
133
+ }
134
+ return outcome;
135
+ }
136
+ /**
137
+ * The place a route's answer belongs in: an owner route's thread, a work
138
+ * item's owner origin, or `internal` for work an agent delegated (its answer
139
+ * goes to that agent, never to Slack).
140
+ *
141
+ * @param route - The route
142
+ * @param d - Collaborators
143
+ * @returns The place, `internal`, or null when unknown
144
+ */
145
+ async function expectedPlaceOf(route, d) {
146
+ if (route.kind === 'owner')
147
+ return placeOfOwnerRoute(route);
148
+ const wi = await d.resolver.workItem(route.workItemId).catch(() => null);
149
+ const origin = wi ? originOfWorkItem(wi) : null;
150
+ if (!origin)
151
+ return null;
152
+ if (origin.kind === 'agent')
153
+ return 'internal';
154
+ if (origin.kind !== 'owner')
155
+ return null;
156
+ return {
157
+ ...(origin.conversationId ? { conversationId: origin.conversationId } : {}),
158
+ ...(origin.chatThreadId ? { thread: origin.chatThreadId } : {}),
159
+ ...(origin.slackChannelId ? { slackChannelId: origin.slackChannelId } : {}),
160
+ ...(origin.threadTs ? { threadTs: origin.threadTs } : {}),
161
+ };
162
+ }
76
163
  /**
77
164
  * {@link deliverReply} without the run-trace record.
78
165
  *
@@ -103,16 +190,15 @@ async function deliverReplyUntraced(input, deps) {
103
190
  const delegated = await postToDelegatedThread(input, dest, d);
104
191
  if (delegated)
105
192
  return delegated;
106
- const ownDm = dest.source === 'hint' ? null : await ownDmInsteadOfOthers(input, dest, d);
107
- if (ownDm) {
108
- logger.warn('Destination is another agent\'s DM with the owner — answering in this agent\'s own owner DM', {
109
- session: input.session,
110
- from: dest.kind === 'conversation' ? dest.conversationId : undefined,
111
- to: ownDm.dest.conversationId,
112
- });
113
- const viaDm = await attempt({ ...input, content: ownDm.content }, ownDm.dest, prompt, d);
114
- if (viaDm.ok)
115
- return viaDm;
193
+ // Work handed over from another agent's DM with the owner: this agent
194
+ // cannot post there, and its answer is not moved to its own DM — it goes
195
+ // to the agent the owner asked (message-routing audit 2026-10-18, R2).
196
+ if (!result.ok && dest.kind === 'conversation' && (await isOthersOwnerDm(input.session, dest.conversationId, d))) {
197
+ const delegator = await delegatorOfWork(input, d);
198
+ return {
199
+ ...result,
200
+ error: notDelivered(`conversation ${dest.conversationId} is another agent's DM with the owner, where you cannot post`, delegator ? `send-message --to ${delegator} "<your answer>" (${delegator} answers the owner there)` : 'send your answer to the agent who handed you this work (send-message --to <agent>)'),
201
+ };
116
202
  }
117
203
  const hintedThread = dest.source === 'hint' && dest.kind === 'conversation' ? dest.thread : undefined;
118
204
  if (dest.source === 'hint') {
@@ -128,25 +214,10 @@ async function deliverReplyUntraced(input, deps) {
128
214
  logger.warn('The conversation the agent named did not take its message', { session: input.session, error: result.error });
129
215
  return withCardFix(result, hintedThread, d);
130
216
  }
131
- // The current WORK's place refused it (the owner's own turn origin is not
132
- // swapped for another place: that is an error the agent sees).
133
- if (dest.source === 'current-work') {
134
- const fallback = await currentWorkFallback(input.session, dest, d);
135
- if (fallback) {
136
- logger.warn('Current work destination did not take the message — trying the next real place', {
137
- session: input.session,
138
- from: dest.kind === 'work' ? dest.destination.kind : 'conversation',
139
- to: fallback.kind === 'work' ? fallback.destination.kind : `conversation ${fallback.conversationId}`,
140
- });
141
- const retry = await attempt(input, fallback, prompt, d);
142
- if (retry.ok)
143
- return retry;
144
- }
145
- }
146
- // An answered reference (decision / ticket / work item / message) must not
147
- // die in a thread the conversation could not post into: post straight to
148
- // that Slack thread, then as a new top-level post in the room's channel.
149
- if (dest.kind === 'conversation' && REFERENCE_SOURCES.has(dest.source)) {
217
+ // An answered reference (decision / ticket / work item / message / route)
218
+ // must not die in a thread the conversation could not post into: post
219
+ // straight to that same Slack thread. Never a new top-level post instead.
220
+ if (dest.kind === 'conversation' && (REFERENCE_SOURCES.has(dest.source) || dest.source === 'route' || dest.source === 'current-work' || dest.source === 'turn-origin')) {
150
221
  for (const fallback of await referenceFallbacks(input.session, dest, d)) {
151
222
  logger.warn('Referenced conversation did not take the reply — trying the next real place', {
152
223
  session: input.session,
@@ -260,8 +331,8 @@ async function withCardFix(result, thread, d) {
260
331
  const REFERENCE_SOURCES = new Set(['decision', 'ticket', 'work-item', 'message']);
261
332
  /**
262
333
  * Where an answered reference goes when its conversation refused the post:
263
- * the Slack thread it names (a card posted straight to Slack has no chat row),
264
- * then a new top-level post in the room's channel.
334
+ * the Slack thread it names (a card posted straight to Slack has no chat
335
+ * row). Only that same thread — never a new top-level post.
265
336
  *
266
337
  * @param session - Agent
267
338
  * @param failed - The conversation destination that refused
@@ -280,13 +351,7 @@ async function referenceFallbacks(session, failed, d) {
280
351
  reason: `conversation ${failed.conversationId} could not post into thread ${failed.thread} — Slack thread post`,
281
352
  });
282
353
  }
283
- const wi = currentWorkItemOf(await d.resolver.poolItems().catch(() => []), session);
284
- out.push({
285
- kind: 'work',
286
- destination: { kind: 'new-top-level', ...(wi ? { topic: shortTopic(wi.title) } : {}), reason: `conversation ${failed.conversationId} did not take the reply` },
287
- source: failed.source,
288
- reason: `conversation ${failed.conversationId} did not take the reply — new top-level post`,
289
- });
354
+ void session;
290
355
  return out;
291
356
  }
292
357
  /**
@@ -298,67 +363,45 @@ async function referenceFallbacks(session, failed, d) {
298
363
  * @param prompt - The agent's prompt reference
299
364
  * @returns A different instruction, or empty when there is none
300
365
  */
366
+ /**
367
+ * What to do when the place a reply resolved to cannot take it. Resending
368
+ * the same `reply` would resolve to the same place, so the advice names
369
+ * the explicit alternatives (message-routing audit 2026-10-18: refusals
370
+ * must be actionable).
371
+ */
372
+ const UNPOSTABLE_ADVICE = 'reply --conversation <id> --thread <thread> "<message>" to name a place you can post in, or tell whoever gave you this work (send-message --to <session> "<message>")';
301
373
  function retryAdvice(dest, prompt) {
302
374
  const same = REFERENCE_SOURCES.has(dest.source) || dest.source === 'prompt';
303
375
  const cmd = fixCommand(prompt);
304
376
  return same && cmd !== 'reply "<your message>"' ? '' : cmd;
305
377
  }
306
378
  /**
307
- * The agent's own owner DM, when the destination that refused its message is
308
- * ANOTHER agent's DM with the owner (work delegated from that DM). The text
309
- * is opened with `Re: <work title>` so the owner sees what it answers.
379
+ * Whether a conversation is another agent's DM with the owner (a Slack DM
380
+ * the replying agent is not in).
310
381
  *
311
- * @param input - Agent, text, references
312
- * @param failed - The destination that refused
382
+ * @param session - Replying agent
383
+ * @param conversationId - The conversation that refused
313
384
  * @param d - Collaborators
314
- * @returns The DM destination and the text to post, or null
385
+ * @returns True for another agent's owner DM
315
386
  */
316
- async function ownDmInsteadOfOthers(input, failed, d) {
317
- if (failed.kind !== 'conversation')
318
- return null;
319
- const slack = d.resolver.slackChannelOfConversation(failed.conversationId);
387
+ async function isOthersOwnerDm(session, conversationId, d) {
388
+ const slack = d.resolver.slackChannelOfConversation(conversationId);
320
389
  if (!slack || !isSlackDm(slack))
321
- return null;
322
- if (await d.resolver.ownsConversation(input.session, failed.conversationId).catch(() => true))
323
- return null;
324
- const dm = await d.resolver.ownerDm(input.session).catch(() => null);
325
- if (!dm || dm === failed.conversationId)
326
- return null;
327
- const wi = input.reference?.workItemId
328
- ? await d.resolver.workItem(input.reference.workItemId).catch(() => null)
329
- : currentWorkItemOf(await d.resolver.poolItems().catch(() => []), input.session);
330
- const topic = wi ? `Re: ${shortTopic(wi.title)}` : undefined;
331
- return {
332
- dest: { kind: 'conversation', conversationId: dm, source: 'owner-dm', reason: 'the owner asked in a DM with another agent — your DM with the owner' },
333
- content: withTopicLine(topic, input.content),
334
- };
390
+ return false;
391
+ return !(await d.resolver.ownsConversation(session, conversationId).catch(() => true));
335
392
  }
336
393
  /**
337
- * The next real place after the current work's destination refused a message.
394
+ * The agent that handed the replier the work it is answering, when known.
338
395
  *
339
- * @param session - Agent
340
- * @param failed - The destination that refused
396
+ * @param input - Agent, reference
341
397
  * @param d - Collaborators
342
- * @returns A fallback destination, or null
398
+ * @returns The delegator's session, or null
343
399
  */
344
- async function currentWorkFallback(session, failed, d) {
345
- if (failed.kind === 'conversation') {
346
- const items = await d.resolver.poolItems().catch(() => []);
347
- const wi = currentWorkItemOf(items, session);
348
- return {
349
- kind: 'work',
350
- destination: { kind: 'new-top-level', ...(wi ? { topic: shortTopic(wi.title) } : {}), reason: `conversation ${failed.conversationId} did not take the reply` },
351
- source: 'current-work',
352
- reason: `conversation ${failed.conversationId} did not take the reply — new top-level post`,
353
- };
354
- }
355
- const origin = d.resolver.turnOrigin(session);
356
- if (origin) {
357
- const thread = origin.slackThreadKey ?? origin.chatThreadId;
358
- return { kind: 'conversation', conversationId: origin.conversationId, ...(thread ? { thread } : {}), source: 'turn-origin', reason: 'the work destination did not take the reply — your last owner conversation' };
359
- }
360
- const dm = await d.resolver.ownerDm(session).catch(() => null);
361
- return dm ? { kind: 'conversation', conversationId: dm, source: 'owner-dm', reason: 'the work destination did not take the reply — your DM with the owner' } : null;
400
+ async function delegatorOfWork(input, d) {
401
+ const id = input.reference?.workItemId ?? d.resolver.latestInput?.(input.session)?.workItemId;
402
+ const wi = id ? await d.resolver.workItem(id).catch(() => null) : null;
403
+ const by = wi?.metadata?.delegatedBy;
404
+ return typeof by === 'string' && by && by !== input.session ? by : null;
362
405
  }
363
406
  /**
364
407
  * Why the agent is posting: the provenance record for the row and the text
@@ -371,15 +414,21 @@ async function currentWorkFallback(session, failed, d) {
371
414
  * @param d - Collaborators
372
415
  * @returns Metadata and the text to post, or null when no trigger is known
373
416
  */
374
- async function provenanceOf(input, postedConversationId, d) {
417
+ async function provenanceOf(input, postedConversationId, d, answering) {
375
418
  try {
376
- const trigger = d.turnTrigger?.(input.session) ?? null;
419
+ const trigger = triggerOfAnsweredInput(input.session, d, answering);
377
420
  if (!trigger)
378
421
  return null;
379
422
  const workItem = trigger.workItemId ? await d.resolver.workItem(trigger.workItemId).catch(() => null) : null;
423
+ // The post answers the owner's message in the conversation it lands in
424
+ // when the owner wrote to the agent there — even when a newer owner
425
+ // message elsewhere is the agent's latest (R8).
426
+ const answersOwnerHere = !!postedConversationId &&
427
+ ((d.ownerAskedRecently?.(input.session, postedConversationId) ?? false) ||
428
+ (trigger.triggerKind === 'owner_message' && trigger.conversationId !== postedConversationId && (d.ownerAskedEarlier?.(input.session, postedConversationId) ?? false)));
380
429
  const phrase = triggeredByPhrase(trigger, {
381
430
  ...(postedConversationId ? { postedConversationId } : {}),
382
- ownerAskedHere: !!postedConversationId && (d.ownerAskedRecently?.(input.session, postedConversationId) ?? false),
431
+ ownerAskedHere: answersOwnerHere,
383
432
  workItem,
384
433
  ...(d.conversationPlace ? { conversationPlace: d.conversationPlace } : {}),
385
434
  });
@@ -394,6 +443,23 @@ async function provenanceOf(input, postedConversationId, d) {
394
443
  return null;
395
444
  }
396
445
  }
446
+ /**
447
+ * The trigger of the input a post answers: for a post addressed to a work
448
+ * item, that work item's dispatch (the session's own trigger when it is that
449
+ * dispatch), never whatever was delivered to the agent last (R8).
450
+ *
451
+ * @param session - Agent
452
+ * @param d - Collaborators
453
+ * @param answering - The work item the post names, when it names one
454
+ * @returns The trigger, or null
455
+ */
456
+ function triggerOfAnsweredInput(session, d, answering) {
457
+ const current = d.turnTrigger?.(session) ?? null;
458
+ const id = answering?.workItemId;
459
+ if (!id || current?.workItemId === id)
460
+ return current;
461
+ return { triggerKind: 'dispatch', workItemId: id, rootKind: 'dispatch', rootRef: id, viaAgents: [], at: Date.now(), ...(current?.traceId ? { traceId: current.traceId } : {}) };
462
+ }
397
463
  /**
398
464
  * Provenance for a post an agent makes straight into a conversation
399
465
  * (`reply-channel`, `POST /chat/channels/:id/messages`), outside
@@ -431,7 +497,8 @@ async function attempt(input, dest, prompt, d) {
431
497
  const ticket = dest.source === 'ticket' ? input.reference?.ticket : dest.source === 'prompt' ? prompt?.ticket : undefined;
432
498
  const deliversTicket = ticket ? { [REPLY_ROUTING_CONSTANTS.DELIVERS_TICKET_METADATA_KEY]: ticket } : undefined;
433
499
  const answeredReference = dest.source === 'prompt' || dest.source === 'ticket' || dest.source === 'decision' || dest.source === 'work-item' || dest.source === 'message';
434
- const provenance = await provenanceOf(input, dest.kind === 'conversation' ? dest.conversationId : undefined, d);
500
+ const answeredWorkItem = dest.source === 'work-item' ? input.reference?.workItemId : dest.source === 'prompt' ? prompt?.workItemId : undefined;
501
+ const provenance = await provenanceOf(input, dest.kind === 'conversation' ? dest.conversationId : undefined, d, answeredWorkItem ? { workItemId: answeredWorkItem } : undefined);
435
502
  const metadata = { ...(deliversTicket ?? {}), ...(provenance?.metadata ?? {}) };
436
503
  if (provenance?.content !== undefined)
437
504
  input = { ...input, content: provenance.content };
@@ -461,7 +528,9 @@ async function attempt(input, dest, prompt, d) {
461
528
  const why = member
462
529
  ? `conversation ${dest.conversationId} (${dest.reason}) could not post it into its Slack thread`
463
530
  : `conversation ${dest.conversationId} (${dest.reason}) did not take it — you are not a member there`;
464
- return { ok: false, destination: dest, error: notDelivered(why, retryAdvice(dest, prompt)) };
531
+ // A work item's place is fixed by the item: resending the same reply lands there again.
532
+ const advice = dest.source === 'work-item' || dest.source === 'current-work' || dest.source === 'route' ? UNPOSTABLE_ADVICE : retryAdvice(dest, prompt);
533
+ return { ok: false, destination: dest, error: notDelivered(why, advice) };
465
534
  }
466
535
  if (answeredReference && input.interim !== true)
467
536
  AgentPromptReferenceService.getInstance().clear(input.session);
@@ -479,7 +548,7 @@ async function attempt(input, dest, prompt, d) {
479
548
  const posted = await deliverToWorkDestination(input.session, dest.destination, input.content, wd);
480
549
  if (!posted) {
481
550
  const why = dest.destination.kind === 'agent' ? `${dest.reason}: ${dest.destination.sessionName} could not be reached` : `${dest.reason}: there is no Slack channel to post in`;
482
- return { ok: false, destination: dest, error: notDelivered(why, fixCommand(prompt)) };
551
+ return { ok: false, destination: dest, error: notDelivered(why, UNPOSTABLE_ADVICE) };
483
552
  }
484
553
  if (answeredReference && input.interim !== true)
485
554
  AgentPromptReferenceService.getInstance().clear(input.session);
@@ -493,9 +562,35 @@ async function attempt(input, dest, prompt, d) {
493
562
  };
494
563
  }
495
564
  catch (err) {
496
- return { ok: false, destination: dest, error: notDelivered(`posting to Slack failed (${err instanceof Error ? err.message : String(err)})`, fixCommand(prompt)) };
565
+ return { ok: false, destination: dest, error: notDelivered(`posting to Slack failed (${err instanceof Error ? err.message : String(err)})`, UNPOSTABLE_ADVICE) };
497
566
  }
498
567
  }
568
+ /**
569
+ * Whether a chat thread is agent-to-agent coordination kept inside Crewly
570
+ * (crewly#1340): the message, or the root of its thread, carries
571
+ * `agentCoordinationInternal`. `reply-channel` keeps a post in such a thread
572
+ * internal; a file must not go to the Slack thread key stored on that
573
+ * message either (Pia's v3/v4 videos for Ella landed in Steve's thread,
574
+ * 2026-10-18).
575
+ *
576
+ * @param threadId - A chat message id (anything else is not internal)
577
+ * @param deps - Collaborators (default: the real ones)
578
+ * @returns Why the thread is internal, or null
579
+ */
580
+ export async function internalCoordinationThread(threadId, deps) {
581
+ if (typeof threadId !== 'string' || !threadId.trim() || parseSlackThreadKey(threadId))
582
+ return null;
583
+ const d = deps ?? (await defaultReplyDeliveryDeps());
584
+ const { AGENT_COORDINATION_CONSTANTS } = await import('../../constants.js');
585
+ const reasonOf = (id) => {
586
+ const v = id ? d.resolver.getMessage(id)?.metadata?.[AGENT_COORDINATION_CONSTANTS.INTERNAL_METADATA_KEY] : undefined;
587
+ return typeof v === 'string' && v ? v : null;
588
+ };
589
+ const msg = d.resolver.getMessage(threadId.trim());
590
+ if (!msg)
591
+ return null;
592
+ return reasonOf(msg.id) ?? reasonOf(msg.threadId);
593
+ }
499
594
  /**
500
595
  * The Slack place (channel + thread) the resolver gives, without posting —
501
596
  * for files and the `[DONE]` notice. Ticket threads that do not exist yet
@@ -747,7 +842,9 @@ export async function defaultReplyDeliveryDeps() {
747
842
  });
748
843
  },
749
844
  surfacedOwnerConversations: (session) => SubAgentMessageQueue.getInstance().surfacedUnansweredConversations(session),
750
- latestInput: (session) => latestInputOf(getTurnTriggerTracker().trigger(session)),
845
+ // What the agent answers: the latest input that names it, not a reminder
846
+ // or notice delivered since (message-routing audit 2026-10-18).
847
+ latestInput: (session) => latestInputOf(getTurnTriggerTracker().routedTrigger(session)),
751
848
  now: () => Date.now(),
752
849
  };
753
850
  const { deliverAgentReplyToConversation } = await import('../../controllers/chat/chat.controller.js');
@@ -768,6 +865,8 @@ export async function defaultReplyDeliveryDeps() {
768
865
  },
769
866
  turnTrigger: (session) => getTurnTriggerTracker().trigger(session),
770
867
  ownerAskedRecently: (session, conversationId) => getTurnTriggerTracker().ownerAskedRecently(session, conversationId),
868
+ ownerAskedEarlier: (session, conversationId) => getTurnTriggerTracker().ownerAskedWithin(session, conversationId, TURN_TRIGGER_CONSTANTS.FRESH_MS),
869
+ routedTrigger: (session) => getTurnTriggerTracker().routedTrigger(session),
771
870
  currentTrace: (session) => getTraceContext().currentTrace(session),
772
871
  conversationPlace: (id) => chatConversationPlace(chatLookup, id),
773
872
  };
@@ -807,4 +906,20 @@ export function chatConversationPlace(chat, conversationId) {
807
906
  return null;
808
907
  }
809
908
  }
909
+ /**
910
+ * The route an agent's own messages carry onward: what it is answering now
911
+ * (its latest routed input; message-route.ts). Never throws.
912
+ *
913
+ * @param session - Agent session
914
+ * @returns The route, or null when its input names none
915
+ */
916
+ export async function currentRouteOf(session) {
917
+ try {
918
+ const { OrcReplyRouteService } = await import('./orc-reply-route.service.js');
919
+ return routeOfTrigger(getTurnTriggerTracker().routedTrigger(session), OrcReplyRouteService.getInstance().getLastOrigin(session));
920
+ }
921
+ catch {
922
+ return null;
923
+ }
924
+ }
810
925
  //# sourceMappingURL=reply-destination.wiring.js.map