@desplega.ai/agent-swarm 1.131.0 → 1.133.1

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 (265) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-w53mbns9.js → actions-cskpayd7.js} +11 -9
  3. package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
  4. package/dist/{app-m9gwqsqv.js → app-ang77nbb.js} +8 -6
  5. package/dist/{assistant-bp65m6f3.js → assistant-b4exebdw.js} +16 -13
  6. package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
  7. package/dist/{boot-reembed-62p0at2k.js → boot-reembed-ps2aftpb.js} +9 -7
  8. package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-znyk3nxb.js} +10 -8
  9. package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
  10. package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-3ct8kgxw.js} +13 -9
  11. package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-akx4ntf2.js} +2 -2
  12. package/dist/{cli-t1xze5qh.js → cli-13w2hwkh.js} +1 -1
  13. package/dist/{cli-efxfnq6q.js → cli-1532szb1.js} +8 -6
  14. package/dist/{cli-d2tfkcga.js → cli-23d3rseb.js} +33 -17
  15. package/dist/{cli-nm5gtp1x.js → cli-2a75z9c5.js} +196 -7
  16. package/dist/{cli-fywms69x.js → cli-2t30rj0d.js} +1 -1
  17. package/dist/{cli-cvgjwnh1.js → cli-3c3tdgab.js} +10 -10
  18. package/dist/{cli-pm6xbh84.js → cli-3zzxa9jz.js} +46 -16
  19. package/dist/{cli-cdeg08wx.js → cli-4k6233gc.js} +1 -1
  20. package/dist/{cli-t55yw3a1.js → cli-5p2tz05r.js} +2 -2
  21. package/dist/{cli-0rcyq6d9.js → cli-5z2yhzvd.js} +125 -47
  22. package/dist/{cli-3kf51j15.js → cli-6zfq31nj.js} +9 -3
  23. package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
  24. package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
  25. package/dist/{cli-72q8m8gk.js → cli-9nfc2es8.js} +6 -6
  26. package/dist/{cli-5ze9rv97.js → cli-9ta3pwms.js} +11 -4
  27. package/dist/{cli-78xwvkhj.js → cli-avr0ns7z.js} +2 -2
  28. package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
  29. package/dist/{cli-2q72qz2s.js → cli-dgk20ft7.js} +15 -6
  30. package/dist/cli-et1bbwqf.js +13 -0
  31. package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
  32. package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
  33. package/dist/{cli-e7ckcg4v.js → cli-ftwpv451.js} +170 -387
  34. package/dist/{cli-g6b8x1h8.js → cli-g854s2dw.js} +1 -1
  35. package/dist/{cli-5kx36evn.js → cli-ggmdxeqa.js} +13 -8
  36. package/dist/cli-hkwfe3qj.js +530 -0
  37. package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
  38. package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
  39. package/dist/{cli-5v4rw6yd.js → cli-pvzqhbs9.js} +2 -2
  40. package/dist/cli-pzq5rjym.js +15 -0
  41. package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
  42. package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
  43. package/dist/cli-qys3rhhz.js +27 -0
  44. package/dist/{cli-rzbpmxdr.js → cli-r6tpm37h.js} +2 -2
  45. package/dist/{cli-qv61hp37.js → cli-rch80c76.js} +2 -2
  46. package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
  47. package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
  48. package/dist/{cli-psh3c4va.js → cli-sfd9v7sj.js} +1 -1
  49. package/dist/{cli-70xqj0z3.js → cli-sh3ytsyj.js} +49 -39
  50. package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
  51. package/dist/{cli-d3x3x1c9.js → cli-xdb4mpnp.js} +2 -2
  52. package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
  53. package/dist/{cli-yw8vghx2.js → cli-xv8t8p3c.js} +65 -32
  54. package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
  55. package/dist/{cli-t76vw5db.js → cli-y08jvxzx.js} +601 -140
  56. package/dist/{cli-a4jzdpa3.js → cli-zfbf3srj.js} +650 -245
  57. package/dist/{cli-a40rnf71.js → cli-zmq89k34.js} +6 -6
  58. package/dist/{cli-qmdnhpej.js → cli-zr9q011v.js} +23 -13
  59. package/dist/cli.js +22 -22
  60. package/dist/{codex-adapter-d24q75wk.js → codex-adapter-bef93pq4.js} +9 -7
  61. package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
  62. package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-5pkxys7x.js} +9 -7
  63. package/dist/{commands-c3nrc5hg.js → commands-e23s1bff.js} +9 -7
  64. package/dist/{db-nt7fg3tw.js → db-m3nabatn.js} +17 -5
  65. package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
  66. package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
  67. package/dist/{handlers-fkr6swat.js → handlers-akazedvr.js} +16 -13
  68. package/dist/{hook-77wpvnjc.js → hook-ze4cpf0v.js} +55 -41
  69. package/dist/{hook-g019yehy.js → hook-zfqyyenw.js} +10 -7
  70. package/dist/{http-jewj0syk.js → http-sja0n6kw.js} +730 -150
  71. package/dist/index-0gr8vd6k.js +39 -0
  72. package/dist/{index-7n2xpbd4.js → index-354vkhdv.js} +154 -74
  73. package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
  74. package/dist/{index-hfvggd0z.js → index-95t3cqq5.js} +16 -14
  75. package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
  76. package/dist/{index-6gm5he8h.js → index-vpn36mma.js} +15 -13
  77. package/dist/{keepalive-j0fjvqfd.js → keepalive-3sp3meqj.js} +16 -10
  78. package/dist/{lead-1n8x30xe.js → lead-mefeewkf.js} +39 -36
  79. package/dist/{maintenance-gvev11sa.js → maintenance-8tbrbjfg.js} +10 -8
  80. package/dist/mistral-conversations-00zrp7g4.js +717 -0
  81. package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
  82. package/dist/{onboard-pvm5b38w.js → onboard-mt2tgyjg.js} +7 -7
  83. package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
  84. package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
  85. package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
  86. package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
  87. package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
  88. package/dist/{otel-impl-p6jvgd9c.js → otel-impl-z6pxt3j9.js} +224 -9
  89. package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-btvqmj58.js} +1138 -408
  90. package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-kyq8cezh.js} +9 -7
  91. package/dist/{rbac-roles-cr80g96t.js → rbac-roles-3ryfrfna.js} +8 -6
  92. package/dist/{rbac-roles-30pspvy7.js → rbac-roles-6qfvbee1.js} +9 -7
  93. package/dist/{render-v2-xm457ng1.js → render-v2-p61ppjey.js} +8 -6
  94. package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-j56bbj4y.js} +8 -6
  95. package/dist/{setup-nytnmwwr.js → setup-tpr6c1kn.js} +2 -2
  96. package/dist/{worker-msytr2cw.js → worker-9w1v0mbb.js} +39 -36
  97. package/openapi.json +542 -16
  98. package/package.json +6 -6
  99. package/src/be/budget-refusal-notify.ts +14 -7
  100. package/src/be/db-queries/task-artifacts.ts +182 -0
  101. package/src/be/db.ts +783 -169
  102. package/src/be/events.ts +5 -0
  103. package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
  104. package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
  105. package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
  106. package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
  107. package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
  108. package/src/be/migrations/136_task_requester_provenance.sql +37 -0
  109. package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
  110. package/src/be/multi-runtime.ts +430 -0
  111. package/src/be/scripts/db.ts +43 -1
  112. package/src/be/scripts/extract-schema.ts +3 -1
  113. package/src/be/scripts/retention.ts +164 -0
  114. package/src/be/scripts/run-saved.ts +16 -1
  115. package/src/be/scripts/typecheck.ts +3 -2
  116. package/src/be/seed-skills/index.ts +7 -0
  117. package/src/be/steering.ts +1 -1
  118. package/src/be/swarm-config-guard.ts +18 -0
  119. package/src/commands/credential-wait.ts +54 -0
  120. package/src/commands/onboard/steps/core-credentials.tsx +1 -1
  121. package/src/commands/onboard/steps/custom-templates.tsx +1 -1
  122. package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
  123. package/src/commands/onboard/steps/health-check.tsx +1 -1
  124. package/src/commands/onboard/steps/start.tsx +1 -1
  125. package/src/commands/profile-sync-audit.ts +56 -0
  126. package/src/commands/profile-sync.ts +287 -15
  127. package/src/commands/provider-credentials.ts +46 -19
  128. package/src/commands/runner.ts +196 -33
  129. package/src/commands/templates.ts +22 -0
  130. package/src/github/handlers.ts +55 -9
  131. package/src/heartbeat/heartbeat.ts +71 -12
  132. package/src/hooks/hook.ts +49 -33
  133. package/src/http/active-sessions.ts +11 -1
  134. package/src/http/agents.ts +261 -30
  135. package/src/http/all-routes.ts +1 -0
  136. package/src/http/config.ts +13 -2
  137. package/src/http/core.ts +102 -4
  138. package/src/http/db-query-bounded.ts +313 -0
  139. package/src/http/db-query-shared.ts +249 -0
  140. package/src/http/db-query.ts +54 -42
  141. package/src/http/events.ts +2 -0
  142. package/src/http/index.ts +27 -2
  143. package/src/http/mcp-bridge.ts +37 -3
  144. package/src/http/mcp.ts +12 -0
  145. package/src/http/memory.ts +13 -2
  146. package/src/http/openapi.ts +1 -0
  147. package/src/http/poll.ts +70 -27
  148. package/src/http/route-def.ts +43 -9
  149. package/src/http/schedules.ts +2 -2
  150. package/src/http/scripts.ts +31 -4
  151. package/src/http/session-data.ts +44 -0
  152. package/src/http/tasks.ts +2 -2
  153. package/src/http/users.ts +74 -52
  154. package/src/http/webhooks.ts +5 -2
  155. package/src/http/workflows.ts +1 -4
  156. package/src/http/x.ts +14 -0
  157. package/src/integrations/kapso/inbound.ts +1 -1
  158. package/src/jira/outbound.ts +39 -8
  159. package/src/jira/webhook-lifecycle.ts +13 -2
  160. package/src/linear/outbound.ts +43 -10
  161. package/src/oauth/keepalive.ts +13 -2
  162. package/src/prompts/base-prompt.ts +1 -1
  163. package/src/prompts/session-templates.ts +45 -7
  164. package/src/providers/claude-adapter.ts +6 -0
  165. package/src/providers/claude-managed-models.ts +34 -10
  166. package/src/providers/codex-adapter.ts +6 -0
  167. package/src/providers/opencode-adapter.ts +6 -0
  168. package/src/providers/pi-mono-adapter.ts +8 -0
  169. package/src/providers/pricing-sources.md +24 -0
  170. package/src/queue-stall-alarm.ts +216 -0
  171. package/src/scheduler/scheduler.ts +7 -0
  172. package/src/script-workflows/executor.ts +2 -1
  173. package/src/scripts-runtime/executors/native.ts +2 -1
  174. package/src/scripts-runtime/executors/types.ts +5 -0
  175. package/src/scripts-runtime/loader.ts +5 -0
  176. package/src/scripts-runtime/swarm-config.ts +8 -0
  177. package/src/scripts-runtime/swarm-sdk.ts +7 -0
  178. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  179. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  180. package/src/tests/budget-claim-gate.test.ts +20 -0
  181. package/src/tests/claude-adapter.test.ts +32 -0
  182. package/src/tests/claude-managed-adapter.test.ts +38 -7
  183. package/src/tests/codex-adapter.test.ts +30 -0
  184. package/src/tests/create-task-validation.test.ts +128 -0
  185. package/src/tests/credential-wait.test.ts +67 -1
  186. package/src/tests/db-query.test.ts +711 -2
  187. package/src/tests/http-api-integration.test.ts +81 -0
  188. package/src/tests/http-users.test.ts +166 -1
  189. package/src/tests/identity-field-budget.test.ts +176 -0
  190. package/src/tests/mcp-tools-user.test.ts +34 -0
  191. package/src/tests/mcp-user-route.test.ts +17 -0
  192. package/src/tests/metrics-http.test.ts +44 -0
  193. package/src/tests/multi-runtime-registration.test.ts +2703 -0
  194. package/src/tests/openapi-response-contract.test.ts +24 -0
  195. package/src/tests/openapi-route-headers.test.ts +83 -0
  196. package/src/tests/opencode-adapter.test.ts +40 -0
  197. package/src/tests/outbound-listener-rejection.test.ts +138 -0
  198. package/src/tests/pi-mono-adapter.test.ts +98 -0
  199. package/src/tests/profile-sync-warning.test.ts +277 -0
  200. package/src/tests/profile-sync.test.ts +232 -6
  201. package/src/tests/prompt-template-session.test.ts +32 -3
  202. package/src/tests/queue-stall-alarm.test.ts +226 -0
  203. package/src/tests/requester-comms.test.ts +32 -0
  204. package/src/tests/runner-requester-profile.test.ts +38 -0
  205. package/src/tests/sandboxed-process.test.ts +45 -0
  206. package/src/tests/schedule-target-type.test.ts +38 -0
  207. package/src/tests/script-connections.test.ts +16 -3
  208. package/src/tests/scripts-external-api.test.ts +8 -3
  209. package/src/tests/scripts-http.test.ts +12 -0
  210. package/src/tests/scripts-retention.test.ts +384 -0
  211. package/src/tests/scripts-runtime-identity.test.ts +375 -0
  212. package/src/tests/scripts-runtime.test.ts +4 -3
  213. package/src/tests/sdk-allowlist.test.ts +47 -0
  214. package/src/tests/send-task-requested-by.test.ts +9 -0
  215. package/src/tests/session-costs.test.ts +506 -1
  216. package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
  217. package/src/tests/steering-core.test.ts +28 -0
  218. package/src/tests/task-artifacts.test.ts +386 -0
  219. package/src/tests/task-attachments-schema.test.ts +17 -1
  220. package/src/tests/task-completion-idempotency.test.ts +57 -0
  221. package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
  222. package/src/tests/update-profile-api.test.ts +67 -39
  223. package/src/tests/update-profile-auth.test.ts +48 -4
  224. package/src/tests/user-attribution-migration.test.ts +54 -0
  225. package/src/tests/user-identity.test.ts +72 -0
  226. package/src/tests/workflow-async-v2.test.ts +39 -1
  227. package/src/tests/workflow-http-v2.test.ts +3 -2
  228. package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
  229. package/src/tests/workflow-retry-v2.test.ts +45 -1
  230. package/src/tests/workflow-swarm-script.test.ts +14 -5
  231. package/src/tools/db-query.ts +18 -8
  232. package/src/tools/get-task-details.ts +24 -3
  233. package/src/tools/manage-user.ts +36 -3
  234. package/src/tools/poll-task.ts +93 -14
  235. package/src/tools/script-common.ts +5 -0
  236. package/src/tools/send-task.ts +1 -4
  237. package/src/tools/store-progress.ts +13 -2
  238. package/src/tools/swarm-config/delete-config.ts +9 -0
  239. package/src/tools/swarm-config/set-config.ts +3 -2
  240. package/src/tools/task-action.ts +30 -0
  241. package/src/tools/task-tool-ctx.ts +8 -1
  242. package/src/tools/update-profile.ts +16 -21
  243. package/src/tools/utils.ts +5 -0
  244. package/src/tools/workflows/trigger-workflow.ts +7 -1
  245. package/src/types.ts +165 -1
  246. package/src/utils/github-pull-request.ts +76 -0
  247. package/src/utils/identity-field-budget.ts +73 -0
  248. package/src/utils/multi-runtime.ts +20 -0
  249. package/src/utils/requester-comms.ts +24 -0
  250. package/src/utils/sandboxed-process.ts +40 -14
  251. package/src/workflows/engine.ts +22 -2
  252. package/src/workflows/executors/swarm-script.ts +24 -1
  253. package/src/workflows/resume.ts +19 -11
  254. package/src/workflows/retry-poller.ts +1 -0
  255. package/src/workflows/triggers.ts +4 -0
  256. package/templates/schedules/daily-compounding-reflection/content.md +12 -3
  257. package/templates/skills/db-query-guidance/config.json +14 -0
  258. package/templates/skills/db-query-guidance/content.md +57 -0
  259. package/dist/cli-3te4jdkm.js +0 -225
  260. package/dist/cli-bmze986d.js +0 -7
  261. package/dist/cli-kh3ba2zf.js +0 -607
  262. package/dist/index-hp1kdbjj.js +0 -36
  263. package/dist/mistral-conversations-81fwav0j.js +0 -30578
  264. package/dist/otel-nqa12wf3.js +0 -3149
  265. package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
