@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
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
processSlackRenderV2,
|
|
40
40
|
renderThreadTree,
|
|
41
41
|
streamOutcomeCard,
|
|
42
|
+
withStatusLead,
|
|
42
43
|
} from "../slack/render-v2";
|
|
43
44
|
import { getAgentDisplayName, getAgentEmoji } from "../slack/responses";
|
|
44
45
|
import { slackContextKey } from "../tasks/context-key";
|
|
@@ -54,6 +55,9 @@ let permalinkFailuresRemaining = 0;
|
|
|
54
55
|
let slackAddressSequence = 0;
|
|
55
56
|
let missingMessageTs: string | undefined;
|
|
56
57
|
let updateFailuresRemaining = 0;
|
|
58
|
+
// A `chat.update` that answers with a Slack verdict other than 404 — the
|
|
59
|
+
// permanent, non-retryable class (`cant_update_message`, `channel_not_found`).
|
|
60
|
+
let rejectedUpdateTs: string | undefined;
|
|
57
61
|
let disableRenderAfterMethod: string | undefined;
|
|
58
62
|
|
|
59
63
|
type RemoteMessage = {
|
|
@@ -207,6 +211,7 @@ const mockApiCall = mock(async (method: string, payload: Record<string, unknown>
|
|
|
207
211
|
throw new Error("temporary update failure");
|
|
208
212
|
}
|
|
209
213
|
if (missingMessageTs === payload.ts) throw { data: { error: "message_not_found" } };
|
|
214
|
+
if (rejectedUpdateTs === payload.ts) throw { data: { error: "cant_update_message" } };
|
|
210
215
|
const message = remoteMessages.get(remoteKey(String(payload.channel), String(payload.ts)));
|
|
211
216
|
if (!message) throw { data: { error: "message_not_found" } };
|
|
212
217
|
const barrier = nextUpdateBarrier;
|
|
@@ -263,6 +268,7 @@ beforeEach(async () => {
|
|
|
263
268
|
permalinkFailuresRemaining = 0;
|
|
264
269
|
missingMessageTs = undefined;
|
|
265
270
|
updateFailuresRemaining = 0;
|
|
271
|
+
rejectedUpdateTs = undefined;
|
|
266
272
|
disableRenderAfterMethod = undefined;
|
|
267
273
|
nextUpdateBarrier = undefined;
|
|
268
274
|
_resetSlackRenderV2ForTests();
|
|
@@ -352,7 +358,7 @@ describe("Slack renderer v2", () => {
|
|
|
352
358
|
expect(streams[0]?.payload).toMatchObject({
|
|
353
359
|
channel: channelId,
|
|
354
360
|
thread_ts: threadTs,
|
|
355
|
-
markdown_text: "
|
|
361
|
+
markdown_text: "✅ Here is the complete answer.",
|
|
356
362
|
});
|
|
357
363
|
});
|
|
358
364
|
}
|
|
@@ -883,6 +889,41 @@ describe("Slack renderer v2", () => {
|
|
|
883
889
|
expect(doneText.startsWith("🧵 ✅ done")).toBe(true);
|
|
884
890
|
});
|
|
885
891
|
|
|
892
|
+
test("a parked deferral reads waiting, and done once its wake-up settles", async () => {
|
|
893
|
+
const lead = await createAgent({ name: "Parked Header Lead", isLead: true, status: "idle" });
|
|
894
|
+
const ask = await createTaskExtended("parked header ask", {
|
|
895
|
+
agentId: lead.id,
|
|
896
|
+
source: "slack",
|
|
897
|
+
});
|
|
898
|
+
const now = new Date();
|
|
899
|
+
const parked = {
|
|
900
|
+
...ask,
|
|
901
|
+
status: "completed" as const,
|
|
902
|
+
deferredAt: now.toISOString(),
|
|
903
|
+
lastUpdatedAt: now.toISOString(),
|
|
904
|
+
finishedAt: now.toISOString(),
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// Stored `completed`, but nothing has answered the ask yet.
|
|
908
|
+
const waitingText = await renderThreadTree([parked], new Map(), now);
|
|
909
|
+
expect(waitingText.startsWith("🧵 ⏳ waiting")).toBe(true);
|
|
910
|
+
expect(waitingText).toContain("↳ ⏳ parked header ask");
|
|
911
|
+
|
|
912
|
+
const wake = {
|
|
913
|
+
...parked,
|
|
914
|
+
id: crypto.randomUUID(),
|
|
915
|
+
source: "schedule" as const,
|
|
916
|
+
taskType: "deferred",
|
|
917
|
+
parentTaskId: ask.id,
|
|
918
|
+
deferredAt: undefined,
|
|
919
|
+
};
|
|
920
|
+
const doneText = await renderThreadTree([parked, wake], new Map(), now);
|
|
921
|
+
expect(doneText.startsWith("🧵 ✅ done")).toBe(true);
|
|
922
|
+
// The ask's own line settles with its wake-up, not only the header.
|
|
923
|
+
expect(doneText).not.toContain("⏳");
|
|
924
|
+
expect(doneText).toContain("↳ ✅ parked header ask");
|
|
925
|
+
});
|
|
926
|
+
|
|
886
927
|
test("does not resolve or render direct-trigger permalink backlinks", async () => {
|
|
887
928
|
const lead = await createAgent({ name: "Trigger Lead", isLead: true, status: "idle" });
|
|
888
929
|
const worker = await createAgent({ name: "Trigger Worker", isLead: false, status: "idle" });
|
|
@@ -1145,7 +1186,7 @@ describe("Slack renderer v2", () => {
|
|
|
1145
1186
|
const outcomeBody = outcomeChunks.join("");
|
|
1146
1187
|
expect(outcomeChunks).toHaveLength(1);
|
|
1147
1188
|
expect(outcomeBody).toBe(
|
|
1148
|
-
"
|
|
1189
|
+
"✅ Implemented the Slack renderer and opened a focused pull request.\n\nSecond paragraph that must be rendered.",
|
|
1149
1190
|
);
|
|
1150
1191
|
expect(outcomeBody).not.toMatch(/\n{3,}/);
|
|
1151
1192
|
expect(outcomeBody).toBe(outcomeBody.trim());
|
|
@@ -1155,7 +1196,7 @@ describe("Slack renderer v2", () => {
|
|
|
1155
1196
|
expect(started.payload.thread_ts).toBe(threadTs);
|
|
1156
1197
|
expect(started.payload.recipient_user_id).toBe("U_REQUESTER");
|
|
1157
1198
|
expect(started.payload.recipient_team_id).toBe("T_TEST");
|
|
1158
|
-
expect(String(started.payload.markdown_text).startsWith("
|
|
1199
|
+
expect(String(started.payload.markdown_text).startsWith("✅ Implemented")).toBe(true);
|
|
1159
1200
|
expect(Object.keys(started.payload).sort()).toEqual([
|
|
1160
1201
|
"channel",
|
|
1161
1202
|
"icon_emoji",
|
|
@@ -1339,7 +1380,7 @@ describe("Slack renderer v2", () => {
|
|
|
1339
1380
|
await processSlackRenderV2();
|
|
1340
1381
|
|
|
1341
1382
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1342
|
-
expect(started?.payload.markdown_text).toBe(
|
|
1383
|
+
expect(started?.payload.markdown_text).toBe(`✅ Done${expected ? `\n\n${expected}` : ""}`);
|
|
1343
1384
|
} finally {
|
|
1344
1385
|
if (originalHost === undefined) delete process.env.AGENT_FS_LIVE_URL;
|
|
1345
1386
|
else process.env.AGENT_FS_LIVE_URL = originalHost;
|
|
@@ -1417,10 +1458,10 @@ describe("Slack renderer v2", () => {
|
|
|
1417
1458
|
await processSlackRenderV2();
|
|
1418
1459
|
|
|
1419
1460
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1420
|
-
expect(started?.payload.markdown_text).toContain("❌ **Failed
|
|
1461
|
+
expect(started?.payload.markdown_text).toContain("❌ **Failed:**");
|
|
1421
1462
|
const outcome = await getSlackOutcomeMessage(ask.id);
|
|
1422
1463
|
const remote = remoteMessages.get(remoteKey(channelId, outcome!.ts));
|
|
1423
|
-
expect(remote?.text).toBe(`❌ **Failed
|
|
1464
|
+
expect(remote?.text).toBe(`❌ **Failed:** ${reason.trim()}`);
|
|
1424
1465
|
expect(remote?.text).not.toContain(getTaskLink(ask.id));
|
|
1425
1466
|
expect(calls.some((call) => call.method === "chat.appendStream")).toBe(false);
|
|
1426
1467
|
const update = calls.find(
|
|
@@ -1450,11 +1491,11 @@ describe("Slack renderer v2", () => {
|
|
|
1450
1491
|
await processSlackRenderV2();
|
|
1451
1492
|
|
|
1452
1493
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1453
|
-
expect(started?.payload.markdown_text).toContain("🚫 **Cancelled
|
|
1494
|
+
expect(started?.payload.markdown_text).toContain("🚫 **Cancelled:**");
|
|
1454
1495
|
const outcome = (await getSlackOutcomeMessage(ask.id))!;
|
|
1455
1496
|
const remote = remoteMessages.get(remoteKey(channelId, outcome.ts));
|
|
1456
1497
|
expect(remote?.text).toBe(
|
|
1457
|
-
`🚫 **Cancelled
|
|
1498
|
+
`🚫 **Cancelled:** requester changed direction ${"context ".repeat(200)}`.trim(),
|
|
1458
1499
|
);
|
|
1459
1500
|
expect(remote?.text).not.toContain(getTaskLink(ask.id));
|
|
1460
1501
|
expect(calls.some((call) => call.method === "chat.appendStream")).toBe(false);
|
|
@@ -1719,7 +1760,7 @@ describe("Slack renderer v2", () => {
|
|
|
1719
1760
|
|
|
1720
1761
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1721
1762
|
expect(started?.payload.markdown_text).toBe(
|
|
1722
|
-
"
|
|
1763
|
+
"✅ This output must reach Slack since no slack-reply was sent.",
|
|
1723
1764
|
);
|
|
1724
1765
|
});
|
|
1725
1766
|
|
|
@@ -1758,7 +1799,7 @@ describe("Slack renderer v2", () => {
|
|
|
1758
1799
|
|
|
1759
1800
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1760
1801
|
expect(started?.payload.markdown_text).toBe(
|
|
1761
|
-
|
|
1802
|
+
`✅ Artifact: https://example.test/downloads/${redacted}/result.json`,
|
|
1762
1803
|
);
|
|
1763
1804
|
expect(started?.payload.markdown_text).not.toContain(secret);
|
|
1764
1805
|
});
|
|
@@ -1789,7 +1830,7 @@ describe("Slack renderer v2", () => {
|
|
|
1789
1830
|
expect(outcome?.finalizedAt).toBeDefined();
|
|
1790
1831
|
});
|
|
1791
1832
|
|
|
1792
|
-
test("
|
|
1833
|
+
test("renders a legacy deferral as ⏳ with its ETA, even after a slack-reply", async () => {
|
|
1793
1834
|
const lead = await createAgent({ name: "Deferring Lead", isLead: true, status: "idle" });
|
|
1794
1835
|
const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_DEFER_AFTER_REPLY");
|
|
1795
1836
|
const ask = await createTaskExtended("defer after replying by hand", {
|
|
@@ -1814,14 +1855,362 @@ describe("Slack renderer v2", () => {
|
|
|
1814
1855
|
await processSlackRenderV2();
|
|
1815
1856
|
|
|
1816
1857
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1817
|
-
|
|
1818
|
-
|
|
1858
|
+
// ⏳ not ✅: the ask was parked, not answered. The ETA is what a human
|
|
1859
|
+
// needs; the agent's internal note and the schedule link are dropped.
|
|
1860
|
+
expect(started?.payload.markdown_text).toBe("⏳ Checking back today 17:30:19 UTC");
|
|
1861
|
+
expect(JSON.stringify(calls)).not.toContain("checking the new defer card");
|
|
1862
|
+
expect(JSON.stringify(calls)).not.toContain("715bf847-fe3e");
|
|
1819
1863
|
expect(JSON.stringify(calls)).not.toContain("Deferred until");
|
|
1820
1864
|
expect((await getTaskById(ask.id))?.output).toBe(deferralLine);
|
|
1821
1865
|
expect(started?.payload.markdown_text).not.toBe(`✅ ${lead.name} completed`);
|
|
1822
1866
|
});
|
|
1823
1867
|
|
|
1824
|
-
test("
|
|
1868
|
+
test("a deferral card opens with ⏳ and closes in place when the wake-up settles", async () => {
|
|
1869
|
+
const lead = await createAgent({ name: "Parking Lead", isLead: true, status: "idle" });
|
|
1870
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_DEFER_RESOLVE");
|
|
1871
|
+
const ask = await createTaskExtended("answer when the build is done", {
|
|
1872
|
+
agentId: lead.id,
|
|
1873
|
+
source: "slack",
|
|
1874
|
+
slackChannelId: channelId,
|
|
1875
|
+
slackThreadTs: threadTs,
|
|
1876
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
1877
|
+
});
|
|
1878
|
+
await startTask(ask.id);
|
|
1879
|
+
// Mirrors defer-task.ts: the human card as stored output, plus the marker
|
|
1880
|
+
// that tells "parked" from "done" on a row whose status is `completed`.
|
|
1881
|
+
await completeTask(ask.id, "Checking back today at 18:38", {
|
|
1882
|
+
addTags: ["deferred"],
|
|
1883
|
+
deferredAt: new Date().toISOString(),
|
|
1884
|
+
});
|
|
1885
|
+
await backdateLastUpdated([ask.id], 20);
|
|
1886
|
+
calls.length = 0;
|
|
1887
|
+
await processSlackRenderV2();
|
|
1888
|
+
|
|
1889
|
+
const opening = calls.find((call) => call.method === "chat.startStream");
|
|
1890
|
+
expect(opening?.payload.markdown_text).toBe("⏳ Checking back today at 18:38");
|
|
1891
|
+
const card = await getSlackOutcomeMessage(ask.id);
|
|
1892
|
+
expect(card?.finalizedAt).toBeTruthy();
|
|
1893
|
+
expect(card?.deferralResolvedAt).toBeUndefined();
|
|
1894
|
+
|
|
1895
|
+
// The wake-up task runs and finishes.
|
|
1896
|
+
const schedule = await createScheduledTask({
|
|
1897
|
+
name: "deferred-resolve",
|
|
1898
|
+
scheduleType: "one_time",
|
|
1899
|
+
taskTemplate: "Deliver the result",
|
|
1900
|
+
taskType: "deferred",
|
|
1901
|
+
tags: ["deferred"],
|
|
1902
|
+
parentTaskId: ask.id,
|
|
1903
|
+
targetAgentId: lead.id,
|
|
1904
|
+
createdByAgentId: lead.id,
|
|
1905
|
+
nextRunAt: new Date().toISOString(),
|
|
1906
|
+
});
|
|
1907
|
+
const wake = await createStandaloneScheduleTask(schedule);
|
|
1908
|
+
await startTask(wake.id);
|
|
1909
|
+
await completeTask(wake.id, "The build passed.");
|
|
1910
|
+
calls.length = 0;
|
|
1911
|
+
await processSlackRenderV2();
|
|
1912
|
+
|
|
1913
|
+
// The SAME message is rewritten — no new opening card for the ask.
|
|
1914
|
+
const rewrite = calls.find(
|
|
1915
|
+
(call) => call.method === "chat.update" && call.payload.ts === card?.ts,
|
|
1916
|
+
);
|
|
1917
|
+
expect(rewrite).toBeDefined();
|
|
1918
|
+
expect(rewrite?.payload.text).toBe("✅ The build passed.");
|
|
1919
|
+
expect(rewrite?.payload.text).not.toContain("Checking back");
|
|
1920
|
+
expect((await getSlackOutcomeMessage(ask.id))?.deferralResolvedAt).toBeTruthy();
|
|
1921
|
+
// The rewrite IS the wake-up's answer: no second card repeating it.
|
|
1922
|
+
expect(calls.filter((call) => call.method === "chat.startStream")).toHaveLength(0);
|
|
1923
|
+
expect(await getSlackOutcomeMessage(wake.id)).toBeNull();
|
|
1924
|
+
|
|
1925
|
+
// Idempotent: a second tick must not rewrite it again.
|
|
1926
|
+
calls.length = 0;
|
|
1927
|
+
await processSlackRenderV2();
|
|
1928
|
+
expect(
|
|
1929
|
+
calls.filter((call) => call.method === "chat.update" && call.payload.ts === card?.ts),
|
|
1930
|
+
).toHaveLength(0);
|
|
1931
|
+
});
|
|
1932
|
+
|
|
1933
|
+
test("a parked ask keeps 👀 and settles to ✅ only when its wake-up answers", async () => {
|
|
1934
|
+
const lead = await createAgent({ name: "Parked Reaction Lead", isLead: true, status: "idle" });
|
|
1935
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_DEFER_REACTION");
|
|
1936
|
+
const triggerTs = `${slackAddressSequence}.2`;
|
|
1937
|
+
const ask = await createTaskExtended("answer when the build is done", {
|
|
1938
|
+
agentId: lead.id,
|
|
1939
|
+
source: "slack",
|
|
1940
|
+
slackChannelId: channelId,
|
|
1941
|
+
slackThreadTs: threadTs,
|
|
1942
|
+
slackTriggerMessageTs: triggerTs,
|
|
1943
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
1944
|
+
});
|
|
1945
|
+
await startTask(ask.id);
|
|
1946
|
+
await completeTask(ask.id, "Checking back today at 18:38", {
|
|
1947
|
+
addTags: ["deferred"],
|
|
1948
|
+
deferredAt: new Date().toISOString(),
|
|
1949
|
+
});
|
|
1950
|
+
await backdateLastUpdated([ask.id], 20);
|
|
1951
|
+
calls.length = 0;
|
|
1952
|
+
await processSlackRenderV2();
|
|
1953
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1954
|
+
const doneReaction = {
|
|
1955
|
+
method: "reactions.add",
|
|
1956
|
+
payload: { channel: channelId, name: "white_check_mark", timestamp: triggerTs },
|
|
1957
|
+
};
|
|
1958
|
+
expect(calls).not.toContainEqual(doneReaction);
|
|
1959
|
+
|
|
1960
|
+
const schedule = await createScheduledTask({
|
|
1961
|
+
name: "deferred-reaction",
|
|
1962
|
+
scheduleType: "one_time",
|
|
1963
|
+
taskTemplate: "Deliver the result",
|
|
1964
|
+
taskType: "deferred",
|
|
1965
|
+
tags: ["deferred"],
|
|
1966
|
+
parentTaskId: ask.id,
|
|
1967
|
+
targetAgentId: lead.id,
|
|
1968
|
+
createdByAgentId: lead.id,
|
|
1969
|
+
nextRunAt: new Date().toISOString(),
|
|
1970
|
+
});
|
|
1971
|
+
const wake = await createStandaloneScheduleTask(schedule);
|
|
1972
|
+
// The wake-up does not inherit the trigger ts; the reaction path walks back.
|
|
1973
|
+
expect(wake.slackTriggerMessageTs).toBeUndefined();
|
|
1974
|
+
await startTask(wake.id);
|
|
1975
|
+
await completeTask(wake.id, "The build passed.");
|
|
1976
|
+
calls.length = 0;
|
|
1977
|
+
await processSlackRenderV2();
|
|
1978
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1979
|
+
expect(calls).toContainEqual(doneReaction);
|
|
1980
|
+
});
|
|
1981
|
+
|
|
1982
|
+
test("a wake-up that deferred again posts the next ⏳ card, and the old card points at it", async () => {
|
|
1983
|
+
const lead = await createAgent({ name: "Chained Lead", isLead: true, status: "idle" });
|
|
1984
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_DEFER_CHAIN");
|
|
1985
|
+
const ask = await createTaskExtended("answer when the build is done", {
|
|
1986
|
+
agentId: lead.id,
|
|
1987
|
+
source: "slack",
|
|
1988
|
+
slackChannelId: channelId,
|
|
1989
|
+
slackThreadTs: threadTs,
|
|
1990
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
1991
|
+
});
|
|
1992
|
+
await startTask(ask.id);
|
|
1993
|
+
await completeTask(ask.id, "Checking back today at 18:38", {
|
|
1994
|
+
addTags: ["deferred"],
|
|
1995
|
+
deferredAt: new Date().toISOString(),
|
|
1996
|
+
});
|
|
1997
|
+
await backdateLastUpdated([ask.id], 20);
|
|
1998
|
+
await processSlackRenderV2();
|
|
1999
|
+
const card = await getSlackOutcomeMessage(ask.id);
|
|
2000
|
+
|
|
2001
|
+
const schedule = await createScheduledTask({
|
|
2002
|
+
name: "deferred-chain",
|
|
2003
|
+
scheduleType: "one_time",
|
|
2004
|
+
taskTemplate: "Deliver the result",
|
|
2005
|
+
taskType: "deferred",
|
|
2006
|
+
tags: ["deferred"],
|
|
2007
|
+
parentTaskId: ask.id,
|
|
2008
|
+
targetAgentId: lead.id,
|
|
2009
|
+
createdByAgentId: lead.id,
|
|
2010
|
+
nextRunAt: new Date().toISOString(),
|
|
2011
|
+
});
|
|
2012
|
+
const wake = await createStandaloneScheduleTask(schedule);
|
|
2013
|
+
await startTask(wake.id);
|
|
2014
|
+
await completeTask(wake.id, "Checking back today at 20:00", {
|
|
2015
|
+
addTags: ["deferred"],
|
|
2016
|
+
deferredAt: new Date().toISOString(),
|
|
2017
|
+
});
|
|
2018
|
+
calls.length = 0;
|
|
2019
|
+
await processSlackRenderV2();
|
|
2020
|
+
|
|
2021
|
+
// The chain continues on a new ⏳ card, which the next wake-up resolves.
|
|
2022
|
+
const next = calls.find((call) => call.method === "chat.startStream");
|
|
2023
|
+
expect(next?.payload.markdown_text).toBe("⏳ Checking back today at 20:00");
|
|
2024
|
+
const nextCard = await getSlackOutcomeMessage(wake.id);
|
|
2025
|
+
expect(nextCard?.permalink).toBeTruthy();
|
|
2026
|
+
// The old card hands over to it rather than repeating the new ETA.
|
|
2027
|
+
const rewrite = calls.find(
|
|
2028
|
+
(call) => call.method === "chat.update" && call.payload.ts === card?.ts,
|
|
2029
|
+
);
|
|
2030
|
+
expect(rewrite?.payload.text).toBe(
|
|
2031
|
+
`↪️ Resumed by ${lead.name} and deferred again — ${nextCard?.permalink}`,
|
|
2032
|
+
);
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
test("a wake-up that failed closes the deferral card as failed, not as done", async () => {
|
|
2036
|
+
const lead = await createAgent({ name: "Failing Wake Lead", isLead: true, status: "idle" });
|
|
2037
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_DEFER_RESOLVE_FAIL");
|
|
2038
|
+
const ask = await createTaskExtended("answer later", {
|
|
2039
|
+
agentId: lead.id,
|
|
2040
|
+
source: "slack",
|
|
2041
|
+
slackChannelId: channelId,
|
|
2042
|
+
slackThreadTs: threadTs,
|
|
2043
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
2044
|
+
});
|
|
2045
|
+
await startTask(ask.id);
|
|
2046
|
+
await completeTask(ask.id, "Waiting on Researcher — or today at 18:38 at the latest", {
|
|
2047
|
+
addTags: ["deferred"],
|
|
2048
|
+
deferredAt: new Date().toISOString(),
|
|
2049
|
+
});
|
|
2050
|
+
await backdateLastUpdated([ask.id], 20);
|
|
2051
|
+
await processSlackRenderV2();
|
|
2052
|
+
const card = await getSlackOutcomeMessage(ask.id);
|
|
2053
|
+
|
|
2054
|
+
const schedule = await createScheduledTask({
|
|
2055
|
+
name: "deferred-resolve-fail",
|
|
2056
|
+
scheduleType: "one_time",
|
|
2057
|
+
taskTemplate: "Deliver the result",
|
|
2058
|
+
taskType: "deferred",
|
|
2059
|
+
tags: ["deferred"],
|
|
2060
|
+
parentTaskId: ask.id,
|
|
2061
|
+
targetAgentId: lead.id,
|
|
2062
|
+
createdByAgentId: lead.id,
|
|
2063
|
+
nextRunAt: new Date().toISOString(),
|
|
2064
|
+
});
|
|
2065
|
+
const wake = await createStandaloneScheduleTask(schedule);
|
|
2066
|
+
await startTask(wake.id);
|
|
2067
|
+
await failTask(wake.id, "the build never finished");
|
|
2068
|
+
calls.length = 0;
|
|
2069
|
+
await processSlackRenderV2();
|
|
2070
|
+
|
|
2071
|
+
const rewrite = calls.find(
|
|
2072
|
+
(call) => call.method === "chat.update" && call.payload.ts === card?.ts,
|
|
2073
|
+
);
|
|
2074
|
+
expect(rewrite?.payload.text).toContain("❌");
|
|
2075
|
+
expect(rewrite?.payload.text).toContain("the build never finished");
|
|
2076
|
+
expect(rewrite?.payload.text).not.toContain("Waiting on Researcher");
|
|
2077
|
+
expect(calls.filter((call) => call.method === "chat.startStream")).toHaveLength(0);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
test("a deferral whose wake-up is still running keeps its ⏳ card untouched", async () => {
|
|
2081
|
+
const lead = await createAgent({ name: "Pending Wake Lead", isLead: true, status: "idle" });
|
|
2082
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_DEFER_STILL_WAITING");
|
|
2083
|
+
const ask = await createTaskExtended("answer eventually", {
|
|
2084
|
+
agentId: lead.id,
|
|
2085
|
+
source: "slack",
|
|
2086
|
+
slackChannelId: channelId,
|
|
2087
|
+
slackThreadTs: threadTs,
|
|
2088
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
2089
|
+
});
|
|
2090
|
+
await startTask(ask.id);
|
|
2091
|
+
await completeTask(ask.id, "Checking back today at 18:38", {
|
|
2092
|
+
addTags: ["deferred"],
|
|
2093
|
+
deferredAt: new Date().toISOString(),
|
|
2094
|
+
});
|
|
2095
|
+
await backdateLastUpdated([ask.id], 20);
|
|
2096
|
+
await processSlackRenderV2();
|
|
2097
|
+
const card = await getSlackOutcomeMessage(ask.id);
|
|
2098
|
+
|
|
2099
|
+
const schedule = await createScheduledTask({
|
|
2100
|
+
name: "deferred-still-running",
|
|
2101
|
+
scheduleType: "one_time",
|
|
2102
|
+
taskTemplate: "Deliver the result",
|
|
2103
|
+
taskType: "deferred",
|
|
2104
|
+
tags: ["deferred"],
|
|
2105
|
+
parentTaskId: ask.id,
|
|
2106
|
+
targetAgentId: lead.id,
|
|
2107
|
+
createdByAgentId: lead.id,
|
|
2108
|
+
nextRunAt: new Date().toISOString(),
|
|
2109
|
+
});
|
|
2110
|
+
const wake = await createStandaloneScheduleTask(schedule);
|
|
2111
|
+
await startTask(wake.id);
|
|
2112
|
+
calls.length = 0;
|
|
2113
|
+
await processSlackRenderV2();
|
|
2114
|
+
|
|
2115
|
+
expect(
|
|
2116
|
+
calls.filter((call) => call.method === "chat.update" && call.payload.ts === card?.ts),
|
|
2117
|
+
).toHaveLength(0);
|
|
2118
|
+
expect((await getSlackOutcomeMessage(ask.id))?.deferralResolvedAt).toBeUndefined();
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
/**
|
|
2122
|
+
* Sets up a deferral card whose wake-up has already settled, so the next
|
|
2123
|
+
* tick is the one that tries to rewrite the card in place.
|
|
2124
|
+
*/
|
|
2125
|
+
async function resolvableDeferralCard(label: string) {
|
|
2126
|
+
const lead = await createAgent({ name: `${label} Lead`, isLead: true, status: "idle" });
|
|
2127
|
+
const { channelId, threadTs } = uniqueSlackAddress(label);
|
|
2128
|
+
const ask = await createTaskExtended("answer when the build is done", {
|
|
2129
|
+
agentId: lead.id,
|
|
2130
|
+
source: "slack",
|
|
2131
|
+
slackChannelId: channelId,
|
|
2132
|
+
slackThreadTs: threadTs,
|
|
2133
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
2134
|
+
});
|
|
2135
|
+
await startTask(ask.id);
|
|
2136
|
+
await completeTask(ask.id, "Checking back today at 18:38", {
|
|
2137
|
+
addTags: ["deferred"],
|
|
2138
|
+
deferredAt: new Date().toISOString(),
|
|
2139
|
+
});
|
|
2140
|
+
await backdateLastUpdated([ask.id], 20);
|
|
2141
|
+
await processSlackRenderV2();
|
|
2142
|
+
const card = await getSlackOutcomeMessage(ask.id);
|
|
2143
|
+
|
|
2144
|
+
const schedule = await createScheduledTask({
|
|
2145
|
+
name: `${label}-resolve`,
|
|
2146
|
+
scheduleType: "one_time",
|
|
2147
|
+
taskTemplate: "Deliver the result",
|
|
2148
|
+
taskType: "deferred",
|
|
2149
|
+
tags: ["deferred"],
|
|
2150
|
+
parentTaskId: ask.id,
|
|
2151
|
+
targetAgentId: lead.id,
|
|
2152
|
+
createdByAgentId: lead.id,
|
|
2153
|
+
nextRunAt: new Date().toISOString(),
|
|
2154
|
+
});
|
|
2155
|
+
const wake = await createStandaloneScheduleTask(schedule);
|
|
2156
|
+
await startTask(wake.id);
|
|
2157
|
+
await completeTask(wake.id, "The build passed.");
|
|
2158
|
+
return { askId: ask.id, wakeId: wake.id, ts: card!.ts };
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
test("a deferral card Slack refuses for good stops being retried", async () => {
|
|
2162
|
+
const card = await resolvableDeferralCard("C_DEFER_REJECTED");
|
|
2163
|
+
// Not a 404: the message is still there, Slack just will not accept the
|
|
2164
|
+
// rewrite. The old code logged this and left the row eligible, so the
|
|
2165
|
+
// same doomed chat.update ran on every single tick from then on.
|
|
2166
|
+
rejectedUpdateTs = card.ts;
|
|
2167
|
+
calls.length = 0;
|
|
2168
|
+
await processSlackRenderV2();
|
|
2169
|
+
expect(
|
|
2170
|
+
calls.filter((call) => call.method === "chat.update" && call.payload.ts === card.ts),
|
|
2171
|
+
).toHaveLength(1);
|
|
2172
|
+
const abandoned = await getSlackOutcomeMessage(card.askId);
|
|
2173
|
+
expect(abandoned?.deferralResolvedAt).toBeTruthy();
|
|
2174
|
+
expect(abandoned?.deferralAbandonedAt).toBeTruthy();
|
|
2175
|
+
|
|
2176
|
+
calls.length = 0;
|
|
2177
|
+
await processSlackRenderV2();
|
|
2178
|
+
await processSlackRenderV2();
|
|
2179
|
+
expect(
|
|
2180
|
+
calls.filter((call) => call.method === "chat.update" && call.payload.ts === card.ts),
|
|
2181
|
+
).toHaveLength(0);
|
|
2182
|
+
// The answer never reached the card, so the wake-up delivers it itself.
|
|
2183
|
+
const fallback = calls.filter((call) => call.method === "chat.startStream");
|
|
2184
|
+
expect(fallback).toHaveLength(1);
|
|
2185
|
+
expect(fallback[0]?.payload.markdown_text).toBe("✅ The build passed.");
|
|
2186
|
+
expect((await getSlackOutcomeMessage(card.wakeId))?.finalizedAt).toBeTruthy();
|
|
2187
|
+
});
|
|
2188
|
+
|
|
2189
|
+
test("a deferral card failing for an unclassifiable reason is retried, but bounded", async () => {
|
|
2190
|
+
const card = await resolvableDeferralCard("C_DEFER_BOUNDED");
|
|
2191
|
+
// No Slack verdict attached, so this class stays retryable — up to a
|
|
2192
|
+
// ceiling. Keep the failure count far above the ceiling to prove the
|
|
2193
|
+
// ceiling, not the mock, is what ends it.
|
|
2194
|
+
updateFailuresRemaining = 100;
|
|
2195
|
+
const attempts = () =>
|
|
2196
|
+
calls.filter((call) => call.method === "chat.update" && call.payload.ts === card.ts).length;
|
|
2197
|
+
|
|
2198
|
+
calls.length = 0;
|
|
2199
|
+
for (let tick = 0; tick < 4; tick++) await processSlackRenderV2();
|
|
2200
|
+
expect(attempts()).toBe(4);
|
|
2201
|
+
expect((await getSlackOutcomeMessage(card.askId))?.deferralResolvedAt).toBeUndefined();
|
|
2202
|
+
|
|
2203
|
+
await processSlackRenderV2();
|
|
2204
|
+
expect(attempts()).toBe(5);
|
|
2205
|
+
expect((await getSlackOutcomeMessage(card.askId))?.deferralResolvedAt).toBeTruthy();
|
|
2206
|
+
|
|
2207
|
+
calls.length = 0;
|
|
2208
|
+
await processSlackRenderV2();
|
|
2209
|
+
await processSlackRenderV2();
|
|
2210
|
+
expect(attempts()).toBe(0);
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
test("a deferral with no pending text still posts its ETA", async () => {
|
|
1825
2214
|
const { channelId, threadTs } = uniqueSlackAddress("C_ETA_ONLY");
|
|
1826
2215
|
const ask = await createTaskExtended("wait", {
|
|
1827
2216
|
source: "slack",
|
|
@@ -1836,8 +2225,10 @@ describe("Slack renderer v2", () => {
|
|
|
1836
2225
|
);
|
|
1837
2226
|
calls.length = 0;
|
|
1838
2227
|
await processSlackRenderV2();
|
|
1839
|
-
|
|
1840
|
-
|
|
2228
|
+
// The ETA IS the message now, so there is no such thing as an "ETA-only"
|
|
2229
|
+
// notice worth suppressing — a thread that says nothing is the bug.
|
|
2230
|
+
const started = calls.find((call) => call.method === "chat.startStream");
|
|
2231
|
+
expect(started?.payload.markdown_text).toBe("⏳ Checking back today 17:30 UTC");
|
|
1841
2232
|
});
|
|
1842
2233
|
|
|
1843
2234
|
test("refreshes a stream started with stale content before finalizing it", async () => {
|
|
@@ -2869,3 +3260,42 @@ describe("Slack renderer v2 delegation (SLACK_RENDER_V2_DELEGATION)", () => {
|
|
|
2869
3260
|
}
|
|
2870
3261
|
});
|
|
2871
3262
|
});
|
|
3263
|
+
|
|
3264
|
+
describe("withStatusLead", () => {
|
|
3265
|
+
test.each([
|
|
3266
|
+
["✅", "Hybrid and graph already match.", "✅ Hybrid and graph already match."],
|
|
3267
|
+
["⏳", "Checking back at 18:00.", "⏳ Checking back at 18:00."],
|
|
3268
|
+
["❌ **Failed:**", "build broke", "❌ **Failed:** build broke"],
|
|
3269
|
+
[
|
|
3270
|
+
"🚫 **Cancelled:**",
|
|
3271
|
+
"requester changed direction",
|
|
3272
|
+
"🚫 **Cancelled:** requester changed direction",
|
|
3273
|
+
],
|
|
3274
|
+
[
|
|
3275
|
+
"↳ ✅ Worker — result:",
|
|
3276
|
+
"Opened PR.\n\nDetails.",
|
|
3277
|
+
"↳ ✅ Worker — result: Opened PR.\n\nDetails.",
|
|
3278
|
+
],
|
|
3279
|
+
["✅", "\n\n Leading blank lines drop.", "✅ Leading blank lines drop."],
|
|
3280
|
+
["✅", "**Bold** opener stays inline.", "✅ **Bold** opener stays inline."],
|
|
3281
|
+
["✅", "1.5x faster than before.", "✅ 1.5x faster than before."],
|
|
3282
|
+
])("inlines %p with a plain opener", (lead, body, expected) => {
|
|
3283
|
+
expect(withStatusLead(lead, body)).toBe(expected);
|
|
3284
|
+
});
|
|
3285
|
+
|
|
3286
|
+
test.each([
|
|
3287
|
+
["code fence", "```ts\nconst x = 1;\n```"],
|
|
3288
|
+
["tilde fence", "~~~\nraw\n~~~"],
|
|
3289
|
+
["dash list", "- first\n- second"],
|
|
3290
|
+
["star list", "* first"],
|
|
3291
|
+
["ordered list", "1. first\n2. second"],
|
|
3292
|
+
["blockquote", "> quoted"],
|
|
3293
|
+
["heading", "# Title\n\nBody"],
|
|
3294
|
+
["table", "| a | b |\n| - | - |"],
|
|
3295
|
+
["thematic break", "---\nafter"],
|
|
3296
|
+
["indented code", " indented"],
|
|
3297
|
+
])("keeps the blank line before a %s", (_name, body) => {
|
|
3298
|
+
expect(withStatusLead("✅", body)).toBe(`✅\n\n${body}`);
|
|
3299
|
+
expect(withStatusLead("❌ **Failed:**", body)).toBe(`❌ **Failed:**\n\n${body}`);
|
|
3300
|
+
});
|
|
3301
|
+
});
|