@desplega.ai/agent-swarm 1.69.0 → 1.69.1
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.
- package/README.md +3 -3
- package/openapi.json +4 -1
- package/package.json +1 -1
- package/src/agentmail/handlers.ts +87 -6
- package/src/be/db.ts +34 -2
- package/src/be/migrations/042_task_context_key.sql +13 -0
- package/src/github/handlers.ts +42 -10
- package/src/gitlab/handlers.ts +29 -5
- package/src/http/schedules.ts +4 -2
- package/src/http/tasks.ts +4 -2
- package/src/linear/sync.ts +22 -10
- package/src/providers/claude-adapter.ts +1 -0
- package/src/scheduler/scheduler.ts +9 -10
- package/src/slack/actions.ts +10 -9
- package/src/slack/assistant.ts +8 -4
- package/src/slack/handlers.ts +8 -3
- package/src/slack/thread-buffer.ts +61 -72
- package/src/tasks/additive-buffer.ts +152 -0
- package/src/tasks/additive-ingress.ts +125 -0
- package/src/tasks/context-key.ts +245 -0
- package/src/tasks/sibling-awareness.ts +144 -0
- package/src/tasks/sibling-block.ts +164 -0
- package/src/tests/additive-buffer.test.ts +186 -0
- package/src/tests/additive-ingress.test.ts +111 -0
- package/src/tests/context-key-db.test.ts +87 -0
- package/src/tests/context-key.test.ts +173 -0
- package/src/tests/sibling-awareness-db.test.ts +172 -0
- package/src/tests/sibling-block.test.ts +232 -0
- package/src/types.ts +5 -0
- package/src/workflows/executors/agent-task.ts +21 -14
package/src/linear/sync.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cancelTask,
|
|
1
|
+
import { cancelTask, getAllAgents, getTaskById, resolveUser } from "../be/db";
|
|
2
2
|
import { getOAuthTokens } from "../be/db-queries/oauth";
|
|
3
3
|
import {
|
|
4
4
|
createTrackerSync,
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from "../be/db-queries/tracker";
|
|
9
9
|
import { ensureToken } from "../oauth/ensure-token";
|
|
10
10
|
import { resolveTemplate } from "../prompts/resolver";
|
|
11
|
+
import { linearContextKey } from "../tasks/context-key";
|
|
12
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
11
13
|
// Side-effect import: registers all Linear event templates in the in-memory registry
|
|
12
14
|
import "./templates";
|
|
13
15
|
|
|
@@ -287,18 +289,26 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
|
|
|
287
289
|
if (existing) {
|
|
288
290
|
const existingTask = getTaskById(existing.swarmId);
|
|
289
291
|
|
|
290
|
-
// If the task is still active,
|
|
292
|
+
// If the task is still active, post a user-visible response on the new
|
|
293
|
+
// session explaining that a sibling is already in flight and the new
|
|
294
|
+
// session can be closed. Do NOT create a duplicate swarm task. If the user
|
|
295
|
+
// wants to force a fresh run, they can re-assign the issue after the
|
|
296
|
+
// current task finishes.
|
|
291
297
|
if (existingTask && !["completed", "failed", "cancelled"].includes(existingTask.status)) {
|
|
292
298
|
console.log(
|
|
293
|
-
`[Linear Sync] Issue ${issueIdentifier} already tracked as active task ${existing.swarmId}, skipping`,
|
|
299
|
+
`[Linear Sync] Issue ${issueIdentifier} already tracked as active task ${existing.swarmId} (status: ${existingTask.status}), skipping duplicate`,
|
|
294
300
|
);
|
|
295
301
|
if (sessionId) {
|
|
296
302
|
taskSessionMap.set(existingTask.id, sessionId);
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
303
|
+
const refuseMsg = [
|
|
304
|
+
`This issue is already being worked on — task \`${existing.swarmId}\` is currently \`${existingTask.status}\`.`,
|
|
305
|
+
"",
|
|
306
|
+
"To avoid duplicating work, I'm not starting a new session for this re-assignment. Progress on the active task will continue to be posted here.",
|
|
307
|
+
"",
|
|
308
|
+
"If you want to force a fresh run, wait for the current task to finish (or cancel it) and re-assign the issue.",
|
|
309
|
+
].join("\n");
|
|
310
|
+
postAgentSessionResponse(sessionId, refuseMsg).catch((err) => {
|
|
311
|
+
console.error("[Linear Sync] Failed to post hard-refuse response:", err);
|
|
302
312
|
});
|
|
303
313
|
}
|
|
304
314
|
return;
|
|
@@ -327,11 +337,12 @@ export async function handleAgentSessionEvent(event: Record<string, unknown>): P
|
|
|
327
337
|
return;
|
|
328
338
|
}
|
|
329
339
|
|
|
330
|
-
const task =
|
|
340
|
+
const task = createTaskWithSiblingAwareness(templateResult.text, {
|
|
331
341
|
agentId: lead?.id ?? "",
|
|
332
342
|
source: "linear",
|
|
333
343
|
taskType: "linear-issue",
|
|
334
344
|
requestedByUserId,
|
|
345
|
+
contextKey: linearContextKey({ issueIdentifier }),
|
|
335
346
|
});
|
|
336
347
|
|
|
337
348
|
// Delete old tracker_sync before creating new one (UNIQUE constraint)
|
|
@@ -570,11 +581,12 @@ export async function handleAgentSessionPrompted(event: Record<string, unknown>)
|
|
|
570
581
|
return;
|
|
571
582
|
}
|
|
572
583
|
|
|
573
|
-
const task =
|
|
584
|
+
const task = createTaskWithSiblingAwareness(followupResult.text, {
|
|
574
585
|
agentId: lead?.id ?? "",
|
|
575
586
|
source: "linear",
|
|
576
587
|
taskType: "linear-issue",
|
|
577
588
|
requestedByUserId: promptedRequestedByUserId,
|
|
589
|
+
contextKey: linearContextKey({ issueIdentifier }),
|
|
578
590
|
});
|
|
579
591
|
|
|
580
592
|
// Repoint the existing tracker_sync to the new follow-up task (can't create a
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { ensure } from "@desplega.ai/business-use";
|
|
2
2
|
import { CronExpressionParser } from "cron-parser";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
getDueScheduledTasks,
|
|
7
|
-
getScheduledTaskById,
|
|
8
|
-
updateScheduledTask,
|
|
9
|
-
} from "@/be/db";
|
|
3
|
+
import { getDb, getDueScheduledTasks, getScheduledTaskById, updateScheduledTask } from "@/be/db";
|
|
4
|
+
import { scheduleContextKey } from "@/tasks/context-key";
|
|
5
|
+
import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
|
|
10
6
|
import type { ScheduledTask } from "@/types";
|
|
11
7
|
import type { ExecutorRegistry } from "@/workflows/executors/registry";
|
|
12
8
|
import { handleScheduleTrigger } from "@/workflows/triggers";
|
|
@@ -49,7 +45,7 @@ async function recoverMissedSchedules(): Promise<void> {
|
|
|
49
45
|
|
|
50
46
|
if (!triggeredWorkflows) {
|
|
51
47
|
const tx = getDb().transaction(() => {
|
|
52
|
-
|
|
48
|
+
createTaskWithSiblingAwareness(schedule.taskTemplate, {
|
|
53
49
|
creatorAgentId: schedule.createdByAgentId,
|
|
54
50
|
taskType: schedule.taskType,
|
|
55
51
|
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, "recovered"],
|
|
@@ -58,6 +54,7 @@ async function recoverMissedSchedules(): Promise<void> {
|
|
|
58
54
|
model: schedule.model,
|
|
59
55
|
scheduleId: schedule.id,
|
|
60
56
|
source: "schedule",
|
|
57
|
+
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
61
58
|
});
|
|
62
59
|
});
|
|
63
60
|
tx();
|
|
@@ -153,7 +150,7 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
|
|
|
153
150
|
if (!triggeredWorkflows) {
|
|
154
151
|
// No workflows linked — create standalone task (existing behavior)
|
|
155
152
|
getDb().transaction(() => {
|
|
156
|
-
|
|
153
|
+
createTaskWithSiblingAwareness(schedule.taskTemplate, {
|
|
157
154
|
creatorAgentId: schedule.createdByAgentId,
|
|
158
155
|
taskType: schedule.taskType,
|
|
159
156
|
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`],
|
|
@@ -162,6 +159,7 @@ async function executeSchedule(schedule: ScheduledTask): Promise<void> {
|
|
|
162
159
|
model: schedule.model,
|
|
163
160
|
scheduleId: schedule.id,
|
|
164
161
|
source: "schedule",
|
|
162
|
+
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
165
163
|
});
|
|
166
164
|
})();
|
|
167
165
|
}
|
|
@@ -343,7 +341,7 @@ export async function runScheduleNow(scheduleId: string): Promise<void> {
|
|
|
343
341
|
if (!triggeredWorkflows) {
|
|
344
342
|
// No workflows linked — create standalone task (existing behavior)
|
|
345
343
|
getDb().transaction(() => {
|
|
346
|
-
|
|
344
|
+
createTaskWithSiblingAwareness(schedule.taskTemplate, {
|
|
347
345
|
creatorAgentId: schedule.createdByAgentId,
|
|
348
346
|
taskType: schedule.taskType,
|
|
349
347
|
tags: [...schedule.tags, "scheduled", `schedule:${schedule.name}`, "manual-run"],
|
|
@@ -352,6 +350,7 @@ export async function runScheduleNow(scheduleId: string): Promise<void> {
|
|
|
352
350
|
model: schedule.model,
|
|
353
351
|
scheduleId: schedule.id,
|
|
354
352
|
source: "schedule",
|
|
353
|
+
contextKey: scheduleContextKey({ scheduleId: schedule.id }),
|
|
355
354
|
});
|
|
356
355
|
})();
|
|
357
356
|
}
|
package/src/slack/actions.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import type { App } from "@slack/bolt";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
getAgentById,
|
|
6
|
-
getLeadAgent,
|
|
7
|
-
getTaskById,
|
|
8
|
-
resolveUser,
|
|
9
|
-
} from "../be/db";
|
|
2
|
+
import { cancelTask, getAgentById, getLeadAgent, getTaskById, resolveUser } from "../be/db";
|
|
3
|
+
import { slackContextKey } from "../tasks/context-key";
|
|
4
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
10
5
|
import { buildCancelledBlocks, getTaskLink } from "./blocks";
|
|
11
6
|
|
|
12
7
|
export function registerActionHandlers(app: App): void {
|
|
@@ -73,7 +68,7 @@ export function registerActionHandlers(app: App): void {
|
|
|
73
68
|
|
|
74
69
|
const lead = getLeadAgent();
|
|
75
70
|
const requestedByUserId = resolveUser({ slackUserId: body.user.id })?.id;
|
|
76
|
-
const followUpTask =
|
|
71
|
+
const followUpTask = createTaskWithSiblingAwareness(followUpText, {
|
|
77
72
|
agentId: lead?.id,
|
|
78
73
|
source: "slack",
|
|
79
74
|
parentTaskId: taskId,
|
|
@@ -81,6 +76,12 @@ export function registerActionHandlers(app: App): void {
|
|
|
81
76
|
slackThreadTs: originalTask.slackThreadTs,
|
|
82
77
|
slackUserId: body.user.id,
|
|
83
78
|
requestedByUserId,
|
|
79
|
+
contextKey: originalTask.slackThreadTs
|
|
80
|
+
? slackContextKey({
|
|
81
|
+
channelId: originalTask.slackChannelId,
|
|
82
|
+
threadTs: originalTask.slackThreadTs,
|
|
83
|
+
})
|
|
84
|
+
: undefined,
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
const taskLink = getTaskLink(followUpTask.id);
|
package/src/slack/assistant.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Assistant } from "@slack/bolt";
|
|
2
2
|
import {
|
|
3
|
-
createTaskExtended,
|
|
4
3
|
getAgentWorkingOnThread,
|
|
5
4
|
getLeadAgent,
|
|
6
5
|
getMostRecentTaskInThread,
|
|
7
6
|
resolveUser,
|
|
8
7
|
} from "../be/db";
|
|
9
8
|
import { resolveTemplate } from "../prompts/resolver";
|
|
9
|
+
import { slackContextKey } from "../tasks/context-key";
|
|
10
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
10
11
|
import { bufferThreadMessage } from "./thread-buffer";
|
|
11
12
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
12
13
|
import "./templates";
|
|
@@ -82,7 +83,7 @@ export function createAssistant(): Assistant {
|
|
|
82
83
|
|
|
83
84
|
// Otherwise, create a follow-up task for the working agent
|
|
84
85
|
const latestTask = getMostRecentTaskInThread(channelId, threadTs);
|
|
85
|
-
|
|
86
|
+
createTaskWithSiblingAwareness(messageText, {
|
|
86
87
|
agentId: workingAgent.id,
|
|
87
88
|
source: "slack",
|
|
88
89
|
slackChannelId: channelId,
|
|
@@ -90,6 +91,7 @@ export function createAssistant(): Assistant {
|
|
|
90
91
|
slackUserId: userId,
|
|
91
92
|
parentTaskId: latestTask?.id,
|
|
92
93
|
requestedByUserId,
|
|
94
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
93
95
|
});
|
|
94
96
|
|
|
95
97
|
await safeSetStatus("Processing follow-up...");
|
|
@@ -114,25 +116,27 @@ export function createAssistant(): Assistant {
|
|
|
114
116
|
const lead = getLeadAgent();
|
|
115
117
|
if (!lead) {
|
|
116
118
|
// No lead — still queue the task
|
|
117
|
-
|
|
119
|
+
createTaskWithSiblingAwareness(messageText + channelContext, {
|
|
118
120
|
source: "slack",
|
|
119
121
|
slackChannelId: channelId,
|
|
120
122
|
slackThreadTs: threadTs,
|
|
121
123
|
slackUserId: userId,
|
|
122
124
|
requestedByUserId,
|
|
125
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
123
126
|
});
|
|
124
127
|
const offlineResult = resolveTemplate("slack.assistant.offline", {});
|
|
125
128
|
await say(offlineResult.text);
|
|
126
129
|
return;
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
createTaskWithSiblingAwareness(messageText + channelContext, {
|
|
130
133
|
agentId: lead.id,
|
|
131
134
|
source: "slack",
|
|
132
135
|
slackChannelId: channelId,
|
|
133
136
|
slackThreadTs: threadTs,
|
|
134
137
|
slackUserId: userId,
|
|
135
138
|
requestedByUserId,
|
|
139
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
136
140
|
});
|
|
137
141
|
// setStatus shows typing indicator — watcher will post final result when done
|
|
138
142
|
} catch (error) {
|
package/src/slack/handlers.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
resolveUser,
|
|
11
11
|
} from "../be/db";
|
|
12
12
|
import { resolveTemplate } from "../prompts/resolver";
|
|
13
|
+
import { slackContextKey } from "../tasks/context-key";
|
|
14
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
13
15
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
14
16
|
import { buildTreeBlocks, type TreeNode } from "./blocks";
|
|
15
17
|
import type { SlackFile } from "./files";
|
|
@@ -538,13 +540,14 @@ export function registerMessageHandler(app: App): void {
|
|
|
538
540
|
}
|
|
539
541
|
|
|
540
542
|
const lead = getLeadAgent();
|
|
541
|
-
|
|
543
|
+
createTaskWithSiblingAwareness(fullTaskDescription, {
|
|
542
544
|
agentId: lead?.id,
|
|
543
545
|
source: "slack",
|
|
544
546
|
slackChannelId: msg.channel,
|
|
545
547
|
slackThreadTs: threadTs,
|
|
546
548
|
slackUserId: msg.user,
|
|
547
549
|
requestedByUserId,
|
|
550
|
+
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
548
551
|
});
|
|
549
552
|
|
|
550
553
|
await say({
|
|
@@ -610,7 +613,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
610
613
|
try {
|
|
611
614
|
const latestTask = getMostRecentTaskInThread(msg.channel, threadTs);
|
|
612
615
|
if (agent.isLead) {
|
|
613
|
-
const task =
|
|
616
|
+
const task = createTaskWithSiblingAwareness(fullTaskDescription, {
|
|
614
617
|
agentId: agent.id,
|
|
615
618
|
source: "slack",
|
|
616
619
|
slackChannelId: msg.channel,
|
|
@@ -618,19 +621,21 @@ export function registerMessageHandler(app: App): void {
|
|
|
618
621
|
slackUserId: msg.user,
|
|
619
622
|
parentTaskId: latestTask?.id,
|
|
620
623
|
requestedByUserId,
|
|
624
|
+
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
621
625
|
});
|
|
622
626
|
results.assigned.push({ agentName: agent.name, taskId: task.id });
|
|
623
627
|
continue;
|
|
624
628
|
}
|
|
625
629
|
|
|
626
630
|
// Workers receive tasks as before
|
|
627
|
-
const task =
|
|
631
|
+
const task = createTaskWithSiblingAwareness(fullTaskDescription, {
|
|
628
632
|
agentId: agent.id,
|
|
629
633
|
source: "slack",
|
|
630
634
|
slackChannelId: msg.channel,
|
|
631
635
|
slackThreadTs: threadTs,
|
|
632
636
|
slackUserId: msg.user,
|
|
633
637
|
requestedByUserId,
|
|
638
|
+
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
634
639
|
});
|
|
635
640
|
|
|
636
641
|
// Check if agent has an in-progress task in this thread (queued follow-up)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
getMostRecentTaskInThread,
|
|
6
|
-
} from "../be/db";
|
|
1
|
+
import { getLatestActiveTaskInThread, getLeadAgent, getMostRecentTaskInThread } from "../be/db";
|
|
2
|
+
import { createAdditiveBuffer } from "../tasks/additive-buffer";
|
|
3
|
+
import { slackContextKey } from "../tasks/context-key";
|
|
4
|
+
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
7
5
|
import { getSlackApp } from "./app";
|
|
8
6
|
import { buildBufferFlushBlocks } from "./blocks";
|
|
9
7
|
import { registerTreeMessage } from "./watcher";
|
|
@@ -12,24 +10,30 @@ interface BufferedMessage {
|
|
|
12
10
|
text: string;
|
|
13
11
|
userId: string;
|
|
14
12
|
ts: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface BufferedThread {
|
|
18
13
|
channelId: string;
|
|
19
14
|
threadTs: string;
|
|
20
|
-
messages: BufferedMessage[];
|
|
21
|
-
timer: Timer;
|
|
22
|
-
slackUserId: string; // original requester (first message sender)
|
|
23
15
|
}
|
|
24
16
|
|
|
25
|
-
const threadBuffers = new Map<string, BufferedThread>();
|
|
26
|
-
|
|
27
17
|
const BUFFER_TIMEOUT_MS = Number(process.env.ADDITIVE_SLACK_BUFFER_MS) || 10_000;
|
|
28
18
|
|
|
29
19
|
function makeKey(channelId: string, threadTs: string): string {
|
|
30
20
|
return `${channelId}:${threadTs}`;
|
|
31
21
|
}
|
|
32
22
|
|
|
23
|
+
function splitKey(key: string): { channelId: string; threadTs: string } | null {
|
|
24
|
+
const idx = key.indexOf(":");
|
|
25
|
+
if (idx === -1) return null;
|
|
26
|
+
return { channelId: key.slice(0, idx), threadTs: key.slice(idx + 1) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const slackBuffer = createAdditiveBuffer<BufferedMessage>({
|
|
30
|
+
timeoutMs: BUFFER_TIMEOUT_MS,
|
|
31
|
+
label: "slack-thread",
|
|
32
|
+
onFlush: async (items, key, reason) => {
|
|
33
|
+
await slackFlush(items, key, reason === "manual");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
33
37
|
/**
|
|
34
38
|
* Add a message to the thread buffer. Resets the debounce timer.
|
|
35
39
|
*/
|
|
@@ -40,43 +44,27 @@ export function bufferThreadMessage(
|
|
|
40
44
|
userId: string,
|
|
41
45
|
ts: string,
|
|
42
46
|
): void {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
existing.timer = setTimeout(() => flushBuffer(key, false), BUFFER_TIMEOUT_MS);
|
|
51
|
-
console.log(
|
|
52
|
-
`[Slack] Buffer append: ${key} (${existing.messages.length} messages, timer reset to ${BUFFER_TIMEOUT_MS}ms)`,
|
|
53
|
-
);
|
|
54
|
-
} else {
|
|
55
|
-
// Create new buffer entry
|
|
56
|
-
const timer = setTimeout(() => flushBuffer(key, false), BUFFER_TIMEOUT_MS);
|
|
57
|
-
threadBuffers.set(key, {
|
|
58
|
-
channelId,
|
|
59
|
-
threadTs,
|
|
60
|
-
messages: [{ text, userId, ts }],
|
|
61
|
-
timer,
|
|
62
|
-
slackUserId: userId,
|
|
63
|
-
});
|
|
64
|
-
console.log(`[Slack] Buffer created: ${key} (timer set to ${BUFFER_TIMEOUT_MS}ms)`);
|
|
65
|
-
}
|
|
47
|
+
slackBuffer.enqueue(makeKey(channelId, threadTs), {
|
|
48
|
+
text,
|
|
49
|
+
userId,
|
|
50
|
+
ts,
|
|
51
|
+
channelId,
|
|
52
|
+
threadTs,
|
|
53
|
+
});
|
|
66
54
|
}
|
|
67
55
|
|
|
68
56
|
/**
|
|
69
57
|
* Check if a thread currently has a pending buffer.
|
|
70
58
|
*/
|
|
71
59
|
export function isThreadBuffered(channelId: string, threadTs: string): boolean {
|
|
72
|
-
return
|
|
60
|
+
return slackBuffer.isBuffered(makeKey(channelId, threadTs));
|
|
73
61
|
}
|
|
74
62
|
|
|
75
63
|
/**
|
|
76
64
|
* Get the number of messages currently in the buffer for a thread key.
|
|
77
65
|
*/
|
|
78
66
|
export function getBufferMessageCount(key: string): number {
|
|
79
|
-
return
|
|
67
|
+
return slackBuffer.count(key);
|
|
80
68
|
}
|
|
81
69
|
|
|
82
70
|
/**
|
|
@@ -84,12 +72,7 @@ export function getBufferMessageCount(key: string): number {
|
|
|
84
72
|
* and flushes with immediate=true (no dependsOn).
|
|
85
73
|
*/
|
|
86
74
|
export async function instantFlush(key: string): Promise<void> {
|
|
87
|
-
|
|
88
|
-
if (buffer) {
|
|
89
|
-
clearTimeout(buffer.timer);
|
|
90
|
-
console.log(`[Slack] Instant flush triggered: ${key}`);
|
|
91
|
-
}
|
|
92
|
-
await flushBuffer(key, true);
|
|
75
|
+
await slackBuffer.instantFlush(key);
|
|
93
76
|
}
|
|
94
77
|
|
|
95
78
|
/**
|
|
@@ -130,27 +113,34 @@ async function getThreadContextForBuffer(channelId: string, threadTs: string): P
|
|
|
130
113
|
}
|
|
131
114
|
|
|
132
115
|
/**
|
|
133
|
-
* Flush the buffer: concatenate messages, create task with optional
|
|
134
|
-
*
|
|
135
|
-
* @param immediate - If true, skip dependency chaining (used by !now)
|
|
116
|
+
* Flush the Slack thread buffer: concatenate messages, create task with optional
|
|
117
|
+
* dependency chaining.
|
|
136
118
|
*/
|
|
137
|
-
async function
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
119
|
+
async function slackFlush(
|
|
120
|
+
items: BufferedMessage[],
|
|
121
|
+
key: string,
|
|
122
|
+
immediate: boolean,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
if (items.length === 0) return;
|
|
125
|
+
|
|
126
|
+
const split = splitKey(key);
|
|
127
|
+
if (!split) {
|
|
128
|
+
console.warn(`[Slack] Buffer flush: malformed key ${key}`);
|
|
141
129
|
return;
|
|
142
130
|
}
|
|
131
|
+
const { channelId, threadTs } = split;
|
|
132
|
+
// Buffer is guaranteed to have at least one item — the first carries the
|
|
133
|
+
// original requester's userId (same semantics as the pre-refactor version).
|
|
134
|
+
const originalRequesterId = items[0]!.userId;
|
|
143
135
|
|
|
144
|
-
console.log(
|
|
145
|
-
`[Slack] Flushing buffer: ${key} (${buffer.messages.length} messages, immediate=${immediate})`,
|
|
146
|
-
);
|
|
136
|
+
console.log(`[Slack] Flushing buffer: ${key} (${items.length} messages, immediate=${immediate})`);
|
|
147
137
|
|
|
148
138
|
// Build combined task description
|
|
149
|
-
const combinedText =
|
|
150
|
-
const description = `[Thread follow-up — ${
|
|
139
|
+
const combinedText = items.map((m) => m.text).join("\n---\n");
|
|
140
|
+
const description = `[Thread follow-up — ${items.length} message(s) buffered]\n\n${combinedText}`;
|
|
151
141
|
|
|
152
142
|
// Find the latest active task in this thread for dependency chaining
|
|
153
|
-
const latestActiveTask = getLatestActiveTaskInThread(
|
|
143
|
+
const latestActiveTask = getLatestActiveTaskInThread(channelId, threadTs);
|
|
154
144
|
if (latestActiveTask) {
|
|
155
145
|
console.log(
|
|
156
146
|
`[Slack] Dependency chaining: latest active task ${latestActiveTask.id} (status: ${latestActiveTask.status})`,
|
|
@@ -160,7 +150,7 @@ async function flushBuffer(key: string, immediate = false): Promise<void> {
|
|
|
160
150
|
const lead = getLeadAgent();
|
|
161
151
|
|
|
162
152
|
// Thread context for the task
|
|
163
|
-
const threadContext = await getThreadContextForBuffer(
|
|
153
|
+
const threadContext = await getThreadContextForBuffer(channelId, threadTs);
|
|
164
154
|
const fullDescription = threadContext
|
|
165
155
|
? `<thread_context>\n${threadContext}\n</thread_context>\n\n${description}`
|
|
166
156
|
: description;
|
|
@@ -169,15 +159,16 @@ async function flushBuffer(key: string, immediate = false): Promise<void> {
|
|
|
169
159
|
// Otherwise, depend on the latest active task so it queues naturally.
|
|
170
160
|
const dependsOn = !immediate && latestActiveTask ? [latestActiveTask.id] : undefined;
|
|
171
161
|
|
|
172
|
-
const mostRecentTask = getMostRecentTaskInThread(
|
|
173
|
-
const task =
|
|
162
|
+
const mostRecentTask = getMostRecentTaskInThread(channelId, threadTs);
|
|
163
|
+
const task = createTaskWithSiblingAwareness(fullDescription, {
|
|
174
164
|
agentId: lead?.id,
|
|
175
165
|
source: "slack",
|
|
176
|
-
slackChannelId:
|
|
177
|
-
slackThreadTs:
|
|
178
|
-
slackUserId:
|
|
166
|
+
slackChannelId: channelId,
|
|
167
|
+
slackThreadTs: threadTs,
|
|
168
|
+
slackUserId: originalRequesterId,
|
|
179
169
|
dependsOn,
|
|
180
170
|
parentTaskId: mostRecentTask?.id,
|
|
171
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
181
172
|
});
|
|
182
173
|
|
|
183
174
|
console.log(
|
|
@@ -189,18 +180,18 @@ async function flushBuffer(key: string, immediate = false): Promise<void> {
|
|
|
189
180
|
if (app) {
|
|
190
181
|
const hasDependency = !immediate && !!latestActiveTask;
|
|
191
182
|
const blocks = buildBufferFlushBlocks({
|
|
192
|
-
messageCount:
|
|
183
|
+
messageCount: items.length,
|
|
193
184
|
taskId: task.id,
|
|
194
185
|
hasDependency,
|
|
195
186
|
});
|
|
196
187
|
const fallbackText = hasDependency
|
|
197
|
-
? `${
|
|
198
|
-
: `${
|
|
188
|
+
? `${items.length} follow-up message(s) queued pending completion of current task`
|
|
189
|
+
: `${items.length} follow-up message(s) batched into task`;
|
|
199
190
|
|
|
200
191
|
try {
|
|
201
192
|
const result = await app.client.chat.postMessage({
|
|
202
|
-
channel:
|
|
203
|
-
thread_ts:
|
|
193
|
+
channel: channelId,
|
|
194
|
+
thread_ts: threadTs,
|
|
204
195
|
text: fallbackText,
|
|
205
196
|
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
206
197
|
blocks: blocks as any,
|
|
@@ -208,7 +199,7 @@ async function flushBuffer(key: string, immediate = false): Promise<void> {
|
|
|
208
199
|
|
|
209
200
|
// Register the batching message as the tree message for this task
|
|
210
201
|
if (result.ts && task) {
|
|
211
|
-
registerTreeMessage(task.id,
|
|
202
|
+
registerTreeMessage(task.id, channelId, threadTs, result.ts);
|
|
212
203
|
console.log(
|
|
213
204
|
`[Slack] Registered batched task ${task.id.slice(0, 8)} tree message from buffer flush`,
|
|
214
205
|
);
|
|
@@ -217,6 +208,4 @@ async function flushBuffer(key: string, immediate = false): Promise<void> {
|
|
|
217
208
|
console.error("[Slack] Failed to post buffer flush feedback:", error);
|
|
218
209
|
}
|
|
219
210
|
}
|
|
220
|
-
|
|
221
|
-
threadBuffers.delete(key);
|
|
222
211
|
}
|