@desplega.ai/agent-swarm 1.167.0 → 1.169.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 (184) hide show
  1. package/dist/{acp-adapter-zpvy6321.js → acp-adapter-m47zfx4a.js} +2 -2
  2. package/dist/{actions-fq2zrwtf.js → actions-ws7k3tvq.js} +25 -19
  3. package/dist/{amp-adapter-5gbk7501.js → amp-adapter-nvv8k8j5.js} +1 -1
  4. package/dist/{app-nctetn1e.js → app-80w6qspx.js} +5 -5
  5. package/dist/{assistant-0aq8gjvj.js → assistant-jtq439kw.js} +29 -20
  6. package/dist/{boot-reembed-1ze5853n.js → boot-reembed-3nc0aaed.js} +4 -4
  7. package/dist/{boot-reembed-fmp5ww69.js → boot-reembed-feda8wn8.js} +4 -4
  8. package/dist/{boot-scrub-sweep-cbp8xd4e.js → boot-scrub-sweep-grsa2er0.js} +5 -5
  9. package/dist/{boot-seeds-22tx8a53.js → boot-seeds-gcfgq44f.js} +7 -7
  10. package/dist/{claude-adapter-0jhgeh1d.js → claude-adapter-pjv2q7t8.js} +7 -7
  11. package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-0wa8se82.js} +2 -2
  12. package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-zxa9p9tx.js} +1 -1
  13. package/dist/{claude-sdk-session-wh77rh83.js → claude-sdk-session-c9j6k9kx.js} +8772 -8663
  14. package/dist/{cli-c2tpw9ck.js → cli-3ry5gx4f.js} +1 -1
  15. package/dist/{cli-wydz3feg.js → cli-61tbnjpf.js} +214 -2472
  16. package/dist/{cli-cexctgn5.js → cli-7vz49g7h.js} +101 -34
  17. package/dist/{cli-6qxmjpem.js → cli-8xjst8q2.js} +34 -22
  18. package/dist/{cli-tx2zvn23.js → cli-91r17hb1.js} +3 -3
  19. package/dist/{cli-577bxkf6.js → cli-a8vps458.js} +182 -2208
  20. package/dist/{cli-tc5zec7v.js → cli-b7y8113e.js} +1 -1
  21. package/dist/{cli-69egj654.js → cli-bqrs5ycj.js} +1 -1
  22. package/dist/{cli-cf2vgn9d.js → cli-dv76vq5d.js} +30 -28
  23. package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
  24. package/dist/{cli-c8r99akr.js → cli-e5qk3m2a.js} +209 -31
  25. package/dist/{cli-z1dqhc9y.js → cli-eqfbsxsn.js} +3 -3
  26. package/dist/{cli-kw0jcfje.js → cli-fyeft04q.js} +1 -1
  27. package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
  28. package/dist/{cli-ypgba1a8.js → cli-jkfxpg1b.js} +1 -1
  29. package/dist/{cli-mxvnnv5q.js → cli-jv36y8wz.js} +1 -1
  30. package/dist/{cli-m7vmeepg.js → cli-km8559dm.js} +6 -6
  31. package/dist/{cli-ykt8f7va.js → cli-kp3vfknv.js} +1 -1
  32. package/dist/{cli-ymmc1zxg.js → cli-m23r88re.js} +8 -5
  33. package/dist/{cli-v1pwxreq.js → cli-n4z21pt8.js} +1 -1
  34. package/dist/{cli-mxry91sa.js → cli-qg2zhjfc.js} +73 -4
  35. package/dist/{cli-1yrcfa5s.js → cli-rbr91whd.js} +4 -4
  36. package/dist/{cli-mkdsy1nq.js → cli-rg1c8ccw.js} +93 -7
  37. package/dist/cli-rh44xx46.js +2659 -0
  38. package/dist/cli-s0k2j7q9.js +2084 -0
  39. package/dist/{cli-ty4bzkqr.js → cli-sajr098b.js} +1 -1
  40. package/dist/{cli-6vfz44sr.js → cli-swrz3mj8.js} +1 -1
  41. package/dist/{cli-p8rt8spp.js → cli-vejds37t.js} +2 -2
  42. package/dist/{cli-jbzbr36n.js → cli-w6xa5nf5.js} +1 -1
  43. package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
  44. package/dist/{cli-qzmzwegq.js → cli-ysnkvdx6.js} +102 -9
  45. package/dist/{cli-y0kjpfm9.js → cli-yvxfaem2.js} +2 -2
  46. package/dist/{cli-pkj1w46r.js → cli-z48ggfyq.js} +1 -1
  47. package/dist/{cli-6bpvv8h4.js → cli-zzev32jw.js} +1 -1
  48. package/dist/cli.js +20 -17
  49. package/dist/{codex-adapter-cgesnjkr.js → codex-adapter-0kpg09ca.js} +3 -3
  50. package/dist/{codex-hook-15dgc4cb.js → codex-hook-295jpbfa.js} +2 -2
  51. package/dist/{codex-session-runner-8kd7ddhr.js → codex-session-runner-8n672hxh.js} +3 -3
  52. package/dist/{commands-h6zaaxym.js → commands-mhmg5n0x.js} +4 -4
  53. package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-wn8n5zzk.js} +2 -2
  54. package/dist/{db-qg3myn53.js → db-wa6s1mwv.js} +4 -4
  55. package/dist/{dispatcher-7vnwj9qw.js → dispatcher-nhs2pkr8.js} +16 -14
  56. package/dist/{dsh-adapter-tz6hfp5r.js → dsh-adapter-5423p70m.js} +1 -1
  57. package/dist/{grok-adapter-jdt2dz2s.js → grok-adapter-28pkdt80.js} +3 -3
  58. package/dist/{handlers-2qtkpncr.js → handlers-5rtf9nng.js} +19 -17
  59. package/dist/{hook-51yfcgsv.js → hook-krjfkpzy.js} +4 -4
  60. package/dist/{hook-qnbdgwvd.js → hook-qkm73zk6.js} +5 -5
  61. package/dist/{http-jsmtag08.js → http-a397xcn4.js} +4845 -3484
  62. package/dist/{index-2t9kesn1.js → index-ac0v8v9d.js} +4 -4
  63. package/dist/{index-ekywyv2v.js → index-kqre45fr.js} +16 -14
  64. package/dist/{index-bt8wkv1w.js → index-rtxg15ah.js} +17 -15
  65. package/dist/{index-t4g06w93.js → index-vvsq7v45.js} +26 -23
  66. package/dist/{index-cx1sb9bq.js → index-ytdhtdry.js} +50 -11
  67. package/dist/{keepalive-4aezx4bj.js → keepalive-psfh8ysx.js} +6 -6
  68. package/dist/{lead-r44bkpm5.js → lead-m6rppjn7.js} +29 -27
  69. package/dist/{maintenance-grz5jrzh.js → maintenance-41f837ke.js} +4 -4
  70. package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
  71. package/dist/{oauth-refresh-sweep-wm4ctjfj.js → oauth-refresh-sweep-6hr55hm8.js} +5 -5
  72. package/dist/{onboard-v60zgqf0.js → onboard-ktrtd7cz.js} +2 -2
  73. package/dist/{otel-impl-ghh4dyf8.js → otel-impl-dq4f2k3m.js} +1 -1
  74. package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-e5v9w0hv.js} +3 -2
  75. package/dist/{pricing-refresh-mjrqvnvw.js → pricing-refresh-kmk7j1ra.js} +6 -6
  76. package/dist/{rbac-roles-fd5tskj9.js → rbac-roles-9w1dkkxd.js} +4 -4
  77. package/dist/{rbac-roles-axv4d74x.js → rbac-roles-9ykx9tc5.js} +5 -5
  78. package/dist/{render-v2-k50j33qd.js → render-v2-ycqm53np.js} +5 -5
  79. package/dist/resume-cq2pkp8q.js +70 -0
  80. package/dist/{setup-4syh0pd2.js → setup-0dcmdvbn.js} +2 -2
  81. package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-1x9k4jcs.js} +1 -1
  82. package/dist/{work-objects-jcdbg8rr.js → work-objects-x01x2fh8.js} +19 -17
  83. package/dist/{worker-x8j8c9sx.js → worker-py1ssb1h.js} +29 -27
  84. package/openapi.json +15622 -15042
  85. package/package.json +8 -5
  86. package/src/apps/store.ts +1 -0
  87. package/src/artifact-sdk/feedback-overlay.ts +528 -0
  88. package/src/be/db/workflow-runs.ts +95 -0
  89. package/src/be/db-queries/slack-inbound.ts +489 -0
  90. package/src/be/db.ts +9 -2
  91. package/src/be/extensions/assets.ts +1 -1
  92. package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
  93. package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
  94. package/src/be/script-connections.ts +41 -7
  95. package/src/be/scripts/run-saved.ts +40 -20
  96. package/src/be/swarm-config-guard.ts +4 -0
  97. package/src/extensions/catalog.generated.json +17 -0
  98. package/src/extensions/catalog.ts +1 -1
  99. package/src/extensions/dispatcher.ts +3 -3
  100. package/src/heartbeat/heartbeat.ts +152 -2
  101. package/src/heartbeat/templates.ts +16 -0
  102. package/src/http/all-routes.ts +1 -0
  103. package/src/http/index.ts +2 -0
  104. package/src/http/integrations.ts +3 -1
  105. package/src/http/mcp-bridge.ts +1 -0
  106. package/src/http/pages-public.ts +7 -2
  107. package/src/http/pages.ts +178 -0
  108. package/src/http/script-connections.ts +35 -0
  109. package/src/http/slack-inbound.ts +107 -0
  110. package/src/http/tasks.ts +45 -27
  111. package/src/http/users.ts +1 -1
  112. package/src/http/webhooks.ts +20 -20
  113. package/src/prompts/session-templates.ts +32 -0
  114. package/src/providers/acp-targets.ts +1 -1
  115. package/src/providers/amp-adapter.ts +2 -2
  116. package/src/providers/claude-adapter.ts +6 -0
  117. package/src/providers/claude-background-keepalive.ts +100 -0
  118. package/src/providers/claude-managed-adapter.ts +2 -11
  119. package/src/providers/claude-sdk-session.ts +7 -1
  120. package/src/providers/dsh-adapter.ts +1 -1
  121. package/src/providers/opencode-adapter.ts +1 -2
  122. package/src/providers/pi-mono-adapter.ts +3 -3
  123. package/src/realtime/browser.generated.txt +8 -8
  124. package/src/realtime/browser.ts +3 -3
  125. package/src/scheduler/scheduler.ts +82 -13
  126. package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
  127. package/src/scripts-runtime/executors/quickjs.ts +9 -2
  128. package/src/scripts-runtime/executors/registry.ts +1 -1
  129. package/src/server.ts +1 -0
  130. package/src/slack/actions.ts +6 -2
  131. package/src/slack/app.ts +5 -0
  132. package/src/slack/assistant.ts +15 -5
  133. package/src/slack/egress-scrub.ts +3 -2
  134. package/src/slack/handlers.ts +36 -18
  135. package/src/slack/inbound-dispatch.ts +549 -0
  136. package/src/slack/inbound-files.ts +2 -0
  137. package/src/telemetry-context.ts +1 -0
  138. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  139. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  140. package/src/tests/claude-background-keepalive.test.ts +139 -0
  141. package/src/tests/codex-oauth-device.test.ts +1 -1
  142. package/src/tests/connection-embedded-auth.test.ts +51 -0
  143. package/src/tests/docs-index-footprint.test.js +3 -3
  144. package/src/tests/dsh-adapter.test.ts +1 -1
  145. package/src/tests/extensions-catalog.test.ts +1 -0
  146. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  147. package/src/tests/heartbeat-checklist.test.ts +243 -0
  148. package/src/tests/pages-feedback.test.ts +218 -0
  149. package/src/tests/schedule-target-type.test.ts +136 -0
  150. package/src/tests/script-connections-http.test.ts +45 -0
  151. package/src/tests/script-connections-mcp.test.ts +41 -0
  152. package/src/tests/script-connections.test.ts +2 -0
  153. package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
  154. package/src/tests/script-executor-registry.test.ts +56 -0
  155. package/src/tests/send-task-self-send.test.ts +107 -0
  156. package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
  157. package/src/tests/slack-inbound-receipts.test.ts +684 -0
  158. package/src/tests/slack-session-status.test.ts +4 -2
  159. package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
  160. package/src/tests/user-token-routes.test.ts +1 -1
  161. package/src/tests/workflow-executors.test.ts +3 -2
  162. package/src/tests/workflow-sub-workflow.test.ts +363 -0
  163. package/src/tools/script-connections/tool.ts +35 -3
  164. package/src/tools/send-task.ts +9 -1
  165. package/src/utils/internal-ai/complete-structured.ts +6 -3
  166. package/src/utils/runtime-model-catalog.ts +1 -1
  167. package/src/workflows/engine.ts +17 -1
  168. package/src/workflows/executors/agent-task.ts +2 -2
  169. package/src/workflows/executors/human-in-the-loop.ts +3 -3
  170. package/src/workflows/executors/registry.ts +2 -0
  171. package/src/workflows/executors/script.ts +1 -1
  172. package/src/workflows/executors/sub-workflow.ts +135 -0
  173. package/src/workflows/executors/wait.ts +3 -3
  174. package/src/workflows/recovery.ts +11 -1
  175. package/src/workflows/resume.ts +77 -0
  176. package/src/workflows/wait-filter.ts +5 -7
  177. package/templates/extensions/README.md +1 -0
  178. package/templates/extensions/slack-reply-gate/README.md +98 -0
  179. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  180. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  181. package/templates/skills/heartbeat-runbook/content.md +3 -1
  182. package/templates/skills/pages/content.md +25 -0
  183. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  184. package/templates/skills/swarm-scripts/content.md +10 -0
