@desplega.ai/agent-swarm 1.145.0 → 1.147.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/README.md +2 -1
- package/dist/{acp-adapter-580t41c0.js → acp-adapter-9n319wqc.js} +61 -6
- package/dist/{actions-m662tjng.js → actions-2vxqvpr9.js} +9 -8
- package/dist/{app-zb3rndm8.js → app-w066xfy0.js} +6 -5
- package/dist/assistant-kt31dj2h.js +236 -0
- package/dist/{boot-reembed-072x00vn.js → boot-reembed-az5rassp.js} +7 -6
- package/dist/{boot-reembed-65zz2smt.js → boot-reembed-j3mm3rfz.js} +8 -7
- package/dist/{boot-scrub-logs-yz0ams04.js → boot-scrub-logs-b6h54817.js} +4 -4
- package/dist/{claude-adapter-paj270y2.js → claude-adapter-w1z04ann.js} +8 -6
- package/dist/{claude-managed-setup-w619jncn.js → claude-managed-setup-f5pmsyrp.js} +24 -3
- package/dist/{claude-sdk-session-swt80j9z.js → claude-sdk-session-f0cc1kec.js} +8 -6
- package/dist/{cli-dt553ktb.js → cli-0b6y7kxm.js} +1 -1
- package/dist/{cli-bxdhw615.js → cli-12fz972k.js} +4 -4
- package/dist/{cli-s2f20r5w.js → cli-1th1728g.js} +2 -2
- package/dist/{cli-rs6ec3b9.js → cli-3tgwnf43.js} +3 -3
- package/dist/{cli-gxnzc6wh.js → cli-4nr988h6.js} +1 -1
- package/dist/{cli-axs8e6d4.js → cli-53s590z8.js} +1 -1
- package/dist/{cli-ygbjp3z8.js → cli-54wwve05.js} +5 -4
- package/dist/{cli-v0asr5yv.js → cli-719k9j2c.js} +2 -2
- package/dist/{cli-anktweqr.js → cli-89xfd7f9.js} +21 -47
- package/dist/{cli-q34a0e10.js → cli-8v0g1yc8.js} +1 -1
- package/dist/cli-9d8ntetj.js +42 -0
- package/dist/{cli-z9fprcrf.js → cli-atcve9y8.js} +881 -738
- package/dist/{cli-jmb7g47h.js → cli-b5bq9crk.js} +14 -31
- package/dist/cli-d8brzxjb.js +190 -0
- package/dist/{cli-gzky0gkr.js → cli-de14znh8.js} +2 -2
- package/dist/{cli-xytwff7f.js → cli-dttph1bw.js} +2 -2
- package/dist/cli-ep19k3mb.js +19 -0
- package/dist/{cli-m1xa8be7.js → cli-f146mn5s.js} +7 -50
- package/dist/{cli-1fj0chdk.js → cli-f7dsy61y.js} +5 -3
- package/dist/{cli-f567d5c1.js → cli-ftht3zzj.js} +2782 -2589
- package/dist/{cli-a0g1gbt5.js → cli-hbv0kq6w.js} +2 -2
- package/dist/{cli-fhsz3twr.js → cli-jsp14b6c.js} +171 -1
- package/dist/{cli-7fnkj1hj.js → cli-kpv03zhj.js} +21 -17
- package/dist/{cli-m4j6trn4.js → cli-m8yrsg97.js} +2 -2
- package/dist/cli-m9sxbkhm.js +779 -0
- package/dist/cli-mmemxxdc.js +823 -0
- package/dist/{cli-tqtvj5pt.js → cli-n8508nre.js} +1 -1
- package/dist/{cli-z96t0thj.js → cli-p1d5b073.js} +37 -2
- package/dist/{cli-gh8y8f36.js → cli-rw51bq3j.js} +13 -10
- package/dist/{cli-59ej1c07.js → cli-smby5m17.js} +7 -6
- package/dist/{cli-65vqzz3v.js → cli-tzvk9haz.js} +94 -372
- package/dist/{cli-dc9k2ba9.js → cli-w9kb3bk1.js} +1 -1
- package/dist/{cli-kpev657q.js → cli-y6b6fb76.js} +25 -3
- package/dist/{cli-123q2get.js → cli-yz3djrm0.js} +9 -9
- package/dist/{cli-wvp6bvy4.js → cli-zhjmqxzh.js} +1 -1
- package/dist/cli.js +13 -13
- package/dist/{codex-adapter-k98vaw2r.js → codex-adapter-zw4dwsx7.js} +2 -2
- package/dist/{codex-hook-9pf19cks.js → codex-hook-bw8p5nh4.js} +2 -2
- package/dist/{codex-session-runner-tngtphx7.js → codex-session-runner-k981s65v.js} +2 -2
- package/dist/{commands-y8nbr3fv.js → commands-sykrxx7e.js} +4 -4
- package/dist/{db-30p4h2t1.js → db-sfbsc8wt.js} +6 -4
- package/dist/handlers-0aspcejm.js +603 -0
- package/dist/{hook-dcw4jfbj.js → hook-4nvqj2sb.js} +7 -5
- package/dist/{hook-21jexvfx.js → hook-hxccs7m5.js} +24 -49
- package/dist/{http-r4kq5g8d.js → http-y06z517d.js} +230 -94
- package/dist/{index-q3qja6ta.js → index-hssww7kj.js} +15 -14
- package/dist/{index-gay66b9w.js → index-k8znfx0z.js} +13 -12
- package/dist/{index-53zyht4z.js → index-tce1rss4.js} +371 -110
- package/dist/{index-nqvf26za.js → index-x5j894zf.js} +14 -13
- package/dist/{keepalive-xs8fa55y.js → keepalive-w733ax66.js} +6 -6
- package/dist/{lead-d9m2t4s0.js → lead-75bjqa9d.js} +29 -27
- package/dist/{maintenance-3zh82g6z.js → maintenance-w4f1zjck.js} +8 -7
- package/dist/{oauth-refresh-sweep-03snsye5.js → oauth-refresh-sweep-wdetky26.js} +5 -5
- package/dist/{onboard-0cg1925x.js → onboard-bb5net8s.js} +2 -2
- package/dist/{otel-impl-0ab95z27.js → otel-impl-0w7c14ap.js} +1 -1
- package/dist/{pi-mono-adapter-dtkz4e7j.js → pi-mono-adapter-2has4gnp.js} +12 -2
- package/dist/{pricing-refresh-e5jhzz4j.js → pricing-refresh-9nzsrgcq.js} +6 -6
- package/dist/{rbac-roles-p1mf40z3.js → rbac-roles-r3hqjp09.js} +5 -5
- package/dist/{rbac-roles-2w3cmrkc.js → rbac-roles-z1jrffbt.js} +4 -4
- package/dist/{registry-9kdck5ye.js → registry-v5wytg0s.js} +1 -2
- package/dist/{render-v2-xhawry3k.js → render-v2-vbr89fnb.js} +6 -5
- package/dist/{seed-pricing-t223sw91.js → seed-pricing-g62hy1pk.js} +7 -5
- package/dist/{setup-zcs0rce7.js → setup-gnfqnptp.js} +2 -2
- package/dist/{worker-qnp2ka62.js → worker-kznxp4qm.js} +29 -27
- package/openapi.json +263 -12
- package/package.json +1 -1
- package/plugin/commands/start-worker.md +4 -9
- package/plugin/pi-skills/start-worker/SKILL.md +4 -9
- package/src/be/db/agents.ts +556 -0
- package/src/be/db/context-versions.ts +117 -0
- package/src/be/db/runtime.ts +406 -0
- package/src/be/db/tasks/read.ts +1167 -0
- package/src/be/db/tasks/write.ts +953 -0
- package/src/be/db.ts +584 -3429
- package/src/be/migrations/147_scheduled_tasks_parent_task.sql +7 -0
- package/src/be/migrations/148_pages_svg.sql +123 -0
- package/src/be/migrations/149_session_tokens.sql +24 -0
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed-pricing.ts +13 -8
- package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
- package/src/be/seed-scripts/catalog/star-history-refresh.ts +101 -0
- package/src/be/seed-scripts/catalog/star-history-renderer.ts +119 -0
- package/src/be/seed-scripts/index.ts +17 -0
- package/src/be/seed-skills/index.ts +14 -0
- package/src/be/swarm-config-guard.ts +5 -2
- package/src/be/task-attachment-store.ts +70 -0
- package/src/be/users.ts +77 -0
- package/src/commands/attachments-section.ts +37 -0
- package/src/commands/claude-managed-setup.ts +45 -3
- package/src/commands/claude-md-session.ts +408 -0
- package/src/commands/context-preamble.ts +15 -3
- package/src/commands/profile-sync.ts +205 -5
- package/src/commands/provider-credentials.ts +60 -27
- package/src/commands/runner.ts +63 -58
- package/src/commands/templates.ts +25 -2
- package/src/hooks/hook.ts +26 -67
- package/src/http/agents.ts +40 -1
- package/src/http/auth.ts +13 -1
- package/src/http/fs.ts +21 -51
- package/src/http/index.ts +2 -0
- package/src/http/mcp.ts +32 -0
- package/src/http/pages-public.ts +20 -4
- package/src/http/pages.ts +11 -3
- package/src/http/poll.ts +4 -0
- package/src/http/repos.ts +3 -3
- package/src/http/sessions.ts +82 -0
- package/src/http/tasks.ts +10 -2
- package/src/http/utils.ts +79 -1
- package/src/prompts/session-templates.ts +12 -8
- package/src/providers/acp-adapter.ts +62 -8
- package/src/providers/pi-mono-adapter.ts +23 -6
- package/src/providers/pricing-sources.md +11 -0
- package/src/scheduler/scheduler.test.ts +49 -2
- package/src/scheduler/scheduler.ts +4 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +8 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +8 -0
- package/src/server.ts +2 -0
- package/src/slack/assistant.ts +87 -46
- package/src/slack/handlers.test.ts +1 -3
- package/src/slack/handlers.ts +102 -87
- package/src/slack/inbound-files.ts +466 -0
- package/src/slack/templates.ts +14 -0
- package/src/tasks/task-terminal-effects.ts +158 -0
- package/src/tests/acp-adapter.test.ts +369 -117
- package/src/tests/budget-claim-gate.test.ts +1 -0
- package/src/tests/build-pi-skills.test.ts +5 -7
- package/src/tests/claude-managed-setup.test.ts +52 -4
- package/src/tests/claude-md-session.test.ts +603 -0
- package/src/tests/codex-skill-resolver.test.ts +19 -0
- package/src/tests/context-versioning.test.ts +107 -0
- package/src/tests/cors-allowlist.test.ts +309 -0
- package/src/tests/credential-check.test.ts +157 -14
- package/src/tests/db-agents.test.ts +91 -0
- package/src/tests/db-context-versions.test.ts +37 -0
- package/src/tests/db-extraction-contract.test.ts +45 -0
- package/src/tests/db-runtime.test.ts +70 -0
- package/src/tests/db-task-read-contract.test.ts +624 -0
- package/src/tests/db-task-write-contract.test.ts +653 -0
- package/src/tests/db-tasks-read.test.ts +183 -0
- package/src/tests/db-tasks-write.test.ts +230 -0
- package/src/tests/defer-task.test.ts +389 -0
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/e2e-visuals-stage.test.ts +151 -0
- package/src/tests/file-lock.test.ts +144 -0
- package/src/tests/fixtures/file-lock-holder.ts +13 -0
- package/src/tests/fixtures/hold-file-lock.ts +25 -0
- package/src/tests/heartbeat-expiry.test.ts +188 -0
- package/src/tests/http-api-integration.test.ts +78 -1
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-svg-migration.test.ts +42 -0
- package/src/tests/preload.ts +4 -1
- package/src/tests/profile-sync-warning.test.ts +16 -0
- package/src/tests/profile-sync.test.ts +126 -0
- package/src/tests/prompt-template-session.test.ts +20 -4
- package/src/tests/provider-registration.test.ts +93 -0
- package/src/tests/rbac-e2e-helpers.test.ts +53 -0
- package/src/tests/rbac-e2e-helpers.ts +19 -2
- package/src/tests/reload-config.test.ts +26 -0
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/runner-attachments-section.test.ts +22 -2
- package/src/tests/runner-context-preamble.test.ts +30 -0
- package/src/tests/runner-deployment-resume.test.ts +107 -0
- package/src/tests/runner-repo-autostash.test.ts +12 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/session-tokens.test.ts +279 -0
- package/src/tests/slack-file-tools-attachments.test.ts +408 -0
- package/src/tests/slack-inbound-files.test.ts +847 -0
- package/src/tests/star-history-refresh.test.ts +102 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +21 -0
- package/src/tests/swarm-repos.test.ts +20 -0
- package/src/tests/tool-annotations.test.ts +4 -3
- package/src/tests/ui-logs-parser.test.ts +176 -0
- package/src/tools/create-page.ts +4 -2
- package/src/tools/defer-task.ts +239 -0
- package/src/tools/repos/update-repo.ts +2 -2
- package/src/tools/schedules/create-schedule.ts +1 -0
- package/src/tools/schedules/list-schedules.ts +1 -0
- package/src/tools/schedules/patch-schedule.ts +1 -0
- package/src/tools/schedules/update-schedule.ts +1 -0
- package/src/tools/slack-download-file.ts +60 -29
- package/src/tools/slack-read.ts +73 -32
- package/src/tools/store-progress.ts +16 -146
- package/src/tools/tool-config.ts +1 -0
- package/src/tools/utils.ts +21 -0
- package/src/types.ts +37 -4
- package/src/utils/acp-session-token.ts +56 -0
- package/src/utils/file-lock.ts +111 -0
- package/src/utils/heartbeat-expiry.ts +78 -0
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/task-attachment-links.ts +22 -0
- package/templates/skills/code-quality/content.md +8 -5
- package/templates/skills/download-task-attachment/content.md +2 -1
- package/{plugin/pi-skills → templates/skills}/review-offered-task/SKILL.md +5 -5
- package/templates/skills/review-offered-task/config.json +14 -0
- package/{plugin/commands/review-offered-task.md → templates/skills/review-offered-task/content.md} +3 -8
- package/templates/skills/scheduling/content.md +4 -0
- package/templates/skills/slack-interaction/content.md +2 -2
- package/templates/skills/work-on-task/SKILL.md +16 -0
- package/templates/skills/work-on-task/config.json +14 -0
- package/templates/skills/work-on-task/content.md +11 -0
- package/dist/assistant-0qt42nzg.js +0 -212
- package/dist/cli-fs1kkqgr.js +0 -317
- package/dist/cli-jnf5zghe.js +0 -269
- package/dist/cli-n1q8q14d.js +0 -177
- package/dist/handlers-yfp1qn81.js +0 -592
- package/plugin/commands/work-on-task.md +0 -39
- package/plugin/pi-skills/work-on-task/SKILL.md +0 -39
package/src/slack/assistant.ts
CHANGED
|
@@ -2,10 +2,17 @@ import { Assistant } from "@slack/bolt";
|
|
|
2
2
|
import { getAgentWorkingOnThread, getLeadAgent, getMostRecentTaskInThread } from "../be/db";
|
|
3
3
|
import { resolveTemplate } from "../prompts/resolver";
|
|
4
4
|
import { slackContextKey } from "../tasks/context-key";
|
|
5
|
-
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
6
5
|
import { ackSlackMessage, reactionName } from "./ack";
|
|
7
6
|
import { resolveSlackUserId, rewriteSlackMentions } from "./enrich";
|
|
8
7
|
import { wasEventSeen } from "./event-dedup";
|
|
8
|
+
import type { SlackFile } from "./files";
|
|
9
|
+
import {
|
|
10
|
+
bufferedFileFailures,
|
|
11
|
+
buildEffectiveText,
|
|
12
|
+
createSlackTaskWithFiles,
|
|
13
|
+
fetchSlackFiles,
|
|
14
|
+
notifySlackFileFailures,
|
|
15
|
+
} from "./inbound-files";
|
|
9
16
|
import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
|
|
10
17
|
import { hasOtherUserMention } from "./router";
|
|
11
18
|
import { bufferThreadMessage, getBufferMessageCount } from "./thread-buffer";
|
|
@@ -80,12 +87,10 @@ export function createAssistant(): Assistant {
|
|
|
80
87
|
const threadTs = (msg.thread_ts as string) || message.ts;
|
|
81
88
|
const channelId = message.channel;
|
|
82
89
|
const messageText = (msg.text as string) || "";
|
|
90
|
+
// `file_share` messages land here too: an image sent with no caption
|
|
91
|
+
// arrives with empty `text` and everything in `files`.
|
|
92
|
+
const files = msg.files as SlackFile[] | undefined;
|
|
83
93
|
const userId = (msg.user as string) || "";
|
|
84
|
-
// Any in-body `<@U…>` mention the requester typed is rewritten via
|
|
85
|
-
// the identity primitive before it reaches agent-visible task text —
|
|
86
|
-
// never a raw Slack ID. Bot-mention routing checks below use the
|
|
87
|
-
// raw `messageText`, not this rendered copy.
|
|
88
|
-
const renderedMessageText = await rewriteSlackMentions(messageText);
|
|
89
94
|
|
|
90
95
|
// Resolve the bot's own Slack user ID (cached after first call) so we can
|
|
91
96
|
// check whether this message is actually addressed to us.
|
|
@@ -123,30 +128,55 @@ export function createAssistant(): Assistant {
|
|
|
123
128
|
// 1. Check if an agent is already working in this thread
|
|
124
129
|
const workingAgent = await getAgentWorkingOnThread(channelId, threadTs);
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
// Follow-up message → route to the same agent. Buffered follow-ups
|
|
132
|
+
// carry the `[File: …]` lines only; their files are not attached.
|
|
133
|
+
if (workingAgent && workingAgent.status !== "offline" && isAdditiveSlack()) {
|
|
134
|
+
const unattached = bufferedFileFailures(files);
|
|
135
|
+
bufferThreadMessage(
|
|
136
|
+
channelId,
|
|
137
|
+
threadTs,
|
|
138
|
+
buildEffectiveText(messageText, files, unattached),
|
|
139
|
+
userId,
|
|
140
|
+
message.ts,
|
|
141
|
+
);
|
|
142
|
+
const count = getBufferMessageCount(`${channelId}:${threadTs}`);
|
|
143
|
+
const event = count === 1 ? "accepted" : "buffered";
|
|
144
|
+
await ackSlackMessage(client, channelId, message.ts, reactionName(event), event);
|
|
145
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
146
|
+
await safeSetStatus("Queuing follow-up...");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fetch shared files before the task text is final: a file that can't be
|
|
151
|
+
// downloaded is flagged in it.
|
|
152
|
+
if (files?.length) await safeSetStatus("Downloading attachments...");
|
|
153
|
+
await using inbound = await fetchSlackFiles(client, files);
|
|
154
|
+
// Any in-body `<@U…>` mention the requester typed is rewritten via
|
|
155
|
+
// the identity primitive before it reaches agent-visible task text —
|
|
156
|
+
// never a raw Slack ID. Bot-mention routing checks above use the
|
|
157
|
+
// raw `messageText`, not this rendered copy.
|
|
158
|
+
const renderedMessageText = await rewriteSlackMentions(
|
|
159
|
+
buildEffectiveText(messageText, inbound.files, inbound.failed),
|
|
160
|
+
);
|
|
136
161
|
|
|
162
|
+
if (workingAgent && workingAgent.status !== "offline") {
|
|
137
163
|
// Otherwise, create a follow-up task for the working agent
|
|
138
164
|
const latestTask = await getMostRecentTaskInThread(channelId, threadTs);
|
|
139
|
-
const task = await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
const { task, unattached } = await createSlackTaskWithFiles(
|
|
166
|
+
renderedMessageText,
|
|
167
|
+
{
|
|
168
|
+
agentId: workingAgent.id,
|
|
169
|
+
source: "slack",
|
|
170
|
+
slackChannelId: channelId,
|
|
171
|
+
slackThreadTs: threadTs,
|
|
172
|
+
slackTriggerMessageTs: message.ts,
|
|
173
|
+
slackUserId: userId,
|
|
174
|
+
parentTaskId: latestTask?.id,
|
|
175
|
+
requestedByUserId,
|
|
176
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
177
|
+
},
|
|
178
|
+
inbound,
|
|
179
|
+
);
|
|
150
180
|
await ackSlackMessage(
|
|
151
181
|
client,
|
|
152
182
|
channelId,
|
|
@@ -154,6 +184,7 @@ export function createAssistant(): Assistant {
|
|
|
154
184
|
reactionName("accepted"),
|
|
155
185
|
"accepted",
|
|
156
186
|
);
|
|
187
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
157
188
|
|
|
158
189
|
if (isSlackRenderV2Enabled()) await ensureSlackThreadTree([task.id]);
|
|
159
190
|
|
|
@@ -182,15 +213,19 @@ export function createAssistant(): Assistant {
|
|
|
182
213
|
const lead = await getLeadAgent();
|
|
183
214
|
if (!lead) {
|
|
184
215
|
// No lead — still queue the task
|
|
185
|
-
const task = await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
const { task, unattached } = await createSlackTaskWithFiles(
|
|
217
|
+
renderedMessageText + channelContext,
|
|
218
|
+
{
|
|
219
|
+
source: "slack",
|
|
220
|
+
slackChannelId: channelId,
|
|
221
|
+
slackThreadTs: threadTs,
|
|
222
|
+
slackTriggerMessageTs: message.ts,
|
|
223
|
+
slackUserId: userId,
|
|
224
|
+
requestedByUserId,
|
|
225
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
226
|
+
},
|
|
227
|
+
inbound,
|
|
228
|
+
);
|
|
194
229
|
await ackSlackMessage(
|
|
195
230
|
client,
|
|
196
231
|
channelId,
|
|
@@ -198,6 +233,7 @@ export function createAssistant(): Assistant {
|
|
|
198
233
|
reactionName("accepted"),
|
|
199
234
|
"accepted",
|
|
200
235
|
);
|
|
236
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
201
237
|
if (isSlackRenderV2Enabled()) {
|
|
202
238
|
await ensureSlackThreadTree([task.id]);
|
|
203
239
|
} else {
|
|
@@ -207,17 +243,22 @@ export function createAssistant(): Assistant {
|
|
|
207
243
|
return;
|
|
208
244
|
}
|
|
209
245
|
|
|
210
|
-
const task = await
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
246
|
+
const { task, unattached } = await createSlackTaskWithFiles(
|
|
247
|
+
renderedMessageText + channelContext,
|
|
248
|
+
{
|
|
249
|
+
agentId: lead.id,
|
|
250
|
+
source: "slack",
|
|
251
|
+
slackChannelId: channelId,
|
|
252
|
+
slackThreadTs: threadTs,
|
|
253
|
+
slackTriggerMessageTs: message.ts,
|
|
254
|
+
slackUserId: userId,
|
|
255
|
+
requestedByUserId,
|
|
256
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
257
|
+
},
|
|
258
|
+
inbound,
|
|
259
|
+
);
|
|
220
260
|
await ackSlackMessage(client, channelId, message.ts, reactionName("accepted"), "accepted");
|
|
261
|
+
await notifySlackFileFailures(client, channelId, threadTs, unattached);
|
|
221
262
|
if (isSlackRenderV2Enabled()) await ensureSlackThreadTree([task.id]);
|
|
222
263
|
// setStatus shows typing indicator — watcher will post final result when done
|
|
223
264
|
} catch (error) {
|
|
@@ -17,15 +17,13 @@ import { createAssistant } from "./assistant";
|
|
|
17
17
|
import * as slackEnrichModule from "./enrich";
|
|
18
18
|
import type { SlackFile } from "./files";
|
|
19
19
|
import {
|
|
20
|
-
buildAttachmentText,
|
|
21
|
-
buildEffectiveText,
|
|
22
20
|
checkUserAccess,
|
|
23
|
-
formatFileSize,
|
|
24
21
|
isBotMessage,
|
|
25
22
|
isSwarmThreadRoot,
|
|
26
23
|
registerMessageHandler,
|
|
27
24
|
type UserFilterConfig,
|
|
28
25
|
} from "./handlers";
|
|
26
|
+
import { buildAttachmentText, buildEffectiveText, formatFileSize } from "./inbound-files";
|
|
29
27
|
import { SLACK_REACTION_CONFIG_KEYS } from "./reaction-shortcode";
|
|
30
28
|
|
|
31
29
|
describe("checkUserAccess", () => {
|
package/src/slack/handlers.ts
CHANGED
|
@@ -9,13 +9,20 @@ import {
|
|
|
9
9
|
} from "../be/db";
|
|
10
10
|
import { resolveTemplate } from "../prompts/resolver";
|
|
11
11
|
import { slackContextKey } from "../tasks/context-key";
|
|
12
|
-
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
13
12
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
14
13
|
import { ackSlackMessage, reactionName } from "./ack";
|
|
15
14
|
import { buildTreeBlocks, type TreeNode } from "./blocks";
|
|
16
15
|
import { enrichSlackUserEmail, resolveSlackUserId, rewriteSlackMentions } from "./enrich";
|
|
17
16
|
import { wasEventSeen } from "./event-dedup";
|
|
18
17
|
import type { SlackFile } from "./files";
|
|
18
|
+
import {
|
|
19
|
+
bufferedFileFailures,
|
|
20
|
+
buildEffectiveText,
|
|
21
|
+
createSlackTaskWithFiles,
|
|
22
|
+
fetchSlackFiles,
|
|
23
|
+
notifySlackFileFailures,
|
|
24
|
+
type SlackFileFailure,
|
|
25
|
+
} from "./inbound-files";
|
|
19
26
|
import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
|
|
20
27
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
21
28
|
import "./templates";
|
|
@@ -336,45 +343,6 @@ async function getThreadContext(
|
|
|
336
343
|
}
|
|
337
344
|
}
|
|
338
345
|
|
|
339
|
-
/**
|
|
340
|
-
* Format a file size in bytes to a human-readable string.
|
|
341
|
-
*/
|
|
342
|
-
export function formatFileSize(bytes: number): string {
|
|
343
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
344
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
345
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
346
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Build a text representation of file attachments for inclusion in messages.
|
|
351
|
-
* Each file is formatted as: [File: filename.ext (mimetype, size) id=FILE_ID]
|
|
352
|
-
*/
|
|
353
|
-
export function buildAttachmentText(files: SlackFile[]): string {
|
|
354
|
-
return files
|
|
355
|
-
.map((f) => `[File: ${f.name} (${f.mimetype}, ${formatFileSize(f.size)}) id=${f.id}]`)
|
|
356
|
-
.join("\n");
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Build the effective message text from the original text and any file attachments.
|
|
361
|
-
* - Text only: returns the text as-is
|
|
362
|
-
* - Files only: returns the attachment metadata
|
|
363
|
-
* - Both: returns text followed by attachment metadata
|
|
364
|
-
*/
|
|
365
|
-
export function buildEffectiveText(text: string | undefined, files?: SlackFile[]): string {
|
|
366
|
-
const hasText = !!text?.trim();
|
|
367
|
-
const hasFiles = files && files.length > 0;
|
|
368
|
-
|
|
369
|
-
if (hasText && hasFiles) {
|
|
370
|
-
return `${text}\n\n${buildAttachmentText(files)}`;
|
|
371
|
-
}
|
|
372
|
-
if (hasFiles) {
|
|
373
|
-
return buildAttachmentText(files);
|
|
374
|
-
}
|
|
375
|
-
return text || "";
|
|
376
|
-
}
|
|
377
|
-
|
|
378
346
|
// Message deduplication (prevents duplicate event processing)
|
|
379
347
|
const processedMessages = new Set<string>();
|
|
380
348
|
const MESSAGE_DEDUP_TTL = 60_000; // 1 minute
|
|
@@ -501,9 +469,11 @@ export function registerMessageHandler(app: App): void {
|
|
|
501
469
|
// Check if bot was mentioned (in original text only)
|
|
502
470
|
const botMentioned = !!msg.text?.includes(`<@${botUserId}>`);
|
|
503
471
|
|
|
504
|
-
// Detect assistant thread context
|
|
505
|
-
//
|
|
506
|
-
//
|
|
472
|
+
// Detect assistant thread context. Bolt's Assistant middleware claims DM thread
|
|
473
|
+
// messages with no subtype or `file_share` (`isAssistantMessage` in @slack/bolt), so
|
|
474
|
+
// those — file uploads included — are handled in assistant.ts. An assistant-thread
|
|
475
|
+
// message that still lands here is treated as an implicit mention so it routes to
|
|
476
|
+
// the lead agent rather than being silently dropped.
|
|
507
477
|
// Guard: suppress implicit mention when the message @-mentions someone else but NOT us —
|
|
508
478
|
// those messages are addressed to a different agent/user (e.g. Devin) and must not spawn.
|
|
509
479
|
const isAssistantThread = !!msg.assistant_thread;
|
|
@@ -517,7 +487,11 @@ export function registerMessageHandler(app: App): void {
|
|
|
517
487
|
false,
|
|
518
488
|
);
|
|
519
489
|
if (additiveSlack && msg.thread_ts) {
|
|
520
|
-
|
|
490
|
+
// A !now follow-up is buffered too: its files ride along as marked `[File: …]` lines only.
|
|
491
|
+
const unattached = bufferedFileFailures(msg.files);
|
|
492
|
+
const stripped = buildEffectiveText(msg.text, msg.files, unattached)
|
|
493
|
+
.replace(/<@[A-Z0-9]+>/g, "")
|
|
494
|
+
.trim();
|
|
521
495
|
if (
|
|
522
496
|
stripped.startsWith("!now") &&
|
|
523
497
|
(await hasSwarmThreadActivity(client, msg.channel, msg.thread_ts))
|
|
@@ -537,6 +511,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
537
511
|
await instantFlush(threadKey);
|
|
538
512
|
|
|
539
513
|
await ackSlackMessage(client, msg.channel, msg.ts, reactionName("now"), "now");
|
|
514
|
+
await notifySlackFileFailures(client, msg.channel, msg.thread_ts, unattached);
|
|
540
515
|
|
|
541
516
|
return;
|
|
542
517
|
}
|
|
@@ -563,7 +538,15 @@ export function registerMessageHandler(app: App): void {
|
|
|
563
538
|
|
|
564
539
|
if (hasSwarmActivity) {
|
|
565
540
|
const threadKey = `${msg.channel}:${msg.thread_ts}`;
|
|
566
|
-
|
|
541
|
+
// Buffered follow-ups carry the `[File: …]` lines only; their files are not attached.
|
|
542
|
+
const unattached = bufferedFileFailures(msg.files);
|
|
543
|
+
bufferThreadMessage(
|
|
544
|
+
msg.channel,
|
|
545
|
+
msg.thread_ts,
|
|
546
|
+
buildEffectiveText(msg.text, msg.files, unattached),
|
|
547
|
+
msg.user,
|
|
548
|
+
msg.ts,
|
|
549
|
+
);
|
|
567
550
|
|
|
568
551
|
// Slack feedback: react with the accepted reaction on first buffer, buffered on appends
|
|
569
552
|
const count = getBufferMessageCount(threadKey);
|
|
@@ -571,6 +554,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
571
554
|
const name = reactionName(event);
|
|
572
555
|
console.log(`[Slack] Additive buffer: ${threadKey} (message #${count}, reaction: ${name})`);
|
|
573
556
|
await ackSlackMessage(client, msg.channel, msg.ts, name, event);
|
|
557
|
+
await notifySlackFileFailures(client, msg.channel, msg.thread_ts, unattached);
|
|
574
558
|
|
|
575
559
|
return; // Don't process further — buffer will flush
|
|
576
560
|
}
|
|
@@ -602,9 +586,11 @@ export function registerMessageHandler(app: App): void {
|
|
|
602
586
|
return;
|
|
603
587
|
}
|
|
604
588
|
|
|
589
|
+
await using inbound = await fetchSlackFiles(client, msg.files);
|
|
590
|
+
const taskText = buildEffectiveText(msg.text, inbound.files, inbound.failed);
|
|
605
591
|
const taskDescription = isImplicitMention
|
|
606
|
-
?
|
|
607
|
-
: extractTaskFromMessage(
|
|
592
|
+
? taskText
|
|
593
|
+
: extractTaskFromMessage(taskText, botUserId);
|
|
608
594
|
if (!taskDescription) {
|
|
609
595
|
if (!isImplicitMention && !isSlackRenderV2Enabled()) {
|
|
610
596
|
await say({
|
|
@@ -634,17 +620,22 @@ export function registerMessageHandler(app: App): void {
|
|
|
634
620
|
}
|
|
635
621
|
|
|
636
622
|
const lead = await getLeadAgent();
|
|
637
|
-
const task = await
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
623
|
+
const { task, unattached } = await createSlackTaskWithFiles(
|
|
624
|
+
fullTaskDescription,
|
|
625
|
+
{
|
|
626
|
+
agentId: lead?.id,
|
|
627
|
+
source: "slack",
|
|
628
|
+
slackChannelId: msg.channel,
|
|
629
|
+
slackThreadTs: threadTs,
|
|
630
|
+
slackTriggerMessageTs: msg.ts,
|
|
631
|
+
slackUserId: msg.user,
|
|
632
|
+
requestedByUserId,
|
|
633
|
+
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
634
|
+
},
|
|
635
|
+
inbound,
|
|
636
|
+
);
|
|
647
637
|
await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
|
|
638
|
+
await notifySlackFileFailures(client, msg.channel, threadTs, unattached);
|
|
648
639
|
|
|
649
640
|
if (isSlackRenderV2Enabled()) {
|
|
650
641
|
await ensureSlackThreadTree([task.id]);
|
|
@@ -668,8 +659,12 @@ export function registerMessageHandler(app: App): void {
|
|
|
668
659
|
return;
|
|
669
660
|
}
|
|
670
661
|
|
|
671
|
-
// Extract task description (
|
|
672
|
-
|
|
662
|
+
// Extract task description (the text plus a `[File: …]` line per attachment)
|
|
663
|
+
await using inbound = await fetchSlackFiles(client, msg.files);
|
|
664
|
+
const taskDescription = extractTaskFromMessage(
|
|
665
|
+
buildEffectiveText(msg.text, inbound.files, inbound.failed),
|
|
666
|
+
botUserId,
|
|
667
|
+
);
|
|
673
668
|
if (!taskDescription) {
|
|
674
669
|
if (!isSlackRenderV2Enabled()) {
|
|
675
670
|
await say({
|
|
@@ -706,6 +701,7 @@ export function registerMessageHandler(app: App): void {
|
|
|
706
701
|
steered: Array<{ agentName: string; acknowledgement: string }>;
|
|
707
702
|
failed: Array<{ agentName: string; reason: string }>;
|
|
708
703
|
} = { assigned: [], queued: [], steered: [], failed: [] };
|
|
704
|
+
const unattached: SlackFileFailure[] = [];
|
|
709
705
|
|
|
710
706
|
for (const match of matches) {
|
|
711
707
|
const agent = await getAgentById(match.agent.id);
|
|
@@ -718,15 +714,18 @@ export function registerMessageHandler(app: App): void {
|
|
|
718
714
|
try {
|
|
719
715
|
const latestTask = await getMostRecentTaskInThread(msg.channel, threadTs);
|
|
720
716
|
if (agent.isLead) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
717
|
+
// Steering only carries text into the running session; a message with
|
|
718
|
+
// files becomes a follow-up task so the files land as attachments.
|
|
719
|
+
const steering =
|
|
720
|
+
msg.thread_ts && inbound.files.length === 0
|
|
721
|
+
? await requestSlackThreadSteering({
|
|
722
|
+
channelId: msg.channel,
|
|
723
|
+
threadTs,
|
|
724
|
+
message: taskDescription,
|
|
725
|
+
messageTimestamps: [msg.ts],
|
|
726
|
+
requestedByUserId,
|
|
727
|
+
})
|
|
728
|
+
: null;
|
|
730
729
|
if (steering) {
|
|
731
730
|
await ackSlackMessage(client, msg.channel, msg.ts, reactionName("steered"), "steered");
|
|
732
731
|
results.steered.push({
|
|
@@ -736,33 +735,45 @@ export function registerMessageHandler(app: App): void {
|
|
|
736
735
|
continue;
|
|
737
736
|
}
|
|
738
737
|
|
|
739
|
-
const
|
|
738
|
+
const created = await createSlackTaskWithFiles(
|
|
739
|
+
fullTaskDescription,
|
|
740
|
+
{
|
|
741
|
+
agentId: agent.id,
|
|
742
|
+
source: "slack",
|
|
743
|
+
slackChannelId: msg.channel,
|
|
744
|
+
slackThreadTs: threadTs,
|
|
745
|
+
slackTriggerMessageTs: msg.ts,
|
|
746
|
+
slackUserId: msg.user,
|
|
747
|
+
parentTaskId: latestTask?.id,
|
|
748
|
+
requestedByUserId,
|
|
749
|
+
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
750
|
+
},
|
|
751
|
+
inbound,
|
|
752
|
+
);
|
|
753
|
+
const { task } = created;
|
|
754
|
+
unattached.push(...created.unattached);
|
|
755
|
+
await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
|
|
756
|
+
results.assigned.push({ agentName: agent.name, taskId: task.id });
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Workers receive tasks as before
|
|
761
|
+
const created = await createSlackTaskWithFiles(
|
|
762
|
+
fullTaskDescription,
|
|
763
|
+
{
|
|
740
764
|
agentId: agent.id,
|
|
741
765
|
source: "slack",
|
|
742
766
|
slackChannelId: msg.channel,
|
|
743
767
|
slackThreadTs: threadTs,
|
|
744
768
|
slackTriggerMessageTs: msg.ts,
|
|
745
769
|
slackUserId: msg.user,
|
|
746
|
-
parentTaskId: latestTask?.id,
|
|
747
770
|
requestedByUserId,
|
|
748
771
|
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
// Workers receive tasks as before
|
|
756
|
-
const task = await createTaskWithSiblingAwareness(fullTaskDescription, {
|
|
757
|
-
agentId: agent.id,
|
|
758
|
-
source: "slack",
|
|
759
|
-
slackChannelId: msg.channel,
|
|
760
|
-
slackThreadTs: threadTs,
|
|
761
|
-
slackTriggerMessageTs: msg.ts,
|
|
762
|
-
slackUserId: msg.user,
|
|
763
|
-
requestedByUserId,
|
|
764
|
-
contextKey: slackContextKey({ channelId: msg.channel, threadTs }),
|
|
765
|
-
});
|
|
772
|
+
},
|
|
773
|
+
inbound,
|
|
774
|
+
);
|
|
775
|
+
const { task } = created;
|
|
776
|
+
unattached.push(...created.unattached);
|
|
766
777
|
await ackSlackMessage(client, msg.channel, msg.ts, reactionName("accepted"), "accepted");
|
|
767
778
|
|
|
768
779
|
// Check if agent has an in-progress task in this thread (queued follow-up)
|
|
@@ -781,6 +792,10 @@ export function registerMessageHandler(app: App): void {
|
|
|
781
792
|
}
|
|
782
793
|
}
|
|
783
794
|
|
|
795
|
+
if (results.assigned.length + results.queued.length > 0) {
|
|
796
|
+
await notifySlackFileFailures(client, msg.channel, threadTs, unattached);
|
|
797
|
+
}
|
|
798
|
+
|
|
784
799
|
// Send consolidated summary as initial tree with Block Kit
|
|
785
800
|
const totalResults = results.assigned.length + results.queued.length + results.failed.length;
|
|
786
801
|
if (totalResults > 0) {
|