@desplega.ai/agent-swarm 1.131.1 → 1.134.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 +3 -3
- package/dist/{actions-jgcy477k.js → actions-7kast243.js} +20 -18
- package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-706yad38.js} +33 -17
- package/dist/app-0g84t8wf.js +28 -0
- package/dist/{artifact-jysj3wrg.js → artifact-dje5ke4h.js} +37 -131
- package/dist/{assistant-f2vkk1hj.js → assistant-ac4htcky.js} +25 -22
- package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-be59fbsz.js} +13 -13
- package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-b1rcqn85.js} +13 -11
- package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-kw2541c4.js} +14 -12
- package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-ja29t92g.js} +11 -9
- package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-5cg1fv8s.js} +33 -29
- package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-9z9sdytx.js} +9 -9
- package/dist/{claude-managed-setup-52z4mq9w.js → claude-managed-setup-3zqp51cr.js} +5 -5
- package/dist/{claude-1ex232fc.js → claude-wqn3dtca.js} +1 -1
- package/dist/{cli-4wgngmwv.js → cli-00w4hbam.js} +1 -1
- package/dist/{cli-er3a4bb0.js → cli-15429qdw.js} +6 -1
- package/dist/{cli-e5dabkx0.js → cli-1cwhyvta.js} +4 -4
- package/dist/{cli-zsspvvm2.js → cli-1ybr6vw2.js} +237 -237
- package/dist/{cli-xpxde7rm.js → cli-2wv4kj0e.js} +60 -31
- package/dist/cli-34w2584r.js +530 -0
- package/dist/{cli-se2p6rkb.js → cli-38taxznr.js} +6 -6
- package/dist/{cli-9sbhdpvb.js → cli-3cgfyfzm.js} +14 -14
- package/dist/{cli-5n6e5yfn.js → cli-3zmdzfjf.js} +79 -74
- package/dist/{cli-3ty0t082.js → cli-43x36mty.js} +197 -8
- package/dist/{cli-ycrnynpw.js → cli-480nehnh.js} +29 -22
- package/dist/{cli-0258203w.js → cli-492e7ap5.js} +140 -20
- package/dist/{cli-3fj60etf.js → cli-497b685t.js} +3 -3
- package/dist/{cli-pxfxkx5m.js → cli-4bap00k0.js} +1 -1
- package/dist/{cli-w2wxd56v.js → cli-4bc8vmhm.js} +848 -770
- package/dist/{cli-aca5pz9f.js → cli-589599p9.js} +1 -1
- package/dist/{cli-t930g0xh.js → cli-5tnr64pm.js} +1 -1
- package/dist/{cli-jxg3s9ey.js → cli-65d93ves.js} +7 -7
- package/dist/{cli-6dpytdr2.js → cli-6x363v1z.js} +2 -2
- package/dist/{cli-dxak0fqd.js → cli-787ph852.js} +11 -11
- package/dist/{cli-793jvbsh.js → cli-7bv8nckk.js} +16 -11
- package/dist/{cli-zyxs93y9.js → cli-7jcc9y1r.js} +3 -3
- package/dist/{cli-f1am9szv.js → cli-87t2gstf.js} +1 -1
- package/dist/{cli-9aryf9tv.js → cli-95m51ztz.js} +1 -1
- package/dist/{cli-4w4fmq9x.js → cli-9w49enmh.js} +2 -2
- package/dist/{cli-v69vk5h0.js → cli-9y5b02vr.js} +13 -13
- package/dist/{cli-x23me2mg.js → cli-a1rrkrb4.js} +1 -1
- package/dist/{cli-psrqtw2m.js → cli-a3pkfpqs.js} +2 -2
- package/dist/{cli-h0rgxd24.js → cli-a4ajmfvp.js} +6 -6
- package/dist/{cli-ygrbmjja.js → cli-aw8bb6sf.js} +6 -6
- package/dist/{cli-g74vpjp2.js → cli-brm3et47.js} +55 -22
- package/dist/{cli-czkae1fp.js → cli-c5ddbdfj.js} +9 -9
- package/dist/{cli-114fvtmj.js → cli-c79a9zrc.js} +1 -1
- package/dist/{cli-49n9f49x.js → cli-dx3dz0ms.js} +3 -3
- package/dist/{cli-6dp3ydxa.js → cli-e07exmsb.js} +18 -14
- package/dist/cli-et1bbwqf.js +13 -0
- package/dist/{cli-yazx6hap.js → cli-f3s9dry0.js} +14 -14
- package/dist/{cli-t8d349cy.js → cli-f4wj0s3m.js} +3 -3
- package/dist/{cli-d6n0thxw.js → cli-f64gdfxc.js} +1 -1
- package/dist/{cli-pwfsqc2x.js → cli-fyd58xgb.js} +55 -55
- package/dist/{cli-7dm2zqy2.js → cli-g0j14czv.js} +2 -2
- package/dist/{cli-b0p7rfnd.js → cli-g2p9bd1z.js} +14 -14
- package/dist/{cli-z905tvmm.js → cli-g814jbst.js} +2 -2
- package/dist/{cli-5dsbcjjw.js → cli-ghexzd2d.js} +970 -970
- package/dist/{cli-jvv0g8bh.js → cli-gpt9kf01.js} +15 -15
- package/dist/{cli-rgrfzg8z.js → cli-grt6qv91.js} +380 -380
- package/dist/{cli-wz9115x0.js → cli-gtafvdy0.js} +8 -8
- package/dist/{cli-q5dzzak0.js → cli-gy07xk4s.js} +2 -2
- package/dist/{cli-sy7jk3tx.js → cli-h0djenzm.js} +2 -2
- package/dist/{cli-nrkkknat.js → cli-h37gbjbf.js} +2 -281
- package/dist/{cli-jevgrb4w.js → cli-h39xxnq6.js} +1200 -491
- package/dist/{cli-gz9sbyk3.js → cli-jepmgn7r.js} +1 -1
- package/dist/{cli-ccf9vy7q.js → cli-jmvk6et8.js} +7 -7
- package/dist/{cli-2rk0cx7s.js → cli-jxj92gkt.js} +51 -41
- package/dist/{cli-bqw0mt1n.js → cli-k46t4664.js} +18 -3
- package/dist/{cli-50vbt00w.js → cli-m07313tr.js} +1153 -696
- package/dist/{cli-4j2mrzag.js → cli-mkq1rk7b.js} +1 -1
- package/dist/{cli-1z7048dy.js → cli-mnm7w46e.js} +1 -1
- package/dist/{cli-kd1ck4rx.js → cli-mp9sg4cg.js} +14 -8
- package/dist/{cli-jbcva0xj.js → cli-mrammd8c.js} +60 -35
- package/dist/cli-nanaxrz6.js +27 -0
- package/dist/{cli-p9swy5t3.js → cli-pq2ee5ja.js} +8 -6
- package/dist/{cli-2wadx7p2.js → cli-pt43pdp1.js} +2 -2
- package/dist/{cli-7sgrxg9a.js → cli-q6d0vvey.js} +4 -4
- package/dist/{cli-k9qkf1hw.js → cli-qf9kpsve.js} +9 -9
- package/dist/cli-qh4sqfws.js +15 -0
- package/dist/{cli-e15jhafb.js → cli-qntkxysp.js} +14 -14
- package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
- package/dist/{cli-z2zcxes1.js → cli-qt5bt3vk.js} +1 -1
- package/dist/{cli-wx82tw1k.js → cli-qtekrty1.js} +3 -3
- package/dist/{cli-g1re8r05.js → cli-rh6f3s9n.js} +3 -3
- package/dist/{cli-n0krnc6d.js → cli-rzhcrw2p.js} +2 -2
- package/dist/{cli-gecy3z2s.js → cli-snw62ze5.js} +4 -4
- package/dist/{cli-n6h8qt8n.js → cli-snz3p4yf.js} +15 -10
- package/dist/{cli-r6kqqbj0.js → cli-tkf8n6cw.js} +11 -11
- package/dist/{cli-4rkfg1wx.js → cli-tkrr24mz.js} +14 -9
- package/dist/{cli-9dr5qzc0.js → cli-tv9f1kyb.js} +54 -43
- package/dist/{cli-gm9cbhbq.js → cli-vtz6yj3v.js} +28 -9
- package/dist/{cli-2f07m0xr.js → cli-wf86d5t7.js} +1 -1
- package/dist/{cli-7zd05xb4.js → cli-whrvct19.js} +45 -45
- package/dist/{cli-ap1czx8f.js → cli-wrrea8xm.js} +3 -3
- package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
- package/dist/{cli-cwy5hqbq.js → cli-yjmk7tzc.js} +1 -1
- package/dist/{cli-6pcz9h15.js → cli-zykjrwcx.js} +180 -394
- package/dist/cli.js +45 -45
- package/dist/{codex-adapter-htjkn22b.js → codex-adapter-hsq6mz3m.js} +21 -19
- package/dist/{codex-hook-hq54t9ft.js → codex-hook-8c500gq8.js} +8 -7
- package/dist/{codex-login-z6yhbkmk.js → codex-login-x7603481.js} +3 -3
- package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-c1x3x143.js} +15 -13
- package/dist/{commands-xhrfwqsk.js → commands-rwtddtst.js} +11 -9
- package/dist/{db-anvwb22f.js → db-j2b355vz.js} +458 -444
- package/dist/{devin-adapter-v1sarqtw.js → devin-adapter-8qmhqpy3.js} +3 -3
- package/dist/{devtools-2y1r7yqk.js → devtools-j2rnj8w7.js} +4 -4
- package/dist/{e2b-jdkwemv2.js → e2b-znpx78eg.js} +16 -16
- package/dist/{es5-vjgtkgp6.js → es5-fmqrjzy3.js} +2 -2
- package/dist/{event-bus-axn823e9.js → event-bus-n8sb35pw.js} +4 -4
- package/dist/{getMachineId-bsd-6tc9wxdj.js → getMachineId-bsd-wpzbhkm6.js} +4 -4
- package/dist/{getMachineId-darwin-sjt9sgnc.js → getMachineId-darwin-45zabjy0.js} +4 -4
- package/dist/{getMachineId-linux-v5n18y9p.js → getMachineId-linux-warnp5pd.js} +3 -3
- package/dist/{getMachineId-unsupported-yrjyqw3d.js → getMachineId-unsupported-5spent2a.js} +3 -3
- package/dist/{getMachineId-win-w3r8063c.js → getMachineId-win-v4bvcw7a.js} +4 -4
- package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-b0wzss55.js} +17 -14
- package/dist/{google-vertex-p5arrga0.js → google-vertex-e8q4dpya.js} +18 -15
- package/dist/{handlers-b9kbc621.js → handlers-hchgrxkh.js} +32 -29
- package/dist/{hook-xkvn6c5h.js → hook-p49egqc0.js} +66 -52
- package/dist/hook-spb00w76.js +34 -0
- package/dist/{http-9xh6ta4r.js → http-nh14kkms.js} +663 -181
- package/dist/index-1qthq7vc.js +6 -0
- package/dist/{index-0g68f0p6.js → index-20xqmn3v.js} +3 -3
- package/dist/{index-vdkr5z2w.js → index-3e03k6e4.js} +114 -114
- package/dist/{index-ywds4812.js → index-4cbyhq2g.js} +43 -41
- package/dist/index-67pzqfrb.js +36 -0
- package/dist/{index-bccrkdkn.js → index-7m8yxkw5.js} +16 -16
- package/dist/{index-eaxyqcmj.js → index-dvsk64sm.js} +163 -83
- package/dist/{index-dqkpnmqm.js → index-e19s2m8k.js} +18 -18
- package/dist/{index-j30k5zxn.js → index-emvxcy98.js} +16 -16
- package/dist/{index-7ndw6ecy.js → index-gqnmr60d.js} +9 -9
- package/dist/{index-9znhar3x.js → index-j02xvjtd.js} +184 -185
- package/dist/{index-1s2442yw.js → index-ns5kksc1.js} +7 -7
- package/dist/{index-dpy2hth8.js → index-pdxc8kfh.js} +71 -71
- package/dist/{index-z1m7pw0v.js → index-ptcqm8bc.js} +107 -107
- package/dist/{index-070v4eyb.js → index-pyp7rexm.js} +5 -6
- package/dist/{index-86znjz1p.js → index-shne41sd.js} +13 -13
- package/dist/{index-2qn2qndv.js → index-t36q2v4t.js} +183 -183
- package/dist/index-tcnvv8w6.js +7 -0
- package/dist/index-w91qbfv1.js +7 -0
- package/dist/{index-fzc0mwvj.js → index-yqweff5n.js} +32 -32
- package/dist/index-yrfphye1.js +39 -0
- package/dist/{keepalive-vc7s2aa6.js → keepalive-9k51nqhc.js} +21 -15
- package/dist/lead-2xq3dxbz.js +88 -0
- package/dist/maintenance-0c0g6nym.js +28 -0
- package/dist/mistral-conversations-2njwdwhj.js +717 -0
- package/dist/{multipart-parser-qaktc7p8.js → multipart-parser-6gftesw0.js} +2 -2
- package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-273a64r2.js} +14 -12
- package/dist/{onboard-xj0f73f9.js → onboard-tsv9wwhz.js} +12 -12
- package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-kt84eq86.js} +26 -26
- package/dist/{openai-completions-3vkmfz2a.js → openai-completions-mrypq7qp.js} +17 -16
- package/dist/{openai-responses-dg4t7j7a.js → openai-responses-qhrk21ez.js} +20 -17
- package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-rar9676j.js} +13 -11
- package/dist/{openrouter-images-g3gm8931.js → openrouter-images-684fysx9.js} +5 -5
- package/dist/{otel-impl-nxg0n0b3.js → otel-impl-wp1mfzd2.js} +752 -537
- package/dist/{photon_rs-ky7dj5qw.js → photon_rs-8dwt2pzy.js} +581 -581
- package/dist/{pi-messages-tqj8q6b9.js → pi-messages-txp9f97r.js} +7 -7
- package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-yz47kk8e.js} +1910 -1180
- package/dist/pricing-refresh-jw1yw2fn.js +22 -0
- package/dist/rbac-roles-dg964c8p.js +32 -0
- package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-p53cg5cv.js} +19 -17
- package/dist/{registry-02cy9z5z.js → registry-mh12y17v.js} +4 -4
- package/dist/render-v2-56vn2rk9.js +36 -0
- package/dist/seed-pricing-52xx6z63.js +19 -0
- package/dist/{setup-46xtgq3c.js → setup-e8gp79sy.js} +7 -7
- package/dist/token-util-zy908s1a.js +6 -0
- package/dist/{token-fxkn2zf2.js → token-xg5711x5.js} +4 -4
- package/dist/worker-s17f1g46.js +88 -0
- package/dist/{x-6mg4kfpe.js → x-xkfh0w0v.js} +5 -5
- package/openapi.json +566 -22
- package/package.json +6 -6
- package/src/be/budget-refusal-notify.ts +14 -7
- package/src/be/db-queries/task-artifacts.ts +182 -0
- package/src/be/db.ts +834 -171
- package/src/be/events.ts +5 -0
- package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
- package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
- package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
- package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
- package/src/be/migrations/136_task_requester_provenance.sql +37 -0
- package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
- package/src/be/migrations/runner.ts +17 -11
- package/src/be/multi-runtime.ts +430 -0
- package/src/be/scripts/extract-schema.ts +3 -1
- package/src/be/scripts/typecheck.ts +6 -2
- package/src/be/seed-prompt-templates.ts +25 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -1
- package/src/be/swarm-config-guard.ts +18 -0
- package/src/commands/credential-wait.ts +54 -0
- package/src/commands/onboard/steps/core-credentials.tsx +1 -1
- package/src/commands/onboard/steps/custom-templates.tsx +1 -1
- package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
- package/src/commands/onboard/steps/health-check.tsx +1 -1
- package/src/commands/onboard/steps/start.tsx +1 -1
- package/src/commands/profile-sync-audit.ts +56 -0
- package/src/commands/profile-sync.ts +287 -15
- package/src/commands/provider-credentials.ts +46 -19
- package/src/commands/runner.ts +199 -33
- package/src/commands/templates.ts +22 -0
- package/src/github/handlers.ts +55 -9
- package/src/heartbeat/heartbeat.ts +109 -17
- package/src/hooks/hook.ts +49 -33
- package/src/http/active-sessions.ts +11 -1
- package/src/http/agents.ts +261 -30
- package/src/http/all-routes.ts +1 -0
- package/src/http/config.ts +13 -2
- package/src/http/core.ts +102 -4
- package/src/http/db-query-bounded.ts +313 -0
- package/src/http/db-query-shared.ts +249 -0
- package/src/http/db-query.ts +54 -42
- package/src/http/events.ts +2 -0
- package/src/http/index.ts +23 -2
- package/src/http/mcp-bridge.ts +37 -3
- package/src/http/mcp.ts +12 -0
- package/src/http/memory.ts +13 -2
- package/src/http/openapi.ts +1 -0
- package/src/http/poll.ts +70 -27
- package/src/http/prompt-templates.ts +36 -13
- package/src/http/route-def.ts +43 -9
- package/src/http/schedules.ts +2 -2
- package/src/http/scripts.ts +10 -1
- package/src/http/session-data.ts +44 -0
- package/src/http/tasks.ts +2 -2
- package/src/http/users.ts +74 -52
- package/src/http/webhooks.ts +5 -2
- package/src/http/workflows.ts +1 -4
- package/src/integrations/kapso/inbound.ts +1 -1
- package/src/jira/outbound.ts +39 -8
- package/src/jira/webhook-lifecycle.ts +13 -2
- package/src/linear/outbound.ts +43 -10
- package/src/oauth/keepalive.ts +13 -2
- package/src/prompts/base-prompt.ts +1 -1
- package/src/prompts/default-drift.ts +21 -0
- package/src/prompts/session-templates.ts +56 -7
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/codex-adapter.ts +6 -0
- package/src/providers/opencode-adapter.ts +6 -0
- package/src/providers/pi-mono-adapter.ts +8 -0
- package/src/providers/pricing-sources.md +24 -0
- package/src/queue-stall-alarm.ts +216 -0
- package/src/rbac/legacy-policy.ts +3 -0
- package/src/rbac/permissions.ts +12 -0
- package/src/scheduler/scheduler.ts +7 -0
- package/src/script-workflows/executor.ts +2 -1
- package/src/scripts-runtime/executors/native.ts +2 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/loader.ts +5 -0
- package/src/scripts-runtime/sdk-allowlist.ts +3 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +4 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +4 -0
- package/src/server.ts +6 -0
- package/src/slack/channel-join.ts +22 -3
- package/src/slack/channel-lifecycle.ts +114 -0
- package/src/tests/budget-claim-gate.test.ts +20 -0
- package/src/tests/claude-adapter.test.ts +32 -0
- package/src/tests/codex-adapter.test.ts +30 -0
- package/src/tests/create-task-validation.test.ts +128 -0
- package/src/tests/credential-wait.test.ts +67 -1
- package/src/tests/db-query.test.ts +711 -2
- package/src/tests/heartbeat-checklist.test.ts +28 -0
- package/src/tests/http-api-integration.test.ts +81 -0
- package/src/tests/http-users.test.ts +166 -1
- package/src/tests/identity-field-budget.test.ts +176 -0
- package/src/tests/mcp-tools-user.test.ts +34 -0
- package/src/tests/mcp-user-route.test.ts +17 -0
- package/src/tests/metrics-http.test.ts +44 -0
- package/src/tests/migration-failure.test.ts +86 -0
- package/src/tests/multi-runtime-registration.test.ts +2810 -0
- package/src/tests/openapi-response-contract.test.ts +24 -0
- package/src/tests/openapi-route-headers.test.ts +83 -0
- package/src/tests/opencode-adapter.test.ts +40 -0
- package/src/tests/outbound-listener-rejection.test.ts +138 -0
- package/src/tests/pi-mono-adapter.test.ts +98 -0
- package/src/tests/preload.ts +2 -0
- package/src/tests/profile-sync-warning.test.ts +277 -0
- package/src/tests/profile-sync.test.ts +232 -6
- package/src/tests/prompt-template-resolver.test.ts +177 -6
- package/src/tests/prompt-template-session.test.ts +39 -3
- package/src/tests/queue-stall-alarm.test.ts +226 -0
- package/src/tests/rbac-charact-slack.test.ts +29 -0
- package/src/tests/rbac-engine.test.ts +3 -0
- package/src/tests/requester-comms.test.ts +32 -0
- package/src/tests/runner-requester-profile.test.ts +38 -0
- package/src/tests/schedule-target-type.test.ts +38 -0
- package/src/tests/script-connections.test.ts +13 -1
- package/src/tests/scripts-runtime-identity.test.ts +375 -0
- package/src/tests/scripts-runtime.test.ts +4 -3
- package/src/tests/sdk-allowlist.test.ts +47 -0
- package/src/tests/send-task-requested-by.test.ts +9 -0
- package/src/tests/session-costs.test.ts +506 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
- package/src/tests/slack-channel-lifecycle.test.ts +241 -0
- package/src/tests/slack-render-v2.test.ts +43 -0
- package/src/tests/steering-core.test.ts +28 -0
- package/src/tests/swarm-config-reserved-keys.test.ts +22 -0
- package/src/tests/task-artifacts.test.ts +386 -0
- package/src/tests/task-attachments-schema.test.ts +17 -1
- package/src/tests/task-completion-idempotency.test.ts +57 -0
- package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
- package/src/tests/tool-annotations.test.ts +4 -0
- package/src/tests/update-profile-api.test.ts +67 -39
- package/src/tests/update-profile-auth.test.ts +48 -4
- package/src/tests/user-attribution-migration.test.ts +54 -0
- package/src/tests/user-identity.test.ts +72 -0
- package/src/tests/workflow-async-v2.test.ts +39 -1
- package/src/tests/workflow-http-v2.test.ts +3 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
- package/src/tests/workflow-retry-v2.test.ts +45 -1
- package/src/tools/db-query.ts +18 -8
- package/src/tools/get-task-details.ts +24 -3
- package/src/tools/manage-user.ts +36 -3
- package/src/tools/poll-task.ts +93 -14
- package/src/tools/script-common.ts +5 -0
- package/src/tools/send-task.ts +1 -4
- package/src/tools/slack-archive-channel.ts +70 -0
- package/src/tools/slack-create-channel.ts +74 -0
- package/src/tools/slack-invite-to-channel.ts +78 -0
- package/src/tools/store-progress.ts +13 -2
- package/src/tools/swarm-config/delete-config.ts +9 -0
- package/src/tools/swarm-config/set-config.ts +3 -2
- package/src/tools/task-action.ts +30 -0
- package/src/tools/task-tool-ctx.ts +8 -1
- package/src/tools/tool-config.ts +4 -1
- package/src/tools/update-profile.ts +16 -21
- package/src/tools/utils.ts +5 -0
- package/src/tools/workflows/trigger-workflow.ts +7 -1
- package/src/types.ts +165 -1
- package/src/utils/github-pull-request.ts +76 -0
- package/src/utils/identity-field-budget.ts +73 -0
- package/src/utils/multi-runtime.ts +20 -0
- package/src/utils/requester-comms.ts +24 -0
- package/src/workflows/engine.ts +22 -2
- package/src/workflows/resume.ts +19 -11
- package/src/workflows/retry-poller.ts +1 -0
- package/src/workflows/triggers.ts +4 -0
- package/templates/skills/db-query-guidance/config.json +14 -0
- package/templates/skills/db-query-guidance/content.md +57 -0
- package/dist/app-5da78bd4.js +0 -26
- package/dist/cli-3te4jdkm.js +0 -225
- package/dist/cli-bmze986d.js +0 -7
- package/dist/cli-kh3ba2zf.js +0 -607
- package/dist/hook-cgk4wxmx.js +0 -31
- package/dist/index-25wjsdv2.js +0 -34
- package/dist/index-76at2ymp.js +0 -7
- package/dist/index-hase6zbe.js +0 -6
- package/dist/index-vg4snay4.js +0 -36
- package/dist/index-vy8edt27.js +0 -7
- package/dist/lead-r2yqfmx7.js +0 -85
- package/dist/maintenance-mw70k468.js +0 -26
- package/dist/mistral-conversations-81fwav0j.js +0 -30578
- package/dist/otel-nqa12wf3.js +0 -3149
- package/dist/pricing-refresh-2cn6effr.js +0 -20
- package/dist/rbac-roles-vynday5g.js +0 -30
- package/dist/render-v2-tc0bh8kk.js +0 -34
- package/dist/seed-pricing-8kceb43f.js +0 -17
- package/dist/token-util-pqyc8kng.js +0 -6
- package/dist/worker-qrbz6hmv.js +0 -85
package/src/be/db.ts
CHANGED
|
@@ -116,19 +116,28 @@ import type {
|
|
|
116
116
|
} from "../types";
|
|
117
117
|
import {
|
|
118
118
|
AgentAvatarSchema,
|
|
119
|
+
type CreateTaskOptions,
|
|
120
|
+
CreateTaskOptionsSchema,
|
|
119
121
|
FollowUpConfigSchema,
|
|
120
122
|
isTerminalTaskStatus,
|
|
121
123
|
type ModelTier,
|
|
122
124
|
parseModelTier,
|
|
123
125
|
ReasoningEffortSchema,
|
|
124
126
|
RoutingAffinitySchema,
|
|
127
|
+
SERVER_GENERATED_ATTACHMENT_CAPABILITY,
|
|
125
128
|
SessionCostModelBreakdownSchema,
|
|
126
129
|
} from "../types";
|
|
127
130
|
import { deriveProviderFromKeyType } from "../utils/credentials";
|
|
128
131
|
import { isEnvFlagEnabled } from "../utils/env-flag";
|
|
129
132
|
import type { RateLimitWindowTelemetry } from "../utils/error-tracker";
|
|
133
|
+
import { extractGitHubPullRequestUrls } from "../utils/github-pull-request";
|
|
134
|
+
import {
|
|
135
|
+
type BudgetedIdentityField,
|
|
136
|
+
checkIdentityFieldBudget,
|
|
137
|
+
IdentityFieldBudgetError,
|
|
138
|
+
} from "../utils/identity-field-budget";
|
|
130
139
|
import { getCurrentRequestUserId } from "../utils/request-auth-context";
|
|
131
|
-
import { scrubSecrets } from "../utils/secret-scrubber";
|
|
140
|
+
import { registerVolatileSecret, scrubSecrets } from "../utils/secret-scrubber";
|
|
132
141
|
import { auditAssetKeys, enforceAssetKeyStartupAudit } from "./asset-key-audit";
|
|
133
142
|
import { migrateLegacyCredentialBindingBlob } from "./connection-bindings-blob-migration";
|
|
134
143
|
import { decryptSecret, encryptSecret, getEncryptionKey, resolveEncryptionKey } from "./crypto";
|
|
@@ -181,6 +190,40 @@ export function isSqliteVecAvailable(): boolean {
|
|
|
181
190
|
return sqliteVecAvailable;
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Resolve the sqlite-vec loadable extension path without opening a Database.
|
|
195
|
+
* Mirrors loadSqliteVec's env-var-first, npm-fallback resolution, but returns
|
|
196
|
+
* the path instead of loading it: the bounded db-query child process (see
|
|
197
|
+
* src/http/db-query-bounded.ts) opens its own read-only connection and must
|
|
198
|
+
* load the extension itself, so the parent resolves the path once and passes
|
|
199
|
+
* it along. The result is memoized — `undefined` is a valid resolved answer
|
|
200
|
+
* (extension genuinely unavailable) and is cached too, so a failing
|
|
201
|
+
* `require` doesn't retry on every query.
|
|
202
|
+
*/
|
|
203
|
+
let cachedSqliteVecExtensionPath: string | undefined | null = null;
|
|
204
|
+
|
|
205
|
+
export function resolveSqliteVecExtensionPath(): string | undefined {
|
|
206
|
+
if (cachedSqliteVecExtensionPath !== null) return cachedSqliteVecExtensionPath;
|
|
207
|
+
const extensionPath = process.env.SQLITE_VEC_EXTENSION_PATH;
|
|
208
|
+
if (extensionPath) {
|
|
209
|
+
cachedSqliteVecExtensionPath = extensionPath;
|
|
210
|
+
return cachedSqliteVecExtensionPath;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
cachedSqliteVecExtensionPath = (
|
|
214
|
+
require("sqlite-vec") as { getLoadablePath(): string }
|
|
215
|
+
).getLoadablePath();
|
|
216
|
+
} catch {
|
|
217
|
+
cachedSqliteVecExtensionPath = undefined;
|
|
218
|
+
}
|
|
219
|
+
return cachedSqliteVecExtensionPath;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Test-only: clear the memoized sqlite-vec extension path cache. */
|
|
223
|
+
export function __resetSqliteVecExtensionPathCacheForTests(): void {
|
|
224
|
+
cachedSqliteVecExtensionPath = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
184
227
|
function loadSqliteVec(database: Database): void {
|
|
185
228
|
sqliteVecAvailable = false;
|
|
186
229
|
try {
|
|
@@ -247,7 +290,17 @@ export function initDb(dbPath = "./agent-swarm-db.sqlite"): Database {
|
|
|
247
290
|
loadSqliteVec(database);
|
|
248
291
|
|
|
249
292
|
// Run database migrations (schema creation + incremental changes)
|
|
250
|
-
|
|
293
|
+
try {
|
|
294
|
+
runMigrations(database);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
db = null;
|
|
297
|
+
try {
|
|
298
|
+
database.close();
|
|
299
|
+
} catch (closeError) {
|
|
300
|
+
console.error("[migrations] Failed to close database after migration failure:", closeError);
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
251
304
|
|
|
252
305
|
// Compatibility migration for legacy databases that predate profile fields
|
|
253
306
|
ensureAgentProfileColumns(database);
|
|
@@ -432,6 +485,13 @@ const VERSIONABLE_FIELDS: VersionableField[] = [
|
|
|
432
485
|
"heartbeatMd",
|
|
433
486
|
];
|
|
434
487
|
|
|
488
|
+
const BUDGETED_IDENTITY_FIELDS: BudgetedIdentityField[] = [
|
|
489
|
+
"soulMd",
|
|
490
|
+
"identityMd",
|
|
491
|
+
"claudeMd",
|
|
492
|
+
"toolsMd",
|
|
493
|
+
];
|
|
494
|
+
|
|
435
495
|
function ensureAgentProfileColumns(database: Database): void {
|
|
436
496
|
const existingColumns = new Set(
|
|
437
497
|
database
|
|
@@ -775,6 +835,19 @@ export function updateAgentCredentialState(
|
|
|
775
835
|
return row ? rowToAgent(row) : null;
|
|
776
836
|
}
|
|
777
837
|
|
|
838
|
+
/**
|
|
839
|
+
* Record which env vars a worker is missing without touching status — the
|
|
840
|
+
* logical status is derived from runtime readiness in multi-runtime mode.
|
|
841
|
+
*/
|
|
842
|
+
export function updateAgentCredentialMissing(agentId: string, missing: string[] | null): void {
|
|
843
|
+
const json = missing && missing.length > 0 ? JSON.stringify(missing) : null;
|
|
844
|
+
getDb()
|
|
845
|
+
.prepare(
|
|
846
|
+
"UPDATE agents SET credentialMissing = ?, lastUpdatedAt = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?",
|
|
847
|
+
)
|
|
848
|
+
.run(json, agentId);
|
|
849
|
+
}
|
|
850
|
+
|
|
778
851
|
export function createAgent(
|
|
779
852
|
agent: Omit<Agent, "id" | "createdAt" | "lastUpdatedAt"> & { id?: string },
|
|
780
853
|
): Agent {
|
|
@@ -1014,12 +1087,22 @@ export function deleteAgent(id: string): boolean {
|
|
|
1014
1087
|
* Get the count of active (in_progress) tasks for an agent.
|
|
1015
1088
|
* Used to determine current capacity usage.
|
|
1016
1089
|
*/
|
|
1090
|
+
/**
|
|
1091
|
+
* Tasks occupying one of the agent's concurrency slots.
|
|
1092
|
+
*
|
|
1093
|
+
* A claimed offer counts too — omitting it let a second concurrent poll take
|
|
1094
|
+
* another task past the limit. It is counted only through `offeredTo`, the
|
|
1095
|
+
* agent actually reviewing it: `agentId` on an offer may still be the lead
|
|
1096
|
+
* that created it, which would otherwise consume the lead's own capacity.
|
|
1097
|
+
*/
|
|
1017
1098
|
export function getActiveTaskCount(agentId: string): number {
|
|
1018
1099
|
const result = getDb()
|
|
1019
|
-
.prepare<{ count: number }, [string]>(
|
|
1020
|
-
|
|
1100
|
+
.prepare<{ count: number }, [string, string]>(
|
|
1101
|
+
`SELECT COUNT(*) as count FROM agent_tasks
|
|
1102
|
+
WHERE (agentId = ? AND status = 'in_progress')
|
|
1103
|
+
OR (offeredTo = ? AND status = 'reviewing')`,
|
|
1021
1104
|
)
|
|
1022
|
-
.get(agentId);
|
|
1105
|
+
.get(agentId, agentId);
|
|
1023
1106
|
return result?.count ?? 0;
|
|
1024
1107
|
}
|
|
1025
1108
|
|
|
@@ -1202,6 +1285,7 @@ type AgentTaskRow = {
|
|
|
1202
1285
|
credentialKeySuffix: string | null;
|
|
1203
1286
|
credentialKeyType: string | null;
|
|
1204
1287
|
requestedByUserId: string | null;
|
|
1288
|
+
requestedByUserIdInherited: number;
|
|
1205
1289
|
swarmVersion: string | null;
|
|
1206
1290
|
provider: string | null;
|
|
1207
1291
|
providerMeta: string | null;
|
|
@@ -2143,13 +2227,29 @@ export function updateTaskVcs(
|
|
|
2143
2227
|
vcsUrl: string;
|
|
2144
2228
|
},
|
|
2145
2229
|
): AgentTask | null {
|
|
2146
|
-
const row = getDb()
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2230
|
+
const row = getDb().transaction(() => {
|
|
2231
|
+
const updated = getDb()
|
|
2232
|
+
.prepare<AgentTaskRow, [string, string, number, string, string, string]>(
|
|
2233
|
+
`UPDATE agent_tasks
|
|
2234
|
+
SET vcsProvider = ?, vcsRepo = ?, vcsNumber = ?, vcsUrl = ?, lastUpdatedAt = ?
|
|
2235
|
+
WHERE id = ? RETURNING *`,
|
|
2236
|
+
)
|
|
2237
|
+
.get(
|
|
2238
|
+
vcs.vcsProvider,
|
|
2239
|
+
vcs.vcsRepo,
|
|
2240
|
+
vcs.vcsNumber,
|
|
2241
|
+
vcs.vcsUrl,
|
|
2242
|
+
new Date().toISOString(),
|
|
2243
|
+
taskId,
|
|
2244
|
+
);
|
|
2245
|
+
if (updated) {
|
|
2246
|
+
reconcileTaskPullRequestAttachments(taskId, updated.agentId, [
|
|
2247
|
+
updated.output,
|
|
2248
|
+
vcs.vcsProvider === "github" ? vcs.vcsUrl : null,
|
|
2249
|
+
]);
|
|
2250
|
+
}
|
|
2251
|
+
return updated;
|
|
2252
|
+
})();
|
|
2153
2253
|
return row ? rowToAgentTask(row) : null;
|
|
2154
2254
|
}
|
|
2155
2255
|
|
|
@@ -2931,13 +3031,23 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
2931
3031
|
return null;
|
|
2932
3032
|
}
|
|
2933
3033
|
|
|
2934
|
-
const
|
|
2935
|
-
|
|
2936
|
-
|
|
3034
|
+
const row = getDb().transaction(() => {
|
|
3035
|
+
const finishedAt = new Date().toISOString();
|
|
3036
|
+
let completed = taskQueries.updateStatus().get("completed", finishedAt, id);
|
|
3037
|
+
if (!completed) return null;
|
|
2937
3038
|
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3039
|
+
if (output) {
|
|
3040
|
+
completed = taskQueries.setOutput().get(scrubSecrets(output), id);
|
|
3041
|
+
}
|
|
3042
|
+
if (completed) {
|
|
3043
|
+
reconcileTaskPullRequestAttachments(id, completed.agentId, [
|
|
3044
|
+
completed.output,
|
|
3045
|
+
completed.vcsProvider === "github" ? completed.vcsUrl : null,
|
|
3046
|
+
]);
|
|
3047
|
+
}
|
|
3048
|
+
return completed;
|
|
3049
|
+
})();
|
|
3050
|
+
if (!row) return null;
|
|
2941
3051
|
|
|
2942
3052
|
if (row && oldTask) {
|
|
2943
3053
|
emitTaskLifecycleTelemetryAfterCommit(
|
|
@@ -2961,8 +3071,8 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
2961
3071
|
newValue: "completed",
|
|
2962
3072
|
});
|
|
2963
3073
|
} catch {}
|
|
2964
|
-
|
|
2965
|
-
|
|
3074
|
+
import("../workflows/event-bus")
|
|
3075
|
+
.then(({ workflowEventBus }) => {
|
|
2966
3076
|
workflowEventBus.emit("task.completed", {
|
|
2967
3077
|
taskId: id,
|
|
2968
3078
|
output,
|
|
@@ -2970,8 +3080,13 @@ export function completeTask(id: string, output?: string): AgentTask | null {
|
|
|
2970
3080
|
workflowRunId: row.workflowRunId,
|
|
2971
3081
|
workflowRunStepId: row.workflowRunStepId,
|
|
2972
3082
|
});
|
|
2973
|
-
})
|
|
2974
|
-
|
|
3083
|
+
})
|
|
3084
|
+
.catch((err) =>
|
|
3085
|
+
console.error(
|
|
3086
|
+
"[db] task.completed event not emitted:",
|
|
3087
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3088
|
+
),
|
|
3089
|
+
);
|
|
2975
3090
|
try {
|
|
2976
3091
|
promotePendingSteeringForTask(id, "Task completed before steering was delivered");
|
|
2977
3092
|
} catch (error) {
|
|
@@ -3022,8 +3137,8 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
3022
3137
|
metadata: { reason: scrubbedReason },
|
|
3023
3138
|
});
|
|
3024
3139
|
} catch {}
|
|
3025
|
-
|
|
3026
|
-
|
|
3140
|
+
import("../workflows/event-bus")
|
|
3141
|
+
.then(({ workflowEventBus }) => {
|
|
3027
3142
|
workflowEventBus.emit("task.failed", {
|
|
3028
3143
|
taskId: id,
|
|
3029
3144
|
failureReason: reason,
|
|
@@ -3031,8 +3146,13 @@ export function failTask(id: string, reason: string): AgentTask | null {
|
|
|
3031
3146
|
workflowRunId: row.workflowRunId,
|
|
3032
3147
|
workflowRunStepId: row.workflowRunStepId,
|
|
3033
3148
|
});
|
|
3034
|
-
})
|
|
3035
|
-
|
|
3149
|
+
})
|
|
3150
|
+
.catch((err) =>
|
|
3151
|
+
console.error(
|
|
3152
|
+
"[db] task.failed event not emitted:",
|
|
3153
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3154
|
+
),
|
|
3155
|
+
);
|
|
3036
3156
|
try {
|
|
3037
3157
|
promotePendingSteeringForTask(id, "Task failed before steering was delivered");
|
|
3038
3158
|
} catch (error) {
|
|
@@ -3070,7 +3190,16 @@ export function overwriteTerminalTaskResultText(
|
|
|
3070
3190
|
patch.failureReason !== undefined
|
|
3071
3191
|
? scrubSecrets(patch.failureReason)
|
|
3072
3192
|
: (task.failureReason ?? null);
|
|
3073
|
-
const row =
|
|
3193
|
+
const row = getDb().transaction(() => {
|
|
3194
|
+
const updated = taskQueries.setTerminalResultText().get(output, failureReason, id) ?? null;
|
|
3195
|
+
if (updated && patch.output !== undefined) {
|
|
3196
|
+
reconcileTaskPullRequestAttachments(id, updated.agentId, [
|
|
3197
|
+
updated.output,
|
|
3198
|
+
updated.vcsProvider === "github" ? updated.vcsUrl : null,
|
|
3199
|
+
]);
|
|
3200
|
+
}
|
|
3201
|
+
return updated;
|
|
3202
|
+
})();
|
|
3074
3203
|
|
|
3075
3204
|
return row ? rowToAgentTask(row) : task;
|
|
3076
3205
|
}
|
|
@@ -3113,16 +3242,21 @@ export function cancelTask(id: string, reason?: string): AgentTask | null {
|
|
|
3113
3242
|
metadata: reason ? { reason } : undefined,
|
|
3114
3243
|
});
|
|
3115
3244
|
} catch {}
|
|
3116
|
-
|
|
3117
|
-
|
|
3245
|
+
import("../workflows/event-bus")
|
|
3246
|
+
.then(({ workflowEventBus }) => {
|
|
3118
3247
|
workflowEventBus.emit("task.cancelled", {
|
|
3119
3248
|
taskId: id,
|
|
3120
3249
|
agentId: row.agentId,
|
|
3121
3250
|
workflowRunId: row.workflowRunId,
|
|
3122
3251
|
workflowRunStepId: row.workflowRunStepId,
|
|
3123
3252
|
});
|
|
3124
|
-
})
|
|
3125
|
-
|
|
3253
|
+
})
|
|
3254
|
+
.catch((err) =>
|
|
3255
|
+
console.error(
|
|
3256
|
+
"[db] task.cancelled event not emitted:",
|
|
3257
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3258
|
+
),
|
|
3259
|
+
);
|
|
3126
3260
|
try {
|
|
3127
3261
|
promotePendingSteeringForTask(id, "Task was cancelled before steering was delivered");
|
|
3128
3262
|
} catch (error) {
|
|
@@ -3203,8 +3337,8 @@ export function supersedeTask(
|
|
|
3203
3337
|
metadata: { reason: args.reason, resumeTaskId: args.resumeTaskId },
|
|
3204
3338
|
});
|
|
3205
3339
|
} catch {}
|
|
3206
|
-
|
|
3207
|
-
|
|
3340
|
+
import("../workflows/event-bus")
|
|
3341
|
+
.then(({ workflowEventBus }) => {
|
|
3208
3342
|
workflowEventBus.emit("task.superseded", {
|
|
3209
3343
|
taskId: id,
|
|
3210
3344
|
reason: args.reason,
|
|
@@ -3213,8 +3347,13 @@ export function supersedeTask(
|
|
|
3213
3347
|
workflowRunId: row.workflowRunId,
|
|
3214
3348
|
workflowRunStepId: row.workflowRunStepId,
|
|
3215
3349
|
});
|
|
3216
|
-
})
|
|
3217
|
-
|
|
3350
|
+
})
|
|
3351
|
+
.catch((err) =>
|
|
3352
|
+
console.error(
|
|
3353
|
+
"[db] task.superseded event not emitted:",
|
|
3354
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3355
|
+
),
|
|
3356
|
+
);
|
|
3218
3357
|
|
|
3219
3358
|
try {
|
|
3220
3359
|
cascadeFailDependents(id, "superseded");
|
|
@@ -3443,15 +3582,20 @@ export function updateTaskProgress(id: string, progress: string): AgentTask | nu
|
|
|
3443
3582
|
newValue: scrubbedProgress,
|
|
3444
3583
|
});
|
|
3445
3584
|
} catch {}
|
|
3446
|
-
|
|
3447
|
-
|
|
3585
|
+
import("../workflows/event-bus")
|
|
3586
|
+
.then(({ workflowEventBus }) => {
|
|
3448
3587
|
workflowEventBus.emit("task.progress", {
|
|
3449
3588
|
taskId: id,
|
|
3450
3589
|
progress: scrubbedProgress,
|
|
3451
3590
|
agentId: row.agentId,
|
|
3452
3591
|
});
|
|
3453
|
-
})
|
|
3454
|
-
|
|
3592
|
+
})
|
|
3593
|
+
.catch((err) =>
|
|
3594
|
+
console.error(
|
|
3595
|
+
"[db] task.progress event not emitted:",
|
|
3596
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
3597
|
+
),
|
|
3598
|
+
);
|
|
3455
3599
|
}
|
|
3456
3600
|
return row ? rowToAgentTask(row) : null;
|
|
3457
3601
|
}
|
|
@@ -3592,6 +3736,101 @@ export function insertTaskAttachment(input: InsertTaskAttachmentInput): TaskAtta
|
|
|
3592
3736
|
})();
|
|
3593
3737
|
}
|
|
3594
3738
|
|
|
3739
|
+
const GENERATED_PULL_REQUEST_PROVIDER_ID = "github";
|
|
3740
|
+
const GENERATED_PULL_REQUEST_INTENT = "task-deliverable";
|
|
3741
|
+
const GENERATED_PULL_REQUEST_DESCRIPTION = "Pull request shipped by this task";
|
|
3742
|
+
const GENERATED_PULL_REQUEST_SOURCE = "task-pull-request-recorder";
|
|
3743
|
+
|
|
3744
|
+
function pullRequestKey(pullRequest: { owner: string; repo: string; number: number }): string {
|
|
3745
|
+
return `${pullRequest.owner.toLowerCase()}/${pullRequest.repo.toLowerCase()}#${pullRequest.number}`;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
/**
|
|
3749
|
+
* Persist PRs detected by server-owned task lifecycle paths. Existing URL
|
|
3750
|
+
* attachments win regardless of display name, so an agent-authored row and an
|
|
3751
|
+
* automatic row never duplicate the same task deliverable. A capability key
|
|
3752
|
+
* rejected by AttachmentInputSchema marks server-generated rows so
|
|
3753
|
+
* reconciliation cannot mistake caller-authored metadata for provenance.
|
|
3754
|
+
*/
|
|
3755
|
+
export function recordTaskPullRequestAttachments(
|
|
3756
|
+
taskId: string,
|
|
3757
|
+
agentId: string | null,
|
|
3758
|
+
text: string | null | undefined,
|
|
3759
|
+
): TaskAttachment[] {
|
|
3760
|
+
const pullRequests = extractGitHubPullRequestUrls(text);
|
|
3761
|
+
if (pullRequests.length === 0) return [];
|
|
3762
|
+
|
|
3763
|
+
const existingPullRequests = new Set(
|
|
3764
|
+
getDb()
|
|
3765
|
+
.prepare<{ url: string }, [string]>(
|
|
3766
|
+
"SELECT url FROM task_attachments WHERE task_id = ? AND kind = 'url' AND url IS NOT NULL",
|
|
3767
|
+
)
|
|
3768
|
+
.all(taskId)
|
|
3769
|
+
.flatMap((row) => extractGitHubPullRequestUrls(row.url))
|
|
3770
|
+
.map(pullRequestKey),
|
|
3771
|
+
);
|
|
3772
|
+
const stored: TaskAttachment[] = [];
|
|
3773
|
+
for (const pullRequest of pullRequests) {
|
|
3774
|
+
const dedupeKey = pullRequestKey(pullRequest);
|
|
3775
|
+
if (existingPullRequests.has(dedupeKey)) continue;
|
|
3776
|
+
stored.push(
|
|
3777
|
+
insertTaskAttachment({
|
|
3778
|
+
taskId,
|
|
3779
|
+
agentId,
|
|
3780
|
+
name: `GitHub pull request #${pullRequest.number}`,
|
|
3781
|
+
kind: "url",
|
|
3782
|
+
url: pullRequest.url,
|
|
3783
|
+
providerId: GENERATED_PULL_REQUEST_PROVIDER_ID,
|
|
3784
|
+
capabilities: {
|
|
3785
|
+
[SERVER_GENERATED_ATTACHMENT_CAPABILITY]: GENERATED_PULL_REQUEST_SOURCE,
|
|
3786
|
+
},
|
|
3787
|
+
intent: GENERATED_PULL_REQUEST_INTENT,
|
|
3788
|
+
description: GENERATED_PULL_REQUEST_DESCRIPTION,
|
|
3789
|
+
}),
|
|
3790
|
+
);
|
|
3791
|
+
existingPullRequests.add(dedupeKey);
|
|
3792
|
+
}
|
|
3793
|
+
return stored;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
/** Reconcile only lifecycle-generated PR rows against their current source text. */
|
|
3797
|
+
function reconcileTaskPullRequestAttachments(
|
|
3798
|
+
taskId: string,
|
|
3799
|
+
agentId: string | null,
|
|
3800
|
+
sourceTexts: Array<string | null | undefined>,
|
|
3801
|
+
): TaskAttachment[] {
|
|
3802
|
+
const desiredPullRequests = sourceTexts.flatMap(extractGitHubPullRequestUrls);
|
|
3803
|
+
const desiredKeys = new Set(desiredPullRequests.map(pullRequestKey));
|
|
3804
|
+
const generatedRows = getDb()
|
|
3805
|
+
.prepare<{ id: string; name: string; url: string }, [string, string]>(
|
|
3806
|
+
`SELECT id, name, url
|
|
3807
|
+
FROM task_attachments
|
|
3808
|
+
WHERE task_id = ?
|
|
3809
|
+
AND kind = 'url'
|
|
3810
|
+
AND url IS NOT NULL
|
|
3811
|
+
AND json_extract(capabilities, '$.${SERVER_GENERATED_ATTACHMENT_CAPABILITY}') = ?`,
|
|
3812
|
+
)
|
|
3813
|
+
.all(taskId, GENERATED_PULL_REQUEST_SOURCE);
|
|
3814
|
+
|
|
3815
|
+
for (const row of generatedRows) {
|
|
3816
|
+
const pullRequest = extractGitHubPullRequestUrls(row.url)[0];
|
|
3817
|
+
const expectedName = pullRequest ? `GitHub pull request #${pullRequest.number}` : null;
|
|
3818
|
+
if (
|
|
3819
|
+
!pullRequest ||
|
|
3820
|
+
row.name !== expectedName ||
|
|
3821
|
+
!desiredKeys.has(pullRequestKey(pullRequest))
|
|
3822
|
+
) {
|
|
3823
|
+
getDb().run("DELETE FROM task_attachments WHERE id = ?", [row.id]);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3827
|
+
return recordTaskPullRequestAttachments(
|
|
3828
|
+
taskId,
|
|
3829
|
+
agentId,
|
|
3830
|
+
desiredPullRequests.map((pullRequest) => pullRequest.url).join("\n"),
|
|
3831
|
+
);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3595
3834
|
function insertTaskAttachmentRow(input: InsertTaskAttachmentInput): TaskAttachment {
|
|
3596
3835
|
const db = getDb();
|
|
3597
3836
|
|
|
@@ -4638,89 +4877,11 @@ export function getAllLogs(limit?: number): AgentLog[] {
|
|
|
4638
4877
|
// Task Pool Operations
|
|
4639
4878
|
// ============================================================================
|
|
4640
4879
|
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
taskType?: string;
|
|
4647
|
-
tags?: string[];
|
|
4648
|
-
priority?: number;
|
|
4649
|
-
dependsOn?: string[];
|
|
4650
|
-
offeredTo?: string;
|
|
4651
|
-
status?: "backlog" | "unassigned"; // Explicitly set initial status
|
|
4652
|
-
slackChannelId?: string;
|
|
4653
|
-
slackThreadTs?: string;
|
|
4654
|
-
/** Exact Slack message that directly triggered this task; never inherited. */
|
|
4655
|
-
slackTriggerMessageTs?: string;
|
|
4656
|
-
slackUserId?: string;
|
|
4657
|
-
/**
|
|
4658
|
-
* Opt out of the residual Slack/contextKey normalization below (see the
|
|
4659
|
-
* "Residual-mismatch guard" comment near the INSERT): a deliberate
|
|
4660
|
-
* cross-channel/thread dispatch (e.g. `send-task`'s
|
|
4661
|
-
* `overrideSlackContext: true`) sets this so its explicit slackChannelId/
|
|
4662
|
-
* slackThreadTs survive even when they disagree with a slack-family
|
|
4663
|
-
* `contextKey`/parent. Trusted callers that don't set this get normalized —
|
|
4664
|
-
* this boundary must not let a caller silently persist a mismatch.
|
|
4665
|
-
*/
|
|
4666
|
-
overrideSlackContext?: boolean;
|
|
4667
|
-
vcsProvider?: "github" | "gitlab";
|
|
4668
|
-
vcsRepo?: string;
|
|
4669
|
-
vcsEventType?: string;
|
|
4670
|
-
vcsNumber?: number;
|
|
4671
|
-
vcsCommentId?: number;
|
|
4672
|
-
vcsAuthor?: string;
|
|
4673
|
-
vcsUrl?: string;
|
|
4674
|
-
vcsInstallationId?: number;
|
|
4675
|
-
vcsNodeId?: string;
|
|
4676
|
-
agentmailInboxId?: string;
|
|
4677
|
-
agentmailMessageId?: string;
|
|
4678
|
-
agentmailThreadId?: string;
|
|
4679
|
-
mentionMessageId?: string;
|
|
4680
|
-
mentionChannelId?: string;
|
|
4681
|
-
dir?: string;
|
|
4682
|
-
parentTaskId?: string;
|
|
4683
|
-
model?: string;
|
|
4684
|
-
modelTier?: ModelTier;
|
|
4685
|
-
effort?: ReasoningEffort;
|
|
4686
|
-
scheduleId?: string;
|
|
4687
|
-
workflowRunId?: string;
|
|
4688
|
-
workflowRunStepId?: string;
|
|
4689
|
-
sourceTaskId?: string;
|
|
4690
|
-
/**
|
|
4691
|
-
* Optional JSON Schema the agent's final output must conform to.
|
|
4692
|
-
*
|
|
4693
|
-
* Enforced via the MCP `store-progress` tool (validated in
|
|
4694
|
-
* `src/tools/store-progress.ts`). NOT enforced when the task runs on
|
|
4695
|
-
* default-mode Devin (no MCP) — see runbooks/harness-providers.md
|
|
4696
|
-
* ("Per-task outputSchema support"). Callers reading `task.output` for
|
|
4697
|
-
* a schema'd task should be defensive about JSON parsing.
|
|
4698
|
-
*/
|
|
4699
|
-
outputSchema?: Record<string, unknown>;
|
|
4700
|
-
/**
|
|
4701
|
-
* When a `parentTaskId` is set, the child inherits the parent's `outputSchema`
|
|
4702
|
-
* by default. Set this to `false` to opt out — used by control-plane children
|
|
4703
|
-
* (e.g. the Lead `reroute-decision` task) that must inherit Slack/VCS context
|
|
4704
|
-
* from the parent but must NOT be forced to satisfy the original work's output
|
|
4705
|
-
* contract on completion (which would block the control task — DES-523).
|
|
4706
|
-
*/
|
|
4707
|
-
inheritParentOutputSchema?: boolean;
|
|
4708
|
-
followUpConfig?: FollowUpConfig;
|
|
4709
|
-
requestedByUserId?: string;
|
|
4710
|
-
contextKey?: string;
|
|
4711
|
-
/**
|
|
4712
|
-
* Internal control-plane escape hatch for continuations that MUST coexist
|
|
4713
|
-
* with their active Linear parent (for example promoted steering). General
|
|
4714
|
-
* task creation keeps tracker-context dedup enabled.
|
|
4715
|
-
*/
|
|
4716
|
-
bypassTrackerContextDedup?: boolean;
|
|
4717
|
-
/**
|
|
4718
|
-
* Routing-affinity snapshot gating pool eligibility (see
|
|
4719
|
-
* `isAgentEligibleForTask`). Inherited from the parent (via `parentTaskId`)
|
|
4720
|
-
* when not explicitly set — same treatment as `vcsRepo`/`contextKey`.
|
|
4721
|
-
*/
|
|
4722
|
-
routingAffinity?: RoutingAffinity;
|
|
4723
|
-
}
|
|
4880
|
+
// The runtime contract for task creation lives in src/types.ts
|
|
4881
|
+
// (`CreateTaskOptionsSchema`); `createTaskExtended` parses options against it.
|
|
4882
|
+
// The type is re-exported so existing importers (src/tasks/sibling-awareness.ts)
|
|
4883
|
+
// keep resolving it from this module.
|
|
4884
|
+
export type { CreateTaskOptions } from "../types";
|
|
4724
4885
|
|
|
4725
4886
|
/**
|
|
4726
4887
|
* Find recent tasks within a time window for deduplication checks.
|
|
@@ -4759,6 +4920,15 @@ export function findRecentSimilarTasks(opts: {
|
|
|
4759
4920
|
}
|
|
4760
4921
|
|
|
4761
4922
|
export function createTaskExtended(task: string, options?: CreateTaskOptions): AgentTask {
|
|
4923
|
+
if (typeof task !== "string" || task.trim().length === 0) {
|
|
4924
|
+
throw new Error("createTaskExtended: 'task' must be a non-empty string");
|
|
4925
|
+
}
|
|
4926
|
+
// Single runtime enforcement point for every task write (REST, MCP,
|
|
4927
|
+
// scripts bridge, webhooks, scheduler, internal callers). Reject, never
|
|
4928
|
+
// coerce: a bad shape throws before anything reaches the INSERT; absent
|
|
4929
|
+
// fields keep the `??` defaults at the bind site below.
|
|
4930
|
+
options = CreateTaskOptionsSchema.parse(options ?? {});
|
|
4931
|
+
let requestedByUserIdInherited = false;
|
|
4762
4932
|
const id = crypto.randomUUID();
|
|
4763
4933
|
const now = new Date().toISOString();
|
|
4764
4934
|
const status: AgentTaskStatus = options?.offeredTo
|
|
@@ -4894,6 +5064,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
4894
5064
|
// Attribution
|
|
4895
5065
|
if (parent.requestedByUserId && !options.requestedByUserId) {
|
|
4896
5066
|
options.requestedByUserId = parent.requestedByUserId;
|
|
5067
|
+
requestedByUserIdInherited = true;
|
|
4897
5068
|
}
|
|
4898
5069
|
if (parent.key && !options.key) {
|
|
4899
5070
|
options.key = parent.key;
|
|
@@ -5009,8 +5180,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5009
5180
|
vcsInstallationId, vcsNodeId,
|
|
5010
5181
|
agentmailInboxId, agentmailMessageId, agentmailThreadId,
|
|
5011
5182
|
mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, scheduleId,
|
|
5012
|
-
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
|
|
5013
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
5183
|
+
workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by
|
|
5184
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
5014
5185
|
)
|
|
5015
5186
|
.get(
|
|
5016
5187
|
id,
|
|
@@ -5055,6 +5226,7 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5055
5226
|
options?.outputSchema ? JSON.stringify(options.outputSchema) : null,
|
|
5056
5227
|
options?.followUpConfig ? JSON.stringify(options.followUpConfig) : null,
|
|
5057
5228
|
options?.requestedByUserId ?? null,
|
|
5229
|
+
requestedByUserIdInherited ? 1 : 0,
|
|
5058
5230
|
options?.contextKey ?? null,
|
|
5059
5231
|
options?.routingAffinity ? JSON.stringify(options.routingAffinity) : null,
|
|
5060
5232
|
pkg.version,
|
|
@@ -5088,8 +5260,8 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5088
5260
|
(task) => task !== null,
|
|
5089
5261
|
);
|
|
5090
5262
|
|
|
5091
|
-
|
|
5092
|
-
|
|
5263
|
+
import("../workflows/event-bus")
|
|
5264
|
+
.then(({ workflowEventBus }) => {
|
|
5093
5265
|
workflowEventBus.emit("task.created", {
|
|
5094
5266
|
taskId: row.id,
|
|
5095
5267
|
task: row.task,
|
|
@@ -5099,8 +5271,13 @@ export function createTaskExtended(task: string, options?: CreateTaskOptions): A
|
|
|
5099
5271
|
workflowRunId: row.workflowRunId,
|
|
5100
5272
|
workflowRunStepId: row.workflowRunStepId,
|
|
5101
5273
|
});
|
|
5102
|
-
})
|
|
5103
|
-
|
|
5274
|
+
})
|
|
5275
|
+
.catch((err) =>
|
|
5276
|
+
console.error(
|
|
5277
|
+
"[db] task.created event not emitted:",
|
|
5278
|
+
scrubSecrets(err instanceof Error ? err.message : String(err)),
|
|
5279
|
+
),
|
|
5280
|
+
);
|
|
5104
5281
|
|
|
5105
5282
|
return rowToAgentTask(row);
|
|
5106
5283
|
}
|
|
@@ -5330,6 +5507,41 @@ export function releaseStaleReviewingTasks(timeoutMinutes: number = 30): number
|
|
|
5330
5507
|
return result.changes;
|
|
5331
5508
|
}
|
|
5332
5509
|
|
|
5510
|
+
/**
|
|
5511
|
+
* Release 'offered' tasks whose offeree can no longer possibly poll for them
|
|
5512
|
+
* — the agent was deleted, or is `offline` (multi-runtime expiry once its
|
|
5513
|
+
* last runtime instance ages out, or an explicit graceful close). Returns
|
|
5514
|
+
* them to `unassigned` rather than back to `offered`: a stuck offer is
|
|
5515
|
+
* invisible to `autoAssignPoolTasks` (which only scans `unassigned`) and to
|
|
5516
|
+
* the poll-based accept/reject path (which only the original offeree can
|
|
5517
|
+
* reach), so `offered` is a dead end once the offeree is gone (#1190).
|
|
5518
|
+
*
|
|
5519
|
+
* Deliberately gated on liveness, not elapsed time: a fixed timeout would
|
|
5520
|
+
* either yank a live offer out from under an offeree that is simply slow to
|
|
5521
|
+
* poll, or (set generously) leave a genuinely dead offeree's task stranded
|
|
5522
|
+
* for the full window regardless of how obviously offline it already is.
|
|
5523
|
+
* `agents.status = 'offline'` already encodes the staleness threshold that
|
|
5524
|
+
* matters — `runtimeStaleThresholdMinutes()` / `RUNTIME_STALE_THRESHOLD_MIN`
|
|
5525
|
+
* for multi-runtime expiry, or immediate for an explicit close — so no
|
|
5526
|
+
* separate timeout knob is introduced here.
|
|
5527
|
+
*/
|
|
5528
|
+
export function releaseStaleOfferedTasksForOfflineAgents(): number {
|
|
5529
|
+
const now = new Date().toISOString();
|
|
5530
|
+
|
|
5531
|
+
const result = getDb().run(
|
|
5532
|
+
`UPDATE agent_tasks
|
|
5533
|
+
SET status = 'unassigned', offeredTo = NULL, offeredAt = NULL, lastUpdatedAt = ?
|
|
5534
|
+
WHERE status = 'offered'
|
|
5535
|
+
AND offeredTo IS NOT NULL
|
|
5536
|
+
AND NOT EXISTS (
|
|
5537
|
+
SELECT 1 FROM agents WHERE agents.id = agent_tasks.offeredTo AND agents.status != 'offline'
|
|
5538
|
+
)`,
|
|
5539
|
+
[now],
|
|
5540
|
+
);
|
|
5541
|
+
|
|
5542
|
+
return result.changes;
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5333
5545
|
export function getOfferedTasksForAgent(agentId: string): AgentTask[] {
|
|
5334
5546
|
return getDb()
|
|
5335
5547
|
.prepare<AgentTaskRow, [string]>(
|
|
@@ -5556,6 +5768,7 @@ export {
|
|
|
5556
5768
|
export function updateAgentProfile(
|
|
5557
5769
|
id: string,
|
|
5558
5770
|
updates: {
|
|
5771
|
+
name?: string;
|
|
5559
5772
|
description?: string;
|
|
5560
5773
|
role?: string;
|
|
5561
5774
|
capabilities?: string[];
|
|
@@ -5579,6 +5792,31 @@ export function updateAgentProfile(
|
|
|
5579
5792
|
.get(id);
|
|
5580
5793
|
if (!current) return null;
|
|
5581
5794
|
|
|
5795
|
+
for (const field of BUDGETED_IDENTITY_FIELDS) {
|
|
5796
|
+
const nextValue = updates[field];
|
|
5797
|
+
if (nextValue === undefined) continue;
|
|
5798
|
+
|
|
5799
|
+
const result = checkIdentityFieldBudget({
|
|
5800
|
+
field,
|
|
5801
|
+
currentValue: current[field] ?? "",
|
|
5802
|
+
nextValue,
|
|
5803
|
+
});
|
|
5804
|
+
if (!result.ok) {
|
|
5805
|
+
throw new IdentityFieldBudgetError(
|
|
5806
|
+
result,
|
|
5807
|
+
computeContentHash(current[field] ?? ""),
|
|
5808
|
+
computeContentHash(nextValue),
|
|
5809
|
+
);
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
if (updates.name !== undefined) {
|
|
5814
|
+
const existingAgent = database
|
|
5815
|
+
.prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
|
|
5816
|
+
.get(updates.name, id);
|
|
5817
|
+
if (existingAgent) throw new Error("Agent name already exists");
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5582
5820
|
// Create context versions for changed fields
|
|
5583
5821
|
for (const field of VERSIONABLE_FIELDS) {
|
|
5584
5822
|
const newValue = updates[field];
|
|
@@ -5631,6 +5869,7 @@ export function updateAgentProfile(
|
|
|
5631
5869
|
string | null,
|
|
5632
5870
|
string | null,
|
|
5633
5871
|
string | null,
|
|
5872
|
+
string | null,
|
|
5634
5873
|
number,
|
|
5635
5874
|
string | null,
|
|
5636
5875
|
string,
|
|
@@ -5638,6 +5877,7 @@ export function updateAgentProfile(
|
|
|
5638
5877
|
]
|
|
5639
5878
|
>(
|
|
5640
5879
|
`UPDATE agents SET
|
|
5880
|
+
name = COALESCE(?, name),
|
|
5641
5881
|
description = COALESCE(?, description),
|
|
5642
5882
|
role = COALESCE(?, role),
|
|
5643
5883
|
capabilities = COALESCE(?, capabilities),
|
|
@@ -5652,6 +5892,7 @@ export function updateAgentProfile(
|
|
|
5652
5892
|
WHERE id = ? RETURNING *`,
|
|
5653
5893
|
)
|
|
5654
5894
|
.get(
|
|
5895
|
+
updates.name ?? null,
|
|
5655
5896
|
updates.description ?? null,
|
|
5656
5897
|
updates.role ?? null,
|
|
5657
5898
|
updates.capabilities ? JSON.stringify(updates.capabilities) : null,
|
|
@@ -5672,23 +5913,7 @@ export function updateAgentProfile(
|
|
|
5672
5913
|
}
|
|
5673
5914
|
|
|
5674
5915
|
export function updateAgentName(id: string, newName: string): Agent | null {
|
|
5675
|
-
|
|
5676
|
-
const existingAgent = getDb()
|
|
5677
|
-
.prepare<AgentRow, [string, string]>("SELECT * FROM agents WHERE name = ? AND id != ?")
|
|
5678
|
-
.get(newName, id);
|
|
5679
|
-
|
|
5680
|
-
if (existingAgent) {
|
|
5681
|
-
throw new Error("Agent name already exists");
|
|
5682
|
-
}
|
|
5683
|
-
|
|
5684
|
-
const now = new Date().toISOString();
|
|
5685
|
-
const row = getDb()
|
|
5686
|
-
.prepare<AgentRow, [string, string, string]>(
|
|
5687
|
-
"UPDATE agents SET name = ?, lastUpdatedAt = ? WHERE id = ? RETURNING *",
|
|
5688
|
-
)
|
|
5689
|
-
.get(newName, now, id);
|
|
5690
|
-
|
|
5691
|
-
return row ? rowToAgent(row) : null;
|
|
5916
|
+
return updateAgentProfile(id, { name: newName });
|
|
5692
5917
|
}
|
|
5693
5918
|
|
|
5694
5919
|
// ============================================================================
|
|
@@ -6873,8 +7098,30 @@ export interface SessionCostSummaryTotals {
|
|
|
6873
7098
|
totalDurationMs: number;
|
|
6874
7099
|
totalSessions: number;
|
|
6875
7100
|
avgCostPerSession: number;
|
|
6876
|
-
/**
|
|
7101
|
+
/**
|
|
7102
|
+
* Cost of tasks with a human requester, over `attributableCostUsd` (not
|
|
7103
|
+
* `totalCostUsd`) — see `attributableCostUsd` for why. Also excludes cost on
|
|
7104
|
+
* a structurally-human-free task that happens to carry a stale/inherited
|
|
7105
|
+
* `requestedByUserId` (measured 2026-08-19: ~5% of the naive-attributed
|
|
7106
|
+
* population), so the two totals stay a consistent partition.
|
|
7107
|
+
*/
|
|
6877
7108
|
attributedCostUsd: number;
|
|
7109
|
+
/**
|
|
7110
|
+
* The corrected coverage denominator: `totalCostUsd` minus the cost of
|
|
7111
|
+
* structurally-human-free tasks (heartbeat/boot-triage, scheduled runs, and
|
|
7112
|
+
* `source='system'` follow-ups whose parent itself had no human requester —
|
|
7113
|
+
* i.e. follow-ups of the swarm's own self-maintenance, not of human work).
|
|
7114
|
+
* These tasks have no human requester *by construction*; stamping one on
|
|
7115
|
+
* them would be a lie, not coverage, so they don't belong in the
|
|
7116
|
+
* denominator at all. `attributedCostUsd / attributableCostUsd` is the
|
|
7117
|
+
* number to show as "coverage" — dividing by `totalCostUsd` instead
|
|
7118
|
+
* silently deflates it with a population that could never have scored.
|
|
7119
|
+
*/
|
|
7120
|
+
attributableCostUsd: number;
|
|
7121
|
+
/** Cost of the structurally-human-free population excluded from `attributableCostUsd`. */
|
|
7122
|
+
excludedCostUsd: number;
|
|
7123
|
+
/** Distinct tasks behind `excludedCostUsd` — surfaced so the UI can name the exclusion count, not just wave at a percentage. */
|
|
7124
|
+
excludedTaskCount: number;
|
|
6878
7125
|
}
|
|
6879
7126
|
|
|
6880
7127
|
export interface SessionCostDailyRow {
|
|
@@ -6907,6 +7154,60 @@ export interface SessionCostByUserRow {
|
|
|
6907
7154
|
/** `opts.userId` sentinel selecting spend with no human requester. */
|
|
6908
7155
|
export const UNATTRIBUTED_USER_ID = "unattributed";
|
|
6909
7156
|
|
|
7157
|
+
// Structurally-human-free tasks and their descendants. An explicitly supplied
|
|
7158
|
+
// requester is a human handoff and stops propagation; a requester copied from
|
|
7159
|
+
// the parent does not.
|
|
7160
|
+
const HUMAN_FREE_TASKS_CTE = `human_free_tasks(id) AS (
|
|
7161
|
+
SELECT task.id
|
|
7162
|
+
FROM agent_tasks task
|
|
7163
|
+
LEFT JOIN agent_tasks parent ON parent.id = task.parentTaskId
|
|
7164
|
+
WHERE COALESCE(task.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
|
|
7165
|
+
OR COALESCE(task.tags, '[]') LIKE '%"heartbeat"%'
|
|
7166
|
+
OR (COALESCE(task.source, '') = 'schedule' AND task.requestedByUserId IS NULL)
|
|
7167
|
+
OR (
|
|
7168
|
+
task.parentTaskId IS NULL
|
|
7169
|
+
AND COALESCE(task.source, '') = 'workflow'
|
|
7170
|
+
AND task.requestedByUserId IS NULL
|
|
7171
|
+
AND EXISTS (
|
|
7172
|
+
SELECT 1
|
|
7173
|
+
FROM workflow_runs run
|
|
7174
|
+
WHERE run.id = task.workflowRunId
|
|
7175
|
+
AND run.triggerType = 'schedule'
|
|
7176
|
+
AND run.created_by IS NULL
|
|
7177
|
+
)
|
|
7178
|
+
)
|
|
7179
|
+
OR (
|
|
7180
|
+
COALESCE(task.source, '') = 'system'
|
|
7181
|
+
AND parent.id IS NOT NULL
|
|
7182
|
+
AND parent.requestedByUserId IS NULL
|
|
7183
|
+
)
|
|
7184
|
+
|
|
7185
|
+
UNION
|
|
7186
|
+
|
|
7187
|
+
SELECT child.id
|
|
7188
|
+
FROM agent_tasks child
|
|
7189
|
+
JOIN human_free_tasks parent ON child.parentTaskId = parent.id
|
|
7190
|
+
WHERE child.requestedByUserId IS NULL
|
|
7191
|
+
OR child.requestedByUserIdInherited = 1
|
|
7192
|
+
)`;
|
|
7193
|
+
const HUMAN_FREE_SQL = "EXISTS (SELECT 1 FROM human_free_tasks WHERE id = t.id)";
|
|
7194
|
+
const ROOT_HUMAN_FREE_SQL = `(
|
|
7195
|
+
COALESCE(t.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
|
|
7196
|
+
OR COALESCE(t.tags, '[]') LIKE '%"heartbeat"%'
|
|
7197
|
+
OR (COALESCE(t.source, '') = 'schedule' AND t.requestedByUserId IS NULL)
|
|
7198
|
+
OR (
|
|
7199
|
+
COALESCE(t.source, '') = 'workflow'
|
|
7200
|
+
AND t.requestedByUserId IS NULL
|
|
7201
|
+
AND EXISTS (
|
|
7202
|
+
SELECT 1
|
|
7203
|
+
FROM workflow_runs run
|
|
7204
|
+
WHERE run.id = t.workflowRunId
|
|
7205
|
+
AND run.triggerType = 'schedule'
|
|
7206
|
+
AND run.created_by IS NULL
|
|
7207
|
+
)
|
|
7208
|
+
)
|
|
7209
|
+
)`;
|
|
7210
|
+
|
|
6910
7211
|
export function getSessionCostSummary(opts: {
|
|
6911
7212
|
startDate?: string;
|
|
6912
7213
|
endDate?: string;
|
|
@@ -6925,6 +7226,15 @@ export function getSessionCostSummary(opts: {
|
|
|
6925
7226
|
// through the task (same shape as `getDailySpendForUser`). Every column is
|
|
6926
7227
|
// `sc.`-qualified because `createdAt`/`agentId` exist on both sides.
|
|
6927
7228
|
const from = "FROM session_costs sc LEFT JOIN agent_tasks t ON t.id = sc.taskId";
|
|
7229
|
+
|
|
7230
|
+
// Structurally-human-free: the swarm maintaining itself, with no human
|
|
7231
|
+
// requester by construction — heartbeat/boot-triage tasks, scheduled runs
|
|
7232
|
+
// without a human creator (including workflow roots launched by such a
|
|
7233
|
+
// schedule), and `source='system'` follow-ups whose parent itself has no
|
|
7234
|
+
// human requester. That classification propagates through descendants while
|
|
7235
|
+
// they remain unattributed, so autonomous fan-out cannot leak back into the
|
|
7236
|
+
// denominator. An explicitly attributed child is an independent handoff and
|
|
7237
|
+
// stops propagation down that branch.
|
|
6928
7238
|
const conditions: string[] = [];
|
|
6929
7239
|
const params: string[] = [];
|
|
6930
7240
|
|
|
@@ -6941,9 +7251,9 @@ export function getSessionCostSummary(opts: {
|
|
|
6941
7251
|
params.push(opts.agentId);
|
|
6942
7252
|
}
|
|
6943
7253
|
if (opts.userId === UNATTRIBUTED_USER_ID) {
|
|
6944
|
-
conditions.push(
|
|
7254
|
+
conditions.push(`(t.requestedByUserId IS NULL OR ${HUMAN_FREE_SQL})`);
|
|
6945
7255
|
} else if (opts.userId) {
|
|
6946
|
-
conditions.push(
|
|
7256
|
+
conditions.push(`(t.requestedByUserId = ? AND NOT ${HUMAN_FREE_SQL})`);
|
|
6947
7257
|
params.push(opts.userId);
|
|
6948
7258
|
}
|
|
6949
7259
|
|
|
@@ -6959,11 +7269,15 @@ export function getSessionCostSummary(opts: {
|
|
|
6959
7269
|
totalDurationMs: number;
|
|
6960
7270
|
totalSessions: number;
|
|
6961
7271
|
attributedCostUsd: number;
|
|
7272
|
+
attributableCostUsd: number;
|
|
7273
|
+
excludedCostUsd: number;
|
|
7274
|
+
excludedTaskCount: number;
|
|
6962
7275
|
};
|
|
6963
7276
|
|
|
6964
7277
|
const totalsRow = getDb()
|
|
6965
7278
|
.prepare<TotalsRow, string[]>(
|
|
6966
|
-
`
|
|
7279
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7280
|
+
SELECT
|
|
6967
7281
|
COALESCE(SUM(sc.totalCostUsd), 0) as totalCostUsd,
|
|
6968
7282
|
COALESCE(SUM(sc.inputTokens), 0) as totalInputTokens,
|
|
6969
7283
|
COALESCE(SUM(sc.outputTokens), 0) as totalOutputTokens,
|
|
@@ -6971,8 +7285,13 @@ export function getSessionCostSummary(opts: {
|
|
|
6971
7285
|
COALESCE(SUM(sc.cacheWriteTokens), 0) as totalCacheWriteTokens,
|
|
6972
7286
|
COALESCE(SUM(sc.durationMs), 0) as totalDurationMs,
|
|
6973
7287
|
COUNT(*) as totalSessions,
|
|
6974
|
-
COALESCE(SUM(CASE WHEN t.requestedByUserId IS NOT NULL
|
|
6975
|
-
as attributedCostUsd
|
|
7288
|
+
COALESCE(SUM(CASE WHEN t.requestedByUserId IS NOT NULL AND NOT ${HUMAN_FREE_SQL}
|
|
7289
|
+
THEN sc.totalCostUsd ELSE 0 END), 0) as attributedCostUsd,
|
|
7290
|
+
COALESCE(SUM(CASE WHEN NOT ${HUMAN_FREE_SQL}
|
|
7291
|
+
THEN sc.totalCostUsd ELSE 0 END), 0) as attributableCostUsd,
|
|
7292
|
+
COALESCE(SUM(CASE WHEN ${HUMAN_FREE_SQL}
|
|
7293
|
+
THEN sc.totalCostUsd ELSE 0 END), 0) as excludedCostUsd,
|
|
7294
|
+
COUNT(DISTINCT CASE WHEN ${HUMAN_FREE_SQL} THEN t.id END) as excludedTaskCount
|
|
6976
7295
|
${from} ${where}`,
|
|
6977
7296
|
)
|
|
6978
7297
|
.get(...params);
|
|
@@ -6993,6 +7312,9 @@ export function getSessionCostSummary(opts: {
|
|
|
6993
7312
|
totalSessions: 0,
|
|
6994
7313
|
avgCostPerSession: 0,
|
|
6995
7314
|
attributedCostUsd: 0,
|
|
7315
|
+
attributableCostUsd: 0,
|
|
7316
|
+
excludedCostUsd: 0,
|
|
7317
|
+
excludedTaskCount: 0,
|
|
6996
7318
|
};
|
|
6997
7319
|
|
|
6998
7320
|
// Daily breakdown
|
|
@@ -7010,7 +7332,8 @@ export function getSessionCostSummary(opts: {
|
|
|
7010
7332
|
},
|
|
7011
7333
|
string[]
|
|
7012
7334
|
>(
|
|
7013
|
-
`
|
|
7335
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7336
|
+
SELECT
|
|
7014
7337
|
DATE(sc.createdAt) as date,
|
|
7015
7338
|
COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
|
|
7016
7339
|
COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
|
|
@@ -7038,7 +7361,8 @@ export function getSessionCostSummary(opts: {
|
|
|
7038
7361
|
},
|
|
7039
7362
|
string[]
|
|
7040
7363
|
>(
|
|
7041
|
-
`
|
|
7364
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7365
|
+
SELECT
|
|
7042
7366
|
sc.agentId as agentId,
|
|
7043
7367
|
COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
|
|
7044
7368
|
COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
|
|
@@ -7052,22 +7376,22 @@ export function getSessionCostSummary(opts: {
|
|
|
7052
7376
|
.all(...params);
|
|
7053
7377
|
}
|
|
7054
7378
|
|
|
7055
|
-
// Per-requester breakdown.
|
|
7056
|
-
//
|
|
7057
|
-
// must stay visible rather than being folded into a person.
|
|
7379
|
+
// Per-requester breakdown. Structurally-human-free work is assigned to the
|
|
7380
|
+
// autonomous bucket even if a stale requester id survives on the task.
|
|
7058
7381
|
let byUser: SessionCostByUserRow[] = [];
|
|
7059
7382
|
if (groupBy === "user" || groupBy === "both") {
|
|
7060
7383
|
byUser = getDb()
|
|
7061
7384
|
.prepare<SessionCostByUserRow, string[]>(
|
|
7062
|
-
`
|
|
7063
|
-
|
|
7385
|
+
`WITH RECURSIVE ${HUMAN_FREE_TASKS_CTE}
|
|
7386
|
+
SELECT
|
|
7387
|
+
CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END as userId,
|
|
7064
7388
|
COALESCE(SUM(sc.totalCostUsd), 0) as costUsd,
|
|
7065
7389
|
COALESCE(SUM(sc.inputTokens), 0) as inputTokens,
|
|
7066
7390
|
COALESCE(SUM(sc.outputTokens), 0) as outputTokens,
|
|
7067
7391
|
COUNT(DISTINCT sc.taskId) as tasks,
|
|
7068
7392
|
COALESCE(SUM(sc.durationMs), 0) as durationMs
|
|
7069
7393
|
${from} ${where}
|
|
7070
|
-
GROUP BY t.requestedByUserId
|
|
7394
|
+
GROUP BY CASE WHEN ${HUMAN_FREE_SQL} THEN NULL ELSE t.requestedByUserId END
|
|
7071
7395
|
ORDER BY costUsd DESC`,
|
|
7072
7396
|
)
|
|
7073
7397
|
.all(...params);
|
|
@@ -7076,6 +7400,235 @@ export function getSessionCostSummary(opts: {
|
|
|
7076
7400
|
return { totals, daily, byAgent, byUser };
|
|
7077
7401
|
}
|
|
7078
7402
|
|
|
7403
|
+
// --- Per-person attribution (four-metric view) ---
|
|
7404
|
+
|
|
7405
|
+
export interface AttributionByPersonRow {
|
|
7406
|
+
userId: string;
|
|
7407
|
+
/** Root tasks (`parentTaskId IS NULL`) only — counting fan-out children would let
|
|
7408
|
+
* whoever triggers the biggest decomposition win by accident. */
|
|
7409
|
+
problemsInitiated: number;
|
|
7410
|
+
/** Roots that completed AND show shippable evidence — see `getAttributionByPerson` doc. */
|
|
7411
|
+
problemsShipped: number;
|
|
7412
|
+
/** Distinct agents engaged across the person's entire task tree (not root-scoped —
|
|
7413
|
+
* a root that fans out to nine agents is nine agents of reach). */
|
|
7414
|
+
agentsReached: number;
|
|
7415
|
+
reposReached: number;
|
|
7416
|
+
surfacesReached: number;
|
|
7417
|
+
/**
|
|
7418
|
+
* Share of root tasks completed without a re-dispatch or a human correction.
|
|
7419
|
+
* Deliberately `null`: distinguishing a legitimate multi-agent fan-out from a
|
|
7420
|
+
* correction-driven re-dispatch isn't computable from a single query pass
|
|
7421
|
+
* (would need to inspect *why* a child task was created, which isn't a stored
|
|
7422
|
+
* fact), and a task-count-based proxy would misclassify normal delegation as
|
|
7423
|
+
* rework. Render as "not yet computed" — do not invent a stand-in.
|
|
7424
|
+
*/
|
|
7425
|
+
firstPassYield: null;
|
|
7426
|
+
}
|
|
7427
|
+
|
|
7428
|
+
/**
|
|
7429
|
+
* Four metrics per human requester, reported side by side — never summed into
|
|
7430
|
+
* a composite score. Scope is root tasks (`problemsInitiated`/`problemsShipped`)
|
|
7431
|
+
* or the person's entire task tree (`*Reached`); `requestedByUserId IS NULL`
|
|
7432
|
+
* (autonomous work) and structurally-human-free rows with a stale/inherited
|
|
7433
|
+
* requester (see `HUMAN_FREE_SQL` in `getSessionCostSummary`) are excluded —
|
|
7434
|
+
* neither belongs to a person.
|
|
7435
|
+
*
|
|
7436
|
+
* "Problems shipped" detection walks each root's full task tree, preferring a
|
|
7437
|
+
* `task_attachments` row (`kind='url'` matching a GitHub PR or GitLab MR URL,
|
|
7438
|
+
* or `kind='page'` for a published artifact) and falling back to the same
|
|
7439
|
+
* provider-specific URL match on any task output. GitLab matching is host
|
|
7440
|
+
* agnostic so self-hosted instances are covered. This does NOT detect a closed ticket (no
|
|
7441
|
+
* Linear/Jira issue-state table exists locally) — "shipped" undercounts
|
|
7442
|
+
* ticket-only outcomes until the real artifacts join lands.
|
|
7443
|
+
*/
|
|
7444
|
+
export function getAttributionByPerson(opts: {
|
|
7445
|
+
startDate?: string;
|
|
7446
|
+
endDate?: string;
|
|
7447
|
+
}): AttributionByPersonRow[] {
|
|
7448
|
+
const conditions: string[] = ["t.requestedByUserId IS NOT NULL"];
|
|
7449
|
+
const params: string[] = [];
|
|
7450
|
+
if (opts.startDate) {
|
|
7451
|
+
conditions.push("t.createdAt >= ?");
|
|
7452
|
+
params.push(opts.startDate);
|
|
7453
|
+
}
|
|
7454
|
+
if (opts.endDate) {
|
|
7455
|
+
// A date-only value represents the whole UTC day. Preserve inclusive
|
|
7456
|
+
// timestamp semantics for callers that provide an exact instant.
|
|
7457
|
+
const dateOnlyEnd = /^\d{4}-\d{2}-\d{2}$/.test(opts.endDate)
|
|
7458
|
+
? new Date(`${opts.endDate}T00:00:00.000Z`)
|
|
7459
|
+
: null;
|
|
7460
|
+
if (
|
|
7461
|
+
dateOnlyEnd &&
|
|
7462
|
+
!Number.isNaN(dateOnlyEnd.getTime()) &&
|
|
7463
|
+
dateOnlyEnd.toISOString().slice(0, 10) === opts.endDate
|
|
7464
|
+
) {
|
|
7465
|
+
dateOnlyEnd.setUTCDate(dateOnlyEnd.getUTCDate() + 1);
|
|
7466
|
+
conditions.push("t.createdAt < ?");
|
|
7467
|
+
params.push(dateOnlyEnd.toISOString());
|
|
7468
|
+
} else {
|
|
7469
|
+
conditions.push("t.createdAt <= ?");
|
|
7470
|
+
params.push(opts.endDate);
|
|
7471
|
+
}
|
|
7472
|
+
}
|
|
7473
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
7474
|
+
|
|
7475
|
+
type RootRow = { userId: string; initiated: number; shipped: number };
|
|
7476
|
+
const rootRows = getDb()
|
|
7477
|
+
.prepare<RootRow, string[]>(
|
|
7478
|
+
`WITH RECURSIVE selected_roots(id, requestedByUserId, status, output) AS (
|
|
7479
|
+
SELECT t.id, t.requestedByUserId, t.status, t.output
|
|
7480
|
+
FROM agent_tasks t
|
|
7481
|
+
${where} AND t.parentTaskId IS NULL AND NOT ${ROOT_HUMAN_FREE_SQL}
|
|
7482
|
+
),
|
|
7483
|
+
task_tree(rootId, taskId, output) AS (
|
|
7484
|
+
SELECT id, id, output
|
|
7485
|
+
FROM selected_roots
|
|
7486
|
+
|
|
7487
|
+
UNION ALL
|
|
7488
|
+
|
|
7489
|
+
SELECT tree.rootId, child.id, child.output
|
|
7490
|
+
FROM agent_tasks child
|
|
7491
|
+
JOIN task_tree tree ON child.parentTaskId = tree.taskId
|
|
7492
|
+
)
|
|
7493
|
+
SELECT
|
|
7494
|
+
t.requestedByUserId as userId,
|
|
7495
|
+
COUNT(*) as initiated,
|
|
7496
|
+
SUM(CASE WHEN t.status = 'completed' AND (
|
|
7497
|
+
EXISTS (
|
|
7498
|
+
SELECT 1
|
|
7499
|
+
FROM task_tree tree
|
|
7500
|
+
JOIN task_attachments ta ON ta.task_id = tree.taskId
|
|
7501
|
+
WHERE tree.rootId = t.id
|
|
7502
|
+
AND ta.kind = 'url'
|
|
7503
|
+
AND (
|
|
7504
|
+
ta.url LIKE '%github.com/%/pull/%'
|
|
7505
|
+
OR ta.url LIKE '%/-/merge_requests/%'
|
|
7506
|
+
)
|
|
7507
|
+
)
|
|
7508
|
+
OR EXISTS (
|
|
7509
|
+
SELECT 1 FROM task_attachments ta
|
|
7510
|
+
JOIN task_tree tree ON tree.taskId = ta.task_id
|
|
7511
|
+
WHERE tree.rootId = t.id AND ta.kind = 'page'
|
|
7512
|
+
)
|
|
7513
|
+
OR EXISTS (
|
|
7514
|
+
SELECT 1 FROM task_tree tree
|
|
7515
|
+
WHERE tree.rootId = t.id
|
|
7516
|
+
AND (
|
|
7517
|
+
tree.output LIKE '%github.com/%/pull/%'
|
|
7518
|
+
OR tree.output LIKE '%/-/merge_requests/%'
|
|
7519
|
+
)
|
|
7520
|
+
)
|
|
7521
|
+
) THEN 1 ELSE 0 END) as shipped
|
|
7522
|
+
FROM selected_roots t
|
|
7523
|
+
GROUP BY t.requestedByUserId`,
|
|
7524
|
+
)
|
|
7525
|
+
.all(...params);
|
|
7526
|
+
|
|
7527
|
+
type ReachRow = {
|
|
7528
|
+
userId: string;
|
|
7529
|
+
agentsReached: number;
|
|
7530
|
+
reposReached: number;
|
|
7531
|
+
surfacesReached: number;
|
|
7532
|
+
};
|
|
7533
|
+
const reachRows = getDb()
|
|
7534
|
+
.prepare<ReachRow, string[]>(
|
|
7535
|
+
`WITH RECURSIVE report_tasks AS (
|
|
7536
|
+
SELECT t.*
|
|
7537
|
+
FROM agent_tasks t
|
|
7538
|
+
${where}
|
|
7539
|
+
),
|
|
7540
|
+
task_ancestry(
|
|
7541
|
+
taskId, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
|
|
7542
|
+
taskType, tags, source, workflowRunId
|
|
7543
|
+
) AS (
|
|
7544
|
+
SELECT
|
|
7545
|
+
id, id, parentTaskId, requestedByUserId, requestedByUserIdInherited,
|
|
7546
|
+
taskType, tags, source, workflowRunId
|
|
7547
|
+
FROM report_tasks
|
|
7548
|
+
|
|
7549
|
+
UNION ALL
|
|
7550
|
+
|
|
7551
|
+
SELECT
|
|
7552
|
+
child.taskId, parent.id, parent.parentTaskId, parent.requestedByUserId,
|
|
7553
|
+
parent.requestedByUserIdInherited, parent.taskType, parent.tags,
|
|
7554
|
+
parent.source, parent.workflowRunId
|
|
7555
|
+
FROM agent_tasks parent
|
|
7556
|
+
JOIN task_ancestry child ON parent.id = child.parentTaskId
|
|
7557
|
+
WHERE child.requestedByUserId IS NULL
|
|
7558
|
+
OR child.requestedByUserIdInherited = 1
|
|
7559
|
+
),
|
|
7560
|
+
human_free_report_tasks(id) AS (
|
|
7561
|
+
SELECT DISTINCT ancestor.taskId
|
|
7562
|
+
FROM task_ancestry ancestor
|
|
7563
|
+
LEFT JOIN agent_tasks parent ON parent.id = ancestor.parentTaskId
|
|
7564
|
+
WHERE COALESCE(ancestor.taskType, '') IN ('heartbeat', 'heartbeat-checklist', 'boot-triage')
|
|
7565
|
+
OR COALESCE(ancestor.tags, '[]') LIKE '%"heartbeat"%'
|
|
7566
|
+
OR (COALESCE(ancestor.source, '') = 'schedule' AND ancestor.requestedByUserId IS NULL)
|
|
7567
|
+
OR (
|
|
7568
|
+
ancestor.parentTaskId IS NULL
|
|
7569
|
+
AND COALESCE(ancestor.source, '') = 'workflow'
|
|
7570
|
+
AND ancestor.requestedByUserId IS NULL
|
|
7571
|
+
AND EXISTS (
|
|
7572
|
+
SELECT 1
|
|
7573
|
+
FROM workflow_runs run
|
|
7574
|
+
WHERE run.id = ancestor.workflowRunId
|
|
7575
|
+
AND run.triggerType = 'schedule'
|
|
7576
|
+
AND run.created_by IS NULL
|
|
7577
|
+
)
|
|
7578
|
+
)
|
|
7579
|
+
OR (
|
|
7580
|
+
COALESCE(ancestor.source, '') = 'system'
|
|
7581
|
+
AND parent.id IS NOT NULL
|
|
7582
|
+
AND parent.requestedByUserId IS NULL
|
|
7583
|
+
)
|
|
7584
|
+
)
|
|
7585
|
+
SELECT
|
|
7586
|
+
t.requestedByUserId as userId,
|
|
7587
|
+
COUNT(DISTINCT t.agentId) as agentsReached,
|
|
7588
|
+
COUNT(DISTINCT t.vcsRepo) as reposReached,
|
|
7589
|
+
COUNT(DISTINCT t.source) as surfacesReached
|
|
7590
|
+
FROM report_tasks t
|
|
7591
|
+
WHERE NOT EXISTS (SELECT 1 FROM human_free_report_tasks WHERE id = t.id)
|
|
7592
|
+
GROUP BY t.requestedByUserId`,
|
|
7593
|
+
)
|
|
7594
|
+
.all(...params);
|
|
7595
|
+
|
|
7596
|
+
const byUser = new Map<string, AttributionByPersonRow>();
|
|
7597
|
+
for (const r of rootRows) {
|
|
7598
|
+
byUser.set(r.userId, {
|
|
7599
|
+
userId: r.userId,
|
|
7600
|
+
problemsInitiated: r.initiated,
|
|
7601
|
+
problemsShipped: r.shipped,
|
|
7602
|
+
agentsReached: 0,
|
|
7603
|
+
reposReached: 0,
|
|
7604
|
+
surfacesReached: 0,
|
|
7605
|
+
firstPassYield: null,
|
|
7606
|
+
});
|
|
7607
|
+
}
|
|
7608
|
+
for (const r of reachRows) {
|
|
7609
|
+
const existing = byUser.get(r.userId);
|
|
7610
|
+
if (existing) {
|
|
7611
|
+
existing.agentsReached = r.agentsReached;
|
|
7612
|
+
existing.reposReached = r.reposReached;
|
|
7613
|
+
existing.surfacesReached = r.surfacesReached;
|
|
7614
|
+
} else {
|
|
7615
|
+
// Reach with no root task in-window (e.g. only fan-out children whose
|
|
7616
|
+
// root predates the window) — still a real person row.
|
|
7617
|
+
byUser.set(r.userId, {
|
|
7618
|
+
userId: r.userId,
|
|
7619
|
+
problemsInitiated: 0,
|
|
7620
|
+
problemsShipped: 0,
|
|
7621
|
+
agentsReached: r.agentsReached,
|
|
7622
|
+
reposReached: r.reposReached,
|
|
7623
|
+
surfacesReached: r.surfacesReached,
|
|
7624
|
+
firstPassYield: null,
|
|
7625
|
+
});
|
|
7626
|
+
}
|
|
7627
|
+
}
|
|
7628
|
+
|
|
7629
|
+
return Array.from(byUser.values());
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7079
7632
|
// --- Dashboard cost summary (P4) ---
|
|
7080
7633
|
|
|
7081
7634
|
export interface DashboardCostSummary {
|
|
@@ -8224,6 +8777,10 @@ export function upsertSwarmConfig(data: {
|
|
|
8224
8777
|
}
|
|
8225
8778
|
}
|
|
8226
8779
|
|
|
8780
|
+
if (config.isSecret) {
|
|
8781
|
+
registerVolatileSecret(config.value, `config:${config.key}`);
|
|
8782
|
+
}
|
|
8783
|
+
|
|
8227
8784
|
return config;
|
|
8228
8785
|
}
|
|
8229
8786
|
|
|
@@ -8565,6 +9122,7 @@ export function insertActiveSession(session: {
|
|
|
8565
9122
|
inboxMessageId?: string;
|
|
8566
9123
|
taskDescription?: string;
|
|
8567
9124
|
runnerSessionId?: string;
|
|
9125
|
+
runtimeInstanceId?: string;
|
|
8568
9126
|
}): ActiveSession {
|
|
8569
9127
|
const id = crypto.randomUUID();
|
|
8570
9128
|
const now = new Date().toISOString();
|
|
@@ -8580,12 +9138,13 @@ export function insertActiveSession(session: {
|
|
|
8580
9138
|
string | null,
|
|
8581
9139
|
string | null,
|
|
8582
9140
|
string | null,
|
|
9141
|
+
string | null,
|
|
8583
9142
|
string,
|
|
8584
9143
|
string,
|
|
8585
9144
|
]
|
|
8586
9145
|
>(
|
|
8587
|
-
`INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, runnerSessionId, startedAt, lastHeartbeatAt)
|
|
8588
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9146
|
+
`INSERT INTO active_sessions (id, agentId, taskId, triggerType, inboxMessageId, taskDescription, runnerSessionId, runtimeInstanceId, startedAt, lastHeartbeatAt)
|
|
9147
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8589
9148
|
RETURNING *`,
|
|
8590
9149
|
)
|
|
8591
9150
|
.get(
|
|
@@ -8596,6 +9155,7 @@ export function insertActiveSession(session: {
|
|
|
8596
9155
|
session.inboxMessageId ?? null,
|
|
8597
9156
|
session.taskDescription ?? null,
|
|
8598
9157
|
session.runnerSessionId ?? null,
|
|
9158
|
+
session.runtimeInstanceId ?? null,
|
|
8599
9159
|
now,
|
|
8600
9160
|
now,
|
|
8601
9161
|
);
|
|
@@ -9208,9 +9768,11 @@ type WorkflowRunRow = {
|
|
|
9208
9768
|
id: string;
|
|
9209
9769
|
workflowId: string;
|
|
9210
9770
|
status: string;
|
|
9771
|
+
triggerType: string;
|
|
9211
9772
|
triggerData: string | null;
|
|
9212
9773
|
context: string | null;
|
|
9213
9774
|
error: string | null;
|
|
9775
|
+
created_by: string | null;
|
|
9214
9776
|
startedAt: string;
|
|
9215
9777
|
lastUpdatedAt: string;
|
|
9216
9778
|
finishedAt: string | null;
|
|
@@ -9224,6 +9786,7 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
|
|
|
9224
9786
|
triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
|
|
9225
9787
|
context: row.context ? (JSON.parse(row.context) as Record<string, unknown>) : undefined,
|
|
9226
9788
|
error: row.error ?? undefined,
|
|
9789
|
+
createdBy: row.created_by ?? undefined,
|
|
9227
9790
|
startedAt: normalizeDateRequired(row.startedAt),
|
|
9228
9791
|
lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
|
|
9229
9792
|
finishedAt: normalizeDate(row.finishedAt) ?? undefined,
|
|
@@ -9233,14 +9796,23 @@ function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun {
|
|
|
9233
9796
|
export function createWorkflowRun(data: {
|
|
9234
9797
|
id: string;
|
|
9235
9798
|
workflowId: string;
|
|
9799
|
+
triggerType?: "schedule" | "manual" | "event" | "api";
|
|
9236
9800
|
triggerData?: unknown;
|
|
9801
|
+
createdBy?: string;
|
|
9237
9802
|
}): WorkflowRun {
|
|
9238
9803
|
const now = new Date().toISOString();
|
|
9239
9804
|
const row = getDb()
|
|
9240
|
-
.prepare<WorkflowRunRow, [string, string, string, string | null]>(
|
|
9241
|
-
`INSERT INTO workflow_runs (id, workflowId, startedAt, triggerData) VALUES (?, ?, ?, ?) RETURNING *`,
|
|
9805
|
+
.prepare<WorkflowRunRow, [string, string, string, string, string | null, string | null]>(
|
|
9806
|
+
`INSERT INTO workflow_runs (id, workflowId, triggerType, startedAt, triggerData, created_by) VALUES (?, ?, ?, ?, ?, ?) RETURNING *`,
|
|
9242
9807
|
)
|
|
9243
|
-
.get(
|
|
9808
|
+
.get(
|
|
9809
|
+
data.id,
|
|
9810
|
+
data.workflowId,
|
|
9811
|
+
data.triggerType ?? "manual",
|
|
9812
|
+
now,
|
|
9813
|
+
data.triggerData ? JSON.stringify(data.triggerData) : null,
|
|
9814
|
+
data.createdBy ?? null,
|
|
9815
|
+
);
|
|
9244
9816
|
if (!row) throw new Error("Failed to create workflow run");
|
|
9245
9817
|
return rowToWorkflowRun(row);
|
|
9246
9818
|
}
|
|
@@ -13161,13 +13733,104 @@ export function updateUser(
|
|
|
13161
13733
|
return row ? rowToUser(row) : null;
|
|
13162
13734
|
}
|
|
13163
13735
|
|
|
13164
|
-
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
return
|
|
13736
|
+
type UserReferenceRow = {
|
|
13737
|
+
tableName: string;
|
|
13738
|
+
columnName: string;
|
|
13739
|
+
};
|
|
13740
|
+
|
|
13741
|
+
function quoteSqlIdentifier(identifier: string): string {
|
|
13742
|
+
return `"${identifier.replaceAll('"', '""')}"`;
|
|
13743
|
+
}
|
|
13744
|
+
|
|
13745
|
+
export function deleteUser(id: string, replacementUserId?: string): boolean {
|
|
13746
|
+
if (replacementUserId === id) {
|
|
13747
|
+
throw new Error("Replacement user must differ from deleted user");
|
|
13748
|
+
}
|
|
13749
|
+
|
|
13750
|
+
const database = getDb();
|
|
13751
|
+
return database.transaction(() => {
|
|
13752
|
+
const userExists = database
|
|
13753
|
+
.prepare<{ present: number }, [string]>(
|
|
13754
|
+
"SELECT EXISTS(SELECT 1 FROM users WHERE id = ?) AS present",
|
|
13755
|
+
)
|
|
13756
|
+
.get(id)?.present;
|
|
13757
|
+
if (!userExists) return false;
|
|
13758
|
+
|
|
13759
|
+
if (replacementUserId) {
|
|
13760
|
+
const replacementExists = database
|
|
13761
|
+
.prepare<{ present: number }, [string]>(
|
|
13762
|
+
"SELECT EXISTS(SELECT 1 FROM users WHERE id = ?) AS present",
|
|
13763
|
+
)
|
|
13764
|
+
.get(replacementUserId)?.present;
|
|
13765
|
+
if (!replacementExists) throw new Error("Replacement user not found");
|
|
13766
|
+
}
|
|
13767
|
+
|
|
13768
|
+
// Preserve rows that carry user attribution but lack ON DELETE semantics.
|
|
13769
|
+
// Schema discovery keeps this correct as new nullable user audit columns are added.
|
|
13770
|
+
// Migration 103 accidentally dropped the scheduled_tasks audit FKs while
|
|
13771
|
+
// recreating that table, so discover those two known logical references as
|
|
13772
|
+
// well until the table is next rebuilt with its original constraints.
|
|
13773
|
+
const references = database
|
|
13774
|
+
.prepare<UserReferenceRow, []>(
|
|
13775
|
+
`SELECT tables.name AS tableName, foreign_keys."from" AS columnName
|
|
13776
|
+
FROM sqlite_schema AS tables
|
|
13777
|
+
JOIN pragma_foreign_key_list(tables.name) AS foreign_keys
|
|
13778
|
+
JOIN pragma_table_info(tables.name) AS columns
|
|
13779
|
+
ON columns.name = foreign_keys."from"
|
|
13780
|
+
WHERE tables.type = 'table'
|
|
13781
|
+
AND foreign_keys."table" = 'users'
|
|
13782
|
+
AND foreign_keys.on_delete IN ('NO ACTION', 'RESTRICT')
|
|
13783
|
+
AND columns."notnull" = 0
|
|
13784
|
+
UNION ALL
|
|
13785
|
+
SELECT 'scheduled_tasks' AS tableName, columns.name AS columnName
|
|
13786
|
+
FROM pragma_table_info('scheduled_tasks') AS columns
|
|
13787
|
+
WHERE columns.name IN ('created_by', 'updated_by')
|
|
13788
|
+
AND columns."notnull" = 0
|
|
13789
|
+
AND NOT EXISTS (
|
|
13790
|
+
SELECT 1
|
|
13791
|
+
FROM pragma_foreign_key_list('scheduled_tasks') AS foreign_keys
|
|
13792
|
+
WHERE foreign_keys."from" = columns.name
|
|
13793
|
+
AND foreign_keys."table" = 'users'
|
|
13794
|
+
)`,
|
|
13795
|
+
)
|
|
13796
|
+
.all();
|
|
13797
|
+
const replacement = replacementUserId ?? null;
|
|
13798
|
+
for (const reference of references) {
|
|
13799
|
+
const table = quoteSqlIdentifier(reference.tableName);
|
|
13800
|
+
const column = quoteSqlIdentifier(reference.columnName);
|
|
13801
|
+
database
|
|
13802
|
+
.prepare<unknown, [string | null, string]>(
|
|
13803
|
+
`UPDATE ${table} SET ${column} = ? WHERE ${column} = ?`,
|
|
13804
|
+
)
|
|
13805
|
+
.run(replacement, id);
|
|
13806
|
+
}
|
|
13807
|
+
|
|
13808
|
+
// Workflow context is persisted JSON rather than a relational column, but
|
|
13809
|
+
// it exposes the same requester identity to downstream interpolation. Keep
|
|
13810
|
+
// it consistent with workflow_runs.created_by inside this transaction.
|
|
13811
|
+
if (replacementUserId) {
|
|
13812
|
+
database
|
|
13813
|
+
.prepare<unknown, [string, string]>(
|
|
13814
|
+
`UPDATE workflow_runs
|
|
13815
|
+
SET context = json_set(context, '$.swarm.requestedByUserId', ?)
|
|
13816
|
+
WHERE json_valid(context)
|
|
13817
|
+
AND json_extract(context, '$.swarm.requestedByUserId') = ?`,
|
|
13818
|
+
)
|
|
13819
|
+
.run(replacementUserId, id);
|
|
13820
|
+
} else {
|
|
13821
|
+
database
|
|
13822
|
+
.prepare<unknown, [string]>(
|
|
13823
|
+
`UPDATE workflow_runs
|
|
13824
|
+
SET context = json_remove(context, '$.swarm.requestedByUserId')
|
|
13825
|
+
WHERE json_valid(context)
|
|
13826
|
+
AND json_extract(context, '$.swarm.requestedByUserId') = ?`,
|
|
13827
|
+
)
|
|
13828
|
+
.run(id);
|
|
13829
|
+
}
|
|
13830
|
+
|
|
13831
|
+
const result = database.prepare("DELETE FROM users WHERE id = ?").run(id);
|
|
13832
|
+
return result.changes > 0;
|
|
13833
|
+
})();
|
|
13171
13834
|
}
|
|
13172
13835
|
|
|
13173
13836
|
// ============================================================================
|