@desplega.ai/agent-swarm 1.113.0 → 1.115.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 (237) hide show
  1. package/README.md +2 -2
  2. package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
  3. package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
  4. package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
  5. package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
  6. package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
  7. package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
  8. package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
  9. package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
  10. package/dist/cli-0pygq9r1.js +429 -0
  11. package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
  12. package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
  13. package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
  14. package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
  15. package/dist/cli-52fg7s7x.js +269 -0
  16. package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
  17. package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
  18. package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
  19. package/dist/cli-86fdnerz.js +405 -0
  20. package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
  21. package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
  22. package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
  23. package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
  24. package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
  25. package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
  26. package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
  27. package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
  28. package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
  29. package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
  30. package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
  31. package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
  32. package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
  33. package/dist/cli-jkhwmmgg.js +136 -0
  34. package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
  35. package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
  36. package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
  37. package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
  38. package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
  39. package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
  40. package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
  41. package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
  42. package/dist/cli-t0vqevnm.js +393 -0
  43. package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
  44. package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
  45. package/dist/cli-wh91zby6.js +187222 -0
  46. package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
  47. package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
  48. package/dist/cli.js +32 -15
  49. package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
  50. package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
  51. package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
  52. package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
  53. package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
  54. package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
  55. package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
  56. package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
  57. package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
  58. package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
  59. package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
  60. package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
  61. package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
  62. package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
  63. package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
  64. package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
  65. package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
  66. package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
  67. package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
  68. package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
  69. package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
  70. package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
  71. package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
  72. package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
  73. package/dist/rbac-roles-kfesjy9y.js +28 -0
  74. package/dist/rbac-roles-mwcwhbxc.js +283 -0
  75. package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
  76. package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
  77. package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
  78. package/openapi.json +1327 -20
  79. package/package.json +3 -3
  80. package/src/agentmail/handlers.ts +16 -6
  81. package/src/agentmail/templates.ts +25 -5
  82. package/src/be/audit-user.ts +28 -4
  83. package/src/be/db-queries/oauth.ts +42 -0
  84. package/src/be/db.ts +314 -28
  85. package/src/be/identity.ts +79 -0
  86. package/src/be/mcp-proxy.ts +132 -0
  87. package/src/be/migrations/109_rbac_roles.sql +79 -0
  88. package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
  89. package/src/be/migrations/112_script_connections_graphql.sql +96 -0
  90. package/src/be/migrations/113_task_routing_affinity.sql +9 -0
  91. package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
  92. package/src/be/modelsdev-cache.json +135 -0
  93. package/src/be/oauth-credential-bindings.ts +35 -0
  94. package/src/be/oauth-refresh-sweep.ts +159 -0
  95. package/src/be/rbac-audit.ts +52 -13
  96. package/src/be/rbac-roles.ts +424 -0
  97. package/src/be/script-connections.ts +459 -40
  98. package/src/be/script-credential-broker.ts +29 -3
  99. package/src/be/scripts/typecheck.ts +51 -7
  100. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
  101. package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
  102. package/src/be/users.ts +24 -0
  103. package/src/cli.tsx +17 -0
  104. package/src/github/handlers.ts +10 -9
  105. package/src/github/templates.ts +45 -9
  106. package/src/gitlab/handlers.ts +20 -8
  107. package/src/gitlab/templates.ts +18 -6
  108. package/src/heartbeat/heartbeat.ts +143 -21
  109. package/src/http/all-routes.ts +3 -0
  110. package/src/http/approval-requests.ts +12 -0
  111. package/src/http/config.ts +44 -10
  112. package/src/http/core.ts +30 -3
  113. package/src/http/favorites.ts +1 -5
  114. package/src/http/index.ts +25 -1
  115. package/src/http/mcp-oauth.ts +2 -0
  116. package/src/http/mcp-servers.ts +44 -3
  117. package/src/http/oauth-generic.ts +105 -0
  118. package/src/http/poll.ts +20 -3
  119. package/src/http/script-connection-proxy.ts +116 -0
  120. package/src/http/script-connections.ts +1657 -0
  121. package/src/http/scripts.ts +27 -7
  122. package/src/http/skills.ts +11 -0
  123. package/src/http/tasks.ts +27 -13
  124. package/src/http/x.ts +6 -2
  125. package/src/integrations/kapso/inbound.ts +23 -6
  126. package/src/jira/sync.ts +111 -17
  127. package/src/jira/templates.ts +10 -2
  128. package/src/mcp-client/http-client.ts +194 -0
  129. package/src/oauth/app-validation.ts +16 -0
  130. package/src/oauth/ensure-token.ts +69 -14
  131. package/src/oauth/mcp-wrapper.ts +15 -0
  132. package/src/oauth/wrapper.ts +48 -20
  133. package/src/prompts/session-templates.ts +3 -3
  134. package/src/providers/codex-models.ts +100 -30
  135. package/src/providers/modelsdev-reasoning.json +51 -0
  136. package/src/providers/pi-mono-mcp-client.ts +2 -133
  137. package/src/providers/reasoning-effort.ts +11 -5
  138. package/src/rbac/admission.ts +68 -0
  139. package/src/rbac/index.ts +2 -0
  140. package/src/rbac/legacy-policy.ts +12 -0
  141. package/src/rbac/permissions.ts +48 -0
  142. package/src/scheduler/scheduler.ts +9 -1
  143. package/src/scripts-runtime/api-client.ts +151 -19
  144. package/src/scripts-runtime/api-types.ts +38 -6
  145. package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
  146. package/src/scripts-runtime/credential-broker/types.ts +8 -0
  147. package/src/scripts-runtime/ctx.ts +11 -1
  148. package/src/scripts-runtime/eval-harness.ts +5 -1
  149. package/src/scripts-runtime/executors/types.ts +2 -1
  150. package/src/scripts-runtime/loader.ts +3 -1
  151. package/src/scripts-runtime/mcp-client.ts +87 -0
  152. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  153. package/src/scripts-runtime/types/stdlib.d.ts +6 -0
  154. package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
  155. package/src/server-user.ts +114 -59
  156. package/src/server.ts +12 -0
  157. package/src/slack/assistant.ts +14 -6
  158. package/src/slack/enrich.ts +17 -0
  159. package/src/slack/handlers.ts +8 -22
  160. package/src/slack/responses.ts +91 -1
  161. package/src/slack/thread-buffer.ts +6 -3
  162. package/src/slack/watcher.ts +9 -6
  163. package/src/tasks/worker-follow-up.ts +100 -3
  164. package/src/tests/agentmail-handlers.test.ts +32 -3
  165. package/src/tests/approval-requests.test.ts +61 -0
  166. package/src/tests/audit-user.test.ts +47 -1
  167. package/src/tests/codex-adapter.test.ts +46 -23
  168. package/src/tests/credential-broker.test.ts +5 -2
  169. package/src/tests/delete-page-tool.test.ts +197 -0
  170. package/src/tests/github-handlers.test.ts +28 -2
  171. package/src/tests/gitlab-handlers.test.ts +125 -0
  172. package/src/tests/heartbeat.test.ts +48 -3
  173. package/src/tests/identity.test.ts +117 -0
  174. package/src/tests/jira-sync.test.ts +99 -1
  175. package/src/tests/kapso-inbound.test.ts +7 -0
  176. package/src/tests/mcp-tools-user.test.ts +60 -6
  177. package/src/tests/model-control.test.ts +8 -2
  178. package/src/tests/oauth-credential-bindings.test.ts +490 -0
  179. package/src/tests/oauth-refresh-sweep.test.ts +160 -0
  180. package/src/tests/pool-affinity.test.ts +401 -0
  181. package/src/tests/prompt-template-remaining.test.ts +2 -2
  182. package/src/tests/rbac-admission-e2e.test.ts +386 -0
  183. package/src/tests/rbac-admission.test.ts +492 -0
  184. package/src/tests/rbac-e2e-helpers.ts +3 -0
  185. package/src/tests/rbac-engine.test.ts +50 -0
  186. package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
  187. package/src/tests/rbac-roles.test.ts +328 -0
  188. package/src/tests/reasoning-effort.test.ts +15 -2
  189. package/src/tests/schedule-target-type.test.ts +50 -0
  190. package/src/tests/scheduled-tasks.test.ts +40 -0
  191. package/src/tests/script-apis-mcp.test.ts +37 -13
  192. package/src/tests/script-connections-http.test.ts +1158 -0
  193. package/src/tests/script-connections-mcp.test.ts +894 -0
  194. package/src/tests/script-connections.test.ts +754 -6
  195. package/src/tests/scripts-external-api.test.ts +49 -1
  196. package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
  197. package/src/tests/scripts-runtime.test.ts +291 -0
  198. package/src/tests/seed-scripts.test.ts +36 -0
  199. package/src/tests/slack-identity-resolution.test.ts +37 -3
  200. package/src/tests/slack-inline-output.test.ts +65 -0
  201. package/src/tests/slack-thread-buffer.test.ts +28 -0
  202. package/src/tests/tool-annotations.test.ts +1 -0
  203. package/src/tests/user-token-rest-auth.test.ts +140 -1
  204. package/src/tests/workflow-swarm-script.test.ts +44 -0
  205. package/src/tests/workflow-triggers-v2.test.ts +300 -1
  206. package/src/tools/credential-bindings/tool.ts +277 -18
  207. package/src/tools/delete-page.ts +145 -0
  208. package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
  209. package/src/tools/request-human-input.ts +2 -0
  210. package/src/tools/resolve-user.ts +77 -27
  211. package/src/tools/script-apis.ts +62 -3
  212. package/src/tools/script-connections/tool.ts +275 -31
  213. package/src/tools/send-task.ts +15 -1
  214. package/src/tools/task-action.ts +34 -2
  215. package/src/tools/templates.ts +53 -1
  216. package/src/tools/tool-config.ts +2 -1
  217. package/src/tools/utils.ts +2 -0
  218. package/src/tools/workflows/create-workflow.ts +8 -1
  219. package/src/tools/workflows/update-workflow.ts +11 -2
  220. package/src/types.ts +90 -11
  221. package/src/utils/scoped-resource.ts +25 -0
  222. package/src/workflows/executors/human-in-the-loop.ts +1 -0
  223. package/src/workflows/executors/swarm-script.ts +7 -1
  224. package/src/workflows/triggers.ts +146 -18
  225. package/templates/skills/swarm-scripts/content.md +33 -8
  226. package/dist/cli-bgef578c.js +0 -771
  227. package/dist/cli-c9mtm09b.js +0 -43
  228. package/dist/cli-ceewqajw.js +0 -1698
  229. package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
  230. package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
  231. package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
  232. package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
  233. package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
  234. package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
  235. package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
  236. package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
  237. package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
