@desplega.ai/agent-swarm 1.91.0 → 1.92.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.
Files changed (58) hide show
  1. package/README.md +2 -1
  2. package/openapi.json +585 -5
  3. package/package.json +1 -1
  4. package/src/be/db.ts +337 -1
  5. package/src/be/migrations/083_script_workflows.sql +51 -0
  6. package/src/be/modelsdev-cache.json +42352 -38595
  7. package/src/be/scripts/typecheck.ts +49 -0
  8. package/src/be/seed-scripts/catalog/compound-insights.ts +216 -6
  9. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
  10. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  11. package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
  12. package/src/be/seed-scripts/index.ts +20 -2
  13. package/src/be/seed-skills/index.ts +7 -0
  14. package/src/be/swarm-config-guard.ts +17 -0
  15. package/src/commands/runner.ts +43 -2
  16. package/src/http/db-query.ts +20 -5
  17. package/src/http/index.ts +10 -0
  18. package/src/http/script-runs.ts +555 -0
  19. package/src/prompts/session-templates.ts +24 -4
  20. package/src/providers/claude-adapter.ts +60 -13
  21. package/src/script-workflows/executor.ts +110 -0
  22. package/src/script-workflows/harness.ts +73 -0
  23. package/src/script-workflows/label-lint.ts +51 -0
  24. package/src/script-workflows/limits.ts +22 -0
  25. package/src/script-workflows/supervisor.ts +139 -0
  26. package/src/script-workflows/workflow-ctx.ts +205 -0
  27. package/src/scripts-runtime/sdk-allowlist.ts +3 -0
  28. package/src/scripts-runtime/types/stdlib.d.ts +60 -0
  29. package/src/scripts-runtime/types/swarm-sdk.d.ts +60 -0
  30. package/src/server.ts +2 -0
  31. package/src/slack/handlers.ts +11 -4
  32. package/src/slack/message-text.ts +98 -0
  33. package/src/slack/thread-buffer.ts +5 -3
  34. package/src/tests/claude-adapter-binary.test.ts +147 -4
  35. package/src/tests/db-query.test.ts +28 -0
  36. package/src/tests/error-tracker.test.ts +121 -0
  37. package/src/tests/harness-provider-resolution.test.ts +33 -0
  38. package/src/tests/mcp-tools.test.ts +6 -0
  39. package/src/tests/prompt-template-session.test.ts +34 -5
  40. package/src/tests/script-runs-http.test.ts +278 -0
  41. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  42. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  43. package/src/tests/scripts-mcp-e2e.test.ts +49 -2
  44. package/src/tests/seed-scripts.test.ts +347 -2
  45. package/src/tests/slack-message-text.test.ts +250 -0
  46. package/src/tests/system-default-skills.test.ts +40 -0
  47. package/src/tools/db-query.ts +16 -6
  48. package/src/tools/script-runs.ts +123 -0
  49. package/src/tools/slack-read.ts +12 -3
  50. package/src/tools/tool-config.ts +4 -1
  51. package/src/types.ts +52 -0
  52. package/src/utils/error-tracker.ts +40 -1
  53. package/src/utils/internal-ai/complete-structured.ts +10 -4
  54. package/src/workflows/executors/raw-llm.ts +76 -59
  55. package/templates/skills/pages/content.md +205 -55
  56. package/templates/skills/script-workflows/config.json +14 -0
  57. package/templates/skills/script-workflows/content.md +68 -0
  58. package/templates/skills/swarm-scripts/content.md +2 -3
