@desplega.ai/agent-swarm 2.6.0 → 2.7.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 (213) hide show
  1. package/README.md +2 -0
  2. package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-ma1js5n7.js} +6 -6
  3. package/dist/{actions-fbqma8aa.js → actions-1vhmtvg9.js} +18 -18
  4. package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-sxqqvmdk.js} +3 -3
  5. package/dist/{app-k8a5cgpp.js → app-mwx1d5a9.js} +3 -3
  6. package/dist/{assistant-a16433gr.js → assistant-yx1p6g82.js} +20 -20
  7. package/dist/{boot-reembed-f4nn561n.js → boot-reembed-5h37a2c2.js} +1 -1
  8. package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-7h78vmvv.js} +2 -1
  9. package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-8mawd4gw.js} +3 -3
  10. package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-ca84dhnn.js} +4 -4
  11. package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-wqt4hfkq.js} +7 -7
  12. package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-6fjqtr2z.js} +2 -2
  13. package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-rqv98sea.js} +2 -2
  14. package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-f29f3b2x.js} +7 -7
  15. package/dist/{cli-sxgtf5fw.js → cli-07q03xm2.js} +1 -1
  16. package/dist/{cli-k21gx5ht.js → cli-1hhbzkbb.js} +6 -6
  17. package/dist/{cli-v49fkghp.js → cli-1m9x8dr5.js} +1 -1
  18. package/dist/{cli-24j1jeeb.js → cli-2pvzmas1.js} +3 -3
  19. package/dist/{cli-w2656ka1.js → cli-2y7v133v.js} +7 -7
  20. package/dist/{cli-g8ka5j4j.js → cli-48gxjmcz.js} +94 -36
  21. package/dist/{cli-2sjhpsns.js → cli-54sj64fc.js} +1 -1
  22. package/dist/{cli-aj8djyqt.js → cli-60sty27p.js} +2 -2
  23. package/dist/{cli-4hxrf0p4.js → cli-6a9qfa8b.js} +308 -47
  24. package/dist/{cli-n7asmce6.js → cli-6mgdba42.js} +3 -3
  25. package/dist/{cli-gf95q3bm.js → cli-6srv653c.js} +3 -3
  26. package/dist/{cli-9abdgn5t.js → cli-86ehn4s5.js} +3 -3
  27. package/dist/{cli-bqztfyve.js → cli-8mbc9za9.js} +1 -1
  28. package/dist/{cli-a2rr0fmg.js → cli-8wmj18cz.js} +1 -1
  29. package/dist/{cli-c2habvsf.js → cli-9gsm2rzp.js} +8 -8
  30. package/dist/{cli-4rf0f5vg.js → cli-9pshx6rd.js} +4 -2
  31. package/dist/{cli-abkm71vr.js → cli-a6ds2epe.js} +1 -1
  32. package/dist/{cli-esakngh8.js → cli-a89zdqz9.js} +1 -1
  33. package/dist/{cli-3ndmpxxv.js → cli-ay40dnj3.js} +2 -2
  34. package/dist/{cli-kf4jree9.js → cli-aypd09nf.js} +1 -1
  35. package/dist/{cli-6y51ryva.js → cli-b3yzyc57.js} +1 -1
  36. package/dist/{cli-ag2yrnt5.js → cli-c668vvcf.js} +2 -2
  37. package/dist/{cli-5sckpqcx.js → cli-d20qvpr2.js} +1 -1
  38. package/dist/{cli-a34pr3w1.js → cli-e1595bgx.js} +2 -2
  39. package/dist/{cli-m70bajzr.js → cli-f12mr20z.js} +1 -1
  40. package/dist/{cli-dqygeeh5.js → cli-f1e0ptjj.js} +10 -1
  41. package/dist/{cli-mvjz5an3.js → cli-f4dbh7a5.js} +7 -7
  42. package/dist/{cli-az25fn5f.js → cli-gsfk58nw.js} +2 -2
  43. package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
  44. package/dist/{cli-cz37jqss.js → cli-h5jeg46p.js} +2 -2
  45. package/dist/{cli-emhjcj2y.js → cli-he9gbdx0.js} +60 -10
  46. package/dist/{cli-3fwqy71j.js → cli-j0ppywdr.js} +38 -31
  47. package/dist/{cli-5rj83bam.js → cli-j3y0x2n4.js} +34 -33
  48. package/dist/{cli-3gfvmzht.js → cli-j9bpmykh.js} +73 -25
  49. package/dist/{cli-m5a9m0ak.js → cli-kv7sbc2c.js} +3 -3
  50. package/dist/{cli-g20jjwt4.js → cli-m30k9777.js} +12 -12
  51. package/dist/{cli-683y0b3a.js → cli-m5e802r7.js} +2 -2
  52. package/dist/{cli-nn6vx2mq.js → cli-ptfq202x.js} +5 -3
  53. package/dist/{cli-d2dpf95n.js → cli-qj442fws.js} +1 -1
  54. package/dist/{cli-03gg9pd1.js → cli-rd5m70gj.js} +4 -4
  55. package/dist/{cli-gnsb1zxa.js → cli-s09j9vp5.js} +1 -1
  56. package/dist/{cli-zqne1qcj.js → cli-s0mgx214.js} +2 -2
  57. package/dist/{cli-sqma6vw1.js → cli-sgqm2582.js} +5 -5
  58. package/dist/{cli-nq677csy.js → cli-sjctjr48.js} +1 -1
  59. package/dist/{cli-3aqd4jaa.js → cli-twctnkng.js} +3 -3
  60. package/dist/{cli-j6rktc91.js → cli-v6shg7na.js} +1 -1
  61. package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
  62. package/dist/{cli-art7c1zh.js → cli-wqr7cea4.js} +4 -4
  63. package/dist/{cli-hfeb68z4.js → cli-x37r7qwf.js} +11 -3
  64. package/dist/{cli-ffvsj2qw.js → cli-yd2nreqw.js} +2 -2
  65. package/dist/{cli-ca6banwz.js → cli-ymjpkxgn.js} +6 -6
  66. package/dist/{cli-mtnj4fwa.js → cli-yxbsmx56.js} +5 -5
  67. package/dist/cli.js +18 -16
  68. package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-zk59x2zh.js} +5 -5
  69. package/dist/{codex-hook-brpnycn5.js → codex-hook-xyjhvxwd.js} +1 -1
  70. package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-kh6s4j3n.js} +5 -5
  71. package/dist/{commands-jqmgk54b.js → commands-68et1qv2.js} +2 -2
  72. package/dist/{controller-z3jq589f.js → controller-fyrbnzg4.js} +2 -2
  73. package/dist/{controllers-connect-r2za39ef.js → controllers-connect-nep9h2xv.js} +19 -19
  74. package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-yss3pg7t.js} +1 -1
  75. package/dist/{db-w36wwb5g.js → db-28gjyvxj.js} +2 -2
  76. package/dist/{db-ppv2tze7.js → db-8ftwvzba.js} +3 -3
  77. package/dist/{dispatcher-pv5h77vf.js → dispatcher-hayexmkr.js} +17 -17
  78. package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-46wawff9.js} +1 -1
  79. package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-8d1e0h4m.js} +1 -1
  80. package/dist/{execute-jev839y1.js → execute-cjrfxfnm.js} +1 -1
  81. package/dist/{external-channel-y6nkzhnn.js → external-channel-xbmxcg09.js} +2 -2
  82. package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-ag4wks2a.js} +7 -7
  83. package/dist/handlers-tzmevqb5.js +41 -0
  84. package/dist/{heartbeat-vn31xwdk.js → heartbeat-psfvcgwp.js} +20 -20
  85. package/dist/{hook-6d0zbvvt.js → hook-8pz04697.js} +5 -5
  86. package/dist/{hook-cpjs8q51.js → hook-cyq4mryn.js} +6 -6
  87. package/dist/{http-9c6medda.js → http-haj1nd6k.js} +109 -114
  88. package/dist/{index-qhjf2htm.js → index-jwt2bj6f.js} +261 -96
  89. package/dist/{index-g6k9tctj.js → index-q52f47pe.js} +1 -1
  90. package/dist/{index-pkma1yf8.js → index-syvhnvtw.js} +1 -1
  91. package/dist/{index-yqgbfgaj.js → index-y1ajx04a.js} +20 -20
  92. package/dist/{index-k0yt1tvq.js → index-z5h22pes.js} +17 -17
  93. package/dist/{index-38t35h0n.js → index-zyq8ct52.js} +17 -17
  94. package/dist/{keepalive-em0117bp.js → keepalive-xdvsamvj.js} +3 -3
  95. package/dist/{lead-23e7z59t.js → lead-hqg57kdp.js} +35 -35
  96. package/dist/{maintenance-kbme3qy9.js → maintenance-4y2xzh3a.js} +1 -1
  97. package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-jd4r7zbs.js} +8 -3
  98. package/dist/{onboard-p6q0vz97.js → onboard-18447q9q.js} +5 -5
  99. package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-26b2frdf.js} +5 -5
  100. package/dist/{otel-impl-54qhqh1z.js → otel-impl-f9tgr8yw.js} +1 -1
  101. package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-qn6qbga1.js} +33 -12
  102. package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-dw2tgb53.js} +3 -3
  103. package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-aygm2760.js} +2 -2
  104. package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-s4nwj2m3.js} +1 -1
  105. package/dist/{render-v2-tjay5hhf.js → render-v2-yfvdfr0x.js} +3 -3
  106. package/dist/{resume-8ayf37g5.js → resume-1bmvjbf3.js} +7 -7
  107. package/dist/{runtime-hqe5cqcd.js → runtime-f7tpzedy.js} +1 -1
  108. package/dist/{session-access-fdhp987s.js → session-access-r5garej6.js} +4 -4
  109. package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-424wzv0r.js} +2 -2
  110. package/dist/{setup-4rrmqcq3.js → setup-8tygvxkq.js} +1 -1
  111. package/dist/{virtual-modules-52km00d6.js → virtual-modules-xsw323b3.js} +2 -2
  112. package/dist/{work-objects-p28fgd70.js → work-objects-ggnghpgf.js} +20 -20
  113. package/dist/{worker-e3w7vye1.js → worker-sqjd1nqf.js} +35 -35
  114. package/openapi.json +69 -1
  115. package/package.json +5 -3
  116. package/src/be/automation-preflight.ts +2 -0
  117. package/src/be/db/api-keys.ts +9 -0
  118. package/src/be/db/workflow-runs.ts +36 -5
  119. package/src/be/db.ts +80 -22
  120. package/src/be/memory/task-learning.ts +155 -0
  121. package/src/be/migrations/245_script_executor.sql +3 -0
  122. package/src/be/oauth-refresh-sweep.ts +2 -1
  123. package/src/be/scripts/boot-reembed.ts +2 -0
  124. package/src/be/scripts/db.ts +14 -4
  125. package/src/be/scripts/embeddings.ts +2 -0
  126. package/src/be/scripts/run-saved.ts +1 -0
  127. package/src/be/scripts/typecheck.ts +9 -3
  128. package/src/be/seed-skills/index.ts +7 -0
  129. package/src/commands/onboard/compose-generator.ts +5 -3
  130. package/src/commands/provider-credentials.ts +2 -1
  131. package/src/heartbeat/heartbeat.ts +14 -1
  132. package/src/heartbeat/script-runner-probe.ts +47 -0
  133. package/src/http/api-keys.ts +24 -9
  134. package/src/http/idempotency-key.ts +1 -1
  135. package/src/http/index.ts +3 -3
  136. package/src/http/pages-public.ts +38 -5
  137. package/src/http/scripts.ts +27 -0
  138. package/src/http/workflows.ts +10 -0
  139. package/src/http/x.ts +5 -0
  140. package/src/prompts/base-prompt.ts +21 -1
  141. package/src/prompts/session-templates.ts +17 -3
  142. package/src/providers/amp-adapter.ts +1 -1
  143. package/src/providers/dsh-adapter.ts +1 -1
  144. package/src/providers/pi-mono-adapter.ts +41 -11
  145. package/src/scheduler/scheduler.ts +2 -1
  146. package/src/scripts-runtime/eval-harness.ts +8 -1
  147. package/src/scripts-runtime/executors/native.ts +22 -16
  148. package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
  149. package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
  150. package/src/scripts-runtime/executors/quickjs.ts +7 -0
  151. package/src/scripts-runtime/executors/registry.ts +32 -0
  152. package/src/scripts-runtime/executors/types.ts +7 -0
  153. package/src/scripts-runtime/host-validated-args.ts +133 -0
  154. package/src/scripts-runtime/loader.ts +30 -3
  155. package/src/scripts-runtime/swarm-sdk.ts +9 -2
  156. package/src/scripts-runtime/types/stdlib.d.ts +9 -1
  157. package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
  158. package/src/tests/api-key-status-by-suffix.test.ts +242 -0
  159. package/src/tests/base-prompt.test.ts +70 -2
  160. package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
  161. package/src/tests/credential-check.test.ts +4 -2
  162. package/src/tests/docs-index.test.ts +135 -0
  163. package/src/tests/dsh-adapter.test.ts +1 -1
  164. package/src/tests/fetch-resolved-env.test.ts +16 -2
  165. package/src/tests/heartbeat-checklist.test.ts +89 -0
  166. package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
  167. package/src/tests/oauth-refresh-sweep.test.ts +34 -1
  168. package/src/tests/onboard-compose.test.ts +20 -0
  169. package/src/tests/opencode-adapter.test.ts +2 -1
  170. package/src/tests/pages-frame-ancestors.test.ts +162 -0
  171. package/src/tests/pi-codemode.test.ts +2 -1
  172. package/src/tests/pi-mono-adapter.test.ts +54 -0
  173. package/src/tests/pi-native-mcp.test.ts +13 -1
  174. package/src/tests/pi-tool-exposure.test.ts +8 -8
  175. package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
  176. package/src/tests/prompt-template-session.test.ts +4 -3
  177. package/src/tests/request-composition-derive.test.ts +329 -0
  178. package/src/tests/script-connections.test.ts +2 -0
  179. package/src/tests/script-executor-conformance.test.ts +36 -1
  180. package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
  181. package/src/tests/script-executor-quickjs.test.ts +140 -0
  182. package/src/tests/script-host-validated-args-native.test.ts +196 -0
  183. package/src/tests/script-per-script-executor.test.ts +415 -0
  184. package/src/tests/scripts-external-api.test.ts +45 -0
  185. package/src/tests/sdk-allowlist.test.ts +3 -2
  186. package/src/tests/self-improvement.test.ts +4 -4
  187. package/src/tests/store-progress-learning.test.ts +288 -0
  188. package/src/tests/ui-logs-parser.test.ts +180 -0
  189. package/src/tests/workflow-engine-v2.test.ts +116 -1
  190. package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
  191. package/src/tests/workflow-run-finished-event.test.ts +263 -0
  192. package/src/tests/workflow-swarm-script.test.ts +34 -0
  193. package/src/tools/script-common.ts +1 -0
  194. package/src/tools/script-run.ts +7 -0
  195. package/src/tools/script-upsert.ts +7 -0
  196. package/src/tools/store-progress.ts +67 -1
  197. package/src/tools/workflows/trigger-workflow.ts +104 -10
  198. package/src/types.ts +12 -1
  199. package/src/utils/credentials.ts +5 -2
  200. package/src/workflows/engine.ts +18 -16
  201. package/src/workflows/executors/swarm-script.ts +5 -0
  202. package/src/workflows/foreach-driver.ts +5 -11
  203. package/src/workflows/node-timeout.ts +30 -0
  204. package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
  205. package/templates/community/linear-delivery-loop/README.md +38 -0
  206. package/templates/community/linear-delivery-loop/config.example.json +31 -0
  207. package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
  208. package/templates/community/linear-delivery-loop/schedule.json +10 -0
  209. package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
  210. package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
  211. package/templates/skills/swarm-docs/config.json +14 -0
  212. package/templates/skills/swarm-docs/content.md +144 -0
  213. package/dist/handlers-eqsnmv95.js +0 -41