@@ -5,6 +5,7 @@ import {
5
5
  getScriptMcpConnectionDescriptors,
6
6
  } from "../script-connections";
7
7
  import { buildScriptCredentialBindingsWithFailures } from "../script-credential-broker";
8
+ import { restoreScratchScriptLastUsedIfUnchanged, touchScratchScriptLastUsed } from "./db";
8
9
 
9
10
  export function getSavedScriptOwnerAgentId(script: ScriptRecord): string | null {
10
11
  return script.scopeId ?? script.createdByAgentId;
@@ -16,10 +17,15 @@ export async function runSavedScriptAsAgent(args: {
16
17
  input: unknown;
17
18
  agentId: string;
18
19
  }) {
20
+ // Touch before executing (not just after) so a scratch script that's already
21
+ // stale when a run starts can't be reaped by the retention sweep while the
22
+ // run — which may take up to the runtime's wall-clock ceiling — is in flight.
23
+ const runStartTouch = args.script.isScratch ? touchScratchScriptLastUsed(args.script.id) : null;
24
+
19
25
  const credentials = await buildScriptCredentialBindingsWithFailures({
20
26
  agentId: args.agentId,
21
27
  });
22
- return runScript({
28
+ const output = await runScript({
23
29
  source: args.script.source,
24
30
  args: args.input,
25
31
  fsMode: args.script.fsMode,
@@ -29,4 +35,13 @@ export async function runSavedScriptAsAgent(args: {
29
35
  apiConnections: getScriptApiConnectionDescriptors({ agentId: args.agentId }),
30
36
  mcpConnections: getScriptMcpConnectionDescriptors({ agentId: args.agentId }),
31
37
  });
38
+ if (output.exitCode === 0 && !output.error && !output.runtimeError) {
39
+ touchScratchScriptLastUsed(args.script.id);
40
+ } else if (runStartTouch) {
41
+ // The run-start touch didn't pan out — restore the pre-run timestamp so a
42
+ // failed invocation doesn't buy the script another retention window, as
43
+ // long as no concurrent run has touched it since.
44
+ restoreScratchScriptLastUsedIfUnchanged(args.script.id, args.script.updatedAt, runStartTouch);
45
+ }
46
+ return output;
32
47
  }
@@ -1,3 +1,4 @@
1
+ import { fileURLToPath } from "node:url";
1
2
  import ts from "typescript";
2
3
  import { getScriptAppTypes } from "@/apps/script-types";
3
4
  import { getScriptApiTypes, getScriptMcpTypes } from "@/be/script-connections";
@@ -127,7 +128,7 @@ export interface SwarmSdk {
127
128
  // --- repos ---
128
129
  repo_list(args?: Record<string, unknown>): Promise<unknown>;
129
130
  // --- schedules ---
130
- 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" }): Promise<unknown>;
131
+ 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>;
131
132
  // --- scripts ---
132
133
  script_search(args: { query?: string; scope?: ScriptScope; limit?: number }): Promise<unknown>;
133
134
  script_run(args: { name?: string; source?: string; args?: unknown; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode; idempotencyKey?: string }): Promise<unknown>;
@@ -799,7 +800,7 @@ const RUNTIME_GLOBALS_FILE = "/virtual/runtime-globals.d.ts";
799
800
  function scriptTypesBase(): string {
800
801
  const dir = process.env.SCRIPT_TYPES_DIR;
801
802
  if (dir) return `${dir}/index.ts`;
802
- return new URL("../../index.ts", import.meta.url).pathname;
803
+ return fileURLToPath(new URL("../../index.ts", import.meta.url));
803
804
  }
804
805
 
805
806
  function createCompilerHost(
@@ -58,6 +58,12 @@ import composioGoogleDocsConfig from "../../../templates/skills/composio-google-
58
58
  import composioGoogleDocsContent from "../../../templates/skills/composio-google-docs/content.md" with {
59
59
  type: "text",
60
60
  };
61
+ import dbQueryGuidanceConfig from "../../../templates/skills/db-query-guidance/config.json" with {
62
+ type: "text",
63
+ };
64
+ import dbQueryGuidanceContent from "../../../templates/skills/db-query-guidance/content.md" with {
65
+ type: "text",
66
+ };
61
67
  import delegateWorkConfig from "../../../templates/skills/delegate-work/config.json" with {
62
68
  type: "text",
63
69
  };
@@ -270,6 +276,7 @@ const BUILT_IN_SKILL_SOURCES = [
270
276
  { config: composioGmailConfig, body: composioGmailContent },
271
277
  { config: composioGoogleCalendarConfig, body: composioGoogleCalendarContent },
272
278
  { config: composioGoogleDocsConfig, body: composioGoogleDocsContent },
279
+ { config: dbQueryGuidanceConfig, body: dbQueryGuidanceContent },
273
280
  { config: delegateWorkConfig, body: delegateWorkContent },
274
281
  { config: designDocsConfig, body: designDocsContent },
275
282
  { config: downloadTaskAttachmentConfig, body: downloadTaskAttachmentContent },
@@ -76,7 +76,7 @@ export function promoteSteeringToTask(task: AgentTask, message: SteeringMessage)
76
76
  source: message.source === "script" ? "api" : message.source,
77
77
  taskType: "follow-up",
78
78
  parentTaskId: task.id,
79
- requestedByUserId: message.createdByUserId ?? task.requestedByUserId,
79
+ requestedByUserId: message.createdByUserId,
80
80
  bypassTrackerContextDedup: true,
81
81
  });
82
82
  }
@@ -126,7 +126,19 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
126
126
  if (value === "sdk" || value === "bearer") return null;
127
127
  return "Invalid BEDROCK_AUTH_MODE value (must be one of: sdk, bearer)";
128
128
  },
129
+ // Logical agent concurrency policy (agent scope) — the authoritative source
130
+ // that upsertSwarmConfigWithPolicyMirror mirrors into agents.maxTasks.
131
+ // Bounded to AgentSchema's maxTasks range [1, 100] so the mirrored column
132
+ // stays schema-valid.
133
+ AGENT_MAX_TASKS: (value) => {
134
+ const str = String(value).trim();
135
+ if (!/^\d+$/.test(str) || Number(str) < 1 || Number(str) > 100) {
136
+ return "Invalid AGENT_MAX_TASKS (must be an integer between 1 and 100)";
137
+ }
138
+ return null;
139
+ },
129
140
  ...booleanValidators([
141
+ "MULTI_RUNTIME_ENABLED",
130
142
  "STEERING_ENABLED",
131
143
  "MEMORY_HYBRID_SEARCH",
132
144
  "MEMORY_GRAPH_EXPANSION",
@@ -150,6 +162,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
150
162
  "OTEL_TRACE_POLL",
151
163
  "ANONYMIZED_TELEMETRY",
152
164
  "SWARM_HIDE_CLOUD_PROMO",
165
+ "DB_QUERY_BOUNDED_ENABLED",
153
166
  ]),
154
167
  ...enumValidator("SLACK_THREAD_STEERING", ["lead", "all"]),
155
168
  ...enumValidator("SLACK_THREAD_STEERING_MODE", ["steer", "queue"]),
@@ -167,8 +180,13 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
167
180
  "WORKFLOW_MAX_ITERATIONS",
168
181
  "WORKFLOW_MAX_STEPS_PER_RUN",
169
182
  "SCHEDULER_INTERVAL_MS",
183
+ "RUNTIME_STALE_THRESHOLD_MIN",
170
184
  "SCRIPT_RUN_CONCURRENCY_CAP",
171
185
  "WORKER_API_READY_TIMEOUT_SECONDS",
186
+ "DB_QUERY_HTTP_BUDGET_MS",
187
+ "DB_QUERY_HTTP_MAX_ROWS",
188
+ "DB_QUERY_MCP_BUDGET_MS",
189
+ "DB_QUERY_MCP_MAX_ROWS",
172
190
  ],
173
191
  1,
174
192
  ),
@@ -209,3 +209,57 @@ export async function awaitCredentials(opts: AwaitCredentialsOptions): Promise<C
209
209
  }
210
210
  return status;
211
211
  }