@@ -0,0 +1,205 @@
1
+ import { stdlib } from "../scripts-runtime/stdlib";
2
+
3
+ type StepStatusResponse =
4
+ | { stepKey: string; stepType: string; result: unknown }
5
+ | { error: string };
6
+
7
+ type StepWriteResponse = { ok: true } | { error: string };
8
+
9
+ type RawLlmConfig = {
10
+ prompt: string;
11
+ model?: string;
12
+ schema?: Record<string, unknown>;
13
+ };
14
+
15
+ type AgentTaskConfig = {
16
+ template?: string;
17
+ task?: string;
18
+ agentId?: string;
19
+ tags?: string[];
20
+ priority?: number;
21
+ offerMode?: boolean;
22
+ dir?: string;
23
+ vcsRepo?: string;
24
+ model?: string;
25
+ parentTaskId?: string;
26
+ requestedByUserId?: string;
27
+ outputSchema?: Record<string, unknown>;
28
+ };
29
+
30
+ type SwarmScriptConfig = {
31
+ name?: string;
32
+ scriptName?: string;
33
+ source?: string;
34
+ args?: unknown;
35
+ scope?: "agent" | "global";
36
+ fsMode?: "none" | "workspace-rw";
37
+ intent?: string;
38
+ idempotencyKey?: string;
39
+ };
40
+
41
+ type WorkflowRunInfo = {
42
+ id: string;
43
+ agentId: string;
44
+ args: unknown;
45
+ };
46
+
47
+ export type WorkflowCtx = {
48
+ run: WorkflowRunInfo;
49
+ step: {
50
+ rawLlm: (label: string, config: RawLlmConfig) => Promise<unknown>;
51
+ agentTask: (label: string, config: AgentTaskConfig) => Promise<unknown>;
52
+ swarmScript: (label: string, config: SwarmScriptConfig) => Promise<unknown>;
53
+ humanInTheLoop: () => Promise<never>;
54
+ };
55
+ swarm: Record<string, (args?: unknown) => Promise<unknown>>;
56
+ stdlib: typeof stdlib;
57
+ logger: Console;
58
+ };
59
+
60
+ function encodeStepKey(label: string): string {
61
+ return encodeURIComponent(label);
62
+ }
63
+
64
+ function headers(apiKey: string, agentId: string): Record<string, string> {
65
+ return {
66
+ Authorization: `Bearer ${apiKey}`,
67
+ "X-Agent-ID": agentId,
68
+ "Content-Type": "application/json",
69
+ };
70
+ }
71
+
72
+ async function readJson(res: Response): Promise<unknown> {
73
+ const text = await res.text();
74
+ return text ? JSON.parse(text) : {};
75
+ }
76
+
77
+ function apiError(prefix: string, status: number, body: unknown): Error {
78
+ const message =
79
+ body && typeof body === "object" && "error" in body
80
+ ? String((body as { error: unknown }).error)
81
+ : JSON.stringify(body);
82
+ return new Error(`${prefix} failed with ${status}: ${message}`);
83
+ }
84
+
85
+ export function buildWorkflowCtx(input: {
86
+ runId: string;
87
+ agentId: string;
88
+ apiKey: string;
89
+ baseUrl: string;
90
+ args: unknown;
91
+ }): WorkflowCtx {
92
+ const baseUrl = input.baseUrl.replace(/\/$/, "");
93
+ const authHeaders = headers(input.apiKey, input.agentId);
94
+
95
+ async function fetchJson(path: string, init: RequestInit = {}): Promise<unknown> {
96
+ const res = await fetch(`${baseUrl}${path}`, {
97
+ ...init,
98
+ headers: { ...authHeaders, ...((init.headers as Record<string, string>) ?? {}) },
99
+ });
100
+ const body = await readJson(res);
101
+ if (!res.ok) throw apiError(path, res.status, body);
102
+ return body;
103
+ }
104
+
105
+ async function completedStep(
106
+ label: string,
107
+ ): Promise<{ found: true; result: unknown } | { found: false }> {
108
+ const res = await fetch(
109
+ `${baseUrl}/api/internal/script-runs/${input.runId}/steps/${encodeStepKey(label)}`,
110
+ {
111
+ headers: authHeaders,
112
+ },
113
+ );
114
+ if (res.status === 404) return { found: false };
115
+ const body = (await readJson(res)) as StepStatusResponse;
116
+ if (!res.ok) throw apiError(`step ${label}`, res.status, body);
117
+ return { found: true, result: "result" in body ? body.result : undefined };
118
+ }
119
+
120
+ async function writeStep(
121
+ label: string,
122
+ stepType: string,
123
+ config: unknown,
124
+ status: "completed" | "failed",
125
+ result?: unknown,
126
+ error?: string,
127
+ ): Promise<void> {
128
+ const body = (await fetchJson(`/api/internal/script-runs/${input.runId}/steps`, {
129
+ method: "POST",
130
+ body: JSON.stringify({ stepKey: label, stepType, config, status, result, error }),
131
+ })) as StepWriteResponse;
132
+ if (!("ok" in body)) throw new Error(`Failed to write journal step ${label}`);
133
+ }
134
+
135
+ async function durableStep(
136
+ label: string,
137
+ stepType: string,
138
+ config: unknown,
139
+ execute: () => Promise<unknown>,
140
+ ): Promise<unknown> {
141
+ const replayed = await completedStep(label);
142
+ if (replayed.found) return replayed.result;
143
+ try {
144
+ const result = await execute();
145
+ await writeStep(label, stepType, config, "completed", result);
146
+ return result;
147
+ } catch (err) {
148
+ const error = err instanceof Error ? err.message : String(err);
149
+ await writeStep(label, stepType, config, "failed", undefined, error);
150
+ throw err;
151
+ }
152
+ }
153
+
154
+ const swarm = new Proxy({} as Record<string, (args?: unknown) => Promise<unknown>>, {
155
+ get(_target, prop) {
156
+ if (typeof prop !== "string") return undefined;
157
+ return (args?: unknown) =>
158
+ fetchJson("/api/mcp-bridge", {
159
+ method: "POST",
160
+ body: JSON.stringify({ tool: prop.replaceAll("_", "-"), args: args ?? {} }),
161
+ });
162
+ },
163
+ });
164
+
165
+ return {
166
+ run: { id: input.runId, agentId: input.agentId, args: input.args },
167
+ step: {
168
+ rawLlm: (label, config) =>
169
+ durableStep(label, "raw-llm", config, async () =>
170
+ fetchJson("/api/internal/raw-llm", {
171
+ method: "POST",
172
+ body: JSON.stringify(config),
173
+ }),
174
+ ),
175
+ agentTask: (label, config) =>
176
+ durableStep(label, "agent-task", config, async () =>
177
+ fetchJson(`/api/internal/script-runs/${input.runId}/agent-task`, {
178
+ method: "POST",
179
+ body: JSON.stringify({ stepKey: label, ...config }),
180
+ }),
181
+ ),
182
+ swarmScript: (label, config) =>
183
+ durableStep(label, "swarm-script", config, async () =>
184
+ fetchJson("/api/scripts/run", {
185
+ method: "POST",
186
+ body: JSON.stringify({
187
+ name: config.name ?? config.scriptName,
188
+ source: config.source,
189
+ args: config.args,
190
+ scope: config.scope,
191
+ fsMode: config.fsMode ?? "none",
192
+ intent: config.intent ?? `script-run:${input.runId}:${label}`,
193
+ idempotencyKey: config.idempotencyKey,
194
+ }),
195
+ }),
196
+ ),
197
+ humanInTheLoop: async () => {
198
+ throw new Error("ctx.step.humanInTheLoop is stubbed in Script Workflows v1");
199
+ },
200
+ },
201
+ swarm,
202
+ stdlib,
203
+ logger: console,
204
+ };
205
+ }
@@ -39,6 +39,9 @@ export const SDK_TOOL_NAME_MAP = {
39
39
  script_upsert: "script-upsert",
40
40
  script_delete: "script-delete", // destructive
41
41
  script_queryTypes: "script-query-types",
42
+ script_launchRun: "launch-script-run",
43
+ script_getRun: "get-script-run",
44
+ script_listRuns: "list-script-runs",
42
45
 
43
46
  // ── swarm / agent ──
44
47
  swarm_get: "get-swarm",
@@ -262,6 +262,20 @@ declare module "swarm-sdk" {
262
262
  }): Promise<unknown>;