@@ -1,4 +1,5 @@
1
1
  import {
2
+ buildRoutingAffinityFromAgent,
2
3
  createTaskExtended,
3
4
  getActiveTaskCount,
4
5
  getAgentById,
@@ -10,7 +11,7 @@ import {
10
11
  } from "../be/db";
11
12
  import { repointTrackerSyncBySwarmId } from "../be/db-queries/tracker";
12
13
  import { resolveTemplate } from "../prompts/resolver";
13
- import type { AgentTask, ResumeReason, TaskAttachment } from "../types";
14
+ import type { Agent, AgentTask, ResumeReason, TaskAttachment } from "../types";
14
15
  import { taskAttachmentDisplayUrl } from "../utils/task-attachment-links";
15
16
  // Side-effect import: registers task lifecycle templates in the in-memory registry.
16
17
  import "../tools/templates";
@@ -69,6 +70,26 @@ export const CRASH_RECOVERY_PIN_TAG = "crash-recovery-pin";
69
70
  */
70
71
  export const GRACEFUL_SHUTDOWN_PIN_TAG = "graceful-shutdown-pin";
71
72
 
73
+ /**
74
+ * Tag set on a reboot-sweep retry child that was pinned back to its
75
+ * original agent (routing-affinity Phase 3). Same reaper-scoping purpose as
76
+ * `CRASH_RECOVERY_PIN_TAG`/`GRACEFUL_SHUTDOWN_PIN_TAG` — kept in sync with
77
+ * the literal duplicated in `getStalePinnedResumes` (src/be/db.ts).
78
+ */
79
+ export const REBOOT_RETRY_PIN_TAG = "reboot-retry-pin";
80
+
81
+ /**
82
+ * Shared "is this agent even a pin candidate" gate: the row still exists and
83
+ * is not `offline`. Used identically by `createResumeFollowUp`'s same-agent
84
+ * pin and the heartbeat's reboot-retry pin (`runRebootSweep`) — both then
85
+ * apply their own capacity (and, for resumes, freshness) rules on top.
86
+ */
87
+ export function getPinCandidateAgent(agentId: string): Agent | null {
88
+ const candidate = getAgentById(agentId);
89
+ if (!candidate || candidate.status === "offline") return null;
90
+ return candidate;
91
+ }
92
+
72
93
  export function getResumeGeneration(task: Pick<AgentTask, "tags">): number {
73
94
  const tag = task.tags.find((value) => value.startsWith(RESUME_GENERATION_TAG_PREFIX));
74
95
  if (!tag) return 0;
@@ -264,8 +285,8 @@ export function createResumeFollowUp(args: {
264
285
  // responsive, so keep requiring `fresh`.
265
286
  let preferredAgentId: string | undefined;
266
287
  if (parent.agentId) {
267
- const candidate = getAgentById(parent.agentId);
268
- if (candidate && candidate.status !== "offline") {
288
+ const candidate = getPinCandidateAgent(parent.agentId);
289
+ if (candidate) {
269
290
  const lastActivity = candidate.lastActivityAt ? Date.parse(candidate.lastActivityAt) : 0;
270
291
  const fresh =
271
292
  Number.isFinite(lastActivity) &&
@@ -328,6 +349,17 @@ export function createResumeFollowUp(args: {
328
349
  // deliberately excluded there so the resume task resolves to the claiming
329
350
  // agent's own provider/model — never the parent's concrete model string.
330
351
  // We only override what's SPECIFIC to the resume task here.
352
+ //
353
+ // Routing affinity: stamp a FRESH snapshot from the parent's own agent —
354
+ // this covers every leg (pinned AND the pool-fallback legs) so a resume
355
+ // that falls to the unassigned pool is still role/capability-gated. When
356
+ // the agent row is already gone, `buildRoutingAffinityFromAgent` returns
357
+ // `null` and we pass `undefined`, letting `createTaskExtended`'s
358
+ // parentTaskId inheritance block fall back to the parent's OWN
359
+ // (already-inherited) `routingAffinity` instead.
360
+ const routingAffinity = parent.agentId
361
+ ? (buildRoutingAffinityFromAgent(parent.agentId) ?? undefined)
362
+ : undefined;
331
363
  const created = createTaskExtended(followUpDescription, {
332
364
  agentId: preferredAgentId,
333
365
  creatorAgentId: parent.creatorAgentId,
@@ -336,6 +368,7 @@ export function createResumeFollowUp(args: {
336
368
  tags,
337
369
  priority,
338
370
  parentTaskId: parent.id,
371
+ routingAffinity,
339
372
  });
340
373
 
341
374
  // Repoint Linear / Jira `tracker_sync` rows from the (now terminal) parent
@@ -447,3 +480,67 @@ export function createRerouteDecisionTask(args: {
447
480
 
448
481
  return { kind: "created", task: created };
449
482
  }
483
+
484
+ /** Result of `createPoolStarvationDecisionTask`. */
485
+ export type CreatePoolStarvationDecisionResult =
486
+ | { kind: "created"; task: AgentTask }
487
+ | { kind: "skipped"; reason: "lead_not_found" | "duplicate_exists" };
488
+
489
+ /**
490
+ * Hand the Lead a re-delegation DECISION task for a pooled, affinity-tagged
491
+ * task that has sat `unassigned` past `POOL_AFFINITY_ESCALATION_MIN` with
492
+ * ZERO eligible registered agents (routing affinity Phase 3). This is the
493
+ * "queue, then escalate" fallback for the pool legs (7/8) that `createResumeFollowUp`
494
+ * and the reboot-retry pin can fall to — mirrors `createRerouteDecisionTask`'s
495
+ * shape (same `taskType: "reroute-decision"` discriminator, so it reuses the
496
+ * same idempotency check and Slack re-delegation carve-out) but for a task
497
+ * that was never pinned in the first place, so there is no `staleResume` /
498
+ * resume-generation budget to derive.
499
+ *
500
+ * Invoked by the heartbeat (`escalateStarvedPoolTasks`), which has already
501
+ * confirmed no registered agent (any status) satisfies `isAgentEligibleForTask`
502
+ * for this task.
503
+ */
504
+ export function createPoolStarvationDecisionTask(args: {
505
+ original: AgentTask;
506
+ }): CreatePoolStarvationDecisionResult {
507
+ const { original } = args;
508
+
509
+ const leadAgent = getLeadAgent();
510
+ if (!leadAgent) return { kind: "skipped", reason: "lead_not_found" };
511
+
512
+ if (hasNonTerminalRerouteDecisionChild(original.id)) {
513
+ return { kind: "skipped", reason: "duplicate_exists" };
514
+ }
515
+
516
+ const affinity = original.routingAffinity;
517
+ const requiredRole = affinity?.role ?? "(none declared)";
518
+ const requiredCapabilities =
519
+ affinity?.capabilities && affinity.capabilities.length > 0
520
+ ? affinity.capabilities.join(", ")
521
+ : "(none declared)";
522
+ const attachmentsBlock = formatAttachmentsBlock(getTaskAttachments(original.id));
523
+
524
+ const decision = resolveTemplate("task.pool.starved.decision", {
525
+ original_task_id: original.id,
526
+ task_desc: original.task.slice(0, 200),
527
+ required_role: requiredRole,
528
+ required_capabilities: requiredCapabilities,
529
+ artifacts_block: attachmentsBlock,
530
+ });
531
+
532
+ const created = createTaskExtended(decision.text, {
533
+ agentId: leadAgent.id,
534
+ creatorAgentId: original.creatorAgentId,
535
+ source: "system",
536
+ taskType: "reroute-decision",
537
+ tags: ["reroute-decision", "pool-starvation"],
538
+ priority: Math.min(100, (original.priority ?? 50) + 10),
539
+ parentTaskId: original.id,
540
+ // Same rationale as createRerouteDecisionTask: don't hold the Lead's
541
+ // re-delegation decision to the original work's output contract.
542
+ inheritParentOutputSchema: false,
543
+ });
544
+
545
+ return { kind: "created", task: created };
546
+ }
@@ -1,4 +1,4 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlinkSync } from "node:fs";
3
3
  import { handleMessageReceived } from "../agentmail/handlers";
4
4
  import type { AgentMailWebhookPayload } from "../agentmail/types";
@@ -69,7 +69,19 @@ function makePayload(opts: {
69
69
  };
70
70
  }
71
71
 
72
- beforeAll(() => {
72
+ // Per-test (not per-file) DB reset: bun's default `retry` (bunfig.toml) retries
73
+ // a failing test in place, re-invoking beforeEach/afterEach around each
74
+ // attempt. With a file-level beforeAll/afterAll instead, a transient failure
75
+ // on attempt 1 (of any cause) leaves its side effects (the just-created user)
76
+ // in place for the retry, which then finds that user already resolved and
77
+ // silently no-ops the create — turning one flaky attempt into a deterministic
78
+ // "before+1 != after" mismatch on every subsequent attempt. Resetting to a
79
+ // fresh DB (and re-registering the lead) before every test/retry makes each
80
+ // attempt idempotent regardless of how many times it's retried, and also
81
+ // re-runs initDb's global resolver registration right before this file's own
82
+ // resolveTemplate() calls, minimizing the window for another concurrently
83
+ // executing test file's own initDb() to have repointed it in between.
84
+ beforeEach(async () => {
73
85
  for (const suffix of ["", "-wal", "-shm"]) {
74
86
  try {
75
87
  unlinkSync(`${TEST_DB_PATH}${suffix}`);
@@ -79,9 +91,17 @@ beforeAll(() => {
79
91
  // Ensure a lead exists so handler's findLeadAgent() returns truthy and a
80
92
  // task gets created on the "no inbox mapping" path.
81
93
  createAgent({ name: "LeadAgent", isLead: true, status: "idle" });
94
+ // Re-register agentmail templates — prompt-template-resolver.test.ts and
95
+ // prompt-template-session.test.ts call clearTemplateDefinitions() and never
96
+ // restore the shared (process-wide) registry, so if either runs first in
97
+ // the same bun worker, resolveTemplate("agentmail.email.*", ...) silently
98
+ // returns { text: "", skipped: true } here instead of throwing — producing
99
+ // a task with an empty body rather than a visible failure. Same defensive
100
+ // pattern as heartbeat-checklist.test.ts's beforeEach.
101
+ await import(`../agentmail/templates?t=${Date.now()}`);
82
102
  });
83
103
 
84
- afterAll(() => {
104
+ afterEach(() => {
85
105
  closeDb();
86
106
  for (const suffix of ["", "-wal", "-shm"]) {
87
107
  try {
@@ -111,6 +131,10 @@ describe("handleMessageReceived — identity auto-link via findOrCreateUserByEma
111
131
  const task = getTaskById(result.taskId!);
112
132
  expect(task).not.toBeNull();
113
133
  expect(task!.requestedByUserId).toBe(user!.id);
134
+ // The resolved canonical name renders in the task text — never the raw
135
+ // From header (Alice's display name + email as typed by the sender).
136
+ expect(task!.task).toContain("Alice Newcomer (email:alice.newcomer@example.com)");
137
+ expect(task!.task).not.toContain("Alice Newcomer <alice.newcomer@example.com>");
114
138
  });
115
139
 
116
140
  test("KNOWN sender (existing users.email) → no duplicate row, auto_merge event, task requestedByUserId populated", async () => {
@@ -162,5 +186,10 @@ describe("handleMessageReceived — identity auto-link via findOrCreateUserByEma
162
186
 
163
187
  const task = getTaskById(result.taskId!);
164
188
  expect(task!.requestedByUserId).toBeFalsy();
189
+ // No display name — not even the raw provider label — is ever rendered
190
+ // for an unresolved sender. The task text carries only the non-name
191
+ // sentinel shape, never the raw From header.
192
+ expect(task!.task).toContain("email:unknown (unknown user)");
193
+ expect(task!.task).not.toContain("Unknown Sender (no address)");
165
194
  });
166
195
  });
@@ -6,6 +6,7 @@ import {
6
6
  createAgent,
7
7
  createApprovalRequest,
8
8
  createTaskExtended,
9
+ createUser,
9
10
  getAgentCurrentTask,
10
11
  getApprovalRequestById,
11
12
  getApprovalRequestByStepId,
@@ -257,6 +258,24 @@ describe("Approval Requests", () => {
257
258
  const result = createApprovalRequest(data);
258
259
  expect(result.questions).toEqual(questions);
259
260
  });
261
+
262
+ test("stamps createdBy when provided, round-trips through getApprovalRequestById", () => {
263
+ const requester = createUser({
264
+ name: "Approval Provenance User",
265
+ email: "approval-provenance@example.com",
266
+ });
267
+ const data = makeApprovalData({ createdBy: requester.id });
268
+ const created = createApprovalRequest(data);
269
+ expect(created.createdBy).toBe(requester.id);
270
+
271
+ const fetched = getApprovalRequestById(created.id);
272
+ expect(fetched?.createdBy).toBe(requester.id);
273
+ });
274
+
275
+ test("createdBy is undefined when not provided", () => {
276
+ const result = createApprovalRequest(makeApprovalData());
277
+ expect(result.createdBy).toBeUndefined();
278
+ });
260
279
  });
261
280
 
262
281
  describe("DB: getApprovalRequestById", () => {
@@ -588,6 +607,48 @@ describe("Approval Requests", () => {
588
607
  expect(created!.workflowRunStepId).toBe(stepId);
589
608
  });
590
609
 
610
+ test("stamps createdBy from meta.requestedByUserId (workflow-run provenance)", async () => {
611
+ const requester = createUser({
612
+ name: "HITL Requester",
613
+ email: "hitl-requester@example.com",
614
+ });
615
+ const stepId = crypto.randomUUID();
616
+ const meta = { ...mockMeta, stepId, requestedByUserId: requester.id };
617
+ const executor = new HumanInTheLoopExecutor(mockDeps);
618
+
619
+ await executor.run({
620
+ config: {
621
+ title: "Deploy approval",
622
+ questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
623
+ approvers: { policy: "any" },
624
+ },
625
+ context: {},
626
+ meta,
627
+ });
628
+
629
+ const created = getApprovalRequestByStepId(stepId);
630
+ expect(created?.createdBy).toBe(requester.id);
631
+ });
632
+
633
+ test("leaves createdBy unset when the workflow run carries no requester", async () => {
634
+ const stepId = crypto.randomUUID();
635
+ const meta = { ...mockMeta, stepId };
636
+ const executor = new HumanInTheLoopExecutor(mockDeps);
637
+
638
+ await executor.run({
639
+ config: {
640
+ title: "Deploy approval",
641
+ questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
642
+ approvers: { policy: "any" },
643
+ },
644
+ context: {},
645
+ meta,
646
+ });
647
+
648
+ const created = getApprovalRequestByStepId(stepId);
649
+ expect(created?.createdBy).toBeUndefined();
650
+ });
651
+
591
652
  test("idempotency: returns async marker for pending existing request", async () => {
592
653
  const stepId = crypto.randomUUID();
593
654
  // Pre-create an approval request for this step
@@ -11,9 +11,12 @@ import { unlink } from "node:fs/promises";
11
11
  import type { IncomingMessage } from "node:http";
12
12
  import { Readable } from "node:stream";
13
13
  import { resolveHttpAuditUserId, resolveTaskAuditUserId } from "../be/audit-user";
14
- import { closeDb, createAgent, createTaskExtended, createUser, initDb } from "../be/db";
14
+ import { closeDb, createAgent, createTaskExtended, createUser, initDb, startTask } from "../be/db";
15
+ import { type IdentityActor, linkIdentity } from "../be/users";
15
16
  import { setRequestAuth } from "../utils/request-auth-context";
16
17
 
18
+ const SYSTEM_ACTOR: IdentityActor = { kind: "system", id: "test" };
19
+
17
20
  const TEST_DB_PATH = "./test-audit-user.sqlite";
18
21
 
19
22
  let agentId: string;
@@ -95,6 +98,49 @@ describe("resolveTaskAuditUserId", () => {
95
98
  test("returns null when owned task has no human requester", () => {
96
99
  expect(resolveTaskAuditUserId(noRequesterTaskId, agentId)).toBeNull();
97
100
  });
101
+
102
+ test("ambient-task fallback: no sourceTaskId resolves via the caller's current in-progress task", () => {
103
+ const ambientAgent = createAgent({ name: "ambient-agent", isLead: false, status: "idle" });
104
+ const ambientTask = createTaskExtended("ambient task", {
105
+ agentId: ambientAgent.id,
106
+ requestedByUserId: humanUserId,
107
+ });
108
+ startTask(ambientTask.id);
109
+
110
+ expect(resolveTaskAuditUserId(undefined, ambientAgent.id)).toBe(humanUserId);
111
+ });
112
+
113
+ test("ambient-task fallback: no in-progress task for the caller still returns null", () => {
114
+ const idleAgent = createAgent({ name: "idle-agent", isLead: false, status: "idle" });
115
+ expect(resolveTaskAuditUserId(undefined, idleAgent.id)).toBeNull();
116
+ });
117
+
118
+ test("machine-carried external-ID fallback: no requestedByUserId but a linked Slack id resolves the user", () => {
119
+ const slackLinkedUser = createUser({ name: "Slack Fallback User" });
120
+ linkIdentity(slackLinkedUser.id, "slack", "U_AUDIT_FALLBACK", SYSTEM_ACTOR);
121
+
122
+ const fallbackAgent = createAgent({ name: "fallback-agent", isLead: false, status: "idle" });
123
+ const fallbackTask = createTaskExtended("slack-originated task, requester never stamped", {
124
+ agentId: fallbackAgent.id,
125
+ slackUserId: "U_AUDIT_FALLBACK",
126
+ });
127
+
128
+ expect(resolveTaskAuditUserId(fallbackTask.id, fallbackAgent.id)).toBe(slackLinkedUser.id);
129
+ });
130
+
131
+ test("machine-carried external-ID fallback: unlinked Slack id still returns null (no guess)", () => {
132
+ const fallbackAgent = createAgent({
133
+ name: "fallback-agent-unlinked",
134
+ isLead: false,
135
+ status: "idle",
136
+ });
137
+ const fallbackTask = createTaskExtended("slack-originated task, unlinked sender", {
138
+ agentId: fallbackAgent.id,
139
+ slackUserId: "U_NEVER_LINKED_AUDIT",
140
+ });
141
+
142
+ expect(resolveTaskAuditUserId(fallbackTask.id, fallbackAgent.id)).toBeNull();
143
+ });
98
144
  });
99
145
 
100
146
  // ─── resolveHttpAuditUserId ──────────────────────────────────────────────────
@@ -255,8 +255,8 @@ describe("CodexSession event mapping", () => {
255
255
  expect(contextUsage).toBeDefined();
256
256
  if (contextUsage && contextUsage.type === "context_usage") {
257
257
  expect(contextUsage.contextUsedTokens).toBe(150);
258
- expect(contextUsage.contextTotalTokens).toBe(200_000);
259
- expect(contextUsage.contextPercent).toBeCloseTo((150 / 200_000) * 100, 6);
258
+ expect(contextUsage.contextTotalTokens).toBe(1_050_000);
259
+ expect(contextUsage.contextPercent).toBeCloseTo((150 / 1_050_000) * 100, 6);
260
260
  expect(contextUsage.contextFormula).toBe("input-cache-output");
261
261
  }
262
262
 
@@ -282,7 +282,7 @@ describe("CodexSession event mapping", () => {
282
282
  expect(result.sessionId).toBe("thread-abc");
283
283
  });
284
284
 
285
- test("Phase 9: chatty turn clamps contextPercent to 100% under the unified formula", async () => {
285
+ test("Phase 9: chatty turn uses the models.dev context window under the unified formula", async () => {
286
286
  // Phase 9 deliberately swapped Codex's per-adapter peak-proxy formula
287
287
  // (`(input - cached) + output`) for the unified `input + output` formula
288
288
  // shared with every other provider. The trade-off: a chatty Codex turn
@@ -303,7 +303,7 @@ describe("CodexSession event mapping", () => {
303
303
  {
304
304
  type: "turn.completed",
305
305
  usage: {
306
- input_tokens: 357142, // total > 200k window would clamp pre-fix
306
+ input_tokens: 357142, // total exceeded the old hardcoded 200k window
307
307
  cached_input_tokens: 278912, // most of input is cache reuse
308
308
  output_tokens: 2156,
309
309
  },
@@ -320,11 +320,10 @@ describe("CodexSession event mapping", () => {
320
320
  const contextUsage = emitted.find((e) => e.type === "context_usage");
321
321
  expect(contextUsage).toBeDefined();
322
322
  if (contextUsage && contextUsage.type === "context_usage") {
323
- // Phase 9 unified: input + output = 357142 + 2156 = 359298 (above 200k).
323
+ // Phase 9 unified: input + output = 357142 + 2156 = 359298.
324
324
  expect(contextUsage.contextUsedTokens).toBe(359298);
325
- expect(contextUsage.contextTotalTokens).toBe(200_000);
326
- // Above 100% raw clamped to exactly 100.
327
- expect(contextUsage.contextPercent).toBe(100);
325
+ expect(contextUsage.contextTotalTokens).toBe(1_050_000);
326
+ expect(contextUsage.contextPercent).toBeCloseTo((359298 / 1_050_000) * 100, 6);
328
327
  expect(contextUsage.contextFormula).toBe("input-cache-output");
329
328
  }
330
329
 
@@ -438,7 +437,7 @@ describe("CodexSession event mapping", () => {
438
437
  if (errorEvent && errorEvent.type === "error") {
439
438
  expect(errorEvent.message).toContain("[context-overflow]");
440
439
  expect(errorEvent.message).toContain("gpt-5.4");
441
- expect(errorEvent.message).toContain("200,000 tokens");
440
+ expect(errorEvent.message).toContain("1,050,000 tokens");
442
441
  // original error preserved at the end
443
442
  expect(errorEvent.message).toContain("context length exceeded");
444
443
  }
@@ -748,16 +747,16 @@ describe("resolveCodexModel", () => {
748
747
  expect(resolveCodexModel("")).toBe(CODEX_DEFAULT_MODEL);
749
748
  });
750
749
 
751
- test("claude shortname 'opus' → gpt-5.4", () => {
752
- expect(resolveCodexModel("opus")).toBe("gpt-5.4");
750
+ test("claude shortname 'opus' → gpt-5.6-sol", () => {
751
+ expect(resolveCodexModel("opus")).toBe("gpt-5.6-sol");
753
752
  });
754
753
 
755
- test("claude shortname 'sonnet' → gpt-5.4", () => {
756
- expect(resolveCodexModel("sonnet")).toBe("gpt-5.4");
754
+ test("claude shortname 'sonnet' → gpt-5.6-terra", () => {
755
+ expect(resolveCodexModel("sonnet")).toBe("gpt-5.6-terra");
757
756
  });
758
757
 
759
- test("claude shortname 'haiku' → gpt-5.4-mini", () => {
760
- expect(resolveCodexModel("haiku")).toBe("gpt-5.4-mini");
758
+ test("claude shortname 'haiku' → gpt-5.6-luna", () => {
759
+ expect(resolveCodexModel("haiku")).toBe("gpt-5.6-luna");
761
760
  });
762
761
 
763
762
  test("passthrough 'gpt-5.4-mini' → gpt-5.4-mini", () => {
@@ -768,6 +767,12 @@ describe("resolveCodexModel", () => {
768
767
  expect(resolveCodexModel("gpt-5.5")).toBe("gpt-5.5");
769
768
  });
770
769
 
770
+ test("passthrough GPT-5.6 Codex models", () => {
771
+ expect(resolveCodexModel("gpt-5.6-sol")).toBe("gpt-5.6-sol");
772
+ expect(resolveCodexModel("gpt-5.6-terra")).toBe("gpt-5.6-terra");
773
+ expect(resolveCodexModel("gpt-5.6-luna")).toBe("gpt-5.6-luna");
774
+ });
775
+
771
776
  test("passthrough 'gpt-5.3-codex' → gpt-5.3-codex", () => {
772
777
  expect(resolveCodexModel("gpt-5.3-codex")).toBe("gpt-5.3-codex");
773
778
  });
@@ -787,24 +792,30 @@ describe("resolveCodexModel", () => {
787
792
  });
788
793
 
789
794
  describe("getCodexContextWindow", () => {
790
- test("gpt-5.4 → 200_000", () => {
791
- expect(getCodexContextWindow("gpt-5.4")).toBe(200_000);
795
+ test("gpt-5.4 → models.dev context", () => {
796
+ expect(getCodexContextWindow("gpt-5.4")).toBe(1_050_000);
792
797
  });
793
798
 
794
- test("gpt-5.4-mini → 200_000", () => {
795
- expect(getCodexContextWindow("gpt-5.4-mini")).toBe(200_000);
799
+ test("gpt-5.4-mini → models.dev context", () => {
800
+ expect(getCodexContextWindow("gpt-5.4-mini")).toBe(400_000);
796
801
  });
797
802
 
798
803
  test("gpt-5.5 → 1_050_000", () => {
799
804
  expect(getCodexContextWindow("gpt-5.5")).toBe(1_050_000);
800
805
  });
801
806
 
802
- test("gpt-5.3-codex 1_000_000 (1M context)", () => {
803
- expect(getCodexContextWindow("gpt-5.3-codex")).toBe(1_000_000);
807
+ test("GPT-5.6 Codex models 1_050_000", () => {
808
+ expect(getCodexContextWindow("gpt-5.6-sol")).toBe(1_050_000);
809
+ expect(getCodexContextWindow("gpt-5.6-terra")).toBe(1_050_000);
810
+ expect(getCodexContextWindow("gpt-5.6-luna")).toBe(1_050_000);
804
811
  });
805
812
 
806
- test("gpt-5.2-codex → 200_000", () => {
807
- expect(getCodexContextWindow("gpt-5.2-codex")).toBe(200_000);
813
+ test("gpt-5.3-codex → models.dev context", () => {
814
+ expect(getCodexContextWindow("gpt-5.3-codex")).toBe(400_000);
815
+ });
816
+
817
+ test("gpt-5.2-codex → models.dev context", () => {
818
+ expect(getCodexContextWindow("gpt-5.2-codex")).toBe(400_000);
808
819
  });
809
820
  });
810
821
 
@@ -821,6 +832,12 @@ describe("computeCodexCostUsd", () => {
821
832
  expect(cost).toBeCloseTo(35, 4);
822
833
  });
823
834
 
835
+ test("GPT-5.6 Codex model pricing matches OpenAI model docs", () => {
836
+ expect(computeCodexCostUsd("gpt-5.6-sol", 1_000_000, 0, 1_000_000)).toBeCloseTo(35, 4);
837
+ expect(computeCodexCostUsd("gpt-5.6-terra", 1_000_000, 0, 1_000_000)).toBeCloseTo(17.5, 4);
838
+ expect(computeCodexCostUsd("gpt-5.6-luna", 1_000_000, 0, 1_000_000)).toBeCloseTo(7, 4);
839
+ });
840
+
824
841
  test("gpt-5.4 with cached input applies the cached discount", () => {
825
842
  // 1M input, 800k cached → 200k uncached.
826
843
  // 200_000 × $2.50/M = $0.50
@@ -1133,6 +1150,12 @@ describe("buildCodexConfig", () => {
1133
1150
  expect(merged.model_reasoning_effort).toBe("xhigh");
1134
1151
  });
1135
1152
 
1153
+ test("reasoningEffort: 'max' on gpt-5.6-sol is applied", async () => {
1154
+ globalThis.fetch = stubFetch({ servers: [] });
1155
+ const merged = await buildCodexConfig(cfg({ reasoningEffort: "max" }), "gpt-5.6-sol", () => {});
1156
+ expect(merged.model_reasoning_effort).toBe("max");
1157
+ });
1158
+
1136
1159
  test("reasoningEffort: 'xhigh' on gpt-5.1-codex (non-max) is rejected — noop, no model_reasoning_effort key", async () => {
1137
1160
  globalThis.fetch = stubFetch({ servers: [] });
1138
1161
  const merged = await buildCodexConfig(
@@ -85,6 +85,7 @@ describe("credential broker", () => {
85
85
  scope: "global",
86
86
  active: true,
87
87
  scopeId: null,
88
+ authKind: "config",
88
89
  },
89
90
  {
90
91
  configKey: "QUERY_KEY",
@@ -93,6 +94,7 @@ describe("credential broker", () => {
93
94
  scope: "global",
94
95
  active: true,
95
96
  scopeId: null,
97
+ authKind: "config",
96
98
  },
97
99
  ]);
98
100
  });
@@ -133,13 +135,14 @@ describe("credential broker", () => {
133
135
  scope: "global",
134
136
  scopeId: null,
135
137
  active: true,
138
+ authKind: "config",
136
139
  placeholder: "[REDACTED:GITHUB_TOKEN]",
137
140
  value: "ghp_test",
138
141
  },
139
142
  ]);
140
143
  });
141
144
 
142
- test("registers resolved broker config values with the scrubber", () => {
145
+ test("registers resolved broker config values with the scrubber", async () => {
143
146
  const bindingsConfig = upsertSwarmConfig({
144
147
  scope: "global",
145
148
  key: CREDENTIAL_BINDINGS_CONFIG_KEY,
@@ -161,7 +164,7 @@ describe("credential broker", () => {
161
164
  });
162
165
 
163
166
  try {
164
- const bindings = buildScriptCredentialBindings({});
167
+ const bindings = await buildScriptCredentialBindings({});
165
168
 
166
169
  expect(bindings.some((binding) => binding.configKey === "VENDOR_SPECIAL_API_KEY")).toBe(true);
167
170
  expect(scrubSecrets("echo not_a_standard_token_shape_12345")).toBe(