@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.
@@ -1,4 +1,4 @@
1
- import { cancelTask, createTaskExtended, getAllAgents, getTaskById, resolveUser } from "../be/db";
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, acknowledge the new session but don't create a duplicate
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
- acknowledgeAgentSession(
298
- sessionId,
299
- `This issue is already being worked on (task ${existing.swarmId}).`,
300
- ).catch((err) => {
301
- console.error("[Linear Sync] Failed to acknowledge duplicate AgentSession:", err);
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 = createTaskExtended(templateResult.text, {
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 = createTaskExtended(followupResult.text, {
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
@@ -212,6 +212,7 @@ class ClaudeSession implements ProviderSession {
212
212
  this.proc = Bun.spawn(cmd, {
213
213
  cwd: this.config.cwd,
214
214
  env: {
215
+ ENABLE_PROMPT_CACHING_1H: "1",
215
216
  ...(config.env || process.env),
216
217
  TASK_FILE: taskFilePath,
217
218
  } as Record<string, string>,
@@ -1,12 +1,8 @@
1
1
  import { ensure } from "@desplega.ai/business-use";
2
2
  import { CronExpressionParser } from "cron-parser";
3
- import {
4
- createTaskExtended,
5
- getDb,
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
- createTaskExtended(schedule.taskTemplate, {
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
- createTaskExtended(schedule.taskTemplate, {
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
- createTaskExtended(schedule.taskTemplate, {
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
  }
@@ -1,12 +1,7 @@
1
1
  import type { App } from "@slack/bolt";
2
- import {
3
- cancelTask,
4
- createTaskExtended,
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 = createTaskExtended(followUpText, {
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);
@@ -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
- createTaskExtended(messageText, {
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
- createTaskExtended(messageText + channelContext, {
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
- createTaskExtended(messageText + channelContext, {
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) {
@@ -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
- createTaskExtended(fullTaskDescription, {
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 = createTaskExtended(fullTaskDescription, {
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 = createTaskExtended(fullTaskDescription, {
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
- createTaskExtended,
3
- getLatestActiveTaskInThread,
4
- getLeadAgent,
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
- const key = makeKey(channelId, threadTs);
44
- const existing = threadBuffers.get(key);
45
-
46
- if (existing) {
47
- // Append to existing buffer, reset timer
48
- existing.messages.push({ text, userId, ts });
49
- clearTimeout(existing.timer);
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 threadBuffers.has(makeKey(channelId, threadTs));
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 threadBuffers.get(key)?.messages.length ?? 0;
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
- const buffer = threadBuffers.get(key);
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 dependency chaining.
134
- * @param key - The buffer key (channelId:threadTs)
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 flushBuffer(key: string, immediate = false): Promise<void> {
138
- const buffer = threadBuffers.get(key);
139
- if (!buffer || buffer.messages.length === 0) {
140
- threadBuffers.delete(key);
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 = buffer.messages.map((m) => m.text).join("\n---\n");
150
- const description = `[Thread follow-up — ${buffer.messages.length} message(s) buffered]\n\n${combinedText}`;
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(buffer.channelId, buffer.threadTs);
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(buffer.channelId, buffer.threadTs);
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(buffer.channelId, buffer.threadTs);
173
- const task = createTaskExtended(fullDescription, {
162
+ const mostRecentTask = getMostRecentTaskInThread(channelId, threadTs);
163
+ const task = createTaskWithSiblingAwareness(fullDescription, {
174
164
  agentId: lead?.id,
175
165
  source: "slack",
176
- slackChannelId: buffer.channelId,
177
- slackThreadTs: buffer.threadTs,
178
- slackUserId: buffer.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: buffer.messages.length,
183
+ messageCount: items.length,
193
184
  taskId: task.id,
194
185
  hasDependency,
195
186
  });
196
187
  const fallbackText = hasDependency
197
- ? `${buffer.messages.length} follow-up message(s) queued pending completion of current task`
198
- : `${buffer.messages.length} follow-up message(s) batched into task`;
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: buffer.channelId,
203
- thread_ts: buffer.threadTs,
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, buffer.channelId, buffer.threadTs, result.ts);
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
  }