263
263
  script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
264
264
  script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
265
+ script_launchRun(args: {
266
+ source: string;
267
+ args?: unknown;
268
+ idempotencyKey?: string;
269
+ scriptName?: string;
270
+ requestedByUserId?: string;
271
+ }): Promise<unknown>;
272
+ script_getRun(args: { id: string }): Promise<unknown>;
273
+ script_listRuns(args?: {
274
+ status?: "running" | "paused" | "completed" | "failed" | "cancelled" | "aborted_limit";
275
+ agentId?: string;
276
+ limit?: number;
277
+ offset?: number;
278
+ }): Promise<unknown>;
265
279
 
266
280
  // --- write: repos ---
267
281
  repo_update(args: Record<string, unknown>): Promise<unknown>;
@@ -320,7 +334,53 @@ declare module "swarm-sdk" {
320
334
 
321
335
  export interface ScriptLogger extends Console {}
322
336
 
337
+ export interface ScriptRunContext {
338
+ id: string;
339
+ agentId: string;
340
+ args: unknown;
341
+ }
342
+
343
+ export interface ScriptWorkflowSteps {
344
+ rawLlm(
345
+ label: string,
346
+ config: { prompt: string; model?: string; schema?: Record<string, unknown> },
347
+ ): Promise<unknown>;
348
+ agentTask(
349
+ label: string,
350
+ config: {
351
+ template?: string;
352
+ task?: string;
353
+ agentId?: string;
354
+ tags?: string[];
355
+ priority?: number;
356
+ offerMode?: boolean;
357
+ dir?: string;
358
+ vcsRepo?: string;
359
+ model?: string;
360
+ parentTaskId?: string;
361
+ requestedByUserId?: string;
362
+ outputSchema?: Record<string, unknown>;
363
+ },
364
+ ): Promise<unknown>;
365
+ swarmScript(
366
+ label: string,
367
+ config: {
368
+ name?: string;
369
+ scriptName?: string;
370
+ source?: string;
371
+ args?: unknown;
372
+ scope?: ScriptScope;
373
+ fsMode?: ScriptFsMode;
374
+ intent?: string;
375
+ idempotencyKey?: string;
376
+ },
377
+ ): Promise<unknown>;
378
+ humanInTheLoop(): Promise<never>;
379
+ }
380
+
323
381
  export interface ScriptContext {
382
+ run?: ScriptRunContext;
383
+ step?: ScriptWorkflowSteps;
324
384
  swarm: SwarmSdk & { config: SwarmConfig };
325
385
  stdlib: ScriptStdlib;
326
386
  logger: ScriptLogger;
@@ -244,6 +244,20 @@ declare module "swarm-sdk" {
244
244
  }): Promise<unknown>;
245
245
  script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
246
246
  script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
247
+ script_launchRun(args: {
248
+ source: string;
249
+ args?: unknown;
250
+ idempotencyKey?: string;
251
+ scriptName?: string;
252
+ requestedByUserId?: string;
253
+ }): Promise<unknown>;
254
+ script_getRun(args: { id: string }): Promise<unknown>;
255
+ script_listRuns(args?: {
256
+ status?: "running" | "paused" | "completed" | "failed" | "cancelled" | "aborted_limit";
257
+ agentId?: string;
258
+ limit?: number;
259
+ offset?: number;
260
+ }): Promise<unknown>;
247
261
 
248
262
  // --- write: repos ---
249
263
  repo_update(args: Record<string, unknown>): Promise<unknown>;
@@ -302,7 +316,53 @@ declare module "swarm-sdk" {
302
316
 
303
317
  export interface ScriptLogger extends Console {}
304
318
 
319
+ export interface ScriptRunContext {
320
+ id: string;
321
+ agentId: string;
322
+ args: unknown;
323
+ }
324
+
325
+ export interface ScriptWorkflowSteps {
326
+ rawLlm(
327
+ label: string,
328
+ config: { prompt: string; model?: string; schema?: Record<string, unknown> },
329
+ ): Promise<unknown>;
330
+ agentTask(
331
+ label: string,
332
+ config: {
333
+ template?: string;
334
+ task?: string;
335
+ agentId?: string;
336
+ tags?: string[];
337
+ priority?: number;
338
+ offerMode?: boolean;
339
+ dir?: string;
340
+ vcsRepo?: string;
341
+ model?: string;
342
+ parentTaskId?: string;
343
+ requestedByUserId?: string;
344
+ outputSchema?: Record<string, unknown>;
345
+ },
346
+ ): Promise<unknown>;
347
+ swarmScript(
348
+ label: string,
349
+ config: {
350
+ name?: string;
351
+ scriptName?: string;
352
+ source?: string;
353
+ args?: unknown;
354
+ scope?: ScriptScope;
355
+ fsMode?: ScriptFsMode;
356
+ intent?: string;
357
+ idempotencyKey?: string;
358
+ },
359
+ ): Promise<unknown>;
360
+ humanInTheLoop(): Promise<never>;
361
+ }
362
+
305
363
  export interface ScriptContext {
364
+ run?: ScriptRunContext;
365
+ step?: ScriptWorkflowSteps;
306
366
  swarm: SwarmSdk & { config: SwarmConfig };
307
367
  stdlib: ScriptStdlib;
308
368
  logger: ScriptLogger;
package/src/server.ts CHANGED
@@ -78,6 +78,7 @@ import {
78
78
  import { registerScriptDeleteTool } from "./tools/script-delete";
79
79
  import { registerScriptQueryTypesTool } from "./tools/script-query-types";
80
80
  import { registerScriptRunTool } from "./tools/script-run";
81
+ import { registerScriptRunsTools } from "./tools/script-runs";
81
82
  import { registerScriptSearchTool } from "./tools/script-search";
82
83
  import { registerScriptUpsertTool } from "./tools/script-upsert";
83
84
  import { registerSendTaskTool } from "./tools/send-task";
@@ -227,6 +228,7 @@ export function createServer() {
227
228
  registerScriptUpsertTool(server);
228
229
  registerScriptDeleteTool(server);
229
230
  registerScriptQueryTypesTool(server);
231
+ registerScriptRunsTools(server);
230
232
 
231
233
  // External command routes - mirrors the `agent-swarm x ...` CLI surface.
232
234
  registerSwarmXTool(server);
@@ -18,6 +18,7 @@ import type { SlackFile } from "./files";
18
18
  import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
19
19
  // Side-effect import: registers all Slack event templates in the in-memory registry
20
20
  import "./templates";
21
+ import { extractSlackMessageText } from "./message-text";
21
22
  import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "./thread-buffer";
22
23
  import { registerTreeMessage } from "./watcher";
23
24
 
@@ -173,6 +174,8 @@ interface ThreadMessage {
173
174
  subtype?: string;
174
175
  text?: string;
175
176
  ts: string;
177
+ attachments?: Array<{ fallback?: string; text?: string; title?: string; pretext?: string }>;
178
+ blocks?: unknown[];
176
179
  }
177
180
 
178
181
  // Cache for bot's own user ID (avoids redundant auth.test calls)
@@ -286,8 +289,11 @@ async function getThreadContext(
286
289
  });
287
290
 
288
291
  const messages = (result.messages || []) as ThreadMessage[];
289
- // Filter out the current message only (keep bot messages for context)
290
- const previousMessages = messages.filter((m) => m.ts !== currentTs && m.text);
292
+ // Filter out the current message; include any message with extractable text
293
+ const previousMessages = messages.filter((m) => {
294
+ if (m.ts === currentTs) return false;
295
+ return extractSlackMessageText(m) !== "";
296
+ });
291
297
 
292
298
  if (previousMessages.length === 0) return "";
293
299
 
@@ -298,13 +304,14 @@ async function getThreadContext(
298
304
  const isBotMessage =
299
305
  m.user === botUserId || m.bot_id !== undefined || m.subtype === "bot_message";
300
306
 
307
+ const text = extractSlackMessageText(m);
301
308
  if (isBotMessage) {
302
309
  // Bot/agent message - truncate if too long
303
- const truncatedText = m.text && m.text.length > 500 ? `${m.text.slice(0, 500)}...` : m.text;
310
+ const truncatedText = text.length > 500 ? `${text.slice(0, 500)}...` : text;
304
311
  formattedMessages.push(`[Agent]: ${truncatedText}`);
305
312
  } else {
306
313
  const userName = m.user ? await getUserDisplayName(client, m.user) : "Unknown";
307
- formattedMessages.push(`${userName}: ${m.text}`);
314
+ formattedMessages.push(`${userName}: ${text}`);
308
315
  }
309
316
  }
310
317
 
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shared utility for extracting displayable text from a Slack message.
3
+ *
4
+ * Slack integration/alert apps (Datadog, PagerDuty, GitHub) often post with
5
+ * an empty top-level `text` field and put all content in legacy `attachments`
6
+ * or Block Kit `blocks`. This helper falls back through those layers so callers
7
+ * never silently drop those messages.
8
+ */
9
+
10
+ interface SlackAttachment {
11
+ fallback?: string;
12
+ text?: string;
13
+ title?: string;
14
+ pretext?: string;
15
+ }
16
+
17
+ // Internal shape used only inside the helper; callers pass `blocks?: unknown[]`
18
+ interface SlackBlockInternal {
19
+ type?: string;
20
+ text?: { type?: string; text?: string };
21
+ /** section blocks may use fields[] instead of (or alongside) a top-level text object */
22
+ fields?: Array<{ type?: string; text?: string }>;
23
+ elements?: unknown[];
24
+ }
25
+
26
+ export interface SlackMessageLike {
27
+ text?: string;
28
+ attachments?: SlackAttachment[];
29
+ /** Typed as unknown[] so any Slack SDK block variant is accepted without casting. */
30
+ blocks?: unknown[];
31
+ }
32
+
33
+ /**
34
+ * Recursively collect plain text from a Slack rich_text node tree.
35
+ *
36
+ * Handles text leaf nodes plus all container types that carry child elements:
37
+ * rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted.
38
+ */
39
+ function collectRichTextParts(node: unknown, parts: string[]): void {
40
+ if (node == null || typeof node !== "object") return;
41
+ const n = node as { type?: string; text?: string; elements?: unknown[] };
42
+ if (n.type === "text" && n.text) {
43
+ parts.push(n.text);
44
+ }
45
+ if (Array.isArray(n.elements)) {
46
+ for (const child of n.elements) {
47
+ collectRichTextParts(child, parts);
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Return the best displayable text for a Slack message.
54
+ *
55
+ * Priority:
56
+ * 1. `msg.text` (non-empty)
57
+ * 2. `msg.attachments[]` — joins `fallback || text || title || pretext` for each
58
+ * 3. `msg.blocks[]` — extracts text from section (text + fields) and rich_text blocks
59
+ * 4. `""` if nothing found
60
+ */
61
+ export function extractSlackMessageText(msg: SlackMessageLike): string {
62
+ if (msg.text?.trim()) return msg.text;
63
+
64
+ // Legacy attachments (Datadog, PagerDuty, GitHub alert apps)
65
+ if (Array.isArray(msg.attachments) && msg.attachments.length > 0) {
66
+ const parts = msg.attachments
67
+ .filter((a): a is SlackAttachment => a != null && typeof a === "object")
68
+ .map((a) => a.fallback || a.text || a.title || a.pretext || "")
69
+ .filter(Boolean);
70
+ if (parts.length > 0) return parts.join("\n");
71
+ }
72
+
73
+ // Block Kit blocks
74
+ if (Array.isArray(msg.blocks) && msg.blocks.length > 0) {
75
+ const parts: string[] = [];
76
+ for (const rawBlock of msg.blocks) {
77
+ if (rawBlock == null || typeof rawBlock !== "object") continue;
78
+ const block = rawBlock as SlackBlockInternal;
79
+ if (block.type === "section") {
80
+ if (block.text?.text) parts.push(block.text.text);
81
+ if (Array.isArray(block.fields)) {
82
+ for (const field of block.fields) {
83
+ if (field != null && typeof field === "object" && field.text) {
84
+ parts.push(field.text);
85
+ }
86
+ }
87
+ }
88
+ } else if (block.type === "rich_text" && Array.isArray(block.elements)) {
89
+ for (const el of block.elements) {
90
+ collectRichTextParts(el, parts);
91
+ }
92
+ }
93
+ }
94
+ if (parts.length > 0) return parts.join("\n");
95
+ }
96
+
97
+ return "";
98
+ }
@@ -4,6 +4,7 @@ import { slackContextKey } from "../tasks/context-key";
4
4
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
5
5
  import { getSlackApp } from "./app";
6
6
  import { buildBufferFlushBlocks } from "./blocks";
7
+ import { extractSlackMessageText } from "./message-text";
7
8
  import { registerTreeMessage } from "./watcher";
8
9
 
9
10
  interface BufferedMessage {
@@ -93,15 +94,16 @@ async function getThreadContextForBuffer(channelId: string, threadTs: string): P
93
94
  if (messages.length === 0) return "";
94
95
 
95
96
  const formatted = messages
96
- .filter((m) => m.text)
97
+ .filter((m) => extractSlackMessageText(m) !== "")
97
98
  .map((m) => {
98
99
  const msg = m as Record<string, unknown>;
99
100
  const isBotMessage = msg.bot_id !== undefined || msg.subtype === "bot_message";
101
+ const text = extractSlackMessageText(m);
100
102
  if (isBotMessage) {
101
- const truncated = m.text && m.text.length > 500 ? `${m.text.slice(0, 500)}...` : m.text;
103
+ const truncated = text.length > 500 ? `${text.slice(0, 500)}...` : text;
102
104
  return `[Agent]: ${truncated}`;
103
105
  }
104
- return `<@${m.user}>: ${m.text}`;
106
+ return `<@${m.user}>: ${text}`;
105
107
  })
106
108
  .join("\n");
107
109