@desplega.ai/agent-swarm 2.7.5 → 2.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
- package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
- package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
- package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
- package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
- package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
- package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
- package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
- package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
- package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
- package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
- package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
- package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
- package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
- package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
- package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
- package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
- package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
- package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
- package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
- package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
- package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
- package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
- package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
- package/dist/cli-82j36f2m.js +45 -0
- package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
- package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
- package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
- package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
- package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
- package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
- package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
- package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
- package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
- package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
- package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
- package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
- package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
- package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
- package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
- package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
- package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
- package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
- package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
- package/dist/cli-g08zdaec.js +55 -0
- package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
- package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
- package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
- package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
- package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
- package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
- package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
- package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
- package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
- package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
- package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
- package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
- package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
- package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
- package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
- package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
- package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
- package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
- package/dist/cli-qghe0h66.js +67 -0
- package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
- package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
- package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
- package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
- package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
- package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
- package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
- package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
- package/dist/cli.js +26 -25
- package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
- package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
- package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
- package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
- package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
- package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
- package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
- package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
- package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
- package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
- package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
- package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
- package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
- package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
- package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
- package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
- package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
- package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
- package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
- package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
- package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
- package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
- package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
- package/dist/handlers-zxmp1ta4.js +32 -0
- package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
- package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
- package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
- package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
- package/dist/index-a8phxqnk.js +37 -0
- package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
- package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
- package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
- package/dist/index-k6h00t4c.js +30 -0
- package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
- package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
- package/dist/index-xrz7te8h.js +33 -0
- package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
- package/dist/lead-956sd2yc.js +71 -0
- package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
- package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
- package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
- package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
- package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
- package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
- package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
- package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
- package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
- package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
- package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
- package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
- package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
- package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
- package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
- package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
- package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
- package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
- package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
- package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
- package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
- package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
- package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
- package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
- package/dist/worker-y15hy08h.js +71 -0
- package/openapi.json +289 -4
- package/package.json +9 -8
- package/src/apps/definition.ts +6 -5
- package/src/be/db/page-theme-tokens.ts +12 -0
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/tasks/read.ts +131 -4
- package/src/be/db/tasks/write.ts +19 -0
- package/src/be/db-client.ts +114 -11
- package/src/be/db.ts +49 -6
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
- package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
- package/src/be/migrations/263_task_list_filter_index.sql +11 -0
- package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
- package/src/be/migrations/266_script_runs_source_task.sql +11 -0
- package/src/be/migrations/269_pages_design_system.sql +23 -0
- package/src/be/runtime-pools/cache-cleanup.ts +109 -0
- package/src/be/runtime-pools/db.ts +65 -6
- package/src/be/runtime-pools/remote-controller.ts +5 -0
- package/src/be/runtime-pools/supervisor.ts +33 -2
- package/src/be/scripts/typecheck.ts +22 -0
- package/src/be/seed/schedules-seeder.ts +0 -10
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
- package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
- package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/seed-skills/bundled-files.generated.json +3 -3
- package/src/be/swarm-config-guard.ts +32 -0
- package/src/be/task-auto-title-listener.ts +127 -0
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/commands/onboard/agent-fs-version.ts +1 -1
- package/src/commands/profile-sync.ts +151 -16
- package/src/commands/runner.ts +15 -7
- package/src/hooks/hook.ts +20 -9
- package/src/http/index.ts +4 -1
- package/src/http/list-total-cache.ts +14 -7
- package/src/http/memory.ts +38 -9
- package/src/http/pages-public.ts +93 -129
- package/src/http/pages.ts +94 -21
- package/src/http/script-runs.ts +174 -5
- package/src/http/status.ts +10 -2
- package/src/http/tasks.ts +4 -2
- package/src/pages/browser-sdk.ts +48 -21
- package/src/pages/color-literals.ts +115 -0
- package/src/pages/page-head.ts +766 -0
- package/src/pages/version.ts +3 -0
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +1 -0
- package/src/providers/codex-app-server.ts +27 -1
- package/src/providers/fx-adapter.ts +1 -1
- package/src/providers/pi-mono-extension.ts +30 -15
- package/src/runtime-providers/controller-protocol.ts +14 -1
- package/src/runtime-providers/controller.ts +5 -0
- package/src/runtime-providers/docker.ts +108 -23
- package/src/runtime-providers/types.ts +16 -0
- package/src/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/types/stdlib.d.ts +22 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
- package/src/server-user.ts +1 -0
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +37 -2
- package/src/slack/left-threads.ts +27 -4
- package/src/tasks/follow-up-gate.ts +318 -0
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tasks/worker-follow-up.ts +21 -1
- package/src/tests/claude-artifacts-read-script.test.ts +209 -0
- package/src/tests/codex-app-server.test.ts +59 -0
- package/src/tests/codex-gateway-route.test.ts +50 -1
- package/src/tests/create-page-tool.test.ts +501 -0
- package/src/tests/db-client-busy-retry.test.ts +81 -0
- package/src/tests/db-client.test.ts +21 -0
- package/src/tests/db-task-read-contract.test.ts +9 -2
- package/src/tests/db-task-write-contract.test.ts +7 -1
- package/src/tests/db-tasks-read.test.ts +21 -0
- package/src/tests/entrypoint-api-readiness.test.ts +416 -210
- package/src/tests/explicit-model-validation.test.ts +14 -8
- package/src/tests/fixtures/pages/head-v1.html +56 -0
- package/src/tests/follow-up-gate-shadow.test.ts +513 -0
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/identity-field-budget.test.ts +1 -1
- package/src/tests/internal-ai/credentials.test.ts +62 -0
- package/src/tests/internal-ai/task-title.test.ts +283 -0
- package/src/tests/list-total-cache.test.ts +27 -2
- package/src/tests/memory-fts-populate.test.ts +11 -9
- package/src/tests/memory-retrieval-purge.test.ts +80 -0
- package/src/tests/merge-gate-detect-changes.test.ts +145 -0
- package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-session-dev-cookie.test.ts +188 -0
- package/src/tests/pages-frame-ancestors.test.ts +28 -1
- package/src/tests/pages-head-v2.test.ts +534 -0
- package/src/tests/pages-http.test.ts +330 -0
- package/src/tests/pages-public-html.test.ts +43 -0
- package/src/tests/pages-storage.test.ts +182 -0
- package/src/tests/pages-versioning.test.ts +72 -0
- package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
- package/src/tests/profile-sync.test.ts +11 -15
- package/src/tests/query-audit-plans.test.ts +27 -0
- package/src/tests/runtime-cache-cleanup.test.ts +367 -0
- package/src/tests/runtime-e2b-pools.test.ts +41 -0
- package/src/tests/runtime-pools-db.test.ts +98 -0
- package/src/tests/runtime-pools-http.test.ts +29 -0
- package/src/tests/runtime-providers-docker.test.ts +253 -0
- package/src/tests/runtime-remote-controller.test.ts +66 -2
- package/src/tests/runtime-supervisor.test.ts +66 -2
- package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
- package/src/tests/script-connections.test.ts +13 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-registry.test.ts +28 -6
- package/src/tests/seed-schedules.test.ts +61 -8
- package/src/tests/seed-scripts.test.ts +168 -2
- package/src/tests/slack-leave-thread.test.ts +145 -5
- package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
- package/src/tests/status.test.ts +11 -0
- package/src/tests/swarm-diff.test.ts +121 -2
- package/src/tests/task-auto-title-config-guard.test.ts +37 -0
- package/src/tests/task-auto-title-listener.test.ts +165 -0
- package/src/tests/tasks-readable-row.test.ts +155 -0
- package/src/tests/theme-import.test.ts +758 -0
- package/src/tests/themes-page-theme.test.ts +155 -0
- package/src/tests/themes-presets.test.ts +172 -0
- package/src/tests/workflow-script-principal.test.ts +2 -1
- package/src/themes/classics.ts +949 -0
- package/src/themes/import.ts +420 -0
- package/src/themes/page-fields.ts +40 -0
- package/src/themes/page-theme.ts +102 -0
- package/src/themes/presets.ts +561 -0
- package/src/themes/tokens.ts +218 -0
- package/src/tools/app-upsert.ts +1 -1
- package/src/tools/create-page.ts +118 -12
- package/src/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +1 -1
- package/src/tools/task-dedup.ts +45 -21
- package/src/tools/utils.ts +33 -0
- package/src/types.ts +83 -3
- package/src/utils/constants.ts +19 -0
- package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
- package/src/utils/internal-ai/credentials.ts +146 -93
- package/src/utils/internal-ai/index.ts +10 -0
- package/src/utils/internal-ai/models.ts +18 -0
- package/src/utils/internal-ai/task-title.ts +282 -0
- package/src/utils/page-session.ts +37 -0
- package/templates/schedules/daily-status-report/content.md +88 -2
- package/templates/schedules/daily-swarm-update-check/content.md +16 -40
- package/templates/skills/apps/SKILL.md +1 -1
- package/templates/skills/apps/content.md +1 -1
- package/templates/skills/composio-google-docs/content.md +10 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/pages/config.json +1 -1
- package/templates/skills/pages/content.md +539 -86
- package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
- package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
- package/templates/skills/pages/files/examples/report-page.html +125 -124
- package/templates/skills/script-workflows/content.md +19 -1
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/dist/cli-2pnswgh8.js +0 -189
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-vwmba1m2.js +0 -31
- package/dist/index-5r71rf98.js +0 -32
- package/dist/index-anmwxs87.js +0 -29
- package/dist/index-spp0ra5c.js +0 -36
- package/dist/lead-fjy418cw.js +0 -69
- package/dist/worker-0c5tyzqe.js +0 -69
- package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
- package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
|
@@ -4,15 +4,23 @@ import {
|
|
|
4
4
|
cancelTask,
|
|
5
5
|
closeDb,
|
|
6
6
|
completeTask,
|
|
7
|
+
countAgentDispatchesInThread,
|
|
7
8
|
createAgent,
|
|
8
9
|
createTaskExtended,
|
|
9
10
|
failTask,
|
|
10
|
-
|
|
11
|
+
findCompletedTasksInThread,
|
|
11
12
|
findRecentCancelledTaskInThread,
|
|
12
13
|
getDbClient,
|
|
13
|
-
getTaskById,
|
|
14
14
|
initDb,
|
|
15
15
|
} from "../be/db";
|
|
16
|
+
import {
|
|
17
|
+
FOLLOW_UP_DISPATCH_CAP,
|
|
18
|
+
FOLLOW_UP_DISPATCH_WINDOW_MINUTES,
|
|
19
|
+
FOLLOW_UP_SAME_WORK_PAGE_SIZE,
|
|
20
|
+
} from "../tasks/follow-up-redelegation-guard";
|
|
21
|
+
import { sendTaskHandler, sendTaskInputSchema } from "../tools/send-task";
|
|
22
|
+
import { DEDUP_SCAN_LIMIT, findDuplicateTask } from "../tools/task-dedup";
|
|
23
|
+
import { ownerCtx } from "../tools/task-tool-ctx";
|
|
16
24
|
|
|
17
25
|
const TEST_DB_PATH = "./test-follow-up-redelivery-guard.sqlite";
|
|
18
26
|
|
|
@@ -31,7 +39,7 @@ afterAll(() => {
|
|
|
31
39
|
}
|
|
32
40
|
});
|
|
33
41
|
|
|
34
|
-
describe("
|
|
42
|
+
describe("findCompletedTasksInThread", () => {
|
|
35
43
|
test("finds completed tasks in a thread within the time window", async () => {
|
|
36
44
|
const agent = await createAgent({
|
|
37
45
|
name: "dedup-worker-1",
|
|
@@ -50,13 +58,12 @@ describe("findCompletedTaskInThread", () => {
|
|
|
50
58
|
await completeTask(task.id, "done");
|
|
51
59
|
|
|
52
60
|
// Should find the completed task within a 2880-minute (48h) window
|
|
53
|
-
const result = await
|
|
54
|
-
expect(result).
|
|
55
|
-
expect(result!.
|
|
56
|
-
expect(result!.status).toBe("completed");
|
|
61
|
+
const result = await findCompletedTasksInThread("C_DEDUP_1", "1000.0001", 2880);
|
|
62
|
+
expect(result.map((t) => t.id)).toEqual([task.id]);
|
|
63
|
+
expect(result[0]!.status).toBe("completed");
|
|
57
64
|
});
|
|
58
65
|
|
|
59
|
-
test("returns
|
|
66
|
+
test("returns nothing when no completed tasks exist in the thread", async () => {
|
|
60
67
|
const agent = await createAgent({
|
|
61
68
|
name: "dedup-worker-2",
|
|
62
69
|
isLead: false,
|
|
@@ -71,11 +78,11 @@ describe("findCompletedTaskInThread", () => {
|
|
|
71
78
|
slackThreadTs: "2000.0001",
|
|
72
79
|
});
|
|
73
80
|
|
|
74
|
-
const result = await
|
|
75
|
-
expect(result).
|
|
81
|
+
const result = await findCompletedTasksInThread("C_DEDUP_2", "2000.0001", 2880);
|
|
82
|
+
expect(result).toEqual([]);
|
|
76
83
|
});
|
|
77
84
|
|
|
78
|
-
test("returns
|
|
85
|
+
test("returns nothing outside the time window", async () => {
|
|
79
86
|
const agent = await createAgent({
|
|
80
87
|
name: "dedup-worker-3",
|
|
81
88
|
isLead: false,
|
|
@@ -99,11 +106,11 @@ describe("findCompletedTaskInThread", () => {
|
|
|
99
106
|
]);
|
|
100
107
|
|
|
101
108
|
// Should not find with a 48 hour window
|
|
102
|
-
const result = await
|
|
103
|
-
expect(result).
|
|
109
|
+
const result = await findCompletedTasksInThread("C_DEDUP_3", "3000.0001", 2880);
|
|
110
|
+
expect(result).toEqual([]);
|
|
104
111
|
});
|
|
105
112
|
|
|
106
|
-
test("returns
|
|
113
|
+
test("returns nothing for a different thread", async () => {
|
|
107
114
|
const agent = await createAgent({
|
|
108
115
|
name: "dedup-worker-4",
|
|
109
116
|
isLead: false,
|
|
@@ -120,118 +127,73 @@ describe("findCompletedTaskInThread", () => {
|
|
|
120
127
|
await completeTask(task.id, "done");
|
|
121
128
|
|
|
122
129
|
// Search in a different thread — should not find
|
|
123
|
-
const result = await
|
|
124
|
-
expect(result).
|
|
130
|
+
const result = await findCompletedTasksInThread("C_DEDUP_4", "4000.9999", 2880);
|
|
131
|
+
expect(result).toEqual([]);
|
|
125
132
|
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("follow-up re-delegation guard logic", () => {
|
|
129
|
-
let leadAgent: Awaited<ReturnType<typeof createAgent>>;
|
|
130
|
-
let workerAgent: Awaited<ReturnType<typeof createAgent>>;
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
name: "
|
|
135
|
-
isLead: true,
|
|
136
|
-
status: "idle",
|
|
137
|
-
capabilities: [],
|
|
138
|
-
});
|
|
139
|
-
workerAgent = await createAgent({
|
|
140
|
-
name: "guard-worker",
|
|
134
|
+
test("caps the rows it returns at the limit, newest first", async () => {
|
|
135
|
+
const agent = await createAgent({
|
|
136
|
+
name: "dedup-worker-5",
|
|
141
137
|
isLead: false,
|
|
142
138
|
status: "idle",
|
|
143
139
|
capabilities: [],
|
|
144
|
-
maxTasks: 5,
|
|
145
140
|
});
|
|
141
|
+
const ids: string[] = [];
|
|
142
|
+
for (let i = 0; i < 3; i++) {
|
|
143
|
+
const task = await createTaskExtended(`capped completion ${i}`, {
|
|
144
|
+
agentId: agent.id,
|
|
145
|
+
slackChannelId: "C_DEDUP_5",
|
|
146
|
+
slackThreadTs: "5000.0001",
|
|
147
|
+
});
|
|
148
|
+
await completeTask(task.id, "done");
|
|
149
|
+
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
150
|
+
new Date(Date.now() - (3 - i) * 60 * 1000).toISOString(),
|
|
151
|
+
task.id,
|
|
152
|
+
]);
|
|
153
|
+
ids.push(task.id);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = await findCompletedTasksInThread("C_DEDUP_5", "5000.0001", 2880, 2);
|
|
157
|
+
expect(result.map((t) => t.id)).toEqual([ids[2], ids[1]]);
|
|
146
158
|
});
|
|
147
159
|
|
|
148
|
-
test("
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
await completeTask(workerTask.id, "Feature X implemented");
|
|
156
|
-
|
|
157
|
-
// Step 2: Create a follow-up task (as store-progress would)
|
|
158
|
-
const followUpTask = await createTaskExtended("Worker task completed — review needed.", {
|
|
159
|
-
agentId: leadAgent.id,
|
|
160
|
-
source: "system",
|
|
161
|
-
taskType: "follow-up",
|
|
162
|
-
parentTaskId: workerTask.id,
|
|
163
|
-
slackChannelId: "C_GUARD_1",
|
|
164
|
-
slackThreadTs: "5000.0001",
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Step 3: Simulate the guard logic from send-task.ts
|
|
168
|
-
// The lead's sourceTaskId would be the follow-up task
|
|
169
|
-
const sourceTask = await getTaskById(followUpTask.id);
|
|
170
|
-
expect(sourceTask).not.toBeNull();
|
|
171
|
-
expect(sourceTask!.taskType).toBe("follow-up");
|
|
172
|
-
expect(sourceTask!.slackChannelId).toBe("C_GUARD_1");
|
|
173
|
-
expect(sourceTask!.slackThreadTs).toBe("5000.0001");
|
|
174
|
-
|
|
175
|
-
// The guard should find the completed worker task
|
|
176
|
-
const recentCompleted = await findCompletedTaskInThread(
|
|
177
|
-
sourceTask!.slackChannelId!,
|
|
178
|
-
sourceTask!.slackThreadTs!,
|
|
179
|
-
2880,
|
|
180
|
-
);
|
|
181
|
-
expect(recentCompleted).not.toBeNull();
|
|
182
|
-
expect(recentCompleted!.id).toBe(workerTask.id);
|
|
183
|
-
|
|
184
|
-
// → Guard would block: re-delegation should be prevented
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("allows delegation when source task is NOT a follow-up (normal behavior)", async () => {
|
|
188
|
-
// Create a normal Slack task (not a follow-up)
|
|
189
|
-
const slackTask = await createTaskExtended("user asked a question", {
|
|
190
|
-
agentId: leadAgent.id,
|
|
191
|
-
source: "slack",
|
|
192
|
-
taskType: "inbox",
|
|
193
|
-
slackChannelId: "C_GUARD_2",
|
|
194
|
-
slackThreadTs: "6000.0001",
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// Even if there are completed tasks in the thread, guard shouldn't trigger
|
|
198
|
-
// because the source task is not a "follow-up"
|
|
199
|
-
const sourceTask = await getTaskById(slackTask.id);
|
|
200
|
-
expect(sourceTask).not.toBeNull();
|
|
201
|
-
expect(sourceTask!.taskType).not.toBe("follow-up");
|
|
202
|
-
|
|
203
|
-
// Guard condition: sourceTask?.taskType === "follow-up" → false
|
|
204
|
-
// → Guard does NOT block: delegation proceeds normally
|
|
205
|
-
const shouldBlock =
|
|
206
|
-
sourceTask?.taskType === "follow-up" && sourceTask.slackThreadTs && sourceTask.slackChannelId;
|
|
207
|
-
expect(shouldBlock).toBeFalsy();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("allows delegation when source task is a follow-up but thread has NO completed work", async () => {
|
|
211
|
-
// Create a follow-up task in a thread with no completed work
|
|
212
|
-
const followUpTask = await createTaskExtended("Worker task failed — action needed.", {
|
|
213
|
-
agentId: leadAgent.id,
|
|
214
|
-
source: "system",
|
|
215
|
-
taskType: "follow-up",
|
|
216
|
-
slackChannelId: "C_GUARD_3",
|
|
217
|
-
slackThreadTs: "7000.0001",
|
|
160
|
+
test("the before cursor returns the next page, including rows tied on lastUpdatedAt", async () => {
|
|
161
|
+
const agent = await createAgent({
|
|
162
|
+
name: "dedup-worker-6",
|
|
163
|
+
isLead: false,
|
|
164
|
+
status: "idle",
|
|
165
|
+
capabilities: [],
|
|
218
166
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
167
|
+
const at = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
|
168
|
+
for (let i = 0; i < 5; i++) {
|
|
169
|
+
const task = await createTaskExtended(`paged completion ${i}`, {
|
|
170
|
+
agentId: agent.id,
|
|
171
|
+
slackChannelId: "C_DEDUP_6",
|
|
172
|
+
slackThreadTs: "6000.0001",
|
|
173
|
+
});
|
|
174
|
+
await completeTask(task.id, "done");
|
|
175
|
+
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
176
|
+
at,
|
|
177
|
+
task.id,
|
|
178
|
+
]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const seen: string[] = [];
|
|
182
|
+
let before: { lastUpdatedAt: string; id: string } | undefined;
|
|
183
|
+
// Bounded so an ignored cursor fails the test instead of looping forever.
|
|
184
|
+
for (let i = 0; i < 10; i++) {
|
|
185
|
+
const page = await findCompletedTasksInThread("C_DEDUP_6", "6000.0001", 2880, 2, before);
|
|
186
|
+
seen.push(...page.map((t) => t.id));
|
|
187
|
+
const last = page.at(-1);
|
|
188
|
+
if (!last || page.length < 2) break;
|
|
189
|
+
before = { lastUpdatedAt: last.lastUpdatedAt, id: last.id };
|
|
190
|
+
}
|
|
191
|
+
expect(seen).toHaveLength(5);
|
|
192
|
+
expect(new Set(seen).size).toBe(5);
|
|
233
193
|
});
|
|
194
|
+
});
|
|
234
195
|
|
|
196
|
+
describe("findRecentCancelledTaskInThread", () => {
|
|
235
197
|
test("findRecentCancelledTaskInThread finds tasks with status='cancelled'", async () => {
|
|
236
198
|
const agent = await createAgent({
|
|
237
199
|
name: "cancel-thread-worker-1",
|
|
@@ -308,125 +270,393 @@ describe("follow-up re-delegation guard logic", () => {
|
|
|
308
270
|
const result = await findRecentCancelledTaskInThread("C_CANCEL_4", "9000.0004", 2880);
|
|
309
271
|
expect(result).toBeNull();
|
|
310
272
|
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("send-task follow-up re-delegation guard", () => {
|
|
276
|
+
let leadAgent: Awaited<ReturnType<typeof createAgent>>;
|
|
277
|
+
let workerAgent: Awaited<ReturnType<typeof createAgent>>;
|
|
278
|
+
|
|
279
|
+
beforeAll(async () => {
|
|
280
|
+
leadAgent = await createAgent({
|
|
281
|
+
name: "guard-lead",
|
|
282
|
+
isLead: true,
|
|
283
|
+
status: "idle",
|
|
284
|
+
capabilities: [],
|
|
285
|
+
});
|
|
286
|
+
workerAgent = await createAgent({
|
|
287
|
+
name: "guard-worker",
|
|
288
|
+
isLead: false,
|
|
289
|
+
status: "idle",
|
|
290
|
+
capabilities: [],
|
|
291
|
+
maxTasks: 100,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
311
294
|
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
295
|
+
async function backdate(taskId: string, minutesAgo: number): Promise<void> {
|
|
296
|
+
const at = new Date(Date.now() - minutesAgo * 60 * 1000).toISOString();
|
|
297
|
+
await getDbClient().run(
|
|
298
|
+
"UPDATE agent_tasks SET createdAt = ?, lastUpdatedAt = ? WHERE id = ?",
|
|
299
|
+
[at, at, taskId],
|
|
300
|
+
);
|
|
301
|
+
}
|
|
315
302
|
|
|
316
|
-
|
|
317
|
-
|
|
303
|
+
/** A worker task in the thread that completed `minutesAgo` minutes ago. */
|
|
304
|
+
async function completedWork(channel: string, thread: string, text: string, minutesAgo = 30) {
|
|
305
|
+
const task = await createTaskExtended(text, {
|
|
318
306
|
agentId: workerAgent.id,
|
|
319
307
|
slackChannelId: channel,
|
|
320
308
|
slackThreadTs: thread,
|
|
321
309
|
});
|
|
322
|
-
await completeTask(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await getDbClient().run("UPDATE agent_tasks SET lastUpdatedAt = ? WHERE id = ?", [
|
|
327
|
-
thirtyMinAgo,
|
|
328
|
-
completedTask.id,
|
|
329
|
-
]);
|
|
310
|
+
await completeTask(task.id, "done");
|
|
311
|
+
await backdate(task.id, minutesAgo);
|
|
312
|
+
return task;
|
|
313
|
+
}
|
|
330
314
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
315
|
+
/** The Lead session store-progress opens after a worker finishes. */
|
|
316
|
+
async function leadSession(channel: string, thread: string, taskType = "follow-up") {
|
|
317
|
+
return createTaskExtended("Worker task completed — review needed.", {
|
|
318
|
+
agentId: leadAgent.id,
|
|
319
|
+
source: "system",
|
|
320
|
+
taskType,
|
|
334
321
|
slackChannelId: channel,
|
|
335
322
|
slackThreadTs: thread,
|
|
336
323
|
});
|
|
337
|
-
|
|
324
|
+
}
|
|
338
325
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
326
|
+
function dispatch(sourceTaskId: string, task: string, extra: Record<string, unknown> = {}) {
|
|
327
|
+
return sendTaskHandler(
|
|
328
|
+
ownerCtx({ agentId: leadAgent.id, sourceTaskId }),
|
|
329
|
+
sendTaskInputSchema.parse({
|
|
330
|
+
task,
|
|
331
|
+
agentId: workerAgent.id,
|
|
332
|
+
routingReason: "skill",
|
|
333
|
+
routingNote: "worker owns this repo",
|
|
334
|
+
...extra,
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
344
338
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
expect(cancelledMoreRecent).toBe(true);
|
|
339
|
+
test("a distinct next-step dispatch from a follow-up passes", async () => {
|
|
340
|
+
await completedWork("C_NEXT", "1.1", "implement the billing export endpoint in the api server");
|
|
341
|
+
const followUp = await leadSession("C_NEXT", "1.1");
|
|
342
|
+
await completeTask(followUp.id, "dispatched step 1");
|
|
343
|
+
const session = await leadSession("C_NEXT", "1.1");
|
|
351
344
|
|
|
352
|
-
|
|
345
|
+
const result = await dispatch(
|
|
346
|
+
session.id,
|
|
347
|
+
"review pull request 42 and post findings as comments",
|
|
348
|
+
);
|
|
349
|
+
expect(result.ok).toBe(true);
|
|
350
|
+
expect(result.data?.task?.slackThreadTs).toBe("1.1");
|
|
353
351
|
});
|
|
354
352
|
|
|
355
|
-
test("
|
|
356
|
-
const
|
|
357
|
-
const
|
|
353
|
+
test("re-dispatching completed work from a follow-up is blocked by the same-work rule", async () => {
|
|
354
|
+
const done = await completedWork("C_SAME", "2.1", "fix the flaky retry test in the scheduler");
|
|
355
|
+
const session = await leadSession("C_SAME", "2.1");
|
|
358
356
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
357
|
+
const result = await dispatch(session.id, "fix the flaky retry test in the scheduler");
|
|
358
|
+
expect(result.ok).toBe(false);
|
|
359
|
+
expect(result.message).toContain("same-work rule");
|
|
360
|
+
expect(result.message).toContain(done.id.slice(0, 8));
|
|
361
|
+
expect(result.message).toContain("allowRedelegation: true");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("allowDuplicate does not bypass the same-work rule", async () => {
|
|
365
|
+
await completedWork("C_DUP", "3.1", "rotate the staging database credentials now");
|
|
366
|
+
const session = await leadSession("C_DUP", "3.1");
|
|
367
|
+
|
|
368
|
+
const result = await dispatch(session.id, "rotate the staging database credentials now", {
|
|
369
|
+
allowDuplicate: true,
|
|
364
370
|
});
|
|
365
|
-
|
|
371
|
+
expect(result.ok).toBe(false);
|
|
372
|
+
expect(result.message).toContain("same-work rule");
|
|
373
|
+
});
|
|
366
374
|
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
375
|
+
test("matching completed work stays blocked behind 50 newer unrelated completions", async () => {
|
|
376
|
+
const done = await completedWork(
|
|
377
|
+
"C_DEEP",
|
|
378
|
+
"2.5",
|
|
379
|
+
"migrate the audit log table to the new schema",
|
|
380
|
+
120,
|
|
381
|
+
);
|
|
382
|
+
for (let i = 0; i < 50; i++) {
|
|
383
|
+
await completedWork("C_DEEP", "2.5", `unrelated chore ${i} touching widget ${i}`, 60 - i);
|
|
384
|
+
}
|
|
385
|
+
const session = await leadSession("C_DEEP", "2.5");
|
|
386
|
+
|
|
387
|
+
const result = await dispatch(session.id, "migrate the audit log table to the new schema");
|
|
388
|
+
expect(result.ok).toBe(false);
|
|
389
|
+
expect(result.message).toContain("same-work rule");
|
|
390
|
+
expect(result.message).toContain(done.id.slice(0, 8));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("matching completed work stays blocked behind more than a page of newer unrelated completions", async () => {
|
|
394
|
+
const done = await completedWork(
|
|
395
|
+
"C_DEEPER",
|
|
396
|
+
"2.6",
|
|
397
|
+
"backfill the invoice totals for the march billing run",
|
|
398
|
+
600,
|
|
399
|
+
);
|
|
400
|
+
for (let i = 0; i < FOLLOW_UP_SAME_WORK_PAGE_SIZE + 50; i++) {
|
|
401
|
+
await completedWork("C_DEEPER", "2.6", `unrelated chore ${i} touching widget ${i}`, 300 - i);
|
|
402
|
+
}
|
|
403
|
+
const session = await leadSession("C_DEEPER", "2.6");
|
|
404
|
+
|
|
405
|
+
const result = await dispatch(
|
|
406
|
+
session.id,
|
|
407
|
+
"backfill the invoice totals for the march billing run",
|
|
408
|
+
);
|
|
409
|
+
expect(result.ok).toBe(false);
|
|
410
|
+
expect(result.message).toContain("same-work rule");
|
|
411
|
+
expect(result.message).toContain(done.id.slice(0, 8));
|
|
412
|
+
});
|
|
373
413
|
|
|
374
|
-
|
|
375
|
-
|
|
414
|
+
test("the loop cap blocks a distinct dispatch once the thread hit the cap", async () => {
|
|
415
|
+
for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP; i++) {
|
|
416
|
+
await createTaskExtended(`loop step number ${i} with unique words ${i * 7}`, {
|
|
417
|
+
agentId: workerAgent.id,
|
|
418
|
+
creatorAgentId: leadAgent.id,
|
|
419
|
+
source: "mcp",
|
|
420
|
+
slackChannelId: "C_LOOP",
|
|
421
|
+
slackThreadTs: "4.1",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const session = await leadSession("C_LOOP", "4.1");
|
|
425
|
+
|
|
426
|
+
const result = await dispatch(
|
|
427
|
+
session.id,
|
|
428
|
+
"draft the quarterly changelog summary for customers",
|
|
429
|
+
);
|
|
430
|
+
expect(result.ok).toBe(false);
|
|
431
|
+
expect(result.message).toContain("loop-cap rule");
|
|
432
|
+
expect(result.message).toContain(`cap ${FOLLOW_UP_DISPATCH_CAP}`);
|
|
433
|
+
expect(result.message).toContain("allowRedelegation: true");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
/** A durable run's `ctx.step.agentTask` step tasks, created by the Lead in the thread. */
|
|
437
|
+
async function runSteps(channel: string, thread: string, steps: number) {
|
|
438
|
+
const runId = crypto.randomUUID();
|
|
439
|
+
for (let i = 0; i < steps; i++) {
|
|
440
|
+
await createTaskExtended(`deck slide ${i} for run ${runId.slice(0, 8)} words ${i * 11}`, {
|
|
441
|
+
agentId: workerAgent.id,
|
|
442
|
+
creatorAgentId: leadAgent.id,
|
|
443
|
+
source: "mcp",
|
|
444
|
+
taskType: "script-run-step",
|
|
445
|
+
contextKey: `script-run:${runId}:slide-${i}`,
|
|
446
|
+
slackChannelId: channel,
|
|
447
|
+
slackThreadTs: thread,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return runId;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
test("a 29-step durable run plus one send-task counts as two dispatches", async () => {
|
|
454
|
+
await runSteps("C_LOOP_RUN", "4.3", 29);
|
|
455
|
+
await createTaskExtended("one plain send-task dispatch into the thread", {
|
|
376
456
|
agentId: workerAgent.id,
|
|
377
|
-
|
|
378
|
-
|
|
457
|
+
creatorAgentId: leadAgent.id,
|
|
458
|
+
source: "mcp",
|
|
459
|
+
slackChannelId: "C_LOOP_RUN",
|
|
460
|
+
slackThreadTs: "4.3",
|
|
379
461
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
462
|
+
expect(
|
|
463
|
+
await countAgentDispatchesInThread(
|
|
464
|
+
"C_LOOP_RUN",
|
|
465
|
+
"4.3",
|
|
466
|
+
leadAgent.id,
|
|
467
|
+
FOLLOW_UP_DISPATCH_WINDOW_MINUTES,
|
|
468
|
+
),
|
|
469
|
+
).toBe(2);
|
|
470
|
+
const session = await leadSession("C_LOOP_RUN", "4.3");
|
|
471
|
+
|
|
472
|
+
const result = await dispatch(session.id, "summarize the deck review notes for the team");
|
|
473
|
+
expect(result.ok).toBe(true);
|
|
474
|
+
});
|
|
387
475
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
476
|
+
test("the loop cap counts each durable run and each send-task once", async () => {
|
|
477
|
+
for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP / 2; i++) {
|
|
478
|
+
await runSteps("C_LOOP_MIX", "4.4", 3);
|
|
479
|
+
await createTaskExtended(`mixed loop send-task ${i} unique words ${i * 17}`, {
|
|
480
|
+
agentId: workerAgent.id,
|
|
481
|
+
creatorAgentId: leadAgent.id,
|
|
482
|
+
source: "mcp",
|
|
483
|
+
slackChannelId: "C_LOOP_MIX",
|
|
484
|
+
slackThreadTs: "4.4",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const session = await leadSession("C_LOOP_MIX", "4.4");
|
|
488
|
+
|
|
489
|
+
const result = await dispatch(session.id, "write the release announcement for the blog");
|
|
490
|
+
expect(result.ok).toBe(false);
|
|
491
|
+
expect(result.message).toContain("loop-cap rule");
|
|
492
|
+
expect(result.message).toContain(`dispatched ${FOLLOW_UP_DISPATCH_CAP} times`);
|
|
493
|
+
});
|
|
393
494
|
|
|
394
|
-
|
|
495
|
+
test("dispatches older than the loop-cap window do not count", async () => {
|
|
496
|
+
for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP; i++) {
|
|
497
|
+
const old = await createTaskExtended(`old loop step ${i} distinct text ${i * 13}`, {
|
|
498
|
+
agentId: workerAgent.id,
|
|
499
|
+
creatorAgentId: leadAgent.id,
|
|
500
|
+
source: "mcp",
|
|
501
|
+
slackChannelId: "C_LOOP_OLD",
|
|
502
|
+
slackThreadTs: "4.2",
|
|
503
|
+
});
|
|
504
|
+
await backdate(old.id, 7 * 60);
|
|
505
|
+
}
|
|
506
|
+
const session = await leadSession("C_LOOP_OLD", "4.2");
|
|
507
|
+
|
|
508
|
+
const result = await dispatch(session.id, "audit the onboarding emails for broken links");
|
|
509
|
+
expect(result.ok).toBe(true);
|
|
395
510
|
});
|
|
396
511
|
|
|
397
|
-
test("
|
|
398
|
-
|
|
399
|
-
const
|
|
512
|
+
test("a failed task more recent than the completion lets the same work through", async () => {
|
|
513
|
+
await completedWork("C_FAIL", "5.1", "migrate the reports page to the new table component");
|
|
514
|
+
const failed = await createTaskExtended("migrate the reports page, second attempt", {
|
|
400
515
|
agentId: workerAgent.id,
|
|
401
|
-
slackChannelId: "
|
|
402
|
-
slackThreadTs: "
|
|
516
|
+
slackChannelId: "C_FAIL",
|
|
517
|
+
slackThreadTs: "5.1",
|
|
403
518
|
});
|
|
404
|
-
await
|
|
519
|
+
await failTask(failed.id, "TypeError: cannot read property of undefined");
|
|
520
|
+
const session = await leadSession("C_FAIL", "5.1");
|
|
405
521
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
522
|
+
const result = await dispatch(
|
|
523
|
+
session.id,
|
|
524
|
+
"migrate the reports page to the new table component",
|
|
525
|
+
);
|
|
526
|
+
expect(result.ok).toBe(true);
|
|
527
|
+
});
|
|
412
528
|
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
agentId:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
slackChannelId: "C_GUARD_4",
|
|
419
|
-
slackThreadTs: "8000.0001",
|
|
529
|
+
test("a failure older than the completion does not bypass the same-work rule", async () => {
|
|
530
|
+
const failed = await createTaskExtended("compress the landing hero images, first try", {
|
|
531
|
+
agentId: workerAgent.id,
|
|
532
|
+
slackChannelId: "C_FAIL_OLD",
|
|
533
|
+
slackThreadTs: "5.2",
|
|
420
534
|
});
|
|
535
|
+
await failTask(failed.id, "network error");
|
|
536
|
+
await backdate(failed.id, 60);
|
|
537
|
+
await completedWork("C_FAIL_OLD", "5.2", "compress the landing hero images to webp", 30);
|
|
538
|
+
const session = await leadSession("C_FAIL_OLD", "5.2");
|
|
539
|
+
|
|
540
|
+
const result = await dispatch(session.id, "compress the landing hero images to webp");
|
|
541
|
+
expect(result.ok).toBe(false);
|
|
542
|
+
expect(result.message).toContain("same-work rule");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("a cancelled task more recent than the completion still lets the same work through", async () => {
|
|
546
|
+
await completedWork("C_CANCEL", "6.1", "translate the pricing page into spanish copy");
|
|
547
|
+
const cancelled = await createTaskExtended("translate the pricing page, rerun", {
|
|
548
|
+
agentId: workerAgent.id,
|
|
549
|
+
slackChannelId: "C_CANCEL",
|
|
550
|
+
slackThreadTs: "6.1",
|
|
551
|
+
});
|
|
552
|
+
await cancelTask(cancelled.id, "cancelled");
|
|
553
|
+
const session = await leadSession("C_CANCEL", "6.1");
|
|
554
|
+
|
|
555
|
+
const result = await dispatch(session.id, "translate the pricing page into spanish copy");
|
|
556
|
+
expect(result.ok).toBe(true);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("allowRedelegation overrides both rules", async () => {
|
|
560
|
+
await completedWork("C_OVERRIDE", "7.1", "regenerate the openapi spec after the route change");
|
|
561
|
+
const session = await leadSession("C_OVERRIDE", "7.1");
|
|
421
562
|
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
sourceTask!.slackThreadTs!,
|
|
426
|
-
2880,
|
|
563
|
+
const blocked = await dispatch(
|
|
564
|
+
session.id,
|
|
565
|
+
"regenerate the openapi spec after the route change",
|
|
427
566
|
);
|
|
428
|
-
expect(
|
|
567
|
+
expect(blocked.ok).toBe(false);
|
|
568
|
+
|
|
569
|
+
const result = await dispatch(
|
|
570
|
+
session.id,
|
|
571
|
+
"regenerate the openapi spec after the route change",
|
|
572
|
+
{ allowRedelegation: true },
|
|
573
|
+
);
|
|
574
|
+
expect(result.ok).toBe(true);
|
|
575
|
+
|
|
576
|
+
for (let i = 0; i < FOLLOW_UP_DISPATCH_CAP; i++) {
|
|
577
|
+
await createTaskExtended(`override loop filler ${i} words ${i * 11}`, {
|
|
578
|
+
agentId: workerAgent.id,
|
|
579
|
+
creatorAgentId: leadAgent.id,
|
|
580
|
+
source: "mcp",
|
|
581
|
+
slackChannelId: "C_OVERRIDE",
|
|
582
|
+
slackThreadTs: "7.1",
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const capped = await dispatch(session.id, "write release notes for the export feature", {
|
|
586
|
+
allowRedelegation: true,
|
|
587
|
+
});
|
|
588
|
+
expect(capped.ok).toBe(true);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("a resume source task is exempt from the guard", async () => {
|
|
592
|
+
await completedWork("C_RESUME", "8.1", "finish the interrupted search index rebuild");
|
|
593
|
+
const resume = await leadSession("C_RESUME", "8.1", "resume");
|
|
594
|
+
|
|
595
|
+
const result = await dispatch(resume.id, "finish the interrupted search index rebuild");
|
|
596
|
+
expect(result.ok).toBe(true);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("a non-follow-up source task is not guarded", async () => {
|
|
600
|
+
await completedWork("C_INBOX", "9.1", "summarize the open incidents for this week");
|
|
601
|
+
const inbox = await leadSession("C_INBOX", "9.1", "inbox");
|
|
602
|
+
|
|
603
|
+
const result = await dispatch(inbox.id, "summarize the open incidents for this week");
|
|
604
|
+
expect(result.ok).toBe(true);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe("findDuplicateTask", () => {
|
|
609
|
+
test("finds an older duplicate behind 50 newer unrelated tasks from the same creator", async () => {
|
|
610
|
+
const creator = await createAgent({
|
|
611
|
+
name: "dedup-deep-creator",
|
|
612
|
+
isLead: true,
|
|
613
|
+
status: "idle",
|
|
614
|
+
capabilities: [],
|
|
615
|
+
});
|
|
616
|
+
const original = await createTaskExtended("backfill the invoice totals for october", {
|
|
617
|
+
creatorAgentId: creator.id,
|
|
618
|
+
});
|
|
619
|
+
await getDbClient().run("UPDATE agent_tasks SET createdAt = ? WHERE id = ?", [
|
|
620
|
+
new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
621
|
+
original.id,
|
|
622
|
+
]);
|
|
623
|
+
for (let i = 0; i < 50; i++) {
|
|
624
|
+
await createTaskExtended(`unrelated chore ${i} touching widget ${i}`, {
|
|
625
|
+
creatorAgentId: creator.id,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const match = await findDuplicateTask({
|
|
630
|
+
taskDescription: "backfill the invoice totals for october",
|
|
631
|
+
creatorAgentId: creator.id,
|
|
632
|
+
});
|
|
633
|
+
expect(match?.task.id).toBe(original.id);
|
|
634
|
+
});
|
|
429
635
|
|
|
430
|
-
|
|
636
|
+
test("scans at most DEDUP_SCAN_LIMIT rows, so a large backlog keeps one dispatch bounded", async () => {
|
|
637
|
+
const creator = await createAgent({
|
|
638
|
+
name: "dedup-backlog-creator",
|
|
639
|
+
isLead: true,
|
|
640
|
+
status: "idle",
|
|
641
|
+
capabilities: [],
|
|
642
|
+
});
|
|
643
|
+
const original = await createTaskExtended("reconcile the ledger exports for november", {
|
|
644
|
+
creatorAgentId: creator.id,
|
|
645
|
+
});
|
|
646
|
+
await getDbClient().run("UPDATE agent_tasks SET createdAt = ? WHERE id = ?", [
|
|
647
|
+
new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
|
648
|
+
original.id,
|
|
649
|
+
]);
|
|
650
|
+
for (let i = 0; i < DEDUP_SCAN_LIMIT; i++) {
|
|
651
|
+
await createTaskExtended(`backlog chore ${i} touching gadget ${i}`, {
|
|
652
|
+
creatorAgentId: creator.id,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const match = await findDuplicateTask({
|
|
657
|
+
taskDescription: "reconcile the ledger exports for november",
|
|
658
|
+
creatorAgentId: creator.id,
|
|
659
|
+
});
|
|
660
|
+
expect(match).toBeNull();
|
|
431
661
|
});
|
|
432
662
|
});
|