@desplega.ai/agent-swarm 2.5.4 → 2.6.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.
Files changed (271) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-ek38cwsv.js → acp-adapter-dgfakfpw.js} +9 -9
  3. package/dist/{actions-92mepq03.js → actions-fbqma8aa.js} +21 -21
  4. package/dist/{amp-adapter-9gaxg015.js → amp-adapter-en0jy6hs.js} +4 -4
  5. package/dist/{app-ctn8gsgx.js → app-k8a5cgpp.js} +3 -3
  6. package/dist/{assistant-zyr5kz47.js → assistant-a16433gr.js} +29 -25
  7. package/dist/{boot-reembed-dwzfttdr.js → boot-reembed-6c1sjfnr.js} +1 -1
  8. package/dist/{boot-reembed-w75maga2.js → boot-reembed-f4nn561n.js} +1 -1
  9. package/dist/{boot-scrub-sweep-e9kq43kf.js → boot-scrub-sweep-d4w9gajf.js} +4 -4
  10. package/dist/{boot-seeds-x28v3fhb.js → boot-seeds-9bmkspyw.js} +4 -4
  11. package/dist/{claude-adapter-f0afqnne.js → claude-adapter-nys1kjxa.js} +9 -9
  12. package/dist/{claude-managed-adapter-x81a7r65.js → claude-managed-adapter-55207973.js} +2 -2
  13. package/dist/{claude-managed-setup-bqzwjd9x.js → claude-managed-setup-3cm35gey.js} +10 -3
  14. package/dist/{claude-sdk-session-7b7whkaq.js → claude-sdk-session-nw5yd8cp.js} +11 -11
  15. package/dist/{cli-c61mbdds.js → cli-03gg9pd1.js} +4 -4
  16. package/dist/{cli-kxpfx2xr.js → cli-0z1xjzwa.js} +2 -2
  17. package/dist/cli-1tem6ns9.js +894 -0
  18. package/dist/{cli-jbnn4vxb.js → cli-24j1jeeb.js} +3 -3
  19. package/dist/{cli-pzatvw94.js → cli-2sjhpsns.js} +2 -2
  20. package/dist/{cli-dew6x255.js → cli-39aet655.js} +2 -2
  21. package/dist/{cli-g1cw7xtt.js → cli-39rrw8yp.js} +1 -1
  22. package/dist/{cli-x0f2v69c.js → cli-3aqd4jaa.js} +12 -7
  23. package/dist/{cli-gbfxp48k.js → cli-3fwqy71j.js} +15 -15
  24. package/dist/{cli-7p4jxgtt.js → cli-3gfvmzht.js} +6 -6
  25. package/dist/{cli-03vpq45j.js → cli-3ndmpxxv.js} +2 -2
  26. package/dist/{cli-asq5ydm4.js → cli-4hxrf0p4.js} +131 -50
  27. package/dist/{cli-mz9afxxf.js → cli-4rf0f5vg.js} +15 -3
  28. package/dist/{cli-k4tjsdx9.js → cli-5rj83bam.js} +272 -195
  29. package/dist/{cli-sbwsxvtd.js → cli-5sckpqcx.js} +2 -2
  30. package/dist/{cli-cz5nmdxt.js → cli-683y0b3a.js} +2 -2
  31. package/dist/{cli-ethgy7ha.js → cli-6y51ryva.js} +2 -2
  32. package/dist/{cli-n4c8e3s5.js → cli-708wdy32.js} +1 -1
  33. package/dist/{cli-pasyvjmm.js → cli-737pyqxr.js} +1 -1
  34. package/dist/{cli-gx4rm0qx.js → cli-7enjwya9.js} +1 -1
  35. package/dist/{cli-6x9ma5w5.js → cli-803188pb.js} +1 -1
  36. package/dist/{cli-hh5we352.js → cli-9abdgn5t.js} +7 -7
  37. package/dist/{cli-ff1tm9n9.js → cli-a2rr0fmg.js} +1 -1
  38. package/dist/{cli-c952c7ac.js → cli-a34pr3w1.js} +2 -2
  39. package/dist/{cli-s682zgqz.js → cli-abkm71vr.js} +1 -1
  40. package/dist/{cli-mmhj8vd7.js → cli-ag2yrnt5.js} +11 -2
  41. package/dist/{cli-bvpgr9ny.js → cli-aj8djyqt.js} +5 -5
  42. package/dist/{cli-gwhjmh8g.js → cli-art7c1zh.js} +5 -5
  43. package/dist/{cli-5bnsc0e1.js → cli-az25fn5f.js} +4 -4
  44. package/dist/{cli-9s05422w.js → cli-bqztfyve.js} +1 -1
  45. package/dist/{cli-bn0rewsy.js → cli-c081bj3b.js} +2 -2
  46. package/dist/{cli-1r5ad7j5.js → cli-c2habvsf.js} +12 -12
  47. package/dist/{cli-64mh9mf3.js → cli-ca6banwz.js} +69 -17
  48. package/dist/{cli-25n3mv5d.js → cli-cz37jqss.js} +5 -5
  49. package/dist/{cli-6n8axjv1.js → cli-d2dpf95n.js} +2 -2
  50. package/dist/{cli-gn5af0x5.js → cli-emhjcj2y.js} +11 -11
  51. package/dist/{cli-6a1xg1p7.js → cli-esakngh8.js} +2 -2
  52. package/dist/{cli-ha9rmwgq.js → cli-fb8m699c.js} +2 -2
  53. package/dist/{cli-4b2z6n0m.js → cli-ffvsj2qw.js} +17 -8
  54. package/dist/{cli-abs8ndtr.js → cli-g20jjwt4.js} +26 -26
  55. package/dist/{cli-jb30dzvk.js → cli-g8ka5j4j.js} +446 -164
  56. package/dist/{cli-19202kew.js → cli-gf95q3bm.js} +5 -5
  57. package/dist/{cli-eftzyh9y.js → cli-gnsb1zxa.js} +2 -2
  58. package/dist/{cli-nwyw3zhg.js → cli-gxee0bqa.js} +1 -1
  59. package/dist/{cli-e42vss7b.js → cli-hfeb68z4.js} +5 -5
  60. package/dist/{cli-g9vd9ece.js → cli-k21gx5ht.js} +14 -16
  61. package/dist/{cli-tsma7p8v.js → cli-kf4jree9.js} +2 -2
  62. package/dist/{cli-dabehxhp.js → cli-kt907da6.js} +1 -1
  63. package/dist/{cli-wx2nj19m.js → cli-m5a9m0ak.js} +6 -6
  64. package/dist/{cli-621ce7kw.js → cli-m70bajzr.js} +3 -3
  65. package/dist/{cli-9j129brq.js → cli-mtnj4fwa.js} +6 -6
  66. package/dist/{cli-xzfq4at3.js → cli-mvjz5an3.js} +15 -11
  67. package/dist/{cli-277c31dw.js → cli-n7asmce6.js} +225 -3
  68. package/dist/{cli-xtrz6bzs.js → cli-nn6vx2mq.js} +1 -1
  69. package/dist/{cli-2655k16q.js → cli-nq677csy.js} +1 -1
  70. package/dist/{cli-0shx9j93.js → cli-pfk92eqz.js} +2 -2
  71. package/dist/{cli-wghjkr0e.js → cli-sqma6vw1.js} +10 -7
  72. package/dist/{cli-nzfnt2gs.js → cli-tf1n0r0b.js} +1 -1
  73. package/dist/{cli-qqzmjxkf.js → cli-v49fkghp.js} +11 -6
  74. package/dist/{cli-x77gen2p.js → cli-w2656ka1.js} +7 -7
  75. package/dist/{cli-5kqnemen.js → cli-yvy48785.js} +1 -1
  76. package/dist/{cli-m4ecdxvz.js → cli-zqne1qcj.js} +7 -7
  77. package/dist/cli.js +17 -17
  78. package/dist/{codex-adapter-wch12q99.js → codex-adapter-d95q4qsn.js} +7 -7
  79. package/dist/{codex-login-rn7c5gxt.js → codex-login-8cg0vhf2.js} +1 -1
  80. package/dist/{codex-session-runner-41pfcr95.js → codex-session-runner-gdwd8h0c.js} +8 -8
  81. package/dist/{commands-9849dkj6.js → commands-jqmgk54b.js} +2 -2
  82. package/dist/{controller-ja0sd95p.js → controller-z3jq589f.js} +4 -4
  83. package/dist/{controllers-connect-xp05dj6x.js → controllers-connect-r2za39ef.js} +23 -23
  84. package/dist/{cursor-adapter-623qxqew.js → cursor-adapter-4mr4jgkw.js} +2 -2
  85. package/dist/{db-47vj40dy.js → db-ppv2tze7.js} +3 -3
  86. package/dist/{db-w759qmx0.js → db-w36wwb5g.js} +4 -2
  87. package/dist/{devin-adapter-4ycy4vrf.js → devin-adapter-byen2cxt.js} +1 -1
  88. package/dist/{dispatcher-7b8e4z2j.js → dispatcher-pv5h77vf.js} +20 -20
  89. package/dist/{docker-s84hjs91.js → docker-fdzyewpz.js} +2 -2
  90. package/dist/{dsh-adapter-2ezw3xea.js → dsh-adapter-d4pdpb3r.js} +2 -2
  91. package/dist/{e2b-9gkdhm3c.js → e2b-222hsajc.js} +2 -2
  92. package/dist/{e2b-cjtf8k1z.js → e2b-fn3zacvt.js} +2 -2
  93. package/dist/{easter-egg-3d-e4qv8dxj.js → easter-egg-3d-4sp4dvmc.js} +1 -1
  94. package/dist/{event-bus-3z78ep5n.js → event-bus-pghg8hqy.js} +1 -1
  95. package/dist/{execute-0xpz8950.js → execute-jev839y1.js} +1 -1
  96. package/dist/{external-channel-e2s028da.js → external-channel-y6nkzhnn.js} +2 -2
  97. package/dist/{grok-adapter-ys7gaqee.js → grok-adapter-t08b9ntm.js} +10 -10
  98. package/dist/handlers-eqsnmv95.js +41 -0
  99. package/dist/{heartbeat-w3mntghj.js → heartbeat-vn31xwdk.js} +23 -23
  100. package/dist/{hook-g7kvv6x9.js → hook-6d0zbvvt.js} +6 -6
  101. package/dist/{hook-3jjywnpb.js → hook-cpjs8q51.js} +8 -8
  102. package/dist/{http-tkp7d4kb.js → http-9c6medda.js} +856 -99
  103. package/dist/{index-xdaahthb.js → index-38t35h0n.js} +20 -20
  104. package/dist/{index-3n8m9ea2.js → index-g6k9tctj.js} +1 -1
  105. package/dist/{index-1ef1myq2.js → index-k0yt1tvq.js} +20 -20
  106. package/dist/{index-31ceyf42.js → index-pkma1yf8.js} +1 -1
  107. package/dist/{index-q7m26kvs.js → index-qhjf2htm.js} +13 -10
  108. package/dist/index-yqgbfgaj.js +43 -0
  109. package/dist/{keepalive-v4ta8wgg.js → keepalive-em0117bp.js} +4 -4
  110. package/dist/lead-23e7z59t.js +79 -0
  111. package/dist/{maintenance-84n1ex21.js → maintenance-kbme3qy9.js} +1 -1
  112. package/dist/{oauth-refresh-sweep-vjnnandw.js → oauth-refresh-sweep-xysj4yse.js} +3 -3
  113. package/dist/{onboard-sfmpe4np.js → onboard-p6q0vz97.js} +3 -3
  114. package/dist/{opencode-adapter-jmpst4q4.js → opencode-adapter-esv3k9yn.js} +7 -7
  115. package/dist/{otel-impl-v6cc0zve.js → otel-impl-54qhqh1z.js} +2 -2
  116. package/dist/{pi-mono-adapter-fk8fkcf6.js → pi-mono-adapter-mymbwhj0.js} +10 -10
  117. package/dist/{pricing-refresh-fsk1e1zc.js → pricing-refresh-yb75wv33.js} +3 -3
  118. package/dist/{rbac-roles-prmkdnkw.js → rbac-roles-1vp2mw4p.js} +2 -2
  119. package/dist/{rbac-roles-zxmxqa39.js → rbac-roles-dexhc9x8.js} +1 -1
  120. package/dist/render-v2-tjay5hhf.js +16 -0
  121. package/dist/{resume-frdtgfa0.js → resume-8ayf37g5.js} +9 -9
  122. package/dist/{runtime-bt5pkpm1.js → runtime-hqe5cqcd.js} +1 -1
  123. package/dist/{session-access-fpez4y5q.js → session-access-fdhp987s.js} +4 -4
  124. package/dist/session-source-backfill-qgtc86z1.js +110 -0
  125. package/dist/{setup-mq6m3m8q.js → setup-4rrmqcq3.js} +1 -1
  126. package/dist/{virtual-modules-mnfbh5mr.js → virtual-modules-52km00d6.js} +2 -2
  127. package/dist/{work-objects-kfahhjsq.js → work-objects-p28fgd70.js} +23 -23
  128. package/dist/worker-e3w7vye1.js +79 -0
  129. package/dist/{x-pbpezr0s.js → x-wxrzvzx8.js} +2 -2
  130. package/openapi.json +1274 -32
  131. package/package.json +1 -1
  132. package/src/assets/source-key.ts +95 -0
  133. package/src/be/db/api-keys.ts +80 -0
  134. package/src/be/db/session-labels.ts +28 -0
  135. package/src/be/db/session-moves.ts +5 -6
  136. package/src/be/db/session-tree.ts +153 -14
  137. package/src/be/db/tasks/read.ts +11 -0
  138. package/src/be/db/tasks/write.ts +6 -1
  139. package/src/be/db/workflow-runs.ts +183 -1
  140. package/src/be/db-retention.ts +61 -6
  141. package/src/be/db.ts +59 -114
  142. package/src/be/memory/providers/openai-embedding.ts +5 -1
  143. package/src/be/migrations/244_api_key_validation.sql +4 -0
  144. package/src/be/onboarding.ts +4 -0
  145. package/src/be/scripts/typecheck.ts +1 -1
  146. package/src/be/session-source-backfill.ts +163 -0
  147. package/src/be/swarm-config-guard.ts +1 -0
  148. package/src/comb/review-batch.ts +28 -1
  149. package/src/comb/templates.ts +7 -2
  150. package/src/commands/claude-managed-setup.ts +7 -1
  151. package/src/commands/credential-refresh.ts +76 -1
  152. package/src/commands/credential-wait.ts +2 -2
  153. package/src/commands/provider-credentials.ts +210 -170
  154. package/src/commands/runner.ts +25 -4
  155. package/src/extensions/apply-task-create.ts +8 -1
  156. package/src/extensions/contract-types.generated.ts +1 -1
  157. package/src/extensions/contract.ts +5 -0
  158. package/src/http/all-routes.ts +2 -0
  159. package/src/http/api-keys.ts +53 -0
  160. package/src/http/assets.ts +15 -0
  161. package/src/http/codex-oauth-accounts.ts +191 -0
  162. package/src/http/comb.ts +15 -2
  163. package/src/http/config.ts +1 -1
  164. package/src/http/core.ts +6 -0
  165. package/src/http/fleet-benchmark.ts +5 -2
  166. package/src/http/index.ts +11 -0
  167. package/src/http/integrations.ts +5 -1
  168. package/src/http/kv.ts +8 -3
  169. package/src/http/onboarding.ts +6 -1
  170. package/src/http/pool-keys.ts +354 -0
  171. package/src/http/sessions.ts +37 -12
  172. package/src/http/stats.ts +1 -0
  173. package/src/http/status.ts +26 -12
  174. package/src/http/tasks.ts +3 -0
  175. package/src/http/used-by.ts +7 -2
  176. package/src/http/workflows.ts +78 -9
  177. package/src/integrations/chat-sdk/inbound.ts +27 -0
  178. package/src/kv-reserved-namespaces.ts +19 -0
  179. package/src/providers/acp-targets.ts +1 -1
  180. package/src/providers/amp-adapter.ts +1 -1
  181. package/src/providers/claude-adapter.ts +5 -1
  182. package/src/providers/claude-managed-adapter.ts +16 -9
  183. package/src/providers/codex-adapter.ts +71 -11
  184. package/src/scheduler/scheduler.ts +3 -2
  185. package/src/scripts-runtime/swarm-sdk.ts +7 -1
  186. package/src/scripts-runtime/types/stdlib.d.ts +1 -1
  187. package/src/scripts-runtime/types/swarm-extension.d.ts +17 -8
  188. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -1
  189. package/src/slack/assistant.ts +7 -0
  190. package/src/slack/channel-labels.ts +74 -0
  191. package/src/slack/handlers.ts +8 -0
  192. package/src/slack/inbound-files.ts +5 -2
  193. package/src/slack/thread-buffer.ts +4 -0
  194. package/src/tasks/sibling-awareness.ts +12 -3
  195. package/src/telemetry-context.ts +5 -6
  196. package/src/telemetry-identity.ts +3 -3
  197. package/src/tests/api-keys-validation.test.ts +170 -0
  198. package/src/tests/claude-managed-adapter.test.ts +46 -0
  199. package/src/tests/claude-routes-e2e.test.ts +7 -2
  200. package/src/tests/codex-credential-pools.test.ts +120 -0
  201. package/src/tests/codex-gateway-route.test.ts +165 -0
  202. package/src/tests/codex-oauth-accounts.test.ts +357 -0
  203. package/src/tests/comb-review-batch.test.ts +63 -2
  204. package/src/tests/config-pool-keys.test.ts +533 -0
  205. package/src/tests/credential-check.test.ts +290 -2
  206. package/src/tests/credential-refresh.test.ts +172 -1
  207. package/src/tests/db-retention.test.ts +9 -3
  208. package/src/tests/db-task-read-contract.test.ts +1 -0
  209. package/src/tests/db-task-write-contract.test.ts +1 -3
  210. package/src/tests/docs-index-footprint.test.js +3 -3
  211. package/src/tests/extensions-pre-task.test.ts +53 -1
  212. package/src/tests/fixtures/extensions/record-task-create/hooks.ts +9 -0
  213. package/src/tests/fixtures/extensions/record-task-create/manifest.json +7 -0
  214. package/src/tests/fleet-benchmark.test.ts +8 -0
  215. package/src/tests/get-workflow-run-tool.test.ts +98 -0
  216. package/src/tests/integrations-http.test.ts +32 -2
  217. package/src/tests/internal-ai/complete-structured.test.ts +37 -1
  218. package/src/tests/internal-ai/credentials.test.ts +23 -0
  219. package/src/tests/kv-http.test.ts +32 -0
  220. package/src/tests/kv-tool.test.ts +25 -0
  221. package/src/tests/multi-runtime-registration.test.ts +12 -12
  222. package/src/tests/onboarding.test.ts +108 -1
  223. package/src/tests/openai-embedding-credentials.test.ts +35 -0
  224. package/src/tests/openrouter-attribution-call-sites.test.ts +2 -2
  225. package/src/tests/org-website.test.ts +36 -0
  226. package/src/tests/provider-key-probes.test.ts +181 -0
  227. package/src/tests/rbac-e2e-helpers.test.ts +88 -1
  228. package/src/tests/rbac-e2e-helpers.ts +36 -11
  229. package/src/tests/script-connections.test.ts +2 -0
  230. package/src/tests/script-runs-retention.test.ts +212 -0
  231. package/src/tests/sdk-allowlist.test.ts +39 -0
  232. package/src/tests/session-source-backfill.test.ts +429 -0
  233. package/src/tests/session-source-keys.test.ts +236 -0
  234. package/src/tests/session-source-labels.test.ts +419 -0
  235. package/src/tests/sessions-detail-batch.test.ts +2 -0
  236. package/src/tests/sessions-move.test.ts +131 -0
  237. package/src/tests/sessions-tree.test.ts +299 -2
  238. package/src/tests/slack-inbound-files.test.ts +103 -23
  239. package/src/tests/status.test.ts +101 -18
  240. package/src/tests/tasks-workflow-run-filter.test.ts +146 -0
  241. package/src/tests/test-net.ts +31 -0
  242. package/src/tests/transcription.test.ts +8 -0
  243. package/src/tests/used-by-route.test.ts +11 -0
  244. package/src/tests/workflow-cascade-cancel.test.ts +172 -0
  245. package/src/tests/workflow-http-v2.test.ts +131 -0
  246. package/src/tests/workflow-step-order.test.ts +92 -0
  247. package/src/tests/workflow-sub-workflow.test.ts +30 -0
  248. package/src/tools/get-tasks.ts +8 -0
  249. package/src/tools/kv/kv-read-auth.ts +4 -2
  250. package/src/tools/task-action.ts +1 -1
  251. package/src/tools/workflows/get-workflow-run.ts +14 -4
  252. package/src/types.ts +24 -0
  253. package/src/utils/company-profile.ts +51 -1
  254. package/src/utils/credentials.ts +5 -2
  255. package/src/utils/internal-ai/complete-structured.ts +5 -0
  256. package/src/utils/internal-ai/credentials.ts +4 -3
  257. package/src/utils/provider-key-probes.ts +328 -0
  258. package/src/utils/transcription.ts +4 -1
  259. package/src/workflows/executors/sub-workflow.ts +8 -1
  260. package/templates/ai-toolbox.manifest.json +2 -2
  261. package/templates/skills/researching/content.md +2 -0
  262. package/templates/skills/swarm-scripts/SKILL.md +1 -0
  263. package/templates/skills/swarm-scripts/content.md +1 -0
  264. package/dist/cli-7421jzsw.js +0 -84
  265. package/dist/cli-e45e4m6b.js +0 -413
  266. package/dist/cli-zw88zjba.js +0 -307
  267. package/dist/handlers-wy1dr6ja.js +0 -41
  268. package/dist/index-nmm79qgr.js +0 -43
  269. package/dist/lead-sawnyqxk.js +0 -79
  270. package/dist/render-v2-pjwxvnkw.js +0 -12
  271. package/dist/worker-v5e5rxj3.js +0 -79
