@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
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import type { AnyMiddlewareArgs, Middleware } from "@slack/bolt";
|
|
4
|
+
import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
|
|
5
|
+
import {
|
|
6
|
+
admitSlackInboundReceipt,
|
|
7
|
+
claimSlackInboundReceipt,
|
|
8
|
+
completeSlackInboundReceipt,
|
|
9
|
+
getNextSlackInboundAvailableAt,
|
|
10
|
+
getSlackInboundReceiptStats,
|
|
11
|
+
markInterruptedSlackInboundReceiptsUncertain,
|
|
12
|
+
releaseSlackInboundReceipt,
|
|
13
|
+
SLACK_INBOUND_LIMITS,
|
|
14
|
+
type SlackInboundAdmission,
|
|
15
|
+
type SlackInboundKind,
|
|
16
|
+
type SlackInboundReceipt,
|
|
17
|
+
type SlackInboundReceiptStats,
|
|
18
|
+
type SlackInboundTransport,
|
|
19
|
+
} from "../be/db-queries/slack-inbound";
|
|
20
|
+
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
21
|
+
import { getSlackConfiguration, type SlackMode } from "./config";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Explicit outcomes for inbound Slack deliveries.
|
|
25
|
+
*
|
|
26
|
+
* Handlers report what happened through this module instead of implying
|
|
27
|
+
* success by returning:
|
|
28
|
+
*
|
|
29
|
+
* - `noteSlackInboundSideEffect(label)` BEFORE a durable mutation (task
|
|
30
|
+
* creation, steering, thread buffering, workflow emission). Marking before is
|
|
31
|
+
* deliberate: a mutation that throws half way still counts, so its delivery
|
|
32
|
+
* becomes `uncertain` rather than being replayed into a duplicate task.
|
|
33
|
+
* - `ignoreSlackInbound(reason)` at an expected filter (bot message, edit,
|
|
34
|
+
* unauthorized user, rate limit, ...).
|
|
35
|
+
* - `reportSlackInboundFailure(code)` where a handler catches an error it does
|
|
36
|
+
* not rethrow.
|
|
37
|
+
*
|
|
38
|
+
* The resolved outcome is:
|
|
39
|
+
* error or reported failure, after a side effect → uncertain
|
|
40
|
+
* error or reported failure, before any side effect → failed (retryable)
|
|
41
|
+
* an ignore reason → ignored
|
|
42
|
+
* otherwise → processed
|
|
43
|
+
*
|
|
44
|
+
* Two entry points establish the delivery context:
|
|
45
|
+
* - `slackInboundOutcomeMiddleware()`, the first Bolt global middleware, wraps
|
|
46
|
+
* every Socket Mode delivery. It only records outcome counters: Socket Mode
|
|
47
|
+
* ingress, its in-memory event dedup and its error propagation are unchanged.
|
|
48
|
+
* - `runAdmittedSlackDelivery()`, used by the durable drain, marks the delivery
|
|
49
|
+
* as admitted from a receipt. That trusted, in-process flag (never a body
|
|
50
|
+
* field) is what lets handlers skip the legacy in-memory `event_id` cache: the
|
|
51
|
+
* receipt's unique key already deduplicated the delivery.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
export type SlackInboundOutcomeState = "processed" | "ignored" | "failed" | "uncertain";
|
|
55
|
+
|
|
56
|
+
export interface SlackInboundOutcome {
|
|
57
|
+
state: SlackInboundOutcomeState;
|
|
58
|
+
code: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DeliveryState {
|
|
62
|
+
transport: SlackInboundTransport;
|
|
63
|
+
admitted: boolean;
|
|
64
|
+
dispatched: boolean;
|
|
65
|
+
sideEffects: string[];
|
|
66
|
+
ignoredReasons: string[];
|
|
67
|
+
failures: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const deliveryStorage = new AsyncLocalStorage<DeliveryState>();
|
|
71
|
+
|
|
72
|
+
function newDeliveryState(transport: SlackInboundTransport, admitted: boolean): DeliveryState {
|
|
73
|
+
return {
|
|
74
|
+
transport,
|
|
75
|
+
admitted,
|
|
76
|
+
dispatched: false,
|
|
77
|
+
sideEffects: [],
|
|
78
|
+
ignoredReasons: [],
|
|
79
|
+
failures: [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Mark a durable mutation about to happen for the current delivery. */
|
|
84
|
+
export function noteSlackInboundSideEffect(label: string): void {
|
|
85
|
+
deliveryStorage.getStore()?.sideEffects.push(label);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Mark the current delivery as intentionally filtered. */
|
|
89
|
+
export function ignoreSlackInbound(reason: string): void {
|
|
90
|
+
deliveryStorage.getStore()?.ignoredReasons.push(reason);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Report an error a handler caught and did not rethrow. */
|
|
94
|
+
export function reportSlackInboundFailure(code: string): void {
|
|
95
|
+
deliveryStorage.getStore()?.failures.push(code);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* True only inside a delivery the durable receipt store already admitted. Set
|
|
100
|
+
* by `runAdmittedSlackDelivery`, never derived from the payload.
|
|
101
|
+
*/
|
|
102
|
+
export function isAdmittedSlackDelivery(): boolean {
|
|
103
|
+
return deliveryStorage.getStore()?.admitted === true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function errorCode(error: unknown): string {
|
|
107
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
|
|
108
|
+
return error.code;
|
|
109
|
+
}
|
|
110
|
+
return "handler_error";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveSlackInboundOutcome(
|
|
114
|
+
state: Pick<DeliveryState, "sideEffects" | "ignoredReasons" | "failures">,
|
|
115
|
+
error?: unknown,
|
|
116
|
+
): SlackInboundOutcome {
|
|
117
|
+
const threw = error !== undefined;
|
|
118
|
+
if (threw || state.failures.length > 0) {
|
|
119
|
+
const code = state.failures[0] ?? errorCode(error);
|
|
120
|
+
return { state: state.sideEffects.length > 0 ? "uncertain" : "failed", code };
|
|
121
|
+
}
|
|
122
|
+
if (state.ignoredReasons.length > 0) {
|
|
123
|
+
return { state: "ignored", code: state.ignoredReasons[0]! };
|
|
124
|
+
}
|
|
125
|
+
return { state: "processed", code: state.sideEffects[0] ?? "completed" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Outcome counters (Socket Mode is observed, not stored) ─────────────────
|
|
129
|
+
|
|
130
|
+
type OutcomeCounters = Record<SlackInboundOutcomeState, number> & {
|
|
131
|
+
lastOutcomeAt: string | null;
|
|
132
|
+
lastFailure: { code: string; at: string } | null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function emptyCounters(): OutcomeCounters {
|
|
136
|
+
return {
|
|
137
|
+
processed: 0,
|
|
138
|
+
ignored: 0,
|
|
139
|
+
failed: 0,
|
|
140
|
+
uncertain: 0,
|
|
141
|
+
lastOutcomeAt: null,
|
|
142
|
+
lastFailure: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const counters: Record<SlackInboundTransport, OutcomeCounters> = {
|
|
147
|
+
socket: emptyCounters(),
|
|
148
|
+
http: emptyCounters(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function recordOutcome(transport: SlackInboundTransport, outcome: SlackInboundOutcome): void {
|
|
152
|
+
const bucket = counters[transport];
|
|
153
|
+
const at = new Date().toISOString();
|
|
154
|
+
bucket[outcome.state] += 1;
|
|
155
|
+
bucket.lastOutcomeAt = at;
|
|
156
|
+
if (outcome.state === "failed" || outcome.state === "uncertain") {
|
|
157
|
+
bucket.lastFailure = { code: outcome.code, at };
|
|
158
|
+
console.warn(`[Slack] inbound ${transport} delivery ${outcome.state}: ${outcome.code}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Test-only. */
|
|
163
|
+
export function resetSlackInboundCountersForTests(): void {
|
|
164
|
+
counters.socket = emptyCounters();
|
|
165
|
+
counters.http = emptyCounters();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* First Bolt global middleware. Everything downstream (the Assistant
|
|
170
|
+
* middleware and every listener) runs inside `next()`, so listener errors,
|
|
171
|
+
* which Bolt routes to its error handler after this middleware, are observed
|
|
172
|
+
* here and rethrown unchanged.
|
|
173
|
+
*/
|
|
174
|
+
export function slackInboundOutcomeMiddleware(): Middleware<AnyMiddlewareArgs> {
|
|
175
|
+
return async ({ next }) => {
|
|
176
|
+
const existing = deliveryStorage.getStore();
|
|
177
|
+
if (existing) {
|
|
178
|
+
// A durable delivery: the drain resolves and stores the outcome. Record
|
|
179
|
+
// the error here too, because Bolt's error handler may swallow it before
|
|
180
|
+
// App.processEvent() returns to the drain.
|
|
181
|
+
existing.dispatched = true;
|
|
182
|
+
try {
|
|
183
|
+
await next();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
existing.failures.push(errorCode(error));
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const state = newDeliveryState("socket", false);
|
|
192
|
+
state.dispatched = true;
|
|
193
|
+
await deliveryStorage.run(state, async () => {
|
|
194
|
+
try {
|
|
195
|
+
await next();
|
|
196
|
+
} catch (error) {
|
|
197
|
+
recordOutcome("socket", resolveSlackInboundOutcome(state, error));
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
recordOutcome("socket", resolveSlackInboundOutcome(state));
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Run `fn` as an admitted durable delivery and resolve its outcome. Never
|
|
207
|
+
* throws: a thrown error becomes a `failed` or `uncertain` outcome.
|
|
208
|
+
*
|
|
209
|
+
* `requireBoltDispatch` turns "the Bolt middleware never ran" (authorization
|
|
210
|
+
* failure, unrecognized payload) into `failed`, because `App.processEvent()`
|
|
211
|
+
* resolving is not by itself evidence that a listener handled the delivery.
|
|
212
|
+
*/
|
|
213
|
+
export async function runAdmittedSlackDelivery(
|
|
214
|
+
transport: SlackInboundTransport,
|
|
215
|
+
fn: () => Promise<void>,
|
|
216
|
+
options: { requireBoltDispatch?: boolean } = {},
|
|
217
|
+
): Promise<SlackInboundOutcome> {
|
|
218
|
+
const state = newDeliveryState(transport, true);
|
|
219
|
+
try {
|
|
220
|
+
await deliveryStorage.run(state, fn);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return resolveSlackInboundOutcome(state, error);
|
|
223
|
+
}
|
|
224
|
+
if (options.requireBoltDispatch && !state.dispatched) {
|
|
225
|
+
return { state: "failed", code: "not_dispatched" };
|
|
226
|
+
}
|
|
227
|
+
return resolveSlackInboundOutcome(state);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Admission ───────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export type SlackInboundKeyResult =
|
|
233
|
+
| {
|
|
234
|
+
ok: true;
|
|
235
|
+
dedupKey: string;
|
|
236
|
+
payloadType: string;
|
|
237
|
+
apiAppId: string | null;
|
|
238
|
+
eventId: string | null;
|
|
239
|
+
}
|
|
240
|
+
| { ok: false; reason: "missing_event_id" | "malformed" };
|
|
241
|
+
|
|
242
|
+
function sha256(value: string): string {
|
|
243
|
+
return createHash("sha256").update(value).digest("hex");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function stringField(body: Record<string, unknown>, key: string): string | null {
|
|
247
|
+
const value = body[key];
|
|
248
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Derive the dedup key for a delivery.
|
|
253
|
+
*
|
|
254
|
+
* - Events: `(api_app_id, event_id)`. `event_callback` without an `event_id`
|
|
255
|
+
* is refused. Other event envelopes (control messages) key on a body hash.
|
|
256
|
+
* - Interactions and commands carry no universal id: key on a kind-scoped
|
|
257
|
+
* SHA-256 of the original body. Action timestamps, view hashes and trigger
|
|
258
|
+
* ids make genuine later actions distinct; retry headers are not part of the
|
|
259
|
+
* body, so a retry maps to the same key.
|
|
260
|
+
*/
|
|
261
|
+
export function computeSlackInboundKey(
|
|
262
|
+
kind: SlackInboundKind,
|
|
263
|
+
rawBody: string,
|
|
264
|
+
body: unknown,
|
|
265
|
+
): SlackInboundKeyResult {
|
|
266
|
+
if (!body || typeof body !== "object") return { ok: false, reason: "malformed" };
|
|
267
|
+
const record = body as Record<string, unknown>;
|
|
268
|
+
const payloadType = stringField(record, "type") ?? (kind === "command" ? "command" : null);
|
|
269
|
+
if (!payloadType) return { ok: false, reason: "malformed" };
|
|
270
|
+
const apiAppId = stringField(record, "api_app_id");
|
|
271
|
+
|
|
272
|
+
if (kind === "event" && payloadType === "event_callback") {
|
|
273
|
+
const eventId = stringField(record, "event_id");
|
|
274
|
+
if (!eventId) return { ok: false, reason: "missing_event_id" };
|
|
275
|
+
return {
|
|
276
|
+
ok: true,
|
|
277
|
+
dedupKey: `event:${apiAppId ?? "-"}:${eventId}`,
|
|
278
|
+
payloadType,
|
|
279
|
+
apiAppId,
|
|
280
|
+
eventId,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
dedupKey: `${kind}:${payloadType}:${sha256(rawBody)}`,
|
|
287
|
+
payloadType,
|
|
288
|
+
apiAppId,
|
|
289
|
+
eventId: null,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface AdmitSlackInboundInput {
|
|
294
|
+
transport: SlackInboundTransport;
|
|
295
|
+
kind: SlackInboundKind;
|
|
296
|
+
rawBody: string;
|
|
297
|
+
body: unknown;
|
|
298
|
+
retryNum?: number | null;
|
|
299
|
+
retryReason?: string | null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export type SlackInboundAdmitResult =
|
|
303
|
+
| SlackInboundAdmission
|
|
304
|
+
| { status: "invalid"; reason: "missing_event_id" | "malformed" };
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Persist a delivery before it is acknowledged. The encrypted payload and the
|
|
308
|
+
* dedup key commit in one transaction; a crash after this returns leaves a
|
|
309
|
+
* recoverable `pending` receipt.
|
|
310
|
+
*/
|
|
311
|
+
export async function admitSlackInbound(
|
|
312
|
+
input: AdmitSlackInboundInput,
|
|
313
|
+
now: Date = new Date(),
|
|
314
|
+
): Promise<SlackInboundAdmitResult> {
|
|
315
|
+
const key = computeSlackInboundKey(input.kind, input.rawBody, input.body);
|
|
316
|
+
if (!key.ok) return { status: "invalid", reason: key.reason };
|
|
317
|
+
const payloadBytes = Buffer.byteLength(input.rawBody, "utf8");
|
|
318
|
+
if (payloadBytes > SLACK_INBOUND_LIMITS.maxPayloadBytes) {
|
|
319
|
+
return { status: "rejected", reason: "payload_too_large" };
|
|
320
|
+
}
|
|
321
|
+
return admitSlackInboundReceipt(
|
|
322
|
+
{
|
|
323
|
+
dedupKey: key.dedupKey,
|
|
324
|
+
transport: input.transport,
|
|
325
|
+
kind: input.kind,
|
|
326
|
+
payloadType: key.payloadType,
|
|
327
|
+
apiAppId: key.apiAppId,
|
|
328
|
+
eventId: key.eventId,
|
|
329
|
+
payloadCiphertext: encryptSecret(input.rawBody, getEncryptionKey()),
|
|
330
|
+
payloadBytes,
|
|
331
|
+
retryNum: input.retryNum ?? null,
|
|
332
|
+
retryReason: input.retryReason ?? null,
|
|
333
|
+
},
|
|
334
|
+
now,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ─── Drain ───────────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
export interface SlackInboundDelivery {
|
|
341
|
+
receipt: SlackInboundReceipt;
|
|
342
|
+
rawBody: string;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Hands one admitted delivery to Bolt (or a test double). */
|
|
346
|
+
export type SlackInboundProcessor = (delivery: SlackInboundDelivery) => Promise<void>;
|
|
347
|
+
|
|
348
|
+
export interface SlackInboundDrainResult {
|
|
349
|
+
receiptId: string;
|
|
350
|
+
outcome: SlackInboundOutcome;
|
|
351
|
+
/** Where the receipt ended up after this attempt. */
|
|
352
|
+
state: "pending" | SlackInboundOutcomeState;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Claim and process the oldest available receipt. Returns null when nothing
|
|
357
|
+
* is claimable.
|
|
358
|
+
*/
|
|
359
|
+
export async function processNextSlackInboundReceipt(
|
|
360
|
+
processor: SlackInboundProcessor,
|
|
361
|
+
options: { now?: () => Date; requireBoltDispatch?: boolean } = {},
|
|
362
|
+
): Promise<SlackInboundDrainResult | null> {
|
|
363
|
+
const now = options.now ?? (() => new Date());
|
|
364
|
+
const receipt = await claimSlackInboundReceipt(now());
|
|
365
|
+
if (!receipt) return null;
|
|
366
|
+
|
|
367
|
+
let rawBody: string;
|
|
368
|
+
try {
|
|
369
|
+
if (!receipt.payloadCiphertext) throw new Error("payload already erased");
|
|
370
|
+
rawBody = decryptSecret(receipt.payloadCiphertext, getEncryptionKey());
|
|
371
|
+
} catch {
|
|
372
|
+
// Nothing ran, but retrying cannot fix an unreadable payload.
|
|
373
|
+
const outcome: SlackInboundOutcome = { state: "failed", code: "payload_unreadable" };
|
|
374
|
+
await completeSlackInboundReceipt(
|
|
375
|
+
receipt.id,
|
|
376
|
+
{ state: "failed", errorCode: outcome.code },
|
|
377
|
+
now(),
|
|
378
|
+
);
|
|
379
|
+
recordOutcome(receipt.transport, outcome);
|
|
380
|
+
return { receiptId: receipt.id, outcome, state: "failed" };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const outcome = await runAdmittedSlackDelivery(
|
|
384
|
+
receipt.transport,
|
|
385
|
+
() => processor({ receipt, rawBody }),
|
|
386
|
+
{ requireBoltDispatch: options.requireBoltDispatch },
|
|
387
|
+
);
|
|
388
|
+
recordOutcome(receipt.transport, outcome);
|
|
389
|
+
|
|
390
|
+
if (outcome.state === "failed" && receipt.attempts < SLACK_INBOUND_LIMITS.maxAttempts) {
|
|
391
|
+
const availableAt = new Date(
|
|
392
|
+
now().getTime() + SLACK_INBOUND_LIMITS.retryBackoffMs * receipt.attempts,
|
|
393
|
+
);
|
|
394
|
+
await releaseSlackInboundReceipt(receipt.id, outcome.code, availableAt, now());
|
|
395
|
+
return { receiptId: receipt.id, outcome, state: "pending" };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
await completeSlackInboundReceipt(
|
|
399
|
+
receipt.id,
|
|
400
|
+
{
|
|
401
|
+
state: outcome.state,
|
|
402
|
+
outcomeCode:
|
|
403
|
+
outcome.state === "processed" || outcome.state === "ignored" ? outcome.code : null,
|
|
404
|
+
errorCode: outcome.state === "failed" || outcome.state === "uncertain" ? outcome.code : null,
|
|
405
|
+
},
|
|
406
|
+
now(),
|
|
407
|
+
);
|
|
408
|
+
return { receiptId: receipt.id, outcome, state: outcome.state };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Arms a one-shot timer and returns its cancel function. */
|
|
412
|
+
export type SlackInboundTimer = (fn: () => void, delayMs: number) => () => void;
|
|
413
|
+
|
|
414
|
+
const defaultTimer: SlackInboundTimer = (fn, delayMs) => {
|
|
415
|
+
const handle = setTimeout(fn, delayMs);
|
|
416
|
+
handle.unref?.();
|
|
417
|
+
return () => clearTimeout(handle);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/** Floor for the retry wake-up, so a clock skew never spins the drain. */
|
|
421
|
+
const MIN_REARM_MS = 250;
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* One bounded, single-flight drain. `wake()` starts a pass (or schedules one
|
|
425
|
+
* more pass if a drain is already running) and resolves when the drain goes
|
|
426
|
+
* idle. At most `maxPerPass` receipts are processed per pass.
|
|
427
|
+
*
|
|
428
|
+
* On going idle the drain arms a timer for the earliest pending receipt, so a
|
|
429
|
+
* receipt released for a backoff retry is retried even when no new delivery
|
|
430
|
+
* calls `wake()`. `stop()` cancels that timer.
|
|
431
|
+
*/
|
|
432
|
+
export function createSlackInboundDrain(
|
|
433
|
+
processor: SlackInboundProcessor,
|
|
434
|
+
options: {
|
|
435
|
+
maxPerPass?: number;
|
|
436
|
+
requireBoltDispatch?: boolean;
|
|
437
|
+
now?: () => Date;
|
|
438
|
+
setTimer?: SlackInboundTimer;
|
|
439
|
+
} = {},
|
|
440
|
+
): { wake(): Promise<void>; stop(): void } {
|
|
441
|
+
const maxPerPass = options.maxPerPass ?? 50;
|
|
442
|
+
const now = options.now ?? (() => new Date());
|
|
443
|
+
const setTimer = options.setTimer ?? defaultTimer;
|
|
444
|
+
let running: Promise<void> | null = null;
|
|
445
|
+
let again = false;
|
|
446
|
+
let stopped = false;
|
|
447
|
+
let cancelTimer: (() => void) | null = null;
|
|
448
|
+
|
|
449
|
+
const clearTimer = () => {
|
|
450
|
+
cancelTimer?.();
|
|
451
|
+
cancelTimer = null;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const pass = async () => {
|
|
455
|
+
for (let i = 0; i < maxPerPass; i++) {
|
|
456
|
+
const result = await processNextSlackInboundReceipt(processor, {
|
|
457
|
+
now,
|
|
458
|
+
requireBoltDispatch: options.requireBoltDispatch,
|
|
459
|
+
});
|
|
460
|
+
if (!result) return;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const armRetryTimer = async () => {
|
|
465
|
+
const next = await getNextSlackInboundAvailableAt();
|
|
466
|
+
if (!next || stopped) return;
|
|
467
|
+
const delayMs = Math.max(next.getTime() - now().getTime(), MIN_REARM_MS);
|
|
468
|
+
cancelTimer = setTimer(() => {
|
|
469
|
+
cancelTimer = null;
|
|
470
|
+
void drain.wake();
|
|
471
|
+
}, delayMs);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const loop = async () => {
|
|
475
|
+
do {
|
|
476
|
+
again = false;
|
|
477
|
+
clearTimer();
|
|
478
|
+
try {
|
|
479
|
+
await pass();
|
|
480
|
+
} catch (error) {
|
|
481
|
+
// A DB failure mid-pass leaves the receipt `processing`; boot recovery
|
|
482
|
+
// turns it `uncertain`. Never loop hot on a broken store.
|
|
483
|
+
console.error("[Slack] inbound drain pass failed:", scrubSecrets(String(error)));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (again) continue;
|
|
487
|
+
try {
|
|
488
|
+
await armRetryTimer();
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error("[Slack] inbound drain could not arm retry:", scrubSecrets(String(error)));
|
|
491
|
+
}
|
|
492
|
+
} while (again);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const drain = {
|
|
496
|
+
wake() {
|
|
497
|
+
if (stopped) return Promise.resolve();
|
|
498
|
+
if (running) {
|
|
499
|
+
again = true;
|
|
500
|
+
return running;
|
|
501
|
+
}
|
|
502
|
+
running = loop().finally(() => {
|
|
503
|
+
running = null;
|
|
504
|
+
});
|
|
505
|
+
return running;
|
|
506
|
+
},
|
|
507
|
+
stop() {
|
|
508
|
+
stopped = true;
|
|
509
|
+
clearTimer();
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
return drain;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Boot recovery; call once before the drain starts. */
|
|
516
|
+
export async function recoverSlackInboundAfterRestart(now: Date = new Date()): Promise<number> {
|
|
517
|
+
const interrupted = await markInterruptedSlackInboundReceiptsUncertain(now);
|
|
518
|
+
if (interrupted > 0) {
|
|
519
|
+
console.warn(
|
|
520
|
+
`[Slack] ${interrupted} inbound receipt(s) were interrupted mid-processing and are now uncertain`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
return interrupted;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ─── Diagnostics ─────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
export interface SlackInboundDiagnostics {
|
|
529
|
+
mode: SlackMode | null;
|
|
530
|
+
disabled: boolean;
|
|
531
|
+
receipts: SlackInboundReceiptStats;
|
|
532
|
+
outcomes: Record<SlackInboundTransport, OutcomeCounters>;
|
|
533
|
+
limits: typeof SLACK_INBOUND_LIMITS;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Operator view. Contains no payloads, tokens, signatures or response URLs. */
|
|
537
|
+
export async function getSlackInboundDiagnostics(): Promise<SlackInboundDiagnostics> {
|
|
538
|
+
const config = getSlackConfiguration();
|
|
539
|
+
return {
|
|
540
|
+
mode: config.mode,
|
|
541
|
+
disabled: config.disabled,
|
|
542
|
+
receipts: await getSlackInboundReceiptStats(),
|
|
543
|
+
outcomes: {
|
|
544
|
+
socket: { ...counters.socket },
|
|
545
|
+
http: { ...counters.http },
|
|
546
|
+
},
|
|
547
|
+
limits: SLACK_INBOUND_LIMITS,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
@@ -28,6 +28,7 @@ import type { AgentTask, CreateTaskOptions, TaskAttachment } from "../types";
|
|
|
28
28
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
29
29
|
import { taskAttachmentFetchCommand } from "../utils/task-attachment-links";
|
|
30
30
|
import { getFileInfo, type SlackFile } from "./files";
|
|
31
|
+
import { noteSlackInboundSideEffect } from "./inbound-dispatch";
|
|
31
32
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
32
33
|
import "./templates";
|
|
33
34
|
|
|
@@ -287,6 +288,7 @@ export async function createSlackTaskWithFiles(
|
|
|
287
288
|
options: CreateTaskOptions,
|
|
288
289
|
inbound: InboundSlackFiles,
|
|
289
290
|
): Promise<{ task: AgentTask; unattached: SlackFileFailure[] }> {
|
|
291
|
+
noteSlackInboundSideEffect("task_created");
|
|
290
292
|
if (inbound.fetched.length === 0) {
|
|
291
293
|
const task = await createTaskWithSiblingAwareness(description, options, { origin: "slack" });
|
|
292
294
|
return { task, unattached: inbound.failed };
|
package/src/telemetry-context.ts
CHANGED
|
@@ -127,6 +127,7 @@ let specsByEvent: Map<string, Record<string, CatalogPropertySpec>> | null = null
|
|
|
127
127
|
|
|
128
128
|
function catalogSpecs(event: string): Record<string, CatalogPropertySpec> | undefined {
|
|
129
129
|
if (!specsByEvent) {
|
|
130
|
+
// oxlint-disable-next-line anti-slop/no-chained-type-assertions -- generated catalog; its JSON literal types do not match the spec type.
|
|
130
131
|
const file = catalog as unknown as {
|
|
131
132
|
events: Array<{
|
|
132
133
|
product: string;
|