package/src/be/db.ts CHANGED
@@ -227,8 +227,10 @@ import { configureTaskWriteDependencies, failTask } from "./db/tasks/write";
227
227
  import { tombstoneWorkflowIdempotencyKeys } from "./db/workflow-idempotency";
228
228
  import {
229
229
  emitChildRunFinished,
230
+ emitRunFinished,
230
231
  getRunLineageBlock,
231
232
  getStepExecutionBlock,
233
+ isTerminalWorkflowRunStatus,
232
234
  rowToWorkflowRun,
233
235
  rowToWorkflowRunStep,
234
236
  WORKFLOW_RUN_SUMMARY_COLUMNS,
@@ -7994,16 +7996,33 @@ export async function updateWorkflowRun(
7994
7996
  updates.push("lastUpdatedAt = ?");
7995
7997
  params.push(new Date().toISOString());
7996
7998
  params.push(id);
7997
- const row = await getDbClient().get<WorkflowRunRow>(
7998
- `UPDATE workflow_runs SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
7999
- params,
8000
- );
7999
+ const update = () =>
8000
+ getDbClient().get<WorkflowRunRow>(
8001
+ `UPDATE workflow_runs SET ${updates.join(", ")} WHERE id = ? RETURNING *`,
8002
+ params,
8003
+ );
8004
+ const endsRun = data.status !== undefined && isTerminalWorkflowRunStatus(data.status);
8005
+ // A run that ends reads the status it replaces in the same transaction, so
8006
+ // only a change out of a live status announces the end: a later terminal
8007
+ // write, same status or not, does not announce it again.
8008
+ const [previousStatus, row] = endsRun
8009
+ ? await getDbClient().transaction(async () => {
8010
+ const previous = await getDbClient().get<{ status: WorkflowRunStatus }>(
8011
+ "SELECT status FROM workflow_runs WHERE id = ?",
8012
+ [id],
8013
+ );
8014
+ return [previous?.status, await update()] as const;
8015
+ })
8016
+ : [undefined, await update()];
8001
8017
  if (!row) return null;
8002
8018
  const run = rowToWorkflowRun(row);
8003
8019
  if (data.status === "completed" || data.status === "failed") {
8004
8020
  emitWorkflowTerminalTelemetry(run);
8005
8021
  }
8006
8022
  if (row.parentStepId) emitChildRunFinished(id, row.parentStepId, run.status);
8023
+ if (endsRun && previousStatus && !isTerminalWorkflowRunStatus(previousStatus)) {
8024
+ emitRunFinished(id, run.workflowId, run.status);
8025
+ }
8007
8026
  return run;
8008
8027
  }
8009
8028
 
@@ -11721,6 +11740,14 @@ export interface AvailableKeyIndicesResult {
11721
11740
  * whose `plan` cannot run the family (`planAllowsModelFamily`) is excluded
11722
11741
  * and reported in `seatBlockedIndices`, also when it is key-wide or
11723
11742
  * model-window blocked; a seat block has no reset time.
11743
+ *
11744
+ * With `keySuffixes` (the caller's own pool, in order), rows are matched by
11745
+ * key suffix instead of the stored `keyIndex`, across every scope: a key has
11746
+ * one state wherever it is pooled, and a key at the same index in another
11747
+ * agent's pool is never blocked by it. A suffix listed more than once is read
11748
+ * by `keyIndex` in `scope`/`scopeId`, as without the list. Returned indices
11749
+ * are positions in `keySuffixes`. Without it, rows of `scope`/`scopeId` are
11750
+ * read by `keyIndex`.
11724
11751
  */
11725
11752
  export async function getAvailableKeyIndices(
11726
11753
  keyType: string,
@@ -11728,41 +11755,58 @@ export async function getAvailableKeyIndices(
11728
11755
  scope = "global",
11729
11756
  scopeId: string | null = null,
11730
11757
  modelFamily?: ModelFamily,
11758
+ keySuffixes?: string[],
11731
11759
  ): Promise<AvailableKeyIndicesResult> {
11732
11760
  const now = new Date().toISOString();
11733
11761
  const nowMs = Date.now();
11734
11762
  const client = getDbClient();
11735
11763
  const effectiveScopeId = scopeId ?? "";
11764
+ const suffixes = keySuffixes?.length ? keySuffixes : undefined;
11765
+ const rowFilter = suffixes
11766
+ ? `keyType = ? AND keySuffix IN (${suffixes.map(() => "?").join(", ")})`
11767
+ : "keyType = ? AND scope = ? AND scopeId = ?";
11768
+ const rowParams = suffixes ? [keyType, ...suffixes] : [keyType, scope, effectiveScopeId];
11736
11769
 
11737
11770
  // Auto-clear expired rate limits. Never an auth bench: only `clearAuthBench` lifts it.
11738
11771
  await client.run(
11739
11772
  `UPDATE api_key_status
11740
11773
  SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
11741
- WHERE keyType = ? AND scope = ? AND scopeId = ?
11774
+ WHERE ${rowFilter}
11742
11775
  AND status = 'rate_limited' AND rateLimitedUntil <= ? AND consecutiveAuthFailures < ?`,
11743
- [now, keyType, scope, effectiveScopeId, now, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11776
+ [now, ...rowParams, now, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
11744
11777
  );
11745
11778
 
11746
11779
  const rows = await client.query<{
11747
11780
  keyIndex: number;
11781
+ keySuffix: string;
11782
+ scope: string;
11783
+ scopeId: string;
11748
11784
  status: string;
11749
11785
  rateLimitWindows: string | null;
11750
11786
  plan: string | null;
11751
11787
  authFailureSeq: number;
11752
11788
  lastValidationFailedAt: string | null;
11753
11789
  }>(
11754
- `SELECT keyIndex, status, rateLimitWindows, plan, authFailureSeq, lastValidationFailedAt FROM api_key_status
11755
- WHERE keyType = ? AND scope = ? AND scopeId = ?`,
11756
- [keyType, scope, effectiveScopeId],
11757
- );
11790
+ `SELECT keyIndex, keySuffix, scope, scopeId, status, rateLimitWindows, plan, authFailureSeq, lastValidationFailedAt FROM api_key_status
11791
+ WHERE ${rowFilter}`,
11792
+ rowParams,
11793
+ );
11794
+ // Pool positions a row applies to: the position holding its suffix, or its stored index.
11795
+ // A suffix held by several pool members cannot tell them apart, so it falls back to
11796
+ // the index read: only a row of the requested scope, at its stored index.
11797
+ const positionsOf = (row: (typeof rows)[number]): number[] => {
11798
+ if (!suffixes) return [row.keyIndex];
11799
+ const positions = suffixes.flatMap((suffix, i) => (suffix === row.keySuffix ? [i] : []));
11800
+ if (positions.length < 2) return positions;
11801
+ const sameScope = row.scope === scope && row.scopeId === effectiveScopeId;
11802
+ return sameScope && positions.includes(row.keyIndex) ? [row.keyIndex] : [];
11803
+ };
11758
11804
 
11759
11805
  const blockedIndices = new Set(
11760
- rows.filter((r) => r.status === "rate_limited").map((r) => r.keyIndex),
11806
+ rows.filter((r) => r.status === "rate_limited").flatMap(positionsOf),
11761
11807
  );
11762
11808
  const rejectedIndices = [
11763
- ...new Set(
11764
- rows.filter((row) => row.lastValidationFailedAt !== null).map((row) => row.keyIndex),
11765
- ),
11809
+ ...new Set(rows.filter((row) => row.lastValidationFailedAt !== null).flatMap(positionsOf)),
11766
11810
  ];
11767
11811
  for (const index of rejectedIndices) blockedIndices.add(index);
11768
11812
 
@@ -11773,14 +11817,17 @@ export async function getAvailableKeyIndices(
11773
11817
  // A key already blocked key-wide doesn't need a separate model-block
11774
11818
  // entry — it's excluded from availableIndices either way, and
11775
11819
  // modelBlockedIndices means "excluded only by a model-scoped block".
11776
- if (blockedIndices.has(row.keyIndex)) continue;
11820
+ const positions = positionsOf(row).filter(
11821
+ (i) => !blockedIndices.has(i) && !modelBlockedIndices.includes(i),
11822
+ );
11823
+ if (positions.length === 0) continue;
11777
11824
  const block = activeModelBlock(
11778
11825
  parseRateLimitWindowsJson(row.rateLimitWindows),
11779
11826
  modelFamily,
11780
11827
  nowMs,
11781
11828
  );
11782
11829
  if (!block) continue;
11783
- modelBlockedIndices.push(row.keyIndex);
11830
+ modelBlockedIndices.push(...positions);
11784
11831
  if (earliestModelResetsAtSec === undefined || block.resetsAt < earliestModelResetsAtSec) {
11785
11832
  earliestModelResetsAtSec = block.resetsAt;
11786
11833
  }
@@ -11794,7 +11841,10 @@ export async function getAvailableKeyIndices(
11794
11841
  const seatBlockedIndices: number[] = [];
11795
11842
  if (modelFamily) {
11796
11843
  for (const row of rows) {
11797
- if (!planAllowsModelFamily(row.plan, modelFamily)) seatBlockedIndices.push(row.keyIndex);
11844
+ if (planAllowsModelFamily(row.plan, modelFamily)) continue;
11845
+ for (const i of positionsOf(row)) {
11846
+ if (!seatBlockedIndices.includes(i)) seatBlockedIndices.push(i);
11847
+ }
11798
11848
  }
11799
11849
  }
11800
11850
  const seatBlockedSet = new Set(seatBlockedIndices);
@@ -12143,22 +12193,30 @@ export async function clearKeyRateLimit(
12143
12193
  const effectiveScopeId = scopeId ?? "";
12144
12194
  const { keyIndex = -1, authFence = null } = opts;
12145
12195
  const clearAuthBench = opts.clearAuthBench === true && authFence !== null;
12146
- const key = [keyType, keySuffix, scope, effectiveScopeId];
12147
12196
  return await getDbClient().transaction(async (tx) => {
12197
+ // The key's rows in every scope: `getAvailableKeyIndices` reads them all by suffix.
12148
12198
  const result = await tx.run(
12149
12199
  `UPDATE api_key_status
12150
12200
  SET status = 'available', rateLimitedUntil = NULL, updatedAt = ?
12151
- WHERE keyType = ? AND keySuffix = ? AND scope = ? AND scopeId = ?
12201
+ WHERE keyType = ? AND keySuffix = ?
12152
12202
  AND status = 'rate_limited' AND ((? AND authFailureSeq <= ?) OR consecutiveAuthFailures < ?)`,
12153
- [now, ...key, clearAuthBench ? 1 : 0, authFence, CODEX_AUTH_FAILURE_BENCH_THRESHOLD],
12203
+ [
12204
+ now,
12205
+ keyType,
12206
+ keySuffix,
12207
+ clearAuthBench ? 1 : 0,
12208
+ authFence,
12209
+ CODEX_AUTH_FAILURE_BENCH_THRESHOLD,
12210
+ ],
12154
12211
  );
12155
12212
  const cleared = result.changes > 0;
12156
12213
  if (!clearAuthBench) return cleared;
12157
12214
  const lifted = await tx.query<{ keySuffix: string }>(
12158
12215
  `UPDATE api_key_status SET consecutiveAuthFailures = 0, updatedAt = ?1,
12159
12216
  status = CASE WHEN keySuffix = ?2 THEN status ELSE 'available' END, rateLimitedUntil = CASE WHEN keySuffix = ?2 THEN rateLimitedUntil END
12160
- WHERE keyType = ?3 AND scope = ?4 AND scopeId = ?5 AND (consecutiveAuthFailures > 0 OR status = 'rate_limited')
12161
- AND ((keySuffix = ?2 AND authFailureSeq <= ?7) OR (keySuffix != ?2 AND keyIndex = ?6)) RETURNING keySuffix`,
12217
+ WHERE keyType = ?3 AND (consecutiveAuthFailures > 0 OR status = 'rate_limited')
12218
+ AND ((keySuffix = ?2 AND authFailureSeq <= ?7)
12219
+ OR (keySuffix != ?2 AND keyIndex = ?6 AND scope = ?4 AND scopeId = ?5)) RETURNING keySuffix`,
12162
12220
  [now, keySuffix, keyType, scope, effectiveScopeId, keyIndex, authFence],
12163
12221
  );
12164
12222
  const retired = lifted.filter((row) => row.keySuffix !== keySuffix);
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Learnings captured through `store-progress`: one swarm-scope `manual` memory
3
+ * per learning, keyed `/learnings/<workstream>/<slug>` and tied to its task by
4
+ * `sourceTaskId`, so "what did we learn doing X" is one `keyPrefix` search.
5
+ *
6
+ * Source stays `manual` so nightly curation (memtax B1 reads only manual and
7
+ * file_index rows) picks learnings up without a new source value.
8
+ */
9
+ import { getDbClient } from "@/be/db";
10
+ import { indexMemoryContent } from "@/be/memory/index-content";
11
+ import { contentSha256 } from "@/commands/profile-sync";
12
+ import type { AgentTask } from "@/types";
13
+ import { scrubSecrets } from "@/utils/secret-scrubber";
14
+
15
+ /** ~500 tokens at ceil(chars / 4); also keeps a learning to one memory chunk. */
16
+ export const LEARNING_MAX_CHARS = 2000;
17
+ export const LEARNING_MAX_TAGS = 5;
18
+ const LEARNINGS_ROOT = "/learnings";
19
+ const SEGMENT_MAX_CHARS = 60;
20
+ const KEY_ATTEMPTS = 4;
21
+
22
+ export type TaskLearningInput = string | { text: string; workstream?: string; tags?: string[] };
23
+
24
+ export type TaskLearningResult =
25
+ | { status: "stored"; memoryId: string; key: string; workstream: string }
26
+ | { status: "replayed"; memoryId: string; key: string }
27
+ | { status: "failed"; error: string };
28
+
29
+ /** Lowercase kebab slug: `Agent Swarm!` -> `agent-swarm`. Empty when nothing usable is left. */
30
+ export function toKebab(value: string, maxChars = SEGMENT_MAX_CHARS): string {
31
+ return value
32
+ .normalize("NFKD")
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9]+/g, "-")
35
+ .replace(/^-+/, "")
36
+ .slice(0, maxChars)
37
+ .replace(/-+$/, "");
38
+ }
39
+
40
+ /** Explicit workstream, else the repo name from `vcsRepo` (`owner/name` or a URL), else `general`. */
41
+ export function resolveLearningWorkstream(
42
+ explicit: string | undefined,
43
+ vcsRepo: string | undefined,
44
+ ): string {
45
+ const fromExplicit = explicit ? toKebab(explicit) : "";
46
+ if (fromExplicit) return fromExplicit;
47
+ const repoName = vcsRepo
48
+ ?.trim()
49
+ .replace(/\/+$/, "")
50
+ .split("/")
51
+ .pop()
52
+ ?.replace(/\.git$/, "");
53
+ const fromRepo = repoName ? toKebab(repoName) : "";
54
+ return fromRepo || "general";
55
+ }
56
+
57
+ export async function storeTaskLearning(params: {
58
+ agentId: string;
59
+ isLead: boolean;
60
+ task: Pick<AgentTask, "id" | "vcsRepo">;
61
+ learning: TaskLearningInput;
62
+ }): Promise<TaskLearningResult> {
63
+ const { agentId, isLead, task } = params;
64
+ const input = typeof params.learning === "string" ? { text: params.learning } : params.learning;
65
+ // Scrub before anything is derived: a token in the title, workstream or a tag
66
+ // would otherwise survive (kebab-cased) in the key and tags, and the replay
67
+ // hash must match the hash of the scrubbed content the store persists.
68
+ const text = scrubSecrets(input.text).trim();
69
+ if (!text) return { status: "failed", error: "learning text is empty" };
70
+ if (text.length > LEARNING_MAX_CHARS) {
71
+ return {
72
+ status: "failed",
73
+ error: `learning text is over ~500 tokens (${LEARNING_MAX_CHARS} characters); shorten it`,
74
+ };
75
+ }
76
+ const tags = [
77
+ ...new Set((input.tags ?? []).map((tag) => toKebab(scrubSecrets(tag))).filter(Boolean)),
78
+ ].filter((tag) => tag !== "learning");
79
+ if (tags.length > LEARNING_MAX_TAGS) {
80
+ return { status: "failed", error: `learning takes at most ${LEARNING_MAX_TAGS} tags` };
81
+ }
82
+
83
+ const workstream = resolveLearningWorkstream(
84
+ input.workstream === undefined ? undefined : scrubSecrets(input.workstream),
85
+ task.vcsRepo === undefined ? undefined : scrubSecrets(task.vcsRepo),
86
+ );
87
+
88
+ try {
89
+ // One write transaction for the replay check and the insert, so two
90
+ // overlapping calls with the same learning store it once.
91
+ return await getDbClient().transaction(() =>
92
+ insertLearning({ agentId, isLead, taskId: task.id, text, tags, workstream }),
93
+ );
94
+ } catch (err) {
95
+ return {
96
+ status: "failed",
97
+ error: scrubSecrets(err instanceof Error ? err.message : String(err)),
98
+ };
99
+ }
100
+ }
101
+
102
+ async function insertLearning(params: {
103
+ agentId: string;
104
+ isLead: boolean;
105
+ taskId: string;
106
+ text: string;
107
+ tags: string[];
108
+ workstream: string;
109
+ }): Promise<TaskLearningResult> {
110
+ const { agentId, isLead, taskId, text, tags, workstream } = params;
111
+ // indexMemoryContent stores text this short as one trimmed chunk, so this
112
+ // hash is the row's contentHash: a replayed call finds its own earlier write.
113
+ const contentHash = contentSha256(text);
114
+ const replay = await getDbClient().get<{ id: string; key: string }>(
115
+ `SELECT id, key FROM agent_memory
116
+ WHERE sourceTaskId = ? AND contentHash = ? AND key LIKE '${LEARNINGS_ROOT}/%'
117
+ LIMIT 1`,
118
+ [taskId, contentHash],
119
+ );
120
+ if (replay) return { status: "replayed", memoryId: replay.id, key: replay.key };
121
+
122
+ const title = text.split(/\r?\n/, 1)[0]!.trim().slice(0, 200);
123
+ const baseKey = `${LEARNINGS_ROOT}/${workstream}/${toKebab(title) || "learning"}`;
124
+
125
+ // Keys are unique per owner and a reused key fails rather than upserts.
126
+ // Keep the path unique across the swarm: on a taken key, add a short suffix.
127
+ for (let attempt = 0; attempt < KEY_ATTEMPTS; attempt++) {
128
+ const key = attempt === 0 ? baseKey : `${baseKey}-${crypto.randomUUID().slice(0, 6)}`;
129
+ const taken = await getDbClient().get<{ id: string }>(
130
+ "SELECT id FROM agent_memory WHERE scope = 'swarm' AND key = ? LIMIT 1",
131
+ [key],
132
+ );
133
+ if (taken) continue;
134
+ try {
135
+ const result = await indexMemoryContent({
136
+ agentId,
137
+ content: text,
138
+ name: title,
139
+ scope: "swarm",
140
+ source: "manual",
141
+ sourceTaskId: taskId,
142
+ tags: ["learning", ...tags],
143
+ intent: "store-progress learning",
144
+ key,
145
+ writer: { principal: { kind: "agent", agentId, isLead }, source: "mcp" },
146
+ });
147
+ return { status: "stored", memoryId: result.memoryIds[0]!, key, workstream };
148
+ } catch (err) {
149
+ // The key is taken after all: try a suffixed one.
150
+ if (/UNIQUE constraint failed/i.test((err as Error).message)) continue;
151
+ throw err;
152
+ }
153
+ }
154
+ return { status: "failed", error: `no free key under ${baseKey}` };
155
+ }
@@ -0,0 +1,3 @@
1
+ -- Optional per-script executor override ("native" | "quickjs").
2
+ -- When NULL, the deployment's SCRIPT_EXECUTOR default applies.
3
+ ALTER TABLE scripts ADD COLUMN executor TEXT DEFAULT NULL;
@@ -20,6 +20,7 @@
20
20
  * same per-authorization locks in ensure-token, so overlap is safe.
