@desplega.ai/agent-swarm 1.152.2 → 1.154.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/README.md +2 -2
  2. package/dist/{acp-adapter-pfeje2v5.js → acp-adapter-eb2aj8xy.js} +4 -4
  3. package/dist/{actions-0sttc7f5.js → actions-kpnwp1kz.js} +31 -27
  4. package/dist/{anthropic-messages-2y8nktar.js → anthropic-messages-9vqsbznt.js} +112 -84
  5. package/dist/{app-p8teva95.js → app-zk1apkf6.js} +10 -7
  6. package/dist/{assistant-r45jnsrt.js → assistant-kwq4y50q.js} +32 -28
  7. package/dist/{azure-openai-responses-f38x2avq.js → azure-openai-responses-04qwdwfq.js} +27 -13
  8. package/dist/{boot-reembed-je26h5y7.js → boot-reembed-22hd3jjn.js} +9 -7
  9. package/dist/{boot-reembed-1qmybsd4.js → boot-reembed-qe168424.js} +10 -8
  10. package/dist/{boot-scrub-logs-gte3h1mm.js → boot-scrub-logs-pkdpsrjd.js} +7 -5
  11. package/dist/{claude-adapter-csc1mzka.js → claude-adapter-3fx90nww.js} +18 -16
  12. package/dist/{claude-managed-adapter-dsmkcrjy.js → claude-managed-adapter-ea2aga6w.js} +3 -3
  13. package/dist/{claude-sdk-session-fcd6f078.js → claude-sdk-session-z7fahf1k.js} +18 -16
  14. package/dist/{cli-r2fpe7cx.js → cli-00w2c66p.js} +61 -55
  15. package/dist/{cli-wbyk465z.js → cli-09y3tjxz.js} +1 -1
  16. package/dist/{cli-ht5rs63n.js → cli-0e31nm4n.js} +16 -7
  17. package/dist/cli-0x5qzvkg.js +4279 -0
  18. package/dist/{cli-d3p82erb.js → cli-18amys3y.js} +9 -9
  19. package/dist/{cli-4ahpr4sw.js → cli-1jpjafw4.js} +5 -5
  20. package/dist/{cli-jbmc0r7r.js → cli-243tzws8.js} +1 -1
  21. package/dist/{cli-ch38q5z4.js → cli-24k806sc.js} +6789 -2542
  22. package/dist/cli-3bkz2p7j.js +2828 -0
  23. package/dist/cli-3er6gcsn.js +1849 -0
  24. package/dist/cli-4k7jazkq.js +3339 -0
  25. package/dist/{cli-c4eeb5q2.js → cli-5b6b62tz.js} +1 -1
  26. package/dist/{cli-qd65dvr6.js → cli-5vs0nh43.js} +65 -44
  27. package/dist/{cli-z3wkng35.js → cli-6r3cgpk2.js} +224 -156
  28. package/dist/{cli-pgj4a5fb.js → cli-71cj99hx.js} +4 -3
  29. package/dist/cli-8sea27q4.js +14075 -0
  30. package/dist/{cli-jvy9qgjs.js → cli-913yakhv.js} +22 -7
  31. package/dist/{cli-pf9q3696.js → cli-927erqqt.js} +5 -2
  32. package/dist/cli-95jnpnqg.js +230 -0
  33. package/dist/{cli-wpv5rgg2.js → cli-abp3g4ax.js} +15 -2
  34. package/dist/{cli-ckz5t2hb.js → cli-acy6bfhr.js} +10 -7
  35. package/dist/{cli-4yc5d8fk.js → cli-avkd6mqv.js} +50 -0
  36. package/dist/{cli-8wrympd7.js → cli-bzr0ftht.js} +26 -7
  37. package/dist/{cli-qn042eh7.js → cli-c1v10xb2.js} +194 -1913
  38. package/dist/cli-cgj4hc12.js +18902 -0
  39. package/dist/{cli-2nnd56zt.js → cli-e0pg4zeh.js} +3 -2
  40. package/dist/{cli-r84ctdxn.js → cli-e31ehqwy.js} +2 -2
  41. package/dist/{cli-gphgnvpf.js → cli-e3229na9.js} +3 -3
  42. package/dist/{cli-gpqcs935.js → cli-e3cyf59d.js} +2 -2
  43. package/dist/{cli-s25avqg9.js → cli-e781c6f9.js} +1 -0
  44. package/dist/{cli-s2qjdd6z.js → cli-eetrvv91.js} +1 -1
  45. package/dist/{cli-j4nv2wqy.js → cli-fdzg6jhf.js} +10 -4
  46. package/dist/{cli-fm4ytsdk.js → cli-g2h29qar.js} +2 -2
  47. package/dist/{cli-zvgj5zg8.js → cli-ge67p080.js} +302 -3497
  48. package/dist/cli-h38x0qhg.js +6912 -0
  49. package/dist/{cli-9xvdm3bx.js → cli-h60qfasa.js} +742 -278
  50. package/dist/{cli-jew3jda5.js → cli-hws0vn86.js} +2 -2
  51. package/dist/{cli-j8dggbcr.js → cli-k0g1sjzm.js} +23 -31
  52. package/dist/{cli-xw7vak96.js → cli-kp3qs53m.js} +5 -5
  53. package/dist/{cli-rgvp6r2x.js → cli-mcnhgnn1.js} +193 -4
  54. package/dist/{cli-zvkq5csn.js → cli-myaxtgh8.js} +7 -6
  55. package/dist/{cli-56h18ng2.js → cli-n62atwh9.js} +1 -1
  56. package/dist/cli-n7h1kqpf.js +29 -0
  57. package/dist/{cli-fmy65jdn.js → cli-pf1v8xhb.js} +7 -5
  58. package/dist/{cli-xy1cchaf.js → cli-q85qhckh.js} +238 -11
  59. package/dist/{cli-3bpzj8me.js → cli-r1d3dzt6.js} +2 -2
  60. package/dist/{pi-mono-adapter-jcqt71cg.js → cli-rxkqvngf.js} +67343 -101083
  61. package/dist/{cli-kj76p1ez.js → cli-scwq51gn.js} +13 -7
  62. package/dist/{cli-b9dd0h3k.js → cli-stwsmd6k.js} +3 -2
  63. package/dist/{cli-c2mk1kgf.js → cli-v4en75c5.js} +8 -1
  64. package/dist/{cli-z61ykvam.js → cli-wg04s9kr.js} +4 -3
  65. package/dist/cli-yn0a417a.js +4069 -0
  66. package/dist/{cli-zfzznn7c.js → cli-ytxs7mn1.js} +26 -2
  67. package/dist/{cli-4j8k1m4w.js → cli-zt1227k1.js} +1162 -192
  68. package/dist/cli.js +28 -35
  69. package/dist/{codex-adapter-fxmpsabw.js → codex-adapter-ttdm995v.js} +12 -11
  70. package/dist/{codex-hook-8jexc34x.js → codex-hook-8gx7bcz0.js} +3 -3
  71. package/dist/{codex-session-runner-rr1e47ea.js → codex-session-runner-3v8nh4c8.js} +12 -11
  72. package/dist/{commands-0f5rxz51.js → commands-1cxwbhhq.js} +7 -5
  73. package/dist/{db-r3jxf3wy.js → db-y9zwsr79.js} +25 -5
  74. package/dist/{dispatcher-06vph6q4.js → dispatcher-9m9zss36.js} +30 -26
  75. package/dist/dsh-adapter-gsb2qqf9.js +19 -0
  76. package/dist/{e2b-413b8t4v.js → e2b-93x1k0j5.js} +6 -7
  77. package/dist/{google-generative-ai-ndjmj5fa.js → google-generative-ai-gwwrnzqn.js} +33 -73
  78. package/dist/{google-vertex-fr665tqs.js → google-vertex-5wfawpwz.js} +38 -68
  79. package/dist/{handlers-0hgtc910.js → handlers-36fm1bwn.js} +33 -29
  80. package/dist/{hook-9kvrv1er.js → hook-pntw4r8h.js} +12 -10
  81. package/dist/{hook-fbfhkat3.js → hook-vwq8y26p.js} +13 -11
  82. package/dist/{http-xbyx8k5j.js → http-20jesakk.js} +357 -261
  83. package/dist/{index-jsw3aynb.js → index-6artrehg.js} +30 -26
  84. package/dist/{cli-pdz9qde4.js → index-9ch1n761.js} +10 -11
  85. package/dist/{index-rnyahfn5.js → index-9n5z2492.js} +30 -26
  86. package/dist/{index-66xz5e9j.js → index-h5gs261z.js} +521 -9
  87. package/dist/{index-tfsdxyv6.js → index-j5wrf1yw.js} +40 -25
  88. package/dist/{index-2sp0384z.js → index-kkeftv5h.js} +738 -21
  89. package/dist/{index-sgk50mbg.js → index-m2xqpz5r.js} +31 -27
  90. package/dist/{index-gpfn1741.js → index-m9ks8a7t.js} +9 -7
  91. package/dist/index-w097eg0x.js +5 -0
  92. package/dist/jiti-loader-b4837mtm.js +34 -0
  93. package/dist/jiti-static-loader-q2g5ds0y.js +31267 -0
  94. package/dist/{keepalive-s3xpr7sw.js → keepalive-pwxvxd2c.js} +9 -7
  95. package/dist/{lead-rq4n5r16.js → lead-m789wqa3.js} +46 -41
  96. package/dist/{maintenance-f0atj1ab.js → maintenance-bvg6w7e2.js} +10 -8
  97. package/dist/{mistral-conversations-qhr84ph3.js → mistral-conversations-zx0kn554.js} +24 -18
  98. package/dist/{node-x1g4v0pq.js → node-5qtycf6a.js} +16 -11
  99. package/dist/{oauth-refresh-sweep-q0t3bxre.js → oauth-refresh-sweep-6v4bd737.js} +8 -6
  100. package/dist/{onboard-br5j1kch.js → onboard-snckpcj1.js} +115 -35
  101. package/dist/{openai-codex-responses-wy6a43wd.js → openai-codex-responses-4vjqzvjp.js} +33 -27
  102. package/dist/{openai-completions-32rdwmze.js → openai-completions-pxsmym7g.js} +52 -64
  103. package/dist/{openai-responses-6xwf7bzg.js → openai-responses-tmn0rqp0.js} +23 -24
  104. package/dist/{opencode-adapter-28axqa09.js → opencode-adapter-qbmv4tpy.js} +6 -6
  105. package/dist/{otel-impl-vdqge2nt.js → otel-impl-e0ytens5.js} +4 -4
  106. package/dist/{pi-messages-b1mbr6gp.js → pi-messages-txpjj39q.js} +3 -3
  107. package/dist/pi-mono-adapter-c4ggemhz.js +1316 -0
  108. package/dist/{pricing-refresh-hb3691fd.js → pricing-refresh-52g2ywrw.js} +9 -7
  109. package/dist/{rbac-roles-t78xt39b.js → rbac-roles-5455kfdn.js} +8 -6
  110. package/dist/{rbac-roles-jh280k9g.js → rbac-roles-hbf0xa52.js} +9 -7
  111. package/dist/{render-v2-7p8r2ymg.js → render-v2-npm8xdzh.js} +16 -9
  112. package/dist/{seed-pricing-hf889enh.js → seed-pricing-650wbr1n.js} +8 -6
  113. package/dist/{setup-w6mb03jv.js → setup-k1mew5rf.js} +4 -5
  114. package/dist/virtual-modules-2q5b2pk4.js +54 -0
  115. package/dist/{work-objects-nt8gy1kj.js → work-objects-2ea3kvwg.js} +33 -29
  116. package/dist/{worker-dwy617t0.js → worker-ec6hvc4m.js} +46 -41
  117. package/openapi.json +109 -16
  118. package/package.json +7 -6
  119. package/plugin/opencode-plugins/lib/memory-raters.ts +7 -0
  120. package/src/be/automation-preflight.ts +6 -4
  121. package/src/be/db/agents.ts +75 -2
  122. package/src/be/db/tasks/read.ts +2 -0
  123. package/src/be/db/tasks/write.ts +7 -1
  124. package/src/be/db.ts +140 -18
  125. package/src/be/extensions/db.ts +35 -4
  126. package/src/be/memory/raters/llm.ts +2 -6
  127. package/src/be/memory/raters/noop.ts +2 -3
  128. package/src/be/memory/raters/registry.ts +4 -10
  129. package/src/be/migrations/157_defer_provenance_and_marker.sql +27 -0
  130. package/src/be/models-catalog.ts +2 -0
  131. package/src/be/modelsdev-cache.json +213 -0
  132. package/src/be/rbac-audit.ts +2 -0
  133. package/src/be/seed/agent-fs-provision.ts +23 -0
  134. package/src/be/seed-pricing.ts +3 -1
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/onboard/compose-generator.ts +16 -1
  137. package/src/commands/onboard/env-generator.ts +14 -1
  138. package/src/commands/onboard/manifest.ts +1 -0
  139. package/src/commands/onboard/steps/integration-slack.tsx +56 -16
  140. package/src/commands/onboard/steps/review.tsx +5 -0
  141. package/src/commands/onboard/types.ts +6 -0
  142. package/src/commands/onboard.tsx +5 -1
  143. package/src/commands/provider-credentials.ts +14 -2
  144. package/src/commands/runner.ts +18 -2
  145. package/src/extensions/identity.ts +15 -7
  146. package/src/extensions/lifecycle.ts +15 -21
  147. package/src/hooks/codex-hook.ts +7 -1
  148. package/src/http/agents.ts +12 -4
  149. package/src/http/config.ts +1 -1
  150. package/src/http/core.ts +5 -3
  151. package/src/http/extensions.ts +108 -34
  152. package/src/http/poll.ts +7 -0
  153. package/src/http/schedules.ts +10 -0
  154. package/src/http/script-runs.ts +35 -26
  155. package/src/http/status.ts +24 -11
  156. package/src/http/tasks.ts +6 -1
  157. package/src/prompts/base-prompt.ts +15 -7
  158. package/src/prompts/memories.ts +5 -7
  159. package/src/prompts/session-templates.ts +3 -2
  160. package/src/prompts/steering-delivery.ts +2 -0
  161. package/src/providers/claude-managed-models.ts +8 -0
  162. package/src/providers/codex-adapter.ts +3 -6
  163. package/src/providers/codex-models.ts +15 -1
  164. package/src/providers/dsh-adapter.ts +248 -0
  165. package/src/providers/index.ts +5 -1
  166. package/src/providers/modelsdev-reasoning.json +50 -0
  167. package/src/providers/pi-mono-extension.ts +2 -5
  168. package/src/rbac/legacy-policy.ts +15 -1
  169. package/src/rbac/types.ts +2 -0
  170. package/src/server.ts +7 -1
  171. package/src/slack/ack.ts +43 -8
  172. package/src/slack/app.ts +32 -10
  173. package/src/slack/config.ts +29 -0
  174. package/src/slack/handlers.ts +7 -1
  175. package/src/slack/render-v2.ts +186 -17
  176. package/src/slack/responses.ts +1 -3
  177. package/src/slack/task-output.ts +66 -8
  178. package/src/slack/thread-buffer.ts +27 -3
  179. package/src/telemetry.ts +2 -3
  180. package/src/tests/agent-fs-provision-seeder.test.ts +56 -2
  181. package/src/tests/agents-harness-provider.test.ts +19 -0
  182. package/src/tests/agents-list-model-display.test.ts +60 -1
  183. package/src/tests/base-prompt.test.ts +25 -0
  184. package/src/tests/claude-managed-adapter.test.ts +6 -0
  185. package/src/tests/codex-gpt6-release.test.ts +97 -0
  186. package/src/tests/codex-steering-hook.test.ts +2 -2
  187. package/src/tests/config-api-only-keys.test.ts +9 -0
  188. package/src/tests/context-window.test.ts +1 -0
  189. package/src/tests/db-task-read-contract.test.ts +9 -0
  190. package/src/tests/db-task-write-contract.test.ts +9 -0
  191. package/src/tests/defer-task.test.ts +115 -16
  192. package/src/tests/dsh-adapter.test.ts +276 -0
  193. package/src/tests/e2e-nightly-report.test.ts +3 -3
  194. package/src/tests/e2e-redact.test.ts +2 -0
  195. package/src/tests/env-flag.test.ts +6 -0
  196. package/src/tests/extension-agent-visibility.test.ts +474 -0
  197. package/src/tests/extensions-db.test.ts +41 -0
  198. package/src/tests/extensions-http.test.ts +148 -4
  199. package/src/tests/extensions-lifecycle.test.ts +21 -1
  200. package/src/tests/extensions-mcp-tools.test.ts +9 -5
  201. package/src/tests/fetch-resolved-env.test.ts +43 -0
  202. package/src/tests/internal-ai/complete-structured.test.ts +32 -9
  203. package/src/tests/internal-ai/credentials.test.ts +4 -4
  204. package/src/tests/internal-ai/models.test.ts +20 -0
  205. package/src/tests/memory-rate-tool.test.ts +7 -10
  206. package/src/tests/memory-rater-defaults.test.ts +23 -0
  207. package/src/tests/memory-rater-e2e.test.ts +2 -3
  208. package/src/tests/migration-046-budgets.test.ts +2 -0
  209. package/src/tests/model-control.test.ts +2 -2
  210. package/src/tests/onboard-compose.test.ts +14 -0
  211. package/src/tests/onboard-env.test.ts +17 -0
  212. package/src/tests/onboard-manifest.test.ts +12 -0
  213. package/src/tests/opus-5-5-registry.test.ts +48 -0
  214. package/src/tests/pricing-refresh.test.ts +1 -1
  215. package/src/tests/pricing-routes.test.ts +15 -3
  216. package/src/tests/provider-registration.test.ts +5 -2
  217. package/src/tests/rbac-admission-e2e.test.ts +54 -0
  218. package/src/tests/rbac-e2e-helpers.ts +2 -0
  219. package/src/tests/rbac-engine.test.ts +32 -0
  220. package/src/tests/rbac-wire-e2e.test.ts +33 -0
  221. package/src/tests/reasoning-effort.test.ts +8 -0
  222. package/src/tests/reload-config.test.ts +33 -0
  223. package/src/tests/script-connections.test.ts +3 -0
  224. package/src/tests/session-costs-model-key-normalize.test.ts +1 -0
  225. package/src/tests/slack-config.test.ts +136 -0
  226. package/src/tests/slack-mode-lifecycle.test.ts +69 -0
  227. package/src/tests/slack-render-v2.test.ts +446 -16
  228. package/src/tests/slack-steering.test.ts +69 -1
  229. package/src/tests/slack-task-output.test.ts +33 -12
  230. package/src/tests/status.test.ts +22 -2
  231. package/src/tests/steering-core.test.ts +35 -0
  232. package/src/tests/steering-delivery.test.ts +39 -0
  233. package/src/tests/steering-transport.test.ts +2 -2
  234. package/src/tests/task-tool-preload-mcp.test.ts +3 -1
  235. package/src/tests/telemetry-init.test.ts +18 -0
  236. package/src/tests/workflow-live-recovery.test.ts +126 -0
  237. package/src/tests/workflow-llm.test.ts +3 -3
  238. package/src/tools/defer-task.ts +64 -26
  239. package/src/tools/extension-common.ts +4 -0
  240. package/src/tools/extension-install.ts +1 -1
  241. package/src/tools/extension-list.ts +4 -3
  242. package/src/tools/join-swarm.ts +1 -1
  243. package/src/tools/schedules/create-schedule.ts +11 -1
  244. package/src/tools/schedules/patch-schedule.ts +5 -0
  245. package/src/tools/schedules/update-schedule.ts +5 -0
  246. package/src/tools/send-task.ts +6 -0
  247. package/src/tools/utils.ts +1 -1
  248. package/src/types.ts +30 -8
  249. package/src/utils/context-window.ts +1 -0
  250. package/src/utils/credentials.ts +8 -0
  251. package/src/utils/internal-ai/complete-structured.ts +8 -0
  252. package/src/utils/internal-ai/models.ts +5 -5
  253. package/src/utils/memory-raters.ts +3 -0
  254. package/src/workflows/engine.ts +28 -0
  255. package/src/workflows/recovery.ts +7 -4
  256. package/templates/schedules/daily-status-report/content.md +25 -12
  257. package/templates/schedules/daily-workflow-health-audit/content.md +1 -1
  258. package/templates/skills/swarm-extensions/content.md +9 -5
  259. package/dist/cli-4wppfkcm.js +0 -16384
  260. package/dist/cli-55qrx3q8.js +0 -6041
  261. package/dist/cli-cg4vnyh4.js +0 -305
  262. package/dist/cli-jyetr64j.js +0 -52
  263. package/dist/cli-pp9ax9pa.js +0 -520
  264. package/dist/cli-qa66qaf0.js +0 -6076
  265. package/dist/cli-qghpvca2.js +0 -590
  266. package/dist/cli-rembwgt8.js +0 -861
  267. package/dist/cli-tcvj7j2r.js +0 -1971
  268. package/dist/cli-vfcth11k.js +0 -36
  269. package/dist/cli-w1d1bgkk.js +0 -1422
  270. package/dist/cli-y6x0gqtg.js +0 -5977
  271. package/dist/es5-tj3tn4jj.js +0 -844
  272. package/dist/index-1kq5hw2p.js +0 -467
  273. package/dist/index-1p8wdstt.js +0 -7
  274. package/dist/index-8fdjzb2f.js +0 -100
  275. package/dist/index-bxmwzbgw.js +0 -365
  276. package/dist/index-nzerxd5z.js +0 -7
  277. package/dist/index-pyymnzat.js +0 -1168
  278. package/dist/index-qv6atbab.js +0 -619
  279. package/dist/index-rja32qa7.js +0 -801
  280. package/dist/index-tzg5srfp.js +0 -211
  281. package/dist/index-vhncw983.js +0 -90
  282. package/dist/index-xk036zsk.js +0 -859
  283. package/dist/index-zyazbzcb.js +0 -6
  284. package/dist/{claude-managed-setup-48dwg9bv.js → claude-managed-setup-f5pmsyrp.js} +3 -3
