@desplega.ai/agent-swarm 2.5.2 → 2.5.4

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 (236) hide show
  1. package/dist/{acp-adapter-csh0zz3b.js → acp-adapter-ek38cwsv.js} +5 -5
  2. package/dist/{actions-6xx079d1.js → actions-92mepq03.js} +19 -19
  3. package/dist/{amp-adapter-8n6f0hz8.js → amp-adapter-9gaxg015.js} +4 -4
  4. package/dist/{app-ft6r6v23.js → app-ctn8gsgx.js} +3 -3
  5. package/dist/{assistant-vt2cyfzd.js → assistant-zyr5kz47.js} +21 -21
  6. package/dist/{boot-reembed-tgrfy27t.js → boot-reembed-dwzfttdr.js} +1 -1
  7. package/dist/{boot-reembed-r3e808ky.js → boot-reembed-w75maga2.js} +1 -1
  8. package/dist/{boot-scrub-sweep-masdwxkm.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
  9. package/dist/{boot-seeds-0n82qc7q.js → boot-seeds-x28v3fhb.js} +4 -4
  10. package/dist/{claude-adapter-s7j47k06.js → claude-adapter-f0afqnne.js} +5 -5
  11. package/dist/{claude-managed-adapter-gt6pa35d.js → claude-managed-adapter-x81a7r65.js} +3 -3
  12. package/dist/{claude-managed-setup-3hzrgrnk.js → claude-managed-setup-bqzwjd9x.js} +2 -2
  13. package/dist/{claude-sdk-session-0zrdzyx2.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
  14. package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
  15. package/dist/{cli-4qkg9968.js → cli-19202kew.js} +4 -4
  16. package/dist/{cli-cyfgxnvb.js → cli-1r5ad7j5.js} +19 -17
  17. package/dist/{cli-jmcjbjkt.js → cli-25n3mv5d.js} +9 -7
  18. package/dist/cli-2655k16q.js +140 -0
  19. package/dist/{cli-wadzapwb.js → cli-277c31dw.js} +1 -1
  20. package/dist/{cli-n6kmqngw.js → cli-4b2z6n0m.js} +29 -7
  21. package/dist/{cli-7ccf7akh.js → cli-5bnsc0e1.js} +9 -7
  22. package/dist/{cli-qrjv7t18.js → cli-621ce7kw.js} +1 -1
  23. package/dist/{cli-xzge46sj.js → cli-64mh9mf3.js} +20 -6
  24. package/dist/{cli-b6tnjdvx.js → cli-6a1xg1p7.js} +6 -2
  25. package/dist/{cli-ydg6ep51.js → cli-6n8axjv1.js} +1 -1
  26. package/dist/{cli-e91vshb1.js → cli-7p4jxgtt.js} +5 -5
  27. package/dist/{cli-f3pfn4x3.js → cli-9j129brq.js} +9 -3
  28. package/dist/{cli-xw9q173h.js → cli-9s05422w.js} +1 -1
  29. package/dist/{cli-15xd0j7w.js → cli-abs8ndtr.js} +12 -12
  30. package/dist/{cli-h5d22pfs.js → cli-asq5ydm4.js} +219 -309
  31. package/dist/{cli-w9etnnxk.js → cli-bf8ma3f1.js} +120 -50
  32. package/dist/{cli-0vgy5wfw.js → cli-bvpgr9ny.js} +2 -2
  33. package/dist/{cli-3e9vs93g.js → cli-c61mbdds.js} +4 -4
  34. package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
  35. package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
  36. package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
  37. package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
  38. package/dist/{cli-b63d08rs.js → cli-eftzyh9y.js} +1 -1
  39. package/dist/{cli-fgn7yg5r.js → cli-ethgy7ha.js} +1 -1
  40. package/dist/{cli-86zpfje5.js → cli-ff1tm9n9.js} +1 -1
  41. package/dist/{cli-cf63dnxj.js → cli-g9vd9ece.js} +37 -12
  42. package/dist/{cli-j3dwd4kz.js → cli-gbfxp48k.js} +770 -126
  43. package/dist/{cli-n2918sed.js → cli-gn5af0x5.js} +21 -21
  44. package/dist/{cli-d172svcw.js → cli-gwhjmh8g.js} +4 -4
  45. package/dist/{cli-stxdrqs2.js → cli-hh5we352.js} +24 -5
  46. package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
  47. package/dist/{cli-csh8fnbg.js → cli-jb30dzvk.js} +473 -58
  48. package/dist/{cli-vaxw3bg0.js → cli-jbnn4vxb.js} +3 -3
  49. package/dist/{cli-ha22h0eg.js → cli-k4tjsdx9.js} +45 -40
  50. package/dist/{cli-f55b6ky0.js → cli-m4ecdxvz.js} +40 -13
  51. package/dist/{cli-mntxtkhn.js → cli-mz9afxxf.js} +2 -1
  52. package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
  53. package/dist/{cli-3qv8naq8.js → cli-rr7zx19j.js} +1591 -353
  54. package/dist/{cli-b1tv22wr.js → cli-s682zgqz.js} +1 -1
  55. package/dist/{cli-bpbcpd8b.js → cli-sbwsxvtd.js} +1 -1
  56. package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
  57. package/dist/{cli-6y43v79h.js → cli-tsma7p8v.js} +1 -1
  58. package/dist/{cli-edtm741g.js → cli-wghjkr0e.js} +5 -5
  59. package/dist/{cli-aaqnqpyw.js → cli-wx2nj19m.js} +30 -11
  60. package/dist/{cli-y3dzwyy3.js → cli-x0f2v69c.js} +180 -60
  61. package/dist/{cli-kcnwn85g.js → cli-xtrz6bzs.js} +7 -7
  62. package/dist/{cli-14v1sxxy.js → cli-xzfq4at3.js} +7 -7
  63. package/dist/cli.js +20 -20
  64. package/dist/{codex-adapter-brh2tcxr.js → codex-adapter-wch12q99.js} +3 -3
  65. package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
  66. package/dist/{codex-session-runner-0dnj21qs.js → codex-session-runner-41pfcr95.js} +2 -2
  67. package/dist/{commands-1cnvseqx.js → commands-9849dkj6.js} +2 -2
  68. package/dist/{controller-kc7s9xhn.js → controller-ja0sd95p.js} +29 -3
  69. package/dist/{controllers-connect-jzx7t4bn.js → controllers-connect-xp05dj6x.js} +20 -20
  70. package/dist/{cursor-adapter-qpwp5sfg.js → cursor-adapter-623qxqew.js} +1 -2
  71. package/dist/{db-4vq88jkc.js → db-47vj40dy.js} +3 -3
  72. package/dist/{db-v5ytg5pw.js → db-w759qmx0.js} +2 -2
  73. package/dist/{dispatcher-trtcrqae.js → dispatcher-7b8e4z2j.js} +17 -17
  74. package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
  75. package/dist/{dsh-adapter-f20xwn1z.js → dsh-adapter-2ezw3xea.js} +2 -3
  76. package/dist/{external-channel-n9bymc2n.js → external-channel-e2s028da.js} +2 -2
  77. package/dist/{grok-adapter-4kpmmnr6.js → grok-adapter-ys7gaqee.js} +6 -6
  78. package/dist/{handlers-ptsraqa8.js → handlers-wy1dr6ja.js} +20 -20
  79. package/dist/{heartbeat-60qtxtjf.js → heartbeat-w3mntghj.js} +20 -20
  80. package/dist/{hook-rbktcstg.js → hook-3jjywnpb.js} +9 -3
  81. package/dist/{hook-ngtvh729.js → hook-g7kvv6x9.js} +3 -2
  82. package/dist/{http-z8xy72gp.js → http-tkp7d4kb.js} +2984 -2483
  83. package/dist/{index-r70q0xjx.js → index-1ef1myq2.js} +17 -17
  84. package/dist/{index-a9nsavhx.js → index-3n8m9ea2.js} +1 -1
  85. package/dist/{index-cbxkmfc5.js → index-9pn1pfkf.js} +1 -1
  86. package/dist/{index-s193j5fh.js → index-nmm79qgr.js} +20 -20
  87. package/dist/{index-6srwvd5b.js → index-p4xfz37z.js} +1 -1
  88. package/dist/{index-06qkmcx6.js → index-q7m26kvs.js} +33 -23
  89. package/dist/{index-1cscynkb.js → index-xdaahthb.js} +17 -17
  90. package/dist/{keepalive-a9srcphj.js → keepalive-v4ta8wgg.js} +3 -3
  91. package/dist/{lead-3devneqy.js → lead-sawnyqxk.js} +35 -35
  92. package/dist/{maintenance-0er16h2y.js → maintenance-84n1ex21.js} +1 -1
  93. package/dist/{oauth-refresh-sweep-b65jcyw2.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
  94. package/dist/{onboard-wqywz5py.js → onboard-sfmpe4np.js} +2 -2
  95. package/dist/{opencode-adapter-e56086m1.js → opencode-adapter-jmpst4q4.js} +4 -4
  96. package/dist/{otel-impl-vk32s884.js → otel-impl-v6cc0zve.js} +1 -1
  97. package/dist/{pi-mono-adapter-tzps8f38.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
  98. package/dist/{pricing-refresh-t27anv4z.js → pricing-refresh-fsk1e1zc.js} +3 -3
  99. package/dist/{rbac-roles-452665y0.js → rbac-roles-prmkdnkw.js} +2 -2
  100. package/dist/{rbac-roles-bmcgsxjn.js → rbac-roles-zxmxqa39.js} +1 -1
  101. package/dist/{render-v2-0ttsk2j4.js → render-v2-pjwxvnkw.js} +3 -3
  102. package/dist/{resume-a26kvr41.js → resume-frdtgfa0.js} +9 -5
  103. package/dist/{session-access-dgh9nkwk.js → session-access-fpez4y5q.js} +4 -4
  104. package/dist/{setup-9vc9qahm.js → setup-mq6m3m8q.js} +1 -1
  105. package/dist/{work-objects-vn8nkxwn.js → work-objects-kfahhjsq.js} +20 -20
  106. package/dist/{worker-1x1h8vnm.js → worker-v5e5rxj3.js} +35 -35
  107. package/openapi.json +3078 -2424
  108. package/package.json +7 -7
  109. package/plugin/commands/create-pr.md +2 -2
  110. package/plugin/pi-skills/create-pr/SKILL.md +2 -2
  111. package/src/be/approval-config-question.ts +389 -0
  112. package/src/be/automation-preflight.ts +37 -3
  113. package/src/be/db/approvals.ts +48 -0
  114. package/src/be/db/session-tree.ts +73 -15
  115. package/src/be/db/skill-search.ts +87 -0
  116. package/src/be/db/tasks/read.ts +29 -2
  117. package/src/be/db/tasks/write.ts +15 -5
  118. package/src/be/db/workflow-idempotency.ts +183 -0
  119. package/src/be/db/workflow-runs.ts +366 -8
  120. package/src/be/db.ts +92 -22
  121. package/src/be/migrations/239_skills_fts.sql +40 -0
  122. package/src/be/migrations/240_workflow_run_parent_step_attempt.sql +37 -0
  123. package/src/be/migrations/241_skills_meta_fts.sql +39 -0
  124. package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
  125. package/src/be/sealed-json.ts +1 -0
  126. package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
  127. package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
  128. package/src/be/seed-skills/bundled-files.generated.json +1 -1
  129. package/src/be/sql-connections.ts +7 -1
  130. package/src/be/swarm-config-guard.ts +25 -0
  131. package/src/be/users.ts +9 -0
  132. package/src/commands/controller.ts +33 -0
  133. package/src/commands/runner.ts +6 -2
  134. package/src/extensions/apply-task-create.ts +57 -11
  135. package/src/extensions/catalog.generated.json +4 -4
  136. package/src/extensions/contract-types.generated.ts +1 -1
  137. package/src/extensions/contract.ts +25 -0
  138. package/src/extensions/dispatcher.ts +4 -0
  139. package/src/extensions/post-bridge.ts +8 -0
  140. package/src/hooks/context-bound-note.ts +196 -0
  141. package/src/hooks/hook.ts +12 -0
  142. package/src/http/approval-requests.ts +209 -38
  143. package/src/http/config.ts +70 -53
  144. package/src/http/idempotency-key.ts +117 -0
  145. package/src/http/sessions.ts +27 -2
  146. package/src/http/workflows.ts +129 -24
  147. package/src/integrations/chat-sdk/inbound.ts +5 -4
  148. package/src/prompts/context-bound-note.ts +48 -0
  149. package/src/prompts/session-templates.ts +25 -1
  150. package/src/providers/amp-adapter.ts +30 -8
  151. package/src/providers/claude-adapter.ts +29 -4
  152. package/src/providers/claude-managed-adapter.ts +26 -0
  153. package/src/providers/claude-sdk-session.ts +10 -0
  154. package/src/providers/claude-session-events.ts +13 -0
  155. package/src/providers/codex-adapter.ts +12 -0
  156. package/src/providers/dsh-adapter.ts +1 -1
  157. package/src/providers/opencode-adapter.ts +18 -0
  158. package/src/providers/pi-mono-adapter.ts +35 -0
  159. package/src/runtime-providers/docker.ts +6 -2
  160. package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
  161. package/src/tests/amp-adapter.test.ts +97 -0
  162. package/src/tests/approval-config-question.test.ts +702 -0
  163. package/src/tests/approval-requests.test.ts +10 -7
  164. package/src/tests/approval-sweeps.test.ts +12 -6
  165. package/src/tests/base-prompt.test.ts +1 -1
  166. package/src/tests/chat-sdk-inbound.test.ts +20 -0
  167. package/src/tests/claude-hook-output.test.ts +79 -1
  168. package/src/tests/claude-managed-adapter.test.ts +275 -0
  169. package/src/tests/codex-adapter.test.ts +163 -0
  170. package/src/tests/context-bound-note.test.ts +269 -0
  171. package/src/tests/dsh-adapter.test.ts +1 -1
  172. package/src/tests/entrypoint-gitlab-credential-helper.test.ts +273 -0
  173. package/src/tests/extensions-example-comms-adherence-gate.test.ts +321 -12
  174. package/src/tests/extensions-pre-task.test.ts +273 -0
  175. package/src/tests/fetch-resolved-env.test.ts +17 -0
  176. package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
  177. package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
  178. package/src/tests/fixtures/fake-amp.ts +22 -8
  179. package/src/tests/fixtures/held-template-render.ts +42 -0
  180. package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
  181. package/src/tests/list-endpoint-slimming.test.ts +33 -0
  182. package/src/tests/migration-240-workflow-run-parent-step-attempt.test.ts +55 -0
  183. package/src/tests/openapi-route-headers.test.ts +2 -0
  184. package/src/tests/opencode-adapter.test.ts +134 -0
  185. package/src/tests/pi-mono-adapter.test.ts +105 -0
  186. package/src/tests/prompt-template-session.test.ts +4 -3
  187. package/src/tests/runner-push-delivery.test.ts +25 -0
  188. package/src/tests/runtime-controller-liveness.test.ts +133 -0
  189. package/src/tests/runtime-providers-docker.test.ts +2 -0
  190. package/src/tests/script-connections.test.ts +9 -0
  191. package/src/tests/script-run-config-scope.test.ts +91 -3
  192. package/src/tests/session-logs.test.ts +70 -0
  193. package/src/tests/sessions-tree.test.ts +205 -1
  194. package/src/tests/skill-search-long-content.test.ts +193 -0
  195. package/src/tests/skill-search.test.ts +173 -22
  196. package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
  197. package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
  198. package/src/tests/workflow-idempotency.test.ts +915 -0
  199. package/src/tests/workflow-sub-workflow.test.ts +538 -1
  200. package/src/tools/request-human-input.ts +27 -5
  201. package/src/tools/workflows/cancel-workflow-run.ts +1 -1
  202. package/src/types.ts +7 -0
  203. package/src/utils/context-window.ts +8 -6
  204. package/src/workflows/approval-cancel.ts +15 -13
  205. package/src/workflows/approval-notifications.ts +15 -4
  206. package/src/workflows/checkpoint.ts +72 -35
  207. package/src/workflows/engine.ts +262 -93
  208. package/src/workflows/executors/base.ts +6 -0
  209. package/src/workflows/executors/foreach.ts +2 -2
  210. package/src/workflows/executors/human-in-the-loop.ts +25 -1
  211. package/src/workflows/executors/sub-workflow.ts +45 -16
  212. package/src/workflows/foreach-driver.ts +10 -3
  213. package/src/workflows/foreach-join.ts +29 -6
  214. package/src/workflows/recovery.ts +68 -1
  215. package/src/workflows/resume.ts +228 -77
  216. package/src/workflows/retry-poller.ts +74 -7
  217. package/src/workflows/task-step-routing.ts +29 -7
  218. package/templates/ai-toolbox.manifest.json +8 -8
  219. package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
  220. package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
  221. package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
  222. package/templates/extensions/comms-adherence-gate/README.md +38 -13
  223. package/templates/extensions/comms-adherence-gate/hooks.ts +272 -69
  224. package/templates/extensions/comms-adherence-gate/manifest.json +2 -2
  225. package/templates/skills/agent-browser/SKILL.md +4 -4
  226. package/templates/skills/agent-browser/content.md +4 -4
  227. package/templates/skills/code-quality/config.json +1 -1
  228. package/templates/skills/code-quality/content.md +2 -2
  229. package/templates/skills/delegate-work/content.md +29 -25
  230. package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
  231. package/templates/skills/qa/content.md +2 -2
  232. package/templates/skills/researching/content.md +2 -0
  233. package/templates/skills/turso-interaction/content.md +1 -1
  234. package/templates/skills/work-on-task/SKILL.md +1 -1
  235. package/templates/skills/work-on-task/content.md +1 -1
  236. package/templates/skills/workflow-iterate/content.md +8 -1
@@ -0,0 +1,40 @@
1
+ -- Full-text index for ranked skill search (searchSkills / skill-search tool).
2
+ -- Same shape as memory_fts: an id column FTS5 does not index, the searchable
3
+ -- text columns, and the porter tokenizer. Column order matters: searchSkills
4
+ -- passes bm25() weights positionally (skill_id, name, description, content).
5
+ CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
6
+ skill_id UNINDEXED,
7
+ name,
8
+ description,
9
+ content,
10
+ tokenize='porter unicode61'
11
+ );
12
+
13
+ INSERT INTO skills_fts(skill_id, name, description, content)
14
+ SELECT id, name, description, content FROM skills
15
+ WHERE id NOT IN (SELECT skill_id FROM skills_fts WHERE skill_id IS NOT NULL);
16
+
17
+ -- Triggers keep the index in sync on every write path, including FK cascade
18
+ -- deletes. A later migration that rebuilds `skills` (CREATE new + DROP +
19
+ -- RENAME) drops these triggers with the old table and must recreate them;
20
+ -- src/tests/skill-search.test.ts asserts they exist on the migrated schema.
21
+ CREATE TRIGGER IF NOT EXISTS skills_fts_insert
22
+ AFTER INSERT ON skills
23
+ BEGIN
24
+ INSERT INTO skills_fts(skill_id, name, description, content)
25
+ VALUES (NEW.id, NEW.name, NEW.description, NEW.content);
26
+ END;
27
+
28
+ CREATE TRIGGER IF NOT EXISTS skills_fts_update
29
+ AFTER UPDATE OF id, name, description, content ON skills
30
+ BEGIN
31
+ DELETE FROM skills_fts WHERE skill_id = OLD.id;
32
+ INSERT INTO skills_fts(skill_id, name, description, content)
33
+ VALUES (NEW.id, NEW.name, NEW.description, NEW.content);
34
+ END;
35
+
36
+ CREATE TRIGGER IF NOT EXISTS skills_fts_delete
37
+ AFTER DELETE ON skills
38
+ BEGIN
39
+ DELETE FROM skills_fts WHERE skill_id = OLD.id;
40
+ END;
@@ -0,0 +1,37 @@
1
+ -- Key a child workflow run by its parent step AND that step's attempt.
2
+ --
3
+ -- The retry poller re-executes a failed step under the same step id, with
4
+ -- `retryCount` bumped. Keyed by step id alone, the retried `sub-workflow`
5
+ -- step reconnected to the failed child of the previous attempt and failed
6
+ -- again without starting a new one.
7
+
8
+ ALTER TABLE workflow_runs ADD COLUMN parentStepAttempt INTEGER NOT NULL DEFAULT 0;
9
+
10
+ -- Before this migration a step had at most one child, created on whichever
11
+ -- attempt first got that far (a retry after a synchronous failure, such as a
12
+ -- disabled child workflow, creates it on attempt 1 or later), and every later
13
+ -- attempt reconnected to it. Hand that child to the attempt that settles or
14
+ -- re-executes it now, or recovery would look up the step's current attempt,
15
+ -- find no child and fail the parent as "deleted":
16
+ -- - a step queued for a retry (`failed` with `nextRetryAt`) starts its next
17
+ -- attempt at `retryCount`, which must start a new child, so the existing
18
+ -- child belongs to the previous attempt;
19
+ -- - any other step (running, waiting, terminal) owns it at `retryCount`.
20
+ -- A child whose step is gone keeps attempt 0.
21
+ UPDATE workflow_runs
22
+ SET parentStepAttempt = (
23
+ SELECT CASE
24
+ WHEN s.status = 'failed' AND s.nextRetryAt IS NOT NULL
25
+ THEN MAX(s.retryCount - 1, 0)
26
+ ELSE s.retryCount
27
+ END
28
+ FROM workflow_run_steps s
29
+ WHERE s.id = workflow_runs.parentStepId
30
+ )
31
+ WHERE parentStepId IS NOT NULL
32
+ AND EXISTS (SELECT 1 FROM workflow_run_steps s WHERE s.id = workflow_runs.parentStepId);
33
+
34
+ DROP INDEX IF EXISTS idx_workflow_runs_parentStepId;
35
+
36
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_runs_parentStep_attempt
37
+ ON workflow_runs(parentStepId, parentStepAttempt) WHERE parentStepId IS NOT NULL;
@@ -0,0 +1,39 @@
1
+ -- Name + description index for ranked skill search. skills_fts (239) scores
2
+ -- the whole row, and FTS5 bm25 length-normalizes over every column, so a
3
+ -- long SKILL.md body buries a name or description hit. This index holds only
4
+ -- the short fields; searchSkillRowsRanked adds its score to skills_fts's.
5
+ -- Column order matters: bm25() weights are positional (skill_id, name,
6
+ -- description).
7
+ CREATE VIRTUAL TABLE IF NOT EXISTS skills_meta_fts USING fts5(
8
+ skill_id UNINDEXED,
9
+ name,
10
+ description,
11
+ tokenize='porter unicode61'
12
+ );
13
+
14
+ INSERT INTO skills_meta_fts(skill_id, name, description)
15
+ SELECT id, name, description FROM skills
16
+ WHERE id NOT IN (SELECT skill_id FROM skills_meta_fts WHERE skill_id IS NOT NULL);
17
+
18
+ -- Same sync rules as skills_fts: a later migration that rebuilds `skills`
19
+ -- must recreate these triggers; src/tests/skill-search.test.ts asserts them.
20
+ CREATE TRIGGER IF NOT EXISTS skills_meta_fts_insert
21
+ AFTER INSERT ON skills
22
+ BEGIN
23
+ INSERT INTO skills_meta_fts(skill_id, name, description)
24
+ VALUES (NEW.id, NEW.name, NEW.description);
25
+ END;
26
+
27
+ CREATE TRIGGER IF NOT EXISTS skills_meta_fts_update
28
+ AFTER UPDATE OF id, name, description ON skills
29
+ BEGIN
30
+ DELETE FROM skills_meta_fts WHERE skill_id = OLD.id;
31
+ INSERT INTO skills_meta_fts(skill_id, name, description)
32
+ VALUES (NEW.id, NEW.name, NEW.description);
33
+ END;
34
+
35
+ CREATE TRIGGER IF NOT EXISTS skills_meta_fts_delete
36
+ AFTER DELETE ON skills
37
+ BEGIN
38
+ DELETE FROM skills_meta_fts WHERE skill_id = OLD.id;
39
+ END;
@@ -0,0 +1,40 @@
1
+ -- Idempotency-Key records for POST /api/workflows and POST /api/workflows/{id}/trigger.
2
+ --
3
+ -- One row is one operation the server accepted under a client key. It commits
4
+ -- in the same transaction as the workflow (create) or the run and its initial
5
+ -- state (trigger), so a retry after a lost response, a restart or a concurrent
6
+ -- duplicate finds it and gets the original result instead of a second
7
+ -- workflow or run.
8
+ --
9
+ -- The scope is (principal, operation, targetId, keyHash): the caller's stable
10
+ -- identity (never the bearer value), the operation, the triggered workflow id
11
+ -- ('' for create) and a SHA-256 of the client key. requestFingerprint is a
12
+ -- SHA-256 of the canonical request; a different request under the same key is
13
+ -- a conflict. responseBody is the original response, sealed (sealJson). When
14
+ -- the workflow is deleted, responseBody is cleared and resourceDeletedAt set:
15
+ -- the key stays taken and is never accepted again.
16
+ --
17
+ -- No FK to workflows: a row outlives the workflow as that tombstone.
18
+
19
+ CREATE TABLE IF NOT EXISTS workflow_idempotency_keys (
20
+ id TEXT PRIMARY KEY,
21
+ principal TEXT NOT NULL,
22
+ operation TEXT NOT NULL CHECK (operation IN ('workflow.create', 'workflow.trigger')),
23
+ targetId TEXT NOT NULL DEFAULT '',
24
+ keyHash TEXT NOT NULL,
25
+ requestFingerprint TEXT NOT NULL,
26
+ workflowId TEXT NOT NULL,
27
+ runId TEXT,
28
+ responseStatus INTEGER NOT NULL,
29
+ responseBody TEXT,
30
+ resourceDeletedAt TEXT,
31
+ createdAt TEXT NOT NULL,
32
+ created_by TEXT,
33
+ updated_by TEXT
34
+ );
35
+
36
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_workflow_idempotency_keys_scope
37
+ ON workflow_idempotency_keys(principal, operation, targetId, keyHash);
38
+
39
+ CREATE INDEX IF NOT EXISTS idx_workflow_idempotency_keys_workflowId
40
+ ON workflow_idempotency_keys(workflowId);
@@ -30,6 +30,7 @@ export const SEALED_REPLAY_COLUMNS: readonly { table: string; column: string }[]
30
30
  { table: "workflow_run_steps", column: "output_replay" },
31
31
  { table: "workflow_runs", column: "context_replay" },
32
32
  { table: "approval_requests", column: "responses" },
33
+ { table: "workflow_idempotency_keys", column: "responseBody" },
33
34
  ];
34
35
 
35
36
  /** SQL `EXISTS` arms matching a sealed value in each `SEALED_REPLAY_COLUMNS` entry. */
@@ -574,14 +574,14 @@ export default async function compoundInsights(args: any, ctx: any) {
574
574
  const rows = rowsToObjects(
575
575
  await ctx.swarm.db_query({
576
576
  sql: `WITH raw AS (
577
- SELECT sessionId, iteration, lineNumber, content,
577
+ SELECT rowid AS seq, sessionId, iteration, lineNumber, content,
578
578
  json_extract(content, '$.tool_name') as jsonToolName
579
579
  FROM session_logs
580
580
  WHERE createdAt > ${w}
581
581
  AND (content LIKE '%"type":"tool_use"%' OR json_extract(content, '$.tool_name') IS NOT NULL)
582
582
  )
583
583
  SELECT sessionId, iteration, lineNumber, jsonToolName, content
584
- FROM raw ORDER BY sessionId, iteration, lineNumber LIMIT 100`,
584
+ FROM raw ORDER BY sessionId, iteration, seq LIMIT 100`,
585
585
  }),
586
586
  );
587
587
  const bySession = new Map<string, string[]>();
@@ -574,14 +574,14 @@ export default async function compoundInsights(args: any, ctx: any) {
574
574
  const rows = rowsToObjects(
575
575
  await ctx.swarm.db_query({
576
576
  sql: `WITH raw AS (
577
- SELECT sessionId, iteration, lineNumber, content,
577
+ SELECT rowid AS seq, sessionId, iteration, lineNumber, content,
578
578
  json_extract(content, '$.tool_name') as jsonToolName
579
579
  FROM session_logs
580
580
  WHERE createdAt > ${w}
581
581
  AND (content LIKE '%"type":"tool_use"%' OR json_extract(content, '$.tool_name') IS NOT NULL)
582
582
  )
583
583
  SELECT sessionId, iteration, lineNumber, jsonToolName, content
584
- FROM raw ORDER BY sessionId, iteration, lineNumber LIMIT 100`,
584
+ FROM raw ORDER BY sessionId, iteration, seq LIMIT 100`,
585
585
  }),
586
586
  );
587
587
  const bySession = new Map<string, string[]>();
@@ -18,7 +18,7 @@
18
18
  "delegate-work": [
19
19
  {
20
20
  "path": "scripts/codex-exec.sh",
21
- "content": "#!/usr/bin/env bash\n#\n# codex-exec.sh — invoke `codex exec` (non-interactive) for delegated\n# implementation work. This is the SINGLE SOURCE OF TRUTH for the invocation\n# flags. The delegate-work skill calls only this script; its routing matrix\n# decides WHICH gpt-5.6 variant (-m) and effort (-e) each task gets:\n# luna = mechanical/bounded terra = everyday frozen-spec slice\n# sol = hard/long-horizon effort: medium|high|xhigh|max (never ultra)\n#\n# Usage:\n# codex-exec.sh [-m <model>] [-e <effort>] -C <workdir> -o <last-message-file> [-l <log-file>] \"PROMPT\"\n# printf '%s' \"$PROMPT\" | codex-exec.sh -C <workdir> -o <file> # prompt via stdin (preferred)\n#\n# Notes:\n# * -C sets codex's working root (the git worktree, for parallel isolation).\n# * -o writes codex's final message (its report) to a file you can read back.\n# * Reads the prompt from stdin when no positional PROMPT is given.\n# * In `exec` mode approvals are already \"never\", so nothing hangs waiting for\n# a human. The sandbox mode below bounds what codex may WRITE.\n#\n# Sandbox default: `workspace-write` — codex may freely edit files inside its\n# working root (the worktree) and read elsewhere on disk (so it can read the\n# plan by absolute path), but cannot write outside the workspace. This keeps\n# parallel worktree runs from clobbering each other or the main tree.\n#\n# Env overrides (flags -m/-e take precedence):\n# CODEX_MODEL default: gpt-5.6-sol gpt-5.6-sol | gpt-5.6-terra | gpt-5.6-luna\n# CODEX_EFFORT default: high reasoning effort (never \"fast\"/low)\n# CODEX_SANDBOX default: workspace-write (passed to codex's -s option)\n# CODEX_BYPASS set to 1 to use --dangerously-bypass-approvals-and-sandbox\n# (fully unsandboxed — opt in only when a phase must write\n# outside its worktree; overrides CODEX_SANDBOX)\nset -euo pipefail\n\nWORKDIR=\".\"\nOUTFILE=\"\"\nLOGFILE=\"\"\nPROMPT=\"\"\nMODEL_FLAG=\"\"\nEFFORT_FLAG=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n -m|--model) MODEL_FLAG=\"$2\"; shift 2 ;;\n -e|--effort) EFFORT_FLAG=\"$2\"; shift 2 ;;\n -C|--cd) WORKDIR=\"$2\"; shift 2 ;;\n -o|--out) OUTFILE=\"$2\"; shift 2 ;;\n -l|--log) LOGFILE=\"$2\"; shift 2 ;;\n --) shift; PROMPT=\"${1:-}\"; break ;;\n -*) echo \"codex-exec.sh: unknown flag: $1\" >&2; exit 2 ;;\n *) PROMPT=\"$1\"; shift ;;\n esac\ndone\n\nMODEL=\"${MODEL_FLAG:-${CODEX_MODEL:-gpt-5.6-sol}}\"\nEFFORT=\"${EFFORT_FLAG:-${CODEX_EFFORT:-high}}\"\nSANDBOX=\"${CODEX_SANDBOX:-workspace-write}\"\n\nargs=( exec\n -m \"$MODEL\"\n -c \"model_reasoning_effort=\\\"$EFFORT\\\"\"\n -C \"$WORKDIR\"\n)\nif [[ \"${CODEX_BYPASS:-0}\" == \"1\" ]]; then\n args+=( --dangerously-bypass-approvals-and-sandbox )\nelse\n args+=( -s \"$SANDBOX\" )\nfi\n[[ -n \"$OUTFILE\" ]] && args+=( -o \"$OUTFILE\" )\n\n# Prompt source: positional arg if present, otherwise stdin (`-`).\nif [[ -n \"$PROMPT\" ]]; then\n set -- \"$PROMPT\"\nelse\n set -- \"-\"\nfi\n\nif [[ -n \"$LOGFILE\" ]]; then\n codex \"${args[@]}\" \"$@\" 2>&1 | tee \"$LOGFILE\"\nelse\n codex \"${args[@]}\" \"$@\"\nfi\n"
21
+ "content": "#!/usr/bin/env bash\n#\n# codex-exec.sh — invoke `codex exec` (non-interactive) for delegated\n# implementation work. This is the SINGLE SOURCE OF TRUTH for the invocation\n# flags. The delegate-work skill calls only this script; its routing matrix\n# decides WHICH gpt-6 variant (-m) and effort (-e) each task gets:\n# gpt-6-luna = trivial/mechanical gpt-6.1-sol = default (medium) to hard (high/xhigh)\n# gpt-6-astra = escalation only effort: medium|high|xhigh|max (never ultra)\n#\n# Usage:\n# codex-exec.sh [-m <model>] [-e <effort>] -C <workdir> -o <last-message-file> [-l <log-file>] \"PROMPT\"\n# printf '%s' \"$PROMPT\" | codex-exec.sh -C <workdir> -o <file> # prompt via stdin (preferred)\n#\n# Notes:\n# * -C sets codex's working root (the git worktree, for parallel isolation).\n# * -o writes codex's final message (its report) to a file you can read back.\n# * Reads the prompt from stdin when no positional PROMPT is given.\n# * In `exec` mode approvals are already \"never\", so nothing hangs waiting for\n# a human. The sandbox mode below bounds what codex may WRITE.\n#\n# Sandbox default: `workspace-write` — codex may freely edit files inside its\n# working root (the worktree) and read elsewhere on disk (so it can read the\n# plan by absolute path), but cannot write outside the workspace. This keeps\n# parallel worktree runs from clobbering each other or the main tree.\n#\n# Env overrides (flags -m/-e take precedence):\n# CODEX_MODEL default: gpt-6.1-sol gpt-6.1-sol | gpt-6-astra | gpt-6-luna\n# CODEX_EFFORT default: high reasoning effort (never \"fast\"/low)\n# CODEX_SANDBOX default: workspace-write (passed to codex's -s option)\n# CODEX_BYPASS set to 1 to use --dangerously-bypass-approvals-and-sandbox\n# (fully unsandboxed — opt in only when a phase must write\n# outside its worktree; overrides CODEX_SANDBOX)\nset -euo pipefail\n\nWORKDIR=\".\"\nOUTFILE=\"\"\nLOGFILE=\"\"\nPROMPT=\"\"\nMODEL_FLAG=\"\"\nEFFORT_FLAG=\"\"\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n -m|--model) MODEL_FLAG=\"$2\"; shift 2 ;;\n -e|--effort) EFFORT_FLAG=\"$2\"; shift 2 ;;\n -C|--cd) WORKDIR=\"$2\"; shift 2 ;;\n -o|--out) OUTFILE=\"$2\"; shift 2 ;;\n -l|--log) LOGFILE=\"$2\"; shift 2 ;;\n --) shift; PROMPT=\"${1:-}\"; break ;;\n -*) echo \"codex-exec.sh: unknown flag: $1\" >&2; exit 2 ;;\n *) PROMPT=\"$1\"; shift ;;\n esac\ndone\n\nMODEL=\"${MODEL_FLAG:-${CODEX_MODEL:-gpt-6.1-sol}}\"\nEFFORT=\"${EFFORT_FLAG:-${CODEX_EFFORT:-high}}\"\nSANDBOX=\"${CODEX_SANDBOX:-workspace-write}\"\n\nargs=( exec\n -m \"$MODEL\"\n -c \"model_reasoning_effort=\\\"$EFFORT\\\"\"\n -C \"$WORKDIR\"\n)\nif [[ \"${CODEX_BYPASS:-0}\" == \"1\" ]]; then\n args+=( --dangerously-bypass-approvals-and-sandbox )\nelse\n args+=( -s \"$SANDBOX\" )\nfi\n[[ -n \"$OUTFILE\" ]] && args+=( -o \"$OUTFILE\" )\n\n# Prompt source: positional arg if present, otherwise stdin (`-`).\nif [[ -n \"$PROMPT\" ]]; then\n set -- \"$PROMPT\"\nelse\n set -- \"-\"\nfi\n\nif [[ -n \"$LOGFILE\" ]]; then\n codex \"${args[@]}\" \"$@\" 2>&1 | tee \"$LOGFILE\"\nelse\n codex \"${args[@]}\" \"$@\"\nfi\n"
22
22
  }
23
23
  ],
24
24
  "design-docs": [
@@ -51,7 +51,13 @@ export async function resolveSqlDsn(
51
51
  // so the parameterless case goes through the tagged-template path instead. Never use `.simple()`.
52
52
  // The statement runs as a server-side cursor fetched in batches, so the API never holds more than
53
53
  // one batch past the caps. DECLARE only accepts SELECT, VALUES, TABLE and WITH ... SELECT.
54
- const asTemplate = (text: string): TemplateStringsArray => Object.assign([text], { raw: [text] });
54
+ // Bun >= 1.4.4 rejects an enumerable `raw`, so shape it like a real (frozen) template object.
55
+ const asTemplate = (text: string): TemplateStringsArray =>
56
+ Object.freeze(
57
+ Object.defineProperty([text], "raw", { value: Object.freeze([text]) }) as string[] & {
58
+ raw: readonly string[];
59
+ },
60
+ );
55
61
 
56
62
  const defaultExecutor: SqlExecutor = async (dsn, query, params, take) => {
57
63
  // Bun.SQL global, not `import { SQL } from "bun"`: the node-target CLI bundle cannot import "bun".
@@ -610,6 +610,8 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
610
610
  // (~80k chars) it risks the SIGTERM-143 context-saturation failure mode
611
611
  // the cap exists to prevent (see context-preamble.ts).
612
612
  ...boundedIntegerValidators(["CONTEXT_PREAMBLE_MAX_TOKENS"], 100, 20000),
613
+ // 0 turns the one-time context-bound note off; no model window reaches 10M.
614
+ ...boundedIntegerValidators(["CONTEXT_BOUND_NOTE_TOKENS"], 0, 10_000_000),
613
615
  MEMORY_MIN_SIMILARITY: (value) =>
614
616
  validateFloatRange("MEMORY_MIN_SIMILARITY", value, 0, 1, "between 0 and 1 inclusive"),
615
617
  MEMORY_ACCESS_BOOST_MAX: (value) =>
@@ -628,6 +630,29 @@ export function validateConfigValue(key: string, value: unknown): string | null
628
630
  return validator ? validator(value) : null;
629
631
  }
630
632
 
633
+ /**
634
+ * Every check `PUT /api/config` runs before a write: scope and scopeId shape,
635
+ * reserved and internal keys, the per-key value validator and the env-export
636
+ * rule. Shared with HITL `config` questions so the two writers cannot drift.
637
+ */
638
+ export function validateConfigEntry(entry: {
639
+ scope: "global" | "agent" | "repo";
640
+ scopeId?: string | null;
641
+ key: string;
642
+ value: unknown;
643
+ exportToEnv?: boolean;
644
+ }): string | null {
645
+ if (entry.scope === "global" && entry.scopeId) return "Global scope must not have scopeId";
646
+ if ((entry.scope === "agent" || entry.scope === "repo") && !entry.scopeId) {
647
+ return "Agent/repo scope requires scopeId";
648
+ }
649
+ if (isReservedConfigKey(entry.key)) return reservedKeyError(entry.key).message;
650
+ if (isInternalConfigKey(entry.key)) return internalConfigKeyError(entry.key).message;
651
+ return (
652
+ validateConfigValue(entry.key, entry.value) ?? validateExportToEnv(entry.key, entry.exportToEnv)
653
+ );
654
+ }
655
+
631
656
  /**
632
657
  * Rejects `exportToEnv: true` for keys only the API server reads. Omitting the
633
658
  * flag is fine: the write path stores false for those keys.
package/src/be/users.ts CHANGED
@@ -632,6 +632,15 @@ export async function mintScriptRunToken(
632
632
  return await insertSessionToken(agentId, null, null, ttlMs, null, undefined, null, null, true);
633
633
  }
634
634
 
635
+ /** Whether a session token was minted by `mintScriptRunToken`. */
636
+ export async function isScriptRunToken(tokenId: string): Promise<boolean> {
637
+ const row = await getDbClient().get<{ scriptRun: number }>(
638
+ "SELECT scriptRun FROM session_tokens WHERE id = ?",
639
+ [tokenId],
640
+ );
641
+ return row?.scriptRun === 1;
642
+ }
643
+
635
644
  export async function insertSessionToken(
636
645
  agentId: string,
637
646
  taskId: string | null,
@@ -1,6 +1,7 @@
1
1
  import { createRuntimeProvider } from "../runtime-providers";
2
2
  import { BuiltinController } from "../runtime-providers/controller";
3
3
  import {
4
+ CONTROLLER_PING_INTERVAL_MS,
4
5
  CONTROLLER_PROTOCOL_VERSION,
5
6
  type ControllerCommand,
6
7
  ControllerServerMessageSchema,
@@ -37,13 +38,21 @@ async function executeCommand(
37
38
  }
38
39
  }
39
40
 
41
+ // A network cut never closes the socket on this side, so the client watches inbound
42
+ // traffic. The API pings every CONTROLLER_PING_INTERVAL_MS and the client pings on
43
+ // each check; Bun surfaces both as `ping` / `pong` events. Three ping intervals
44
+ // without any frame means the link is half-open.
45
+ export const CONTROLLER_LIVENESS_TIMEOUT_MS = 3 * CONTROLLER_PING_INTERVAL_MS;
46
+
40
47
  export function startControllerClient(options: {
41
48
  apiUrl: string;
42
49
  token: string;
43
50
  provider: RuntimeProvider;
44
51
  target: RuntimeTargetConfig;
45
52
  onFatal?: () => void;
53
+ livenessTimeoutMs?: number;
46
54
  }): { stop: () => void } {
55
+ const livenessTimeoutMs = options.livenessTimeoutMs ?? CONTROLLER_LIVENESS_TIMEOUT_MS;
47
56
  const url = new URL("/api/controllers/connect", options.apiUrl);
48
57
  if (url.protocol !== "http:" && url.protocol !== "https:")
49
58
  throw new Error("Controller API URL must use HTTP or HTTPS");
@@ -52,6 +61,7 @@ export function startControllerClient(options: {
52
61
  let stopped = false;
53
62
  let socket: WebSocket | undefined;
54
63
  let reconnect: ReturnType<typeof setTimeout> | undefined;
64
+ let liveness: ReturnType<typeof setInterval> | undefined;
55
65
  let attempts = 0;
56
66
  const log = (message: string) => console.log(scrubSecrets(`[runtime-controller] ${message}`));
57
67
 
@@ -63,7 +73,28 @@ export function startControllerClient(options: {
63
73
  socket = ws;
64
74
  let controller: BuiltinController | undefined;
65
75
  let connectedAt = 0;
76
+ let lastTrafficAt = Date.now();
77
+ const markAlive = () => {
78
+ lastTrafficAt = Date.now();
79
+ };
80
+ ws.addEventListener("ping", markAlive);
81
+ ws.addEventListener("pong", markAlive);
82
+ clearInterval(liveness);
83
+ liveness = setInterval(
84
+ () => {
85
+ if (socket !== ws) return;
86
+ if (Date.now() - lastTrafficAt > livenessTimeoutMs) {
87
+ log(`No traffic for ${livenessTimeoutMs}ms. Dropping the connection`);
88
+ clearInterval(liveness);
89
+ ws.terminate();
90
+ return;
91
+ }
92
+ if (ws.readyState === WebSocket.OPEN) ws.ping();
93
+ },
94
+ Math.ceil(livenessTimeoutMs / 3),
95
+ );
66
96
  ws.onmessage = (event) => {
97
+ markAlive();
67
98
  void (async () => {
68
99
  const message = ControllerServerMessageSchema.parse(JSON.parse(String(event.data)));
69
100
  if (message.type === "welcome") {
@@ -106,6 +137,7 @@ export function startControllerClient(options: {
106
137
  ws.onerror = () => log("Connection failed");
107
138
  ws.onclose = (event) => {
108
139
  if (stopped || socket !== ws) return;
140
+ clearInterval(liveness);
109
141
  if (event.code === 1008) {
110
142
  stopped = true;
111
143
  log("Controller access rejected or token revoked. Mint a token and restart this process.");
@@ -129,6 +161,7 @@ export function startControllerClient(options: {
129
161
  stop: () => {
130
162
  stopped = true;
131
163
  if (reconnect) clearTimeout(reconnect);
164
+ clearInterval(liveness);
132
165
  socket?.close(1000, "Controller stopped");
133
166
  },
134
167
  };
@@ -3352,6 +3352,8 @@ export interface PollOptions {
3352
3352
  pollTimeout: number;
3353
3353
  /** Push scheduling fetches once, then returns control to the runner. */
3354
3354
  singlePoll?: boolean;
3355
+ /** Stop an open poll window before its next claim request. */
3356
+ shouldStop?: () => boolean;
3355
3357
  /** Distinguishes a failed fetch from an empty successful response. */
3356
3358
  onPollFailure?: () => void;
3357
3359
  since?: string; // Optional: for filtering finished tasks
@@ -3669,6 +3671,7 @@ async function pollForTriggerOnce(opts: PollOptions): Promise<Trigger | null> {
3669
3671
  }
3670
3672
 
3671
3673
  while (Date.now() - startTime < opts.pollTimeout) {
3674
+ if (opts.shouldStop?.()) return null;
3672
3675
  try {
3673
3676
  // Build URL with optional since parameter
3674
3677
  let url = `${opts.apiUrl}/api/poll`;
@@ -7099,7 +7102,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7099
7102
  }
7100
7103
  }
7101
7104
 
7102
- // Only poll if we have capacity and the API is not draining
7105
+ // Only poll if we have capacity and neither the API nor worker is draining
7103
7106
  const schedule = pushDelivery.schedule({
7104
7107
  mode: state.deliveryMode ?? "poll",
7105
7108
  push: deliveryChecks.push,
@@ -7108,7 +7111,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7108
7111
  exitAfterTasks === null
7109
7112
  ? state.maxConcurrent
7110
7113
  : Math.min(state.maxConcurrent, Math.max(0, exitAfterTasks - state.tasksProcessed)),
7111
- draining: state.apiDraining,
7114
+ draining: state.apiDraining || shuttingDown,
7112
7115
  lastPushPingAt,
7113
7116
  pollInterval: PollIntervalMs,
7114
7117
  });
@@ -7146,6 +7149,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
7146
7149
  runtimeInstanceId,
7147
7150
  pollTimeout: effectiveTimeout,
7148
7151
  singlePoll: schedule.singlePoll,
7152
+ shouldStop: () => shuttingDown,
7149
7153
  onPollFailure,
7150
7154
  harnessProvider: state.harnessProvider,
7151
7155
  onApiDrainSignal: (draining) => applyApiDrainSignal(state, role, draining),
@@ -2,7 +2,8 @@ import { explicitModelErrorForAgent } from "../be/model-validation";
2
2
  import type { RequestInfo } from "../tools/utils";
3
3
  import { type CreateTaskOptions, CreateTaskOptionsSchema } from "../types";
4
4
  import { scrubSecrets } from "../utils/secret-scrubber";
5
- import type { TaskCreateModify, TaskCreateOrigin } from "./contract";
5
+ import { workflowEventBus } from "../workflows/event-bus";
6
+ import type { TaskBlockedEvent, TaskCreateModify, TaskCreateOrigin } from "./contract";
6
7
  import { dispatchPre, extensionIdForAgent, listRegistered } from "./dispatcher";
7
8
 
8
9
  const MODIFIABLE_OPTION_KEYS = [
@@ -69,6 +70,14 @@ function warnDroppedKey(extensionName: string, key: string): void {
69
70
  );
70
71
  }
71
72
 
73
+ function scrubFields<T extends object>(fields: T): T {
74
+ return Object.fromEntries(
75
+ Object.entries(fields)
76
+ .filter(([, value]) => value !== undefined)
77
+ .map(([key, value]) => [key, typeof value === "string" ? scrubSecrets(value) : value]),
78
+ ) as T;
79
+ }
80
+
72
81
  export async function applyPreTaskCreate(
73
82
  args: ApplyPreTaskCreateArgs,
74
83
  ): Promise<ApplyPreTaskCreateResult> {
@@ -80,6 +89,48 @@ export async function applyPreTaskCreate(
80
89
  ? (`extension:${creatingExtension.record.name}` as const)
81
90
  : args.origin;
82
91
 
92
+ const blocked = (
93
+ reason: string,
94
+ extension: TaskBlockedEvent["extension"],
95
+ ): ApplyPreTaskCreateResult => {
96
+ const { options, requestInfo } = args;
97
+ const event: TaskBlockedEvent = {
98
+ extension,
99
+ reason: scrubSecrets(reason),
100
+ origin,
101
+ source: options.source,
102
+ requester: scrubFields({
103
+ userId: options.requestedByUserId,
104
+ agentId: requestInfo?.agentId ?? options.creatorAgentId,
105
+ slackUserId: options.slackUserId,
106
+ }),
107
+ context: scrubFields({
108
+ contextKey: options.contextKey ?? requestInfo?.contextKey,
109
+ scheduleId: options.scheduleId,
110
+ workflowRunId: options.workflowRunId,
111
+ workflowRunStepId: options.workflowRunStepId,
112
+ parentTaskId: options.parentTaskId,
113
+ vcsRepo: options.vcsRepo,
114
+ vcsNumber: options.vcsNumber,
115
+ slackChannelId: options.slackChannelId,
116
+ slackThreadTs: options.slackThreadTs,
117
+ }),
118
+ description: scrubSecrets(args.description).slice(0, 500),
119
+ };
120
+ try {
121
+ workflowEventBus.emit("task.create_blocked", {
122
+ ...event,
123
+ skipExtensionId: creatingExtensionId,
124
+ });
125
+ } catch (error) {
126
+ console.error(
127
+ "[extensions] task.create_blocked event not emitted:",
128
+ scrubSecrets(error instanceof Error ? error.message : String(error)),
129
+ );
130
+ }
131
+ return { kind: "blocked", reason: event.reason, extension };
132
+ };
133
+
83
134
  // The last extension that rewrote `model` or `agentId`. The final pair is re-checked
84
135
  // against the assignee's harness, because the caller validated the pre-hook pair only.
85
136
  let modelEditor: { id: string; name: string } | undefined;
@@ -125,11 +176,7 @@ export async function applyPreTaskCreate(
125
176
  );
126
177
 
127
178
  if (result.action === "block") {
128
- return {
129
- kind: "blocked",
130
- reason: result.reason,
131
- extension: result.extension,
132
- };
179
+ return blocked(result.reason, result.extension);
133
180
  }
134
181
  if (result.action === "continue") {
135
182
  return { kind: "proceed", description: args.description, options: args.options };
@@ -146,11 +193,10 @@ export async function applyPreTaskCreate(
146
193
  agentId: options.agentId,
147
194
  });
148
195
  if (modelError) {
149
- return {
150
- kind: "blocked",
151
- reason: `pre.task.create rewrite from extension "${modelEditor.name}" is invalid: ${modelError}`,
152
- extension: modelEditor,
153
- };
196
+ return blocked(
197
+ `pre.task.create rewrite from extension "${modelEditor.name}" is invalid: ${modelError}`,
198
+ modelEditor,
199
+ );
154
200
  }
155
201
  }
156
202
  return {