@@ -85,11 +85,11 @@ class Connection extends Events {
85
85
  };
86
86
  socket.onerror = () => reject(new Error("Realtime connection failed"));
87
87
  socket.onmessage = (event) => {
88
- const message = JSON.parse(event.data) as Frame;
88
+ const payload = JSON.parse(event.data);
89
+ const message = payload as Frame;
89
90
  if (message.seq) socket.send(JSON.stringify({ ack: message.seq }));
90
91
  if (message.type === "hello") this.me = message.me;
91
- if (message.type === "room")
92
- this.rooms.get(message.name ?? "")?.receive(message as unknown as View);
92
+ if (message.type === "room") this.rooms.get(message.name ?? "")?.receive(payload as View);
93
93
  if (message.type === "presence")
94
94
  this.rooms.get(message.name ?? "")?.receivePresence(message.peers);
95
95
  if (message.type === "channel")
@@ -15,6 +15,7 @@ import {
15
15
  getUserById,
16
16
  getWorkflow,
17
17
  getWorkflowRun,
18
+ recordInlineScriptRun,
18
19
  type UpdateScheduledTaskData,
19
20
  updateScheduledTask,
20
21
  } from "@/be/db";
@@ -32,6 +33,7 @@ import { startWorkflowExecution } from "@/workflows/engine";
32
33
  import type { ExecutorRegistry } from "@/workflows/executors/registry";
33
34
  import { handleScheduleTrigger } from "@/workflows/triggers";
34
35
  import { ensure } from "../utils/business-use";
36
+ import { scrubObject, scrubSecrets } from "../utils/secret-scrubber";
35
37
 
36
38
  import {
37
39
  dispatchDeferredTaskWait,
@@ -83,23 +85,90 @@ async function executeScheduleScript(schedule: ScheduledTask): Promise<void> {
83
85
  }
84
86
 
85
87
  const agentId = schedule.createdByAgentId ?? "schedule";
86
- const output = await runScript({
87
- source: script.source,
88
- args: schedule.scriptArgs ?? {},
89
- fsMode: "none",
88
+ const args = schedule.scriptArgs ?? {};
89
+ const record = {
90
90
  agentId,
91
- egressSecrets: await buildScriptCredentialBindings({ agentId }),
92
- apiConnections: getScriptApiConnectionDescriptors({ agentId }),
93
- mcpConnections: getScriptMcpConnectionDescriptors({ agentId }),
94
- timeoutMs: 60_000,
95
- });
91
+ scriptName: schedule.scriptName,
92
+ // A pointer, not a copy: script schedules fire as often as every minute and
93
+ // script_runs has no retention sweep, so copying the source on every fire
94
+ // would grow the table by tens of MB a day. script_versions keeps the text.
95
+ source: `// Scheduled run of '${script.name}' v${script.version} (global, contentHash ${script.contentHash}) by schedule '${schedule.name}'.\n// Full source: script_versions row for this version.\n`,
96
+ args,
97
+ startedAt: new Date().toISOString(),
98
+ };
99
+
100
+ let output: Awaited<ReturnType<typeof runScript>>;
101
+ try {
102
+ output = await runScript({
103
+ source: script.source,
104
+ args,
105
+ fsMode: "none",
106
+ agentId,
107
+ egressSecrets: await buildScriptCredentialBindings({ agentId }),
108
+ apiConnections: getScriptApiConnectionDescriptors({ agentId }),
109
+ mcpConnections: getScriptMcpConnectionDescriptors({ agentId }),
110
+ timeoutMs: 60_000,
111
+ });
112
+ } catch (err) {
113
+ await recordScheduleScriptRun({
114
+ ...record,
115
+ status: "failed",
116
+ error: err instanceof Error ? err.message : String(err),
117
+ });
118
+ throw err;
119
+ }
96
120
 
97
121
  if (output.exitCode !== 0 || output.error) {
98
- throw new Error(
122
+ const message =
99
123
  output.stderr ||
100
- `Script '${schedule.scriptName}' exited with code ${output.exitCode}${
101
- output.error ? ` (${output.error})` : ""
102
- }`,
124
+ `Script '${schedule.scriptName}' exited with code ${output.exitCode}${
125
+ output.error ? ` (${output.error})` : ""
126
+ }`;
127
+ await recordScheduleScriptRun({
128
+ ...record,
129
+ status: "failed",
130
+ output: output.result,
131
+ error: message,
132
+ });
133
+ throw new Error(message);
134
+ }
135
+
136
+ await recordScheduleScriptRun({ ...record, status: "completed", output: output.result });
137
+ }
138
+
139
+ /**
140
+ * Persist a scheduled script fire as a terminal `script_runs` row, so script
141
+ * audits see schedule-driven runs the same way as POST /api/scripts/run.
142
+ * Best effort: a persistence error never changes the schedule's outcome.
143
+ */
144
+ async function recordScheduleScriptRun(run: {
145
+ agentId: string;
146
+ scriptName: string;
147
+ source: string;
148
+ args: unknown;
149
+ startedAt: string;
150
+ status: "completed" | "failed";
151
+ output?: unknown;
152
+ error?: string;
153
+ }): Promise<void> {
154
+ try {
155
+ await recordInlineScriptRun({
156
+ id: crypto.randomUUID(),
157
+ agentId: run.agentId,
158
+ scriptName: run.scriptName,
159
+ source: run.source,
160
+ args: scrubObject(run.args),
161
+ status: run.status,
162
+ output: scrubObject(run.output),
163
+ error: run.error === undefined ? undefined : scrubSecrets(run.error),
164
+ startedAt: run.startedAt,
165
+ finishedAt: new Date().toISOString(),
166
+ });
167
+ } catch (err) {
168
+ console.warn(
169
+ `[scheduler] could not record script run for '${run.scriptName}': ${scrubSecrets(
170
+ err instanceof Error ? err.message : String(err),
171
+ )}`,
103
172
  );
104
173
  }
105
174
  }
@@ -0,0 +1,26 @@
1
+ export const DEFAULT_QUICKJS_POOL_SIZE = 4;
2
+ export const MAX_QUICKJS_POOL_SIZE = 32;
3
+
4
+ /** Shared by config writes and the executor's deployment-env check. */
5
+ export function validateQuickJSPoolSize(value: unknown): string | null {
6
+ const error = `Invalid SCRIPT_QUICKJS_POOL_SIZE (must be an integer between 1 and ${MAX_QUICKJS_POOL_SIZE})`;
7
+ if (typeof value !== "string" && typeof value !== "number") return error;
8
+ const text = String(value).trim();
9
+ const size = Number(text);
10
+ if (
11
+ !/^\d+$/.test(text) ||
12
+ !Number.isSafeInteger(size) ||
13
+ size < 1 ||
14
+ size > MAX_QUICKJS_POOL_SIZE
15
+ ) {
16
+ return error;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ export function resolveQuickJSPoolSize(value: string | undefined): number {
22
+ if (value === undefined) return DEFAULT_QUICKJS_POOL_SIZE;
23
+ const error = validateQuickJSPoolSize(value);
24
+ if (error) throw new Error(error);
25
+ return Number(value);
26
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveQuickJSPoolSize } from "./quickjs-pool-config";
1
2
  import type { QuickJSWorkerMessage, QuickJSWorkerRequest } from "./quickjs-worker";
2
3
  import type { ExecutorInput, ExecutorOutput, ScriptExecutor, ScriptExecutorError } from "./types";
3
4
 
@@ -14,7 +15,7 @@ import type { ExecutorInput, ExecutorOutput, ScriptExecutor, ScriptExecutorError
14
15
 
15
16
  // Each worker keeps one QuickJS WASM module (~25 ms to load) and runs one job
16
17
  // at a time. Jobs beyond the pool size wait in a queue.
17
- const POOL_SIZE = 4;
18
+ // Size is read when the shared pool is first created; changing it needs an API restart.
18
19
  // The worker enforces the wall clock itself. This margin only catches a
19
20
  // worker that stopped answering, for example a host call that never settles.
20
21
  const UNRESPONSIVE_GRACE_MS = 5_000;
@@ -121,7 +122,13 @@ export class QuickJSScriptExecutor implements ScriptExecutor {
121
122
  }
122
123
  if (input.signal?.aborted) return emptyOutput("killed");
123
124
 
124
- sharedPool ??= new QuickJSWorkerPool(POOL_SIZE);
125
+ try {
126
+ sharedPool ??= new QuickJSWorkerPool(
127
+ resolveQuickJSPoolSize(process.env.SCRIPT_QUICKJS_POOL_SIZE),
128
+ );
129
+ } catch (error) {
130
+ return emptyOutput("executor_error", error instanceof Error ? error.message : String(error));
131
+ }
125
132
  const pool = sharedPool;
126
133
  const slot = await pool.acquire();
127
134
  if (input.signal?.aborted) {
@@ -8,7 +8,7 @@ const EXECUTORS: Record<string, () => ScriptExecutor> = {
8
8
  };
9
9
 
10
10
  export function getScriptExecutor(name = process.env.SCRIPT_EXECUTOR ?? "native"): ScriptExecutor {
11
- const factory = EXECUTORS[name];
11
+ const factory = EXECUTORS[name.trim()];
12
12
  if (!factory) {
13
13
  throw new Error(
14
14
  `Unknown SCRIPT_EXECUTOR: ${name}. Available: ${Object.keys(EXECUTORS).join(", ")}`,
package/src/server.ts CHANGED
@@ -323,6 +323,7 @@ function resolveToolAllowlist(value: string | undefined): string[] | undefined {
323
323
 
324
324
  /** Drop every registered tool the allowlist does not name; warn on unknown names. */
325
325
  function applyToolAllowlist(server: McpServer, allowlist: readonly string[]): void {
326
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- the MCP SDK keeps its tool registry private.
326
327
  const tools = (server as unknown as { _registeredTools: Record<string, { remove(): void }> })
327
328
  ._registeredTools;
328
329
  const allowed = new Set(allowlist);
@@ -5,6 +5,7 @@ import { slackContextKey } from "../tasks/context-key";
5
5
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
6
6
  import { buildCancelledBlocks, getTaskLink } from "./blocks";
7
7
  import { resolveSlackUserId } from "./enrich";
8
+ import { ignoreSlackInbound, noteSlackInboundSideEffect } from "./inbound-dispatch";
8
9
  import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
9
10
  import { getAgentDisplayName, getAgentEmoji } from "./responses";
10
11
 
@@ -70,10 +71,12 @@ export function registerActionHandlers(app: App): void {
70
71
  const taskId = view.private_metadata;
71
72
  const followUpText = view.state.values.follow_up_input?.follow_up_text?.value || "";
72
73
 
73
- if (!taskId || !followUpText) return;
74
+ if (!taskId || !followUpText) return ignoreSlackInbound("empty_follow_up");
74
75
 
75
76
  const originalTask = await getTaskById(taskId);
76
- if (!originalTask || !originalTask.slackChannelId) return;
77
+ if (!originalTask || !originalTask.slackChannelId) {
78
+ return ignoreSlackInbound("follow_up_target_missing");
79
+ }
77
80
 
78
81
  const lead = await getLeadAgent();
79
82
  // Resolve via the shared cascade. Sample context = the modal callback ID
@@ -82,6 +85,7 @@ export function registerActionHandlers(app: App): void {
82
85
  sampleEventType: "view_submission",
83
86
  sampleContext: view.callback_id || "follow_up_submit",
84
87
  });
88
+ noteSlackInboundSideEffect("task_created");
85
89
  const followUpTask = await createTaskWithSiblingAwareness(
86
90
  followUpText,
87
91
  {
package/src/slack/app.ts CHANGED
@@ -3,6 +3,7 @@ import { emitBuiltInIntegrationConnectedOnce, ensureSlackRenderV2Activation } fr
3
3
  import { getSlackConfiguration } from "./config";
4
4
  import { type SlackConnectionState, setSlackConnectionState } from "./connection-state";
5
5
  import { installSlackEgressScrub } from "./egress-scrub";
6
+ import { slackInboundOutcomeMiddleware } from "./inbound-dispatch";
6
7
  import { getSlackSocketModeBlockReason, SLACK_DEV_SOCKET_MODE_OPT_IN } from "./socket-mode-guard";
7
8
  import { startTaskWatcher, stopTaskWatcher } from "./watcher";
8
9
 
@@ -103,6 +104,10 @@ export async function initSlackApp(): Promise<App | null> {
103
104
  // Failed validation must remain retryable without requiring stopSlackApp().
104
105
  initialized = true;
105
106
 
107
+ // First global middleware: every handler below runs inside it, so each
108
+ // delivery resolves to an explicit processed/ignored/failed/uncertain outcome.
109
+ app.use(slackInboundOutcomeMiddleware());
110
+
106
111
  // Register handlers
107
112
  const { registerMessageHandler } = await import("./handlers");
108
113
  const { registerCommandHandler } = await import("./commands");
@@ -6,6 +6,12 @@ import { ackSlackMessage, reactionName } from "./ack";
6
6
  import { resolveSlackUserId, rewriteSlackMentions } from "./enrich";
7
7
  import { wasEventSeen } from "./event-dedup";
8
8
  import type { SlackFile } from "./files";
9
+ import {
10
+ ignoreSlackInbound,
11
+ isAdmittedSlackDelivery,
12
+ noteSlackInboundSideEffect,
13
+ reportSlackInboundFailure,
14
+ } from "./inbound-dispatch";
9
15
  import {
10
16
  buildEffectiveText,
11
17
  createSlackTaskWithFiles,
@@ -47,6 +53,7 @@ export function createAssistant(): Assistant {
47
53
  });
48
54
  } catch (error) {
49
55
  console.error("[Slack] Assistant threadStarted error:", error);
56
+ reportSlackInboundFailure("assistant_thread_started_error");
50
57
  }
51
58
  },
52
59
 
@@ -56,11 +63,12 @@ export function createAssistant(): Assistant {
56
63
 
57
64
  userMessage: async ({ message, body, say, setStatus, setTitle, getThreadContext, client }) => {
58
65
  // Slack retries deliveries on 3s timeout / 5xx. Drop duplicates before
59
- // any task-creation work runs (DES-293).
66
+ // any task-creation work runs (DES-293). A delivery admitted from a
67
+ // durable receipt was already deduplicated by its unique key.
60
68
  const eventId = body?.event_id;
61
- if (wasEventSeen(eventId)) {
69
+ if (!isAdmittedSlackDelivery() && wasEventSeen(eventId)) {
62
70
  console.log(`[Slack] dropping Slack retry: event_id=${eventId}`);
63
- return;
71
+ return ignoreSlackInbound("duplicate_event");
64
72
  }
65
73
 
66
74
  // Wrap setStatus/setTitle to swallow all errors gracefully.
@@ -94,7 +102,7 @@ export function createAssistant(): Assistant {
94
102
 
95
103
  try {
96
104
  // Cast to access fields — Bolt's message union type is complex
97
- const msg = message as unknown as Record<string, unknown>;
105
+ const msg: Record<string, unknown> = { ...message };
98
106
  const threadTs = (msg.thread_ts as string) || message.ts;
99
107
  const channelId = message.channel;
100
108
  statusThread = { channelId, threadTs };
@@ -123,7 +131,7 @@ export function createAssistant(): Assistant {
123
131
  console.log(
124
132
  `[Slack] assistant: skipping message in ${channelId}/${threadTs} — mentions another user, not us`,
125
133
  );
126
- return;
134
+ return ignoreSlackInbound("other_user_mention");
127
135
  }
128
136
  }
129
137
 
@@ -142,6 +150,7 @@ export function createAssistant(): Assistant {
142
150
 
143
151
  // Follow-up message → buffer text and file metadata until the flush.
144
152
  if (workingAgent && workingAgent.status !== "offline" && isAdditiveSlack()) {
153
+ noteSlackInboundSideEffect("thread_buffer");
145
154
  bufferThreadMessage(
146
155
  channelId,
147
156
  threadTs,
@@ -282,6 +291,7 @@ export function createAssistant(): Assistant {
282
291
  // setStatus shows typing indicator — watcher will post final result when done
283
292
  } catch (error) {
284
293
  console.error("[Slack] Assistant userMessage error:", error);
294
+ reportSlackInboundFailure("assistant_user_message_error");
285
295
  }
286
296
  },
287
297
  });
@@ -64,7 +64,7 @@ const INSTALLED = Symbol.for("agent-swarm.slack-egress-scrub");
64
64
 
65
65
  type PatchableProto = {
66
66
  apiCall: (method: string, options?: Args) => Promise<unknown>;
67
- filesUploadV2: (options: object) => Promise<unknown>;
67
+ filesUploadV2: (options: Args) => Promise<unknown>;
68
68
  [INSTALLED]?: true;
69
69
  };
70
70
 
@@ -73,6 +73,7 @@ type PatchableProto = {
73
73
  * Call it before the Bolt `App` (and its receiver) are constructed.
74
74
  */
75
75
  export function installSlackEgressScrub(): void {
76
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- we patch private WebClient prototype methods.
76
77
  const proto = webApi.WebClient.prototype as unknown as PatchableProto;
77
78
  if (proto[INSTALLED]) return;
78
79
  const originalApiCall = proto.apiCall;
@@ -80,7 +81,7 @@ export function installSlackEgressScrub(): void {
80
81
  proto.apiCall = function (this: unknown, method: string, options?: Args) {
81
82
  return originalApiCall.call(this, method, scrubSlackWriteArgs(method, options));
82
83
  };
83
- proto.filesUploadV2 = function (this: unknown, options: object) {
84
+ proto.filesUploadV2 = function (this: unknown, options: Args) {
84
85
  return originalUpload.call(this, scrubSlackUploadArgs(options));
85
86
  };
86
87
  proto[INSTALLED] = true;
@@ -21,6 +21,12 @@ import { buildTreeBlocks, type TreeNode } from "./blocks";
21
21
  import { enrichSlackUserEmail, resolveSlackUserId, rewriteSlackMentions } from "./enrich";
22
22
  import { wasEventSeen } from "./event-dedup";
23
23
  import type { SlackFile } from "./files";
24
+ import {
25
+ ignoreSlackInbound,
26
+ isAdmittedSlackDelivery,
27
+ noteSlackInboundSideEffect,
28
+ reportSlackInboundFailure,
29
+ } from "./inbound-dispatch";
24
30
  import {
25
31
  buildEffectiveText,
26
32
  createSlackTaskWithFiles,
@@ -409,17 +415,18 @@ export function registerMessageHandler(app: App): void {
409
415
  // Handle all message events
410
416
  app.event("message", async ({ event, body, client, say }) => {
411
417
  // Slack retries deliveries on 3s timeout / 5xx. Drop the duplicates
412
- // before any task-creation work runs (DES-293).
418
+ // before any task-creation work runs (DES-293). A delivery admitted from a
419
+ // durable receipt was already deduplicated by its unique key.
413
420
  const eventId = body?.event_id;
414
- if (wasEventSeen(eventId)) {
421
+ if (!isAdmittedSlackDelivery() && wasEventSeen(eventId)) {
415
422
  console.log(`[Slack] dropping Slack retry: event_id=${eventId}`);
416
- return;
423
+ return ignoreSlackInbound("duplicate_event");
417
424
  }
418
425
 
419
426
  const msg = event as MessageEvent;
420
427
 
421
428
  // Ignore message_changed events
422
- if (msg.subtype === "message_changed") return;
429
+ if (msg.subtype === "message_changed") return ignoreSlackInbound("message_changed");
423
430
 
424
431
  // Cache bot user ID on first message (avoids calling auth.test on every event)
425
432
  if (!cachedBotUserId) {
@@ -438,24 +445,26 @@ export function registerMessageHandler(app: App): void {
438
445
  // causing agent completion messages to be misidentified as human messages
439
446
  // and triggering duplicate task creation.
440
447
  if (isBotMessage(msg, cachedBotUserId)) {
441
- return;
448
+ return ignoreSlackInbound("bot_message");
442
449
  }
443
450
  const hasText = !!msg.text?.trim();
444
451
  const hasFiles = !!(msg.files && msg.files.length > 0);
445
452
 
446
453
  // Require either text or files, and always require a user
447
- if ((!hasText && !hasFiles) || !msg.user) return;
454
+ if ((!hasText && !hasFiles) || !msg.user) return ignoreSlackInbound("empty_message");
448
455
 
449
- // Deduplicate events (Slack can send same event twice)
456
+ // Deduplicate events (Slack can send same event twice). An admitted
457
+ // delivery skips this cache too: a retry of a receipt released after a
458
+ // pre-side-effect failure must not be dropped as its own duplicate.
450
459
  const messageKey = `${msg.channel}:${msg.ts}`;
451
- if (isMessageProcessed(messageKey)) {
452
- return;
460
+ if (!isAdmittedSlackDelivery() && isMessageProcessed(messageKey)) {
461
+ return ignoreSlackInbound("duplicate_message");
453
462
  }
454
463
 
455
464
  // Check user authorization
456
465
  if (!(await isUserAllowed(client, msg.user))) {
457
466
  console.log(`[Slack] Ignoring message from unauthorized user ${msg.user}`);
458
- return;
467
+ return ignoreSlackInbound("unauthorized_user");
459
468
  }
460
469
 
461
470
  // Resolve canonical user identity via the three-step cascade:
@@ -468,6 +477,7 @@ export function registerMessageHandler(app: App): void {
468
477
  });
469
478
 
470
479
  // Keep channel/user for legacy consumers and channelId/userId for the extension contract.
480
+ noteSlackInboundSideEffect("workflow_event");
471
481
  workflowEventBus.emit("slack.message", {
472
482
  channel: msg.channel,
473
483
  channelId: msg.channel,
@@ -486,7 +496,7 @@ export function registerMessageHandler(app: App): void {
486
496
  console.error(
487
497
  "[Slack] Bot user ID unavailable — skipping message to avoid silent misbehavior",
488
498
  );
489
- return;
499
+ return reportSlackInboundFailure("bot_user_unavailable");
490
500
  }
491
501
  const botUserId = cachedBotUserId;
492
502
 
@@ -523,6 +533,7 @@ export function registerMessageHandler(app: App): void {
523
533
  `[Slack] !now command detected in thread ${threadKey}${nowMessage ? ` with message: "${nowMessage}"` : ""}`,
524
534
  );
525
535
 
536
+ noteSlackInboundSideEffect("thread_flush");
526
537
  if (nowMessage || msg.files?.length) {
527
538
  bufferThreadMessage(
528
539
  msg.channel,
@@ -553,7 +564,7 @@ export function registerMessageHandler(app: App): void {
553
564
  console.log(
554
565
  `[Slack] Skipping ADDITIVE buffer in ${msg.channel}/${msg.thread_ts}: message mentions another user`,
555
566
  );
556
- return;
567
+ return ignoreSlackInbound("other_user_mention");
557
568
  }
558
569
  // Treat the thread as having swarm activity if either:
559
570
  // - a Slack task is already linked to it (someone started it via @mention), or
@@ -565,6 +576,7 @@ export function registerMessageHandler(app: App): void {
565
576
 
566
577
  if (hasSwarmActivity) {
567
578
  const threadKey = `${msg.channel}:${msg.thread_ts}`;
579
+ noteSlackInboundSideEffect("thread_buffer");
568
580
  bufferThreadMessage(
569
581
  msg.channel,
570
582
  msg.thread_ts,
@@ -614,7 +626,7 @@ export function registerMessageHandler(app: App): void {
614
626
  );
615
627
  if (routeResult.action === "block") {
616
628
  console.info("[Slack] Extension blocked message routing:", scrubSecrets(routeResult.reason));
617
- return;
629
+ return ignoreSlackInbound("extension_blocked");
618
630
  }
619
631
 
620
632
  let matches: AgentMatch[] | undefined;
@@ -658,7 +670,7 @@ export function registerMessageHandler(app: App): void {
658
670
  );
659
671
 
660
672
  if (matches.length === 0) {
661
- if (!botMentioned && !isImplicitMention) return;
673
+ if (!botMentioned && !isImplicitMention) return ignoreSlackInbound("not_addressed");
662
674
 
663
675
  // Bot was mentioned (or message is in assistant thread) but no online agents matched — queue the request
664
676
  if (!checkRateLimit(msg.user)) {
@@ -668,7 +680,7 @@ export function registerMessageHandler(app: App): void {
668
680
  thread_ts: msg.thread_ts || msg.ts,
669
681
  });
670
682
  }
671
- return;
683
+ return ignoreSlackInbound("rate_limited");
672
684
  }
673
685
 
674
686
  await using inbound = await fetchSlackFiles(client, msg.files);
@@ -683,7 +695,7 @@ export function registerMessageHandler(app: App): void {
683
695
  thread_ts: msg.thread_ts || msg.ts,
684
696
  });
685
697
  }
686
- return;
698
+ return ignoreSlackInbound("empty_task");
687
699
  }
688
700
 
689
701
  const threadTs = msg.thread_ts || msg.ts;
@@ -743,7 +755,7 @@ export function registerMessageHandler(app: App): void {
743
755
  thread_ts: msg.thread_ts || msg.ts,
744
756
  });
745
757
  }
746
- return;
758
+ return ignoreSlackInbound("rate_limited");
747
759
  }
748
760
 
749
761
  // Extract task description (the text plus a `[File: …]` line per attachment)
@@ -759,7 +771,7 @@ export function registerMessageHandler(app: App): void {
759
771
  thread_ts: msg.thread_ts || msg.ts,
760
772
  });
761
773
  }
762
- return;
774
+ return ignoreSlackInbound("empty_task");
763
775
  }
764
776
 
765
777
  // Create tasks for each matched agent
@@ -803,6 +815,7 @@ export function registerMessageHandler(app: App): void {
803
815
  if (agent.isLead) {
804
816
  // Steering only carries text into the running session; a message with
805
817
  // files becomes a follow-up task so the files land as attachments.
818
+ if (msg.thread_ts && inbound.files.length === 0) noteSlackInboundSideEffect("steering");
806
819
  const steering =
807
820
  msg.thread_ts && inbound.files.length === 0
808
821
  ? await requestSlackThreadSteering({
@@ -882,6 +895,11 @@ export function registerMessageHandler(app: App): void {
882
895
  results.assigned.push({ agentName: agent.name, taskId: task.id });
883
896
  }
884
897
  } catch (error) {
898
+ // A policy block is an answered outcome; anything else is a failure the
899
+ // user sees as "could not assign" and the dispatcher must not call success.
900
+ if (!(error instanceof TaskCreationBlockedError)) {
901
+ reportSlackInboundFailure("task_creation_error");
902
+ }
885
903
  results.failed.push({
886
904
  agentName: agent.name,
887
905
  reason: error instanceof TaskCreationBlockedError ? error.reason : "error",