@desplega.ai/agent-swarm 1.60.0 → 1.62.0
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/openapi.json +1 -1
- package/package.json +1 -1
- package/src/be/db.ts +15 -0
- package/src/be/migrations/034_slack_reply_sent.sql +4 -0
- package/src/slack/blocks.ts +179 -2
- package/src/slack/handlers.ts +46 -7
- package/src/slack/responses.ts +59 -4
- package/src/slack/thread-buffer.ts +10 -1
- package/src/slack/watcher.ts +417 -22
- package/src/tests/slack-blocks.test.ts +597 -0
- package/src/tests/slack-watcher.test.ts +737 -2
- package/src/tools/send-task.ts +8 -5
- package/src/tools/slack-reply.ts +13 -1
- package/src/types.ts +1 -0
package/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Swarm API",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.62.0",
|
|
6
6
|
"description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
|
|
7
7
|
},
|
|
8
8
|
"servers": [
|
package/package.json
CHANGED
package/src/be/db.ts
CHANGED
|
@@ -710,6 +710,7 @@ type AgentTaskRow = {
|
|
|
710
710
|
slackChannelId: string | null;
|
|
711
711
|
slackThreadTs: string | null;
|
|
712
712
|
slackUserId: string | null;
|
|
713
|
+
slackReplySent: number;
|
|
713
714
|
vcsProvider: string | null;
|
|
714
715
|
vcsRepo: string | null;
|
|
715
716
|
vcsEventType: string | null;
|
|
@@ -768,6 +769,7 @@ function rowToAgentTask(row: AgentTaskRow): AgentTask {
|
|
|
768
769
|
slackChannelId: row.slackChannelId ?? undefined,
|
|
769
770
|
slackThreadTs: row.slackThreadTs ?? undefined,
|
|
770
771
|
slackUserId: row.slackUserId ?? undefined,
|
|
772
|
+
slackReplySent: !!row.slackReplySent,
|
|
771
773
|
vcsProvider: (row.vcsProvider as "github" | "gitlab" | null) ?? undefined,
|
|
772
774
|
vcsRepo: row.vcsRepo ?? undefined,
|
|
773
775
|
vcsEventType: row.vcsEventType ?? undefined,
|
|
@@ -968,6 +970,19 @@ export function getTaskById(id: string): AgentTask | null {
|
|
|
968
970
|
return row ? rowToAgentTask(row) : null;
|
|
969
971
|
}
|
|
970
972
|
|
|
973
|
+
export function markTaskSlackReplySent(taskId: string): void {
|
|
974
|
+
getDb().run(`UPDATE agent_tasks SET slackReplySent = 1 WHERE id = ?`, [taskId]);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export function getChildTasks(parentTaskId: string): AgentTask[] {
|
|
978
|
+
return getDb()
|
|
979
|
+
.prepare<AgentTaskRow, [string]>(
|
|
980
|
+
`SELECT * FROM agent_tasks WHERE parentTaskId = ? ORDER BY createdAt ASC`,
|
|
981
|
+
)
|
|
982
|
+
.all(parentTaskId)
|
|
983
|
+
.map(rowToAgentTask);
|
|
984
|
+
}
|
|
985
|
+
|
|
971
986
|
export function updateTaskClaudeSessionId(
|
|
972
987
|
taskId: string,
|
|
973
988
|
claudeSessionId: string,
|
package/src/slack/blocks.ts
CHANGED
|
@@ -128,6 +128,38 @@ function cancelActionBlock(taskId: string): SlackBlock {
|
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// --- Utilities ---
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Format a duration between two dates in a compact human-readable form.
|
|
135
|
+
* Examples: "45s", "2m 14s", "1h 30m"
|
|
136
|
+
*/
|
|
137
|
+
export function formatDuration(start: Date, end: Date): string {
|
|
138
|
+
const ms = end.getTime() - start.getTime();
|
|
139
|
+
const seconds = Math.floor(ms / 1000);
|
|
140
|
+
if (seconds < 60) return `${seconds}s`;
|
|
141
|
+
const minutes = Math.floor(seconds / 60);
|
|
142
|
+
const remainingSeconds = seconds % 60;
|
|
143
|
+
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
|
144
|
+
const hours = Math.floor(minutes / 60);
|
|
145
|
+
const remainingMinutes = minutes % 60;
|
|
146
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Tree types ---
|
|
150
|
+
|
|
151
|
+
export interface TreeNode {
|
|
152
|
+
taskId: string;
|
|
153
|
+
agentName: string;
|
|
154
|
+
status: "pending" | "in_progress" | "completed" | "failed" | "cancelled";
|
|
155
|
+
progress?: string;
|
|
156
|
+
duration?: string;
|
|
157
|
+
slackReplySent?: boolean;
|
|
158
|
+
output?: string; // Only used when !slackReplySent on completion
|
|
159
|
+
failureReason?: string; // Always shown on failure
|
|
160
|
+
children: TreeNode[];
|
|
161
|
+
}
|
|
162
|
+
|
|
131
163
|
// --- High-level block builders ---
|
|
132
164
|
|
|
133
165
|
/**
|
|
@@ -139,14 +171,19 @@ export function buildCompletedBlocks(opts: {
|
|
|
139
171
|
taskId: string;
|
|
140
172
|
body: string;
|
|
141
173
|
duration?: string;
|
|
174
|
+
minimal?: boolean; // true = suppress body (agent already replied via slack-reply)
|
|
142
175
|
}): SlackBlock[] {
|
|
143
176
|
const taskLink = getTaskLink(opts.taskId);
|
|
144
177
|
let line = `✅ *${opts.agentName}* (${taskLink})`;
|
|
145
178
|
if (opts.duration) line += ` · ${opts.duration}`;
|
|
146
179
|
|
|
147
180
|
const blocks: SlackBlock[] = [sectionBlock(line)];
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
|
|
182
|
+
// Only include body if not minimal (agent didn't reply via slack-reply)
|
|
183
|
+
if (!opts.minimal) {
|
|
184
|
+
for (const chunk of splitText(opts.body)) {
|
|
185
|
+
blocks.push(sectionBlock(chunk));
|
|
186
|
+
}
|
|
150
187
|
}
|
|
151
188
|
return blocks;
|
|
152
189
|
}
|
|
@@ -231,3 +268,143 @@ export function buildBufferFlushBlocks(opts: {
|
|
|
231
268
|
|
|
232
269
|
return [contextBlock(`📡 _${text}_ (${taskLink})`)];
|
|
233
270
|
}
|
|
271
|
+
|
|
272
|
+
// --- Tree rendering ---
|
|
273
|
+
|
|
274
|
+
const STATUS_ICON: Record<TreeNode["status"], string> = {
|
|
275
|
+
pending: "📡",
|
|
276
|
+
in_progress: "⏳",
|
|
277
|
+
completed: "✅",
|
|
278
|
+
failed: "❌",
|
|
279
|
+
cancelled: "🚫",
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const MAX_VISIBLE_CHILDREN = 8;
|
|
283
|
+
const MAX_OUTPUT_LENGTH = 120;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Truncate output to the first sentence or MAX_OUTPUT_LENGTH, whichever is shorter.
|
|
287
|
+
*/
|
|
288
|
+
function truncateOutput(text: string): string {
|
|
289
|
+
// Find first sentence boundary (. followed by space or end)
|
|
290
|
+
const sentenceEnd = text.search(/\.\s/);
|
|
291
|
+
const firstSentence = sentenceEnd !== -1 ? text.slice(0, sentenceEnd + 1) : text;
|
|
292
|
+
if (firstSentence.length <= MAX_OUTPUT_LENGTH) return firstSentence;
|
|
293
|
+
return `${text.slice(0, MAX_OUTPUT_LENGTH)}…`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Render a single node line: icon + bold name + task link + optional duration.
|
|
298
|
+
*/
|
|
299
|
+
function renderNodeLine(node: TreeNode): string {
|
|
300
|
+
const icon = STATUS_ICON[node.status];
|
|
301
|
+
const taskLink = getTaskLink(node.taskId);
|
|
302
|
+
let line = `${icon} *${node.agentName}* (${taskLink})`;
|
|
303
|
+
if (node.duration) line += ` · ${node.duration}`;
|
|
304
|
+
return line;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Render detail lines for a child node (progress, output, failure reason).
|
|
309
|
+
* Returns an array of indented lines to appear below the child's main line.
|
|
310
|
+
*/
|
|
311
|
+
function renderChildDetail(node: TreeNode, indent: string): string[] {
|
|
312
|
+
const lines: string[] = [];
|
|
313
|
+
|
|
314
|
+
if (node.status === "failed" && node.failureReason) {
|
|
315
|
+
lines.push(`${indent}Error: ${node.failureReason}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (node.status === "in_progress" && node.progress) {
|
|
319
|
+
lines.push(`${indent}${node.progress}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (node.status === "completed" && !node.slackReplySent && node.output) {
|
|
323
|
+
lines.push(`${indent}${truncateOutput(node.output)}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return lines;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Render a single root node and its children as a mrkdwn tree string.
|
|
331
|
+
*/
|
|
332
|
+
function renderTree(root: TreeNode): string {
|
|
333
|
+
const lines: string[] = [];
|
|
334
|
+
|
|
335
|
+
// Root line
|
|
336
|
+
lines.push(renderNodeLine(root));
|
|
337
|
+
|
|
338
|
+
// Root-level detail (progress for in-progress root with no children)
|
|
339
|
+
if (root.children.length === 0) {
|
|
340
|
+
if (root.status === "in_progress" && root.progress) {
|
|
341
|
+
lines.push(` ${root.progress}`);
|
|
342
|
+
}
|
|
343
|
+
if (root.status === "failed" && root.failureReason) {
|
|
344
|
+
lines.push(` Error: ${root.failureReason}`);
|
|
345
|
+
}
|
|
346
|
+
if (root.status === "completed" && !root.slackReplySent && root.output) {
|
|
347
|
+
lines.push(` ${truncateOutput(root.output)}`);
|
|
348
|
+
}
|
|
349
|
+
return lines.join("\n");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const visibleChildren = root.children.slice(0, MAX_VISIBLE_CHILDREN);
|
|
353
|
+
const hiddenCount = root.children.length - visibleChildren.length;
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < visibleChildren.length; i++) {
|
|
356
|
+
const child = visibleChildren[i] as TreeNode;
|
|
357
|
+
const isLast = i === visibleChildren.length - 1 && hiddenCount === 0;
|
|
358
|
+
const prefix = isLast ? "└ " : "├ ";
|
|
359
|
+
const continuationPrefix = isLast ? " " : "│ ";
|
|
360
|
+
|
|
361
|
+
lines.push(`${prefix}${renderNodeLine(child)}`);
|
|
362
|
+
|
|
363
|
+
for (const detail of renderChildDetail(child, continuationPrefix)) {
|
|
364
|
+
lines.push(detail);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (hiddenCount > 0) {
|
|
369
|
+
lines.push(`└ _and ${hiddenCount} more..._`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return lines.join("\n");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check if any node in the tree is still active (pending or in_progress).
|
|
377
|
+
*/
|
|
378
|
+
function isTreeActive(node: TreeNode): boolean {
|
|
379
|
+
if (node.status === "pending" || node.status === "in_progress") return true;
|
|
380
|
+
return node.children.some(isTreeActive);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Build Slack blocks for a tree-based status message.
|
|
385
|
+
*
|
|
386
|
+
* Renders one or more root nodes as mrkdwn trees with status icons,
|
|
387
|
+
* agent names, task links, durations, progress text, and error details.
|
|
388
|
+
*
|
|
389
|
+
* For in-progress trees, includes a cancel button per active root.
|
|
390
|
+
*
|
|
391
|
+
* @param roots - Array of root TreeNode objects (one per assigned task in a round)
|
|
392
|
+
* @returns SlackBlock[] suitable for chat.postMessage / chat.update
|
|
393
|
+
*/
|
|
394
|
+
export function buildTreeBlocks(roots: TreeNode[]): SlackBlock[] {
|
|
395
|
+
console.log(
|
|
396
|
+
`[Slack] Building tree blocks for ${roots.length} root(s): ${roots.map((r) => r.taskId.slice(0, 8)).join(", ")}`,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const treeTexts = roots.map(renderTree);
|
|
400
|
+
const blocks: SlackBlock[] = [sectionBlock(treeTexts.join("\n\n"))];
|
|
401
|
+
|
|
402
|
+
// Add cancel buttons for active roots
|
|
403
|
+
for (const root of roots) {
|
|
404
|
+
if (isTreeActive(root)) {
|
|
405
|
+
blocks.push(cancelActionBlock(root.taskId));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return blocks;
|
|
410
|
+
}
|
package/src/slack/handlers.ts
CHANGED
|
@@ -11,13 +11,13 @@ import {
|
|
|
11
11
|
} from "../be/db";
|
|
12
12
|
import { resolveTemplate } from "../prompts/resolver";
|
|
13
13
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
14
|
-
import {
|
|
14
|
+
import { buildTreeBlocks, type TreeNode } from "./blocks";
|
|
15
15
|
import type { SlackFile } from "./files";
|
|
16
16
|
import { extractTaskFromMessage, routeMessage } from "./router";
|
|
17
17
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
18
18
|
import "./templates";
|
|
19
19
|
import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "./thread-buffer";
|
|
20
|
-
import {
|
|
20
|
+
import { registerTreeMessage } from "./watcher";
|
|
21
21
|
|
|
22
22
|
// User filtering configuration from environment variables
|
|
23
23
|
const allowedEmailDomains = (process.env.SLACK_ALLOWED_EMAIL_DOMAINS || "")
|
|
@@ -591,9 +591,40 @@ export function registerMessageHandler(app: App): void {
|
|
|
591
591
|
}
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
// Send consolidated summary with Block Kit
|
|
594
|
+
// Send consolidated summary as initial tree with Block Kit
|
|
595
595
|
const totalResults = results.assigned.length + results.queued.length + results.failed.length;
|
|
596
596
|
if (totalResults > 0) {
|
|
597
|
+
// Build initial tree nodes from assignment results
|
|
598
|
+
const initialNodes: TreeNode[] = results.assigned.map(({ agentName, taskId }) => ({
|
|
599
|
+
taskId,
|
|
600
|
+
agentName,
|
|
601
|
+
status: "in_progress" as const,
|
|
602
|
+
children: [],
|
|
603
|
+
}));
|
|
604
|
+
|
|
605
|
+
// Add queued tasks
|
|
606
|
+
for (const q of results.queued) {
|
|
607
|
+
initialNodes.push({
|
|
608
|
+
taskId: q.taskId,
|
|
609
|
+
agentName: q.agentName,
|
|
610
|
+
status: "pending" as const,
|
|
611
|
+
children: [],
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const blocks = buildTreeBlocks(initialNodes);
|
|
616
|
+
|
|
617
|
+
// Append failed assignment lines as context below the tree
|
|
618
|
+
if (results.failed.length > 0) {
|
|
619
|
+
const failedLines = results.failed
|
|
620
|
+
.map((f) => `⚠️ Could not assign to: *${f.agentName}* — ${f.reason}`)
|
|
621
|
+
.join("\n");
|
|
622
|
+
blocks.push({
|
|
623
|
+
type: "context",
|
|
624
|
+
elements: [{ type: "mrkdwn", text: failedLines }],
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
597
628
|
// Build plain-text fallback
|
|
598
629
|
const parts: string[] = [];
|
|
599
630
|
if (results.assigned.length > 0) {
|
|
@@ -609,17 +640,25 @@ export function registerMessageHandler(app: App): void {
|
|
|
609
640
|
parts.push(`Could not assign to: ${names}`);
|
|
610
641
|
}
|
|
611
642
|
|
|
643
|
+
console.log(
|
|
644
|
+
`[Slack] Posting initial tree message with ${initialNodes.length} node(s)${results.failed.length > 0 ? ` and ${results.failed.length} failed assignment(s)` : ""}`,
|
|
645
|
+
);
|
|
646
|
+
|
|
612
647
|
const resp = await say({
|
|
613
648
|
text: parts.join(". "),
|
|
614
|
-
blocks
|
|
649
|
+
blocks,
|
|
615
650
|
thread_ts: msg.thread_ts || msg.ts,
|
|
616
651
|
});
|
|
617
652
|
|
|
618
|
-
// Register the
|
|
619
|
-
// (assignment → progress → completion all in one evolving message)
|
|
653
|
+
// Register the tree message so the watcher can update it in-place
|
|
654
|
+
// (assignment → progress → completion all in one evolving tree message)
|
|
620
655
|
if (resp?.ts) {
|
|
621
656
|
for (const { taskId } of results.assigned) {
|
|
622
|
-
|
|
657
|
+
registerTreeMessage(taskId, msg.channel, threadTs, resp.ts);
|
|
658
|
+
}
|
|
659
|
+
// Also register queued tasks so they appear in the tree when they start
|
|
660
|
+
for (const { taskId } of results.queued) {
|
|
661
|
+
registerTreeMessage(taskId, msg.channel, threadTs, resp.ts);
|
|
623
662
|
}
|
|
624
663
|
}
|
|
625
664
|
}
|
package/src/slack/responses.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildCompletedBlocks,
|
|
8
8
|
buildFailedBlocks,
|
|
9
9
|
buildProgressBlocks,
|
|
10
|
+
formatDuration,
|
|
10
11
|
markdownToSlack,
|
|
11
12
|
} from "./blocks";
|
|
12
13
|
|
|
@@ -49,11 +50,24 @@ export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
|
|
|
49
50
|
if (task.status === "completed") {
|
|
50
51
|
const output = task.output || "Task completed.";
|
|
51
52
|
const slackOutput = markdownToSlack(output);
|
|
52
|
-
const
|
|
53
|
+
const duration =
|
|
54
|
+
task.finishedAt && task.createdAt
|
|
55
|
+
? formatDuration(new Date(task.createdAt), new Date(task.finishedAt))
|
|
56
|
+
: undefined;
|
|
57
|
+
console.log(
|
|
58
|
+
`[Slack] sendTaskResponse: task=${task.id} slackReplySent=${!!task.slackReplySent} minimal=${!!task.slackReplySent}`,
|
|
59
|
+
);
|
|
60
|
+
const blocks = buildCompletedBlocks({
|
|
61
|
+
agentName,
|
|
62
|
+
taskId: task.id,
|
|
63
|
+
body: slackOutput,
|
|
64
|
+
duration,
|
|
65
|
+
minimal: !!task.slackReplySent,
|
|
66
|
+
});
|
|
53
67
|
await sendWithPersona(client, {
|
|
54
68
|
channel: task.slackChannelId,
|
|
55
69
|
thread_ts: task.slackThreadTs,
|
|
56
|
-
text: slackOutput,
|
|
70
|
+
text: task.slackReplySent ? `✅ ${agentName} completed` : slackOutput,
|
|
57
71
|
username: getAgentDisplayName(agent),
|
|
58
72
|
icon_emoji: getAgentEmoji(agent),
|
|
59
73
|
blocks,
|
|
@@ -161,8 +175,21 @@ export async function updateToFinal(task: AgentTask, messageTs: string): Promise
|
|
|
161
175
|
if (task.status === "completed") {
|
|
162
176
|
const output = task.output || "Task completed.";
|
|
163
177
|
const slackOutput = markdownToSlack(output);
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
const duration =
|
|
179
|
+
task.finishedAt && task.createdAt
|
|
180
|
+
? formatDuration(new Date(task.createdAt), new Date(task.finishedAt))
|
|
181
|
+
: undefined;
|
|
182
|
+
console.log(
|
|
183
|
+
`[Slack] updateToFinal: task=${task.id} slackReplySent=${!!task.slackReplySent} minimal=${!!task.slackReplySent}`,
|
|
184
|
+
);
|
|
185
|
+
blocks = buildCompletedBlocks({
|
|
186
|
+
agentName,
|
|
187
|
+
taskId: task.id,
|
|
188
|
+
body: slackOutput,
|
|
189
|
+
duration,
|
|
190
|
+
minimal: !!task.slackReplySent,
|
|
191
|
+
});
|
|
192
|
+
text = task.slackReplySent ? `✅ ${agentName} completed` : slackOutput;
|
|
166
193
|
} else if (task.status === "cancelled") {
|
|
167
194
|
blocks = buildCancelledBlocks({ agentName, taskId: task.id });
|
|
168
195
|
text = "Task cancelled";
|
|
@@ -187,6 +214,34 @@ export async function updateToFinal(task: AgentTask, messageTs: string): Promise
|
|
|
187
214
|
}
|
|
188
215
|
}
|
|
189
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Update a tree message directly with pre-built blocks via chat.update.
|
|
219
|
+
* Used by the watcher's tree rendering loop (Phase 5).
|
|
220
|
+
*/
|
|
221
|
+
export async function updateTreeMessage(
|
|
222
|
+
channelId: string,
|
|
223
|
+
messageTs: string,
|
|
224
|
+
blocks: unknown[],
|
|
225
|
+
fallbackText: string,
|
|
226
|
+
): Promise<boolean> {
|
|
227
|
+
const app = getSlackApp();
|
|
228
|
+
if (!app) return false;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
await app.client.chat.update({
|
|
232
|
+
channel: channelId,
|
|
233
|
+
ts: messageTs,
|
|
234
|
+
text: fallbackText,
|
|
235
|
+
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
236
|
+
blocks: blocks as any,
|
|
237
|
+
});
|
|
238
|
+
return true;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error(`[Slack] Failed to update tree message:`, error);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
190
245
|
async function sendWithPersona(
|
|
191
246
|
client: WebClient,
|
|
192
247
|
options: {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../be/db";
|
|
7
7
|
import { getSlackApp } from "./app";
|
|
8
8
|
import { buildBufferFlushBlocks } from "./blocks";
|
|
9
|
+
import { registerTreeMessage } from "./watcher";
|
|
9
10
|
|
|
10
11
|
interface BufferedMessage {
|
|
11
12
|
text: string;
|
|
@@ -197,13 +198,21 @@ async function flushBuffer(key: string, immediate = false): Promise<void> {
|
|
|
197
198
|
: `${buffer.messages.length} follow-up message(s) batched into task`;
|
|
198
199
|
|
|
199
200
|
try {
|
|
200
|
-
await app.client.chat.postMessage({
|
|
201
|
+
const result = await app.client.chat.postMessage({
|
|
201
202
|
channel: buffer.channelId,
|
|
202
203
|
thread_ts: buffer.threadTs,
|
|
203
204
|
text: fallbackText,
|
|
204
205
|
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
205
206
|
blocks: blocks as any,
|
|
206
207
|
});
|
|
208
|
+
|
|
209
|
+
// Register the batching message as the tree message for this task
|
|
210
|
+
if (result.ts && task) {
|
|
211
|
+
registerTreeMessage(task.id, buffer.channelId, buffer.threadTs, result.ts);
|
|
212
|
+
console.log(
|
|
213
|
+
`[Slack] Registered batched task ${task.id.slice(0, 8)} tree message from buffer flush`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
207
216
|
} catch (error) {
|
|
208
217
|
console.error("[Slack] Failed to post buffer flush feedback:", error);
|
|
209
218
|
}
|