crewly 1.20.225 → 1.20.227

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 (95) hide show
  1. package/dist/backend/backend/src/constants.d.ts +11 -1
  2. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  3. package/dist/backend/backend/src/constants.js +11 -1
  4. package/dist/backend/backend/src/constants.js.map +1 -1
  5. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.d.ts.map +1 -1
  6. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js +6 -0
  7. package/dist/backend/backend/src/controllers/chat/agent-reply.controller.js.map +1 -1
  8. package/dist/backend/backend/src/index.d.ts.map +1 -1
  9. package/dist/backend/backend/src/index.js +10 -0
  10. package/dist/backend/backend/src/index.js.map +1 -1
  11. package/dist/backend/backend/src/services/agent/claude-runtime.service.d.ts.map +1 -1
  12. package/dist/backend/backend/src/services/agent/claude-runtime.service.js +15 -0
  13. package/dist/backend/backend/src/services/agent/claude-runtime.service.js.map +1 -1
  14. package/dist/backend/backend/src/services/apps/app-thumbnail.service.d.ts.map +1 -1
  15. package/dist/backend/backend/src/services/apps/app-thumbnail.service.js +8 -2
  16. package/dist/backend/backend/src/services/apps/app-thumbnail.service.js.map +1 -1
  17. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +35 -0
  18. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  19. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +64 -4
  20. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  21. package/dist/backend/backend/src/services/decisions/decision.service.d.ts +6 -0
  22. package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  23. package/dist/backend/backend/src/services/decisions/decision.service.js +29 -9
  24. package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
  25. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts +9 -1
  26. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
  27. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js +8 -2
  28. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
  29. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.d.ts.map +1 -1
  30. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.js +20 -2
  31. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.wiring.js.map +1 -1
  32. package/dist/backend/backend/src/services/messaging/queue-priority.d.ts +127 -0
  33. package/dist/backend/backend/src/services/messaging/queue-priority.d.ts.map +1 -0
  34. package/dist/backend/backend/src/services/messaging/queue-priority.js +146 -0
  35. package/dist/backend/backend/src/services/messaging/queue-priority.js.map +1 -0
  36. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +55 -3
  37. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  38. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js +142 -10
  39. package/dist/backend/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  40. package/dist/backend/backend/src/services/open-items/open-items.service.d.ts +12 -0
  41. package/dist/backend/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/open-items/open-items.service.js +17 -2
  43. package/dist/backend/backend/src/services/open-items/open-items.service.js.map +1 -1
  44. package/dist/backend/backend/src/services/slack/slack-initializer.d.ts.map +1 -1
  45. package/dist/backend/backend/src/services/slack/slack-initializer.js +25 -0
  46. package/dist/backend/backend/src/services/slack/slack-initializer.js.map +1 -1
  47. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +36 -0
  48. package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  49. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +134 -5
  50. package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  51. package/dist/backend/backend/src/services/slack/slack.service.d.ts +35 -4
  52. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/slack/slack.service.js +95 -0
  54. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  55. package/dist/backend/build-info.json +2 -2
  56. package/dist/cli/backend/src/constants.d.ts +11 -1
  57. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  58. package/dist/cli/backend/src/constants.js +11 -1
  59. package/dist/cli/backend/src/constants.js.map +1 -1
  60. package/dist/cli/backend/src/services/agent/claude-runtime.service.d.ts.map +1 -1
  61. package/dist/cli/backend/src/services/agent/claude-runtime.service.js +15 -0
  62. package/dist/cli/backend/src/services/agent/claude-runtime.service.js.map +1 -1
  63. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +35 -0
  64. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  65. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +64 -4
  66. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  67. package/dist/cli/backend/src/services/decisions/decision.service.d.ts +6 -0
  68. package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  69. package/dist/cli/backend/src/services/decisions/decision.service.js +29 -9
  70. package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
  71. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts +9 -1
  72. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
  73. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js +8 -2
  74. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
  75. package/dist/cli/backend/src/services/messaging/queue-priority.d.ts +127 -0
  76. package/dist/cli/backend/src/services/messaging/queue-priority.d.ts.map +1 -0
  77. package/dist/cli/backend/src/services/messaging/queue-priority.js +146 -0
  78. package/dist/cli/backend/src/services/messaging/queue-priority.js.map +1 -0
  79. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts +55 -3
  80. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.d.ts.map +1 -1
  81. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js +142 -10
  82. package/dist/cli/backend/src/services/messaging/sub-agent-message-queue.service.js.map +1 -1
  83. package/dist/cli/backend/src/services/open-items/open-items.service.d.ts +12 -0
  84. package/dist/cli/backend/src/services/open-items/open-items.service.d.ts.map +1 -1
  85. package/dist/cli/backend/src/services/open-items/open-items.service.js +17 -2
  86. package/dist/cli/backend/src/services/open-items/open-items.service.js.map +1 -1
  87. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +36 -0
  88. package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
  89. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +134 -5
  90. package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
  91. package/dist/cli/backend/src/services/slack/slack.service.d.ts +35 -4
  92. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  93. package/dist/cli/backend/src/services/slack/slack.service.js +95 -0
  94. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  95. package/package.json +1 -1