212
+
213
+ /** Options for {@link retryBootStep}; injectable sleep/log keep tests deterministic. */
214
+ export interface RetryBootStepOptions {
215
+ /** Names the step in retry logs and the exhaustion error. */
216
+ label?: string;
217
+ attempts?: number;
218
+ initialDelayMs?: number;
219
+ maxDelayMs?: number;
220
+ sleep?: (ms: number) => Promise<void>;
221
+ log?: (line: string) => void;
222
+ }
223
+
224
+ /**
225
+ * Bounded retry for a recovery-critical boot step after the credential wait.
226
+ * A runtime that went stale mid-wait is only revived by a SUCCESSFUL
227
+ * registration, and the readiness report that follows is the only transition
228
+ * out of `waiting_for_credentials` — so unlike the runner's best-effort
229
+ * periodic paths, these steps retry with bounded backoff and propagate
230
+ * failure once exhausted. The caller decides what exhaustion means (the
231
+ * runner treats it like boot-registration failure rather than entering the
232
+ * work loop as though recovery succeeded).
233
+ */
234
+ export async function retryBootStep(
235
+ step: () => Promise<void>,
236
+ opts: RetryBootStepOptions = {},
237
+ ): Promise<void> {
238
+ const label = opts.label ?? "boot step";
239
+ const attempts = opts.attempts ?? 5;
240
+ const sleep = opts.sleep ?? ((ms: number) => Bun.sleep(ms));
241
+ const log = opts.log ?? ((line: string) => console.warn(line));
242
+ const maxDelayMs = opts.maxDelayMs ?? 10_000;
243
+ let delayMs = opts.initialDelayMs ?? 1_000;
244
+ let lastError: unknown;
245
+
246
+ for (let attempt = 1; attempt <= attempts; attempt++) {
247
+ try {
248
+ await step();
249
+ return;
250
+ } catch (err) {
251
+ lastError = err;
252
+ if (attempt < attempts) {
253
+ log(
254
+ `[boot] ${label} failed (attempt ${attempt}/${attempts}, retry in ${delayMs}ms): ${err}`,
255
+ );
256
+ await sleep(delayMs);
257
+ delayMs = Math.min(delayMs * 2, maxDelayMs);
258
+ }
259
+ }
260
+ }
261
+
262
+ throw lastError instanceof Error
263
+ ? lastError
264
+ : new Error(`${label} failed after ${attempts} attempt(s): ${lastError}`);
265
+ }
@@ -61,7 +61,7 @@ export function CoreCredentialsStep({ state, goToNext, addLog }: StepProps) {
61
61
  setTimeout(() => setReady(true), 1000);
62
62
  };
