@desplega.ai/agent-swarm 2.0.0 → 2.1.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 (238) hide show
  1. package/dist/{acp-adapter-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
  2. package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
  3. package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
  4. package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
  5. package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
  6. package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
  7. package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
  8. package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
  9. package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
  10. package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
  11. package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
  12. package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
  13. package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
  14. package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
  15. package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
  16. package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
  17. package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
  18. package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
  19. package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
  20. package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
  21. package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
  22. package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
  23. package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
  24. package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
  25. package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
  26. package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
  27. package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
  28. package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
  29. package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
  30. package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
  31. package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
  32. package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
  33. package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
  34. package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
  35. package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
  36. package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
  37. package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
  38. package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
  39. package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
  40. package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
  41. package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
  42. package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
  43. package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
  44. package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
  45. package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
  46. package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
  47. package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
  48. package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
  49. package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
  50. package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
  51. package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
  52. package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
  53. package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
  54. package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
  55. package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
  56. package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
  57. package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
  58. package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
  59. package/dist/cli-wfnemr1t.js +86 -0
  60. package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
  61. package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
  62. package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
  63. package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
  64. package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
  65. package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
  66. package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
  67. package/dist/cli.js +24 -23
  68. package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
  69. package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
  70. package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
  71. package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
  72. package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
  73. package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
  74. package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
  75. package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
  76. package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
  77. package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
  78. package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
  79. package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
  80. package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
  81. package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
  82. package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
  83. package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
  84. package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
  85. package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
  86. package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
  87. package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
  88. package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
  89. package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
  90. package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
  91. package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
  92. package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
  93. package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
  94. package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
  95. package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
  96. package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
  97. package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
  98. package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
  99. package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
  101. package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
  102. package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
  103. package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
  104. package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
  105. package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
  106. package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
  107. package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
  108. package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
  109. package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
  110. package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
  111. package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
  112. package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
  113. package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
  114. package/openapi.json +777 -6
  115. package/package.json +8 -7
  116. package/src/agentmail/handlers.ts +21 -11
  117. package/src/apps/definition.ts +16 -0
  118. package/src/azure-devops/api.ts +52 -2
  119. package/src/azure-devops/boards-sync.ts +50 -20
  120. package/src/azure-devops/index.ts +1 -0
  121. package/src/azure-devops/outbound.ts +95 -0
  122. package/src/be/boot-seeds.ts +11 -0
  123. package/src/be/db/model-profiles.ts +315 -0
  124. package/src/be/db/tasks/read.ts +7 -0
  125. package/src/be/db.ts +53 -47
  126. package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
  127. package/src/be/migrations/220_model_profiles.sql +137 -0
  128. package/src/be/model-tier-resolution.ts +145 -11
  129. package/src/be/model-validation.ts +8 -1
  130. package/src/be/scripts/typecheck.ts +12 -0
  131. package/src/be/secret-registry.ts +6 -2
  132. package/src/be/seed-model-profiles.ts +41 -0
  133. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
  134. package/src/be/seed-scripts/index.ts +9 -0
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/runner.ts +3 -17
  137. package/src/extensions/catalog.generated.json +17 -0
  138. package/src/extensions/contract-types.generated.ts +1 -1
  139. package/src/extensions/loader.ts +10 -5
  140. package/src/heartbeat/heartbeat.ts +2 -1
  141. package/src/http/agents.ts +49 -2
  142. package/src/http/all-routes.ts +1 -0
  143. package/src/http/apps.ts +30 -3
  144. package/src/http/config.ts +26 -2
  145. package/src/http/core.ts +4 -0
  146. package/src/http/index.ts +11 -1
  147. package/src/http/model-profiles.ts +303 -0
  148. package/src/http/pages-public.ts +11 -5
  149. package/src/http/poll.ts +16 -5
  150. package/src/http/script-runs.ts +1 -1
  151. package/src/http/scripts.ts +25 -2
  152. package/src/http/tasks.ts +18 -0
  153. package/src/http/webhooks.ts +1 -2
  154. package/src/pages/feedback-overlay.ts +65 -6
  155. package/src/providers/acp-targets.ts +1 -1
  156. package/src/providers/amp-adapter.ts +1 -1
  157. package/src/providers/dsh-adapter.ts +1 -1
  158. package/src/providers/pi-mono-extension.ts +17 -3
  159. package/src/providers/reasoning-effort.ts +10 -0
  160. package/src/rbac/legacy-policy.ts +8 -0
  161. package/src/rbac/permissions.ts +5 -0
  162. package/src/scheduler/deferred-task-waits.ts +18 -5
  163. package/src/scheduler/scheduler.ts +7 -1
  164. package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
  165. package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
  166. package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
  167. package/src/scripts-runtime/extract-args-schema.ts +9 -8
  168. package/src/scripts-runtime/quickjs-compat.ts +464 -0
  169. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  170. package/src/scripts-runtime/types/stdlib.d.ts +12 -0
  171. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
  173. package/src/server.ts +10 -1
  174. package/src/slack/blocks.ts +4 -2
  175. package/src/slack/reaction-shortcode.ts +14 -0
  176. package/src/slack/render-v2.ts +12 -7
  177. package/src/slack/responses.ts +5 -4
  178. package/src/slack/thread-buffer.ts +41 -22
  179. package/src/tasks/worker-follow-up.ts +11 -2
  180. package/src/tests/agentmail-filters.test.ts +47 -0
  181. package/src/tests/apps-spike2.test.ts +35 -0
  182. package/src/tests/azure-devops-outbound.test.ts +420 -0
  183. package/src/tests/db-task-read-contract.test.ts +2 -0
  184. package/src/tests/db-task-write-contract.test.ts +1 -0
  185. package/src/tests/deferred-task-wake.test.ts +78 -1
  186. package/src/tests/docs-index-footprint.test.js +2 -2
  187. package/src/tests/dsh-adapter.test.ts +1 -1
  188. package/src/tests/env-flag.test.ts +1 -0
  189. package/src/tests/extensions-catalog.test.ts +1 -0
  190. package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
  191. package/src/tests/extensions-loader.test.ts +15 -0
  192. package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
  193. package/src/tests/heartbeat-jev-gate.test.ts +385 -0
  194. package/src/tests/model-profile-backcompat.test.ts +327 -0
  195. package/src/tests/model-profile-providers.test.ts +302 -0
  196. package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
  197. package/src/tests/model-profiles-api.test.ts +614 -0
  198. package/src/tests/pages-feedback-keys.test.ts +121 -0
  199. package/src/tests/pages-feedback.test.ts +3 -0
  200. package/src/tests/pi-context-compaction.test.ts +193 -0
  201. package/src/tests/profile-claim.test.ts +269 -0
  202. package/src/tests/quickjs-compat.test.ts +213 -0
  203. package/src/tests/rbac-engine.test.ts +21 -0
  204. package/src/tests/script-connections.test.ts +4 -0
  205. package/src/tests/script-executor-conformance.test.ts +252 -1
  206. package/src/tests/script-executor-quickjs.test.ts +96 -0
  207. package/src/tests/script-runs-http.test.ts +89 -0
  208. package/src/tests/scripts-http.test.ts +16 -0
  209. package/src/tests/secret-scrubber.test.ts +10 -0
  210. package/src/tests/seed-model-profiles.test.ts +174 -0
  211. package/src/tests/seed-scripts.test.ts +2 -2
  212. package/src/tests/skill-invocations.test.ts +4 -2
  213. package/src/tests/slack-blocks.test.ts +36 -0
  214. package/src/tests/slack-inline-output.test.ts +14 -0
  215. package/src/tests/slack-reaction-config.test.ts +20 -0
  216. package/src/tests/slack-render-v2.test.ts +37 -0
  217. package/src/tests/slack-thread-buffer.test.ts +98 -1
  218. package/src/tests/workflow-llm.test.ts +61 -0
  219. package/src/tools/model-profiles/list-model-profiles.ts +54 -0
  220. package/src/tools/request-human-input.ts +11 -3
  221. package/src/tools/script-apis.ts +5 -1
  222. package/src/tools/script-upsert.ts +4 -1
  223. package/src/tools/send-task.ts +24 -1
  224. package/src/tools/system-one-decide.ts +104 -0
  225. package/src/tools/task-action.ts +22 -1
  226. package/src/tools/tool-config.ts +6 -0
  227. package/src/types.ts +36 -1
  228. package/src/utils/secret-scrubber.ts +19 -12
  229. package/src/utils/vcs-repo-match.ts +29 -0
  230. package/src/workflows/executors/raw-llm.ts +8 -3
  231. package/src/workflows/executors/system-one-decision.ts +21 -11
  232. package/src/workflows/executors/workflow-llm.ts +22 -0
  233. package/src/workflows/index.ts +21 -12
  234. package/templates/extensions/README.md +1 -0
  235. package/templates/extensions/comms-adherence-gate/README.md +88 -0
  236. package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
  237. package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
  238. package/dist/cli-5t4f29ez.js +0 -33
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Model profiles: a named (harness, model, effort) picked per task (`profile`)
3
+ * or applied as an agent's default (agent-scoped `MODEL_PROFILE` config row).
4
+ * Claim resolves them (`src/be/model-tier-resolution.ts`). See
5
+ * runbooks/model-tiers.md § Model profiles.
6
+ */
7
+ import {
8
+ type ModelProfile,
9
+ type ProviderName,
10
+ ProviderNameSchema,
11
+ type ReasoningEffort,
12
+ ReasoningEffortSchema,
13
+ } from "../../types";
14
+ import { vcsRepoName, vcsRepoUrlMatches } from "../../utils/vcs-repo-match";
15
+ import { getDbClient } from "./runtime";
16
+ import { SUPERSEDED_RESUME_REPAIR_WINDOW_MS } from "./tasks/read";
17
+
18
+ /** Agent-scoped swarm_config key holding the agent's default profile name. */
19
+ export const MODEL_PROFILE_CONFIG_KEY = "MODEL_PROFILE";
20
+
21
+ type ModelProfileRow = {
22
+ id: string;
23
+ name: string;
24
+ harness: string;
25
+ model: string;
26
+ effort: string | null;
27
+ allowCustomModel: number;
28
+ description: string | null;
29
+ seededFrom: string | null;
30
+ createdAt: string;
31
+ lastUpdatedAt: string;
32
+ };
33
+
34
+ function rowToModelProfile(row: ModelProfileRow): ModelProfile {
35
+ return {
36
+ id: row.id,
37
+ name: row.name,
38
+ harness: ProviderNameSchema.parse(row.harness),
39
+ model: row.model,
40
+ effort: ReasoningEffortSchema.safeParse(row.effort).success
41
+ ? (row.effort as ReasoningEffort)
42
+ : null,
43
+ allowCustomModel: row.allowCustomModel === 1,
44
+ description: row.description,
45
+ seededFrom: row.seededFrom,
46
+ createdAt: row.createdAt,
47
+ lastUpdatedAt: row.lastUpdatedAt,
48
+ };
49
+ }
50
+
51
+ export async function listModelProfiles(
52
+ filter: { harness?: ProviderName } = {},
53
+ ): Promise<ModelProfile[]> {
54
+ const rows = filter.harness
55
+ ? await getDbClient().query<ModelProfileRow>(
56
+ "SELECT * FROM model_profiles WHERE harness = ? ORDER BY harness, name",
57
+ [filter.harness],
58
+ )
59
+ : await getDbClient().query<ModelProfileRow>(
60
+ "SELECT * FROM model_profiles ORDER BY harness, name",
61
+ );
62
+ return rows.map(rowToModelProfile);
63
+ }
64
+
65
+ export async function getModelProfileByName(name: string): Promise<ModelProfile | null> {
66
+ const row = await getDbClient().get<ModelProfileRow>(
67
+ "SELECT * FROM model_profiles WHERE name = ?",
68
+ [name.trim()],
69
+ );
70
+ return row ? rowToModelProfile(row) : null;
71
+ }
72
+
73
+ export interface ModelProfileInput {
74
+ name: string;
75
+ harness: ProviderName;
76
+ model: string;
77
+ effort?: ReasoningEffort | null;
78
+ allowCustomModel?: boolean;
79
+ description?: string | null;
80
+ seededFrom?: string | null;
81
+ }
82
+
83
+ /** Insert a profile. Returns null when the name is taken. */
84
+ export async function createModelProfile(
85
+ input: ModelProfileInput,
86
+ audit: { userId?: string | null } = {},
87
+ ): Promise<ModelProfile | null> {
88
+ const now = new Date().toISOString();
89
+ const row = await getDbClient().get<ModelProfileRow>(
90
+ `INSERT INTO model_profiles
91
+ (id, name, harness, model, effort, allowCustomModel, description, seededFrom, createdAt, lastUpdatedAt, created_by, updated_by)
92
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
93
+ ON CONFLICT(name) DO NOTHING
94
+ RETURNING *`,
95
+ [
96
+ crypto.randomUUID(),
97
+ input.name,
98
+ input.harness,
99
+ input.model,
100
+ input.effort ?? null,
101
+ input.allowCustomModel ? 1 : 0,
102
+ input.description ?? null,
103
+ input.seededFrom ?? null,
104
+ now,
105
+ now,
106
+ audit.userId ?? null,
107
+ audit.userId ?? null,
108
+ ],
109
+ );
110
+ return row ? rowToModelProfile(row) : null;
111
+ }
112
+
113
+ export type ModelProfilePatch = Partial<
114
+ Pick<ModelProfileInput, "model" | "effort" | "allowCustomModel" | "description">
115
+ >;
116
+
117
+ /** Update a profile's model, effort or description. The name and harness are fixed. */
118
+ export async function updateModelProfile(
119
+ name: string,
120
+ patch: ModelProfilePatch,
121
+ audit: { userId?: string | null } = {},
122
+ ): Promise<ModelProfile | null> {
123
+ const sets: string[] = [];
124
+ const params: (string | number | null)[] = [];
125
+ if (patch.model !== undefined) {
126
+ sets.push("model = ?");
127
+ params.push(patch.model);
128
+ }
129
+ if (patch.effort !== undefined) {
130
+ sets.push("effort = ?");
131
+ params.push(patch.effort);
132
+ }
133
+ if (patch.allowCustomModel !== undefined) {
134
+ sets.push("allowCustomModel = ?");
135
+ params.push(patch.allowCustomModel ? 1 : 0);
136
+ }
137
+ if (patch.description !== undefined) {
138
+ sets.push("description = ?");
139
+ params.push(patch.description);
140
+ }
141
+ sets.push("lastUpdatedAt = ?", "updated_by = ?");
142
+ params.push(new Date().toISOString(), audit.userId ?? null);
143
+ const row = await getDbClient().get<ModelProfileRow>(
144
+ `UPDATE model_profiles SET ${sets.join(", ")} WHERE name = ? RETURNING *`,
145
+ [...params, name],
146
+ );
147
+ return row ? rowToModelProfile(row) : null;
148
+ }
149
+
150
+ /**
151
+ * Tasks (alias `t`) that still hold their profile: unfinished ones, and
152
+ * superseded ones still owed a resume. The resume copies the profile in a
153
+ * later write than the supersede (HTTP supersede, heartbeat remediation), and
154
+ * after a restart the heartbeat's orphan repair makes it, inside the same
155
+ * window (`SUPERSEDED_RESUME_REPAIR_WINDOW_MS`). Binds one parameter: the
156
+ * oldest `finishedAt` the repair still covers.
157
+ */
158
+ const TASK_HOLDS_PROFILE_SQL = `(
159
+ t.status NOT IN ('completed', 'failed', 'cancelled', 'superseded')
160
+ OR (t.status = 'superseded'
161
+ AND t.workflowRunStepId IS NULL
162
+ AND (t.finishedAt IS NULL OR t.finishedAt > ?)
163
+ AND NOT EXISTS (
164
+ SELECT 1 FROM agent_tasks c WHERE c.parentTaskId = t.id AND c.taskType = 'resume')))`;
165
+
166
+ function repairWindowStart(): string {
167
+ return new Date(Date.now() - SUPERSEDED_RESUME_REPAIR_WINDOW_MS).toISOString();
168
+ }
169
+
170
+ /** Who still points at a profile: agents with it as default, and tasks that still hold it. */
171
+ export async function modelProfileReferences(
172
+ name: string,
173
+ ): Promise<{ agentIds: string[]; openTaskCount: number }> {
174
+ const agents = await getDbClient().query<{ scopeId: string }>(
175
+ "SELECT scopeId FROM swarm_config WHERE scope = 'agent' AND key = ? AND value = ?",
176
+ [MODEL_PROFILE_CONFIG_KEY, name],
177
+ );
178
+ const tasks = await getDbClient().get<{ n: number }>(
179
+ `SELECT COUNT(*) AS n FROM agent_tasks t WHERE t.profile = ? AND ${TASK_HOLDS_PROFILE_SQL}`,
180
+ [name, repairWindowStart()],
181
+ );
182
+ return { agentIds: agents.map((a) => a.scopeId), openTaskCount: tasks?.n ?? 0 };
183
+ }
184
+
185
+ /**
186
+ * Which migration-220 reference trigger refused a write: `missing` when a task
187
+ * or an agent default names a profile that does not exist.
188
+ */
189
+ export function modelProfileRefError(error: unknown): "missing" | null {
190
+ const message = error instanceof Error ? error.message : "";
191
+ return message.includes("model-profile-missing:") ? "missing" : null;
192
+ }
193
+
194
+ /**
195
+ * Delete a profile unless a task still holds it (`TASK_HOLDS_PROFILE_SQL`) or
196
+ * an agent default names it. The check is part of the DELETE statement, so a
197
+ * reference written after the caller's own count still makes it `in-use`; the
198
+ * triggers in migration 220 refuse the opposite order (a reference written
199
+ * after this delete).
200
+ */
201
+ export async function deleteModelProfile(name: string): Promise<"deleted" | "missing" | "in-use"> {
202
+ const row = await getDbClient().get<{ id: string }>(
203
+ `DELETE FROM model_profiles
204
+ WHERE name = ?
205
+ AND NOT EXISTS (
206
+ SELECT 1 FROM agent_tasks t
207
+ WHERE t.profile = model_profiles.name AND ${TASK_HOLDS_PROFILE_SQL})
208
+ AND NOT EXISTS (
209
+ SELECT 1 FROM swarm_config
210
+ WHERE scope = 'agent' AND key = ? AND trim(value) = model_profiles.name)
211
+ RETURNING id`,
212
+ [name, repairWindowStart(), MODEL_PROFILE_CONFIG_KEY],
213
+ );
214
+ if (row) return "deleted";
215
+ return (await getModelProfileByName(name)) ? "in-use" : "missing";
216
+ }
217
+
218
+ /** The agent's default profile name (agent-scoped MODEL_PROFILE), or null. */
219
+ export async function getAgentDefaultProfileName(agentId: string): Promise<string | null> {
220
+ const row = await getDbClient().get<{ value: string }>(
221
+ "SELECT value FROM swarm_config WHERE scope = 'agent' AND scopeId = ? AND key = ?",
222
+ [agentId, MODEL_PROFILE_CONFIG_KEY],
223
+ );
224
+ return row?.value.trim() || null;
225
+ }
226
+
227
+ /**
228
+ * Which of MODEL_OVERRIDE / REASONING_EFFORT_OVERRIDE the task's repo sets at
229
+ * repo scope, exported or not. The worker env takes a repo row over the
230
+ * agent's own (repo > agent), and a withheld one removes the key, so the
231
+ * agent's default profile must not supply that axis. Finds the repo the way
232
+ * the worker does (`fetchRepoConfig`). `includeSecrets: false` leaves out
233
+ * secret rows, as `/api/config/resolved` does for a worker that cannot read
234
+ * them (`readsRepoSecrets`): that worker's env never gets them.
235
+ */
236
+ export async function repoModelOverrideKeys(
237
+ vcsRepo: string | null | undefined,
238
+ { includeSecrets = true }: { includeSecrets?: boolean } = {},
239
+ ): Promise<Set<string>> {
240
+ if (!vcsRepo?.trim()) return new Set();
241
+ const repos = await getDbClient().query<{ id: string; url: string }>(
242
+ "SELECT id, url FROM swarm_repos WHERE name = ? ORDER BY name ASC",
243
+ [vcsRepoName(vcsRepo)],
244
+ );
245
+ const repo = repos.find((r) => vcsRepoUrlMatches(r.url, vcsRepo));
246
+ if (!repo) return new Set();
247
+ const rows = await getDbClient().query<{ key: string }>(
248
+ `SELECT key FROM swarm_config
249
+ WHERE scope = 'repo' AND scopeId = ? AND key IN ('MODEL_OVERRIDE', 'REASONING_EFFORT_OVERRIDE')
250
+ ${includeSecrets ? "" : "AND isSecret = 0"}`,
251
+ [repo.id],
252
+ );
253
+ return new Set(rows.map((row) => row.key));
254
+ }
255
+
256
+ /**
257
+ * Why a task cannot ask for `profile`, or null when it can. PR 1 validity is
258
+ * the harness only: the profile must exist and, when the target agent is known
259
+ * and has a harness, run on that harness. `profile` is exclusive with `model`
260
+ * and `modelTier`, which pick the model another way.
261
+ */
262
+ export async function taskProfileError(check: {
263
+ profile?: string | null;
264
+ model?: string | null;
265
+ modelTier?: string | null;
266
+ agentId?: string | null;
267
+ }): Promise<string | null> {
268
+ const name = check.profile?.trim();
269
+ if (!name) return null;
270
+ if (check.model?.trim() || check.modelTier?.trim()) {
271
+ return "Pass `profile`, or `model`/`modelTier`, not both.";
272
+ }
273
+ const profile = await getModelProfileByName(name);
274
+ if (!profile) {
275
+ return `Unknown model profile "${name}". List profiles with list-model-profiles.`;
276
+ }
277
+ if (!check.agentId) return null;
278
+ const agent = await getDbClient().get<{ harness: string | null }>(
279
+ "SELECT COALESCE(harness_provider, provider) AS harness FROM agents WHERE id = ?",
280
+ [check.agentId],
281
+ );
282
+ if (agent?.harness && agent.harness !== profile.harness) {
283
+ return `Model profile "${name}" runs on the ${profile.harness} harness; the target agent runs ${agent.harness}.`;
284
+ }
285
+ return null;
286
+ }
287
+
288
+ type OverrideRowState = { value: string; exportToEnv?: number } | null;
289
+
290
+ /** What a config row does to the worker env: its value, withheld (exportToEnv off), or absent. */
291
+ function effectiveOverride(row: OverrideRowState): string | null {
292
+ if (!row) return null;
293
+ return row.exportToEnv === 0 ? "\u0000withheld" : row.value;
294
+ }
295
+
296
+ /**
297
+ * An agent's default model profile (MODEL_PROFILE) beats its MODEL_OVERRIDE and
298
+ * REASONING_EFFORT_OVERRIDE at claim. An effective change to either override, by
299
+ * any path, drops the profile default, so a stale default never beats a fresh
300
+ * override. Effective means what the worker env sees: the value, and whether
301
+ * the row is exported (`exportToEnv`).
302
+ */
303
+ export async function clearAgentModelProfileOnOverrideChange(
304
+ agentId: string,
305
+ key: string,
306
+ before: OverrideRowState,
307
+ after: OverrideRowState,
308
+ ): Promise<void> {
309
+ if (key !== "MODEL_OVERRIDE" && key !== "REASONING_EFFORT_OVERRIDE") return;
310
+ if (effectiveOverride(before) === effectiveOverride(after)) return;
311
+ await getDbClient().run(
312
+ "DELETE FROM swarm_config WHERE scope = 'agent' AND scopeId = ? AND key = ?",
313
+ [agentId, MODEL_PROFILE_CONFIG_KEY],
314
+ );
315
+ }
@@ -92,6 +92,8 @@ export type AgentTaskRow = {
92
92
  resolvedModel?: string | null;
93
93
  modelSource?: string | null;
94
94
  modelAlias?: string | null;
95
+ profile?: string | null;
96
+ resolvedProfile?: string | null;
95
97
  scheduleId: string | null;
96
98
  workflowRunId: string | null;
97
99
  workflowRunStepId: string | null;
@@ -322,6 +324,8 @@ export function rowToAgentTask(row: AgentTaskRow): AgentTask {
322
324
  resolvedEffort: ReasoningEffortSchema.safeParse(row.resolvedEffort).success
323
325
  ? (row.resolvedEffort as ReasoningEffort)
324
326
  : undefined,
327
+ profile: row.profile ?? undefined,
328
+ resolvedProfile: row.resolvedProfile ?? undefined,
325
329
  scheduleId: row.scheduleId ?? undefined,
326
330
  workflowRunId: row.workflowRunId ?? undefined,
327
331
  workflowRunStepId: row.workflowRunStepId ?? undefined,
@@ -456,6 +460,9 @@ export async function hasNonTerminalResumeChild(parentId: string): Promise<boole
456
460
  return row !== undefined && row !== null;
457
461
  }
458
462
 
463
+ /** How far back the heartbeat repairs a superseded task that never got a resume. */
464
+ export const SUPERSEDED_RESUME_REPAIR_WINDOW_MS = 24 * 60 * 60 * 1000;
465
+
459
466
  /**
460
467
  * Superseded tasks that never got a resume child. Both supersede paths
461
468
  * (heartbeat remediation and `POST /api/tasks/:id/supersede`) supersede
package/src/be/db.ts CHANGED
@@ -193,6 +193,7 @@ import {
193
193
  createContextVersion,
194
194
  getLatestContextVersion,
195
195
  } from "./db/context-versions";
196
+ import { clearAgentModelProfileOnOverrideChange } from "./db/model-profiles";
196
197
  import { getDb, getDbClient } from "./db/runtime";
197
198
  import { classifyTaskHumanFree, reclassifyTaskHumanFree } from "./db/tasks/human-free";
198
199
  import {
@@ -342,6 +343,7 @@ export {
342
343
  type PendingChatRelay,
343
344
  recordChatRelayError,
344
345
  resetTasksNotified,
346
+ SUPERSEDED_RESUME_REPAIR_WINDOW_MS,
345
347
  type TaskFilters,
346
348
  } from "./db/tasks/read";
347
349
 
@@ -2857,9 +2859,9 @@ export async function createTaskExtended(
2857
2859
  vcsProvider, vcsRepo, vcsEventType, vcsNumber, vcsCommentId, vcsAuthor, vcsUrl,
2858
2860
  vcsInstallationId, vcsNodeId,
2859
2861
  agentmailInboxId, agentmailMessageId, agentmailThreadId,
2860
- mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, scheduleId,
2862
+ mentionMessageId, mentionChannelId, dir, parentTaskId, model, modelTier, effort, profile, scheduleId,
2861
2863
  workflowRunId, workflowRunStepId, outputSchema, followUpConfig, requestedByUserId, requestedByUserIdInherited, contextKey, routingAffinity, swarmVersion, createdAt, lastUpdatedAt, created_by, updated_by, isHumanFree
2862
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2864
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
2863
2865
  [
2864
2866
  id,
2865
2867
  assetKey,
@@ -2901,6 +2903,7 @@ export async function createTaskExtended(
2901
2903
  options?.model ?? null,
2902
2904
  options?.modelTier ?? null,
2903
2905
  options?.effort ?? null,
2906
+ options?.profile?.trim() || null,
2904
2907
  options?.scheduleId ?? null,
2905
2908
  options?.workflowRunId ?? null,
2906
2909
  options?.workflowRunStepId ?? null,
@@ -6662,55 +6665,55 @@ export async function upsertSwarmConfig(data: {
6662
6665
  const row = await getDbClient().transaction(async () => {
6663
6666
  const existing =
6664
6667
  scopeId === null
6665
- ? await getDbClient().get<{ id: string }>(
6666
- "SELECT id FROM swarm_config WHERE scope = ? AND scopeId IS NULL AND key = ?",
6668
+ ? await getDbClient().get<SwarmConfigRow>(
6669
+ "SELECT * FROM swarm_config WHERE scope = ? AND scopeId IS NULL AND key = ?",
6667
6670
  [data.scope, data.key],
6668
6671
  )
6669
- : await getDbClient().get<{ id: string }>(
6670
- "SELECT id FROM swarm_config WHERE scope = ? AND scopeId = ? AND key = ?",
6672
+ : await getDbClient().get<SwarmConfigRow>(
6673
+ "SELECT * FROM swarm_config WHERE scope = ? AND scopeId = ? AND key = ?",
6671
6674
  [data.scope, scopeId, data.key],
6672
6675
  );
6673
6676
 
6674
- if (existing) {
6675
- // COALESCE keeps the stored flag when the caller did not send one.
6676
- return await getDbClient().get<SwarmConfigRow>(
6677
- `UPDATE swarm_config SET value = ?, isSecret = ?, envPath = ?, description = ?, encrypted = ?, lastUpdatedAt = ?,
6678
- exportToEnv = CASE WHEN ? THEN 0 ELSE COALESCE(?, exportToEnv) END
6679
- WHERE id = ? RETURNING *`,
6680
- [
6681
- storedValue,
6682
- isSecret,
6683
- envPath,
6684
- description,
6685
- encryptedFlag,
6686
- now,
6687
- restricted ? 1 : 0,
6688
- data.exportToEnv === undefined ? null : data.exportToEnv ? 1 : 0,
6689
- existing.id,
6690
- ],
6691
- );
6677
+ // COALESCE keeps the stored flag when the caller did not send one.
6678
+ const written = existing
6679
+ ? await getDbClient().get<SwarmConfigRow>(
6680
+ `UPDATE swarm_config SET value = ?, isSecret = ?, envPath = ?, description = ?, encrypted = ?, lastUpdatedAt = ?,
6681
+ exportToEnv = CASE WHEN ? THEN 0 ELSE COALESCE(?, exportToEnv) END
6682
+ WHERE id = ? RETURNING *`,
6683
+ [
6684
+ storedValue,
6685
+ isSecret,
6686
+ envPath,
6687
+ description,
6688
+ encryptedFlag,
6689
+ now,
6690
+ restricted ? 1 : 0,
6691
+ data.exportToEnv === undefined ? null : data.exportToEnv ? 1 : 0,
6692
+ existing.id,
6693
+ ],
6694
+ )
6695
+ : await getDbClient().get<SwarmConfigRow>(
6696
+ `INSERT INTO swarm_config (id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt, encrypted, exportToEnv)
6697
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
6698
+ [
6699
+ crypto.randomUUID(),
6700
+ data.scope,
6701
+ scopeId,
6702
+ data.key,
6703
+ storedValue,
6704
+ isSecret,
6705
+ envPath,
6706
+ description,
6707
+ now,
6708
+ now,
6709
+ encryptedFlag,
6710
+ restricted ? 0 : (data.exportToEnv ?? defaultExportToEnv(data.key)) ? 1 : 0,
6711
+ ],
6712
+ );
6713
+ if (data.scope === "agent" && scopeId) {
6714
+ await clearAgentModelProfileOnOverrideChange(scopeId, data.key, existing, written);
6692
6715
  }
6693
-
6694
- const id = crypto.randomUUID();
6695
- const exportToEnv = restricted ? 0 : (data.exportToEnv ?? defaultExportToEnv(data.key)) ? 1 : 0;
6696
- return await getDbClient().get<SwarmConfigRow>(
6697
- `INSERT INTO swarm_config (id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt, encrypted, exportToEnv)
6698
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *`,
6699
- [
6700
- id,
6701
- data.scope,
6702
- scopeId,
6703
- data.key,
6704
- storedValue,
6705
- isSecret,
6706
- envPath,
6707
- description,
6708
- now,
6709
- now,
6710
- encryptedFlag,
6711
- exportToEnv,
6712
- ],
6713
- );
6716
+ return written;
6714
6717
  });
6715
6718
 
6716
6719
  if (!row) throw new Error("Failed to upsert swarm config");
@@ -6759,10 +6762,13 @@ export async function emitBuiltInIntegrationConnectedOnce(
6759
6762
  * predate hardening must remain removable through remediation paths.
6760
6763
  */
6761
6764
  export async function deleteSwarmConfig(id: string): Promise<boolean> {
6762
- const row = await getDbClient().get<Pick<SwarmConfigRow, "scope" | "scopeId">>(
6763
- "DELETE FROM swarm_config WHERE id = ? RETURNING scope, scopeId",
6765
+ const row = await getDbClient().get<SwarmConfigRow>(
6766
+ "DELETE FROM swarm_config WHERE id = ? RETURNING *",
6764
6767
  [id],
6765
6768
  );
6769
+ if (row?.scope === "agent" && row.scopeId) {
6770
+ await clearAgentModelProfileOnOverrideChange(row.scopeId, row.key, row, null);
6771
+ }
6766
6772
  if (row) getDbClient().afterCommit(() => publishWorkerConfigNudge(row.scope, row.scopeId));
6767
6773
  return row !== null;
6768
6774
  }
@@ -0,0 +1,22 @@
1
+ -- The pi extension posted its per-LLM-call `context` usage snapshot as
2
+ -- eventType 'compaction', so every pi turn drew an AUTO COMPACTION divider and
3
+ -- bumped agent_tasks.compactionCount. Those rows never carry compactTrigger or
4
+ -- preCompactTokens (real compactions from every harness do). Relabel them as
5
+ -- 'progress' and recompute the counter for the pi tasks.
6
+
7
+ UPDATE task_context_snapshots
8
+ SET eventType = 'progress'
9
+ WHERE eventType = 'compaction'
10
+ AND compactTrigger IS NULL
11
+ AND preCompactTokens IS NULL
12
+ AND sessionId LIKE 'pi-%'
13
+ AND taskId IN (SELECT id FROM agent_tasks WHERE provider = 'pi');
14
+
15
+ UPDATE agent_tasks
16
+ SET compactionCount = (
17
+ SELECT COUNT(*)
18
+ FROM task_context_snapshots s
19
+ WHERE s.taskId = agent_tasks.id AND s.eventType = 'compaction'
20
+ )
21
+ WHERE provider = 'pi'
22
+ AND COALESCE(compactionCount, 0) > 0;
@@ -0,0 +1,137 @@
1
+ -- Model profiles (runbooks/model-tiers.md § Model profiles): a named
2
+ -- (harness, model, effort) that Lead picks per task or that applies as an
3
+ -- agent's default. Claim stamps the profile on the task. The tier seeds
4
+ -- (`<harness>-<tier>`) are written at boot by src/be/seed-model-profiles.ts.
5
+ CREATE TABLE IF NOT EXISTS model_profiles (
6
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
7
+ name TEXT NOT NULL UNIQUE,
8
+ harness TEXT NOT NULL,
9
+ model TEXT NOT NULL,
10
+ effort TEXT,
11
+ allowCustomModel INTEGER NOT NULL DEFAULT 0,
12
+ description TEXT,
13
+ -- `tier:<tier>` for a boot seed, `agent-config` for a profile this migration
14
+ -- made from an agent's MODEL_OVERRIDE, NULL for one an operator created.
15
+ seededFrom TEXT,
16
+ createdAt TEXT NOT NULL,
17
+ lastUpdatedAt TEXT NOT NULL,
18
+ created_by TEXT REFERENCES users(id),
19
+ updated_by TEXT REFERENCES users(id)
20
+ );
21
+
22
+ -- `profile` is the creator's ask; `resolvedProfile` is the name claim applied
23
+ -- (the task's ask or the agent's default), mirroring model / resolvedModel.
24
+ ALTER TABLE agent_tasks ADD COLUMN profile TEXT;
25
+ ALTER TABLE agent_tasks ADD COLUMN resolvedProfile TEXT;
26
+
27
+ -- A profile name a task asks for, or an agent has as its default, must exist
28
+ -- when the row is written. Enforced here so the check and the write are one
29
+ -- statement: an app-level check before a separate insert races a concurrent
30
+ -- delete. The delete side is the matching single-statement guard in
31
+ -- deleteModelProfile (src/be/db/model-profiles.ts). It is not a trigger on
32
+ -- model_profiles because one reading agent_tasks would break every later
33
+ -- migration that rebuilds agent_tasks (DROP + RENAME re-parses that trigger).
34
+ CREATE TRIGGER IF NOT EXISTS model_profiles_task_ref_insert
35
+ BEFORE INSERT ON agent_tasks
36
+ WHEN NEW.profile IS NOT NULL
37
+ AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = NEW.profile)
38
+ BEGIN
39
+ SELECT RAISE(ABORT, 'model-profile-missing: the task''s model profile does not exist');
40
+ END;
41
+
42
+ CREATE TRIGGER IF NOT EXISTS model_profiles_task_ref_update
43
+ BEFORE UPDATE OF profile ON agent_tasks
44
+ WHEN NEW.profile IS NOT NULL
45
+ AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = NEW.profile)
46
+ BEGIN
47
+ SELECT RAISE(ABORT, 'model-profile-missing: the task''s model profile does not exist');
48
+ END;
49
+
50
+ CREATE TRIGGER IF NOT EXISTS model_profiles_default_ref_insert
51
+ BEFORE INSERT ON swarm_config
52
+ WHEN NEW.scope = 'agent' AND NEW.key = 'MODEL_PROFILE'
53
+ AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = trim(NEW.value))
54
+ BEGIN
55
+ SELECT RAISE(ABORT, 'model-profile-missing: the agent''s default model profile does not exist');
56
+ END;
57
+
58
+ CREATE TRIGGER IF NOT EXISTS model_profiles_default_ref_update
59
+ BEFORE UPDATE OF scope, key, value ON swarm_config
60
+ WHEN NEW.scope = 'agent' AND NEW.key = 'MODEL_PROFILE'
61
+ AND NOT EXISTS (SELECT 1 FROM model_profiles WHERE name = trim(NEW.value))
62
+ BEGIN
63
+ SELECT RAISE(ABORT, 'model-profile-missing: the agent''s default model profile does not exist');
64
+ END;
65
+
66
+ -- Each agent's MODEL_OVERRIDE + REASONING_EFFORT_OVERRIDE pair becomes a
67
+ -- profile, deduped by (harness, model, effort), and that agent's default
68
+ -- (agent-scoped MODEL_PROFILE, never exported to the worker env). Claim then
69
+ -- supplies the same model and effort the worker read from its env before.
70
+ -- A row with exportToEnv = 0 never reached the worker, so it is not carried
71
+ -- over: a withheld model makes no default, a withheld effort no profile effort.
72
+ -- ACP keeps MODEL_OVERRIDE (no portable mapping); `latest:` aliases are not
73
+ -- valid agent defaults, so they are left alone too.
74
+ CREATE TEMP TABLE _model_profile_overrides AS
75
+ SELECT
76
+ a.id AS agentId,
77
+ COALESCE(a.harness_provider, a.provider) AS harness,
78
+ trim(m.value) AS model,
79
+ CASE
80
+ WHEN lower(trim(e.value)) IN ('off', 'low', 'medium', 'high', 'xhigh', 'max')
81
+ THEN lower(trim(e.value))
82
+ END AS effort
83
+ FROM agents a
84
+ JOIN swarm_config m
85
+ ON m.scope = 'agent' AND m.scopeId = a.id AND m.key = 'MODEL_OVERRIDE'
86
+ AND m.isSecret = 0 AND m.encrypted = 0 AND m.exportToEnv = 1
87
+ LEFT JOIN swarm_config e
88
+ ON e.scope = 'agent' AND e.scopeId = a.id AND e.key = 'REASONING_EFFORT_OVERRIDE'
89
+ AND e.isSecret = 0 AND e.encrypted = 0 AND e.exportToEnv = 1
90
+ WHERE COALESCE(a.harness_provider, a.provider) IS NOT NULL
91
+ AND COALESCE(a.harness_provider, a.provider) <> 'acp'
92
+ AND trim(m.value) <> ''
93
+ AND lower(trim(m.value)) NOT LIKE 'latest:%';
94
+
95
+ -- Name: `<harness>-<model>[-<effort>]`, without repeating a harness prefix the
96
+ -- model already carries, e.g. `codex-gpt-6.1-sol-xhigh`, `claude-opus-5-5-high`.
97
+ INSERT OR IGNORE INTO model_profiles
98
+ (name, harness, model, effort, allowCustomModel, description, seededFrom, createdAt, lastUpdatedAt)
99
+ SELECT
100
+ lower(replace(replace(
101
+ CASE WHEN lower(model) LIKE lower(harness) || '-%' THEN model ELSE harness || '-' || model END,
102
+ '/', '-'), ':', '-')) || COALESCE('-' || effort, ''),
103
+ harness,
104
+ model,
105
+ effort,
106
+ 1,
107
+ 'Created from an agent''s MODEL_OVERRIDE and REASONING_EFFORT_OVERRIDE.',
108
+ 'agent-config',
109
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
110
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
111
+ FROM _model_profile_overrides
112
+ GROUP BY harness, model, effort;
113
+
114
+ INSERT OR IGNORE INTO swarm_config
115
+ (id, scope, scopeId, key, value, isSecret, envPath, description, createdAt, lastUpdatedAt, encrypted, exportToEnv)
116
+ SELECT
117
+ lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
118
+ substr(lower(hex(randomblob(2))), 2) || '-' ||
119
+ substr('89ab', 1 + (abs(random()) % 4), 1) || substr(lower(hex(randomblob(2))), 2) || '-' ||
120
+ lower(hex(randomblob(6))),
121
+ 'agent',
122
+ o.agentId,
123
+ 'MODEL_PROFILE',
124
+ p.name,
125
+ 0,
126
+ NULL,
127
+ 'Default model profile, created from this agent''s MODEL_OVERRIDE.',
128
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
129
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
130
+ 0,
131
+ 0
132
+ FROM _model_profile_overrides o
133
+ JOIN model_profiles p
134
+ ON p.harness = o.harness AND p.model = o.model AND p.effort IS o.effort
135
+ AND p.seededFrom = 'agent-config';
136
+
137
+ DROP TABLE _model_profile_overrides;