@@ -0,0 +1,127 @@
1
+ /**
2
+ * What an agent's message queue needs to know about a message beyond its
3
+ * text: whether the owner wrote it (it goes ahead of system traffic), which
4
+ * conversation and thread it belongs to (for a colleague's message or a
5
+ * reminder, a reply there after it was queued means it was answered — never
6
+ * for the owner's own message), the owner message it carries (so a second
7
+ * copy is recognised), and which earlier reminder it replaces.
8
+ *
9
+ * Callers that know this (the chat dispatcher for an owner's message, the
10
+ * decision service for an owner's answer, the owner-message watchdog for its
11
+ * reminder, the open-items service for a "promised work is ready" reminder)
12
+ * run their delivery inside {@link withQueueMeta}. If the agent is busy and
13
+ * the delivery path queues the message, {@link SubAgentMessageQueue.enqueue}
14
+ * picks the meta up from the async context — no delivery signature changes.
15
+ *
16
+ * Case that motivated it (2026-10-05, #education, D-270): the owner's answer
17
+ * to Atlas's decision card was queued at position 7 behind stale reminders,
18
+ * handed over one per idle moment oldest-first, and reached Atlas after
19
+ * ~18 minutes; a duplicate of an owner message Atlas had already answered sat
20
+ * in front of it.
21
+ *
22
+ * @module services/messaging/queue-priority
23
+ */
24
+ /** Where a queued message's answer belongs. */
25
+ export interface QueueConversation {
26
+ /** chat-v2 channel the message was recorded in */
27
+ chatChannelId?: string;
28
+ /** chat-v2 thread the answer belongs in (huddles) */
29
+ chatThreadId?: string;
30
+ /** Slack channel / DM */
31
+ slackChannelId?: string;
32
+ /** Slack thread root */
33
+ threadTs?: string;
34
+ }
35
+ /** Metadata stored with a queued message. */
36
+ export interface QueueMessageMeta {
37
+ /** The owner wrote it (or it carries the owner's words / answer): goes ahead of everything else */
38
+ owner?: boolean;
39
+ /** Identity of the owner message it carries (`slack:<ch>:<ts>`, `chat:<ch>:<id>`, `decision:<id>`); a later queued copy is a duplicate */
40
+ ref?: string;
41
+ /** Conversation (and thread) the answer belongs in */
42
+ where?: QueueConversation;
43
+ /** A newer queued message with the same key replaces an older one (a reminder about the same promise) */
44
+ supersedeKey?: string;
45
+ /**
46
+ * A harness reminder that carries the owner's words (the watchdog's nudge):
47
+ * owner priority, but — unlike the owner's own message — dropped once the
48
+ * agent has answered in its thread.
49
+ */
50
+ reminder?: boolean;
51
+ }
52
+ /**
53
+ * Run a delivery with queue metadata for one message to one agent. If the
54
+ * delivery queues that message for that agent, the queued copy carries the
55
+ * metadata.
56
+ *
57
+ * @param session - The agent the delivery is for
58
+ * @param data - The exact text being delivered
59
+ * @param meta - The metadata
60
+ * @param fn - The delivery
61
+ * @returns What `fn` returns
62
+ */
63
+ export declare function withQueueMeta<T>(session: string, data: string, meta: QueueMessageMeta, fn: () => T): T;
64
+ /**
65
+ * The metadata of the delivery running now, if it is this message for this agent.
66
+ *
67
+ * @param session - The agent a message is being queued for
68
+ * @param data - The message being queued
69
+ * @returns The metadata, or undefined
70
+ */
71
+ export declare function currentQueueMeta(session: string, data: string): QueueMessageMeta | undefined;
72
+ /**
73
+ * The agents' recent posts, by agent. Fed by the chat-v2 turn stream and the
74
+ * `reply` endpoint; read by the queue when it flushes.
75
+ */
76
+ export declare class AgentPostLog {
77
+ private static instance;
78
+ private readonly posts;
79
+ /** @returns The process-wide log */
80
+ static getInstance(): AgentPostLog;
81
+ /** Reset (tests). */
82
+ static resetInstance(): void;
83
+ /**
84
+ * Record that an agent posted (a real answer, not an interim "working on it").
85
+ *
86
+ * @param session - The agent
87
+ * @param where - Where it posted
88
+ * @param at - When (default now)
89
+ */
90
+ note(session: string, where: QueueConversation, at?: number): void;
91
+ /**
92
+ * Whether the agent posted in a conversation (and thread) after a moment.
93
+ *
94
+ * Matched by thread only (see {@link sameConversation}).
95
+ *
96
+ * @param session - The agent
97
+ * @param where - The queued message's conversation
98
+ * @param since - When it was queued
99
+ * @returns True when the agent spoke there after `since`
100
+ */
101
+ postedSince(session: string, where: QueueConversation, since: number): boolean;
102
+ }
103
+ /**
104
+ * Whether a post landed in the conversation (and thread) a queued message belongs to.
105
+ *
106
+ * @param post - Where the agent posted
107
+ * @param queued - Where the queued message's answer belongs
108
+ * @returns True on a match
109
+ */
110
+ export declare function sameConversation(post: QueueConversation, queued: QueueConversation): boolean;
111
+ /**
112
+ * Feed a recorded chat-v2 turn to the post log: an agent's real answer (not
113
+ * an interim "working on it") in a conversation, and the Slack thread it
114
+ * mirrors to when the turn says so.
115
+ *
116
+ * @param dto - The turn (sender, channel, thread, metadata)
117
+ * @param log - The post log (default: the process-wide one)
118
+ */
119
+ export declare function noteAgentChatTurn(dto: {
120
+ senderType: string;
121
+ senderId: string;
122
+ channelId: string;
123
+ threadId?: string | null;
124
+ createdAt?: number;
125
+ metadata?: Record<string, unknown>;
126
+ }, log?: AgentPostLog): void;
127
+ //# sourceMappingURL=queue-priority.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-priority.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/messaging/queue-priority.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,+CAA+C;AAC/C,MAAM,WAAW,iBAAiB;IACjC,kDAAkD;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yBAAyB;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAChC,mGAAmG;IACnG,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,0IAA0I;IAC1I,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,yGAAyG;IACzG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtG;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAG5F;AAiBD;;;GAGG;AACH,qBAAa,YAAY;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IAExD,oCAAoC;IACpC,MAAM,CAAC,WAAW,IAAI,YAAY;IAKlC,qBAAqB;IACrB,MAAM,CAAC,aAAa,IAAI,IAAI;IAI5B;;;;;;OAMG;IACH,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,EAAE,GAAE,MAAmB,GAAG,IAAI;IAS9E;;;;;;;;;OASG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;CAK9E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAW5F;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,GAAG,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAClJ,GAAG,GAAE,YAAyC,GAC5C,IAAI,CAWN"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * What an agent's message queue needs to know about a message beyond its
3
+ * text: whether the owner wrote it (it goes ahead of system traffic), which
4
+ * conversation and thread it belongs to (for a colleague's message or a
5
+ * reminder, a reply there after it was queued means it was answered — never
6
+ * for the owner's own message), the owner message it carries (so a second
7
+ * copy is recognised), and which earlier reminder it replaces.
8
+ *
9
+ * Callers that know this (the chat dispatcher for an owner's message, the
10
+ * decision service for an owner's answer, the owner-message watchdog for its
11
+ * reminder, the open-items service for a "promised work is ready" reminder)
12
+ * run their delivery inside {@link withQueueMeta}. If the agent is busy and
13
+ * the delivery path queues the message, {@link SubAgentMessageQueue.enqueue}
14
+ * picks the meta up from the async context — no delivery signature changes.
15
+ *
16
+ * Case that motivated it (2026-10-05, #education, D-270): the owner's answer
17
+ * to Atlas's decision card was queued at position 7 behind stale reminders,
18
+ * handed over one per idle moment oldest-first, and reached Atlas after
19
+ * ~18 minutes; a duplicate of an owner message Atlas had already answered sat
20
+ * in front of it.
21
+ *
22
+ * @module services/messaging/queue-priority
23
+ */
24
+ import { AsyncLocalStorage } from 'async_hooks';
25
+ const context = new AsyncLocalStorage();
26
+ /**
27
+ * Run a delivery with queue metadata for one message to one agent. If the
28
+ * delivery queues that message for that agent, the queued copy carries the
29
+ * metadata.
30
+ *
31
+ * @param session - The agent the delivery is for
32
+ * @param data - The exact text being delivered
33
+ * @param meta - The metadata
34
+ * @param fn - The delivery
35
+ * @returns What `fn` returns
36
+ */
37
+ export function withQueueMeta(session, data, meta, fn) {
38
+ return context.run({ session, data, meta }, fn);
39
+ }
40
+ /**
41
+ * The metadata of the delivery running now, if it is this message for this agent.
42
+ *
43
+ * @param session - The agent a message is being queued for
44
+ * @param data - The message being queued
45
+ * @returns The metadata, or undefined
46
+ */
47
+ export function currentQueueMeta(session, data) {
48
+ const bound = context.getStore();
49
+ return bound && bound.session === session && bound.data === data ? bound.meta : undefined;
50
+ }
51
+ /** How long posts are remembered; a queued message older than this is judged by the other rules. */
52
+ const POST_KEEP_MS = 24 * 60 * 60 * 1000;
53
+ /** Posts remembered per agent. */
54
+ const POSTS_PER_AGENT = 200;
55
+ /**
56
+ * The agents' recent posts, by agent. Fed by the chat-v2 turn stream and the
57
+ * `reply` endpoint; read by the queue when it flushes.
58
+ */
59
+ export class AgentPostLog {
60
+ static instance = null;
61
+ posts = new Map();
62
+ /** @returns The process-wide log */
63
+ static getInstance() {
64
+ if (!AgentPostLog.instance)
65
+ AgentPostLog.instance = new AgentPostLog();
66
+ return AgentPostLog.instance;
67
+ }
68
+ /** Reset (tests). */
69
+ static resetInstance() {
70
+ AgentPostLog.instance = null;
71
+ }
72
+ /**
73
+ * Record that an agent posted (a real answer, not an interim "working on it").
74
+ *
75
+ * @param session - The agent
76
+ * @param where - Where it posted
77
+ * @param at - When (default now)
78
+ */
79
+ note(session, where, at = Date.now()) {
80
+ if (!session || (!where.chatChannelId && !where.slackChannelId))
81
+ return;
82
+ const list = this.posts.get(session) ?? [];
83
+ list.push({ at, where: { ...where } });
84
+ const cutoff = Date.now() - POST_KEEP_MS;
85
+ const kept = list.filter((p) => p.at >= cutoff).slice(-POSTS_PER_AGENT);
86
+ this.posts.set(session, kept);
87
+ }
88
+ /**
89
+ * Whether the agent posted in a conversation (and thread) after a moment.
90
+ *
91
+ * Matched by thread only (see {@link sameConversation}).
92
+ *
93
+ * @param session - The agent
94
+ * @param where - The queued message's conversation
95
+ * @param since - When it was queued
96
+ * @returns True when the agent spoke there after `since`
97
+ */
98
+ postedSince(session, where, since) {
99
+ const list = this.posts.get(session);
100
+ if (!list)
101
+ return false;
102
+ return list.some((p) => p.at > since && sameConversation(p.where, where));
103
+ }
104
+ }
105
+ /**
106
+ * Whether a post landed in the conversation (and thread) a queued message belongs to.
107
+ *
108
+ * @param post - Where the agent posted
109
+ * @param queued - Where the queued message's answer belongs
110
+ * @returns True on a match
111
+ */
112
+ export function sameConversation(post, queued) {
113
+ // Only a thread identifies one conversation. A top-level DM is never
114
+ // matched: the agent posting there about something else (an earlier
115
+ // request finishing) must not make a newer owner message look answered.
116
+ if (queued.chatChannelId && queued.chatThreadId && post.chatChannelId === queued.chatChannelId && post.chatThreadId === queued.chatThreadId) {
117
+ return true;
118
+ }
119
+ if (queued.slackChannelId && queued.threadTs && post.slackChannelId === queued.slackChannelId && post.threadTs === queued.threadTs) {
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+ /**
125
+ * Feed a recorded chat-v2 turn to the post log: an agent's real answer (not
126
+ * an interim "working on it") in a conversation, and the Slack thread it
127
+ * mirrors to when the turn says so.
128
+ *
129
+ * @param dto - The turn (sender, channel, thread, metadata)
130
+ * @param log - The post log (default: the process-wide one)
131
+ */
132
+ export function noteAgentChatTurn(dto, log = AgentPostLog.getInstance()) {
133
+ if (dto.senderType !== 'agent' || !dto.senderId)
134
+ return;
135
+ const meta = dto.metadata ?? {};
136
+ if (meta.interim === true)
137
+ return;
138
+ const slackChannelId = typeof meta.slackChannelId === 'string' ? meta.slackChannelId : undefined;
139
+ const threadTs = typeof meta.slackThreadTs === 'string' ? meta.slackThreadTs : undefined;
140
+ log.note(dto.senderId, {
141
+ chatChannelId: dto.channelId,
142
+ ...(dto.threadId ? { chatThreadId: dto.threadId } : {}),
143
+ ...(slackChannelId && threadTs ? { slackChannelId, threadTs } : {}),
144
+ });
145
+ }
146
+ //# sourceMappingURL=queue-priority.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queue-priority.js","sourceRoot":"","sources":["../../../../../../backend/src/services/messaging/queue-priority.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * @module sub-agent-message-queue
12
12
  */
13
+ import { type QueueMessageMeta } from './queue-priority.js';
13
14
  /**
14
15
  * A single queued message destined for a sub-agent.
15
16
  */
@@ -38,7 +39,23 @@ export interface QueuedAgentMessage {
38
39
  * conversation — runs when it is finally delivered (crewly#1015 review).
39
40
  */
40
41
  workItemId?: string;
42
+ /**
43
+ * Who wrote it and where its answer belongs (see queue-priority.ts):
44
+ * owner messages go ahead of system traffic, and a message answered in
45
+ * its thread, a second copy of an owner message, or a reminder replaced
46
+ * by a newer one is dropped at flush time.
47
+ */
48
+ meta?: QueueMessageMeta;
41
49
  }
50
+ /**
51
+ * Owner messages first, each group in its existing order (stable).
52
+ *
53
+ * @param list - Queue
54
+ * @returns The queue in delivery order
55
+ */
56
+ export declare function ownerFirst(list: QueuedAgentMessage[]): QueuedAgentMessage[];
57
+ /** Why a queued message was dropped before delivery as no longer needed. */
58
+ export type QueuePruneReason = 'answered-in-thread' | 'duplicate-owner-message' | 'superseded';
42
59
  /** A hand-over prepared for a queued WorkItem brief (see {@link SubAgentMessageQueue.setHandOverPreparer}). */
43
60
  export interface QueuedHandOver {
44
61
  /** Text to write */
@@ -198,19 +215,26 @@ export declare class SubAgentMessageQueue {
198
215
  }>;
199
216
  /**
200
217
  * Enqueue a message for a session that is not yet active.
201
- * If the queue exceeds MAX_QUEUE_SIZE, the oldest message is dropped.
218
+ * If the queue exceeds MAX_QUEUE_SIZE, the oldest non-owner message is
219
+ * dropped (the oldest message when all are the owner's).
202
220
  *
203
221
  * A message identical to one already waiting for the same session is not
204
222
  * added again: the reconciler re-sends the same reminder while an agent
205
223
  * is stopped (daily token cap, busy), and each copy would cost the agent
206
- * a turn once delivered. The waiting copy keeps its place.
224
+ * a turn once delivered. The waiting copy keeps its place. Neither is a
225
+ * second copy of the same owner message (same `ref`).
226
+ *
227
+ * Owner messages (see {@link QueueMessageMeta}, taken from `meta.queueMeta`
228
+ * or the delivery running now — {@link withQueueMeta}) go to the front,
229
+ * behind owner messages already waiting; everything else goes to the back.
207
230
  *
208
231
  * @param sessionName - The target agent session name
209
232
  * @param data - The raw data string to deliver later
210
- * @param meta - `workItemId` when the message hands over a WorkItem
233
+ * @param meta - `workItemId` when the message hands over a WorkItem; `queueMeta` to override the context
211
234
  */
212
235
  enqueue(sessionName: string, data: string, meta?: {
213
236
  workItemId?: string;
237
+ queueMeta?: QueueMessageMeta;
214
238
  }): void;
215
239
  /**
216
240
  * Dequeue all pending messages for a session (FIFO order).
@@ -257,6 +281,34 @@ export declare class SubAgentMessageQueue {
257
281
  failed: number;
258
282
  skippedStale: number;
259
283
  }>;
284
+ /**
285
+ * Drop queued messages that are no longer needed, before delivery:
286
+ * a later copy of an owner message already queued (same `ref`), and a
287
+ * reminder replaced by a newer one (same `supersedeKey`; the newest is
288
+ * kept). Each drop is logged. Messages without metadata are untouched.
289
+ *
290
+ * @param sessionName - The agent
291
+ * @param pending - Its queue, in delivery order
292
+ * @param out - Counters (`skippedStale` is incremented per drop)
293
+ * @returns The messages still to deliver, in order
294
+ */
295
+ private pruneNoLongerNeeded;
296
+ /**
297
+ * Whether the agent has already posted in the thread a queued message
298
+ * belongs to, after it was queued — it was answered (the agent read the
299
+ * thread). Only messages that name a thread are judged this way.
300
+ *
301
+ * Never an owner's own message (or the owner's decision answer): the owner
302
+ * may ask a follow-up in the same thread while the agent is still
303
+ * answering the first question, and that post must not make the follow-up
304
+ * look answered. Owner messages are dropped only as exact duplicates.
305
+ * Harness reminders carrying the owner's words (`reminder`) are judged.
306
+ *
307
+ * @param m - Queued message
308
+ * @returns True when it was answered
309
+ */
310
+ private answeredInThread;
311
+ private logPrune;
260
312
  /**
261
313
  * Sessions that still have messages waiting for them. A restart must bring
262
314
  * these agents back: a message queued for an agent is work in hand even
@@ -1 +1 @@
1
- {"version":3,"file":"sub-agent-message-queue.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/messaging/sub-agent-message-queue.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sub-agent-message-queue.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/messaging/sub-agent-message-queue.service.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,28 @@ import { SUB_AGENT_QUEUE_CONSTANTS } from '../../constants.js';
16
16
  import { mkdirSync, existsSync } from 'fs';
17
17
  import { atomicWriteFileSync, quarantineCorruptFileSync, readJsonStoreSync, CorruptJsonFileError } from '../../utils/file-io.utils.js';
18
18
  import { getCrewlyHomePath } from '../core/crewly-home.utils.js';
19
+ import { AgentPostLog, currentQueueMeta } from './queue-priority.js';
20
+ /**
21
+ * Whether a queued message was written by (or carries the answer of) the owner.
22
+ *
23
+ * @param m - Queued message
24
+ * @returns True for owner traffic
25
+ */
26
+ function isOwnerItem(m) {
27
+ return m.meta?.owner === true;
28
+ }
29
+ /**
30
+ * Owner messages first, each group in its existing order (stable).
31
+ *
32
+ * @param list - Queue
33
+ * @returns The queue in delivery order
34
+ */
35
+ export function ownerFirst(list) {
36
+ const owner = list.filter(isOwnerItem);
37
+ if (owner.length === 0 || owner.length === list.length)
38
+ return list;
39
+ return [...owner, ...list.filter((m) => !isOwnerItem(m))];
40
+ }
19
41
  /**
20
42
  * Singleton service that holds pending messages per agent session.
21
43
  *
@@ -103,7 +125,7 @@ export class SubAgentMessageQueue {
103
125
  if (usable.length === 0)
104
126
  continue;
105
127
  // Restored messages are older than anything queued since: they go first.
106
- this.pendingMessages.set(sessionName, [...usable, ...current]);
128
+ this.pendingMessages.set(sessionName, ownerFirst([...usable, ...current]));
107
129
  restored += usable.length;
108
130
  }
109
131
  if (restored > 0) {
@@ -338,18 +360,25 @@ export class SubAgentMessageQueue {
338
360
  }
339
361
  /**
340
362
  * Enqueue a message for a session that is not yet active.
341
- * If the queue exceeds MAX_QUEUE_SIZE, the oldest message is dropped.
363
+ * If the queue exceeds MAX_QUEUE_SIZE, the oldest non-owner message is
364
+ * dropped (the oldest message when all are the owner's).
342
365
  *
343
366
  * A message identical to one already waiting for the same session is not
344
367
  * added again: the reconciler re-sends the same reminder while an agent
345
368
  * is stopped (daily token cap, busy), and each copy would cost the agent
346
- * a turn once delivered. The waiting copy keeps its place.
369
+ * a turn once delivered. The waiting copy keeps its place. Neither is a
370
+ * second copy of the same owner message (same `ref`).
371
+ *
372
+ * Owner messages (see {@link QueueMessageMeta}, taken from `meta.queueMeta`
373
+ * or the delivery running now — {@link withQueueMeta}) go to the front,
374
+ * behind owner messages already waiting; everything else goes to the back.
347
375
  *
348
376
  * @param sessionName - The target agent session name
349
377
  * @param data - The raw data string to deliver later
350
- * @param meta - `workItemId` when the message hands over a WorkItem
378
+ * @param meta - `workItemId` when the message hands over a WorkItem; `queueMeta` to override the context
351
379
  */
352
380
  enqueue(sessionName, data, meta = {}) {
381
+ const queueMeta = meta.queueMeta ?? currentQueueMeta(sessionName, data);
353
382
  let queue = this.pendingMessages.get(sessionName);
354
383
  if (!queue) {
355
384
  queue = [];
@@ -363,9 +392,18 @@ export class SubAgentMessageQueue {
363
392
  });
364
393
  return;
365
394
  }
366
- // Drop oldest if at capacity
395
+ if (queueMeta?.ref && queue.some((m) => m.meta?.ref === queueMeta.ref)) {
396
+ this.logger.info('Same owner message already queued for the agent — not added again', {
397
+ sessionName,
398
+ ref: queueMeta.ref,
399
+ queueSize: queue.length,
400
+ });
401
+ return;
402
+ }
403
+ // Drop the oldest non-owner message if at capacity (the owner's last).
367
404
  if (queue.length >= SUB_AGENT_QUEUE_CONSTANTS.MAX_QUEUE_SIZE) {
368
- const dropped = queue.shift();
405
+ const victim = queue.findIndex((m) => !isOwnerItem(m));
406
+ const dropped = queue.splice(victim >= 0 ? victim : 0, 1)[0];
369
407
  this.logger.warn('Queue at capacity, dropping oldest message', {
370
408
  sessionName,
371
409
  droppedAt: dropped?.queuedAt,
@@ -374,16 +412,27 @@ export class SubAgentMessageQueue {
374
412
  if (dropped)
375
413
  this.reportDrop(sessionName, [dropped], 'capacity');
376
414
  }
377
- queue.push({
415
+ const item = {
378
416
  data,
379
417
  queuedAt: Date.now(),
380
418
  sessionName,
381
419
  ...(meta.workItemId ? { workItemId: meta.workItemId } : {}),
382
- });
420
+ ...(queueMeta && Object.keys(queueMeta).length > 0 ? { meta: { ...queueMeta } } : {}),
421
+ };
422
+ // The owner's messages go ahead of system traffic, in the order they came.
423
+ let position = queue.length;
424
+ if (isOwnerItem(item)) {
425
+ const firstOther = queue.findIndex((m) => !isOwnerItem(m));
426
+ if (firstOther >= 0)
427
+ position = firstOther;
428
+ }
429
+ queue.splice(position, 0, item);
383
430
  this.save();
384
431
  this.logger.info('Message queued for sub-agent', {
385
432
  sessionName,
386
433
  queueSize: queue.length,
434
+ position: position + 1,
435
+ ...(isOwnerItem(item) ? { owner: true } : {}),
387
436
  dataLength: data.length,
388
437
  });
389
438
  }
@@ -446,8 +495,8 @@ export class SubAgentMessageQueue {
446
495
  * @returns How many were delivered, deferred again, failed, and dropped as stale
447
496
  */
448
497
  async flush(sessionName, send, gapMs = 0) {
449
- const pending = this.dequeueAll(sessionName);
450
498
  const out = { delivered: 0, deferred: 0, failed: 0, skippedStale: 0 };
499
+ const pending = this.pruneNoLongerNeeded(sessionName, this.dequeueAll(sessionName), out);
451
500
  // A send that failed ("Session does not exist", "Runtime has exited")
452
501
  // or threw is not a delivery: the message goes back on the queue, up
453
502
  // to MAX_DELIVERY_ATTEMPTS, then the drop is reported (crewly#1014).
@@ -473,6 +522,11 @@ export class SubAgentMessageQueue {
473
522
  }
474
523
  };
475
524
  for (const [i, queued] of pending.entries()) {
525
+ if (this.answeredInThread(queued)) {
526
+ out.skippedStale += 1;
527
+ this.logPrune(sessionName, queued, 'answered-in-thread');
528
+ continue;
529
+ }
476
530
  if (await this.isStale(queued.data, sessionName)) {
477
531
  out.skippedStale += 1;
478
532
  continue;
@@ -541,7 +595,7 @@ export class SubAgentMessageQueue {
541
595
  // Back at the front, in their original order, ahead of anything
542
596
  // queued meanwhile (failed ones came before the held one).
543
597
  const queue = (this.pendingMessages.get(sessionName) ?? []).filter((m) => !heldAgain.some((h) => h.data === m.data) && !failedAgain.some((f) => f.data === m.data));
544
- this.pendingMessages.set(sessionName, [...failedAgain, ...heldAgain, ...queue]);
598
+ this.pendingMessages.set(sessionName, ownerFirst([...failedAgain, ...heldAgain, ...queue]));
545
599
  this.save();
546
600
  }
547
601
  if (undeliverable.length > 0)
@@ -555,6 +609,84 @@ export class SubAgentMessageQueue {
555
609
  }
556
610
  return out;
557
611
  }
612
+ /**
613
+ * Drop queued messages that are no longer needed, before delivery:
614
+ * a later copy of an owner message already queued (same `ref`), and a
615
+ * reminder replaced by a newer one (same `supersedeKey`; the newest is
616
+ * kept). Each drop is logged. Messages without metadata are untouched.
617
+ *
618
+ * @param sessionName - The agent
619
+ * @param pending - Its queue, in delivery order
620
+ * @param out - Counters (`skippedStale` is incremented per drop)
621
+ * @returns The messages still to deliver, in order
622
+ */
623
+ pruneNoLongerNeeded(sessionName, pending, out) {
624
+ const newestBySupersede = new Map();
625
+ for (const m of pending) {
626
+ const key = m.meta?.supersedeKey;
627
+ if (!key)
628
+ continue;
629
+ const cur = newestBySupersede.get(key);
630
+ if (!cur || m.queuedAt >= cur.queuedAt)
631
+ newestBySupersede.set(key, m);
632
+ }
633
+ const seenRefs = new Set();
634
+ const kept = [];
635
+ for (const m of pending) {
636
+ const key = m.meta?.supersedeKey;
637
+ if (key && newestBySupersede.get(key) !== m) {
638
+ out.skippedStale += 1;
639
+ this.logPrune(sessionName, m, 'superseded');
640
+ continue;
641
+ }
642
+ const ref = m.meta?.ref;
643
+ if (ref) {
644
+ if (seenRefs.has(ref)) {
645
+ out.skippedStale += 1;
646
+ this.logPrune(sessionName, m, 'duplicate-owner-message');
647
+ continue;
648
+ }
649
+ seenRefs.add(ref);
650
+ }
651
+ kept.push(m);
652
+ }
653
+ if (kept.length < pending.length)
654
+ this.save();
655
+ return kept;
656
+ }
657
+ /**
658
+ * Whether the agent has already posted in the thread a queued message
659
+ * belongs to, after it was queued — it was answered (the agent read the
660
+ * thread). Only messages that name a thread are judged this way.
661
+ *
662
+ * Never an owner's own message (or the owner's decision answer): the owner
663
+ * may ask a follow-up in the same thread while the agent is still
664
+ * answering the first question, and that post must not make the follow-up
665
+ * look answered. Owner messages are dropped only as exact duplicates.
666
+ * Harness reminders carrying the owner's words (`reminder`) are judged.
667
+ *
668
+ * @param m - Queued message
669
+ * @returns True when it was answered
670
+ */
671
+ answeredInThread(m) {
672
+ const where = m.meta?.where;
673
+ if (!where)
674
+ return false;
675
+ if (m.meta?.owner && !m.meta.reminder)
676
+ return false;
677
+ return AgentPostLog.getInstance().postedSince(m.sessionName, where, m.queuedAt);
678
+ }
679
+ logPrune(sessionName, m, reason) {
680
+ this.logger.info('Queued message dropped before delivery — no longer needed', {
681
+ sessionName,
682
+ reason,
683
+ queuedAt: new Date(m.queuedAt).toISOString(),
684
+ ...(m.meta?.owner ? { owner: true } : {}),
685
+ ...(m.meta?.ref ? { ref: m.meta.ref } : {}),
686
+ ...(m.meta?.supersedeKey ? { supersedeKey: m.meta.supersedeKey } : {}),
687
+ preview: m.data.slice(0, 80),
688
+ });
689
+ }
558
690
  /**
559
691
  * Sessions that still have messages waiting for them. A restart must bring
560
692
  * these agents back: a message queued for an agent is work in hand even