@@ -1,12 +1,161 @@
1
- import { TERMINAL_TASK_STATUSES, type WorkflowRunStatus } from "../../types";
1
+ import {
2
+ TERMINAL_TASK_STATUSES,
3
+ type WorkflowRun,
4
+ type WorkflowRunStatus,
5
+ type WorkflowRunStep,
6
+ type WorkflowRunStepSlim,
7
+ type WorkflowRunStepStatus,
8
+ type WorkflowRunSummary,
9
+ } from "../../types";
2
10
  import { scrubSecrets } from "../../utils/secret-scrubber";
11
+ import { normalizeDate, normalizeDateRequired } from "../date-utils";
3
12
  import { scrubJsonValue } from "../scrub-json";
13
+ import { defineWorkflowPayload, type WorkflowPayloadView } from "../workflow-replay";
4
14
  import { getDbClient } from "./runtime";
5
15
 
6
16
  // Parent/child links between workflow runs (migration 203): a `sub-workflow`
7
17
  // step starts a child run that records the step in `parentStepId`, and the
8
18
  // step's `retryCount` at that time in `parentStepAttempt` (migration 240).
9
19
 
20
+ export type WorkflowRunRow = {
21
+ id: string;
22
+ workflowId: string;
23
+ status: string;
24
+ triggerType: string;
25
+ triggerData: string | null;
26
+ /** Absent from list rows: see `WORKFLOW_RUN_SUMMARY_COLUMNS`. */
27
+ context?: string | null;
28
+ /** Sealed exact `context` (migration 201). Absent from list rows. */
29
+ context_replay?: string | null;
30
+ error: string | null;
31
+ created_by: string | null;
32
+ startedAt: string;
33
+ lastUpdatedAt: string;
34
+ finishedAt: string | null;
35
+ /** The `sub-workflow` step that started this run (migration 203). */
36
+ parentStepId?: string | null;
37
+ /** That step's attempt (`retryCount`) when it started this run (migration 240). */
38
+ parentStepAttempt?: number | null;
39
+ };
40
+
41
+ /**
42
+ * `view` picks the `context` copy: `replay` (default) is the exact sealed value
43
+ * the engine resumes from; `display` is the scrubbed column for API, MCP and UI
44
+ * responses. See `src/be/workflow-replay.ts`.
45
+ */
46
+ export function rowToWorkflowRun(
47
+ row: WorkflowRunRow,
48
+ view: WorkflowPayloadView = "replay",
49
+ ): WorkflowRun {
50
+ const run: WorkflowRun = {
51
+ id: row.id,
52
+ workflowId: row.workflowId,
53
+ status: row.status as WorkflowRunStatus,
54
+ triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
55
+ context: undefined,
56
+ error: row.error ?? undefined,
57
+ createdBy: row.created_by ?? undefined,
58
+ startedAt: normalizeDateRequired(row.startedAt),
59
+ lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
60
+ finishedAt: normalizeDate(row.finishedAt) ?? undefined,
61
+ parentStepId: row.parentStepId ?? undefined,
62
+ parentStepAttempt: row.parentStepId ? (row.parentStepAttempt ?? 0) : undefined,
63
+ };
64
+ return defineWorkflowPayload(run, "context", row.context, row.context_replay, view, {
65
+ runId: row.id,
66
+ target: "its context",
67
+ });
68
+ }
69
+
70
+ // Everything but `context`. `triggerData` stays: it is small, and stored
71
+ // scripts read it off list rows (dedupe by trigger, failure reports). The
72
+ // parent link and attempt stay so a list row identifies a child's invocation.
73
+ export const WORKFLOW_RUN_SUMMARY_COLUMNS =
74
+ "id, workflowId, status, triggerType, triggerData, error, created_by, startedAt, lastUpdatedAt, finishedAt, parentStepId, parentStepAttempt";
75
+
76
+ export type WorkflowRunStepRow = {
77
+ id: string;
78
+ runId: string;
79
+ nodeId: string;
80
+ nodeType: string;
81
+ status: string;
82
+ /** Absent from slim rows: see `WORKFLOW_RUN_STEP_SLIM_COLUMNS`. */
83
+ input?: string | null;
84
+ output: string | null;
85
+ /** Sealed exact `output` (migration 201). Absent from slim rows. */
86
+ output_replay?: string | null;
87
+ error: string | null;
88
+ startedAt: string;
89
+ finishedAt: string | null;
90
+ retryCount: number;
91
+ maxRetries: number;
92
+ nextRetryAt: string | null;
93
+ idempotencyKey: string | null;
94
+ diagnostics: string | null;
95
+ nextPort: string | null;
96
+ };
97
+
98
+ /** `view` picks the `output` copy, as in `rowToWorkflowRun`. */
99
+ export function rowToWorkflowRunStep(
100
+ row: WorkflowRunStepRow,
101
+ view: WorkflowPayloadView = "replay",
102
+ ): WorkflowRunStep {
103
+ const step: WorkflowRunStep = {
104
+ id: row.id,
105
+ runId: row.runId,
106
+ nodeId: row.nodeId,
107
+ nodeType: row.nodeType,
108
+ status: row.status as WorkflowRunStepStatus,
109
+ input: row.input ? JSON.parse(row.input) : undefined,
110
+ output: undefined,
111
+ error: row.error ?? undefined,
112
+ startedAt: normalizeDateRequired(row.startedAt),
113
+ finishedAt: normalizeDate(row.finishedAt) ?? undefined,
114
+ retryCount: row.retryCount,
115
+ maxRetries: row.maxRetries,
116
+ nextRetryAt: normalizeDate(row.nextRetryAt) ?? undefined,
117
+ idempotencyKey: row.idempotencyKey ?? undefined,
118
+ diagnostics: row.diagnostics ?? undefined,
119
+ nextPort: row.nextPort ?? undefined,
120
+ };
121
+ return defineWorkflowPayload(step, "output", row.output, row.output_replay, view, {
122
+ runId: row.runId,
123
+ target: `the output of step ${row.id} (node ${row.nodeId})`,
124
+ });
125
+ }
126
+
127
+ // Everything but `input` (a context snapshot per step, so loops grow it with
128
+ // every iteration) and `output_replay` (the display view never opens it).
129
+ const WORKFLOW_RUN_STEP_SLIM_COLUMNS =
130
+ "id, runId, nodeId, nodeType, status, output, error, startedAt, finishedAt, retryCount, maxRetries, nextRetryAt, idempotencyKey, diagnostics, nextPort";
131
+
132
+ /**
133
+ * `getWorkflowRun` without `context`, in the display view. Backs
134
+ * `GET /api/workflow-runs/{id}?fields=slim`: the large column is neither read
135
+ * nor parsed.
136
+ */
137
+ export async function getWorkflowRunSummary(id: string): Promise<WorkflowRunSummary | null> {
138
+ const row = await getDbClient().get<WorkflowRunRow>(
139
+ `SELECT ${WORKFLOW_RUN_SUMMARY_COLUMNS} FROM workflow_runs WHERE id = ?`,
140
+ [id],
141
+ );
142
+ return row ? rowToWorkflowRun(row, "display") : null;
143
+ }
144
+
145
+ /**
146
+ * `getWorkflowRunStepsByRunId` without `input`, in the display view. Same
147
+ * order, including the rowid tie-break for steps that share `startedAt`.
148
+ */
149
+ export async function getWorkflowRunStepsSlimByRunId(
150
+ runId: string,
151
+ ): Promise<WorkflowRunStepSlim[]> {
152
+ const rows = await getDbClient().query<WorkflowRunStepRow>(
153
+ `SELECT ${WORKFLOW_RUN_STEP_SLIM_COLUMNS} FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC, rowid ASC`,
154
+ [runId],
155
+ );
156
+ return rows.map((row) => rowToWorkflowRunStep(row, "display"));
157
+ }
158
+
10
159
  const TERMINAL_STATUSES = new Set<WorkflowRunStatus>([
11
160
  "completed",
12
161
  "failed",
@@ -242,6 +391,39 @@ export async function workflowRunTreeHasOpenWork(runIds: string[]): Promise<bool
242
391
  return open;
243
392
  }
244
393
 
394
+ export type WorkflowRunTree = {
395
+ rootRunId: string;
396
+ /** Root first, in `getWorkflowRunTreeIds` order. List rows: no `context`. */
397
+ runs: WorkflowRun[];
398
+ hasOpenWork: boolean;
399
+ };
400
+
401
+ /**
402
+ * The run and every run below it, with whether any of them still has open
403
+ * work (see `workflowRunTreeHasOpenWork`). One read transaction, so `runs` and
404
+ * `hasOpenWork` come from the same snapshot. Null when the root is missing.
405
+ * Throws WorkflowRunTreeError on a cycle or a chain past the depth bound.
406
+ */
407
+ export async function getWorkflowRunTree(rootRunId: string): Promise<WorkflowRunTree | null> {
408
+ return getDbClient().transaction(
409
+ async () => {
410
+ if ((await getWorkflowRunStatus(rootRunId)) === null) return null;
411
+ const ids = await getWorkflowRunTreeIds(rootRunId);
412
+ const byId = new Map<string, WorkflowRun>();
413
+ await forEachChunk(ids, async (chunk, placeholders) => {
414
+ const rows = await getDbClient().query<WorkflowRunRow>(
415
+ `SELECT ${WORKFLOW_RUN_SUMMARY_COLUMNS} FROM workflow_runs WHERE id IN (${placeholders})`,
416
+ chunk,
417
+ );
418
+ for (const row of rows) byId.set(row.id, rowToWorkflowRun(row, "display"));
419
+ });
420
+ const runs = ids.flatMap((id) => byId.get(id) ?? []);
421
+ return { rootRunId, runs, hasOpenWork: await workflowRunTreeHasOpenWork(ids) };
422
+ },
423
+ { readOnly: true },
424
+ );
425
+ }
426
+
245
427
  /**
246
428
  * Cancel the pending, running and waiting steps of the given runs. The
247
429
  * migration-140 trigger closes each cancelled step's pending approval.
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { Attributes, DbRetentionSweepMetric, SpanStatus, SwarmSpan } from "../otel";
3
3
  import { recordDbRetentionStatement, recordDbRetentionSweep, startSpan, withSpan } from "../otel";
4
+ import { TERMINAL_SCRIPT_RUN_STATUSES } from "../types";
4
5
  import { isEnvFlagEnabled } from "../utils/env-flag";
5
6
  import { scrubSecrets } from "../utils/secret-scrubber";
6
7
  import { getDbClient } from "./db";
@@ -23,7 +24,11 @@ const YIELD_MS = 25;
23
24
  *
24
25
  * Two policies exist:
25
26
  * - `age`: the env value is a number of days; rows whose `timeColumn` is older
26
- * than that horizon are deleted.
27
+ * than that horizon are deleted. Optional narrowing: `terminalStatuses`
28
+ * deletes only rows whose `statusColumn` is in that list, `keepWhenSet`
29
+ * keeps every row where that column is non-NULL, and
30
+ * `cutoffFormat: "sqliteDatetime"` compares against a `YYYY-MM-DD HH:MM:SS`
31
+ * cutoff for a column that mixes that format with ISO strings.
27
32
  * - `keepLatest`: the env value is a count; within each `partitionBy` group,
28
33
  * only the newest `rankBy` rows are kept. The newest row of a group is
29
34
  * never deleted, because the smallest accepted count is 1.
@@ -65,6 +70,29 @@ export const DB_RETENTION_TABLES = [
65
70
  initialBatchSize: 50,
66
71
  maxBatchSize: 100,
67
72
  },
73
+ {
74
+ // Every inline script run stores its full source, args and output. Only
75
+ // terminal runs are eligible: a running or paused durable run must survive
76
+ // to be resumed. Runs with an idempotencyKey are kept, so a retried launch
77
+ // still gets its 409 instead of executing twice. The status filter, the
78
+ // time range and the key exclusion all resolve on indexes
79
+ // (idx_script_runs_status_startedAt, idx_script_runs_idempotencyKey).
80
+ // Workflow runs write `datetime('now')` (space separator) while inline runs
81
+ // write ISO strings, hence the sqliteDatetime cutoff. script_run_journal
82
+ // rows go with their run through ON DELETE CASCADE on idx_srj_runId.
83
+ // Rows average ~30 KB and reach ~1.3 MB, so batches stay small.
84
+ table: "script_runs",
85
+ policy: "age",
86
+ timeColumn: "startedAt",
87
+ cutoffFormat: "sqliteDatetime",
88
+ statusColumn: "status",
89
+ terminalStatuses: TERMINAL_SCRIPT_RUN_STATUSES,
90
+ keepWhenSet: "idempotencyKey",
91
+ envKey: "SCRIPT_RUNS_RETENTION_DAYS",
92
+ metricsKey: "scriptRuns",
93
+ initialBatchSize: 100,
94
+ maxBatchSize: 500,
95
+ },
68
96
  ] as const satisfies ReadonlyArray<
69
97
  {
70
98
  table: string;
@@ -73,7 +101,14 @@ export const DB_RETENTION_TABLES = [
73
101
  initialBatchSize?: number;
74
102
  maxBatchSize?: number;
75
103
  } & (
76
- | { policy: "age"; timeColumn: string }
104
+ | {
105
+ policy: "age";
106
+ timeColumn: string;
107
+ cutoffFormat?: "sqliteDatetime";
108
+ statusColumn?: string;
109
+ terminalStatuses?: readonly string[];
110
+ keepWhenSet?: string;
111
+ }
77
112
  | { policy: "keepLatest"; partitionBy: string; rankBy: string }
78
113
  )
79
114
  >;
@@ -326,13 +361,33 @@ export function sweepStatements(table: RetentionTable, value: number, now: Date)
326
361
  params: [value],
327
362
  };
328
363
  }
329
- const cutoff = new Date(now.getTime() - value * DAY_MS).toISOString();
364
+ const iso = new Date(now.getTime() - value * DAY_MS).toISOString();
365
+ // Text comparison of the two formats: ' ' sorts before 'T', so against a
366
+ // space-separated cutoff an ISO row on the cutoff's own date is kept, never
367
+ // deleted early. It is swept on the next day's ticks instead.
368
+ const cutoff = "cutoffFormat" in table ? `${iso.slice(0, 10)} ${iso.slice(11, 19)}` : iso;
369
+ const conditions: string[] = [];
370
+ const params: Array<string | number> = [];
371
+ if ("terminalStatuses" in table) {
372
+ conditions.push(
373
+ `${table.statusColumn} IN (${table.terminalStatuses.map(() => "?").join(", ")})`,
374
+ );
375
+ params.push(...table.terminalStatuses);
376
+ }
377
+ conditions.push(`${table.timeColumn} < ?`);
378
+ params.push(cutoff);
379
+ if ("keepWhenSet" in table) {
380
+ conditions.push(
381
+ `rowid NOT IN (SELECT rowid FROM ${table.table} WHERE ${table.keepWhenSet} IS NOT NULL)`,
382
+ );
383
+ }
384
+ const where = conditions.join(" AND ");
330
385
  return {
331
- backlogSql: `SELECT COUNT(*) AS n FROM ${table.table} WHERE ${table.timeColumn} < ?`,
386
+ backlogSql: `SELECT COUNT(*) AS n FROM ${table.table} WHERE ${where}`,
332
387
  deleteSql: `DELETE FROM ${table.table} WHERE rowid IN (
333
- SELECT rowid FROM ${table.table} WHERE ${table.timeColumn} < ? ORDER BY ${table.timeColumn} LIMIT ?
388
+ SELECT rowid FROM ${table.table} WHERE ${where} ORDER BY ${table.timeColumn} LIMIT ?
334
389
  )`,
335
- params: [cutoff],
390
+ params,
336
391
  };
337
392
  }
338
393
 
package/src/be/db.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import pkg from "../../package.json";
3
- import { defaultAssetKey, normalizeAssetKey, sessionGroupKey } from "../assets/key";
3
+ import { defaultAssetKey, normalizeAssetKey } from "../assets/key";
4
+ import { sessionSourceKey, sessionThreadGroupKey } from "../assets/source-key";
4
5
  import {
5
6
  generateDefaultClaudeMd,
6
7
  generateDefaultIdentityMd,
@@ -207,6 +208,7 @@ import {
207
208
  sessionCountQuery,
208
209
  sessionPageFields,
209
210
  sessionSummaryQuery,
211
+ threadRootsSql,
210
212
  } from "./db/session-tree";
211
213
  import { searchSkillRowsRanked } from "./db/skill-search";
212
214
  import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
@@ -227,6 +229,11 @@ import {
227
229
  emitChildRunFinished,
228
230
  getRunLineageBlock,
229
231
  getStepExecutionBlock,
232
+ rowToWorkflowRun,
233
+ rowToWorkflowRunStep,
234
+ WORKFLOW_RUN_SUMMARY_COLUMNS,
235
+ type WorkflowRunRow,
236
+ type WorkflowRunStepRow,
230
237
  WorkflowStepNotExecutableError,
231
238
  wakeParentsOfDeletedRuns,
232
239
  } from "./db/workflow-runs";
@@ -242,7 +249,7 @@ import {
242
249
  } from "./swarm-config-guard";
243
250
  import { emitTaskStarted } from "./task-lifecycle-events";
244
251
  import { publishWorkerConfigNudge, publishWorkerNudge } from "./worker-nudges";
245
- import { defineWorkflowPayload, type WorkflowPayloadView } from "./workflow-replay";
252
+ import type { WorkflowPayloadView } from "./workflow-replay";
246
253
 
247
254
  export {
248
255
  refreshActiveSessionOnActivity,
@@ -285,7 +292,7 @@ export {
285
292
  updateAgentStatus,
286
293
  updateAgentStatusFromCapacity,
287
294
  } from "./db/agents";
288
- export { recordKeySeatMismatch } from "./db/api-keys";
295
+ export { recordKeySeatMismatch, recordKeyValidation } from "./db/api-keys";
289
296
  export {
290
297
  type ApprovalRequestSummary,
291
298
  type ApprovalVote,
@@ -2497,6 +2504,8 @@ export async function createTaskExtended(
2497
2504
  );
2498
2505
  }
2499
2506
  let requestedByUserIdInherited = false;
2507
+ // The parent key is read again inside the INSERT transaction (see below).
2508
+ let keyInherited = false;
2500
2509
  // True only when `options.routingAffinity` ends up populated purely via
2501
2510
  // the plain parent-fallback inherit below (child declared no affinity of
2502
2511
  // its own, the parent wasn't lead-only, AND the parent's affinity actually
@@ -2661,6 +2670,7 @@ export async function createTaskExtended(
2661
2670
  }
2662
2671
  if (parent.key && !options.key) {
2663
2672
  options.key = parent.key;
2673
+ keyInherited = true;
2664
2674
  }
2665
2675
  if (parent.contextKey && !options.contextKey) {
2666
2676
  options.contextKey = parent.contextKey;
@@ -2850,7 +2860,18 @@ export async function createTaskExtended(
2850
2860
  }
2851
2861
 
2852
2862
  const auditUserId = getCurrentRequestUserId() ?? null;
2853
- const assetKey = normalizeAssetKey(options?.key ?? defaultAssetKey("task", id));
2863
+ const assetKey = normalizeAssetKey(
2864
+ options?.key ??
2865
+ (options?.parentTaskId
2866
+ ? null
2867
+ : sessionSourceKey({
2868
+ id,
2869
+ slackChannelId: options?.slackChannelId,
2870
+ slackThreadTs: options?.slackThreadTs,
2871
+ contextKey: options?.contextKey,
2872
+ })) ??
2873
+ defaultAssetKey("task", id),
2874
+ );
2854
2875
  // The Linear tracker dedup guard and the INSERT commit as one unit. Two
2855
2876
  // distinct webhook deliveries for the same issue are dispatched
2856
2877
  // fire-and-forget (src/linear/webhook.ts), nothing serializes them, and
@@ -2875,6 +2896,19 @@ export async function createTaskExtended(
2875
2896
  : await findExistingLinearTrackerContextWork(options?.contextKey);
2876
2897
  if (existingTrackerWork) return { existing: existingTrackerWork };
2877
2898
 
2899
+ // A chain move (a user move, the source key backfill) can commit between
2900
+ // the parent read above and this write lock. Its chain query did not see
2901
+ // this child, so take the parent's key as it is now.
2902
+ const parentKey =
2903
+ keyInherited && options?.parentTaskId
2904
+ ? (
2905
+ await getDbClient().get<{ key: string }>('SELECT "key" FROM agent_tasks WHERE id = ?', [
2906
+ options.parentTaskId,
2907
+ ])
2908
+ )?.key
2909
+ : undefined;
2910
+ const insertKey = parentKey ? normalizeAssetKey(parentKey) : assetKey;
2911
+
2878
2912
  let routingDecisionSnapshot: RoutingDecisionSnapshot | null = null;
2879
2913
  try {
2880
2914
  routingDecisionSnapshot = await captureRoutingDecisionSnapshot(options);
@@ -2905,7 +2939,7 @@ export async function createTaskExtended(
2905
2939
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2906
2940
  [
2907
2941
  id,
2908
- assetKey,
2942
+ insertKey,
2909
2943
  options?.agentId ?? null,
2910
2944
  options?.creatorAgentId ?? null,
2911
2945
  // Free text from every task source; scrub at the single INSERT.
@@ -7859,53 +7893,6 @@ export async function getWorkflowsByScheduleId(scheduleId: string): Promise<Work
7859
7893
  // Workflow Run CRUD
7860
7894
  // ============================================================================
7861
7895
 
7862
- type WorkflowRunRow = {
7863
- id: string;
7864
- workflowId: string;
7865
- status: string;
7866
- triggerType: string;
7867
- triggerData: string | null;
7868
- /** Absent from list rows: see `WORKFLOW_RUN_SUMMARY_COLUMNS`. */
7869
- context?: string | null;
7870
- /** Sealed exact `context` (migration 201). Absent from list rows. */
7871
- context_replay?: string | null;
7872
- error: string | null;
7873
- created_by: string | null;
7874
- startedAt: string;
7875
- lastUpdatedAt: string;
7876
- finishedAt: string | null;
7877
- /** The `sub-workflow` step that started this run (migration 203). */
7878
- parentStepId?: string | null;
7879
- /** That step's attempt (`retryCount`) when it started this run (migration 240). */
7880
- parentStepAttempt?: number | null;
7881
- };
7882
-
7883
- /**
7884
- * `view` picks the `context` copy: `replay` (default) is the exact sealed value
7885
- * the engine resumes from; `display` is the scrubbed column for API, MCP and UI
7886
- * responses. See `src/be/workflow-replay.ts`.
7887
- */
7888
- function rowToWorkflowRun(row: WorkflowRunRow, view: WorkflowPayloadView = "replay"): WorkflowRun {
7889
- const run: WorkflowRun = {
7890
- id: row.id,
7891
- workflowId: row.workflowId,
7892
- status: row.status as WorkflowRunStatus,
7893
- triggerData: row.triggerData ? JSON.parse(row.triggerData) : undefined,
7894
- context: undefined,
7895
- error: row.error ?? undefined,
7896
- createdBy: row.created_by ?? undefined,
7897
- startedAt: normalizeDateRequired(row.startedAt),
7898
- lastUpdatedAt: normalizeDateRequired(row.lastUpdatedAt),
7899
- finishedAt: normalizeDate(row.finishedAt) ?? undefined,
7900
- parentStepId: row.parentStepId ?? undefined,
7901
- parentStepAttempt: row.parentStepId ? (row.parentStepAttempt ?? 0) : undefined,
7902
- };
7903
- return defineWorkflowPayload(run, "context", row.context, row.context_replay, view, {
7904
- runId: row.id,
7905
- target: "its context",
7906
- });
7907
- }
7908
-
7909
7896
  export async function createWorkflowRun(data: {
7910
7897
  id: string;
7911
7898
  workflowId: string;
@@ -8031,12 +8018,6 @@ export type WorkflowRunListOptions = {
8031
8018
  includeContext?: boolean;
8032
8019
  };
8033
8020
 
8034
- // Everything but `context`. `triggerData` stays: it is small, and stored
8035
- // scripts read it off list rows (dedupe by trigger, failure reports). The
8036
- // parent link and attempt stay so a list row identifies a child's invocation.
8037
- const WORKFLOW_RUN_SUMMARY_COLUMNS =
8038
- "id, workflowId, status, triggerType, triggerData, error, created_by, startedAt, lastUpdatedAt, finishedAt, parentStepId, parentStepAttempt";
8039
-
8040
8021
  export type WorkflowRunPage = {
8041
8022
  runs: WorkflowRun[];
8042
8023
  page: {
@@ -8121,56 +8102,6 @@ export async function listWorkflowRunsPage(
8121
8102
  // Workflow Run Step CRUD
8122
8103
  // ============================================================================
8123
8104
 
8124
- type WorkflowRunStepRow = {
8125
- id: string;
8126
- runId: string;
8127
- nodeId: string;
8128
- nodeType: string;
8129
- status: string;
8130
- input: string | null;
8131
- output: string | null;
8132
- /** Sealed exact `output` (migration 201). */
8133
- output_replay: string | null;
8134
- error: string | null;
8135
- startedAt: string;
8136
- finishedAt: string | null;
8137
- retryCount: number;
8138
- maxRetries: number;
8139
- nextRetryAt: string | null;
8140
- idempotencyKey: string | null;
8141
- diagnostics: string | null;
8142
- nextPort: string | null;
8143
- };
8144
-
8145
- /** `view` picks the `output` copy, as in `rowToWorkflowRun`. */
8146
- function rowToWorkflowRunStep(
8147
- row: WorkflowRunStepRow,
8148
- view: WorkflowPayloadView = "replay",
8149
- ): WorkflowRunStep {
8150
- const step: WorkflowRunStep = {
8151
- id: row.id,
8152
- runId: row.runId,
8153
- nodeId: row.nodeId,
8154
- nodeType: row.nodeType,
8155
- status: row.status as WorkflowRunStepStatus,
8156
- input: row.input ? JSON.parse(row.input) : undefined,
8157
- output: undefined,
8158
- error: row.error ?? undefined,
8159
- startedAt: normalizeDateRequired(row.startedAt),
8160
- finishedAt: normalizeDate(row.finishedAt) ?? undefined,
8161
- retryCount: row.retryCount,
8162
- maxRetries: row.maxRetries,
8163
- nextRetryAt: normalizeDate(row.nextRetryAt) ?? undefined,
8164
- idempotencyKey: row.idempotencyKey ?? undefined,
8165
- diagnostics: row.diagnostics ?? undefined,
8166
- nextPort: row.nextPort ?? undefined,
8167
- };
8168
- return defineWorkflowPayload(step, "output", row.output, row.output_replay, view, {
8169
- runId: row.runId,
8170
- target: `the output of step ${row.id} (node ${row.nodeId})`,
8171
- });
8172
- }
8173
-
8174
8105
  export async function createWorkflowRunStep(data: {
8175
8106
  id: string;
8176
8107
  runId: string;
@@ -8284,7 +8215,7 @@ export async function getWorkflowRunStepsByRunId(
8284
8215
  view: WorkflowPayloadView = "replay",
8285
8216
  ): Promise<WorkflowRunStep[]> {
8286
8217
  const rows = await getDbClient().query<WorkflowRunStepRow>(
8287
- "SELECT * FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC",
8218
+ "SELECT * FROM workflow_run_steps WHERE runId = ? ORDER BY startedAt ASC, rowid ASC",
8288
8219
  [runId],
8289
8220
  );
8290
8221
  return rows.map((row) => rowToWorkflowRunStep(row, view));
@@ -8400,7 +8331,7 @@ export async function getLatestStepForNode(
8400
8331
  nodeId: string,
8401
8332
  ): Promise<WorkflowRunStep | null> {
8402
8333
  const row = await getDbClient().get<WorkflowRunStepRow>(
8403
- "SELECT * FROM workflow_run_steps WHERE runId = ? AND nodeId = ? ORDER BY startedAt DESC LIMIT 1",
8334
+ "SELECT * FROM workflow_run_steps WHERE runId = ? AND nodeId = ? ORDER BY startedAt DESC, rowid DESC LIMIT 1",
8404
8335
  [runId, nodeId],
8405
8336
  );
8406
8337
  return row ? rowToWorkflowRunStep(row) : null;
@@ -11742,6 +11673,8 @@ export interface ApiKeyStatus {
11742
11673
  /** Auth failures in a row since the last success or clear. */
11743
11674
  consecutiveAuthFailures: number;
11744
11675
  lastAuthFailureAt: string | null;
11676
+ lastValidationError: string | null;
11677
+ lastValidationFailedAt: string | null;
11745
11678
  createdAt: string;
11746
11679
  updatedAt: string;
11747
11680
  }
@@ -11767,6 +11700,7 @@ function rowToApiKeyStatus(row: ApiKeyStatusRow): ApiKeyStatus {
11767
11700
 
11768
11701
  export interface AvailableKeyIndicesResult {
11769
11702
  availableIndices: number[];
11703
+ rejectedIndices: number[];
11770
11704
  /** Indices excluded only by an active model-scoped window block (not key-wide). */
11771
11705
  modelBlockedIndices: number[];
11772
11706
  /** ISO of the earliest resetsAt among modelBlockedIndices, or null when none. */
@@ -11815,8 +11749,9 @@ export async function getAvailableKeyIndices(
11815
11749
  rateLimitWindows: string | null;
11816
11750
  plan: string | null;
11817
11751
  authFailureSeq: number;
11752
+ lastValidationFailedAt: string | null;
11818
11753
  }>(
11819
- `SELECT keyIndex, status, rateLimitWindows, plan, authFailureSeq FROM api_key_status
11754
+ `SELECT keyIndex, status, rateLimitWindows, plan, authFailureSeq, lastValidationFailedAt FROM api_key_status
11820
11755
  WHERE keyType = ? AND scope = ? AND scopeId = ?`,
11821
11756
  [keyType, scope, effectiveScopeId],
11822
11757
  );
@@ -11824,6 +11759,12 @@ export async function getAvailableKeyIndices(
11824
11759
  const blockedIndices = new Set(
11825
11760
  rows.filter((r) => r.status === "rate_limited").map((r) => r.keyIndex),
11826
11761
  );
11762
+ const rejectedIndices = [
11763
+ ...new Set(
11764
+ rows.filter((row) => row.lastValidationFailedAt !== null).map((row) => row.keyIndex),
11765
+ ),
11766
+ ];
11767
+ for (const index of rejectedIndices) blockedIndices.add(index);
11827
11768
 
11828
11769
  const modelBlockedIndices: number[] = [];
11829
11770
  let earliestModelResetsAtSec: number | undefined;
@@ -11866,6 +11807,7 @@ export async function getAvailableKeyIndices(
11866
11807
 
11867
11808
  return {
11868
11809
  availableIndices,
11810
+ rejectedIndices,
11869
11811
  modelBlockedIndices,
11870
11812
  seatBlockedIndices,
11871
11813
  earliestModelResetAt:
@@ -12997,6 +12939,7 @@ export async function listRecentSessions(
12997
12939
  opts: ListRecentSessionsOpts = {},
12998
12940
  ): Promise<SessionListItem[] | SessionListItemSummary[]> {
12999
12941
  const query = sessionSummaryQuery(opts);
12942
+ const threadRoots = threadRootsSql();
13000
12943
  const rootRows = await getDbClient().query<
13001
12944
  AgentTaskRow & {
13002
12945
  __chainCount: number;
@@ -13005,15 +12948,17 @@ export async function listRecentSessions(
13005
12948
  __readState: SessionReadState | null;
13006
12949
  __snoozeUntil: string | null;
13007
12950
  __needsYou: number;
12951
+ __threadRoots: number;
13008
12952
  }
13009
12953
  >(
13010
12954
  `SELECT r.*, a.chainCount AS __chainCount, a.lastActivityAt AS __lastActivityAt,
13011
12955
  a.latestStatus AS __latestStatus, a.readState AS __readState,
13012
- a.snoozeUntil AS __snoozeUntil, a.needsYou AS __needsYou
12956
+ a.snoozeUntil AS __snoozeUntil, a.needsYou AS __needsYou,
12957
+ ${threadRoots.sql} AS __threadRoots
13013
12958
  FROM (${query.sql}) a CROSS JOIN agent_tasks r ON r.id = a.root_id
13014
12959
  ORDER BY ${opts.triage === "later" ? "a.snoozeUntil ASC," : ""}
13015
12960
  a.lastActivityAt DESC, r.createdAt DESC, r.id DESC LIMIT ? OFFSET ?`,
13016
- [...query.params, opts.limit ?? 25, opts.offset ?? 0],
12961
+ [...threadRoots.params, ...query.params, opts.limit ?? 25, opts.offset ?? 0],
13017
12962
  );
13018
12963
  const fields = await sessionPageFields(
13019
12964
  rootRows.map((row) => ({
@@ -13029,7 +12974,7 @@ export async function listRecentSessions(
13029
12974
  ...(row.__readState ? { readState: row.__readState } : {}),
13030
12975
  snoozeUntil: row.__snoozeUntil,
13031
12976
  needsYou: !!row.__needsYou,
13032
- groupKey: sessionGroupKey(row.key),
12977
+ groupKey: sessionThreadGroupKey(row.key, row.__threadRoots),
13033
12978
  ...fields.get(row.id),
13034
12979
  }));
13035
12980
  }
@@ -1,4 +1,5 @@
1
1
  import OpenAI from "openai";
2
+ import { selectCredential } from "../../../utils/credentials";
2
3
  import { getOpenRouterAttributionHeaders } from "../../../utils/openrouter-base-url";
3
4
  import { DEFAULT_EMBEDDING_MODEL, EMBEDDING_DIMENSIONS } from "../constants";
4
5
  import type { EmbeddingProvider } from "../types";
@@ -46,7 +47,10 @@ export class OpenAIEmbeddingProvider implements EmbeddingProvider {
46
47
  private readonly apiKey: string | undefined;
47
48
 
48
49
  constructor(config?: OpenAIEmbeddingConfig) {
49
- this.apiKey = config?.apiKey ?? process.env.EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY;
50
+ const apiKey = config?.apiKey ?? process.env.EMBEDDING_API_KEY ?? process.env.OPENAI_API_KEY;
51
+ this.apiKey = apiKey
52
+ ? selectCredential(apiKey, undefined, "EMBEDDING_API_KEY").selected
53
+ : undefined;
50
54
 
51
55
  this.model = config?.model ?? process.env.EMBEDDING_MODEL ?? "text-embedding-3-small";
52
56
 
@@ -0,0 +1,4 @@
1
+ -- Last provider rejection of a pooled key by a worker live test (HTTP 401).
2
+ -- A key with lastValidationFailedAt set is left out of /api/keys/available.
3
+ ALTER TABLE api_key_status ADD COLUMN lastValidationError TEXT;
4
+ ALTER TABLE api_key_status ADD COLUMN lastValidationFailedAt TEXT;
@@ -130,6 +130,10 @@ export const OnboardingSignalsSchema = z.object({
130
130
  state: z.enum(["unverified", "configured", "verified"]),
131
131
  workers: z.number().int().nonnegative(),
132
132
  verifiedWorkers: z.number().int().nonnegative(),
133
+ /** The latest live test failed: its error. */
134
+ error: z.string().nullable(),
135
+ /** The latest live test passed, but some pool keys failed: names them. */
136
+ warning: z.string().nullable(),
133
137
  }),
134
138
  ),
135
139
  embeddings: z.object({
@@ -250,7 +250,7 @@ export interface SwarmSdk {
250
250
  workflow_list(args?: { enabled?: boolean; includeFull?: boolean; consecutiveErrorsMin?: number; lastRunStatus?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled" }): Promise<unknown>;
251
251
  workflow_get(args: { id: string }): Promise<unknown>;
252
252
  workflow_listRuns(args: { workflowId: string; status?: "running" | "waiting" | "completed" | "failed" | "skipped" | "cancelled"; limit?: number; offset?: number }): Promise<unknown>;
253
- workflow_getRun(args: { id: string }): Promise<unknown>;
253
+ workflow_getRun(args: { id: string; fields?: "full" | "slim" }): Promise<unknown>;
254
254
  // --- prompt templates ---
255
255
  prompt_list(args?: { eventType?: string; scope?: "global" | "agent" | "repo"; scopeId?: string; isDefault?: boolean }): Promise<unknown>;
256
256
  prompt_get(args: { id: string }): Promise<unknown>;