@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.
- package/dist/{acp-adapter-zpvy6321.js → acp-adapter-m47zfx4a.js} +2 -2
- package/dist/{actions-fq2zrwtf.js → actions-ws7k3tvq.js} +25 -19
- package/dist/{amp-adapter-5gbk7501.js → amp-adapter-nvv8k8j5.js} +1 -1
- package/dist/{app-nctetn1e.js → app-80w6qspx.js} +5 -5
- package/dist/{assistant-0aq8gjvj.js → assistant-jtq439kw.js} +29 -20
- package/dist/{boot-reembed-1ze5853n.js → boot-reembed-3nc0aaed.js} +4 -4
- package/dist/{boot-reembed-fmp5ww69.js → boot-reembed-feda8wn8.js} +4 -4
- package/dist/{boot-scrub-sweep-cbp8xd4e.js → boot-scrub-sweep-grsa2er0.js} +5 -5
- package/dist/{boot-seeds-22tx8a53.js → boot-seeds-gcfgq44f.js} +7 -7
- package/dist/{claude-adapter-0jhgeh1d.js → claude-adapter-pjv2q7t8.js} +7 -7
- package/dist/{claude-managed-adapter-tzwxy803.js → claude-managed-adapter-0wa8se82.js} +2 -2
- package/dist/{claude-managed-setup-nrhfx5b0.js → claude-managed-setup-zxa9p9tx.js} +1 -1
- package/dist/{claude-sdk-session-wh77rh83.js → claude-sdk-session-c9j6k9kx.js} +8772 -8663
- package/dist/{cli-c2tpw9ck.js → cli-3ry5gx4f.js} +1 -1
- package/dist/{cli-wydz3feg.js → cli-61tbnjpf.js} +214 -2472
- package/dist/{cli-cexctgn5.js → cli-7vz49g7h.js} +101 -34
- package/dist/{cli-6qxmjpem.js → cli-8xjst8q2.js} +34 -22
- package/dist/{cli-tx2zvn23.js → cli-91r17hb1.js} +3 -3
- package/dist/{cli-577bxkf6.js → cli-a8vps458.js} +182 -2208
- package/dist/{cli-tc5zec7v.js → cli-b7y8113e.js} +1 -1
- package/dist/{cli-69egj654.js → cli-bqrs5ycj.js} +1 -1
- package/dist/{cli-cf2vgn9d.js → cli-dv76vq5d.js} +30 -28
- package/dist/{cli-g5erf0zt.js → cli-e2prcx5m.js} +31 -0
- package/dist/{cli-c8r99akr.js → cli-e5qk3m2a.js} +209 -31
- package/dist/{cli-z1dqhc9y.js → cli-eqfbsxsn.js} +3 -3
- package/dist/{cli-kw0jcfje.js → cli-fyeft04q.js} +1 -1
- package/dist/{cli-5qkcdv8c.js → cli-gmdajfw5.js} +3 -3
- package/dist/{cli-ypgba1a8.js → cli-jkfxpg1b.js} +1 -1
- package/dist/{cli-mxvnnv5q.js → cli-jv36y8wz.js} +1 -1
- package/dist/{cli-m7vmeepg.js → cli-km8559dm.js} +6 -6
- package/dist/{cli-ykt8f7va.js → cli-kp3vfknv.js} +1 -1
- package/dist/{cli-ymmc1zxg.js → cli-m23r88re.js} +8 -5
- package/dist/{cli-v1pwxreq.js → cli-n4z21pt8.js} +1 -1
- package/dist/{cli-mxry91sa.js → cli-qg2zhjfc.js} +73 -4
- package/dist/{cli-1yrcfa5s.js → cli-rbr91whd.js} +4 -4
- package/dist/{cli-mkdsy1nq.js → cli-rg1c8ccw.js} +93 -7
- package/dist/cli-rh44xx46.js +2659 -0
- package/dist/cli-s0k2j7q9.js +2084 -0
- package/dist/{cli-ty4bzkqr.js → cli-sajr098b.js} +1 -1
- package/dist/{cli-6vfz44sr.js → cli-swrz3mj8.js} +1 -1
- package/dist/{cli-p8rt8spp.js → cli-vejds37t.js} +2 -2
- package/dist/{cli-jbzbr36n.js → cli-w6xa5nf5.js} +1 -1
- package/dist/{cli-9q3wt23f.js → cli-x6z90nq9.js} +1 -1
- package/dist/{cli-qzmzwegq.js → cli-ysnkvdx6.js} +102 -9
- package/dist/{cli-y0kjpfm9.js → cli-yvxfaem2.js} +2 -2
- package/dist/{cli-pkj1w46r.js → cli-z48ggfyq.js} +1 -1
- package/dist/{cli-6bpvv8h4.js → cli-zzev32jw.js} +1 -1
- package/dist/cli.js +20 -17
- package/dist/{codex-adapter-cgesnjkr.js → codex-adapter-0kpg09ca.js} +3 -3
- package/dist/{codex-hook-15dgc4cb.js → codex-hook-295jpbfa.js} +2 -2
- package/dist/{codex-session-runner-8kd7ddhr.js → codex-session-runner-8n672hxh.js} +3 -3
- package/dist/{commands-h6zaaxym.js → commands-mhmg5n0x.js} +4 -4
- package/dist/{cursor-adapter-v8mm6bp5.js → cursor-adapter-wn8n5zzk.js} +2 -2
- package/dist/{db-qg3myn53.js → db-wa6s1mwv.js} +4 -4
- package/dist/{dispatcher-7vnwj9qw.js → dispatcher-nhs2pkr8.js} +16 -14
- package/dist/{dsh-adapter-tz6hfp5r.js → dsh-adapter-5423p70m.js} +1 -1
- package/dist/{grok-adapter-jdt2dz2s.js → grok-adapter-28pkdt80.js} +3 -3
- package/dist/{handlers-2qtkpncr.js → handlers-5rtf9nng.js} +19 -17
- package/dist/{hook-51yfcgsv.js → hook-krjfkpzy.js} +4 -4
- package/dist/{hook-qnbdgwvd.js → hook-qkm73zk6.js} +5 -5
- package/dist/{http-jsmtag08.js → http-a397xcn4.js} +4845 -3484
- package/dist/{index-2t9kesn1.js → index-ac0v8v9d.js} +4 -4
- package/dist/{index-ekywyv2v.js → index-kqre45fr.js} +16 -14
- package/dist/{index-bt8wkv1w.js → index-rtxg15ah.js} +17 -15
- package/dist/{index-t4g06w93.js → index-vvsq7v45.js} +26 -23
- package/dist/{index-cx1sb9bq.js → index-ytdhtdry.js} +50 -11
- package/dist/{keepalive-4aezx4bj.js → keepalive-psfh8ysx.js} +6 -6
- package/dist/{lead-r44bkpm5.js → lead-m6rppjn7.js} +29 -27
- package/dist/{maintenance-grz5jrzh.js → maintenance-41f837ke.js} +4 -4
- package/dist/{node-qm9eb6z0.js → node-fkajaq96.js} +1 -1
- package/dist/{oauth-refresh-sweep-wm4ctjfj.js → oauth-refresh-sweep-6hr55hm8.js} +5 -5
- package/dist/{onboard-v60zgqf0.js → onboard-ktrtd7cz.js} +2 -2
- package/dist/{otel-impl-ghh4dyf8.js → otel-impl-dq4f2k3m.js} +1 -1
- package/dist/{pi-mono-adapter-0eg8kw2c.js → pi-mono-adapter-e5v9w0hv.js} +3 -2
- package/dist/{pricing-refresh-mjrqvnvw.js → pricing-refresh-kmk7j1ra.js} +6 -6
- package/dist/{rbac-roles-fd5tskj9.js → rbac-roles-9w1dkkxd.js} +4 -4
- package/dist/{rbac-roles-axv4d74x.js → rbac-roles-9ykx9tc5.js} +5 -5
- package/dist/{render-v2-k50j33qd.js → render-v2-ycqm53np.js} +5 -5
- package/dist/resume-cq2pkp8q.js +70 -0
- package/dist/{setup-4syh0pd2.js → setup-0dcmdvbn.js} +2 -2
- package/dist/{virtual-modules-ftr9kwe8.js → virtual-modules-1x9k4jcs.js} +1 -1
- package/dist/{work-objects-jcdbg8rr.js → work-objects-x01x2fh8.js} +19 -17
- package/dist/{worker-x8j8c9sx.js → worker-py1ssb1h.js} +29 -27
- package/openapi.json +15622 -15042
- package/package.json +8 -5
- package/src/apps/store.ts +1 -0
- package/src/artifact-sdk/feedback-overlay.ts +528 -0
- package/src/be/db/workflow-runs.ts +95 -0
- package/src/be/db-queries/slack-inbound.ts +489 -0
- package/src/be/db.ts +9 -2
- package/src/be/extensions/assets.ts +1 -1
- package/src/be/migrations/203_workflow_run_parent_step.sql +10 -0
- package/src/be/migrations/204_slack_inbound_receipts.sql +51 -0
- package/src/be/script-connections.ts +41 -7
- package/src/be/scripts/run-saved.ts +40 -20
- package/src/be/swarm-config-guard.ts +4 -0
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/catalog.ts +1 -1
- package/src/extensions/dispatcher.ts +3 -3
- package/src/heartbeat/heartbeat.ts +152 -2
- package/src/heartbeat/templates.ts +16 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/index.ts +2 -0
- package/src/http/integrations.ts +3 -1
- package/src/http/mcp-bridge.ts +1 -0
- package/src/http/pages-public.ts +7 -2
- package/src/http/pages.ts +178 -0
- package/src/http/script-connections.ts +35 -0
- package/src/http/slack-inbound.ts +107 -0
- package/src/http/tasks.ts +45 -27
- package/src/http/users.ts +1 -1
- package/src/http/webhooks.ts +20 -20
- package/src/prompts/session-templates.ts +32 -0
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +2 -2
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/claude-background-keepalive.ts +100 -0
- package/src/providers/claude-managed-adapter.ts +2 -11
- package/src/providers/claude-sdk-session.ts +7 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/opencode-adapter.ts +1 -2
- package/src/providers/pi-mono-adapter.ts +3 -3
- package/src/realtime/browser.generated.txt +8 -8
- package/src/realtime/browser.ts +3 -3
- package/src/scheduler/scheduler.ts +82 -13
- package/src/scripts-runtime/executors/quickjs-pool-config.ts +26 -0
- package/src/scripts-runtime/executors/quickjs.ts +9 -2
- package/src/scripts-runtime/executors/registry.ts +1 -1
- package/src/server.ts +1 -0
- package/src/slack/actions.ts +6 -2
- package/src/slack/app.ts +5 -0
- package/src/slack/assistant.ts +15 -5
- package/src/slack/egress-scrub.ts +3 -2
- package/src/slack/handlers.ts +36 -18
- package/src/slack/inbound-dispatch.ts +549 -0
- package/src/slack/inbound-files.ts +2 -0
- package/src/telemetry-context.ts +1 -0
- package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
- package/src/tests/claude-background-keepalive.fixture.ts +162 -0
- package/src/tests/claude-background-keepalive.test.ts +139 -0
- package/src/tests/codex-oauth-device.test.ts +1 -1
- package/src/tests/connection-embedded-auth.test.ts +51 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
- package/src/tests/heartbeat-checklist.test.ts +243 -0
- package/src/tests/pages-feedback.test.ts +218 -0
- package/src/tests/schedule-target-type.test.ts +136 -0
- package/src/tests/script-connections-http.test.ts +45 -0
- package/src/tests/script-connections-mcp.test.ts +41 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-executor-quickjs-pool.test.ts +106 -0
- package/src/tests/script-executor-registry.test.ts +56 -0
- package/src/tests/send-task-self-send.test.ts +107 -0
- package/src/tests/slack-inbound-diagnostics-auth.test.ts +93 -0
- package/src/tests/slack-inbound-receipts.test.ts +684 -0
- package/src/tests/slack-session-status.test.ts +4 -2
- package/src/tests/ui-e2e-ingest-payload.test.ts +5 -1
- package/src/tests/user-token-routes.test.ts +1 -1
- package/src/tests/workflow-executors.test.ts +3 -2
- package/src/tests/workflow-sub-workflow.test.ts +363 -0
- package/src/tools/script-connections/tool.ts +35 -3
- package/src/tools/send-task.ts +9 -1
- package/src/utils/internal-ai/complete-structured.ts +6 -3
- package/src/utils/runtime-model-catalog.ts +1 -1
- package/src/workflows/engine.ts +17 -1
- package/src/workflows/executors/agent-task.ts +2 -2
- package/src/workflows/executors/human-in-the-loop.ts +3 -3
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +1 -1
- package/src/workflows/executors/sub-workflow.ts +135 -0
- package/src/workflows/executors/wait.ts +3 -3
- package/src/workflows/recovery.ts +11 -1
- package/src/workflows/resume.ts +77 -0
- package/src/workflows/wait-filter.ts +5 -7
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/slack-reply-gate/README.md +98 -0
- package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
- package/templates/extensions/slack-reply-gate/manifest.json +8 -0
- package/templates/skills/heartbeat-runbook/content.md +3 -1
- package/templates/skills/pages/content.md +25 -0
- package/templates/skills/swarm-scripts/SKILL.md +10 -0
- package/templates/skills/swarm-scripts/content.md +10 -0
package/src/realtime/browser.ts
CHANGED
|
@@ -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
|
|
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
|
|
87
|
-
|
|
88
|
-
args: schedule.scriptArgs ?? {},
|
|
89
|
-
fsMode: "none",
|
|
88
|
+
const args = schedule.scriptArgs ?? {};
|
|
89
|
+
const record = {
|
|
90
90
|
agentId,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
122
|
+
const message =
|
|
99
123
|
output.stderr ||
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/src/slack/actions.ts
CHANGED
|
@@ -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)
|
|
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");
|
package/src/slack/assistant.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
84
|
+
proto.filesUploadV2 = function (this: unknown, options: Args) {
|
|
84
85
|
return originalUpload.call(this, scrubSlackUploadArgs(options));
|
|
85
86
|
};
|
|
86
87
|
proto[INSTALLED] = true;
|
package/src/slack/handlers.ts
CHANGED
|
@@ -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",
|