63
63
 
64
- generate();
64
+ generate().catch((err) => addLog(`Credential generation failed: ${err}`));
65
65
  }, [state.services, addLog]);
66
66
 
67
67
  useEffect(() => {
@@ -40,7 +40,7 @@ export function CustomTemplatesStep({
40
40
  }
41
41
  };
42
42
 
43
- load();
43
+ load().catch((err) => addLog(`Template load failed: ${err}`));
44
44
  }, [addLog, goToError]);
45
45
 
46
46
  if (subStep === "loading") {
@@ -55,7 +55,7 @@ export function HarnessCredentialsStep({ goToNext, addLog }: StepProps) {
55
55
  setCliError(msg);
56
56
  setSubStep("manual_oauth");
57
57
  }
58
- })();
58
+ })().catch((err) => addLog(`Harness credential setup failed: ${err}`));
59
59
 
60
60
  return () => {
61
61
  cancelled = true;
@@ -92,7 +92,7 @@ export function HealthCheckStep({ state, addLog, goToNext }: StepProps) {
92
92
  }
93
93
  };
94
94
 
95
- poll();
95
+ poll().catch((err) => addLog(`Health poll failed: ${err}`));
96
96
  return () => {
97
97
  cancelled = true;
98
98
  };
@@ -35,7 +35,7 @@ export function StartStep({ state, addLog, goToNext }: StepProps) {
35
35
  }
