@desplega.ai/agent-swarm 1.152.2 → 1.154.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 -2
- package/dist/{acp-adapter-pfeje2v5.js → acp-adapter-eb2aj8xy.js} +4 -4
- package/dist/{actions-0sttc7f5.js → actions-kpnwp1kz.js} +31 -27
- package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
- package/dist/{app-p8teva95.js → app-zk1apkf6.js} +10 -7
- package/dist/{assistant-r45jnsrt.js → assistant-kwq4y50q.js} +32 -28
- package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
- package/dist/{boot-reembed-je26h5y7.js → boot-reembed-22hd3jjn.js} +9 -7
- package/dist/{boot-reembed-1qmybsd4.js → boot-reembed-qe168424.js} +10 -8
- package/dist/{boot-scrub-logs-gte3h1mm.js → boot-scrub-logs-pkdpsrjd.js} +7 -5
- package/dist/{claude-adapter-csc1mzka.js → claude-adapter-3fx90nww.js} +18 -16
- package/dist/{claude-managed-adapter-dsmkcrjy.js → claude-managed-adapter-ea2aga6w.js} +3 -3
- package/dist/{claude-sdk-session-fcd6f078.js → claude-sdk-session-z7fahf1k.js} +18 -16
- package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
- package/dist/{cli-wbyk465z.js → cli-09y3tjxz.js} +1 -1
- package/dist/{cli-ht5rs63n.js → cli-0e31nm4n.js} +16 -7
- package/dist/cli-0x5qzvkg.js +4279 -0
- package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
- package/dist/{cli-4ahpr4sw.js → cli-1jpjafw4.js} +5 -5
- package/dist/{cli-jbmc0r7r.js → cli-243tzws8.js} +1 -1
- package/dist/{cli-ch38q5z4.js → cli-24k806sc.js} +6789 -2542
- package/dist/cli-3bkz2p7j.js +2828 -0
- package/dist/cli-3er6gcsn.js +1849 -0
- package/dist/cli-4k7jazkq.js +3339 -0
- package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
- package/dist/{cli-qd65dvr6.js → cli-5vs0nh43.js} +65 -44
- package/dist/{cli-z3wkng35.js → cli-6r3cgpk2.js} +224 -156
- package/dist/{cli-pgj4a5fb.js → cli-71cj99hx.js} +4 -3
- package/dist/cli-8sea27q4.js +14075 -0
- package/dist/{cli-jvy9qgjs.js → cli-913yakhv.js} +22 -7
- package/dist/{cli-pf9q3696.js → cli-927erqqt.js} +5 -2
- package/dist/cli-95jnpnqg.js +230 -0
- package/dist/{cli-wpv5rgg2.js → cli-abp3g4ax.js} +15 -2
- package/dist/{cli-ckz5t2hb.js → cli-acy6bfhr.js} +10 -7
- package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
- package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
- package/dist/{cli-qn042eh7.js → cli-c1v10xb2.js} +194 -1913
- package/dist/cli-cgj4hc12.js +18902 -0
- package/dist/{cli-2nnd56zt.js → cli-e0pg4zeh.js} +3 -2
- package/dist/{cli-r84ctdxn.js → cli-e31ehqwy.js} +2 -2
- package/dist/{cli-gphgnvpf.js → cli-e3229na9.js} +3 -3
- package/dist/{cli-gpqcs935.js → cli-e3cyf59d.js} +2 -2
- package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
- package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
- package/dist/{cli-j4nv2wqy.js → cli-fdzg6jhf.js} +10 -4
- package/dist/{cli-fm4ytsdk.js → cli-g2h29qar.js} +2 -2
- package/dist/{cli-zvgj5zg8.js → cli-ge67p080.js} +302 -3497
- package/dist/cli-h38x0qhg.js +6912 -0
- package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
- package/dist/{cli-jew3jda5.js → cli-hws0vn86.js} +2 -2
- package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
- package/dist/{cli-xw7vak96.js → cli-kp3qs53m.js} +5 -5
- package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
- package/dist/{cli-zvkq5csn.js → cli-myaxtgh8.js} +7 -6
- package/dist/{cli-56h18ng2.js → cli-n62atwh9.js} +1 -1
- package/dist/cli-n7h1kqpf.js +29 -0
- package/dist/{cli-fmy65jdn.js → cli-pf1v8xhb.js} +7 -5
- package/dist/{cli-xy1cchaf.js → cli-q85qhckh.js} +238 -11
- package/dist/{cli-3bpzj8me.js → cli-r1d3dzt6.js} +2 -2
- package/dist/{pi-mono-adapter-jcqt71cg.js → cli-rxkqvngf.js} +67343 -101083
- package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
- package/dist/{cli-b9dd0h3k.js → cli-stwsmd6k.js} +3 -2
- package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
- package/dist/{cli-z61ykvam.js → cli-wg04s9kr.js} +4 -3
- package/dist/cli-yn0a417a.js +4069 -0
- package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
- package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
- package/dist/cli.js +28 -35
- package/dist/{codex-adapter-fxmpsabw.js → codex-adapter-ttdm995v.js} +12 -11
- package/dist/{codex-hook-8jexc34x.js → codex-hook-8gx7bcz0.js} +3 -3
- package/dist/{codex-session-runner-rr1e47ea.js → codex-session-runner-3v8nh4c8.js} +12 -11
- package/dist/{commands-0f5rxz51.js → commands-1cxwbhhq.js} +7 -5
- package/dist/{db-r3jxf3wy.js → db-y9zwsr79.js} +25 -5
- package/dist/{dispatcher-06vph6q4.js → dispatcher-9m9zss36.js} +30 -26
- package/dist/dsh-adapter-gsb2qqf9.js +19 -0
- package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
- package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-gwwrnzqn.js} +33 -73
- package/dist/{google-vertex-fr665tqs.js → google-vertex-5wfawpwz.js} +38 -68
- package/dist/{handlers-0hgtc910.js → handlers-36fm1bwn.js} +33 -29
- package/dist/{hook-9kvrv1er.js → hook-pntw4r8h.js} +12 -10
- package/dist/{hook-fbfhkat3.js → hook-vwq8y26p.js} +13 -11
- package/dist/{http-xbyx8k5j.js → http-20jesakk.js} +357 -261
- package/dist/{index-jsw3aynb.js → index-6artrehg.js} +30 -26
- package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
- package/dist/{index-rnyahfn5.js → index-9n5z2492.js} +30 -26
- package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
- package/dist/{index-tfsdxyv6.js → index-j5wrf1yw.js} +40 -25
- package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
- package/dist/{index-sgk50mbg.js → index-m2xqpz5r.js} +31 -27
- package/dist/{index-gpfn1741.js → index-m9ks8a7t.js} +9 -7
- package/dist/index-w097eg0x.js +5 -0
- package/dist/jiti-loader-b4837mtm.js +34 -0
- package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
- package/dist/{keepalive-s3xpr7sw.js → keepalive-pwxvxd2c.js} +9 -7
- package/dist/{lead-rq4n5r16.js → lead-m789wqa3.js} +46 -41
- package/dist/{maintenance-f0atj1ab.js → maintenance-bvg6w7e2.js} +10 -8
- package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
- package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
- package/dist/{oauth-refresh-sweep-q0t3bxre.js → oauth-refresh-sweep-6v4bd737.js} +8 -6
- package/dist/{onboard-br5j1kch.js → onboard-snckpcj1.js} +115 -35
- package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
- package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
- package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
- package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
- package/dist/{otel-impl-vdqge2nt.js → otel-impl-e0ytens5.js} +4 -4
- package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
- package/dist/pi-mono-adapter-c4ggemhz.js +1316 -0
- package/dist/{pricing-refresh-hb3691fd.js → pricing-refresh-52g2ywrw.js} +9 -7
- package/dist/{rbac-roles-t78xt39b.js → rbac-roles-5455kfdn.js} +8 -6
- package/dist/{rbac-roles-jh280k9g.js → rbac-roles-hbf0xa52.js} +9 -7
- package/dist/{render-v2-7p8r2ymg.js → render-v2-npm8xdzh.js} +16 -9
- package/dist/{seed-pricing-hf889enh.js → seed-pricing-650wbr1n.js} +8 -6
- package/dist/{setup-w6mb03jv.js → setup-k1mew5rf.js} +4 -5
- package/dist/virtual-modules-2q5b2pk4.js +54 -0
- package/dist/{work-objects-nt8gy1kj.js → work-objects-2ea3kvwg.js} +33 -29
- package/dist/{worker-dwy617t0.js → worker-ec6hvc4m.js} +46 -41
- package/openapi.json +109 -16
- package/package.json +7 -6
- package/plugin/opencode-plugins/lib/memory-raters.ts +7 -0
- package/src/be/automation-preflight.ts +6 -4
- package/src/be/db/agents.ts +75 -2
- package/src/be/db/tasks/read.ts +2 -0
- package/src/be/db/tasks/write.ts +7 -1
- package/src/be/db.ts +140 -18
- package/src/be/extensions/db.ts +35 -4
- package/src/be/memory/raters/llm.ts +2 -6
- package/src/be/memory/raters/noop.ts +2 -3
- package/src/be/memory/raters/registry.ts +4 -10
- package/src/be/migrations/157_defer_provenance_and_marker.sql +27 -0
- package/src/be/models-catalog.ts +2 -0
- package/src/be/modelsdev-cache.json +213 -0
- package/src/be/rbac-audit.ts +2 -0
- package/src/be/seed/agent-fs-provision.ts +23 -0
- package/src/be/seed-pricing.ts +3 -1
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/onboard/compose-generator.ts +16 -1
- package/src/commands/onboard/env-generator.ts +14 -1
- package/src/commands/onboard/manifest.ts +1 -0
- package/src/commands/onboard/steps/integration-slack.tsx +56 -16
- package/src/commands/onboard/steps/review.tsx +5 -0
- package/src/commands/onboard/types.ts +6 -0
- package/src/commands/onboard.tsx +5 -1
- package/src/commands/provider-credentials.ts +14 -2
- package/src/commands/runner.ts +18 -2
- package/src/extensions/identity.ts +15 -7
- package/src/extensions/lifecycle.ts +15 -21
- package/src/hooks/codex-hook.ts +7 -1
- package/src/http/agents.ts +12 -4
- package/src/http/config.ts +1 -1
- package/src/http/core.ts +5 -3
- package/src/http/extensions.ts +108 -34
- package/src/http/poll.ts +7 -0
- package/src/http/schedules.ts +10 -0
- package/src/http/script-runs.ts +35 -26
- package/src/http/status.ts +24 -11
- package/src/http/tasks.ts +6 -1
- package/src/prompts/base-prompt.ts +15 -7
- package/src/prompts/memories.ts +5 -7
- package/src/prompts/session-templates.ts +3 -2
- package/src/prompts/steering-delivery.ts +2 -0
- package/src/providers/claude-managed-models.ts +8 -0
- package/src/providers/codex-adapter.ts +3 -6
- package/src/providers/codex-models.ts +15 -1
- package/src/providers/dsh-adapter.ts +248 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/modelsdev-reasoning.json +50 -0
- package/src/providers/pi-mono-extension.ts +2 -5
- package/src/rbac/legacy-policy.ts +15 -1
- package/src/rbac/types.ts +2 -0
- package/src/server.ts +7 -1
- package/src/slack/ack.ts +43 -8
- package/src/slack/app.ts +32 -10
- package/src/slack/config.ts +29 -0
- package/src/slack/handlers.ts +7 -1
- package/src/slack/render-v2.ts +186 -17
- package/src/slack/responses.ts +1 -3
- package/src/slack/task-output.ts +66 -8
- package/src/slack/thread-buffer.ts +27 -3
- package/src/telemetry.ts +2 -3
- package/src/tests/agent-fs-provision-seeder.test.ts +56 -2
- package/src/tests/agents-harness-provider.test.ts +19 -0
- package/src/tests/agents-list-model-display.test.ts +60 -1
- package/src/tests/base-prompt.test.ts +25 -0
- package/src/tests/claude-managed-adapter.test.ts +6 -0
- package/src/tests/codex-gpt6-release.test.ts +97 -0
- package/src/tests/codex-steering-hook.test.ts +2 -2
- package/src/tests/config-api-only-keys.test.ts +9 -0
- package/src/tests/context-window.test.ts +1 -0
- package/src/tests/db-task-read-contract.test.ts +9 -0
- package/src/tests/db-task-write-contract.test.ts +9 -0
- package/src/tests/defer-task.test.ts +115 -16
- package/src/tests/dsh-adapter.test.ts +276 -0
- package/src/tests/e2e-nightly-report.test.ts +3 -3
- package/src/tests/e2e-redact.test.ts +2 -0
- package/src/tests/env-flag.test.ts +6 -0
- package/src/tests/extension-agent-visibility.test.ts +474 -0
- package/src/tests/extensions-db.test.ts +41 -0
- package/src/tests/extensions-http.test.ts +148 -4
- package/src/tests/extensions-lifecycle.test.ts +21 -1
- package/src/tests/extensions-mcp-tools.test.ts +9 -5
- package/src/tests/fetch-resolved-env.test.ts +43 -0
- package/src/tests/internal-ai/complete-structured.test.ts +32 -9
- package/src/tests/internal-ai/credentials.test.ts +4 -4
- package/src/tests/internal-ai/models.test.ts +20 -0
- package/src/tests/memory-rate-tool.test.ts +7 -10
- package/src/tests/memory-rater-defaults.test.ts +23 -0
- package/src/tests/memory-rater-e2e.test.ts +2 -3
- package/src/tests/migration-046-budgets.test.ts +2 -0
- package/src/tests/model-control.test.ts +2 -2
- package/src/tests/onboard-compose.test.ts +14 -0
- package/src/tests/onboard-env.test.ts +17 -0
- package/src/tests/onboard-manifest.test.ts +12 -0
- package/src/tests/opus-5-5-registry.test.ts +48 -0
- package/src/tests/pricing-refresh.test.ts +1 -1
- package/src/tests/pricing-routes.test.ts +15 -3
- package/src/tests/provider-registration.test.ts +5 -2
- package/src/tests/rbac-admission-e2e.test.ts +54 -0
- package/src/tests/rbac-e2e-helpers.ts +2 -0
- package/src/tests/rbac-engine.test.ts +32 -0
- package/src/tests/rbac-wire-e2e.test.ts +33 -0
- package/src/tests/reasoning-effort.test.ts +8 -0
- package/src/tests/reload-config.test.ts +33 -0
- package/src/tests/script-connections.test.ts +3 -0
- package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
- package/src/tests/slack-config.test.ts +136 -0
- package/src/tests/slack-mode-lifecycle.test.ts +69 -0
- package/src/tests/slack-render-v2.test.ts +446 -16
- package/src/tests/slack-steering.test.ts +69 -1
- package/src/tests/slack-task-output.test.ts +33 -12
- package/src/tests/status.test.ts +22 -2
- package/src/tests/steering-core.test.ts +35 -0
- package/src/tests/steering-delivery.test.ts +39 -0
- package/src/tests/steering-transport.test.ts +2 -2
- package/src/tests/task-tool-preload-mcp.test.ts +3 -1
- package/src/tests/telemetry-init.test.ts +18 -0
- package/src/tests/workflow-live-recovery.test.ts +126 -0
- package/src/tests/workflow-llm.test.ts +3 -3
- package/src/tools/defer-task.ts +64 -26
- package/src/tools/extension-common.ts +4 -0
- package/src/tools/extension-install.ts +1 -1
- package/src/tools/extension-list.ts +4 -3
- package/src/tools/join-swarm.ts +1 -1
- package/src/tools/schedules/create-schedule.ts +11 -1
- package/src/tools/schedules/patch-schedule.ts +5 -0
- package/src/tools/schedules/update-schedule.ts +5 -0
- package/src/tools/send-task.ts +6 -0
- package/src/tools/utils.ts +1 -1
- package/src/types.ts +30 -8
- package/src/utils/context-window.ts +1 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/internal-ai/complete-structured.ts +8 -0
- package/src/utils/internal-ai/models.ts +5 -5
- package/src/utils/memory-raters.ts +3 -0
- package/src/workflows/engine.ts +28 -0
- package/src/workflows/recovery.ts +7 -4
- package/templates/schedules/daily-status-report/content.md +25 -12
- package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
- package/templates/skills/swarm-extensions/content.md +9 -5
- package/dist/cli-4wppfkcm.js +0 -16384
- package/dist/cli-55qrx3q8.js +0 -6041
- package/dist/cli-cg4vnyh4.js +0 -305
- package/dist/cli-jyetr64j.js +0 -52
- package/dist/cli-pp9ax9pa.js +0 -520
- package/dist/cli-qa66qaf0.js +0 -6076
- package/dist/cli-qghpvca2.js +0 -590
- package/dist/cli-rembwgt8.js +0 -861
- package/dist/cli-tcvj7j2r.js +0 -1971
- package/dist/cli-vfcth11k.js +0 -36
- package/dist/cli-w1d1bgkk.js +0 -1422
- package/dist/cli-y6x0gqtg.js +0 -5977
- package/dist/es5-tj3tn4jj.js +0 -844
- package/dist/index-1kq5hw2p.js +0 -467
- package/dist/index-1p8wdstt.js +0 -7
- package/dist/index-8fdjzb2f.js +0 -100
- package/dist/index-bxmwzbgw.js +0 -365
- package/dist/index-nzerxd5z.js +0 -7
- package/dist/index-pyymnzat.js +0 -1168
- package/dist/index-qv6atbab.js +0 -619
- package/dist/index-rja32qa7.js +0 -801
- package/dist/index-tzg5srfp.js +0 -211
- package/dist/index-vhncw983.js +0 -90
- package/dist/index-xk036zsk.js +0 -859
- package/dist/index-zyazbzcb.js +0 -6
- package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
|
@@ -56,6 +56,20 @@ const leadOrOperatorOrUser: LegacyRule = {
|
|
|
56
56
|
principal.kind === "operator" || principal.kind === "user" || actsAsLead(principal),
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
const extensionWrite: LegacyRule = {
|
|
60
|
+
name: "extension-owner-or-elevated",
|
|
61
|
+
denyReason: "requires extension owner, lead agent, operator, or user authentication",
|
|
62
|
+
evaluate: (principal, resource) => {
|
|
63
|
+
if (leadOrOperatorOrUser.evaluate(principal, resource)) return true;
|
|
64
|
+
return (
|
|
65
|
+
principal.kind === "agent" &&
|
|
66
|
+
principal.agentId !== "" &&
|
|
67
|
+
resource?.kind === "extension" &&
|
|
68
|
+
(resource.extensionId === undefined || resource.createdByAgentId === principal.agentId)
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
59
73
|
// Deployment-only: agents cannot reset this through swarm_config.
|
|
60
74
|
const extensionActivation: LegacyRule = {
|
|
61
75
|
name: "extension-activation",
|
|
@@ -242,6 +256,6 @@ export const LEGACY_POLICY = {
|
|
|
242
256
|
"script.api.update": leadOnly,
|
|
243
257
|
"script.api.rotate": leadOnly,
|
|
244
258
|
"script.api.delete": leadOnly,
|
|
245
|
-
"extension.write":
|
|
259
|
+
"extension.write": extensionWrite,
|
|
246
260
|
"extension.activate": extensionActivation,
|
|
247
261
|
} as const satisfies Record<PermissionVerb, LegacyRule>;
|
package/src/rbac/types.ts
CHANGED
|
@@ -32,6 +32,8 @@ export type RbacResource =
|
|
|
32
32
|
| { kind: "app"; appId: string }
|
|
33
33
|
/** Skills, mcp-servers, memory entries, scripts. */
|
|
34
34
|
| { kind: "owned"; ownerAgentId?: string | null; scope?: string }
|
|
35
|
+
/** An omitted extensionId denotes a new install; existing rows retain their creator. */
|
|
36
|
+
| { kind: "extension"; extensionId?: string; createdByAgentId?: string | null }
|
|
35
37
|
| { kind: "none" };
|
|
36
38
|
|
|
37
39
|
export type RbacDecision =
|
package/src/server.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { registerGithubTaskReactions } from "./github/task-reactions";
|
|
|
9
9
|
import { loadGlobalConfigsIntoEnv } from "./http/core";
|
|
10
10
|
import { resolveTemplate } from "./prompts/resolver";
|
|
11
11
|
import { isRbacEnabled } from "./rbac";
|
|
12
|
+
import { getSlackConfiguration } from "./slack/config";
|
|
12
13
|
import { registerAcceptSteerTool } from "./tools/accept-steer";
|
|
13
14
|
import { registerAppDiffTool } from "./tools/app-diff";
|
|
14
15
|
import { registerAppGetTool } from "./tools/app-get";
|
|
@@ -268,7 +269,12 @@ export function hasCapability(cap: CAPABILITIES_T): boolean {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
export function getEnabledCapabilities(): CAPABILITIES_T[] {
|
|
271
|
-
|
|
272
|
+
const capabilities = Array.from(getCapabilities());
|
|
273
|
+
// Phase 1 has no HTTP receiver. Do not advertise usable Slack tools to
|
|
274
|
+
// workers when HTTP (or an invalid transport) was selected.
|
|
275
|
+
return getSlackConfiguration().mode === "socket"
|
|
276
|
+
? capabilities
|
|
277
|
+
: capabilities.filter((capability) => capability !== "slack");
|
|
272
278
|
}
|
|
273
279
|
|
|
274
280
|
/**
|
package/src/slack/ack.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WebClient } from "@slack/web-api";
|
|
2
|
-
import { getLogsByTaskIdChronological, getSlackTasksInThread } from "../be/db";
|
|
2
|
+
import { getLogsByTaskIdChronological, getSlackTasksInThread, getTaskById } from "../be/db";
|
|
3
3
|
import { recordSlackReactionInvalidName } from "../otel";
|
|
4
4
|
import { type AgentTask, isTerminalTaskStatus } from "../types";
|
|
5
5
|
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
SLACK_REACTION_DEFAULTS,
|
|
11
11
|
type SlackReactionEvent,
|
|
12
12
|
} from "./reaction-shortcode";
|
|
13
|
+
import { deferralWakes, isAwaitingWake } from "./task-output";
|
|
13
14
|
|
|
14
15
|
export { reactionName, type SlackReactionEvent } from "./reaction-shortcode";
|
|
15
16
|
|
|
@@ -170,28 +171,62 @@ export async function finalizeSlackSteerReactions(
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
// Deferral chains are short; the cap only guards against a parent cycle.
|
|
175
|
+
const MAX_DEFERRAL_CHAIN_HOPS = 10;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The Slack message a task answers. A deferral's wake-up does not inherit
|
|
179
|
+
* `slackTriggerMessageTs`, so walk its `parentTaskId` chain back to the task
|
|
180
|
+
* that deferred on the human's message.
|
|
181
|
+
*/
|
|
182
|
+
async function triggerMessageTs(task: AgentTask): Promise<string | undefined> {
|
|
183
|
+
let current: AgentTask | null = task;
|
|
184
|
+
for (let hop = 0; current && hop <= MAX_DEFERRAL_CHAIN_HOPS; hop++) {
|
|
185
|
+
if (current.slackTriggerMessageTs) return current.slackTriggerMessageTs;
|
|
186
|
+
if (current.taskType !== "deferred" || !current.parentTaskId) return undefined;
|
|
187
|
+
current = await getTaskById(current.parentTaskId);
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The tasks answering `timestamp`, plus the wake-ups that continue them. */
|
|
193
|
+
function tasksAnsweringTrigger(threadTasks: AgentTask[], timestamp: string): AgentTask[] {
|
|
194
|
+
const linked = threadTasks.filter((task) => task.slackTriggerMessageTs === timestamp);
|
|
195
|
+
for (let index = 0; index < linked.length; index++) {
|
|
196
|
+
for (const wake of deferralWakes(linked[index]!, threadTasks)) {
|
|
197
|
+
if (!linked.includes(wake)) linked.push(wake);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return linked;
|
|
201
|
+
}
|
|
202
|
+
|
|
173
203
|
export async function finalizeTerminalSlackReactions(tasks: AgentTask[]): Promise<void> {
|
|
174
204
|
const app = getSlackApp();
|
|
175
205
|
if (!app) return;
|
|
176
206
|
|
|
177
207
|
const triggers = new Map<string, { channelId: string; threadTs: string; timestamp: string }>();
|
|
178
208
|
for (const task of tasks) {
|
|
179
|
-
if (!task.slackChannelId || !task.slackThreadTs
|
|
180
|
-
const
|
|
209
|
+
if (!task.slackChannelId || !task.slackThreadTs) continue;
|
|
210
|
+
const timestamp = await triggerMessageTs(task);
|
|
211
|
+
if (!timestamp) continue;
|
|
212
|
+
const key = `${task.slackChannelId}\0${timestamp}`;
|
|
181
213
|
triggers.set(key, {
|
|
182
214
|
channelId: task.slackChannelId,
|
|
183
215
|
threadTs: task.slackThreadTs,
|
|
184
|
-
timestamp
|
|
216
|
+
timestamp,
|
|
185
217
|
});
|
|
186
218
|
}
|
|
187
219
|
|
|
188
220
|
for (const { channelId, threadTs, timestamp } of triggers.values()) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
221
|
+
const threadTasks = await getSlackTasksInThread(channelId, threadTs);
|
|
222
|
+
const linkedTasks = tasksAnsweringTrigger(threadTasks, timestamp);
|
|
223
|
+
// A parked deferral is stored `completed`, but the message is not
|
|
224
|
+
// answered yet: keep 👀 until its wake-up settles.
|
|
192
225
|
if (
|
|
193
226
|
linkedTasks.length === 0 ||
|
|
194
|
-
linkedTasks.some(
|
|
227
|
+
linkedTasks.some(
|
|
228
|
+
(task) => !isTerminalTaskStatus(task.status) || isAwaitingWake(task, threadTasks),
|
|
229
|
+
)
|
|
195
230
|
) {
|
|
196
231
|
continue;
|
|
197
232
|
}
|
package/src/slack/app.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { App, LogLevel } from "@slack/bolt";
|
|
2
2
|
import { emitBuiltInIntegrationConnectedOnce, ensureSlackRenderV2Activation } from "../be/db";
|
|
3
|
+
import { getSlackConfiguration } from "./config";
|
|
3
4
|
import { getSlackSocketModeBlockReason, SLACK_DEV_SOCKET_MODE_OPT_IN } from "./socket-mode-guard";
|
|
4
5
|
import { startTaskWatcher, stopTaskWatcher } from "./watcher";
|
|
5
6
|
|
|
@@ -16,23 +17,38 @@ export async function initSlackApp(): Promise<App | null> {
|
|
|
16
17
|
console.log("[Slack] Already initialized, skipping");
|
|
17
18
|
return app;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Check if Slack is explicitly disabled
|
|
22
|
-
const slackDisable = process.env.SLACK_DISABLE;
|
|
23
|
-
if (slackDisable === "true" || slackDisable === "1") {
|
|
20
|
+
const config = getSlackConfiguration();
|
|
21
|
+
if (config.disabled) {
|
|
24
22
|
console.log("[Slack] Disabled via SLACK_DISABLE");
|
|
25
23
|
return null;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if (!config.mode) {
|
|
27
|
+
console.error(
|
|
28
|
+
"[Slack] Invalid SLACK_MODE; expected socket or http. Slack integration disabled",
|
|
29
|
+
);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (config.missingCredentials.length > 0) {
|
|
34
|
+
console.log(
|
|
35
|
+
`[Slack] Missing ${config.missingCredentials.join(" or ")} for ${config.mode} mode, Slack integration disabled`,
|
|
36
|
+
);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
// Phase 1 establishes the transport contract only. Never fall back to a
|
|
41
|
+
// socket when HTTP was explicitly selected; the receiver lands in Phase 3.
|
|
42
|
+
if (config.mode === "http") {
|
|
43
|
+
console.error(
|
|
44
|
+
"[Slack] HTTP mode is configured but unavailable until the HTTP receiver is installed",
|
|
45
|
+
);
|
|
33
46
|
return null;
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
const botToken = process.env.SLACK_BOT_TOKEN as string;
|
|
50
|
+
const appToken = process.env.SLACK_APP_TOKEN as string;
|
|
51
|
+
|
|
36
52
|
const socketModeBlockReason = getSlackSocketModeBlockReason(process.env);
|
|
37
53
|
if (socketModeBlockReason) {
|
|
38
54
|
console.error(
|
|
@@ -51,6 +67,9 @@ export async function initSlackApp(): Promise<App | null> {
|
|
|
51
67
|
...(slackApiUrl ? { clientOptions: { slackApiUrl } } : {}),
|
|
52
68
|
});
|
|
53
69
|
|
|
70
|
+
// Failed validation must remain retryable without requiring stopSlackApp().
|
|
71
|
+
initialized = true;
|
|
72
|
+
|
|
54
73
|
// Register handlers
|
|
55
74
|
const { registerMessageHandler } = await import("./handlers");
|
|
56
75
|
const { registerCommandHandler } = await import("./commands");
|
|
@@ -69,7 +88,7 @@ export async function initSlackApp(): Promise<App | null> {
|
|
|
69
88
|
return app;
|
|
70
89
|
}
|
|
71
90
|
|
|
72
|
-
export async function startSlackApp(): Promise<
|
|
91
|
+
export async function startSlackApp(): Promise<boolean> {
|
|
73
92
|
if (!app) {
|
|
74
93
|
await initSlackApp();
|
|
75
94
|
}
|
|
@@ -84,7 +103,10 @@ export async function startSlackApp(): Promise<void> {
|
|
|
84
103
|
|
|
85
104
|
// Start watching for task completions
|
|
86
105
|
await startTaskWatcher();
|
|
106
|
+
return true;
|
|
87
107
|
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
88
110
|
}
|
|
89
111
|
|
|
90
112
|
export async function stopSlackApp(): Promise<void> {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const SLACK_MODES = ["socket", "http"] as const;
|
|
2
|
+
export type SlackMode = (typeof SLACK_MODES)[number];
|
|
3
|
+
|
|
4
|
+
export type SlackConfiguration = {
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
mode: SlackMode | null;
|
|
7
|
+
missingCredentials: string[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Resolve Slack transport and credential presence without exposing credential values. */
|
|
11
|
+
export function getSlackConfiguration(env: NodeJS.ProcessEnv = process.env): SlackConfiguration {
|
|
12
|
+
const rawMode = env.SLACK_MODE === undefined ? "socket" : env.SLACK_MODE.trim();
|
|
13
|
+
const mode = SLACK_MODES.includes(rawMode as SlackMode) ? (rawMode as SlackMode) : null;
|
|
14
|
+
const missingCredentials = ["SLACK_BOT_TOKEN"];
|
|
15
|
+
|
|
16
|
+
if (mode === "socket") missingCredentials.push("SLACK_APP_TOKEN");
|
|
17
|
+
if (mode === "http") missingCredentials.push("SLACK_SIGNING_SECRET");
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
disabled: env.SLACK_DISABLE === "true" || env.SLACK_DISABLE === "1",
|
|
21
|
+
mode,
|
|
22
|
+
missingCredentials: missingCredentials.filter((key) => !env[key]?.trim()),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isSlackConfigured(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
27
|
+
const config = getSlackConfiguration(env);
|
|
28
|
+
return !config.disabled && config.mode !== null && config.missingCredentials.length === 0;
|
|
29
|
+
}
|
package/src/slack/handlers.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getLeadAgent,
|
|
9
9
|
getMostRecentTaskInThread,
|
|
10
10
|
getTasksByAgentId,
|
|
11
|
+
isExtensionAgent,
|
|
11
12
|
} from "../be/db";
|
|
12
13
|
import { dispatchPre } from "../extensions/dispatcher";
|
|
13
14
|
import { resolveTemplate } from "../prompts/resolver";
|
|
@@ -621,7 +622,12 @@ export function registerMessageHandler(app: App): void {
|
|
|
621
622
|
const { target } = routeResult.data;
|
|
622
623
|
if (target.kind === "agent") {
|
|
623
624
|
const agent = await getAgentById(target.agentId);
|
|
624
|
-
if (agent) {
|
|
625
|
+
if (agent && isExtensionAgent(agent)) {
|
|
626
|
+
console.warn(
|
|
627
|
+
"[Slack] Extension selected an extension-identity agent. Using the built-in router:",
|
|
628
|
+
scrubSecrets(target.agentId),
|
|
629
|
+
);
|
|
630
|
+
} else if (agent) {
|
|
625
631
|
matches = [{ agent, matchedText: "extension" }];
|
|
626
632
|
} else {
|
|
627
633
|
console.warn(
|
package/src/slack/render-v2.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ensureSlackDelegationActivation,
|
|
9
9
|
ensureSlackRenderV2Activation,
|
|
10
10
|
getAgentById,
|
|
11
|
+
getResolvableDeferralOutcomes,
|
|
11
12
|
getSlackOutcomeMessage,
|
|
12
13
|
getSlackTasksInThread,
|
|
13
14
|
getSlackTasksMissingTree,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
getTaskAttachments,
|
|
18
19
|
getTaskById,
|
|
19
20
|
isPendingSlackMessage,
|
|
21
|
+
markSlackDeferralResolved,
|
|
20
22
|
markSlackTreeRendered,
|
|
21
23
|
reserveSlackMessage,
|
|
22
24
|
type SlackConclusionKind,
|
|
@@ -44,7 +46,7 @@ import {
|
|
|
44
46
|
import { buildAskClosure, type ClosureState, closureState } from "./closure";
|
|
45
47
|
import { reactionName, type SlackReactionEvent } from "./reaction-shortcode";
|
|
46
48
|
import { getAgentDisplayName, getAgentEmoji } from "./responses";
|
|
47
|
-
import {
|
|
49
|
+
import { isAwaitingWake, isDeferredTask, slackTaskOutput } from "./task-output";
|
|
48
50
|
|
|
49
51
|
const TREE_UPDATE_DEBOUNCE_MS = 500;
|
|
50
52
|
const TREE_UPDATE_MIN_INTERVAL_MS = 3_000;
|
|
@@ -219,8 +221,16 @@ function isStalledMember(task: AgentTask, now: Date): boolean {
|
|
|
219
221
|
* Blocked detection reuses `checkDependencies` — the same source `get-tasks`
|
|
220
222
|
* `readyOnly` uses — rather than re-deriving it from the tree's own task list,
|
|
221
223
|
* since a dependency can point outside the current Slack thread.
|
|
224
|
+
*
|
|
225
|
+
* A deferral reads ⏳ only while it is parked: once its wake-up settles in
|
|
226
|
+
* `threadTasks`, it falls through to the `completed` it is stored as.
|
|
222
227
|
*/
|
|
223
|
-
async function taskStateGlyph(
|
|
228
|
+
async function taskStateGlyph(
|
|
229
|
+
task: AgentTask,
|
|
230
|
+
now: Date,
|
|
231
|
+
threadTasks: readonly AgentTask[] = [],
|
|
232
|
+
): Promise<string> {
|
|
233
|
+
if (isAwaitingWake(task, threadTasks)) return "⏳";
|
|
224
234
|
switch (task.status) {
|
|
225
235
|
case "backlog":
|
|
226
236
|
case "unassigned":
|
|
@@ -335,6 +345,7 @@ async function renderNodeLines(
|
|
|
335
345
|
depth: number,
|
|
336
346
|
now: Date,
|
|
337
347
|
isAsk: boolean,
|
|
348
|
+
threadTasks: readonly AgentTask[],
|
|
338
349
|
): Promise<string[]> {
|
|
339
350
|
const duration = formatV2Duration(new Date(node.task.createdAt), terminalEnd(node.task, now));
|
|
340
351
|
const indent = FIGURE_SPACE.repeat(
|
|
@@ -344,7 +355,7 @@ async function renderNodeLines(
|
|
|
344
355
|
indent.length > MAX_TREE_PREFIX_LENGTH
|
|
345
356
|
? `${indent.slice(0, MAX_TREE_PREFIX_LENGTH - 1)}…`
|
|
346
357
|
: indent;
|
|
347
|
-
let line = `${boundedPrefix}↳ ${await taskStateGlyph(node.task, now)} ${await renderNodeLabel(node, isAsk)} · ${duration} · ${getTaskLink(node.task.id)}`;
|
|
358
|
+
let line = `${boundedPrefix}↳ ${await taskStateGlyph(node.task, now, threadTasks)} ${await renderNodeLabel(node, isAsk)} · ${duration} · ${getTaskLink(node.task.id)}`;
|
|
348
359
|
const progress = renderProgress(node.task.progress);
|
|
349
360
|
const suffix = isTerminalTreeStatus(node.task.status) ? "" : progress ? ` · ${progress}` : "";
|
|
350
361
|
if (suffix && line.length + suffix.length <= MAX_TREE_NODE_LINE_LENGTH) {
|
|
@@ -355,7 +366,7 @@ async function renderNodeLines(
|
|
|
355
366
|
}
|
|
356
367
|
const lines = [line];
|
|
357
368
|
for (const child of node.children) {
|
|
358
|
-
lines.push(...(await renderNodeLines(child, depth + 1, now, false)));
|
|
369
|
+
lines.push(...(await renderNodeLines(child, depth + 1, now, false, threadTasks)));
|
|
359
370
|
}
|
|
360
371
|
return lines;
|
|
361
372
|
}
|
|
@@ -382,6 +393,8 @@ function threadHeaderText(tasks: AgentTask[], now: Date, duration: string): stri
|
|
|
382
393
|
if (tasks.some((task) => isStalledMember(task, now))) return `🧵 ⚠️ stalled — ${duration}`;
|
|
383
394
|
if (tasks.some((task) => !isTerminalTreeStatus(task.status)))
|
|
384
395
|
return `🧵 🔄 working — ${duration}`;
|
|
396
|
+
// A parked deferral is stored `completed`, but the ask is not answered yet.
|
|
397
|
+
if (tasks.some((task) => isAwaitingWake(task, tasks))) return `🧵 ⏳ waiting — ${duration}`;
|
|
385
398
|
const hasFailure = tasks.some((task) => task.status === "failed");
|
|
386
399
|
return hasFailure ? `🧵 ❌ done with failures — ${duration}` : `🧵 ✅ done — ${duration}`;
|
|
387
400
|
}
|
|
@@ -406,7 +419,7 @@ export async function renderThreadTree(
|
|
|
406
419
|
const lines = [threadHeaderText(tasks, now, threadDuration)];
|
|
407
420
|
const roots = buildRenderForest(tasks);
|
|
408
421
|
for (const root of roots) {
|
|
409
|
-
lines.push(...(await renderNodeLines(root, 1, now, root.task.source === "slack")));
|
|
422
|
+
lines.push(...(await renderNodeLines(root, 1, now, root.task.source === "slack", tasks)));
|
|
410
423
|
}
|
|
411
424
|
const text = normalizeV2Text(lines.join("\n"));
|
|
412
425
|
if (text.length <= MAX_SECTION_LENGTH) return text;
|
|
@@ -416,7 +429,7 @@ export async function renderThreadTree(
|
|
|
416
429
|
for (let index = tasks.length - 1; index >= 0; index--) {
|
|
417
430
|
const task = tasks[index]!;
|
|
418
431
|
const recentLine = (
|
|
419
|
-
await renderNodeLines({ task, children: [] }, 1, now, task.source === "slack")
|
|
432
|
+
await renderNodeLines({ task, children: [] }, 1, now, task.source === "slack", tasks)
|
|
420
433
|
)[0]!;
|
|
421
434
|
const candidateLines = [recentLine, ...recentLines];
|
|
422
435
|
const omitted = index;
|
|
@@ -741,24 +754,52 @@ function outcomeText(value: string | null | undefined, fallback: string): string
|
|
|
741
754
|
return value?.trim() ? value : fallback;
|
|
742
755
|
}
|
|
743
756
|
|
|
757
|
+
// Markdown that only parses at the start of a line: a code fence, a list
|
|
758
|
+
// item, a blockquote, a heading, a table row, a thematic break, or an
|
|
759
|
+
// indented code block. Put after the status icon, it would render as literal
|
|
760
|
+
// text, so the body keeps its own line.
|
|
761
|
+
const LINE_START_BLOCK =
|
|
762
|
+
/^(?:`{3}|~{3}|[-*+]\s|\d+[.)]\s|>|#{1,6}\s|\||(?:-{3,}|\*{3,}|_{3,})[ \t]*(?:\r?\n|$)|(?: {4}|\t))/;
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* `✅ The build passed.` — the status icon (and label, when there is one)
|
|
766
|
+
* opens the body's first line instead of standing alone above it. A body
|
|
767
|
+
* that opens with a line-start construct (see `LINE_START_BLOCK`) keeps the
|
|
768
|
+
* blank line so its markdown still renders.
|
|
769
|
+
*/
|
|
770
|
+
export function withStatusLead(lead: string, body: string): string {
|
|
771
|
+
const text = body.replace(/^(?:[ \t]*\r?\n)+/, "");
|
|
772
|
+
if (LINE_START_BLOCK.test(text)) return `${lead}\n\n${body}`;
|
|
773
|
+
return `${lead} ${text.trimStart()}`;
|
|
774
|
+
}
|
|
775
|
+
|
|
744
776
|
async function outcomeContent(task: AgentTask, slackReplySent: boolean): Promise<string> {
|
|
745
777
|
if (task.status === "failed") {
|
|
746
|
-
return
|
|
778
|
+
return withStatusLead("❌ **Failed:**", outcomeText(task.failureReason, "Task failed."));
|
|
747
779
|
}
|
|
748
780
|
if (task.status === "cancelled") {
|
|
749
|
-
return
|
|
781
|
+
return withStatusLead(
|
|
782
|
+
"🚫 **Cancelled:**",
|
|
783
|
+
outcomeText(task.failureReason, "Task was cancelled."),
|
|
784
|
+
);
|
|
750
785
|
}
|
|
751
786
|
// A deferral's output is engine-authored, never posted by hand via
|
|
752
787
|
// slack-reply, so it must not collapse into the "agent completed" summary
|
|
753
788
|
// even when the agent also sent a slack-reply earlier in the same task.
|
|
754
|
-
const isDeferred = task
|
|
789
|
+
const isDeferred = isDeferredTask(task);
|
|
755
790
|
if (slackReplySent && task.status === "completed" && !isDeferred) {
|
|
756
791
|
const agentName = task.agentId
|
|
757
792
|
? ((await getAgentById(task.agentId))?.name ?? "Agent")
|
|
758
793
|
: "Agent";
|
|
759
794
|
return `✅ ${agentName} completed`;
|
|
760
795
|
}
|
|
761
|
-
|
|
796
|
+
// A deferral is `completed` in the database but unfinished to a human: the
|
|
797
|
+
// work resumes in a wake-up task. Rendering it ✅ told the thread the ask
|
|
798
|
+
// was answered when it was only parked.
|
|
799
|
+
if (isDeferred) {
|
|
800
|
+
return withStatusLead("⏳", outcomeText(slackTaskOutput(task), "Deferred."));
|
|
801
|
+
}
|
|
802
|
+
return withStatusLead("✅", outcomeText(slackTaskOutput(task), "Task completed."));
|
|
762
803
|
}
|
|
763
804
|
|
|
764
805
|
async function agentDisplayNameFor(task: AgentTask): Promise<string> {
|
|
@@ -782,12 +823,24 @@ export async function childOutcomeContent(
|
|
|
782
823
|
): Promise<string> {
|
|
783
824
|
const agentName = await agentDisplayNameFor(task);
|
|
784
825
|
if (task.status === "failed") {
|
|
785
|
-
return
|
|
826
|
+
return withStatusLead(
|
|
827
|
+
`↳ ❌ ${agentName} — failed:`,
|
|
828
|
+
outcomeText(task.failureReason, "Task failed."),
|
|
829
|
+
);
|
|
786
830
|
}
|
|
787
|
-
if (slackReplySent && !task
|
|
831
|
+
if (slackReplySent && !isDeferredTask(task)) {
|
|
788
832
|
return `↳ ✅ ${agentName} completed`;
|
|
789
833
|
}
|
|
790
|
-
|
|
834
|
+
if (isDeferredTask(task)) {
|
|
835
|
+
return withStatusLead(
|
|
836
|
+
`↳ ⏳ ${agentName} — deferred:`,
|
|
837
|
+
outcomeText(slackTaskOutput(task), "Deferred."),
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
return withStatusLead(
|
|
841
|
+
`↳ ✅ ${agentName} — result:`,
|
|
842
|
+
outcomeText(slackTaskOutput(task), "Task completed."),
|
|
843
|
+
);
|
|
791
844
|
}
|
|
792
845
|
|
|
793
846
|
// A deferred wake continues a human conversation even though the scheduler
|
|
@@ -809,7 +862,6 @@ function isSlackContinuation(task: AgentTask): boolean {
|
|
|
809
862
|
*/
|
|
810
863
|
function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): boolean {
|
|
811
864
|
return (
|
|
812
|
-
!isSlackEtaOnlyNotice(task) &&
|
|
813
865
|
task.source !== "slack" &&
|
|
814
866
|
!isSlackContinuation(task) &&
|
|
815
867
|
!!task.slackChannelId &&
|
|
@@ -823,7 +875,6 @@ function isChildCardCandidate(task: AgentTask, delegationActivatedAt: string): b
|
|
|
823
875
|
|
|
824
876
|
function taskNeedsDirectOutcome(task: AgentTask, activatedAt: string): boolean {
|
|
825
877
|
return (
|
|
826
|
-
!isSlackEtaOnlyNotice(task) &&
|
|
827
878
|
((task.source === "slack" && isAskOutcomeStatus(task.status)) ||
|
|
828
879
|
(isSlackContinuation(task) && isOutcomeStatus(task.status))) &&
|
|
829
880
|
task.createdAt >= activatedAt
|
|
@@ -842,7 +893,7 @@ async function conclusionResultsLines(closure: AgentTask[]): Promise<string[]> {
|
|
|
842
893
|
const lines: string[] = [];
|
|
843
894
|
for (const member of closure) {
|
|
844
895
|
if (member.taskType === "follow-up" || member.taskType === "reroute-decision") continue;
|
|
845
|
-
if (!isOutcomeStatus(member.status)
|
|
896
|
+
if (!isOutcomeStatus(member.status)) continue;
|
|
846
897
|
const glyph = await taskStateGlyph(member, new Date());
|
|
847
898
|
const agentName = await agentDisplayNameFor(member);
|
|
848
899
|
const card = await getSlackOutcomeMessage(member.id);
|
|
@@ -1212,6 +1263,119 @@ export async function streamOutcomeCard(
|
|
|
1212
1263
|
});
|
|
1213
1264
|
}
|
|
1214
1265
|
|
|
1266
|
+
/**
|
|
1267
|
+
* The body a resolved deferral card is rewritten to. The ⏳ block promised
|
|
1268
|
+
* the thread an answer; this is that promise being kept in place, instead of
|
|
1269
|
+
* in a second card below it — `isAnsweredByDeferralCard` keeps the wake-up
|
|
1270
|
+
* from posting one, so the answer lands in the thread exactly once. It is
|
|
1271
|
+
* the card the wake-up would have posted, collapsed the same way when the
|
|
1272
|
+
* wake-up already answered by hand with `slack-reply`.
|
|
1273
|
+
*
|
|
1274
|
+
* A wake-up that deferred again does post its own card — the next ⏳ in the
|
|
1275
|
+
* chain, which its own wake-up resolves — so this card only points at it.
|
|
1276
|
+
*/
|
|
1277
|
+
async function resolvedDeferralContent(wake: AgentTask): Promise<string> {
|
|
1278
|
+
if (!isDeferredTask(wake)) {
|
|
1279
|
+
const content = await outcomeContent(wake, wake.slackReplySent);
|
|
1280
|
+
return outcomePresentation(wake, content, attachmentLine(await getTaskAttachments(wake.id)));
|
|
1281
|
+
}
|
|
1282
|
+
const card = await getSlackOutcomeMessage(wake.id);
|
|
1283
|
+
const pointer = card?.permalink ? ` — ${card.permalink}` : ".";
|
|
1284
|
+
return `↪️ Resumed by ${await agentDisplayNameFor(wake)} and deferred again${pointer}`;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* True when `task` is a wake-up whose answer goes into its deferral's ⏳ card
|
|
1289
|
+
* (`refreshResolvedDeferralCards`), so it must not post an outcome card of
|
|
1290
|
+
* its own. False — and the wake-up posts its own card as before — when it
|
|
1291
|
+
* deferred again, when the deferral never got a card, and when the in-place
|
|
1292
|
+
* rewrite was abandoned.
|
|
1293
|
+
*/
|
|
1294
|
+
async function isAnsweredByDeferralCard(
|
|
1295
|
+
task: AgentTask,
|
|
1296
|
+
threadTasks: readonly AgentTask[],
|
|
1297
|
+
): Promise<boolean> {
|
|
1298
|
+
if (!isSlackContinuation(task) || !task.parentTaskId || isDeferredTask(task)) return false;
|
|
1299
|
+
const deferred = threadTasks.find((candidate) => candidate.id === task.parentTaskId);
|
|
1300
|
+
// Mirrors getResolvableDeferralOutcomes: only a `deferredAt` card is rewritten.
|
|
1301
|
+
if (!deferred?.deferredAt) return false;
|
|
1302
|
+
const card = await getSlackOutcomeMessage(deferred.id);
|
|
1303
|
+
return !!card?.finalizedAt && !isPendingSlackMessage(card) && !card.deferralAbandonedAt;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* `callSlackWithRetry` only ever retries rate limits, so any error that
|
|
1308
|
+
* escapes it still carrying a Slack API error code is a verdict rather than a
|
|
1309
|
+
* blip: `message_not_found`, `channel_not_found`, `cant_update_message` and
|
|
1310
|
+
* `msg_too_long` all answer identically on the next tick.
|
|
1311
|
+
*/
|
|
1312
|
+
function isTerminalSlackError(error: unknown): boolean {
|
|
1313
|
+
const code = (error as { data?: { error?: string } }).data?.error;
|
|
1314
|
+
return typeof code === "string" && code.length > 0 && code !== "ratelimited";
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Everything else reaching the catch has no Slack verdict attached (a socket
|
|
1318
|
+
// reset, a DNS blip, a local read that threw), so it is worth retrying — but
|
|
1319
|
+
// only a bounded number of times. Without a ceiling a card that fails for a
|
|
1320
|
+
// reason we cannot classify is re-attempted at tick cadence forever.
|
|
1321
|
+
const DEFERRAL_REFRESH_MAX_ATTEMPTS = 5;
|
|
1322
|
+
const deferralRefreshAttempts = new Map<string, number>();
|
|
1323
|
+
|
|
1324
|
+
/**
|
|
1325
|
+
* Rewrite every ⏳ deferral card whose wake-up task has settled, in place.
|
|
1326
|
+
*
|
|
1327
|
+
* Runs after the tree loop and independently of it: resolution needs only
|
|
1328
|
+
* the retained outcome `ts`, so it must not be skipped when the thread has
|
|
1329
|
+
* no other pending render work. After, so a wake-up that deferred again has
|
|
1330
|
+
* already posted the card this one points at. Each card is rewritten exactly
|
|
1331
|
+
* once (`deferral_resolved_at`).
|
|
1332
|
+
*
|
|
1333
|
+
* The marker is also burned when Slack refuses the rewrite for good, and when
|
|
1334
|
+
* an unclassifiable failure has used up its attempts — a card that can never
|
|
1335
|
+
* be rewritten must stop competing for the per-tick row budget with cards
|
|
1336
|
+
* that still can. Those are marked abandoned, so the wake-up's answer goes
|
|
1337
|
+
* out in its own card on the next tick instead of being lost.
|
|
1338
|
+
*/
|
|
1339
|
+
export async function refreshResolvedDeferralCards(): Promise<void> {
|
|
1340
|
+
const app = getSlackApp();
|
|
1341
|
+
if (!app) return;
|
|
1342
|
+
for (const { card, wakeTaskId } of await getResolvableDeferralOutcomes()) {
|
|
1343
|
+
try {
|
|
1344
|
+
const wake = await getTaskById(wakeTaskId);
|
|
1345
|
+
if (!wake) continue;
|
|
1346
|
+
const content = await resolvedDeferralContent(wake);
|
|
1347
|
+
await callSlackWithRetry(app.client, "chat.update", {
|
|
1348
|
+
channel: card.channelId,
|
|
1349
|
+
ts: card.ts,
|
|
1350
|
+
text: content,
|
|
1351
|
+
});
|
|
1352
|
+
await markSlackDeferralResolved(card.id);
|
|
1353
|
+
deferralRefreshAttempts.delete(card.id);
|
|
1354
|
+
// The rewrite is the wake-up's outcome, so it settles the ask's reaction.
|
|
1355
|
+
await finalizeTerminalSlackReactions([wake]);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
const attempts = (deferralRefreshAttempts.get(card.id) ?? 0) + 1;
|
|
1358
|
+
deferralRefreshAttempts.set(card.id, attempts);
|
|
1359
|
+
const terminal = isTerminalSlackError(error);
|
|
1360
|
+
if (terminal || attempts >= DEFERRAL_REFRESH_MAX_ATTEMPTS) {
|
|
1361
|
+
await markSlackDeferralResolved(card.id, { abandoned: true });
|
|
1362
|
+
deferralRefreshAttempts.delete(card.id);
|
|
1363
|
+
console.error(
|
|
1364
|
+
`[Slack] Giving up on deferral card ${card.id} after ${attempts} attempt(s)` +
|
|
1365
|
+
`${terminal ? " (Slack refused the rewrite)" : ""}:`,
|
|
1366
|
+
error,
|
|
1367
|
+
);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
console.error(
|
|
1371
|
+
`[Slack] Failed to resolve deferral card ${card.id} ` +
|
|
1372
|
+
`(attempt ${attempts}/${DEFERRAL_REFRESH_MAX_ATTEMPTS}):`,
|
|
1373
|
+
error,
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1215
1379
|
export async function processSlackRenderV2(): Promise<void> {
|
|
1216
1380
|
if (!isSlackRenderV2Enabled()) return;
|
|
1217
1381
|
const activatedAt = await ensureSlackRenderV2Activation();
|
|
@@ -1270,7 +1434,9 @@ export async function processSlackRenderV2(): Promise<void> {
|
|
|
1270
1434
|
// not represent immediately active outcome work, so avoid waking an old
|
|
1271
1435
|
// tree solely to verify its permalink on every render tick.
|
|
1272
1436
|
const isDirectCandidate =
|
|
1273
|
-
taskNeedsDirectOutcome(task, activatedAt) &&
|
|
1437
|
+
taskNeedsDirectOutcome(task, activatedAt) &&
|
|
1438
|
+
isOutcomeStatus(task.status) &&
|
|
1439
|
+
!(await isAnsweredByDeferralCard(task, tasks));
|
|
1274
1440
|
const isChildCandidate =
|
|
1275
1441
|
delegationEnabled &&
|
|
1276
1442
|
delegationActivatedAt !== null &&
|
|
@@ -1365,6 +1531,7 @@ export async function processSlackRenderV2(): Promise<void> {
|
|
|
1365
1531
|
if (!isSlackRenderV2Enabled()) return;
|
|
1366
1532
|
if (!taskNeedsDirectOutcome(task, activatedAt)) continue;
|
|
1367
1533
|
if ((await getSlackOutcomeMessage(task.id))?.finalizedAt) continue;
|
|
1534
|
+
if (await isAnsweredByDeferralCard(task, tasks)) continue;
|
|
1368
1535
|
const deferByClosure =
|
|
1369
1536
|
task.source === "slack" &&
|
|
1370
1537
|
delegationEnabled &&
|
|
@@ -1444,6 +1611,8 @@ export async function processSlackRenderV2(): Promise<void> {
|
|
|
1444
1611
|
console.error(`[Slack] Failed to update v2 tree ${tree.id}:`, error);
|
|
1445
1612
|
}
|
|
1446
1613
|
}
|
|
1614
|
+
|
|
1615
|
+
await refreshResolvedDeferralCards();
|
|
1447
1616
|
}
|
|
1448
1617
|
|
|
1449
1618
|
export function _resetSlackRenderV2ForTests(): void {
|
package/src/slack/responses.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
markdownToSlack,
|
|
16
16
|
splitSlackSectionText,
|
|
17
17
|
} from "./blocks";
|
|
18
|
-
import {
|
|
18
|
+
import { slackTaskOutput } from "./task-output";
|
|
19
19
|
|
|
20
20
|
// Re-export for backward compatibility
|
|
21
21
|
export { markdownToSlack } from "./blocks";
|
|
@@ -116,7 +116,6 @@ export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
|
|
|
116
116
|
* Send a task completion message to Slack with the agent's persona.
|
|
117
117
|
*/
|
|
118
118
|
export async function sendTaskResponse(task: AgentTask): Promise<boolean> {
|
|
119
|
-
if (isSlackEtaOnlyNotice(task)) return true;
|
|
120
119
|
const app = getSlackApp();
|
|
121
120
|
if (!app || !task.slackChannelId || !task.slackThreadTs) {
|
|
122
121
|
return false;
|
|
@@ -287,7 +286,6 @@ export async function updateToFinal(
|
|
|
287
286
|
task: AgentTask,
|
|
288
287
|
messageTs: string,
|
|
289
288
|
): Promise<SlackUpdateResult> {
|
|
290
|
-
if (isSlackEtaOnlyNotice(task)) return "ok";
|
|
291
289
|
const app = getSlackApp();
|
|
292
290
|
if (!app || !task.slackChannelId || !task.agentId) return "failed";
|
|
293
291
|
|