@desplega.ai/agent-swarm 2.7.2 → 2.7.3
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 +6 -0
- package/dist/{acp-adapter-624yge7a.js → acp-adapter-pdzw76ja.js} +1 -1
- package/dist/{actions-rd9xn281.js → actions-a90cfpca.js} +21 -21
- package/dist/{amp-adapter-k27hc00p.js → amp-adapter-4vtywcjk.js} +2 -2
- package/dist/{app-sjm3xc7g.js → app-8k68gygk.js} +3 -3
- package/dist/{assistant-6jbzs8wa.js → assistant-ck01q2yg.js} +32 -21
- package/dist/{boot-reembed-e2348eqc.js → boot-reembed-33spavpd.js} +1 -1
- package/dist/{boot-reembed-w53pd5xt.js → boot-reembed-xzge83a2.js} +1 -1
- package/dist/{boot-scrub-sweep-cpy4yg97.js → boot-scrub-sweep-mxvb8dr3.js} +3 -3
- package/dist/{boot-seeds-f6j4t6k6.js → boot-seeds-rpm8bb5r.js} +4 -4
- package/dist/{claude-adapter-v3ac8esd.js → claude-adapter-q12bcfj9.js} +3 -3
- package/dist/{claude-managed-setup-yyrkqvtm.js → claude-managed-setup-11vb6q89.js} +1 -1
- package/dist/{claude-sdk-session-85j69v6d.js → claude-sdk-session-gkv1107j.js} +10325 -10243
- package/dist/{cli-rs59z1qx.js → cli-07pcqwaj.js} +1 -1
- package/dist/{cli-6vq3ymqm.js → cli-0tkm519n.js} +1 -1
- package/dist/{cli-s7cm1927.js → cli-0xzbk52h.js} +4 -4
- package/dist/{cli-g0zc1b40.js → cli-0zae6bs6.js} +2 -2
- package/dist/{cli-p8yrsmfn.js → cli-10kjx0dm.js} +1 -1
- package/dist/{cli-7xsq63pe.js → cli-157ykn37.js} +10 -9
- package/dist/{cli-vjcj9k2p.js → cli-2h0rfmqk.js} +1 -1
- package/dist/{cli-9wasax9f.js → cli-35nzvqaf.js} +1 -1
- package/dist/{cli-e1p7dqe1.js → cli-4aydmmdy.js} +24 -3
- package/dist/{cli-942pp3bc.js → cli-54ax65rn.js} +3 -3
- package/dist/{cli-10cp92df.js → cli-5ke7gxwr.js} +6 -6
- package/dist/{cli-femw4q81.js → cli-6151a7dr.js} +2 -2
- package/dist/{cli-3cywb44q.js → cli-77eq8txs.js} +85 -3
- package/dist/{cli-atpfk4yd.js → cli-7cwrwb5x.js} +5 -5
- package/dist/{cli-bfbpvccx.js → cli-8a1k6mat.js} +61 -49
- package/dist/{cli-c18941tx.js → cli-8ch3b2r7.js} +3 -3
- package/dist/{cli-rs2s1t05.js → cli-9fk56y8c.js} +4 -4
- package/dist/{cli-0wx63vvw.js → cli-9km88w11.js} +1 -1
- package/dist/{cli-qy0z3mfj.js → cli-9vttw9nz.js} +56 -46
- package/dist/{cli-mzgppb7q.js → cli-a0w50zw9.js} +4 -4
- package/dist/{cli-z8a4mfz1.js → cli-aqp59jmv.js} +4 -4
- package/dist/{cli-5d8wd50e.js → cli-bs0fwb23.js} +1 -1
- package/dist/{cli-fa4kxxx3.js → cli-ct5920jb.js} +2 -2
- package/dist/{cli-0awbtmv2.js → cli-dek8hqjc.js} +10 -10
- package/dist/{cli-qp6adcmw.js → cli-e24w2mvk.js} +1 -1
- package/dist/{cli-t3npd8hm.js → cli-ghfhn1qc.js} +1 -1
- package/dist/{cli-jmcf8cnz.js → cli-ja369zab.js} +1 -1
- package/dist/{cli-wtjg6je3.js → cli-jhh7tbx6.js} +1 -1
- package/dist/{cli-e19bgatr.js → cli-jmk31131.js} +2 -2
- package/dist/{cli-2cj8fqv1.js → cli-kjcdzea8.js} +1 -1
- package/dist/{cli-1xpr5992.js → cli-kjha1q9k.js} +3 -3
- package/dist/{cli-9jfty8px.js → cli-ktcdczth.js} +538 -30
- package/dist/{cli-exd52qn4.js → cli-mcmpbpcc.js} +2 -2
- package/dist/{cli-2m2p3snc.js → cli-mmf43vcp.js} +6 -6
- package/dist/{cli-b2rhbaty.js → cli-pnm99spe.js} +10 -10
- package/dist/{cli-9jn9cj0q.js → cli-v9jk2k63.js} +1 -1
- package/dist/{cli-mt7bxjfd.js → cli-vhgcwppn.js} +1 -1
- package/dist/{cli-qa4hgda3.js → cli-vvz64zc5.js} +14 -13
- package/dist/{cli-txr0z97c.js → cli-wmhajw68.js} +171 -32
- package/dist/{cli-hzrmv8pg.js → cli-x38rmhap.js} +3 -3
- package/dist/{cli-hq9hd4hp.js → cli-xz6qjdqj.js} +1 -1
- package/dist/{cli-g9ypd5en.js → cli-z52gq55b.js} +4 -0
- package/dist/{cli-2qbmjfta.js → cli-z6219sd3.js} +4 -4
- package/dist/cli.js +19 -19
- package/dist/{codex-adapter-zm1bkdde.js → codex-adapter-46kbvvkx.js} +1 -1
- package/dist/{codex-hook-tkag7fxq.js → codex-hook-tm9ecrd5.js} +1 -1
- package/dist/{codex-session-runner-8k02mqw2.js → codex-session-runner-qy93hdrv.js} +1 -1
- package/dist/{commands-nw1dn4av.js → commands-hwdhacfx.js} +2 -2
- package/dist/{controller-3kkkmps0.js → controller-hjza5p1f.js} +3 -3
- package/dist/{controllers-connect-n2c4167r.js → controllers-connect-7m25cay2.js} +20 -20
- package/dist/{cursor-adapter-h5028emw.js → cursor-adapter-1qh7ps8w.js} +1 -1
- package/dist/{db-76y2msnf.js → db-w23k9301.js} +2 -2
- package/dist/{db-8rk0p8j1.js → db-wm8by383.js} +3 -3
- package/dist/{dispatcher-xp4wsx30.js → dispatcher-en25ebgc.js} +18 -18
- package/dist/{dsh-adapter-vyrr31bn.js → dsh-adapter-zkme32vd.js} +1 -1
- package/dist/{e2b-eesfj689.js → e2b-9z9mvt2p.js} +1 -1
- package/dist/{e2b-03nha1wz.js → e2b-yhvn70es.js} +1 -1
- package/dist/{embedding-space-ejefyep1.js → embedding-space-rgf4aypt.js} +6 -6
- package/dist/{embeddings-w297xaw0.js → embeddings-z9cmxhx1.js} +1 -1
- package/dist/{external-channel-mf9vp1fe.js → external-channel-g6vv85zv.js} +2 -2
- package/dist/{grok-adapter-mpzyyprz.js → grok-adapter-f6n1tpq3.js} +2 -2
- package/dist/handlers-ss5948rq.js +42 -0
- package/dist/{heartbeat-qnaycjzs.js → heartbeat-5xcdzz45.js} +21 -21
- package/dist/{hook-h6sqfzxr.js → hook-6423rme3.js} +2 -2
- package/dist/{hook-4zqdgea4.js → hook-8dk1csdd.js} +4 -4
- package/dist/{http-65ts210h.js → http-6mfh2ww9.js} +255 -60
- package/dist/{index-sqe7f6mj.js → index-0js6bgff.js} +1 -1
- package/dist/{index-farm1zsn.js → index-0ng1dd1t.js} +10937 -9527
- package/dist/{index-k3ewf1jg.js → index-5eyn1wc5.js} +18 -18
- package/dist/{index-vp4wa3qn.js → index-71gnebsn.js} +18 -18
- package/dist/{index-hdfad00b.js → index-q5gzpjdd.js} +12 -10
- package/dist/index-z3b1vfjx.js +44 -0
- package/dist/{keepalive-52k6skha.js → keepalive-cymckeg6.js} +3 -3
- package/dist/{lead-y9rj0gr1.js → lead-baknj2dh.js} +29 -29
- package/dist/{maintenance-f86wqfq6.js → maintenance-dfrnsx3n.js} +1 -1
- package/dist/{oauth-refresh-sweep-vddv9ky7.js → oauth-refresh-sweep-5dztk8x6.js} +2 -2
- package/dist/{onboard-8z2qk6gq.js → onboard-h36vja6y.js} +11 -6
- package/dist/{otel-impl-epet9j18.js → otel-impl-jerq8vz8.js} +1 -1
- package/dist/{pricing-refresh-6wm42a51.js → pricing-refresh-t3nx5krn.js} +3 -3
- package/dist/{rbac-roles-2kfcs1sc.js → rbac-roles-dw1awawd.js} +2 -2
- package/dist/{rbac-roles-13a0vj6d.js → rbac-roles-km0fc3e8.js} +1 -1
- package/dist/{registry-ajhexe44.js → registry-hzfh268v.js} +1 -1
- package/dist/{render-v2-cy8zsfsf.js → render-v2-65x4ayj8.js} +3 -3
- package/dist/{resume-c783qe6b.js → resume-61znk3hj.js} +6 -6
- package/dist/{session-access-s4tv0az8.js → session-access-4yc9htrg.js} +4 -4
- package/dist/{session-source-backfill-qzapz0ne.js → session-source-backfill-kr5bdjtp.js} +2 -2
- package/dist/{setup-st7htx1k.js → setup-9h61yr2p.js} +1 -1
- package/dist/{work-objects-ftj4zw70.js → work-objects-x7rb53qn.js} +27 -22
- package/dist/{worker-859fk25k.js → worker-7f3khgn0.js} +29 -29
- package/openapi.json +134 -3
- package/package.json +5 -5
- package/src/be/db/agents.ts +16 -1
- package/src/be/db/runtime.ts +25 -15
- package/src/be/db/session-summaries.ts +120 -0
- package/src/be/db/tasks/read.ts +7 -4
- package/src/be/db/tasks/search-index.ts +159 -0
- package/src/be/db/tasks/write.ts +3 -0
- package/src/be/db-queries/slack-inbound.ts +77 -0
- package/src/be/db.ts +27 -8
- package/src/be/mcp-overflow-sweep.ts +39 -0
- package/src/be/migrations/253_session_summaries.sql +300 -0
- package/src/be/migrations/254_query_audit_indexes.sql +25 -0
- package/src/be/migrations/256_agent_slack_icon.sql +2 -0
- package/src/be/migrations/257_agent_tasks_fts.sql +55 -0
- package/src/comb/config.ts +6 -2
- package/src/comb/requester.ts +69 -0
- package/src/comb/review-batch.ts +24 -2
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/onboard/compose-generator.ts +7 -2
- package/src/commands/onboard/env-generator.ts +8 -3
- package/src/commands/runner.ts +41 -24
- package/src/fs/agent-fs-provider.ts +9 -0
- package/src/heartbeat/heartbeat.ts +13 -6
- package/src/http/agents.ts +10 -2
- package/src/http/index.ts +15 -0
- package/src/http/integrations.ts +20 -0
- package/src/http/list-total-cache.ts +62 -0
- package/src/http/sessions.ts +9 -2
- package/src/http/slack-inbound.ts +1 -0
- package/src/http/status.ts +4 -1
- package/src/http/tasks.ts +12 -2
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/queue-stall-alarm.ts +4 -1
- package/src/slack/actions.ts +2 -2
- package/src/slack/app-icon.ts +50 -0
- package/src/slack/app.ts +13 -1
- package/src/slack/assistant.ts +22 -1
- package/src/slack/event-dedup.test.ts +11 -0
- package/src/slack/event-dedup.ts +8 -4
- package/src/slack/inbound-dispatch.ts +58 -0
- package/src/slack/render-v2.ts +7 -8
- package/src/slack/responses.ts +21 -8
- package/src/slack/socket-ack.ts +237 -0
- package/src/slack/thread-buffer.ts +3 -5
- package/src/slack/watcher.ts +2 -2
- package/src/slack/work-objects.ts +5 -1
- package/src/tests/agent-fs-release-notes.test.ts +40 -0
- package/src/tests/agent-slack-icon.test.ts +85 -0
- package/src/tests/approval-cancellation-migration.test.ts +2 -0
- package/src/tests/asset-key-migration.test.ts +26 -0
- package/src/tests/comb-default.test.ts +159 -0
- package/src/tests/comb-review-batch.test.ts +192 -4
- package/src/tests/comb-status.test.ts +65 -2
- package/src/tests/db-agents.test.ts +13 -0
- package/src/tests/db-tasks-read.test.ts +10 -15
- package/src/tests/deploy-one-box.test.ts +161 -0
- package/src/tests/deploy-stubs.ts +68 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/e2e-codex-auth-chain.test.ts +18 -1
- package/src/tests/fixtures/deploy-stubs/bun +3 -0
- package/src/tests/fixtures/deploy-stubs/bunx +27 -0
- package/src/tests/fixtures/deploy-stubs/curl +26 -0
- package/src/tests/fixtures/deploy-stubs/docker +122 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +5 -5
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +4 -4
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +12 -12
- package/src/tests/fixtures/onboard/full.compose.yml.txt +12 -12
- package/src/tests/fixtures/onboard/research.compose.yml.txt +4 -4
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +2 -2
- package/src/tests/heartbeat-expiry.test.ts +151 -25
- package/src/tests/http-api-integration.test.ts +31 -0
- package/src/tests/human-free-reclassify.test.ts +6 -0
- package/src/tests/install-sh-bind.test.ts +242 -0
- package/src/tests/install-sh-tunnel.test.ts +89 -0
- package/src/tests/kv-storage.test.ts +28 -0
- package/src/tests/list-endpoint-slimming.test.ts +3 -0
- package/src/tests/list-total-cache.test.ts +63 -0
- package/src/tests/mcp-overflow-sweep.test.ts +84 -0
- package/src/tests/onboard-compose.test.ts +17 -3
- package/src/tests/onboard-env.test.ts +13 -0
- package/src/tests/onboarding.test.ts +1 -1
- package/src/tests/preload.ts +1 -0
- package/src/tests/query-audit-plans.test.ts +314 -0
- package/src/tests/script-connections.test.ts +6 -0
- package/src/tests/session-summaries.test.ts +384 -0
- package/src/tests/sessions-tree.test.ts +5 -1
- package/src/tests/slack-app-icon.test.ts +90 -0
- package/src/tests/slack-external-channel-egress.test.ts +1 -1
- package/src/tests/slack-inbound-receipts.test.ts +34 -0
- package/src/tests/slack-inline-output.test.ts +29 -2
- package/src/tests/slack-render-v2-query-plans.test.ts +3 -2
- package/src/tests/slack-render-v2.test.ts +35 -0
- package/src/tests/slack-socket-ack.test.ts +553 -0
- package/src/tests/task-lineage.test.ts +3 -0
- package/src/tests/task-search-filter.test.ts +4 -4
- package/src/tests/task-search-fts.test.ts +320 -0
- package/src/tests/update-profile-auth.test.ts +28 -0
- package/src/tools/get-swarm.ts +1 -0
- package/src/tools/get-tasks.ts +6 -1
- package/src/tools/join-swarm.ts +1 -0
- package/src/tools/my-agent-info.ts +1 -0
- package/src/tools/slack-post.ts +2 -2
- package/src/tools/slack-reply.ts +2 -2
- package/src/tools/slack-start-thread.ts +2 -2
- package/src/tools/slack-update.ts +1 -1
- package/src/tools/update-profile.ts +20 -4
- package/src/tools/utils.ts +3 -4
- package/src/types.ts +47 -0
- package/src/utils/constants.ts +127 -5
- package/templates/official/lead/HEARTBEAT.md +4 -2
- package/templates/official/lead/config.json +2 -2
- package/templates/skills/swarm-docs/content.md +2 -0
- package/dist/handlers-7x8sywgr.js +0 -42
- package/dist/index-mc30r1hs.js +0 -44
|
@@ -45,6 +45,17 @@ describe("wasEventSeen (production cache)", () => {
|
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
describe("isolated test cache (TTL behavior)", () => {
|
|
48
|
+
test("default TTL still drops a redelivery after Slack's 24h delayed-retry window", () => {
|
|
49
|
+
const cache = _createTestCache();
|
|
50
|
+
try {
|
|
51
|
+
expect(cache.wasEventSeen("Ev_late")).toBe(false);
|
|
52
|
+
cache.advance(24 * 60 * 60 * 1000); // last delayed-events retry
|
|
53
|
+
expect(cache.wasEventSeen("Ev_late")).toBe(true);
|
|
54
|
+
} finally {
|
|
55
|
+
cache.destroy();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
48
59
|
test("entry expires after TTL elapses", () => {
|
|
49
60
|
const cache = _createTestCache(1000); // 1s TTL
|
|
50
61
|
try {
|
package/src/slack/event-dedup.ts
CHANGED
|
@@ -13,15 +13,19 @@
|
|
|
13
13
|
* This module exposes a single in-memory check-and-insert that returns `false`
|
|
14
14
|
* the first time we see an event_id (caller should proceed) and `true` on
|
|
15
15
|
* subsequent retries within the TTL window — i.e. it answers "was this event
|
|
16
|
-
* already seen?" (default TTL
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* already seen?" (default TTL 25h). The TTL must outlive the last possible
|
|
17
|
+
* redelivery: Slack's standard schedule ends after ~5 min, but the optional
|
|
18
|
+
* Delayed Events feature retries hourly for 24h. The socket-ack wrapper
|
|
19
|
+
* (`socket-ack.ts`) runs listeners for an event whose ack failed, so this cache
|
|
20
|
+
* is what drops the later redelivery. Entries are an id and a number, so the
|
|
21
|
+
* long TTL costs little memory.
|
|
19
22
|
*
|
|
20
23
|
* Single-pod-only. The API server (which owns the Slack socket) runs as a
|
|
21
24
|
* single API process; if that ever changes, swap this for a DB-backed table.
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
|
-
const
|
|
27
|
+
export const EVENT_DEDUP_TTL_MS = 25 * 60 * 60 * 1000; // 24h delayed-retry window + 1h margin
|
|
28
|
+
const DEFAULT_TTL_MS = EVENT_DEDUP_TTL_MS;
|
|
25
29
|
const CLEANUP_INTERVAL_MS = 60_000; // 1 minute
|
|
26
30
|
|
|
27
31
|
interface DedupCache {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getNextSlackInboundAvailableAt,
|
|
10
10
|
getSlackInboundReceiptStats,
|
|
11
11
|
markInterruptedSlackInboundReceiptsUncertain,
|
|
12
|
+
recordSlackInboundInline,
|
|
12
13
|
releaseSlackInboundReceipt,
|
|
13
14
|
SLACK_INBOUND_LIMITS,
|
|
14
15
|
type SlackInboundAdmission,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
type SlackInboundReceipt,
|
|
17
18
|
type SlackInboundReceiptStats,
|
|
18
19
|
type SlackInboundTransport,
|
|
20
|
+
touchSlackInboundDuplicate,
|
|
19
21
|
} from "../be/db-queries/slack-inbound";
|
|
20
22
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
21
23
|
import { getSlackConfiguration, type SlackMode } from "./config";
|
|
@@ -128,6 +130,8 @@ export function resolveSlackInboundOutcome(
|
|
|
128
130
|
// ─── Outcome counters (Socket Mode is observed, not stored) ─────────────────
|
|
129
131
|
|
|
130
132
|
type OutcomeCounters = Record<SlackInboundOutcomeState, number> & {
|
|
133
|
+
/** Socket acks that failed because the client was not ready; the event still ran. */
|
|
134
|
+
ackNotReady: number;
|
|
131
135
|
lastOutcomeAt: string | null;
|
|
132
136
|
lastFailure: { code: string; at: string } | null;
|
|
133
137
|
};
|
|
@@ -138,6 +142,7 @@ function emptyCounters(): OutcomeCounters {
|
|
|
138
142
|
ignored: 0,
|
|
139
143
|
failed: 0,
|
|
140
144
|
uncertain: 0,
|
|
145
|
+
ackNotReady: 0,
|
|
141
146
|
lastOutcomeAt: null,
|
|
142
147
|
lastFailure: null,
|
|
143
148
|
};
|
|
@@ -159,6 +164,11 @@ function recordOutcome(transport: SlackInboundTransport, outcome: SlackInboundOu
|
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
|
|
167
|
+
/** Count a Socket Mode ack that failed because the client was not ready. */
|
|
168
|
+
export function recordSlackSocketAckNotReady(): void {
|
|
169
|
+
counters.socket.ackNotReady += 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
162
172
|
/** Test-only. */
|
|
163
173
|
export function resetSlackInboundCountersForTests(): void {
|
|
164
174
|
counters.socket = emptyCounters();
|
|
@@ -290,6 +300,54 @@ export function computeSlackInboundKey(
|
|
|
290
300
|
};
|
|
291
301
|
}
|
|
292
302
|
|
|
303
|
+
/** Dedup key for a Socket Mode `event_callback` body, or null for any other envelope. */
|
|
304
|
+
function socketEventKey(body: unknown): Extract<SlackInboundKeyResult, { ok: true }> | null {
|
|
305
|
+
if ((body as { type?: unknown } | null)?.type !== "event_callback") return null;
|
|
306
|
+
const key = computeSlackInboundKey("event", "", body);
|
|
307
|
+
return key.ok ? key : null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Is this Socket Mode event already stored as a receipt (admitted after a
|
|
312
|
+
* failed ack)? Counts the sighting. A stored receipt is owned by the drain, so
|
|
313
|
+
* a redelivery needs no second run.
|
|
314
|
+
*/
|
|
315
|
+
export async function isSlackSocketEventStored(
|
|
316
|
+
body: unknown,
|
|
317
|
+
now: Date = new Date(),
|
|
318
|
+
): Promise<boolean> {
|
|
319
|
+
const key = socketEventKey(body);
|
|
320
|
+
return key ? touchSlackInboundDuplicate(key.dedupKey, now) : false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Remember a Socket Mode event that runs inline after a failed ack, so its
|
|
325
|
+
* redelivery is dropped by `isSlackSocketEventStored` even after a restart.
|
|
326
|
+
* Stores the dedup key only, never the payload. `invalid` when the envelope has
|
|
327
|
+
* no usable key (nothing to remember, the caller still runs it).
|
|
328
|
+
*/
|
|
329
|
+
export async function recordSlackSocketEventRanInline(
|
|
330
|
+
body: unknown,
|
|
331
|
+
retry: { retryNum?: number | null; retryReason?: string | null } = {},
|
|
332
|
+
now: Date = new Date(),
|
|
333
|
+
): Promise<"recorded" | "duplicate" | "invalid"> {
|
|
334
|
+
const key = socketEventKey(body);
|
|
335
|
+
if (!key) return "invalid";
|
|
336
|
+
return recordSlackInboundInline(
|
|
337
|
+
{
|
|
338
|
+
dedupKey: key.dedupKey,
|
|
339
|
+
transport: "socket",
|
|
340
|
+
kind: "event",
|
|
341
|
+
payloadType: key.payloadType,
|
|
342
|
+
apiAppId: key.apiAppId,
|
|
343
|
+
eventId: key.eventId,
|
|
344
|
+
retryNum: retry.retryNum,
|
|
345
|
+
retryReason: retry.retryReason,
|
|
346
|
+
},
|
|
347
|
+
now,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
293
351
|
export interface AdmitSlackInboundInput {
|
|
294
352
|
transport: SlackInboundTransport;
|
|
295
353
|
kind: SlackInboundKind;
|
package/src/slack/render-v2.ts
CHANGED
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
} from "./blocks";
|
|
53
53
|
import { buildAskClosure, type ClosureState, closureState } from "./closure";
|
|
54
54
|
import { completedStatusIcon, reactionName, type SlackReactionEvent } from "./reaction-shortcode";
|
|
55
|
-
import { getAgentDisplayName,
|
|
55
|
+
import { getAgentDisplayName, getAgentSlackIcon } from "./responses";
|
|
56
56
|
import {
|
|
57
57
|
_resetSlackSessionStatusForTests,
|
|
58
58
|
beginSlackStatusTick,
|
|
@@ -675,7 +675,7 @@ async function createThreadTree(task: AgentTask): Promise<SlackMessageRecord | n
|
|
|
675
675
|
blocks: treeBlocks(text),
|
|
676
676
|
unfurl_links: false,
|
|
677
677
|
unfurl_media: false,
|
|
678
|
-
...(agent ? { username: getAgentDisplayName(agent),
|
|
678
|
+
...(agent ? { username: getAgentDisplayName(agent), ...getAgentSlackIcon(agent) } : {}),
|
|
679
679
|
metadata: {
|
|
680
680
|
event_type: SLACK_RENDER_METADATA_EVENT,
|
|
681
681
|
event_payload: { message_id: reservation.id, kind: "tree" },
|
|
@@ -1312,10 +1312,10 @@ export async function streamOutcomeCard(
|
|
|
1312
1312
|
markdown_text: presentation,
|
|
1313
1313
|
};
|
|
1314
1314
|
const agent = task.agentId ? await getAgentById(task.agentId) : undefined;
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1315
|
+
const persona = agent
|
|
1316
|
+
? { username: getAgentDisplayName(agent), ...getAgentSlackIcon(agent) }
|
|
1317
|
+
: {};
|
|
1318
|
+
Object.assign(startPayload, persona);
|
|
1319
1319
|
if (!task.slackChannelId.startsWith("D") && task.slackUserId) {
|
|
1320
1320
|
const teamId = await slackTeamId(app.client);
|
|
1321
1321
|
if (teamId) {
|
|
@@ -1379,8 +1379,7 @@ export async function streamOutcomeCard(
|
|
|
1379
1379
|
thread_ts: task.slackThreadTs,
|
|
1380
1380
|
text: presentation,
|
|
1381
1381
|
blocks: [{ type: "markdown", text: presentation }, ...captions],
|
|
1382
|
-
...
|
|
1383
|
-
...(startPayload.icon_emoji ? { icon_emoji: startPayload.icon_emoji } : {}),
|
|
1382
|
+
...persona,
|
|
1384
1383
|
});
|
|
1385
1384
|
deliveredViaFallback = true;
|
|
1386
1385
|
}
|
package/src/slack/responses.ts
CHANGED
|
@@ -99,7 +99,7 @@ export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
|
|
|
99
99
|
thread_ts: task.slackThreadTs,
|
|
100
100
|
text: batch.join("\n\n"),
|
|
101
101
|
username: getAgentDisplayName(agent),
|
|
102
|
-
|
|
102
|
+
icon: getAgentSlackIcon(agent),
|
|
103
103
|
blocks: batch.map((chunk) => ({
|
|
104
104
|
type: "section",
|
|
105
105
|
text: { type: "mrkdwn", text: chunk },
|
|
@@ -173,7 +173,7 @@ export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
|
|
|
173
173
|
? `${completedStatusIcon()} ${agentName} completed${i > 0 ? ` (continued ${i + 1}/${blockBatches.length})` : ""}`
|
|
174
174
|
: body,
|
|
175
175
|
username: getAgentDisplayName(agent),
|
|
176
|
-
|
|
176
|
+
icon: getAgentSlackIcon(agent),
|
|
177
177
|
blocks: blockBatches[i],
|
|
178
178
|
});
|
|
179
179
|
}
|
|
@@ -185,7 +185,7 @@ export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
|
|
|
185
185
|
thread_ts: task.slackThreadTs,
|
|
186
186
|
text: `Task failed: ${reason}`,
|
|
187
187
|
username: getAgentDisplayName(agent),
|
|
188
|
-
|
|
188
|
+
icon: getAgentSlackIcon(agent),
|
|
189
189
|
blocks,
|
|
190
190
|
});
|
|
191
191
|
} else if (task.status === "cancelled") {
|
|
@@ -195,7 +195,7 @@ export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
|
|
|
195
195
|
thread_ts: task.slackThreadTs,
|
|
196
196
|
text: "Task cancelled",
|
|
197
197
|
username: getAgentDisplayName(agent),
|
|
198
|
-
|
|
198
|
+
icon: getAgentSlackIcon(agent),
|
|
199
199
|
blocks,
|
|
200
200
|
});
|
|
201
201
|
}
|
|
@@ -232,7 +232,7 @@ export async function sendProgressUpdate(
|
|
|
232
232
|
thread_ts: task.slackThreadTs,
|
|
233
233
|
text: progress,
|
|
234
234
|
username: getAgentDisplayName(agent),
|
|
235
|
-
|
|
235
|
+
icon: getAgentSlackIcon(agent),
|
|
236
236
|
blocks,
|
|
237
237
|
});
|
|
238
238
|
} catch (error) {
|
|
@@ -354,7 +354,7 @@ export async function updateToFinal(
|
|
|
354
354
|
thread_ts: task.slackThreadTs ?? messageTs,
|
|
355
355
|
text: `${completedStatusIcon()} ${agentName} completed (continued ${i + 1}/${completionBlockBatches.length})`,
|
|
356
356
|
username: getAgentDisplayName(agent),
|
|
357
|
-
|
|
357
|
+
icon: getAgentSlackIcon(agent),
|
|
358
358
|
blocks: completionBlockBatches[i],
|
|
359
359
|
});
|
|
360
360
|
}
|
|
@@ -418,7 +418,7 @@ export async function sendWithPersona(
|
|
|
418
418
|
thread_ts: string;
|
|
419
419
|
text: string;
|
|
420
420
|
username: string;
|
|
421
|
-
|
|
421
|
+
icon: SlackPersonaIcon;
|
|
422
422
|
blocks?: unknown[];
|
|
423
423
|
},
|
|
424
424
|
): Promise<string | undefined> {
|
|
@@ -433,7 +433,7 @@ export async function sendWithPersona(
|
|
|
433
433
|
unfurl_links: false,
|
|
434
434
|
unfurl_media: false,
|
|
435
435
|
username: options.username,
|
|
436
|
-
|
|
436
|
+
...options.icon,
|
|
437
437
|
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects are typed as plain JSON
|
|
438
438
|
blocks: blocks as any,
|
|
439
439
|
});
|
|
@@ -441,6 +441,19 @@ export async function sendWithPersona(
|
|
|
441
441
|
return result.ts;
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
/** Empty = no override, so Slack shows the app's own avatar. */
|
|
445
|
+
export type SlackPersonaIcon =
|
|
446
|
+
| { icon_emoji: string }
|
|
447
|
+
| { icon_url: string }
|
|
448
|
+
| Record<string, never>;
|
|
449
|
+
|
|
450
|
+
export function getAgentSlackIcon(agent: Agent): SlackPersonaIcon {
|
|
451
|
+
if (agent.slackIcon?.type === "url") return { icon_url: agent.slackIcon.url };
|
|
452
|
+
if (agent.slackIcon?.type === "emoji") return { icon_emoji: agent.slackIcon.emoji };
|
|
453
|
+
if (agent.slackIcon?.type === "app") return {};
|
|
454
|
+
return { icon_emoji: getAgentEmoji(agent) };
|
|
455
|
+
}
|
|
456
|
+
|
|
444
457
|
export function getAgentEmoji(agent: Agent): string {
|
|
445
458
|
if (agent.isLead) return ":crown:";
|
|
446
459
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { App, SocketModeReceiver } from "@slack/bolt";
|
|
2
|
+
import {
|
|
3
|
+
admitSlackInbound,
|
|
4
|
+
createSlackInboundDrain,
|
|
5
|
+
isSlackSocketEventStored,
|
|
6
|
+
recordSlackSocketAckNotReady,
|
|
7
|
+
recordSlackSocketEventRanInline,
|
|
8
|
+
} from "./inbound-dispatch";
|
|
9
|
+
|
|
10
|
+
/** @slack/socket-mode `sendWhileNotReadyError()`: the socket is down or reconnecting. */
|
|
11
|
+
const NOT_READY_MESSAGE = "Failed to send a WebSocket message as the client is not ready";
|
|
12
|
+
|
|
13
|
+
export function isSocketNotReadyError(error: unknown): boolean {
|
|
14
|
+
return error instanceof Error && error.message === NOT_READY_MESSAGE;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Events whose effect is "last write wins". Run late from the drain they would
|
|
19
|
+
* overwrite a newer event that already ran live, so they never wait in the queue.
|
|
20
|
+
* They run inline and leave only a dedup-key tombstone behind.
|
|
21
|
+
*/
|
|
22
|
+
const ORDER_DEPENDENT_EVENTS = new Set(["assistant_thread_context_changed"]);
|
|
23
|
+
|
|
24
|
+
function isOrderDependentEvent(body: unknown): boolean {
|
|
25
|
+
const type = (body as { event?: { type?: unknown } } | undefined)?.event?.type;
|
|
26
|
+
return typeof type === "string" && ORDER_DEPENDENT_EVENTS.has(type);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type SocketAck = (response?: Record<string, unknown>) => Promise<void>;
|
|
30
|
+
|
|
31
|
+
interface SocketEnvelope {
|
|
32
|
+
ack?: SocketAck;
|
|
33
|
+
envelope_id?: string;
|
|
34
|
+
type?: string;
|
|
35
|
+
body?: unknown;
|
|
36
|
+
retry_num?: number;
|
|
37
|
+
retry_reason?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface SocketAckOptions {
|
|
41
|
+
/** Called after a not-ready event was stored as a pending receipt: start the drain. */
|
|
42
|
+
onAdmitted: () => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Bolt acks an Events API delivery before it runs any middleware or listener,
|
|
47
|
+
* and the receiver drops the event when that ack throws. A stalled event loop
|
|
48
|
+
* can kill the socket between receive and ack, so the event is lost.
|
|
49
|
+
*
|
|
50
|
+
* Wrap the ack Bolt receives on each `slack_event`. When it fails only because
|
|
51
|
+
* the socket is not ready, persist the event as a `pending` receipt (encrypted
|
|
52
|
+
* payload plus dedup key, in one transaction) and wake the durable drain, which
|
|
53
|
+
* hands it back to Bolt (`app.processEvent`) and records its outcome. Bolt's own
|
|
54
|
+
* copy still drops, because the original error is rethrown. Work is persisted
|
|
55
|
+
* before anything runs, so a crash or a failing middleware leaves a recoverable
|
|
56
|
+
* receipt rather than a "processed" marker: a `pending` receipt is retried after
|
|
57
|
+
* a restart, and a delivery that failed before any side effect is retried with
|
|
58
|
+
* backoff. Only a crash mid-listener leaves it `uncertain`, which the drain
|
|
59
|
+
* never replays, as for every durable delivery.
|
|
60
|
+
*
|
|
61
|
+
* If the receipt cannot be stored (store down, backlog full, no event id) the
|
|
62
|
+
* ack error is swallowed instead and Bolt runs the listeners inline, deduped
|
|
63
|
+
* only by the in-memory `event_id` cache: losing the event is the worse failure.
|
|
64
|
+
* Order-dependent events (`assistant_thread_context_changed`, last write wins)
|
|
65
|
+
* take that inline path by design: queued, an older one could run after a newer
|
|
66
|
+
* one that ran live and overwrite it. Their dedup key is stored alongside, off
|
|
67
|
+
* the ack path (Bolt runs the listener once the ack settles, so awaiting the
|
|
68
|
+
* write would let a newer live event run first), with no payload and no way for
|
|
69
|
+
* the drain to claim it. The in-memory cache is then not the only thing standing
|
|
70
|
+
* between a redelivery and a restart: after one, an older context would
|
|
71
|
+
* otherwise overwrite a newer one. A crash before that write lands loses only
|
|
72
|
+
* this protection, and a redelivery in that window can run twice.
|
|
73
|
+
* Any other ack error still throws.
|
|
74
|
+
*
|
|
75
|
+
* Slack redelivers the unacked event with the same `event_id` and a
|
|
76
|
+
* `retry_num`. The redelivery is dropped before it reaches Bolt when its key is
|
|
77
|
+
* stored, so the receipt stays the single owner of the work, also across a
|
|
78
|
+
* restart. Only retries pay the lookup; first deliveries emit synchronously. If
|
|
79
|
+
* the lookup fails the retry runs.
|
|
80
|
+
*
|
|
81
|
+
* Only `events_api` envelopes are wrapped. Interactive payloads (modal
|
|
82
|
+
* submissions, button actions) and slash commands ack from inside their
|
|
83
|
+
* handler, which does the work after acking: swallowing a failed ack there
|
|
84
|
+
* would run the handler while Slack shows the user an error, and the retry
|
|
85
|
+
* would run it again.
|
|
86
|
+
*
|
|
87
|
+
* Wrapping `emit` (public) is the narrowest hook: the receiver builds its own
|
|
88
|
+
* client and reads `args.ack` when its `slack_event` listener runs.
|
|
89
|
+
*/
|
|
90
|
+
function tolerateSocketNotReadyAcks(
|
|
91
|
+
client: SocketModeReceiver["client"],
|
|
92
|
+
options: SocketAckOptions,
|
|
93
|
+
): void {
|
|
94
|
+
const emit = client.emit.bind(client);
|
|
95
|
+
const deliver = (event: string | symbol, args: unknown[]) => emit(event, ...args);
|
|
96
|
+
|
|
97
|
+
client.emit = (event: string | symbol, ...args: unknown[]): boolean => {
|
|
98
|
+
const payload = args[0] as SocketEnvelope | undefined;
|
|
99
|
+
if (
|
|
100
|
+
event !== "slack_event" ||
|
|
101
|
+
payload?.type !== "events_api" ||
|
|
102
|
+
typeof payload.ack !== "function"
|
|
103
|
+
) {
|
|
104
|
+
return deliver(event, args);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ack = payload.ack;
|
|
108
|
+
const envelope = payload.envelope_id ?? "unknown";
|
|
109
|
+
payload.ack = async (response) => {
|
|
110
|
+
try {
|
|
111
|
+
await ack(response);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!isSocketNotReadyError(error)) throw error;
|
|
114
|
+
recordSlackSocketAckNotReady();
|
|
115
|
+
const disposition = await dispose(payload, envelope);
|
|
116
|
+
if (disposition === "drain") {
|
|
117
|
+
options.onAdmitted();
|
|
118
|
+
throw error; // Bolt drops its copy; the receipt carries the event.
|
|
119
|
+
}
|
|
120
|
+
if (disposition === "drop") throw error; // An earlier delivery already ran it.
|
|
121
|
+
console.warn(
|
|
122
|
+
`[Slack] socket ack failed (client not ready), processing inline: envelope_id=${envelope}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (typeof payload.retry_num !== "number" || payload.retry_num < 1) {
|
|
128
|
+
return deliver(event, args);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// A redelivery: drop it when a receipt already owns the event.
|
|
132
|
+
void (async () => {
|
|
133
|
+
let stored = false;
|
|
134
|
+
try {
|
|
135
|
+
stored = await isSlackSocketEventStored(payload.body);
|
|
136
|
+
} catch (storeError) {
|
|
137
|
+
console.warn("[Slack] could not check stored event key, processing:", storeError);
|
|
138
|
+
}
|
|
139
|
+
if (!stored) {
|
|
140
|
+
deliver(event, args);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log(`[Slack] dropping redelivery of a stored event: envelope_id=${envelope}`);
|
|
144
|
+
try {
|
|
145
|
+
await ack();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.warn("[Slack] ack of a dropped redelivery failed:", error);
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
return client.listenerCount(event) > 0;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type Disposition =
|
|
155
|
+
/** Stored as a pending receipt: the drain runs it. */
|
|
156
|
+
| "drain"
|
|
157
|
+
/** Its key was already stored: an earlier delivery owns it. */
|
|
158
|
+
| "drop"
|
|
159
|
+
/** Not stored for the drain (store failure, or order-dependent): Bolt runs it inline. */
|
|
160
|
+
| "inline";
|
|
161
|
+
|
|
162
|
+
async function dispose(payload: SocketEnvelope, envelope: string): Promise<Disposition> {
|
|
163
|
+
if (isOrderDependentEvent(payload.body)) {
|
|
164
|
+
// Not awaited: Bolt runs the listener once the ack settles, so a slow or
|
|
165
|
+
// blocked write would hold this event behind a newer one that ran live.
|
|
166
|
+
void rememberInline(payload, envelope);
|
|
167
|
+
return "inline";
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const result = await admitSlackInbound({
|
|
171
|
+
transport: "socket",
|
|
172
|
+
kind: "event",
|
|
173
|
+
rawBody: JSON.stringify(payload.body),
|
|
174
|
+
body: payload.body,
|
|
175
|
+
retryNum: payload.retry_num,
|
|
176
|
+
retryReason: payload.retry_reason,
|
|
177
|
+
});
|
|
178
|
+
if (result.status === "admitted" || result.status === "duplicate") {
|
|
179
|
+
console.warn(
|
|
180
|
+
`[Slack] socket ack failed (client not ready), event stored for the drain: envelope_id=${envelope}`,
|
|
181
|
+
);
|
|
182
|
+
return "drain";
|
|
183
|
+
}
|
|
184
|
+
console.warn(
|
|
185
|
+
`[Slack] could not store unacked event (${result.reason}): envelope_id=${envelope}`,
|
|
186
|
+
);
|
|
187
|
+
} catch (storeError) {
|
|
188
|
+
console.warn("[Slack] could not persist unacked event:", storeError);
|
|
189
|
+
}
|
|
190
|
+
return "inline";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* An order-dependent event runs inline. Its key is stored alongside (no payload,
|
|
195
|
+
* nothing for the drain to claim) so a redelivery after a restart is dropped
|
|
196
|
+
* instead of overwriting a newer event that ran in between. The write is
|
|
197
|
+
* best-effort and off the ack path: it never delays the event, and a store
|
|
198
|
+
* failure only costs the restart protection, never the event.
|
|
199
|
+
*/
|
|
200
|
+
async function rememberInline(payload: SocketEnvelope, envelope: string): Promise<void> {
|
|
201
|
+
try {
|
|
202
|
+
const result = await recordSlackSocketEventRanInline(payload.body, {
|
|
203
|
+
retryNum: payload.retry_num,
|
|
204
|
+
retryReason: payload.retry_reason,
|
|
205
|
+
});
|
|
206
|
+
if (result === "duplicate") {
|
|
207
|
+
console.warn(`[Slack] inline event key was already stored: envelope_id=${envelope}`);
|
|
208
|
+
}
|
|
209
|
+
} catch (storeError) {
|
|
210
|
+
console.warn("[Slack] could not remember unacked inline event:", storeError);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Install the not-ready ack tolerance on the receiver's client and the durable
|
|
216
|
+
* drain that runs the receipts it stores. The caller owns the drain: `wake()` it
|
|
217
|
+
* after boot recovery to pick up receipts stored before a restart, `stop()` it
|
|
218
|
+
* on shutdown.
|
|
219
|
+
*/
|
|
220
|
+
export function installSocketAckRecovery(
|
|
221
|
+
client: SocketModeReceiver["client"],
|
|
222
|
+
app: Pick<App, "processEvent">,
|
|
223
|
+
): ReturnType<typeof createSlackInboundDrain> {
|
|
224
|
+
const drain = createSlackInboundDrain(
|
|
225
|
+
async ({ receipt, rawBody }) => {
|
|
226
|
+
await app.processEvent({
|
|
227
|
+
body: JSON.parse(rawBody),
|
|
228
|
+
ack: async () => {},
|
|
229
|
+
retryNum: receipt.retryNum ?? undefined,
|
|
230
|
+
retryReason: receipt.retryReason ?? undefined,
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
{ requireBoltDispatch: true },
|
|
234
|
+
);
|
|
235
|
+
tolerateSocketNotReadyAcks(client, { onAdmitted: () => void drain.wake() });
|
|
236
|
+
return drain;
|
|
237
|
+
}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
} from "./inbound-files";
|
|
24
24
|
import { extractSlackMessageText } from "./message-text";
|
|
25
25
|
import { ensureSlackThreadTree, isSlackRenderV2Enabled } from "./render-v2";
|
|
26
|
-
import { getAgentDisplayName,
|
|
26
|
+
import { getAgentDisplayName, getAgentSlackIcon } from "./responses";
|
|
27
27
|
import { formatSlackSteeringAck, requestSlackThreadSteering } from "./steering";
|
|
28
28
|
import { registerTreeMessage } from "./watcher";
|
|
29
29
|
|
|
@@ -257,9 +257,7 @@ async function slackFlush(
|
|
|
257
257
|
text: formatSlackSteeringAck(steering.result),
|
|
258
258
|
unfurl_links: false,
|
|
259
259
|
unfurl_media: false,
|
|
260
|
-
...(agent
|
|
261
|
-
? { username: getAgentDisplayName(agent), icon_emoji: getAgentEmoji(agent) }
|
|
262
|
-
: {}),
|
|
260
|
+
...(agent ? { username: getAgentDisplayName(agent), ...getAgentSlackIcon(agent) } : {}),
|
|
263
261
|
});
|
|
264
262
|
} catch (error) {
|
|
265
263
|
console.error("[Slack] Failed to post steering feedback:", error);
|
|
@@ -363,7 +361,7 @@ async function slackFlush(
|
|
|
363
361
|
text: fallbackText,
|
|
364
362
|
unfurl_links: false,
|
|
365
363
|
unfurl_media: false,
|
|
366
|
-
...(lead ? { username: getAgentDisplayName(lead),
|
|
364
|
+
...(lead ? { username: getAgentDisplayName(lead), ...getAgentSlackIcon(lead) } : {}),
|
|
367
365
|
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
368
366
|
blocks: blocks as any,
|
|
369
367
|
});
|
package/src/slack/watcher.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { buildTreeBlocks, formatDuration } from "./blocks";
|
|
|
18
18
|
import { isSlackRenderV2Enabled, processSlackRenderV2 } from "./render-v2";
|
|
19
19
|
import {
|
|
20
20
|
getAgentDisplayName,
|
|
21
|
-
|
|
21
|
+
getAgentSlackIcon,
|
|
22
22
|
sendInlineTaskOutput,
|
|
23
23
|
sendProgressUpdate,
|
|
24
24
|
sendTaskResponse,
|
|
@@ -491,7 +491,7 @@ async function postInitialDMTreeMessage(task: AgentTask): Promise<string | undef
|
|
|
491
491
|
unfurl_links: false,
|
|
492
492
|
unfurl_media: false,
|
|
493
493
|
username: getAgentDisplayName(agent),
|
|
494
|
-
|
|
494
|
+
...getAgentSlackIcon(agent),
|
|
495
495
|
// biome-ignore lint/suspicious/noExplicitAny: Block Kit objects
|
|
496
496
|
blocks: blocks as any,
|
|
497
497
|
});
|
|
@@ -3,8 +3,10 @@ import type { EntityPresentDetailsArguments, WebClient } from "@slack/web-api";
|
|
|
3
3
|
import { getAgentById, getTaskById } from "../be/db";
|
|
4
4
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
5
5
|
import { getTaskUrl } from "./blocks";
|
|
6
|
+
import { wasEventSeen } from "./event-dedup";
|
|
6
7
|
import { shouldBlockChannel } from "./external-channel";
|
|
7
8
|
import { isUserAllowed } from "./handlers";
|
|
9
|
+
import { isAdmittedSlackDelivery } from "./inbound-dispatch";
|
|
8
10
|
import { slackTaskOutput } from "./task-output";
|
|
9
11
|
|
|
10
12
|
type DetailsEvent = SlackEventMiddlewareArgs<"entity_details_requested">["event"];
|
|
@@ -93,8 +95,10 @@ async function getDetails(event: DetailsEvent, client: WebClient): Promise<Detai
|
|
|
93
95
|
|
|
94
96
|
export function registerWorkObjectHandlers(app: App): void {
|
|
95
97
|
// Bolt acknowledges Events API deliveries; the flexpane needs a separate API response.
|
|
96
|
-
app.event("entity_details_requested", async ({ event, client, logger }) => {
|
|
98
|
+
app.event("entity_details_requested", async ({ event, body, client, logger }) => {
|
|
97
99
|
if (!isEnvFlagEnabled("SLACK_WORK_OBJECTS_ENABLED", false)) return;
|
|
100
|
+
// A redelivery after a tolerated socket ack failure must not present twice.
|
|
101
|
+
if (!isAdmittedSlackDelivery() && wasEventSeen(body?.event_id)) return;
|
|
98
102
|
|
|
99
103
|
let details: Details;
|
|
100
104
|
try {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { renderAgentFsReleaseNotes } from "../../scripts/agent-fs-release-notes";
|
|
3
|
+
|
|
4
|
+
const currentYaml = 'agentFs:\n image:\n tag: "0.18.11"\n';
|
|
5
|
+
const unchangedSection = `## agent-fs
|
|
6
|
+
|
|
7
|
+
Ships [agent-fs 0.18.11](https://github.com/desplega-ai/agent-fs/releases/tag/v0.18.11).
|
|
8
|
+
|
|
9
|
+
agent-swarm pins agent-fs via the chart image tag and npm packages \`@desplega.ai/agent-fs\` and \`@desplega.ai/agent-fs-just-bash\`; each agent-fs release needs a \`chore(deps): bump agent-fs pins\` PR here.`;
|
|
10
|
+
|
|
11
|
+
describe("agent-fs release notes", () => {
|
|
12
|
+
test("renders the bump when the version changed", () => {
|
|
13
|
+
expect(renderAgentFsReleaseNotes(currentYaml, 'agentFs:\n image:\n tag: "0.18.9"\n')).toBe(
|
|
14
|
+
unchangedSection.replace(").\n", ") (bumped from 0.18.9).\n"),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("omits the bump when the version is unchanged", () => {
|
|
19
|
+
expect(renderAgentFsReleaseNotes(currentYaml, currentYaml)).toBe(unchangedSection);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("omits the bump when there is no previous release tag", () => {
|
|
23
|
+
expect(renderAgentFsReleaseNotes(currentYaml)).toBe(unchangedSection);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("omits the bump when the previous chart has no agentFs.image.tag", () => {
|
|
27
|
+
expect(renderAgentFsReleaseNotes(currentYaml, "agentFs:\n image: {}\n")).toBe(
|
|
28
|
+
unchangedSection,
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test.each([
|
|
33
|
+
"agentFs:\n image: {}\n",
|
|
34
|
+
'agentFs:\n image:\n tag: ""\n',
|
|
35
|
+
])("fails loudly when the current chart tag is missing or empty: %s", (yaml) => {
|
|
36
|
+
expect(() => renderAgentFsReleaseNotes(yaml)).toThrow(
|
|
37
|
+
"Current chart is missing agentFs.image.tag",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|