@@ -18,7 +18,6 @@ import { getTaskOutputValidationError } from "@/tasks/terminal-result-guard";
18
18
  import { assertOwnsTask, ownerCtx } from "@/tools/task-tool-ctx";
19
19
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
20
20
  import { isTerminalTaskStatus } from "@/types";
21
- import { getAppUrl } from "@/utils/constants";
22
21
 
23
22
  /** Thrown inside the transaction to abort and roll back the schedule INSERT. */
24
23
  class DeferAbortedError extends Error {}
@@ -55,7 +54,7 @@ function partsInTz(date: Date, tz: string) {
55
54
  }
56
55
 
57
56
  /**
58
- * "today"/"tomorrow" or "MM-dd" plus "HH:MM:ss", both computed in `tz`.
57
+ * "today"/"tomorrow" or "MM-dd" plus "HH:MM", both computed in `tz`.
59
58
  * Falls back to UTC (parts still computed correctly) if `tz` is not a valid
60
59
  * IANA zone — the caller decides whether to label the fallback.
61
60
  */
@@ -71,7 +70,9 @@ function formatDeferralDateTime(
71
70
  zone = "UTC";
72
71
  }
73
72
  const t = partsInTz(target, zone);
74
- const time = `${t.hour}:${t.minute}:${t.second}`;
73
+ // Minute precision: this string is read by a human deciding whether to wait,
74
+ // and the wake-up is scheduler-polled anyway, so seconds were false precision.
75
+ const time = `${t.hour}:${t.minute}`;
75
76
  const n = partsInTz(now, zone);