36
36
  };
37
37
 
38
- run();
38
+ run().catch((err) => addLog(`Start failed: ${err}`));
39
39
  }, [state.outputDir, addLog]);
40
40
 
41
41
  useEffect(() => {
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { getApiKey } from "../utils/api-key.ts";
4
+ import { getMcpBaseUrl } from "../utils/constants.ts";
5
+ import { scrubSecrets } from "../utils/secret-scrubber.ts";
6
+ import {
7
+ auditProfileDivergences,
8
+ type FileReader,
9
+ type ProfileDivergence,
10
+ } from "./profile-sync.ts";
11
+
12
+ export interface ProfileSyncAuditConfig {
13
+ agentId: string;
14
+ apiUrl: string;
15
+ apiKey: string;
16
+ claudeMdPath?: string;
17
+ }
18
+
19
+ export async function runProfileSyncAudit(
20
+ config: ProfileSyncAuditConfig,
21
+ deps: { fetchImpl?: typeof fetch; readFile?: FileReader } = {},
22
+ ): Promise<{ agentId: string; divergences: ProfileDivergence[] }> {
23
+ const divergences = await auditProfileDivergences(
24
+ { ...config, fetchImpl: deps.fetchImpl },
25
+ deps.readFile,
26
+ );
27
+ return { agentId: config.agentId, divergences };
28
+ }
29
+
30
+ export function profileSyncAuditExitCode(divergences: ProfileDivergence[]): 0 | 2 {
31
+ return divergences.length > 0 ? 2 : 0;
32
+ }
33
+
34
+ if (import.meta.main) {
35
+ const agentId = process.env.AGENT_ID;
36
+ const apiUrl = getMcpBaseUrl();
37
+ const apiKey = getApiKey();
38
+ if (!agentId || !apiUrl || !apiKey) {
39
+ console.error("profile-sync-audit requires AGENT_ID, MCP_BASE_URL, and an API key");
40
+ process.exit(1);
41
+ }
42
+
43
+ try {
44
+ const result = await runProfileSyncAudit({
45
+ agentId,
46
+ apiUrl,
47
+ apiKey,
48
+ });
49
+ console.log(JSON.stringify(result, null, 2));
50
+ process.exit(profileSyncAuditExitCode(result.divergences));
51
+ } catch (error) {
52
+ const message = error instanceof Error ? error.message : String(error);
53
+ console.error(scrubSecrets(`profile-sync-audit failed: ${message}`));
54
+ process.exit(1);
55
+ }
56
+ }
@@ -2,7 +2,7 @@
2
2
  * Harness-agnostic FS → DB profile sync (worker-side, HTTP-only).
3
3
  *
4
4
  * Persists an agent's self-editable identity / config files back to the API:
5
- * - SOUL.md / IDENTITY.md / TOOLS.md / HEARTBEAT.md (bundled identity POST)
5
+ * - SOUL.md / IDENTITY.md / TOOLS.md / HEARTBEAT.md (independent POSTs)
6
6
  * - ~/.claude/CLAUDE.md (claude POST)
7
7
  * - /workspace/start-up.sh (agent-managed section) (setup POST)
8
8
  *
@@ -27,8 +27,15 @@
27
27
  * but it must be VISIBLE.
28
28
  */
29
29
 
30
+ import { resolveTemplateAsync } from "../prompts/resolver.ts";
31
+ import type { Agent, SwarmEvent } from "../types.ts";
30
32
  import { MAX_PROFILE_FILE_LENGTH } from "../utils/constants.ts";
33
+ import {
34
+ type BudgetedIdentityField,
35
+ IDENTITY_FIELD_BUDGETS,
36
+ } from "../utils/identity-field-budget.ts";
31
37
  import { scrubSecrets } from "../utils/secret-scrubber.ts";
38
+ import "./templates.ts";
32
39
 
33
40
  export const SOUL_MD_PATH = "/workspace/SOUL.md";
34
41
  export const IDENTITY_MD_PATH = "/workspace/IDENTITY.md";
@@ -56,6 +63,223 @@ export function contentSha256(content: string): string {
56
63
  return new Bun.CryptoHasher("sha256").update(content).digest("hex");
57
64
  }
58
65
 
66
+ export const PROFILE_MARKDOWN_FIELDS = [
67
+ "soulMd",
68
+ "identityMd",
69
+ "claudeMd",
70
+ "toolsMd",
71
+ "heartbeatMd",
72
+ ] as const;
73
+
74
+ export type ProfileMarkdownField = (typeof PROFILE_MARKDOWN_FIELDS)[number];
75
+ export type ProfileMarkdownSnapshot = Partial<
76
+ Record<ProfileMarkdownField, string | null | undefined>
77
+ >;
78
+
79
+ export interface ProfileDivergence {
80
+ field: ProfileMarkdownField;
81
+ diskSize: number | null;
82
+ dbSize: number;
83
+ budget: number | null;
84
+ delta: number | null;
85
+ diskHash: string | null;
86
+ dbHash: string;
87
+ diskMissing: boolean;
88
+ }
89
+
90
+ /** Pure disk-vs-DB comparison used by local profile-sync audits. */
91
+ export function findProfileDivergences(
92
+ disk: ProfileMarkdownSnapshot,
93
+ db: ProfileMarkdownSnapshot,
94
+ ): ProfileDivergence[] {
95
+ const divergences: ProfileDivergence[] = [];
96
+
97
+ for (const field of PROFILE_MARKDOWN_FIELDS) {
98
+ const diskValue = disk[field];
99
+ const dbValue = db[field] ?? "";
100
+ const budget =
101
+ field === "heartbeatMd" ? null : IDENTITY_FIELD_BUDGETS[field as BudgetedIdentityField];
102
+ if (diskValue === undefined || diskValue === null) {
103
+ if (dbValue.length === 0) continue;
104
+ divergences.push({
105
+ field,
106
+ diskSize: null,
107
+ dbSize: dbValue.length,
108
+ budget,
109
+ delta: null,
110
+ diskHash: null,
111
+ dbHash: contentSha256(dbValue),
112
+ diskMissing: true,
113
+ });
114
+ continue;
115
+ }
116
+ if (diskValue === dbValue) continue;
117
+
118
+ divergences.push({
119
+ field,
120
+ diskSize: diskValue.length,
121
+ dbSize: dbValue.length,
122
+ budget,
123
+ delta: diskValue.length - dbValue.length,
124
+ diskHash: contentSha256(diskValue),
125
+ dbHash: contentSha256(dbValue),
126
+ diskMissing: false,
127
+ });
128
+ }
129
+
130
+ return divergences;
131
+ }
132
+
133
+ const PROFILE_FIELD_DETAILS: Record<
134
+ BudgetedIdentityField,
135
+ { label: string; recoveryGuidance: string }
136
+ > = {
137
+ soulMd: {
138
+ label: `soulMd (${SOUL_MD_PATH})`,
139
+ recoveryGuidance: `Inspect and shrink the live ${SOUL_MD_PATH} first, moving durable tail content into memory. After a worker restart, use ${SOUL_MD_PATH}.pre-boot-<timestamp>.bak as the fallback copy of the rejected content.`,
140
+ },
141
+ identityMd: {
142
+ label: `identityMd (${IDENTITY_MD_PATH})`,
143
+ recoveryGuidance: `Inspect and shrink the live ${IDENTITY_MD_PATH} first, moving durable tail content into memory. After a worker restart, use ${IDENTITY_MD_PATH}.pre-boot-<timestamp>.bak as the fallback copy of the rejected content.`,
144
+ },
145
+ claudeMd: {
146
+ label:
147
+ "claudeMd (/workspace/CLAUDE.md for non-Claude harnesses; ~/.claude/CLAUDE.md for Claude)",
148
+ recoveryGuidance:
149
+ "Inspect and shrink the live CLAUDE.md source first, moving durable tail content into memory. After a worker restart, non-Claude harnesses can use /workspace/CLAUDE.md.pre-boot-<timestamp>.bak as the fallback copy; Claude's native ~/.claude/CLAUDE.md source is not archived by that boot step.",
150
+ },
151
+ toolsMd: {
152
+ label: `toolsMd (${TOOLS_MD_PATH})`,
153
+ recoveryGuidance: `Inspect and shrink the live ${TOOLS_MD_PATH} first, moving durable tail content into memory. After a worker restart, use ${TOOLS_MD_PATH}.pre-boot-<timestamp>.bak as the fallback copy of the rejected content.`,
154
+ },
155
+ };
156
+
157
+ /** Render one persisted sync rejection through the prompt-template registry. */
158
+ export async function renderProfileSyncRejectionBanner(event: SwarmEvent): Promise<string | null> {
159
+ const data = event.data;
160
+ const field = data?.field;
161
+ const diskSize = data?.diskSize;
162
+ const dbSize = data?.dbSize;
163
+ const budget = data?.budget;
164
+ const delta = data?.delta;
165
+ if (
166
+ typeof field !== "string" ||
167
+ !(field in PROFILE_FIELD_DETAILS) ||
168
+ typeof diskSize !== "number" ||
169
+ typeof dbSize !== "number" ||
170
+ typeof budget !== "number" ||
171
+ typeof delta !== "number"
172
+ ) {
173
+ return null;
174
+ }
175
+
176
+ const details = PROFILE_FIELD_DETAILS[field as BudgetedIdentityField];
177
+ const result = await resolveTemplateAsync("task.profile_sync_rejection", {
178
+ timestamp: event.createdAt,
179
+ field_label: details.label,
180
+ disk_size: diskSize,
181
+ db_size: dbSize,
182
+ budget,
183
+ delta: delta >= 0 ? `+${delta}` : String(delta),
184
+ event_id: event.id,
185
+ recovery_guidance: details.recoveryGuidance,
186
+ });
187
+ return result.skipped ? null : result.text;
188
+ }
189
+
190
+ /** Fetch every field's latest unresolved rejection before a provider session. */
191
+ export async function fetchProfileSyncRejectionBanner(
192
+ config: Pick<ProfileSyncOptions, "agentId" | "apiUrl" | "apiKey" | "claudeMdPath">,
193
+ fetchImpl: typeof fetch = fetch,
194
+ readFile: FileReader = readFileIfExists,
195
+ ): Promise<string> {
196
+ try {
197
+ const responses = await Promise.all(
198
+ (Object.keys(IDENTITY_FIELD_BUDGETS) as BudgetedIdentityField[]).map(async (field) => {
199
+ const fetchLatestEvent = async (event: SwarmEvent["event"]): Promise<SwarmEvent | null> => {
200
+ const query = new URLSearchParams({
201
+ event,
202
+ agentId: config.agentId,
203
+ dataField: field,
204
+ limit: "1",
205
+ });
206
+ const response = await fetchImpl(`${config.apiUrl}/api/events?${query}`, {
207
+ headers: {
208
+ Authorization: `Bearer ${config.apiKey}`,
209
+ "X-Agent-ID": config.agentId,
210
+ },
211
+ });
212
+ if (!response.ok) return null;
213
+ const payload = (await response.json()) as { events?: SwarmEvent[] };
214
+ return payload.events?.[0] ?? null;
215
+ };
216
+ const [rejection, reconciliation] = await Promise.all([
217
+ fetchLatestEvent("system.profile_sync_rejected"),
218
+ fetchLatestEvent("system.profile_sync_reconciled"),
219
+ ]);
220
+ return { rejection, reconciliation };
221
+ }),
222
+ );
223
+ const latestEvents = responses.flatMap(({ rejection, reconciliation }) =>
224
+ rejection ? [{ rejection, reconciliation }] : [],
225
+ );
226
+ if (latestEvents.length === 0) return "";
227
+
228
+ let profile: Record<string, unknown> | null = null;
229
+ const profileResponse = await fetchImpl(`${config.apiUrl}/me`, {
230
+ headers: {
231
+ Authorization: `Bearer ${config.apiKey}`,
232
+ "X-Agent-ID": config.agentId,
233
+ },
234
+ });
235
+ if (profileResponse.ok) {
236
+ profile = (await profileResponse.json()) as Record<string, unknown>;
237
+ }
238
+
239
+ let disk: Partial<Record<BudgetedIdentityField, string | undefined>> | null = null;
240
+ if (config.claudeMdPath) {
241
+ disk = {
242
+ soulMd: await readFile(SOUL_MD_PATH),
243
+ identityMd: await readFile(IDENTITY_MD_PATH),
244
+ claudeMd: await readFile(config.claudeMdPath),
245
+ toolsMd: await readFile(TOOLS_MD_PATH),
246
+ };
247
+ }
248
+
249
+ const unresolved = latestEvents.flatMap(({ rejection, reconciliation }) => {
250
+ if (reconciliation && reconciliation.createdAt > rejection.createdAt) return [];
251
+ const event = rejection;
252
+ const field = event.data?.field;
253
+ const rejectedDbHash = event.data?.dbHash;
254
+ const currentValue = typeof field === "string" ? profile?.[field] : undefined;
255
+ const diskValue =
256
+ typeof field === "string" ? disk?.[field as BudgetedIdentityField] : undefined;
257
+ if (typeof currentValue === "string" && diskValue === currentValue) return [];
258
+ return !(
259
+ typeof rejectedDbHash === "string" &&
260
+ typeof currentValue === "string" &&
261
+ contentSha256(currentValue) !== rejectedDbHash
262
+ )
263
+ ? [event]
264
+ : [];
265
+ });
266
+ const banners = await Promise.all(unresolved.map(renderProfileSyncRejectionBanner));
267
+ return banners.filter((banner): banner is string => banner !== null).join("");
268
+ } catch {
269
+ return "";
270
+ }
271
+ }
272
+
273
+ export async function prependProfileSyncRejectionBanner(
274
+ prompt: string,
275
+ config: Pick<ProfileSyncOptions, "agentId" | "apiUrl" | "apiKey" | "claudeMdPath">,
276
+ fetchImpl: typeof fetch = fetch,
277
+ readFile: FileReader = readFileIfExists,
278
+ ): Promise<{ prompt: string; injected: boolean }> {
279
+ const banner = await fetchProfileSyncRejectionBanner(config, fetchImpl, readFile);
280
+ return banner ? { prompt: banner + prompt, injected: true } : { prompt, injected: false };
281
+ }
282
+
59
283
  export async function writeIdentityBaselines(baselines: IdentityBaselines): Promise<void> {
60
284
  await Bun.write(IDENTITY_BASELINES_PATH, JSON.stringify(baselines));
61
285
  }
@@ -168,9 +392,23 @@ export function resolveClaudeMdPath(completedProviders: readonly string[]): stri
168
392
  }
169
393
 
170
394
  /** A single profile-update POST body, tagged with a label for logging. */
171
- interface ProfilePayload {
395
+ export interface ProfilePayload {
172
396
  label: string;
173
- body: Record<string, unknown>;
397
+ body: Record<string, string>;
398
+ }
399
+
400
+ /**
401
+ * Keep identity-file sync independent: one rejected ratcheting-budget write
402
+ * must not discard valid edits to other files from the same session.
403
+ */
404
+ export function buildIndependentIdentityPayloads(
405
+ updates: Record<string, string>,
406
+ changeSource: ProfileChangeSource,
407
+ ): ProfilePayload[] {
408
+ return Object.entries(updates).map(([field, value]) => ({
409
+ label: `identity.${field}`,
410
+ body: { [field]: value, changeSource },
411
+ }));
174
412
  }
175
413
 
176
414
  /**
@@ -229,8 +467,6 @@ export function buildIdentityPayload(
229
467
  const content = files.soulMd;
230
468
  if (baselines?.soulMd && contentSha256(content) === baselines.soulMd) {
231
469
  // File unchanged during session — skip to preserve Lead's DB edits
232
- } else if (content.length > MAX_PROFILE_FILE_LENGTH) {
233
- warnProfileFileTooLarge(agentId, "soulMd", content.length);
234
470
  } else if (content.trim()) {
235
471
  if (content.length < IDENTITY_FILE_MIN_LENGTH) {
236
472
  console.error(
@@ -246,8 +482,6 @@ export function buildIdentityPayload(
246
482
  const content = files.identityMd;
247
483
  if (baselines?.identityMd && contentSha256(content) === baselines.identityMd) {
248
484
  // File unchanged during session — skip to preserve Lead's DB edits
249
- } else if (content.length > MAX_PROFILE_FILE_LENGTH) {
250
- warnProfileFileTooLarge(agentId, "identityMd", content.length);
251
485
  } else if (content.trim()) {
252
486
  if (content.length < IDENTITY_FILE_MIN_LENGTH) {
253
487
  console.error(
@@ -263,8 +497,6 @@ export function buildIdentityPayload(
263
497
  const content = files.toolsMd;
264
498
  if (baselines?.toolsMd && contentSha256(content) === baselines.toolsMd) {
265
499
  // File unchanged during session — skip
266
- } else if (content.length > MAX_PROFILE_FILE_LENGTH) {
267
- warnProfileFileTooLarge(agentId, "toolsMd", content.length);
268
500
  } else if (content.trim()) {
269
501
  updates.toolsMd = content;
270
502
  }
@@ -298,6 +530,50 @@ async function readFileIfExists(path: string): Promise<string | undefined> {
298
530
  }
299
531
  }
300
532
 
533
+ /**
534
+ * Compare the five local profile markdown files with the authenticated agent's
535
+ * stored DB values. This must run inside the agent's local environment; a
536
+ * central script cannot read another worker container's `/workspace` files.
537
+ */
538
+ export async function auditProfileDivergences(
539
+ opts: Pick<ProfileSyncOptions, "agentId" | "apiUrl" | "apiKey" | "claudeMdPath" | "fetchImpl">,
540
+ readFile: FileReader = readFileIfExists,
541
+ ): Promise<ProfileDivergence[]> {
542
+ const doFetch = opts.fetchImpl ?? fetch;
543
+ const response = await doFetch(`${opts.apiUrl}/me`, {
544
+ headers: {
545
+ Authorization: `Bearer ${opts.apiKey}`,
546
+ "X-Agent-ID": opts.agentId,
547
+ },
548
+ });
549
+ if (!response.ok) {
550
+ throw new Error(`Profile divergence audit failed: HTTP ${response.status}`);
551
+ }
552
+
553
+ const stored = (await response.json()) as Agent;
554
+ const claudeMdPath =
555
+ opts.claudeMdPath ??
556
+ (stored.harnessProvider
557
+ ? stored.harnessProvider === "claude"
558
+ ? CLAUDE_MD_PATH
559
+ : WORKSPACE_CLAUDE_MD_PATH
560
+ : null);
561
+ if (!claudeMdPath) {
562
+ throw new Error(
563
+ "Profile divergence audit requires a registered harness provider or an explicit claudeMdPath",
564
+ );
565
+ }
566
+ const disk: ProfileMarkdownSnapshot = {
567
+ soulMd: await readFile(SOUL_MD_PATH),
568
+ identityMd: await readFile(IDENTITY_MD_PATH),
569
+ claudeMd: await readFile(claudeMdPath),
570
+ toolsMd: await readFile(TOOLS_MD_PATH),
571
+ heartbeatMd: await readFile(HEARTBEAT_MD_PATH),
572
+ };
573
+
574
+ return findProfileDivergences(disk, stored);
575
+ }
576
+
301
577
  /**
302
578
  * Collect the profile-update POST bodies to send. Each entry is one POST.
303
579
  * `fields` selects which groups to include. The file reader is injectable so
@@ -331,16 +607,12 @@ export async function collectProfilePayloads(
331
607
  baselines,
332
608
  agentId,
333
609
  );
334
- if (Object.keys(updates).length > 0) {
335
- payloads.push({ label: "identity", body: { ...updates, changeSource } });
336
- }
610
+ payloads.push(...buildIndependentIdentityPayloads(updates, changeSource));
337
611
  }
338
612
 
339
613
  if (fields.includes("claude")) {
340
614
  const raw = await readFile(claudeMdPath);
341
- if (raw && raw.length > MAX_PROFILE_FILE_LENGTH) {
342
- warnProfileFileTooLarge(agentId, "claudeMd", raw.length);
343
- } else if (raw?.trim()) {
615
+ if (raw?.trim()) {
344
616
  if (baselines?.claudeMd && contentSha256(raw) === baselines.claudeMd) {
345
617
  // CLAUDE.md unchanged during session — skip to preserve Lead's DB edits
346
618
  } else {