21
21
  */
22
22
  import { forceRefreshAuthorizationOrThrow } from "../oauth/ensure-token";
23
+ import { isEnvFlagEnabled } from "../utils/env-flag";
23
24
  import { scrubSecrets } from "../utils/secret-scrubber";
24
25
  import {
25
26
  type AuthorizationSweepRow,
@@ -138,7 +139,7 @@ async function runSweepTick(trigger: "startup" | "interval"): Promise<void> {
138
139
  * already running.
139
140
  */
140
141
  export function startOAuthRefreshSweep(): void {
141
- if (process.env.OAUTH_REFRESH_SWEEP_DISABLE === "true") {
142
+ if (isEnvFlagEnabled("OAUTH_REFRESH_SWEEP_DISABLE", false)) {
142
143
  console.log("[oauth-refresh-sweep] Disabled via OAUTH_REFRESH_SWEEP_DISABLE");
143
144
  return;
144
145
  }
@@ -26,6 +26,7 @@ type ScriptRow = {
26
26
  isScratch: number;
27
27
  typeChecked: number;
28
28
  fsMode: "none" | "workspace-rw";
29
+ executor: "native" | "quickjs" | null;
29
30
  createdByAgentId: string | null;
30
31
  createdAt: string;
31
32
  updatedAt: string;
@@ -37,6 +38,7 @@ function toScriptRecord(row: ScriptRow) {
37
38
  scopeId: row.scopeId ?? null,
38
39
  isScratch: row.isScratch === 1,
39
40
  typeChecked: row.typeChecked === 1,
41
+ executor: row.executor ?? null,
40
42
  createdByAgentId: row.createdByAgentId ?? null,
41
43
  };
42
44
  }
@@ -3,6 +3,7 @@ import type {
3
3
  ScriptApiAuthMode,
4
4
  ScriptApiRecord,
5
5
  ScriptApiWithSecret,
6
+ ScriptExecutorName,
6
7
  ScriptFsMode,
7
8
  ScriptRecord,
8
9
  ScriptScope,
@@ -36,6 +37,7 @@ type ScriptWriteArgs = ScriptIdentity & {
36
37
  isScratch?: boolean;
37
38
  typeChecked?: boolean;
38
39
  fsMode?: ScriptFsMode;
40
+ executor?: ScriptExecutorName | null;
39
41
  agentId?: string | null;
40
42
  changeReason?: string | null;
41
43
  embeddingMode?: "sync" | "skip";
@@ -62,6 +64,7 @@ function rowToScript(row: ScriptRow): ScriptRecord {
62
64
  scopeId: row.scopeId ?? null,
63
65
  isScratch: row.isScratch === 1,
64
66
  typeChecked: row.typeChecked === 1,
67
+ executor: row.executor ?? null,
65
68
  createdByAgentId: row.createdByAgentId ?? null,
66
69
  };
67
70
  }
@@ -113,6 +116,7 @@ export async function insertScript(args: ScriptWriteArgs): Promise<ScriptRecord>
113
116
  const scopeId = normalizeScopeId(args.scope, args.scopeId);
114
117
  const contentHash = computeContentHash(args.source);
115
118
  const fsMode = args.fsMode ?? "none";
119
+ const executor = args.executor ?? null;
116
120
  const isScratch = args.isScratch ? 1 : 0;
117
121
  const typeChecked = args.typeChecked ? 1 : 0;
118
122
 
@@ -120,10 +124,10 @@ export async function insertScript(args: ScriptWriteArgs): Promise<ScriptRecord>
120
124
  const row = await tx.get<ScriptRow>(
121
125
  `INSERT INTO scripts (
122
126
  id, "key", name, scope, scopeId, source, description, intent, signatureJson,
123
- argsJsonSchema, contentHash, isScratch, typeChecked, fsMode, createdByAgentId, createdAt, updatedAt,
127
+ argsJsonSchema, contentHash, isScratch, typeChecked, fsMode, executor, createdByAgentId, createdAt, updatedAt,
124
128
  created_by, updated_by
125
129
  )
126
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
127
131
  RETURNING *`,
128
132
  [
129
133
  id,
@@ -140,6 +144,7 @@ export async function insertScript(args: ScriptWriteArgs): Promise<ScriptRecord>
140
144
  isScratch,
141
145
  typeChecked,
142
146
  fsMode,
147
+ executor,
143
148
  args.agentId ?? null,
144
149
  now,
145
150
  now,
@@ -189,6 +194,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
189
194
  const contentHash = computeContentHash(args.source);
190
195
  if (existing.contentHash === contentHash) {
191
196
  const fsMode = args.fsMode ?? existing.fsMode;
197
+ const executor = args.executor !== undefined ? args.executor : existing.executor;
192
198
  const isScratch = args.isScratch ?? existing.isScratch;
193
199
  const typeChecked = args.typeChecked ?? existing.typeChecked;
194
200
  const argsJsonSchema =
@@ -206,6 +212,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
206
212
  isScratch;
207
213
  if (
208
214
  fsMode !== existing.fsMode ||
215
+ executor !== existing.executor ||
209
216
  isScratch !== existing.isScratch ||
210
217
  typeChecked !== existing.typeChecked ||
211
218
  trackedMetadataChanged ||
@@ -214,7 +221,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
214
221
  const row = await getDbClient().get<ScriptRow>(
215
222
  `UPDATE scripts
216
223
  SET description = ?, intent = ?, signatureJson = ?, argsJsonSchema = ?,
217
- isScratch = ?, typeChecked = ?, fsMode = ?, updatedAt = ?, updated_by = ?
224
+ isScratch = ?, typeChecked = ?, fsMode = ?, executor = ?, updatedAt = ?, updated_by = ?
218
225
  WHERE id = ?
219
226
  RETURNING *`,
220
227
  [
@@ -225,6 +232,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
225
232
  isScratch ? 1 : 0,
226
233
  typeChecked ? 1 : 0,
227
234
  fsMode,
235
+ executor ?? null,
228
236
  new Date().toISOString(),
229
237
  args.createdBy ?? null,
230
238
  existing.id,
@@ -253,6 +261,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
253
261
  const now = new Date().toISOString();
254
262
  const newVersion = existing.version + 1;
255
263
  const fsMode = args.fsMode ?? existing.fsMode;
264
+ const executor = args.executor !== undefined ? args.executor : existing.executor;
256
265
  const isScratch = args.isScratch ?? existing.isScratch;
257
266
  const typeChecked = args.typeChecked ?? existing.typeChecked;
258
267
  const argsJsonSchema =
@@ -262,7 +271,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
262
271
  const row = await tx.get<ScriptRow>(
263
272
  `UPDATE scripts
264
273
  SET source = ?, description = ?, intent = ?, signatureJson = ?, argsJsonSchema = ?,
265
- contentHash = ?, version = ?, isScratch = ?, typeChecked = ?, fsMode = ?, updatedAt = ?, updated_by = ?
274
+ contentHash = ?, version = ?, isScratch = ?, typeChecked = ?, fsMode = ?, executor = ?, updatedAt = ?, updated_by = ?
266
275
  WHERE id = ?
267
276
  RETURNING *`,
268
277
  [
@@ -276,6 +285,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
276
285
  isScratch ? 1 : 0,
277
286
  typeChecked ? 1 : 0,
278
287
  fsMode,
288
+ executor ?? null,
279
289
  now,
280
290
  args.createdBy ?? null,
281
291
  existing.id,
@@ -28,6 +28,7 @@ type ScriptEmbeddingCandidateRow = ScriptEmbeddingRow & {
28
28
  isScratch: number;
29
29
  typeChecked: number;
30
30
  fsMode: "none" | "workspace-rw";
31
+ executor: "native" | "quickjs" | null;
31
32
  createdByAgentId: string | null;
32
33
  createdAt: string;
33
34
  updatedAt: string;
@@ -70,6 +71,7 @@ function rowToScript(row: ScriptEmbeddingCandidateRow): ScriptRecord {
70
71
  isScratch: row.isScratch === 1,
71
72
  typeChecked: row.typeChecked === 1,
72
73
  fsMode: row.fsMode,
74
+ executor: row.executor ?? null,
73
75
  createdByAgentId: row.createdByAgentId ?? null,
74
76
  createdAt: row.createdAt,
75
77
  updatedAt: row.updatedAt,
@@ -50,6 +50,7 @@ export async function runSavedScriptAsAgent(
50
50
  source: args.script.source,
51
51
  args: args.input,
52
52
  fsMode: args.script.fsMode,
53
+ executor: args.script.executor,
53
54
  agentId: args.agentId,
54
55
  egressSecrets: credentials.egressSecrets,
55
56
  failedBindings: credentials.failedBindings,
@@ -33,6 +33,7 @@ export const SCRIPT_SDK_TYPES = `
33
33
  export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
34
34
  export type ScriptScope = "agent" | "global";
35
35
  export type ScriptFsMode = "none" | "workspace-rw";
36
+ export type ScriptExecutorName = "native" | "quickjs";
36
37
  export type ScriptApiRawOptions = { raw: true };
37
38
  export type ScriptApiDefaultOptions = { raw?: false };
38
39
  export type RoutingReason = "skill" | "continuity" | "overflow" | "human_pinned" | "reroute_fault";
@@ -224,7 +225,7 @@ export interface SwarmSdk {
224
225
  schedule_list(args?: { enabled?: boolean; name?: string; scheduleType?: "recurring" | "one_time"; targetType?: "agent-task" | "workflow" | "script"; workflowId?: string; scriptName?: string; hideCompleted?: boolean; consecutiveErrorsMin?: number; lastRunStatus?: "failed" | "succeeded"; includeFull?: boolean }): Promise<unknown>;
225
226
  // --- scripts ---
226
227
  script_search(args: { query?: string; scope?: ScriptScope; limit?: number }): Promise<unknown>;
227
- script_run(args: { name?: string; source?: string; args?: unknown; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode; idempotencyKey?: string }): Promise<unknown>;
228
+ script_run(args: { name?: string; source?: string; args?: unknown; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode; executor?: ScriptExecutorName | null; idempotencyKey?: string }): Promise<unknown>;
228
229
  // --- swarm / agent ---
229
230
  swarm_get(args?: { includeFull?: boolean }): Promise<unknown>;
230
231
  agent_info(args?: Record<string, unknown>): Promise<unknown>;
@@ -341,7 +342,12 @@ export interface SwarmSdk {
341
342
  workflow_patch(args: Record<string, unknown>): Promise<unknown>;
342
343
  workflow_patchNode(args: Record<string, unknown>): Promise<unknown>;
343
344
  workflow_delete(args: { id: string }): Promise<unknown>;
344
- workflow_trigger(args: { id: string; triggerData?: Record<string, unknown> }): Promise<unknown>;
345
+ workflow_trigger(args: {
346
+ id: string;
347
+ triggerData?: Record<string, unknown>;
348
+ wait?: boolean;
349
+ idempotencyKey?: string;
350
+ }): Promise<unknown>;
345
351
  workflow_retryRun(args: { id: string }): Promise<unknown>;
346
352
  workflow_cancelRun(args: { id: string }): Promise<unknown>;
347
353
 
@@ -351,7 +357,7 @@ export interface SwarmSdk {
351
357
  prompt_preview(args: Record<string, unknown>): Promise<unknown>;
352
358
 
353
359
  // --- write: scripts ---
354
- script_upsert(args: { name: string; source: string; description?: string; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode }): Promise<unknown>;
360
+ script_upsert(args: { name: string; source: string; description?: string; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode; executor?: ScriptExecutorName | null }): Promise<unknown>;
355
361
  script_delete(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
356
362
  script_queryTypes(args: { name: string; scope?: ScriptScope }): Promise<unknown>;
357
363
  script_launchRun(args: { source: string; args?: unknown; idempotencyKey?: string; scriptName?: string; requestedByUserId?: string }): Promise<unknown>;
@@ -208,6 +208,12 @@ import stepRunningConfig from "../../../templates/skills/step-running/config.jso
208
208
  import stepRunningContent from "../../../templates/skills/step-running/content.md" with {
209
209
  type: "text",
210
210
  };
211
+ import swarmDocsConfig from "../../../templates/skills/swarm-docs/config.json" with {
212
+ type: "text",
213
+ };
214
+ import swarmDocsContent from "../../../templates/skills/swarm-docs/content.md" with {
215
+ type: "text",
216
+ };
211
217
  import swarmExtensionsConfig from "../../../templates/skills/swarm-extensions/config.json" with {
212
218
  type: "text",
213
219
  };
@@ -367,6 +373,7 @@ const BUILT_IN_SKILL_SOURCES = [
367
373
  { config: reviewingConfig, body: reviewingContent },
368
374
  { config: scheduledTaskResilienceConfig, body: scheduledTaskResilienceContent },
369
375
  { config: schedulingConfig, body: schedulingContent },
376
+ { config: swarmDocsConfig, body: swarmDocsContent },
370
377
  { config: swarmExtensionsConfig, body: swarmExtensionsContent },
371
378
  { config: scriptBuilderConfig, body: scriptBuilderContent },
372
379
  { config: scriptWorkflowsConfig, body: scriptWorkflowsContent },
@@ -243,9 +243,11 @@ export function generateCompose(state: OnboardState): string {
243
243
  lines.push(` - "${port}:3000"`);
244
244
  lines.push("");
245
245
  lines.push(" volumes:");
246
- lines.push(" - swarm_logs:/app/logs");
247
- lines.push(" - swarm_shared:/app/shared");
248
- lines.push(` - swarm_${svc.sanitizedName}:/app/agent`);
246
+ // Same targets as docker-compose.example.yml: the worker logs to /logs and
247
+ // keeps its clones and files under /workspace.
248
+ lines.push(" - swarm_logs:/logs");
249
+ lines.push(" - swarm_shared:/workspace/shared");
250
+ lines.push(` - swarm_${svc.sanitizedName}:/workspace/personal`);
249
251
  if (state.provider === "bedrock" && state.awsProfile) {
250
252
  lines.push(engine === "podman" ? AWS_PROFILE_VOLUME_SELINUX : AWS_PROFILE_VOLUME);
251
253
  }
@@ -49,6 +49,7 @@ import {
49
49
  CREDENTIAL_POOL_VARS,
50
50
  splitCredentialPool,
51
51
  } from "../utils/credentials";
52
+ import { isEnvFlagEnabled } from "../utils/env-flag";
52
53
  import {
53
54
  checkAnthropicApiKey,
54
55
  checkDeepSeekApiKey,
@@ -664,7 +665,7 @@ export async function validateProviderCredentials(
664
665
  * the dashboard surfaces "unreported".
665
666
  */
666
667
  export function isCredCheckDisabled(env: NodeJS.ProcessEnv): boolean {
667
- return env.CRED_CHECK_DISABLE === "1";
668
+ return isEnvFlagEnabled("CRED_CHECK_DISABLE", false, env);
668
669
  }
669
670
 
670
671
  /**
@@ -83,6 +83,7 @@ import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../utils/open
83
83
  import { scrubSecrets } from "../utils/secret-scrubber";
84
84
  import { getExecutorRegistry } from "../workflows";
85
85
  import { recoverIncompleteRuns } from "../workflows/recovery";
86
+ import { probeScriptRunner } from "./script-runner-probe";
86
87
  // Side-effect import: registers heartbeat event templates in the in-memory registry
87
88
  import "./templates";
88
89
 
@@ -1703,8 +1704,20 @@ export async function defaultChecklistGateRunner(
1703
1704
  scriptName: string,
1704
1705
  leadAgentId: string,
1705
1706
  signal: AbortSignal,
1706
- deps: Parameters<typeof runSavedScriptAsAgent>[1] = {},
1707
+ deps: Parameters<typeof runSavedScriptAsAgent>[1] & {
1708
+ probe?: typeof probeScriptRunner;
1709
+ } = {},
1707
1710
  ): Promise<unknown> {
1711
+ try {
1712
+ const unhealthy = await (deps.probe ?? probeScriptRunner)(leadAgentId);
1713
+ if (unhealthy) return unhealthy;
1714
+ } catch (error) {
1715
+ const message = error instanceof Error ? error.message : String(error);
1716
+ return {
1717
+ quiet: false,
1718
+ reason: `script-runner-unhealthy: ${scrubSecrets(message)}`.slice(0, 300),
1719
+ };
1720
+ }
1708
1721
  const script = await getScript({ name: scriptName, scope: "global" });
1709
1722
  if (!script) throw new Error(`global script "${scriptName}" not found`);
1710
1723
  const output = await runSavedScriptAsAgent(