76
77
  if (t.year === n.year && t.month === n.month && t.day === n.day) return { date: "today", time };
77
78
  const tm = partsInTz(new Date(now.getTime() + 86_400_000), zone);
@@ -98,38 +99,59 @@ async function resolveRequesterTimezone(
98
99
  return { tz: "UTC", isFallback: true };
99
100
  }
100
101
 
101
- /** First line of `note`, `Pending:` prefix stripped, capped to ~100 chars. */
102
- function renderShortDesc(note: string, max = 100): string {
103
- const firstLine = (note.split("\n")[0] ?? "").trim().replace(/^pending:\s*/i, "");
104
- if (firstLine.length <= max) return firstLine;
105
- return `${firstLine.slice(0, max - 1).trimEnd()}…`;
102
+ /** "Researcher", "Researcher and Picateclas", "A, B and C". */
103
+ function joinNames(names: string[]): string {
104
+ if (names.length < 2) return names[0] ?? "";
105
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
106
+ }
107
+
108
+ /**
109
+ * Who a `wakeOn` deferral is waiting on, in the card's words: the distinct
110
+ * agent names behind the watched tasks, or a bare count when none of them is
111
+ * assigned (an unassigned watched task has no name a human would recognise).
112
+ */
113
+ export function renderWaitingOn(agentNames: string[], watchedCount: number): string {
114
+ const named = joinNames([...new Set(agentNames.filter((name) => name.trim()))]);
115
+ if (named) return named;
116
+ return `${watchedCount} task${watchedCount === 1 ? "" : "s"}`;
117
+ }
118
+
119
+ /** "today at 18:38" / "tomorrow at 18:38" / "on 09-24 at 18:38". */
120
+ function renderWhen(nextRunAt: string, tz: string, isFallbackTz: boolean): string {
121
+ const { date, time } = formatDeferralDateTime(new Date(nextRunAt), new Date(), tz);
122
+ const timeLabel = isFallbackTz ? `${time} UTC` : time;
123
+ const day = date === "today" || date === "tomorrow" ? date : `on ${date}`;
124
+ return `${day} at ${timeLabel}`;
106
125
  }
107
126
 
108
127
  /**
109
128
  * Human-facing deferral text for tasks without an outputSchema — this is what
110
- * lands verbatim in a human's Slack thread as the task's terminal output.
111
- * One line: `Deferred until {date} {time} ([shortId](url)) -> {short desc}`.
112
- * No ISO timestamp, no bare UUID, no checks list, no `Pending:` prefix —
113
- * those stay in the task log. The `[text](url)` link is plain GFM markdown:
114
- * `markdownToSlack` (src/slack/blocks.ts) already down-converts it to a
115
- * readable `text (url)` fallback for Slack, and it reads fine verbatim in
116
- * the non-Slack task-output row / UI too.
129
+ * lands verbatim in a human's Slack thread as the task's terminal output, and
130
+ * in the UI's task-detail output row.
131
+ *
132
+ * It answers one question — when does this come back — in the two shapes a
133
+ * deferral actually has:
134
+ *
135
+ * time-based (no wakeOn) `Checking back today at 18:38`
136
+ * event-based (wakeOn) `Waiting on Researcher — or today at 18:38 at the latest`
137
+ *
138
+ * A wakeOn deferral always carries a time ceiling too (`delayMs`/`runAt` is
139
+ * required), so the "both" wording is the only event shape reachable — which
140
+ * is also the honest one: the event resumes it, the ceiling guarantees it.
141
+ *
142
+ * Deliberately absent: the agent's `note` (written for the wake-up agent, not
143
+ * for a human — it goes to the task log and the wake-up task's template), the
144
+ * `checks` list, the schedule UUID and its app.agent-swarm.dev link. The glyph
145
+ * is added by the Slack renderer, matching how ✅/❌ outcomes are composed.
117
146
  */
118
147
  function renderHumanFacingDeferral(
119
- note: string,
120
148
  nextRunAt: string,
121
- scheduleId: string,
122
149
  tz: string,
123
150
  isFallbackTz: boolean,
151
+ waitingOn: string | undefined,
124
152
  ): string {
125
- const now = new Date();
126
- const target = new Date(nextRunAt);
127
- const { date, time } = formatDeferralDateTime(target, now, tz);
128
- const timeLabel = isFallbackTz ? `${time} UTC` : time;
129
- const shortId = scheduleId.slice(0, 8);
130
- const link = `[${shortId}](${getAppUrl()}/schedules/${scheduleId})`;
131
- const shortDesc = renderShortDesc(note);
132
- return `Deferred until ${date} ${timeLabel} (${link}) -> ${shortDesc}`;
153
+ const when = renderWhen(nextRunAt, tz, isFallbackTz);
154
+ return waitingOn ? `Waiting on ${waitingOn} — or ${when} at the latest` : `Checking back ${when}`;
133
155
  }
134
156
 
135
157
  export const registerDeferTaskTool = (server: McpServer) => {
@@ -284,6 +306,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
284
306
 
285
307
  try {
286
308
  const committed = await getDbClient().transaction(async () => {
309
+ const watchedAgentNames: string[] = [];
287
310
  for (const watchedId of watchedTaskIds) {
288
311
  if (watchedId === taskId)
289
312
  throw new DeferAbortedError("Cannot wake on the task being deferred.");
@@ -293,6 +316,11 @@ export const registerDeferTaskTool = (server: McpServer) => {
293
316
  throw new DeferAbortedError(
294
317
  `Watched task ${watchedId} is already ${watched.status}; read its result instead of deferring.`,
295
318
  );
319
+ // Resolved now, not at render time: the card is baked into the
320
+ // task's terminal output, and an agent can be renamed or removed
321
+ // between the deferral and whoever reads the thread later.
322
+ const watchedAgent = watched.agentId ? await getAgentById(watched.agentId) : null;
323
+ if (watchedAgent?.name) watchedAgentNames.push(watchedAgent.name);
296
324
  }
297
325
  const schedule = await createScheduledTask({
298
326
  // Unique name (`getScheduledTaskByName` is a unique lookup). The UUID
@@ -308,6 +336,10 @@ export const registerDeferTaskTool = (server: McpServer) => {
308
336
  taskType: "deferred",
309
337
  tags: ["deferred"],
310
338
  priority: task.priority,
339
+ // Provenance: `nextRunAt` is cleared once this one_time schedule
340
+ // fires, so the requested delay is only recoverable from here.
341
+ requestedDelayMs: delayMs,
342
+ requestedRunAt: runAt,
311
343
  // No `model`: a concrete provider model pinned now can be
312
344
  // incompatible with the assignee/provider at wake-up, especially
313
345
  // after a delay. Only the portable modelTier travels — mirrors
@@ -344,9 +376,15 @@ export const registerDeferTaskTool = (server: McpServer) => {
344
376
 
345
377
  const terminalOutput = task.outputSchema
346
378
  ? output!
347
- : renderHumanFacingDeferral(note, nextRunAt, schedule.id, requesterTz, isFallbackTz);
379
+ : renderHumanFacingDeferral(
380
+ nextRunAt,
381
+ requesterTz,
382
+ isFallbackTz,
383
+ wakeOn ? renderWaitingOn(watchedAgentNames, watchedTaskIds.length) : undefined,
384
+ );
348
385
  const completed = await completeTask(taskId, terminalOutput, {
349
386
  addTags: ["deferred"],
387
+ deferredAt: new Date().toISOString(),
350
388
  });
351
389
  if (!completed) {
352
390
  // Another writer terminally completed/failed/cancelled this task
@@ -10,6 +10,7 @@ const extensionListItemSchema = z.looseObject({
10
10
  name: z.string().optional(),
11
11
  version: z.number().optional(),
12
12
  activeVersion: z.number().optional(),
13
+ createdByAgentId: z.string().nullable().optional(),
13
14
  enabled: z.boolean().optional(),
14
15
  status: z.string().optional(),
15
16
  priority: z.number().optional(),
@@ -21,6 +22,7 @@ export const extensionToolOutputSchema = swarmToolOutputSchema({
21
22
  name: z.string().optional(),
22
23
  version: z.number().optional(),
23
24
  activeVersion: z.number().optional(),
25
+ createdByAgentId: z.string().nullable().optional(),
24
26
  enabled: z.boolean().optional(),
25
27
  status: z.string().optional(),
26
28
  priority: z.number().optional(),
@@ -38,6 +40,8 @@ export function coerceExtensionSummary(entry: unknown) {
38
40
  const extension = isRecord(entry) ? entry : {};
39
41
  return {
40
42
  id: typeof extension.id === "string" ? extension.id : undefined,
43
+ createdByAgentId:
44
+ typeof extension.createdByAgentId === "string" ? extension.createdByAgentId : null,
41
45
  name: typeof extension.name === "string" ? extension.name : undefined,
42
46
  version: typeof extension.version === "number" ? extension.version : undefined,
43
47
  activeVersion:
@@ -9,7 +9,7 @@ import {
9
9
  } from "./extension-common";
10
10
 
11
11
  export const EXTENSION_INSTALL_DESCRIPTION =
12
- "Validate and store a TypeScript extension bundle (manifest plus a files map with one hooks.ts). Fetch the hook contract first: GET /api/extensions/type-defs returns swarm-extension.d.ts with every event and modify shape. The extension remains disabled until a lead, operator, or dashboard user enables it with extension-enable.";
12
+ "Any authenticated agent may create and install its own TypeScript extension bundle (manifest plus a files map with one hooks.ts). Fetch the hook contract first: GET /api/extensions/type-defs returns swarm-extension.d.ts with every event and modify shape. The installing agent is recorded as createdByAgentId. Workers may install subsequent versions only for their own extensions; those versions remain inactive. A new extension remains disabled until a lead, operator, or dashboard user enables it with extension-enable.";
13
13
 
14
14
  function extensionInstallResult(data: unknown, fallbackName: string) {
15
15
  const body = data as {
@@ -24,11 +24,12 @@ function extensionListItems(data: unknown): ExtensionListItem[] {
24
24
  }
25
25
 
26
26
  function renderExtensionsTable(extensions: ExtensionListItem[]): string {
27
- const header = "| Name | Version | Active | Enabled | Status | Priority | Failures |";
28
- const divider = "| --- | ---: | ---: | --- | --- | ---: | ---: |";
27
+ const header =
28
+ "| Name | Owner agent | Version | Active | Enabled | Status | Priority | Failures |";
29
+ const divider = "| --- | --- | ---: | ---: | --- | --- | ---: | ---: |";
29
30
  const rows = extensions.map(
30
31
  (extension) =>
31
- `| ${extension.name ?? "?"} | ${extension.version ?? "?"} | ${extension.activeVersion ?? "?"} | ${extension.enabled ?? false} | ${extension.status ?? "unknown"} | ${extension.priority ?? "?"} | ${extension.consecutiveFailures ?? 0} |`,
32
+ `| ${extension.name ?? "?"} | ${extension.createdByAgentId ?? "operator/user"} | ${extension.version ?? "?"} | ${extension.activeVersion ?? "?"} | ${extension.enabled ?? false} | ${extension.status ?? "unknown"} | ${extension.priority ?? "?"} | ${extension.consecutiveFailures ?? 0} |`,
32
33
  );
33
34
  return [header, divider, ...rows].join("\n");
34
35
  }
@@ -84,7 +84,7 @@ export const registerJoinSwarmTool = (server: McpServer) => {
84
84
 
85
85
  try {
86
86
  const agent = await getDbClient().transaction(async () => {
87
- const agents = await getAllAgents();
87
+ const agents = await getAllAgents({ includeExtensions: true });
88
88
 
89
89
  const existingIdAgent = agents.find((agent) => agent.id === agentId);
90
90
 
@@ -3,7 +3,14 @@ import { CronExpressionParser } from "cron-parser";
3
3
  import * as z from "zod";
4
4
  import { authorizeAssetKeyWrite } from "@/be/asset-key-auth";
5
5
  import { resolveTaskAuditUserId } from "@/be/audit-user";
6
- import { createScheduledTask, getAgentById, getScheduledTaskByName, getWorkflow } from "@/be/db";
6
+ import {
7
+ createScheduledTask,
8
+ extensionAgentAssignmentError,
9
+ getAgentById,
10
+ getScheduledTaskByName,
11
+ getWorkflow,
12
+ isExtensionAgent,
13
+ } from "@/be/db";
7
14
  import { getScript } from "@/be/scripts/db";
8
15
  import { calculateNextRun } from "@/scheduler";
9
16
  import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
@@ -229,6 +236,9 @@ export const registerCreateScheduleTool = (server: McpServer) => {
229
236
  if (!agent) {
230
237
  return toolErr(`Target agent not found: ${targetAgentId}`);
231
238
  }
239
+ if (isExtensionAgent(agent)) {
240
+ return toolErr(extensionAgentAssignmentError(agent));
241
+ }
232
242
  }
233
243
 
234
244
  // Cross-field targetType validation
@@ -4,10 +4,12 @@ import * as z from "zod";
4
4
  import { authorizeAssetKeyWrite } from "@/be/asset-key-auth";
5
5
  import { resolveTaskAuditUserId } from "@/be/audit-user";
6
6
  import {
7
+ extensionAgentAssignmentError,
7
8
  getAgentById,
8
9
  getScheduledTaskById,
9
10
  getScheduledTaskByName,
10
11
  getWorkflow,
12
+ isExtensionAgent,
11
13
  updateScheduledTask,
12
14
  } from "@/be/db";
13
15
  import { mergeScheduleTiming, validateRecurringTiming } from "@/be/schedules/validate";
@@ -187,6 +189,9 @@ export const registerPatchScheduleTool = (server: McpServer) => {
187
189
  if (!agent) {
188
190
  return toolErr(`Target agent not found: ${targetAgentId}`);
189
191
  }
192
+ if (isExtensionAgent(agent)) {
193
+ return toolErr(extensionAgentAssignmentError(agent));
194
+ }
190
195
  }
191
196
 
192
197
  // Check if new name conflicts with existing
@@ -4,10 +4,12 @@ import * as z from "zod";
4
4
  import { authorizeAssetKeyWrite } from "@/be/asset-key-auth";
5
5
  import { resolveTaskAuditUserId } from "@/be/audit-user";
6
6
  import {
7
+ extensionAgentAssignmentError,
7
8
  getAgentById,
8
9
  getScheduledTaskById,
9
10
  getScheduledTaskByName,
10
11
  getWorkflow,
12
+ isExtensionAgent,
11
13
  updateScheduledTask,
12
14
  } from "@/be/db";
13
15
  import { mergeScheduleTiming, validateRecurringTiming } from "@/be/schedules/validate";
@@ -186,6 +188,9 @@ export const registerUpdateScheduleTool = (server: McpServer) => {
186
188
  if (!agent) {
187
189
  return toolErr(`Target agent not found: ${targetAgentId}`);
188
190
  }
191
+ if (isExtensionAgent(agent)) {
192
+ return toolErr(extensionAgentAssignmentError(agent));
193
+ }
189
194
  }
190
195
 
191
196
  // Check if new name conflicts with existing
@@ -4,6 +4,7 @@ import { AssetKeyAuthorizationError, authorizeAssetKeyWrite } from "@/be/asset-k
4
4
  import { resolveTaskAuditUserId } from "@/be/audit-user";
5
5
  import {
6
6
  createTaskExtended,
7
+ extensionAgentAssignmentError,
7
8
  findCompletedTaskInThread,
8
9
  findExistingLinearTrackerContextWork,
9
10
  findRecentCancelledTaskInThread,
@@ -13,6 +14,7 @@ import {
13
14
  getTaskById,
14
15
  getUserById,
15
16
  hasCapacity,
17
+ isExtensionAgent,
16
18
  } from "@/be/db";
17
19
  import { repointTrackerSyncBySwarmId } from "@/be/db-queries/tracker";
18
20
  import { applyPreTaskCreate } from "@/extensions/apply-task-create";
@@ -562,6 +564,10 @@ export async function sendTaskHandler(
562
564
  };
563
565
  }
564
566
 
567
+ if (isExtensionAgent(agent)) {
568
+ return { success: false, message: extensionAgentAssignmentError(agent) };
569
+ }
570
+
565
571
  if (taskOptions.routingAffinity?.leadOnly && !agent.isLead) {
566
572
  return {
567
573
  success: false,
@@ -275,7 +275,7 @@ const storeProgressBlockedWaitingNudge = (r: SwarmToolResult): string | undefine
275
275
  if (!r.ok) return undefined;
276
276
  const ms = (r.data as { blockedWaitingElapsedMs?: unknown } | undefined)?.blockedWaitingElapsedMs;
277
277
  if (typeof ms !== "number") return undefined;
278
- return `That reads as blocked-waiting, ${formatIdleDuration(ms)} since your last update — call defer-task so a wake-up task resumes you instead of polling manually.`;
278
+ return `That reads as blocked-waiting, ${formatIdleDuration(ms)} since your last update — keep working if you can, and if the wait is genuinely long, defer-task is one way to resume later instead of polling.`;
279
279
  };
280
280
 
281
281
  const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined => {
package/src/types.ts CHANGED
@@ -136,16 +136,22 @@ export const DEFAULT_MODEL_TIER_MAP: Record<ProviderName, Record<ModelTier, stri
136
136
  ultra: "gpt-5.6-sol",
137
137
  },
138
138
  pi: {
139
- smol: "openrouter/deepseek/deepseek-v4-flash",
140
- regular: "openrouter/deepseek/deepseek-v4-flash",
141
- smart: "openrouter/deepseek/deepseek-v4-pro",
142
- ultra: "openrouter/anthropic/claude-opus-4.8",
139
+ smol: "openrouter/deepseek/deepseek-v4.1-flash",
140
+ regular: "openrouter/deepseek/deepseek-v4.1-flash",
141
+ smart: "openrouter/deepseek/deepseek-v4-pro-0813",
142
+ ultra: "openrouter/anthropic/claude-opus-5.5",
143
143
  },
144
144
  opencode: {
145
- smol: "openrouter/deepseek/deepseek-v4-flash",
146
- regular: "openrouter/deepseek/deepseek-v4-flash",
147
- smart: "openrouter/deepseek/deepseek-v4-pro",
148
- ultra: "openrouter/anthropic/claude-opus-4.8",
145
+ smol: "openrouter/deepseek/deepseek-v4.1-flash",
146
+ regular: "openrouter/deepseek/deepseek-v4.1-flash",
147
+ smart: "openrouter/deepseek/deepseek-v4-pro-0813",
148
+ ultra: "openrouter/anthropic/claude-opus-5.5",
149
+ },
150
+ dsh: {
151
+ smol: "openrouter/deepseek/deepseek-v4.1-flash",
152
+ regular: "openrouter/deepseek/deepseek-v4.1-flash",
153
+ smart: "openrouter/deepseek/deepseek-v4-pro-0813",
154
+ ultra: "openrouter/anthropic/claude-opus-5.5",
149
155
  },
150
156
  devin: {
151
157
  smol: "devin",
@@ -367,6 +373,7 @@ export const ProviderNameSchema = z.enum([
367
373
  "claude-managed",
368
374
  "opencode",
369
375
  "acp",
376
+ "dsh",
370
377
  ]);
371
378
  export type ProviderName = z.infer<typeof ProviderNameSchema>;
372
379
 
@@ -401,6 +408,8 @@ export const SteeringMessageSchema = z
401
408
  status: SteeringStatusSchema,
402
409
  deliveredMode: SteerModeSchema.optional(),
403
410
  source: SteeringSourceSchema,
411
+ /** Display label resolved by the API from the sender identity. */
412
+ senderLabel: z.string().optional(),
404
413
  createdByKind: z.enum(["user", "agent", "system"]),
405
414
  createdByUserId: z.string().optional(),
406
415
  createdByAgentId: z.string().optional(),
@@ -455,6 +464,7 @@ export const PROVIDER_STEER_CAPABILITIES: Record<ProviderName, SteerMode[]> = {
455
464
  // single in-flight turn and the only interrupt is `session/cancel` (abort).
456
465
  // Advertise nothing rather than promise semantics we can't honor.
457
466
  acp: [],
467
+ dsh: [],
458
468
  };
459
469
 
460
470
  export type DevinProviderMeta = {
@@ -474,6 +484,7 @@ export type ProviderMetaMap = {
474
484
  "claude-managed": NoProviderMeta;
475
485
  opencode: NoProviderMeta;
476
486
  acp: NoProviderMeta;
487
+ dsh: NoProviderMeta;
477
488
  };
478
489
 
479
490
  export const FollowUpConfigSchema = z
@@ -624,6 +635,12 @@ export const AgentTaskSchema = z
624
635
  // Pause tracking
625
636
  wasPaused: z.boolean().default(false),
626
637
 
638
+ // Set by `defer-task` when this task ends in a deferral. The task's status
639
+ // is `completed` either way, so this is what tells "parked, resuming in a
640
+ // wake-up task" apart from "done" — see `isDeferredTask` in
641
+ // src/slack/task-output.ts.
642
+ deferredAt: z.iso.datetime().optional(),
643
+
627
644
  // Context usage aggregates
628
645
  compactionCount: z.number().int().min(0).optional(),
629
646
  peakContextPercent: z.number().min(0).max(100).optional(),
@@ -1644,6 +1661,11 @@ export const ScheduledTaskSchema = z
1644
1661
  // Set only by `defer-task`: the task this schedule wakes up to continue.
1645
1662
  // Passed through as the created task's `parentTaskId`.
1646
1663
  parentTaskId: z.string().optional(),
1664
+ // Set only by `defer-task`: what the caller asked for, kept because the
1665
+ // scheduler clears `nextRunAt` once a one_time schedule fires. Exactly one
1666
+ // of the two is present. Provenance only — never read to schedule a run.
1667
+ requestedDelayMs: z.number().int().positive().optional(),
1668
+ requestedRunAt: z.iso.datetime().optional(),
1647
1669
  timezone: z.string().default("UTC"),
1648
1670
  consecutiveErrors: z.number().int().min(0).default(0),
1649
1671
  lastErrorAt: z.iso.datetime().optional(),
@@ -29,6 +29,7 @@ const CONTEXT_WINDOW_DEFAULTS: Record<string, number> = {
29
29
  // Anthropic Fable / Mythos tier
30
30
  "claude-fable-5-1": 1_000_000,
31
31
  "claude-mythos-5-1": 1_000_000,
32
+ "claude-opus-5-5": 1_000_000,
32
33
  "claude-opus-5": 1_000_000,
33
34
  "claude-fable-5": 1_000_000,
34
35
  "claude-mythos-5": 1_000_000,
@@ -8,6 +8,7 @@ export const CREDENTIAL_POOL_VARS = [
8
8
  "OPENAI_API_KEY",
9
9
  "CODEX_OAUTH",
10
10
  "DEVIN_API_KEY",
11
+ "DEEPSEEK_API_KEY",
11
12
  ] as const;
12
13
 
13
14
  /**
@@ -26,6 +27,7 @@ export const PROVIDER_CREDENTIAL_VARS: Record<string, readonly string[]> = {
26
27
  pi: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
27
28
  codex: ["OPENAI_API_KEY", "CODEX_OAUTH"],
28
29
  devin: ["DEVIN_API_KEY"],
30
+ dsh: ["OPENROUTER_API_KEY", "DEEPSEEK_API_KEY"],
29
31
  opencode: ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
30
32
  };
31
33
 
@@ -57,6 +59,10 @@ const SLASH_MODEL_PROVIDERS = new Set(["opencode", "pi"]);
57
59
  export function getModelAwareCredentialVars(provider: string, model?: string): readonly string[] {
58
60
  const base = PROVIDER_CREDENTIAL_VARS[provider];
59
61
  if (!base) return CREDENTIAL_POOL_VARS;
62
+ // dsh has exact prefix routing; the generic slash rule only filters OpenAI keys.
63
+ if (provider === "dsh" && model) {
64
+ return model.startsWith("openrouter/") ? ["OPENROUTER_API_KEY"] : ["DEEPSEEK_API_KEY"];
65
+ }
60
66
  if (!SLASH_MODEL_PROVIDERS.has(provider) || !model) return base;
61
67
  if (model.includes("/")) {
62
68
  return base.filter((v) => v !== "OPENAI_API_KEY");
@@ -82,6 +88,8 @@ export function deriveProviderFromKeyType(keyType: string): string {
82
88
  case "OPENAI_API_KEY":
83
89
  case "CODEX_OAUTH":
84
90
  return "codex";
91
+ case "DEEPSEEK_API_KEY":
92
+ return "dsh";
85
93
  case "DEVIN_API_KEY":
86
94
  return "devin";
87
95
  default:
@@ -284,6 +284,14 @@ export async function completeStructured<TZod extends z.ZodTypeAny>(
284
284
  return null;
285
285
  }
286
286
 
287
+ // Dynamic IDs can be absent even though the typed catalog overload is non-nullable.
288
+ if (!model) {
289
+ console.error(
290
+ scrubSecrets(`internal-ai: unknown model ${provider}/${modelId} (callerTag=${callerTag})`),
291
+ );
292
+ return null;
293
+ }
294
+
287
295
  // The builtin catalog hardcodes openrouter.ai; OPENROUTER_BASE_URL reroutes
288
296
  // through a gateway (pi-ai's stream path uses `model.baseUrl` verbatim).
289
297
  // Resolved from the caller's session env when provided — swarm_config
@@ -17,10 +17,10 @@ export type CredentialKind = "openrouter" | "anthropic" | "openai" | "openai-cod
17
17
  * shellout, not pi-ai's `getModel`.
18
18
  */
19
19
  export const DEFAULT_MODEL: Record<CredentialKind, string> = {
20
- openrouter: "openrouter/google/gemini-3-flash-preview",
20
+ openrouter: "openrouter/deepseek/deepseek-v4.1-flash",
21
21
  anthropic: "anthropic/claude-haiku-4-5",
22
- openai: "openai/gpt-5.4-mini",
23
- "openai-codex": "openai-codex/gpt-5.4-mini",
22
+ openai: "openai/gpt-6-luna",
23
+ "openai-codex": "openai-codex/gpt-6-luna",
24
24
  "claude-cli": "haiku",
25
25
  };
26
26
 
@@ -35,8 +35,8 @@ export function resolveModelString(kind: CredentialKind): string {
35
35
 
36
36
  /**
37
37
  * Split a `provider/model-id` string on the FIRST `/` so that OpenRouter
38
- * compound IDs like `openrouter/google/gemini-3-flash-preview` parse as
39
- * `("openrouter", "google/gemini-3-flash-preview")`. Mirrors the existing
38
+ * compound IDs like `openrouter/deepseek/deepseek-v4.1-flash` parse as
39
+ * `("openrouter", "deepseek/deepseek-v4.1-flash")`. Mirrors the existing
40
40
  * convention in `src/providers/pi-mono-adapter.ts:161-170`.
41
41
  */
42
42
  export function parseModelStr(modelStr: string): [provider: string, modelId: string] {
@@ -0,0 +1,3 @@
1
+ // Keep the dependency-free resolver in the plugin bundle so opencode's isolated
2
+ // runtime and the API/other workers execute the same defaults.
3
+ export { getMemoryRaterNames } from "../../plugin/opencode-plugins/lib/memory-raters";
@@ -198,6 +198,14 @@ interface StepResult {
198
198
  successors: WorkflowNode[];
199
199
  }
200
200
 
201
+ // A run may have overlapping resume walks. Keep ownership until the last walk
202
+ // settles, including checkpointing and routing between executor calls.
203
+ const activeWalks = new Map<string, number>();
204
+
205
+ export function isWorkflowRunActive(runId: string): boolean {
206
+ return activeWalks.has(runId);
207
+ }
208
+
201
209
  /**
202
210
  * Event-loop style graph walker.
203
211
  *
@@ -214,6 +222,26 @@ export async function walkGraph(
214
222
  workflowId?: string,
215
223
  secretKeys: Set<string> = new Set(),
216
224
  options: WorkflowExecutionOptions = {},
225
+ ): Promise<void> {
226
+ activeWalks.set(runId, (activeWalks.get(runId) ?? 0) + 1);
227
+ try {
228
+ await walkGraphOwned(def, runId, ctx, startNodes, registry, workflowId, secretKeys, options);
229
+ } finally {
230
+ const remaining = activeWalks.get(runId)! - 1;
231
+ if (remaining === 0) activeWalks.delete(runId);
232
+ else activeWalks.set(runId, remaining);
233
+ }
234
+ }
235
+
236
+ async function walkGraphOwned(
237
+ def: WorkflowDefinition,
238
+ runId: string,
239
+ ctx: Record<string, unknown>,
240
+ startNodes: WorkflowNode[],
241
+ registry: ExecutorRegistry,
242
+ workflowId?: string,
243
+ secretKeys: Set<string> = new Set(),
244
+ options: WorkflowExecutionOptions = {},
217
245
  ): Promise<void> {
218
246
  // Initial triggers carry the requester in options. Resumes, retries, and
219
247
  // recovery start from persisted state, so rehydrate it from the run.
@@ -11,7 +11,7 @@ import {
11
11
  updateWorkflowRun,
12
12
  } from "../be/db";
13
13
  import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
14
- import { findReadyNodes, walkGraph } from "./engine";
14
+ import { findReadyNodes, isWorkflowRunActive, walkGraph } from "./engine";
15
15
  import type { ExecutorRegistry } from "./executors/registry";
16
16
  import { getSecretInputKeys } from "./input";
17
17
  import { finalizeOrWait, resumeWaitState } from "./resume";
@@ -22,10 +22,10 @@ import {
22
22
  } from "./task-step-routing";
23
23
 
24
24
  /**
25
- * Recover incomplete workflow runs on server startup.
25
+ * Recover incomplete workflow runs on startup and during heartbeat sweeps.
26
26
  *
27
27
  * Two cases:
28
- * 1. `running` runs — were mid-execution when server died.
28
+ * 1. `running` runs without a live graph walk — interrupted execution.
29
29
  * Find completed steps, compute ready nodes, continue walking.
30
30
  * 2. `waiting` runs — were waiting for a task that may have finished while we were down.
31
31
  * Check if the linked task is done and resume/fail accordingly.
@@ -46,7 +46,7 @@ export async function recoverIncompleteRuns(registry: ExecutorRegistry): Promise
46
46
  recovered += await recoverWaitStates(registry);
47
47
 
48
48
  if (recovered > 0) {
49
- console.log(`[workflows] Recovered ${recovered} incomplete run(s) on startup`);
49
+ console.log(`[workflows] Recovered ${recovered} incomplete run(s)`);
50
50
  }
51
51
 
52
52
  return recovered;
@@ -64,6 +64,7 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
64
64
 
65
65
  for (const runId of runningRunIds) {
66
66
  try {
67
+ if (isWorkflowRunActive(runId)) continue;
67
68
  const run = await getWorkflowRun(runId);
68
69
  if (!run || run.status !== "running") continue;
69
70
 
@@ -75,6 +76,8 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
75
76
 
76
77
  // Find the next nodes that are ready to execute
77
78
  const readyNodes = findReadyNodes(workflow.definition, completedNodeIds);
79
+ // DB reads above yield; a trigger/resume may have acquired the run meanwhile.
80
+ if (isWorkflowRunActive(runId)) continue;
78
81
  if (readyNodes.length === 0) {
79
82
  // All nodes completed or nothing is ready — mark as completed
80
83
  await updateWorkflowRun(runId, {