@cohortapp/agent-sdk 2.17.0 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (526) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture.mjs +8 -6
  23. package/lib/collective/global-config.mjs +63 -1
  24. package/lib/collective/presence.mjs +142 -5
  25. package/lib/comms/send-gate.mjs +559 -1
  26. package/lib/diagnostics/alerts.mjs +49 -0
  27. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  28. package/lib/engine/agents/definitions.mjs +343 -0
  29. package/lib/engine/agents/persist.mjs +275 -0
  30. package/lib/engine/agents/runtime.mjs +748 -0
  31. package/lib/engine/agents/usage.mjs +95 -0
  32. package/lib/engine/auth-status.mjs +139 -0
  33. package/lib/engine/budget.mjs +194 -0
  34. package/lib/engine/cli.mjs +1204 -0
  35. package/lib/engine/commands/index.mjs +269 -0
  36. package/lib/engine/context/budget.mjs +219 -0
  37. package/lib/engine/context/cache.mjs +125 -0
  38. package/lib/engine/context/child-env.mjs +215 -0
  39. package/lib/engine/context/compaction.mjs +342 -0
  40. package/lib/engine/context/images.mjs +90 -0
  41. package/lib/engine/context/instructions.mjs +327 -0
  42. package/lib/engine/context/lazy-instructions.mjs +169 -0
  43. package/lib/engine/context/manager.mjs +182 -0
  44. package/lib/engine/context/real-path.mjs +91 -0
  45. package/lib/engine/context/secret-values.mjs +163 -0
  46. package/lib/engine/context/settings.mjs +274 -0
  47. package/lib/engine/context/stream-input.mjs +159 -0
  48. package/lib/engine/guard.mjs +152 -0
  49. package/lib/engine/hooks.mjs +713 -0
  50. package/lib/engine/loop.mjs +560 -0
  51. package/lib/engine/mcp/client.mjs +254 -0
  52. package/lib/engine/mcp/config.mjs +301 -0
  53. package/lib/engine/mcp/http.mjs +201 -0
  54. package/lib/engine/mcp/index.mjs +146 -0
  55. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  56. package/lib/engine/mcp/naming.mjs +66 -0
  57. package/lib/engine/mcp/resources.mjs +89 -0
  58. package/lib/engine/mcp/results.mjs +133 -0
  59. package/lib/engine/mcp/stdio.mjs +137 -0
  60. package/lib/engine/mcp/supervisor.mjs +116 -0
  61. package/lib/engine/messages.mjs +104 -0
  62. package/lib/engine/output/json.mjs +164 -0
  63. package/lib/engine/output/stream-json.mjs +266 -0
  64. package/lib/engine/permissions.mjs +845 -0
  65. package/lib/engine/process-identity.mjs +164 -0
  66. package/lib/engine/process-tree.mjs +551 -0
  67. package/lib/engine/prompt.mjs +60 -0
  68. package/lib/engine/session/store.mjs +299 -0
  69. package/lib/engine/session-runtime/args.mjs +97 -0
  70. package/lib/engine/session-runtime/host.mjs +143 -0
  71. package/lib/engine/session-runtime/inbox.mjs +122 -0
  72. package/lib/engine/session-runtime/notifications.mjs +129 -0
  73. package/lib/engine/session-runtime/registry.mjs +328 -0
  74. package/lib/engine/session-runtime/runner.mjs +344 -0
  75. package/lib/engine/session-runtime/socket.mjs +212 -0
  76. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  77. package/lib/engine/skills/index.mjs +321 -0
  78. package/lib/engine/tools/bash-background.mjs +533 -0
  79. package/lib/engine/tools/bash.mjs +216 -0
  80. package/lib/engine/tools/edit.mjs +97 -0
  81. package/lib/engine/tools/glob.mjs +81 -0
  82. package/lib/engine/tools/grep.mjs +224 -0
  83. package/lib/engine/tools/index.mjs +84 -0
  84. package/lib/engine/tools/list-agents.mjs +32 -0
  85. package/lib/engine/tools/ls.mjs +127 -0
  86. package/lib/engine/tools/monitor.mjs +82 -0
  87. package/lib/engine/tools/notebook-edit.mjs +218 -0
  88. package/lib/engine/tools/read.mjs +103 -0
  89. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  90. package/lib/engine/tools/schema.mjs +144 -0
  91. package/lib/engine/tools/send-message.mjs +77 -0
  92. package/lib/engine/tools/session.mjs +70 -0
  93. package/lib/engine/tools/todo.mjs +144 -0
  94. package/lib/engine/tools/toolsearch.mjs +217 -0
  95. package/lib/engine/tools/walk.mjs +193 -0
  96. package/lib/engine/tools/web-switch.mjs +31 -0
  97. package/lib/engine/tools/webfetch-html.mjs +387 -0
  98. package/lib/engine/tools/webfetch-net.mjs +340 -0
  99. package/lib/engine/tools/webfetch.mjs +198 -0
  100. package/lib/engine/tools/websearch.mjs +91 -0
  101. package/lib/engine/tools/workflow.mjs +95 -0
  102. package/lib/engine/tools/write.mjs +76 -0
  103. package/lib/engine/tui/line-editor.mjs +137 -0
  104. package/lib/engine/tui/render.mjs +86 -0
  105. package/lib/engine/tui/tui.mjs +274 -0
  106. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  107. package/lib/engine/wire/effort.mjs +36 -0
  108. package/lib/engine/wire/errors.mjs +496 -0
  109. package/lib/engine/wire/http.mjs +441 -0
  110. package/lib/engine/wire/index.mjs +76 -0
  111. package/lib/engine/wire/openai-chat.mjs +332 -0
  112. package/lib/engine/wire/prompt-cache.mjs +79 -0
  113. package/lib/engine/wire/search.mjs +140 -0
  114. package/lib/engine/wire/sse.mjs +114 -0
  115. package/lib/engine/wire/stall.mjs +349 -0
  116. package/lib/engine/wire/token-provider.mjs +175 -0
  117. package/lib/engine/wire/usage.mjs +192 -0
  118. package/lib/engine/workflow/host.mjs +524 -0
  119. package/lib/engine/workflow/journal.mjs +188 -0
  120. package/lib/engine/workflow/json-schema.mjs +171 -0
  121. package/lib/engine/workflow/meta.mjs +329 -0
  122. package/lib/engine/workflow/notifications.mjs +52 -0
  123. package/lib/engine/workflow/runtime.mjs +447 -0
  124. package/lib/engine/workflow/sandbox.mjs +534 -0
  125. package/lib/engine/workflow/worker.mjs +141 -0
  126. package/lib/engine/workflow/worktree.mjs +74 -0
  127. package/lib/execution/disposition.mjs +1 -1
  128. package/lib/execution/intake.mjs +10 -0
  129. package/lib/execution/surface-policy.mjs +15 -0
  130. package/lib/learning/curator.mjs +8 -6
  131. package/lib/learning/reflect.mjs +8 -6
  132. package/lib/model-router/catalog/cohort.yaml +137 -0
  133. package/lib/model-router/catalog.mjs +118 -1
  134. package/lib/model-router/failover.mjs +67 -16
  135. package/lib/model-router/llm-task.mjs +39 -3
  136. package/lib/model-router/resolve.mjs +89 -3
  137. package/lib/model-router/spawn.mjs +46 -47
  138. package/lib/model-router/taxonomy.mjs +126 -4
  139. package/lib/org/cost-sync.mjs +141 -11
  140. package/lib/org/inbound/broadcast.mjs +289 -0
  141. package/lib/org/inbound/collective.mjs +375 -0
  142. package/lib/org/inbound/directedness.mjs +96 -8
  143. package/lib/org/inbound/facts.mjs +78 -2
  144. package/lib/org/inbound/project.mjs +22 -0
  145. package/lib/org/inbound/surfaces.mjs +14 -0
  146. package/lib/org/llm-token.mjs +879 -0
  147. package/lib/org/mesh.mjs +61 -0
  148. package/lib/org/messaging.mjs +3 -1
  149. package/lib/org/protocol.checksum +1 -1
  150. package/lib/org/protocol.mjs +15 -0
  151. package/lib/org/quota.mjs +520 -0
  152. package/lib/org/tool-surface.mjs +104 -16
  153. package/lib/org/ui-parity.mjs +16 -1
  154. package/lib/org/work-ledger.mjs +37 -6
  155. package/lib/rate-guard.mjs +114 -1
  156. package/lib/resource-governor.mjs +41 -6
  157. package/lib/runtime/adapter.mjs +823 -0
  158. package/lib/runtime/child-env.mjs +191 -0
  159. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  160. package/lib/runtime/seat-engine.mjs +162 -0
  161. package/lib/session/ask-ledger.mjs +271 -0
  162. package/lib/session/current-work.mjs +676 -0
  163. package/lib/session/feed-core.mjs +40 -3
  164. package/lib/session/launch-args.mjs +56 -4
  165. package/lib/session/status-summary.mjs +26 -9
  166. package/lib/session/upgrade-notice.mjs +42 -0
  167. package/lib/setup/claude-probe.mjs +117 -13
  168. package/lib/setup/enrich.mjs +13 -10
  169. package/lib/setup/sections/model.mjs +39 -13
  170. package/lib/telemetry/collect.mjs +208 -9
  171. package/lib/upgrade/ignored-drift.mjs +105 -0
  172. package/lib/voice/post-call-brief.mjs +30 -17
  173. package/package.json +13 -3
  174. package/plugins/maestro-skills/skills/board-work.md +5 -0
  175. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  176. package/plugins/maestro-skills/skills/main-session.md +18 -7
  177. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  178. package/scripts/ci/run-tests.mjs +47 -19
  179. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  180. package/scripts/collective/hook-runner.mjs +29 -2
  181. package/scripts/continuous-monitor.sh +13 -0
  182. package/scripts/cost/track-claude-usage.mjs +15 -0
  183. package/scripts/daemon/agent-daemon.mjs +408 -20
  184. package/scripts/daemon/assurance.mjs +48 -12
  185. package/scripts/daemon/cadence-consumer.mjs +218 -68
  186. package/scripts/daemon/cadence-handlers.mjs +73 -4
  187. package/scripts/daemon/classifier.mjs +75 -26
  188. package/scripts/daemon/context-compiler.mjs +51 -37
  189. package/scripts/daemon/deliver.mjs +30 -1
  190. package/scripts/daemon/dispatcher.mjs +595 -149
  191. package/scripts/daemon/health.mjs +14 -1
  192. package/scripts/daemon/maestro-daemon.mjs +11 -0
  193. package/scripts/daemon/prompt-builder.mjs +24 -0
  194. package/scripts/daemon/responder.mjs +246 -79
  195. package/scripts/daemon/sdk-version.mjs +98 -16
  196. package/scripts/eval/probe-gateway.mjs +635 -0
  197. package/scripts/eval/replay/extract.mjs +270 -0
  198. package/scripts/eval/replay/grade.mjs +260 -0
  199. package/scripts/eval/replay/lib/config.mjs +50 -0
  200. package/scripts/eval/replay/lib/effects.mjs +65 -0
  201. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  202. package/scripts/eval/replay/lib/judge.mjs +72 -0
  203. package/scripts/eval/replay/lib/redact.mjs +136 -0
  204. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  205. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  206. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  207. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  208. package/scripts/eval/replay/report.mjs +185 -0
  209. package/scripts/eval/replay/run.mjs +404 -0
  210. package/scripts/fleet/rollout.mjs +1094 -0
  211. package/scripts/hooks/pre-send-audit.sh +36 -245
  212. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  213. package/scripts/hooks/validate-state-yaml.sh +190 -0
  214. package/scripts/huddle/huddle-llm.mjs +361 -0
  215. package/scripts/huddle/huddle-server.mjs +46 -121
  216. package/scripts/local-triggers/autoupdate.sh +448 -78
  217. package/scripts/local-triggers/run-trigger.sh +13 -0
  218. package/scripts/maintenance/pin-integrity.mjs +364 -0
  219. package/scripts/poll-slack-events.sh +41 -9
  220. package/scripts/poller/slack-socket-mode.mjs +28 -3
  221. package/scripts/session/supervisor.mjs +80 -13
  222. package/scripts/spawn-session.sh +13 -0
  223. package/bin/maestro.test.mjs +0 -1574
  224. package/lib/action-executor.test.mjs +0 -871
  225. package/lib/archetype.test.mjs +0 -132
  226. package/lib/assurance/plan-note.test.mjs +0 -234
  227. package/lib/assurance/room-budget.test.mjs +0 -486
  228. package/lib/assurance/tier.test.mjs +0 -174
  229. package/lib/autonomy.test.mjs +0 -66
  230. package/lib/backlog.test.mjs +0 -302
  231. package/lib/backup/policy.test.mjs +0 -305
  232. package/lib/budget-escalate.test.mjs +0 -232
  233. package/lib/budget-guard.envelope.test.mjs +0 -476
  234. package/lib/budget-guard.test.mjs +0 -427
  235. package/lib/cadence-bus-requeue.test.mjs +0 -83
  236. package/lib/cadence-bus-schedule.test.mjs +0 -194
  237. package/lib/cadence-bus.test.mjs +0 -720
  238. package/lib/cadences.test.mjs +0 -230
  239. package/lib/capability/inventory.test.mjs +0 -232
  240. package/lib/capability.test.mjs +0 -78
  241. package/lib/channels/base-adapter.test.mjs +0 -590
  242. package/lib/channels/channels.test.mjs +0 -371
  243. package/lib/channels/contract.test.mjs +0 -162
  244. package/lib/channels/inbox-item.test.mjs +0 -368
  245. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  246. package/lib/channels/pairing.test.mjs +0 -270
  247. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  248. package/lib/channels/slack-adapter.test.mjs +0 -212
  249. package/lib/channels/telegram-adapter.test.mjs +0 -306
  250. package/lib/channels/voice/adapter.test.mjs +0 -278
  251. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  252. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  253. package/lib/charter.test.mjs +0 -89
  254. package/lib/claude-bin.test.mjs +0 -131
  255. package/lib/cli/board.test.mjs +0 -227
  256. package/lib/cli/design.test.mjs +0 -270
  257. package/lib/cli/doctor-checks.test.mjs +0 -336
  258. package/lib/cli/global-setup-extras.test.mjs +0 -462
  259. package/lib/cli/inbox.test.mjs +0 -230
  260. package/lib/cli/session-ack.test.mjs +0 -63
  261. package/lib/cli/session.test.mjs +0 -613
  262. package/lib/collective/capture.test.mjs +0 -121
  263. package/lib/collective/cards.test.mjs +0 -114
  264. package/lib/collective/config.test.mjs +0 -123
  265. package/lib/collective/global-config.test.mjs +0 -220
  266. package/lib/collective/global-skills.test.mjs +0 -126
  267. package/lib/collective/presence.test.mjs +0 -95
  268. package/lib/collective/recall.test.mjs +0 -116
  269. package/lib/collective/vendor-skills.test.mjs +0 -306
  270. package/lib/comms/send-gate.test.mjs +0 -770
  271. package/lib/comms.test.mjs +0 -41
  272. package/lib/context/budget.test.mjs +0 -252
  273. package/lib/context/history-scope.test.mjs +0 -79
  274. package/lib/cost/ledger-row.test.mjs +0 -183
  275. package/lib/design/design-md.test.mjs +0 -318
  276. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  277. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  278. package/lib/design/fixtures/foundation.json +0 -133
  279. package/lib/design/refresh-gate.test.mjs +0 -144
  280. package/lib/design/write.test.mjs +0 -241
  281. package/lib/diagnostics/alerts.test.mjs +0 -318
  282. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  283. package/lib/diagnostics/counters.test.mjs +0 -206
  284. package/lib/diagnostics/events.test.mjs +0 -290
  285. package/lib/diagnostics/otel.test.mjs +0 -196
  286. package/lib/diagnostics/trace.test.mjs +0 -251
  287. package/lib/env-compat.test.mjs +0 -104
  288. package/lib/execution/disposition.test.mjs +0 -553
  289. package/lib/execution/drive.test.mjs +0 -270
  290. package/lib/execution/effects.test.mjs +0 -344
  291. package/lib/execution/intake.test.mjs +0 -389
  292. package/lib/execution/journal.test.mjs +0 -261
  293. package/lib/execution/match.test.mjs +0 -235
  294. package/lib/execution/pipeline.test.mjs +0 -392
  295. package/lib/execution/route.test.mjs +0 -186
  296. package/lib/execution/surface-policy.test.mjs +0 -162
  297. package/lib/fs-atomic.test.mjs +0 -72
  298. package/lib/fs-ownership.test.mjs +0 -158
  299. package/lib/goals/admission.test.mjs +0 -164
  300. package/lib/goals/classify.test.mjs +0 -167
  301. package/lib/goals/collaborate.test.mjs +0 -336
  302. package/lib/goals/gaps.test.mjs +0 -284
  303. package/lib/goals/loop.test.mjs +0 -845
  304. package/lib/hooks/bus.test.mjs +0 -387
  305. package/lib/identity/persona.test.mjs +0 -142
  306. package/lib/kpi-sensors.test.mjs +0 -278
  307. package/lib/kpi.test.mjs +0 -244
  308. package/lib/learning/config.test.mjs +0 -75
  309. package/lib/learning/counters.test.mjs +0 -69
  310. package/lib/learning/curator-consolidate.test.mjs +0 -238
  311. package/lib/learning/curator.test.mjs +0 -106
  312. package/lib/learning/reflect.test.mjs +0 -0
  313. package/lib/learning/session-index.test.mjs +0 -125
  314. package/lib/learning/skill-writer.test.mjs +0 -210
  315. package/lib/mandate/audit.test.mjs +0 -195
  316. package/lib/mandate/contract.test.mjs +0 -185
  317. package/lib/mandate/derive.test.mjs +0 -274
  318. package/lib/mandate/model.test.mjs +0 -164
  319. package/lib/mandate/refresh.test.mjs +0 -389
  320. package/lib/mcp/server.test.mjs +0 -426
  321. package/lib/model-router/auth-profiles.test.mjs +0 -580
  322. package/lib/model-router/catalog.test.mjs +0 -385
  323. package/lib/model-router/economics.test.mjs +0 -438
  324. package/lib/model-router/failover.test.mjs +0 -439
  325. package/lib/model-router/health.test.mjs +0 -338
  326. package/lib/model-router/integration-coverage.test.mjs +0 -831
  327. package/lib/model-router/integration.test.mjs +0 -564
  328. package/lib/model-router/ledger.test.mjs +0 -415
  329. package/lib/model-router/llm-task.test.mjs +0 -392
  330. package/lib/model-router/org-credentials.test.mjs +0 -265
  331. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  332. package/lib/model-router/reconcile.test.mjs +0 -316
  333. package/lib/model-router/repair.test.mjs +0 -180
  334. package/lib/model-router/spawn.test.mjs +0 -446
  335. package/lib/model-router/taxonomy.test.mjs +0 -410
  336. package/lib/model-router.test.mjs +0 -1207
  337. package/lib/org/activity.test.mjs +0 -134
  338. package/lib/org/approvals.test.mjs +0 -216
  339. package/lib/org/awareness.test.mjs +0 -159
  340. package/lib/org/board-mine-cache.test.mjs +0 -53
  341. package/lib/org/board.test.mjs +0 -187
  342. package/lib/org/bootstrap-context.test.mjs +0 -153
  343. package/lib/org/client.test.mjs +0 -1206
  344. package/lib/org/cohort-client.test.mjs +0 -126
  345. package/lib/org/cost-sync.test.mjs +0 -153
  346. package/lib/org/doctor.test.mjs +0 -346
  347. package/lib/org/engagement-ledger.test.mjs +0 -112
  348. package/lib/org/engagement.test.mjs +0 -739
  349. package/lib/org/handoff.test.mjs +0 -269
  350. package/lib/org/inbound/directedness.test.mjs +0 -668
  351. package/lib/org/inbound/facts.test.mjs +0 -471
  352. package/lib/org/inbound/hydrate.test.mjs +0 -908
  353. package/lib/org/inbound/index.test.mjs +0 -429
  354. package/lib/org/inbound/project.test.mjs +0 -287
  355. package/lib/org/integration-tools.test.mjs +0 -160
  356. package/lib/org/keys.test.mjs +0 -92
  357. package/lib/org/knowledge.test.mjs +0 -326
  358. package/lib/org/leases.test.mjs +0 -235
  359. package/lib/org/mesh-directives.test.mjs +0 -110
  360. package/lib/org/mesh-integration.test.mjs +0 -127
  361. package/lib/org/mesh.test.mjs +0 -400
  362. package/lib/org/messaging.test.mjs +0 -471
  363. package/lib/org/param-contract.test.mjs +0 -477
  364. package/lib/org/policy.test.mjs +0 -237
  365. package/lib/org/protocol.checksum.test.mjs +0 -90
  366. package/lib/org/protocol.test.mjs +0 -323
  367. package/lib/org/push.test.mjs +0 -792
  368. package/lib/org/registry.test.mjs +0 -100
  369. package/lib/org/resource-tools.test.mjs +0 -361
  370. package/lib/org/tool-access.test.mjs +0 -144
  371. package/lib/org/tool-surface-integration.test.mjs +0 -120
  372. package/lib/org/tool-surface.test.mjs +0 -1268
  373. package/lib/org/typing.test.mjs +0 -291
  374. package/lib/org/ui-parity.test.mjs +0 -560
  375. package/lib/org/verify.test.mjs +0 -194
  376. package/lib/org/work-ledger.test.mjs +0 -273
  377. package/lib/plan/adoption-e2e.test.mjs +0 -366
  378. package/lib/plan/budget-enforcement.test.mjs +0 -400
  379. package/lib/plan/compile.test.mjs +0 -382
  380. package/lib/plan/emit.test.mjs +0 -269
  381. package/lib/plan/explain.test.mjs +0 -188
  382. package/lib/prompts/parallelism.test.mjs +0 -177
  383. package/lib/rag/rag.test.mjs +0 -505
  384. package/lib/rate-guard.test.mjs +0 -272
  385. package/lib/reactive-gate.test.mjs +0 -57
  386. package/lib/render.test.mjs +0 -68
  387. package/lib/resource-governor.test.mjs +0 -488
  388. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  389. package/lib/scheduling/jitter.test.mjs +0 -140
  390. package/lib/secrets/broker.test.mjs +0 -280
  391. package/lib/secrets/providers.test.mjs +0 -274
  392. package/lib/security/audit-engine.test.mjs +0 -424
  393. package/lib/security/coerce-args.test.mjs +0 -281
  394. package/lib/security/dangerous-tools.test.mjs +0 -68
  395. package/lib/security/external-content.test.mjs +0 -84
  396. package/lib/security/redact.test.mjs +0 -441
  397. package/lib/security/secret-equal.test.mjs +0 -55
  398. package/lib/session/config.test.mjs +0 -92
  399. package/lib/session/feed-core.test.mjs +0 -198
  400. package/lib/session/first-run.test.mjs +0 -121
  401. package/lib/session/frontdoor.test.mjs +0 -205
  402. package/lib/session/handoffs.test.mjs +0 -183
  403. package/lib/session/identity.test.mjs +0 -180
  404. package/lib/session/inbox-claims.test.mjs +0 -286
  405. package/lib/session/launch-args.test.mjs +0 -157
  406. package/lib/session/liveness.test.mjs +0 -100
  407. package/lib/session/status-summary.test.mjs +0 -118
  408. package/lib/session-permissions.test.mjs +0 -120
  409. package/lib/setup/claude-probe.test.mjs +0 -187
  410. package/lib/setup/completeness.test.mjs +0 -110
  411. package/lib/setup/context-pack.test.mjs +0 -89
  412. package/lib/setup/enrich.test.mjs +0 -115
  413. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  414. package/lib/setup/integration.test.mjs +0 -162
  415. package/lib/setup/io.test.mjs +0 -77
  416. package/lib/setup/runner.test.mjs +0 -132
  417. package/lib/setup/sections/identity.test.mjs +0 -234
  418. package/lib/setup/sections/inventory.test.mjs +0 -198
  419. package/lib/setup/sections/learning.test.mjs +0 -81
  420. package/lib/setup/sections/mandate.test.mjs +0 -388
  421. package/lib/setup/sections/messaging.test.mjs +0 -127
  422. package/lib/setup/sections/model.test.mjs +0 -240
  423. package/lib/setup/sections/org.test.mjs +0 -346
  424. package/lib/setup/sections/orgmail.test.mjs +0 -118
  425. package/lib/setup/sections/recovery.test.mjs +0 -98
  426. package/lib/setup/sections/subagents.test.mjs +0 -429
  427. package/lib/setup/sections/verify.test.mjs +0 -175
  428. package/lib/setup/sot.test.mjs +0 -81
  429. package/lib/setup/state.test.mjs +0 -115
  430. package/lib/singleton.test.mjs +0 -151
  431. package/lib/subagents/cli.test.mjs +0 -389
  432. package/lib/subagents/client.test.mjs +0 -309
  433. package/lib/subagents/gap.test.mjs +0 -234
  434. package/lib/subagents/lock.test.mjs +0 -248
  435. package/lib/subagents/manifest.test.mjs +0 -175
  436. package/lib/subagents/refs.test.mjs +0 -204
  437. package/lib/subagents/resolve.test.mjs +0 -422
  438. package/lib/subagents/schema.test.mjs +0 -328
  439. package/lib/telemetry/alerts.test.mjs +0 -109
  440. package/lib/telemetry/collect.test.mjs +0 -1274
  441. package/lib/tool-definitions-integration.test.mjs +0 -83
  442. package/lib/tool-definitions.test.mjs +0 -437
  443. package/lib/upgrade/global-refresh.test.mjs +0 -65
  444. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  445. package/lib/upgrade/post-steps.test.mjs +0 -200
  446. package/lib/upgrade/verify.test.mjs +0 -164
  447. package/lib/util/fetch-timeout.test.mjs +0 -202
  448. package/lib/util/reconnect.test.mjs +0 -369
  449. package/lib/util/unhandled.test.mjs +0 -216
  450. package/lib/voice/outbound.test.mjs +0 -69
  451. package/lib/voice/session-rotation.test.mjs +0 -114
  452. package/lib/voice/stt.test.mjs +0 -226
  453. package/lib/voice/voice.test.mjs +0 -990
  454. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  455. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  456. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  457. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  458. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  459. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  460. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  461. package/scripts/ci/check.test.mjs +0 -194
  462. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  463. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  464. package/scripts/collective/hook-runner.test.mjs +0 -173
  465. package/scripts/cost/fleet-digest.test.mjs +0 -207
  466. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  467. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  468. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  469. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  470. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  471. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  472. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  473. package/scripts/daemon/assurance.test.mjs +0 -1791
  474. package/scripts/daemon/board-mirror.test.mjs +0 -165
  475. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  476. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  477. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  478. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  479. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  480. package/scripts/daemon/classifier.test.mjs +0 -266
  481. package/scripts/daemon/classify-kind.test.mjs +0 -40
  482. package/scripts/daemon/context-compiler.test.mjs +0 -406
  483. package/scripts/daemon/deliver.test.mjs +0 -564
  484. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  485. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  486. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  487. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  488. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  489. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  490. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  491. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  492. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  493. package/scripts/daemon/integration.test.mjs +0 -149
  494. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  495. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  496. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  497. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  498. package/scripts/daemon/responder-cost.test.mjs +0 -68
  499. package/scripts/daemon/responder-history.test.mjs +0 -221
  500. package/scripts/daemon/sdk-version.test.mjs +0 -31
  501. package/scripts/daemon/session-lock.test.mjs +0 -252
  502. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  503. package/scripts/daemon/typing-registry.test.mjs +0 -102
  504. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  505. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  506. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  507. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  508. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  509. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  510. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  511. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  512. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  513. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  514. package/scripts/poller-launchd/install.test.mjs +0 -243
  515. package/scripts/restore-from-backup.test.mjs +0 -181
  516. package/scripts/session/feed.test.mjs +0 -196
  517. package/scripts/session/supervisor-sh.test.mjs +0 -218
  518. package/scripts/session/supervisor.test.mjs +0 -482
  519. package/scripts/setup/configure-macos.test.mjs +0 -306
  520. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  521. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  522. package/scripts/setup/generate-capability.test.mjs +0 -134
  523. package/scripts/setup/init-agent.test.mjs +0 -370
  524. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  525. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  526. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -30,17 +30,37 @@ let governor = resourceGovernor;
30
30
  let rateGuard = rateGuardModule;
31
31
  let budgetGuard = budgetGuardModule;
32
32
  const RATE_PROVIDER = process.env.MAESTRO_RATE_PROVIDER || "anthropic";
33
+ // cohort-llm clients (seat token + quota). `undefined` = resolve lazily from the
34
+ // seat's config; `null` = none. Injectable for tests.
35
+ let cohortClients = undefined;
33
36
 
34
37
  /**
35
38
  * Test seam: swap the governance modules for fakes. Each arg is optional;
36
39
  * omitted modules keep the real implementation. Returns a restore fn.
40
+ * `cohort` is `{quota, token}` (lib/org/llm-token.mjs seatCohortClients shape).
37
41
  */
38
- export function setGovernanceForTests({ governor: g, rateGuard: r, budgetGuard: b } = {}) {
39
- const prev = { governor, rateGuard, budgetGuard };
42
+ export function setGovernanceForTests({ governor: g, rateGuard: r, budgetGuard: b, cohort: c } = {}) {
43
+ const prev = { governor, rateGuard, budgetGuard, cohortClients };
40
44
  if (g) governor = g;
41
45
  if (r) rateGuard = r;
42
46
  if (b) budgetGuard = b;
43
- return () => { governor = prev.governor; rateGuard = prev.rateGuard; budgetGuard = prev.budgetGuard; };
47
+ if (c !== undefined) cohortClients = c;
48
+ return () => { governor = prev.governor; rateGuard = prev.rateGuard; budgetGuard = prev.budgetGuard; cohortClients = prev.cohortClients; };
49
+ }
50
+
51
+ /** The seat's cohort-llm clients, or null when it has none. Never throws. */
52
+ function getCohortClients() {
53
+ if (cohortClients === undefined) {
54
+ try {
55
+ cohortClients = seatCohortClients({
56
+ agentRoot: AGENT_REPO_DIR,
57
+ log: (level, msg) => { try { console[level === "error" ? "error" : "warn"](`[dispatcher] cohort-llm: ${msg}`); } catch { /* */ } },
58
+ });
59
+ } catch {
60
+ cohortClients = null;
61
+ }
62
+ }
63
+ return cohortClients;
44
64
  }
45
65
 
46
66
  // Indirection so a test can capture the EXACT argv handed to `claude` by the
@@ -99,6 +119,10 @@ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.ur
99
119
  // Resolve the claude binary against the agent's PATH (not launchd's bare
100
120
  // env). Without this, every daemon-spawned `claude --print` exits ENOENT.
101
121
  import { resolveClaudeBin, augmentedPath, daemonClaudeArgs } from "../../lib/claude-bin.mjs";
122
+ import { buildSpawn, admitSpawnCached, isCohortRouted, readSeatEngine } from "../../lib/runtime/adapter.mjs";
123
+ import { seatEngineFields, engineRunShape, sessionBudgetUsd, seatTokenSync } from "../../lib/runtime/seat-engine.mjs";
124
+ import { seatCohortClients } from "../../lib/org/llm-token.mjs";
125
+ import { COHORT_LLM_BILLED_BY } from "../../lib/org/cost-sync.mjs";
102
126
  const CLAUDE_BIN = resolveClaudeBin();
103
127
 
104
128
  // Model router — opt-in. When config/model-routing.yaml is present in the
@@ -112,10 +136,11 @@ const CLAUDE_BIN = resolveClaudeBin();
112
136
  // - v2 (schema_version: 2): resolveChain → RouteDecision. The decision carries
113
137
  // the chosen backend, the session-retarget env, the spawn knobs, the
114
138
  // ordered failover chain, an estimated cost, and a decision_id that joins
115
- // the ledger row ↔ resume marker ↔ routing audit. We build the argv + child
116
- // env from the decision via the execution layer's pure builders
117
- // (buildSpawnArgs / buildChildEnv from lib/model-router/spawn.mjs) so the
118
- // §7.3 child-env scrubbing + argv contract match spawnRouted exactly.
139
+ // the ledger row ↔ resume marker ↔ routing audit. The argv + child env are
140
+ // built from the decision by the runtime adapter (lib/runtime/adapter.mjs),
141
+ // whose retarget env goes through the same runtime env builder
142
+ // (lib/runtime/child-env.mjs) that spawnRouted uses, so the credential scrub
143
+ // matches spawnRouted exactly.
119
144
  //
120
145
  // NOTE on spawnRouted: the daemon's spawnSession() must register the child
121
146
  // process SYNCHRONOUSLY (tests + getStatus() observe active_sessions right after
@@ -135,7 +160,6 @@ import {
135
160
  modelFlagFor,
136
161
  resolveChain,
137
162
  } from "../../lib/model-router.mjs";
138
- import { buildSpawnArgs, buildChildEnv } from "../../lib/model-router/spawn.mjs";
139
163
  import { budgetLadder, spawnKnobsFor } from "../../lib/model-router/economics.mjs";
140
164
  // Observability spine (WS — diagnostics). The dispatcher emits `dispatched` at
141
165
  // spawn and `session_opened`/`session_closed` from the existing close hook,
@@ -151,6 +175,7 @@ import {
151
175
  releaseSession,
152
176
  } from "./lib/session-router.mjs";
153
177
  import { replyTier } from "../../lib/assurance/tier.mjs";
178
+ import { activeEntryFor, linkBoardTask } from "../../lib/session/current-work.mjs";
154
179
 
155
180
  /**
156
181
  * The reply tier comes from `lib/assurance/tier.mjs` (design §5.1) — the ONE
@@ -364,14 +389,15 @@ export function parseUsageFromText(text) {
364
389
  }
365
390
  }
366
391
  if (!obj || typeof obj !== "object") return { ok: false, reason: "no-json-object" };
392
+ const cohortMeta = cohortResultMeta(obj);
367
393
  const usage = obj.usage && typeof obj.usage === "object" ? obj.usage : null;
368
- if (!usage) return { ok: false, reason: "no-usage-field" };
394
+ if (!usage) return { ok: false, reason: "no-usage-field", ...cohortMeta };
369
395
  const inputTokens = Number(usage.input_tokens);
370
396
  const outputTokens = Number(usage.output_tokens);
371
397
  if (!Number.isFinite(inputTokens) || !Number.isFinite(outputTokens)) {
372
- return { ok: false, reason: "non-numeric-tokens" };
398
+ return { ok: false, reason: "non-numeric-tokens", ...cohortMeta };
373
399
  }
374
- const out = { ok: true, inputTokens, outputTokens };
400
+ const out = { ok: true, inputTokens, outputTokens, ...cohortMeta };
375
401
  const cacheRead = Number(usage.cache_read_input_tokens);
376
402
  if (Number.isFinite(cacheRead)) out.cacheReadTokens = cacheRead;
377
403
  // Cache CREATION tokens bill at 1.25x input and were previously dropped on
@@ -380,14 +406,35 @@ export function parseUsageFromText(text) {
380
406
  // the CLI's total_cost_usd.
381
407
  const cacheWrite = Number(usage.cache_creation_input_tokens);
382
408
  if (Number.isFinite(cacheWrite)) out.cacheWriteTokens = cacheWrite;
383
- const totalCost = Number(obj.total_cost_usd);
384
- if (Number.isFinite(totalCost) && totalCost >= 0) out.totalCostUsd = totalCost;
409
+ // A NUMBER, not anything Number() will coerce. The Cohort Engine writes
410
+ // `total_cost_usd: null` when not every request reported gateway cost
411
+ // (lib/engine/output/json.mjs), and `Number(null)` is 0 — finite, >= 0 —
412
+ // which recorded such a session as $0.00 authoritative: the free-looking
413
+ // row this whole parser exists to never write. The stock CLI always sends a
414
+ // number, so the coercion was harmless until the engine port made null a
415
+ // value this field can carry. (engine-envelope-parity.test.mjs)
416
+ const totalCost = obj.total_cost_usd;
417
+ if (typeof totalCost === "number" && Number.isFinite(totalCost) && totalCost >= 0) out.totalCostUsd = totalCost;
385
418
  if (typeof obj.model === "string" && obj.model) {
386
419
  out.model = /opus/i.test(obj.model) ? "opus" : /haiku/i.test(obj.model) ? "haiku" : "sonnet";
387
420
  }
388
421
  return out;
389
422
  }
390
423
 
424
+ /**
425
+ * What a Cohort Engine result says about the gateway that served it: the
426
+ * `cohort` block (lib/engine/output/json.mjs) carries the gateway request ids.
427
+ * PURE. Empty for a claude result.
428
+ * @param {object} obj the parsed run result
429
+ * @returns {{servedByCohort?:true, requestIds?:string[]}}
430
+ */
431
+ export function cohortResultMeta(obj) {
432
+ const c = obj && typeof obj === "object" && obj.cohort && typeof obj.cohort === "object" ? obj.cohort : null;
433
+ if (!c) return {};
434
+ const ids = Array.isArray(c.requestIds) ? c.requestIds.filter((x) => typeof x === "string" && x) : [];
435
+ return { servedByCohort: true, requestIds: ids };
436
+ }
437
+
391
438
  /**
392
439
  * Append a TRUTHFUL cost-ledger row for a finished dispatcher session via
393
440
  * scripts/cost/track-claude-usage.mjs (source "dispatcher"). Real token counts
@@ -403,7 +450,7 @@ export function parseUsageFromText(text) {
403
450
  *
404
451
  * Best-effort + detached; never blocks the close path.
405
452
  */
406
- function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId }) {
453
+ function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId, billedBy = null }) {
407
454
  const usage = parseUsageFromText(stdout);
408
455
  try {
409
456
  const trackerPath = join(AGENT_REPO_DIR, "scripts/cost/track-claude-usage.mjs");
@@ -420,6 +467,10 @@ function recordDispatcherCost({ stdout, model, durationMs, exitCode, decisionId
420
467
  // routing decision + resume marker (SPEC §4.7). The tracker captures it as a
421
468
  // generic flag; absent on the v1 / no-config paths.
422
469
  if (decisionId) trackerArgs.push("--decision-id", String(decisionId));
470
+ // A session cohort-llm served was metered by the gateway: mark the row so
471
+ // org cost-sync never reports it twice, and keep the gateway request ids.
472
+ if (billedBy || usage.servedByCohort) trackerArgs.push("--billed-by", String(billedBy || COHORT_LLM_BILLED_BY));
473
+ if (Array.isArray(usage.requestIds) && usage.requestIds.length) trackerArgs.push("--request-ids", usage.requestIds.join(","));
423
474
  if (usage.ok) {
424
475
  trackerArgs.push("--input-tokens", String(usage.inputTokens));
425
476
  trackerArgs.push("--output-tokens", String(usage.outputTokens));
@@ -705,6 +756,21 @@ export function reconcileResumePending(opts = {}) {
705
756
  continue;
706
757
  }
707
758
 
759
+ // cohort-llm admission: a marker whose work now routes to cohort-llm is not
760
+ // resumed into a refused quota. A deferral is not a try (no strike).
761
+ // W4-E1: the admission (with the engine it resolved and its token) rides to the spawn.
762
+ let admission = { ok: true };
763
+ {
764
+ const admitResume = typeof opts.admitResume === "function" ? opts.admitResume : defaultAdmitResume;
765
+ try { admission = admitResume({ marker }) || { ok: true }; } catch { admission = { ok: true }; }
766
+ if (!admission.ok) {
767
+ const d = admission.deferral || {};
768
+ logSession({ event: "resume_cohort_deferred", sessionId: marker.sessionId, item_id: marker.itemId, code: d.code || null, kind: d.kind || null, until: Number.isFinite(d.until) ? new Date(d.until).toISOString() : null, cold: admission.cold === true });
769
+ stats.deferred++;
770
+ continue;
771
+ }
772
+ }
773
+
708
774
  // Bump the attempt count on the marker BEFORE re-dispatch so a crash mid-
709
775
  // resume still advances toward the 3-strike cap (no infinite loop).
710
776
  marker.recoveryAttempts = attempts + 1;
@@ -716,7 +782,7 @@ export function reconcileResumePending(opts = {}) {
716
782
 
717
783
  const spawnResume = typeof opts.spawnResume === "function" ? opts.spawnResume : defaultSpawnResume;
718
784
  try {
719
- spawnResume({ marker });
785
+ spawnResume({ marker, admission });
720
786
  logSession({ event: "resume_dispatched", sessionId: marker.sessionId, claudeSessionId: marker.claudeSessionId, item_id: marker.itemId, attempt: marker.recoveryAttempts });
721
787
  stats.resumed++;
722
788
  } catch (err) {
@@ -771,9 +837,12 @@ export function resumeModelFlag(marker, target) {
771
837
  * [--print, --output-format json, <permission args>, <daemon args>,
772
838
  * <router knobs>, --session-id <id>, --model <flag>, <prompt>]
773
839
  *
774
- * and the same env rule: a retarget (envForSpawn carries ANTHROPIC_BASE_URL)
775
- * goes through buildChildEnv's §7.3 credential scrub; the Anthropic path keeps
776
- * the explicit empty-key posture (keychain / OAuth-token fallthrough).
840
+ * and the same env rule (both built by the runtime adapter): a retarget
841
+ * (envForSpawn carries ANTHROPIC_BASE_URL) goes through the runtime env
842
+ * builder's credential scrub — foreign keys AND the seat's OAuth token — and a
843
+ * retarget without a third-party token is refused and resumed on the stock
844
+ * path instead (`refusedRetarget`); the Anthropic path keeps the explicit
845
+ * empty-key posture (keychain / OAuth-token fallthrough).
777
846
  * MAESTRO_SESSION_ID rides the env so delivery receipts attribute correctly.
778
847
  *
779
848
  * @param {object} o
@@ -789,31 +858,67 @@ export function buildResumeSpawn(o = {}) {
789
858
  const marker = o.marker || {};
790
859
  const target = o.target && typeof o.target === "object" ? o.target : null;
791
860
  const pathFn = typeof o.augmentedPath === "function" ? o.augmentedPath : augmentedPath;
792
- const modelFlag = resumeModelFlag(marker, target);
793
- const knobArgs = [];
794
- if (target) {
795
- if (target.maxTurns != null && Number.isFinite(Number(target.maxTurns))) knobArgs.push("--max-turns", String(target.maxTurns));
796
- if (target.effort) knobArgs.push("--effort", String(target.effort));
797
- if (target.agentsJson) knobArgs.push("--agents", typeof target.agentsJson === "string" ? target.agentsJson : JSON.stringify(target.agentsJson));
798
- }
799
- const args = [
800
- "--print",
801
- "--output-format", "json",
802
- ...(Array.isArray(o.permissionArgs) ? o.permissionArgs : []),
803
- ...(Array.isArray(o.daemonArgs) ? o.daemonArgs : []),
804
- ...knobArgs,
805
- "--session-id", marker.claudeSessionId,
806
- "--model", modelFlag,
807
- marker.prompt,
808
- ];
861
+ // argv + env come from the runtime adapter (lib/runtime/adapter.mjs, lane
862
+ // "resume"): the same contract as a fresh dispatcher spawn, built in one place.
809
863
  const base = o.baseEnv && typeof o.baseEnv === "object" ? o.baseEnv : {};
810
- const envForSpawn = (target && target.envForSpawn) || {};
811
- const retargeting = !!envForSpawn.ANTHROPIC_BASE_URL;
812
- const env = retargeting
813
- ? { ...buildChildEnv({ ...base, PATH: pathFn() }, envForSpawn), PATH: pathFn() }
814
- : { ...base, PATH: pathFn(), ANTHROPIC_API_KEY: "", ANTHROPIC_AUTH_TOKEN: "", ...envForSpawn };
815
- if (marker.sessionId) env.MAESTRO_SESSION_ID = String(marker.sessionId);
816
- return { args, env, modelFlag, retargeting };
864
+ const daemonArgs = Array.isArray(o.daemonArgs) ? o.daemonArgs : [];
865
+ const deps = {
866
+ augmentedPath: pathFn,
867
+ daemonArgs: () => daemonArgs,
868
+ resolveBin: typeof o.resolveBin === "function" ? o.resolveBin : () => CLAUDE_BIN,
869
+ };
870
+ // W4-E1: engine cohort continues the same --session-id through `cli.mjs run`
871
+ // (never --resume), on the marker's Anthropic slot mapped to a tier, with no retarget.
872
+ const engine = o.engine === "cohort" ? "cohort" : "claude";
873
+ if (engine === "cohort") {
874
+ const shape = engineRunShape({ target, laneModel: marker.model || "sonnet" });
875
+ const spec = buildSpawn({
876
+ lane: "resume",
877
+ model: shape.model,
878
+ prompt: marker.prompt,
879
+ sessionId: marker.claudeSessionId,
880
+ permissions: Array.isArray(o.permissionArgs) ? o.permissionArgs : [],
881
+ mcp: {},
882
+ knobs: shape.knobs,
883
+ env: base,
884
+ retarget: shape.retarget,
885
+ extraEnv: { MAESTRO_SESSION_ID: marker.sessionId || undefined },
886
+ ...seatEngineFields({ engine, agentRoot: o.agentRoot || null, token: o.cohortToken || null, baseUrl: o.cohortBaseUrl || null }),
887
+ ...(o.maxBudgetUsd != null ? { maxBudgetUsd: o.maxBudgetUsd } : {}),
888
+ }, deps);
889
+ if (!spec.ok) throw new Error(`buildResumeSpawn: ${spec.error.message}`);
890
+ return { args: spec.argv, env: spec.env, bin: spec.bin, modelFlag: shape.model, retargeting: false, refusedRetarget: null, engine };
891
+ }
892
+ const spawnInput = (t, flag) => ({
893
+ lane: "resume",
894
+ model: flag,
895
+ prompt: marker.prompt,
896
+ sessionId: marker.claudeSessionId,
897
+ permissions: Array.isArray(o.permissionArgs) ? o.permissionArgs : [],
898
+ mcp: {},
899
+ knobs: t,
900
+ env: base,
901
+ retarget: (t && t.envForSpawn) || {},
902
+ extraEnv: { MAESTRO_SESSION_ID: marker.sessionId || undefined },
903
+ });
904
+ let modelFlag = resumeModelFlag(marker, target);
905
+ let spec = buildSpawn(spawnInput(target, modelFlag), deps);
906
+ let refusedRetarget = null;
907
+ if (!spec.ok && /^retarget_/.test(spec.error.code)) {
908
+ // The retarget was refused (no third-party token): resume on the stock
909
+ // Anthropic path instead of handing that backend the seat's own
910
+ // credential. resumeModelFlag with a non-retarget target keeps F11 — a
911
+ // foreign pinned id is never passed to Anthropic.
912
+ refusedRetarget = spec.error;
913
+ // CF-11: the Anthropic rerun must not inherit the third-party decision's
914
+ // knobs (--max-turns/--effort/--agents were sized for that backend).
915
+ const stock = { ...target, envForSpawn: {}, modelFlag: null, maxTurns: null, effort: null, agentsJson: null };
916
+ modelFlag = resumeModelFlag(marker, stock);
917
+ spec = buildSpawn(spawnInput(stock, modelFlag), deps);
918
+ }
919
+ // A claude resume past the retarget fallback cannot fail: a failure is a defect.
920
+ if (!spec.ok) throw new Error(`buildResumeSpawn: ${spec.error.message}`);
921
+ return { args: spec.argv, env: spec.env, bin: spec.bin, modelFlag, retargeting: spec.retargeting, refusedRetarget, engine };
817
922
  }
818
923
 
819
924
  /**
@@ -832,7 +937,7 @@ export function buildResumeSpawn(o = {}) {
832
937
  * and the fresh prompt continues the work.
833
938
  *
834
939
  * Audit F11: the argv and env are built through the SAME resolveSpawnTarget /
835
- * buildChildEnv path as spawnSession (buildResumeSpawn), so a retargeted
940
+ * runtime-adapter path as spawnSession (buildResumeSpawn), so a retargeted
836
941
  * session resumes on its backend, the router's knobs ride along, and
837
942
  * `--output-format json` makes the run's usage measurable for the ledger.
838
943
  *
@@ -840,41 +945,85 @@ export function buildResumeSpawn(o = {}) {
840
945
  * Best-effort; failures are logged, the marker stays (next reconcile retries
841
946
  * up to the strike cap). On a clean close the marker is cleared here.
842
947
  */
843
- function defaultSpawnResume({ marker }) {
948
+ /**
949
+ * The router's CURRENT decision for a resume marker. resolveSpawnTarget never
950
+ * throws (it degrades to the v1 path / stock CLI internally); the request is
951
+ * rebuilt from what the marker kept of the original classification. null when
952
+ * there is no router or it faulted.
953
+ */
954
+ function resumeTargetFor(marker) {
844
955
  const source = marker.source || "backlog";
845
- // The router's CURRENT decision for this work. resolveSpawnTarget never
846
- // throws (it degrades to the v1 path / stock CLI internally); the request is
847
- // rebuilt from what the marker kept of the original classification.
848
- let target = null;
849
956
  try {
850
957
  const routingConfig = getRoutingConfig();
851
- if (routingConfig) {
852
- const classResult = { model: marker.model || "sonnet", priority: marker.priority || "normal", summary: marker.summary || null };
853
- const routingRequest = requestFromClassifierResult(classResult, { source, role: "responder" });
854
- target = resolveSpawnTarget(routingConfig, routingRequest, classResult, source, marker.modelFlag || marker.model || "sonnet");
855
- }
958
+ if (!routingConfig) return null;
959
+ const classResult = { model: marker.model || "sonnet", priority: marker.priority || "normal", summary: marker.summary || null };
960
+ const routingRequest = requestFromClassifierResult(classResult, { source, role: "responder" });
961
+ return resolveSpawnTarget(routingConfig, routingRequest, classResult, source, marker.modelFlag || marker.model || "sonnet");
856
962
  } catch (err) {
857
963
  // A router fault must not block a resume: fall back to the marker's flag.
858
964
  console.warn(`[dispatcher] resume routing failed for ${marker.sessionId}, using marker flag: ${err.message}`);
859
- target = null;
965
+ return null;
860
966
  }
861
- const { args, env: spawnEnv, modelFlag } = buildResumeSpawn({
967
+ }
968
+
969
+ /** The cohort-llm gate for a resume; a cold cache primes itself for the next reconcile. */
970
+ function defaultAdmitResume({ marker }) {
971
+ const target = resumeTargetFor(marker);
972
+ // W4-E1: the engine is resolved once here and rides the admission to the spawn.
973
+ const engine = readSeatEngine(AGENT_REPO_DIR);
974
+ const gate = cohortGateFor(target, engine);
975
+ if (!gate.ok && gate.cold) {
976
+ const c = getCohortClients();
977
+ if (c && c.quota) Promise.resolve().then(() => c.quota.canAdmit()).catch(() => null);
978
+ }
979
+ if (gate.ok && engine === "cohort") {
980
+ // An engine-cohort resume starts with a seat token. None cached (a cleared
981
+ // cache) is a short retry, exactly as a fresh dispatch parks it: a deferral,
982
+ // not a try, so the marker takes no strike. Mint one for the next reconcile.
983
+ const token = gate.token || seatTokenSync(getCohortClients()?.token);
984
+ if (!token) {
985
+ const tm = getCohortClients()?.token;
986
+ if (tm && typeof tm.getToken === "function") Promise.resolve().then(() => tm.getToken()).catch(() => null);
987
+ return { ok: false, cohort: true, engine, deferral: { kind: "retry", until: Date.now() + 30_000, code: "cohort_token_unavailable", message: "no cohort-llm seat token for the engine", refusal: null } };
988
+ }
989
+ return { ...gate, engine, token };
990
+ }
991
+ return { ...gate, engine };
992
+ }
993
+
994
+ function defaultSpawnResume({ marker, admission = null }) {
995
+ const target = resumeTargetFor(marker);
996
+ // W4-E1: the engine the admission resolved (else read now), its token, and the envelope's cap.
997
+ const engine = admission && (admission.engine === "cohort" || admission.engine === "claude") ? admission.engine : readSeatEngine(AGENT_REPO_DIR);
998
+ const engineFields = engine === "cohort"
999
+ ? { engine, agentRoot: AGENT_REPO_DIR, cohortToken: (admission && admission.token) || seatTokenSync(getCohortClients()?.token), cohortBaseUrl: getCohortClients()?.token?.baseUrl || null, ...(() => { const b = engineSessionBudget(); return b.maxBudgetUsd != null ? { maxBudgetUsd: b.maxBudgetUsd } : {}; })() }
1000
+ : {};
1001
+ const { args, env: spawnEnv, bin, modelFlag, refusedRetarget } = buildResumeSpawn({
862
1002
  marker,
863
1003
  target,
864
1004
  baseEnv: process.env,
865
1005
  permissionArgs: sessionPermissionArgs({ source: "dispatcher-resume", priority: marker.priority }),
866
1006
  daemonArgs: daemonClaudeArgs(),
1007
+ ...engineFields,
867
1008
  });
1009
+ if (refusedRetarget) {
1010
+ logSession({ event: "resume_retarget_refused", sessionId: marker.sessionId, code: refusedRetarget.code, error: refusedRetarget.message });
1011
+ }
1012
+ // An engine-cohort seat is cohort-served whatever routing picked (the same test
1013
+ // cohortGateFor applies), so its refusals key the "cohort" backend.
1014
+ const resumeEngineCohort = engine === "cohort";
1015
+ const resumeServedByCohort = resumeEngineCohort || (!refusedRetarget && isCohortRouted({ target }));
1016
+ const resumeBreaker = resumeServedByCohort ? rateGuardModule.COHORT_BACKEND : RATE_PROVIDER;
868
1017
  const decisionId = (target && target.decisionId) || marker.decision_id || null;
869
1018
  const startedAt = Date.now();
870
- const proc = _spawn(CLAUDE_BIN, args, { cwd: AGENT_REPO_DIR, env: spawnEnv, stdio: ["ignore", "pipe", "pipe"] });
1019
+ const proc = _spawn(bin, args, { cwd: AGENT_REPO_DIR, env: spawnEnv, stdio: ["ignore", "pipe", "pipe"] });
871
1020
  let stdout = "";
872
1021
  let stderr = "";
873
1022
  proc.stdout.on("data", (c) => { stdout += c.toString(); });
874
1023
  proc.stderr.on("data", (c) => { stderr += c.toString(); });
875
1024
  proc.on("close", (code) => {
876
1025
  // A resumed run costs money too: parse its --output-format json usage.
877
- try { recordDispatcherCost({ stdout, model: marker.model, durationMs: Date.now() - startedAt, exitCode: code, decisionId }); }
1026
+ try { recordDispatcherCost({ stdout, model: marker.model, durationMs: Date.now() - startedAt, exitCode: code, decisionId, billedBy: resumeServedByCohort ? COHORT_LLM_BILLED_BY : null }); }
878
1027
  catch { /* cost tracking is best-effort */ }
879
1028
  if (code === 0) {
880
1029
  // Resume succeeded — the underlying work is done; clear the marker.
@@ -883,11 +1032,18 @@ function defaultSpawnResume({ marker }) {
883
1032
  } else {
884
1033
  // A usage limit or 429 during resume must open the breaker so subsequent
885
1034
  // spawns gate. Usage-limit → hold until reset; 429 → transient backoff.
886
- const ul = rateGuard.classifyUsageLimit?.(stderr, { agentRoot: AGENT_REPO_DIR }) ?? { isLimit: false, resetAt: null };
887
- if (ul.isLimit) {
888
- try { rateGuard.recordUsageLimit(RATE_PROVIDER, ul.resetAt, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
1035
+ // Structured cohort refusal FIRST; an engine-cohort lane never reads the
1036
+ // English subscription-limit text (see the spawn path below).
1037
+ const resumeRefusal = resumeServedByCohort ? (rateGuard.classifyCohortRefusal?.(`${stdout}\n${stderr}`) ?? null) : null;
1038
+ const resumeUlText = resumeRefusal ? { isLimit: false, resetAt: null } : (rateGuard.classifyUsageLimit?.(stderr, { agentRoot: AGENT_REPO_DIR }) ?? { isLimit: false, resetAt: null });
1039
+ const ul = resumeEngineCohort ? { isLimit: false, resetAt: null } : resumeUlText;
1040
+ if (resumeEngineCohort && resumeUlText.isLimit) logSession({ event: "usage_limit_text_ignored", sessionId: marker.sessionId, engine: "cohort" });
1041
+ if (resumeRefusal) {
1042
+ try { rateGuard.recordCohortRefusal?.(resumeRefusal, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
1043
+ } else if (ul.isLimit) {
1044
+ try { rateGuard.recordUsageLimit(resumeBreaker, ul.resetAt, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
889
1045
  } else if (rateGuard.classifyStderr(stderr)) {
890
- try { rateGuard.recordRateLimit(RATE_PROVIDER, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
1046
+ try { rateGuard.recordRateLimit(resumeBreaker, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
891
1047
  }
892
1048
  logSession({ event: "resume_exit_nonzero", sessionId: marker.sessionId, item_id: marker.itemId, exit_code: code });
893
1049
  }
@@ -897,18 +1053,29 @@ function defaultSpawnResume({ marker }) {
897
1053
  });
898
1054
  }
899
1055
 
1056
+ /**
1057
+ * The entry shape is `lib/session/current-work.activeEntryFor` — the SAME
1058
+ * module that reads active.json back into the "what am I doing right now"
1059
+ * block, so the writer and the reader cannot drift. It carries the Cohort ask
1060
+ * identity (channelId + messageId) so the board row hq opens for an inbox ask
1061
+ * can be linked to this session ({@link linkActiveSessionBoard}), and the
1062
+ * board item id when a backlog row is for one.
1063
+ */
900
1064
  function writeActiveSession(sessionId, entry) {
901
1065
  try {
902
1066
  let active = {};
903
1067
  try { active = JSON.parse(readFileSync(ACTIVE_PATH, "utf-8")); } catch {}
904
- active[sessionId] = {
905
- sender: entry.item?.sender || null,
906
- channel: entry.item?.channel || entry.item?.channel_id || null,
907
- summary: entry.classResult?.summary || "unknown",
908
- model: entry.classResult?.model || "sonnet",
909
- startTime: Date.now(),
910
- source: entry.source,
911
- };
1068
+ active[sessionId] = activeEntryFor({ item: entry.item, classResult: entry.classResult, source: entry.source, now: Date.now() });
1069
+ // A board row that resolved BEFORE the spawn (the daemon's `working` track
1070
+ // is fire-and-forget and can win the race) is applied now, not lost.
1071
+ const ask = active[sessionId].ask;
1072
+ if (ask) {
1073
+ const pending = pendingBoardLinks.get(`${ask.channelId}:${ask.messageId}`);
1074
+ if (pending) {
1075
+ active[sessionId] = { ...active[sessionId], boardTaskId: pending.taskId, boardCol: pending.col || null };
1076
+ pendingBoardLinks.delete(`${ask.channelId}:${ask.messageId}`);
1077
+ }
1078
+ }
912
1079
  const tmpPath = ACTIVE_PATH + ".tmp";
913
1080
  writeFileSync(tmpPath, JSON.stringify(active, null, 2));
914
1081
  renameSync(tmpPath, ACTIVE_PATH);
@@ -917,6 +1084,45 @@ function writeActiveSession(sessionId, entry) {
917
1084
  }
918
1085
  }
919
1086
 
1087
+ /** Board rows that resolved for an ask whose session is not in active.json yet. Bounded. */
1088
+ const pendingBoardLinks = new Map();
1089
+ const PENDING_BOARD_LINKS_MAX = 64;
1090
+
1091
+ /**
1092
+ * Attach the board row hq opened for an ask to the live session working it,
1093
+ * so the current-work block can cite the item id. Called by the daemon when
1094
+ * its `working` board.track resolves with a taskId (agent-daemon.mjs).
1095
+ *
1096
+ * PURE core (`linkBoardTask`) + one atomic rewrite of active.json. Fail-open:
1097
+ * a board link must never break a dispatch. When no live session carries the
1098
+ * ask yet (the track won the race against the spawn), the link is parked and
1099
+ * applied by the next writeActiveSession for that ask.
1100
+ *
1101
+ * @param {{channelId:string, messageId:string}} ask
1102
+ * @param {{taskId:string, col?:string|null}} row
1103
+ * @returns {{linked:boolean, sessionId:string|null, parked:boolean}}
1104
+ */
1105
+ export function linkActiveSessionBoard(ask, row) {
1106
+ try {
1107
+ if (!ask || !ask.channelId || !ask.messageId || !row || !row.taskId) return { linked: false, sessionId: null, parked: false };
1108
+ let active = {};
1109
+ try { active = JSON.parse(readFileSync(ACTIVE_PATH, "utf-8")); } catch {}
1110
+ const r = linkBoardTask(active, ask, row);
1111
+ if (!r.sessionId) {
1112
+ if (pendingBoardLinks.size >= PENDING_BOARD_LINKS_MAX) pendingBoardLinks.delete(pendingBoardLinks.keys().next().value);
1113
+ pendingBoardLinks.set(`${ask.channelId}:${ask.messageId}`, { taskId: String(row.taskId), col: row.col || null });
1114
+ return { linked: false, sessionId: null, parked: true };
1115
+ }
1116
+ const tmpPath = ACTIVE_PATH + ".tmp";
1117
+ writeFileSync(tmpPath, JSON.stringify(r.active, null, 2));
1118
+ renameSync(tmpPath, ACTIVE_PATH);
1119
+ return { linked: true, sessionId: r.sessionId, parked: false };
1120
+ } catch (err) {
1121
+ console.warn(`[dispatcher] Failed to link board row to session: ${err.message}`);
1122
+ return { linked: false, sessionId: null, parked: false };
1123
+ }
1124
+ }
1125
+
920
1126
  function removeActiveSession(sessionId) {
921
1127
  try {
922
1128
  let active = {};
@@ -1084,6 +1290,13 @@ export function canDispatchBacklog(item) {
1084
1290
  * fires onClose once it is later drained and spawned). It does NOT fire on the
1085
1291
  * pre-spawn drop paths (backlog dedup / claim-denied / governor-DEFER backlog),
1086
1292
  * which leave the durable on-disk item untouched for the next sweep anyway.
1293
+ * @param {(o:{sessionId:string, item:object})=>void} [opts.onSpawn]
1294
+ * Invoked EXACTLY ONCE, when a child process has actually been started for
1295
+ * this entry, with the session id the dispatcher minted. This is the ONLY
1296
+ * truthful "started" signal: `dispatch()` returns silently on every not-now
1297
+ * path (rate-limited, governor QUEUE/DEFER, backlog cap, claim denied, the
1298
+ * cohort pre-spawn gate), and a caller that announced "started" at the call
1299
+ * site announced it for those too. Survives queueing like onClose.
1087
1300
  */
1088
1301
  export function dispatch(prompt, item, classResult, source = "inbox", opts = {}) {
1089
1302
  // `obligationKey` names the debt this session was spawned to discharge. It is
@@ -1096,6 +1309,8 @@ export function dispatch(prompt, item, classResult, source = "inbox", opts = {})
1096
1309
  prompt, item, classResult, source,
1097
1310
  obligationKey: opts.obligationKey || null,
1098
1311
  onClose: typeof opts.onClose === "function" ? opts.onClose : null,
1312
+ onSpawn: typeof opts.onSpawn === "function" ? opts.onSpawn : null,
1313
+ cohortPrimed: opts.cohortPrimed === true,
1099
1314
  };
1100
1315
  const priority = classResult.priority;
1101
1316
  const isPriorityInbox = source === "inbox" && (priority === "critical" || priority === "high");
@@ -1386,13 +1601,6 @@ function spawnSession(entry) {
1386
1601
  ? (source === "backlog" ? OPUS_BACKLOG_TIMEOUT : OPUS_INBOX_TIMEOUT)
1387
1602
  : (source === "backlog" ? SONNET_BACKLOG_TIMEOUT : SONNET_INBOX_TIMEOUT);
1388
1603
 
1389
- // Track backlog items to prevent retry storms
1390
- if (source === "backlog") {
1391
- const key = backlogKey(item);
1392
- activeBacklogKeys.add(key);
1393
- backlogRetryCount.set(key, (backlogRetryCount.get(key) || 0) + 1);
1394
- }
1395
-
1396
1604
  // Resolve the spawn target via the model router. v2 configs go through
1397
1605
  // resolveChain (full RouteDecision: model + retarget env + spawn knobs +
1398
1606
  // decision_id + estimated cost + failover chain); v1 configs keep the legacy
@@ -1401,7 +1609,44 @@ function spawnSession(entry) {
1401
1609
  const routingConfig = getRoutingConfig();
1402
1610
  const routingRequest = requestFromClassifierResult(classResult, { source, role: "responder" });
1403
1611
  const target = resolveSpawnTarget(routingConfig, routingRequest, classResult, source, model, item);
1404
- const effectiveModelFlag = target.modelFlag || model;
1612
+ let effectiveModelFlag = target.modelFlag || model;
1613
+
1614
+ // cohort-llm pre-spawn gate (design C6 / §8.3). Stock Claude Code retries a
1615
+ // 429 ten times and ignores the gateway's quota headers, so a session that
1616
+ // cohort-llm would refuse must not start. Decided BEFORE any counter, claim
1617
+ // or lock below moves, so a deferral leaves no trace but the requeue. Human
1618
+ // replies are not exempt: a refused seat would 429 them just the same.
1619
+ //
1620
+ // W4-E1 (CF-44): the seat's engine is resolved ONCE for this spawn and used by
1621
+ // the gate, the spawn and the breaker alike. Read per spawn, so flipping
1622
+ // runtime.engine back to "claude" restores claude on the next spawn.
1623
+ const seatEngine = readSeatEngine(AGENT_REPO_DIR);
1624
+ const engineCohortSpawn = seatEngine === "cohort";
1625
+ let engineToken = null;
1626
+ {
1627
+ const gate = cohortGateFor(target, seatEngine);
1628
+ if (!gate.ok) {
1629
+ deferSpawnForCohort(entry, gate, target);
1630
+ return false;
1631
+ }
1632
+ if (engineCohortSpawn) {
1633
+ // The engine starts with the seat token the admission read. None (a
1634
+ // cleared cache) is a short retry, parked like any cohort deferral —
1635
+ // never a claude spawn on the seat's Anthropic credential.
1636
+ engineToken = gate.token || seatTokenSync(getCohortClients()?.token);
1637
+ if (!engineToken) {
1638
+ deferSpawnForCohort(entry, { ok: false, cohort: true, deferral: { kind: "retry", until: Date.now() + 30_000, code: "cohort_token_unavailable", message: "no cohort-llm seat token for the engine", refusal: null } }, target);
1639
+ return false;
1640
+ }
1641
+ }
1642
+ }
1643
+
1644
+ // Track backlog items to prevent retry storms
1645
+ if (source === "backlog") {
1646
+ const key = backlogKey(item);
1647
+ activeBacklogKeys.add(key);
1648
+ backlogRetryCount.set(key, (backlogRetryCount.get(key) || 0) + 1);
1649
+ }
1405
1650
 
1406
1651
  // Session continuity. A fresh `randomUUID()` per dispatch meant every full
1407
1652
  // session in a live thread started COLD — the agent re-read the room, re-did
@@ -1452,72 +1697,71 @@ function spawnSession(entry) {
1452
1697
  }
1453
1698
  const claudeSessionId = resumedSessionId || randomUUID();
1454
1699
 
1455
- // The v2 router supplies spawn knobs (SPEC §4.4 / §6.5): --max-turns bounds
1456
- // the session, --effort tunes reasoning where supported, --agents attaches the
1457
- // cheap-subagent (Haiku-Explore) fan-out map — the sanctioned intra-session
1458
- // cost lever that keeps the main loop's cache intact. v1 / no-config spawns
1459
- // carry none of these (knobs are null), so this is purely additive.
1460
- const knobArgs = [];
1461
- if (target.maxTurns != null && Number.isFinite(Number(target.maxTurns))) {
1462
- knobArgs.push("--max-turns", String(target.maxTurns));
1463
- }
1464
- if (target.effort) knobArgs.push("--effort", String(target.effort));
1465
- if (target.agentsJson) {
1466
- knobArgs.push("--agents", typeof target.agentsJson === "string" ? target.agentsJson : JSON.stringify(target.agentsJson));
1467
- }
1468
-
1469
- const args = [
1470
- "--print",
1471
- // --output-format json so this run's stdout carries the REAL token usage we
1472
- // record to the cost ledger (recovery C1). The dispatcher's stdout is only
1473
- // tee'd to a per-session log file — it's never streamed to the user (the
1474
- // session sends its own user-facing messages via the Slack/Gmail APIs), so
1475
- // switching the format does not affect any reply.
1476
- "--output-format", "json",
1477
- ...sessionPermissionArgs({ source: "dispatcher", priority: classResult?.priority }),
1478
- ...daemonClaudeArgs(),
1479
- ...knobArgs,
1480
- "--session-id", claudeSessionId,
1481
- "--model", effectiveModelFlag,
1700
+ // argv + env come from the runtime adapter (lib/runtime/adapter.mjs, lane
1701
+ // "dispatcher"). What it builds, unchanged from the inline version:
1702
+ // argv --print --output-format json <permissions> <daemon MCP args>
1703
+ // <router knobs> --session-id <id> --model <flag> <prompt>
1704
+ // --output-format json so stdout carries the REAL token usage for the
1705
+ // cost ledger (recovery C1); the dispatcher's stdout only goes to a
1706
+ // per-session log, never to a user. The v2 knobs (--max-turns,
1707
+ // --effort, --agents Haiku-Explore) are null on v1 / no-config spawns.
1708
+ // env no router / an Anthropic decision: stock posture — strip
1709
+ // ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN so `claude` rides the seat's
1710
+ // subscription credential (CEO directive 2026-04-27).
1711
+ // A v2 retarget (envForSpawn carries ANTHROPIC_BASE_URL): the one
1712
+ // runtime env builder scrubs every foreign credential AND the seat's
1713
+ // OAuth token, and forces ANTHROPIC_API_KEY="" and
1714
+ // CLAUDE_CODE_ATTRIBUTION_HEADER=0.
1715
+ // MAESTRO_SESSION_ID / MAESTRO_OBLIGATION_KEY are set after every merge so a
1716
+ // router target can never drop the delivery-receipt attribution; they are
1717
+ // plain identifiers, never credentials.
1718
+ // W4-E1: an engine-cohort seat runs `cli.mjs run` — its tier from the router
1719
+ // only when the router chose cohort, no retarget, and a per-session spend cap
1720
+ // from the seat's funded envelope. A claude seat's input is exactly as before.
1721
+ const engineShape = engineCohortSpawn ? engineRunShape({ target, laneModel: model }) : null;
1722
+ const spawnInput = {
1723
+ lane: "dispatcher",
1724
+ bin: CLAUDE_BIN,
1725
+ model: engineShape ? engineShape.model : effectiveModelFlag,
1482
1726
  prompt,
1483
- ];
1484
-
1485
- // Build the spawn env.
1486
- // Default (no router): strip ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN so
1487
- // `claude` falls through to the keychain OAuth (Max subscription) per
1488
- // CEO directive 2026-04-27.
1489
- // Router active: merge the resolved target's envForSpawn — this points the
1490
- // CLI at the chosen backend (Moonshot, OpenRouter, NIM, etc.) and sets
1491
- // ANTHROPIC_API_KEY="" explicitly so Claude Code doesn't fall back to
1492
- // OAuth against api.anthropic.com.
1493
- //
1494
- // For a v2 retarget (envForSpawn carries ANTHROPIC_BASE_URL), we build the
1495
- // child env through the execution layer's buildChildEnv so the §7.3 allowlist
1496
- // scrub applies (no foreign *_API_KEY / *_AUTH_TOKEN leaks into a third-party
1497
- // session). buildChildEnv already injects ANTHROPIC_API_KEY="" for a retarget.
1498
- // For the common Anthropic-session case (envForSpawn === {}) and the v1 path
1499
- // we keep the prior explicit empty-key env verbatim (byte-compatible).
1500
- const retargeting = !!(target.envForSpawn && target.envForSpawn.ANTHROPIC_BASE_URL);
1501
- const spawnEnv = retargeting
1502
- ? { ...buildChildEnv({ ...process.env, PATH: augmentedPath() }, target.envForSpawn), PATH: augmentedPath() }
1503
- : {
1504
- ...process.env,
1505
- PATH: augmentedPath(),
1506
- ANTHROPIC_API_KEY: "",
1507
- ANTHROPIC_AUTH_TOKEN: "",
1508
- ...(target.envForSpawn || {}),
1509
- };
1510
- // ATTRIBUTION for delivery receipts. Set after the merges above so a router
1511
- // target's envForSpawn can never drop it — a retargeted session replies
1512
- // through exactly the same CLI lanes and owes exactly the same receipt.
1513
- // These are plain identifiers, never credentials, so the §7.3 allowlist scrub
1514
- // in buildChildEnv has nothing to object to.
1515
- spawnEnv.MAESTRO_SESSION_ID = sessionId;
1516
- if (entry.obligationKey) spawnEnv.MAESTRO_OBLIGATION_KEY = String(entry.obligationKey);
1517
-
1518
- const proc = _spawn(CLAUDE_BIN, args, {
1727
+ sessionId: claudeSessionId,
1728
+ permissions: sessionPermissionArgs({ source: "dispatcher", priority: classResult?.priority }),
1729
+ mcp: {},
1730
+ knobs: engineShape ? engineShape.knobs : target,
1731
+ env: process.env,
1732
+ retarget: engineShape ? engineShape.retarget : (target.envForSpawn || {}),
1733
+ extraEnv: { MAESTRO_SESSION_ID: sessionId, MAESTRO_OBLIGATION_KEY: entry.obligationKey || undefined },
1734
+ ...seatEngineFields({ engine: seatEngine, agentRoot: AGENT_REPO_DIR, token: engineToken, baseUrl: engineCohortSpawn ? getCohortClients()?.token?.baseUrl : null }),
1735
+ ...(engineCohortSpawn ? engineSessionBudget() : {}),
1736
+ };
1737
+ let spawnSpec = buildSpawn(spawnInput);
1738
+ let refusedRetarget = null;
1739
+ if (!spawnSpec.ok && /^retarget_/.test(spawnSpec.error.code)) {
1740
+ // Refused retarget (no third-party token). Answer on the stock Anthropic
1741
+ // path with the classifier's Anthropic model rather than send that backend
1742
+ // the seat's own credential — or drop the item.
1743
+ console.warn(`[dispatcher] ${spawnSpec.error.message} — spawning ${sessionId} on the stock path`);
1744
+ logSession({ event: "retarget_refused", sessionId, code: spawnSpec.error.code, decision_id: target.decisionId || null });
1745
+ refusedRetarget = spawnSpec.error;
1746
+ effectiveModelFlag = model;
1747
+ // CF-11: the rerun is an ANTHROPIC session. The third-party decision's
1748
+ // --max-turns/--effort/--agents were sized for that backend and must not
1749
+ // ride along.
1750
+ spawnSpec = buildSpawn({ ...spawnInput, model, knobs: null, retarget: {} });
1751
+ }
1752
+ // A seat on engine cohort is cohort-served whatever routing picked — the same
1753
+ // test cohortGateFor admits it by — so its breaker is the "cohort" backend.
1754
+ const engineCohort = engineCohortSpawn;
1755
+ const servedByCohort = engineCohort || (!refusedRetarget && isCohortRouted({ target }));
1756
+ const breakerProvider = servedByCohort ? rateGuardModule.COHORT_BACKEND : RATE_PROVIDER;
1757
+ // Both engines are wired and the engine's token was checked before any claim,
1758
+ // so a failure past the retarget fallback is a defect.
1759
+ if (!spawnSpec.ok) throw new Error(`[dispatcher] runtime adapter: ${spawnSpec.error.message}`);
1760
+ const args = spawnSpec.argv;
1761
+
1762
+ const proc = _spawn(spawnSpec.bin, args, {
1519
1763
  cwd: AGENT_REPO_DIR,
1520
- env: spawnEnv,
1764
+ env: spawnSpec.env,
1521
1765
  stdio: ["ignore", "pipe", "pipe"],
1522
1766
  });
1523
1767
 
@@ -1525,14 +1769,20 @@ function spawnSession(entry) {
1525
1769
  // and it isn't the stock Anthropic CLI no-op. The v2 path also carries the
1526
1770
  // decision_id + the one-line explain so `maestro router why` can join this
1527
1771
  // session to its routing-audit + ledger rows.
1772
+ //
1773
+ // CF-11: the event describes the run that ACTUALLY happened. After a refused
1774
+ // retarget that is the stock Anthropic session with the classifier's model
1775
+ // and no router knobs; the intended backend rides along as `refused`.
1528
1776
  if (target.backend && target.transport !== "anthropic-cli") {
1529
1777
  logSession({
1530
1778
  event: "routed",
1531
1779
  sessionId,
1532
1780
  decision_id: target.decisionId,
1533
- backend: target.backend,
1534
- transport: target.transport,
1535
- model: target.model,
1781
+ backend: refusedRetarget ? "anthropic" : target.backend,
1782
+ transport: refusedRetarget ? "anthropic-cli" : target.transport,
1783
+ model: refusedRetarget ? model : target.model,
1784
+ knobs: refusedRetarget ? null : { maxTurns: target.maxTurns ?? null, effort: target.effort ?? null, agents: target.agentsJson ? true : false },
1785
+ ...(refusedRetarget ? { refused: { backend: target.backend, model: target.model, code: refusedRetarget.code } } : {}),
1536
1786
  tried: target.decision ? target.decision.tried : target._v1Resolved?.tried,
1537
1787
  fallback_reason: target.decision ? target.decision.audit?.fallback_reason : target._v1Resolved?.fallback_reason,
1538
1788
  explain: target.explain,
@@ -1562,6 +1812,11 @@ function spawnSession(entry) {
1562
1812
  const startTime = Date.now();
1563
1813
  activeSessions.set(sessionId, { process: proc, item, classResult, startTime, model, source, claudeSessionId });
1564
1814
  writeActiveSession(sessionId, entry);
1815
+ // A child exists from this line on — the one moment "started" is true.
1816
+ if (entry.onSpawn) {
1817
+ try { entry.onSpawn({ sessionId, item }); }
1818
+ catch (err) { console.warn(`[dispatcher] onSpawn callback threw for ${sessionId}: ${err.message}`); }
1819
+ }
1565
1820
 
1566
1821
  // WS4: drop the in-flight resume marker. Present-after-crash = mid-flight, so
1567
1822
  // a startup reconcile can re-dispatch `claude --print --resume`. Cleared on a
@@ -1667,7 +1922,7 @@ function spawnSession(entry) {
1667
1922
  // bound re-dispatch.
1668
1923
  if (code === 0) {
1669
1924
  clearResumePending(sessionId);
1670
- try { rateGuard.recordSuccess(RATE_PROVIDER, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
1925
+ try { rateGuard.recordSuccess(breakerProvider, { agentRoot: AGENT_REPO_DIR }); } catch { /* */ }
1671
1926
  } else {
1672
1927
  // A Max/subscription USAGE limit ("you've hit your session limit · resets
1673
1928
  // 9:40pm") is checked FIRST and on BOTH streams — for `claude --print` it
@@ -1675,15 +1930,32 @@ function spawnSession(entry) {
1675
1930
  // RESETS (not a decorrelated 429 backoff), so the seat stops storming a
1676
1931
  // drained pool. A plain 429/overload falls through to the transient path.
1677
1932
  const combined = `${stdout || ""}\n${stderr || ""}`;
1678
- const ul = rateGuard.classifyUsageLimit?.(combined, { agentRoot: AGENT_REPO_DIR }) ?? { isLimit: false, resetAt: null };
1679
- if (ul.isLimit) {
1933
+ // A cohort-llm refusal is checked FIRST on a cohort-served run: its body
1934
+ // says "rate_limit_error", and the English path would otherwise read a
1935
+ // spent seat window as an Anthropic incident. It holds the "cohort"
1936
+ // backend until the refusal's resetsAt and never opens a provider breaker.
1937
+ const cohortRefusal = servedByCohort ? (rateGuard.classifyCohortRefusal?.(combined) ?? null) : null;
1938
+ // An engine-cohort lane has no Claude subscription: its limits arrive as the
1939
+ // structured refusal above. Its English text ("Session limit reached") is
1940
+ // never read as a subscription usage limit — without a structured refusal
1941
+ // it is logged and left to the transient-429 check (cohort backend).
1942
+ const ulText = cohortRefusal ? { isLimit: false, resetAt: null } : (rateGuard.classifyUsageLimit?.(combined, { agentRoot: AGENT_REPO_DIR }) ?? { isLimit: false, resetAt: null });
1943
+ const ul = engineCohort ? { isLimit: false, resetAt: null } : ulText;
1944
+ if (engineCohort && ulText.isLimit) logSession({ event: "usage_limit_text_ignored", sessionId, engine: "cohort" });
1945
+ if (cohortRefusal) {
1680
1946
  try {
1681
- const rec = rateGuard.recordUsageLimit(RATE_PROVIDER, ul.resetAt, { agentRoot: AGENT_REPO_DIR });
1947
+ const rec = rateGuard.recordCohortRefusal(cohortRefusal, { agentRoot: AGENT_REPO_DIR });
1948
+ logSession({ event: "cohort_refusal_recorded", sessionId, code: cohortRefusal.code, disposition: rec.disposition && rec.disposition.kind, open_until: rec.openUntil });
1949
+ } catch { /* */ }
1950
+ try { const c = getCohortClients(); if (c && c.quota && typeof c.quota.invalidate === "function") c.quota.invalidate(); } catch { /* */ }
1951
+ } else if (ul.isLimit) {
1952
+ try {
1953
+ const rec = rateGuard.recordUsageLimit(breakerProvider, ul.resetAt, { agentRoot: AGENT_REPO_DIR });
1682
1954
  logSession({ event: "usage_limit_recorded", sessionId, open_until: rec.openUntil, reset_at: rec.resetAt });
1683
1955
  } catch { /* */ }
1684
1956
  } else if (rateGuard.classifyStderr(stderr)) {
1685
1957
  try {
1686
- const rec = rateGuard.recordRateLimit(RATE_PROVIDER, { agentRoot: AGENT_REPO_DIR });
1958
+ const rec = rateGuard.recordRateLimit(breakerProvider, { agentRoot: AGENT_REPO_DIR });
1687
1959
  logSession({ event: "rate_limit_recorded", sessionId, open_until: rec.openUntil, consecutive: rec.consecutive429 });
1688
1960
  } catch { /* */ }
1689
1961
  }
@@ -1777,6 +2049,7 @@ function spawnSession(entry) {
1777
2049
  durationMs: Date.now() - startTime,
1778
2050
  exitCode: code,
1779
2051
  decisionId: target.decisionId,
2052
+ billedBy: servedByCohort ? COHORT_LLM_BILLED_BY : null,
1780
2053
  });
1781
2054
  if (!costUsage.ok) {
1782
2055
  logSession({ event: "cost_usage_parse_failed", sessionId, reason: costUsage.reason });
@@ -1961,10 +2234,181 @@ function drainQueue() {
1961
2234
  // Commit: remove from whichever queue it sat at the head of, then spawn.
1962
2235
  if (priorityQueue[0] === next) priorityQueue.shift();
1963
2236
  else normalQueue.shift();
2237
+ // A cohort-llm deferral parks the entry OFF the queue (cohortParked) and
2238
+ // returns false; keep draining — the entries behind it may route elsewhere.
1964
2239
  spawnSession(next);
1965
2240
  }
1966
2241
  }
1967
2242
 
2243
+ // ── cohort-llm pre-spawn gate ────────────────────────────────────────────────
2244
+
2245
+ /**
2246
+ * The gate for one spawn target. Synchronous (spawnSession must register its
2247
+ * child synchronously), so it reads the quota CACHE; a cold cache answers
2248
+ * `{cold:true}` and the caller primes it.
2249
+ */
2250
+ function cohortGateFor(target, engine = readSeatEngine(AGENT_REPO_DIR)) {
2251
+ // A seat on engine cohort is cohort-metered whatever the router picked: the
2252
+ // engine talks to cohort-llm directly, so its spawns are gated even when no
2253
+ // model routing targets the cohort provider. The caller passes the engine it
2254
+ // resolved for this spawn (W4-E1), so the gate and the spawn cannot disagree.
2255
+ if (!isCohortRouted({ engine, target })) return { ok: true, cohort: false };
2256
+ const clients = getCohortClients();
2257
+ if (!clients || !clients.quota) {
2258
+ return {
2259
+ ok: false,
2260
+ cohort: true,
2261
+ deferral: { kind: "retry", until: Date.now() + 30_000, code: "cohort_quota_unconfigured", message: "this seat has no cohort-llm client (no OrgApiKey?)", refusal: null },
2262
+ };
2263
+ }
2264
+ return admitSpawnCached({ engine, target }, { quota: clients.quota, token: clients.token, rateGuard, rateGuardDeps: { agentRoot: AGENT_REPO_DIR } });
2265
+ }
2266
+
2267
+ /**
2268
+ * W4-E1 (design §8.3, A2 "no per-session dollar cap"): an engine-cohort
2269
+ * session's `--max-budget-usd`, from the seat's funded envelope — one day's
2270
+ * binding allowance (lib/runtime/seat-engine.mjs sessionBudgetUsd). No funded
2271
+ * envelope, or a budget guard that cannot say: no cap is invented.
2272
+ * @returns {{maxBudgetUsd:number}|{}}
2273
+ */
2274
+ function engineSessionBudget() {
2275
+ try {
2276
+ const allowance = typeof budgetGuard.dailyAllowance === "function" ? budgetGuard.dailyAllowance({ agentRoot: AGENT_REPO_DIR }) : null;
2277
+ const usd = sessionBudgetUsd(allowance);
2278
+ return usd == null ? {} : { maxBudgetUsd: usd };
2279
+ } catch {
2280
+ return {}; // the guard's own bands still govern the seat
2281
+ }
2282
+ }
2283
+
2284
+ /**
2285
+ * Hand a deferred spawn back without losing it:
2286
+ * inbox PARKED off the queue (cohortParked — never dropped) until the
2287
+ * deferral lifts or the cold cache is primed, then put back at the
2288
+ * head of its queue. Parking, not requeueing at the head: a seat
2289
+ * that routes only SOME work to cohort must keep draining the
2290
+ * entries behind a refused one (they may route to Anthropic), and a
2291
+ * 7d/30d window or a credits hold must not stall the whole inbox.
2292
+ * backlog the item claim is released and the item cools until the deferral
2293
+ * lifts (the on-disk item is retried by the sweep).
2294
+ * A cold cache is primed and the work retried once the reading lands. The
2295
+ * deferral is logged once per hold, not on every re-gate.
2296
+ */
2297
+ function deferSpawnForCohort(entry, gate, target) {
2298
+ const { item, classResult, source } = entry;
2299
+ const d = gate.deferral || {};
2300
+ const until = Number.isFinite(d.until) ? d.until : Date.now() + 30_000;
2301
+ const holdKey = `${gate.cold === true ? "cold" : `${d.code || ""}|${d.kind || ""}|${until}`}`;
2302
+ if (entry.cohortHoldKey !== holdKey) {
2303
+ entry.cohortHoldKey = holdKey;
2304
+ logSession({
2305
+ event: "cohort_deferred",
2306
+ source,
2307
+ code: d.code || null,
2308
+ kind: d.kind || null,
2309
+ until: new Date(until).toISOString(),
2310
+ cold: gate.cold === true,
2311
+ decision_id: (target && target.decisionId) || null,
2312
+ summary: classResult && classResult.summary,
2313
+ });
2314
+ }
2315
+ if (source === "backlog") {
2316
+ if (item && item.id) { try { releaseItemClaim(item.id); } catch { /* */ } }
2317
+ if (!gate.cold) {
2318
+ backlogCooldownUntil.set(backlogKey(item), until);
2319
+ try { saveCooldowns(); } catch { /* */ }
2320
+ }
2321
+ } else {
2322
+ parkForCohort(entry, gate.cold ? null : until);
2323
+ }
2324
+ if (gate.cold) primeCohortQuota(entry);
2325
+ }
2326
+
2327
+ // ── cohort-parked inbox entries ──────────────────────────────────────────────
2328
+ // {entry, until} — `until` null means "waiting on a cold-cache prime".
2329
+ const cohortParked = [];
2330
+ let cohortParkTimer = null;
2331
+ /** Parked entries re-check at least this often (and setTimeout cannot hold a 30d delay). */
2332
+ const COHORT_PARK_RECHECK_MAX_MS = 10 * 60_000;
2333
+ const COHORT_PARK_RECHECK_MIN_MS = 1_000;
2334
+ let cohortInboxPrimeInFlight = false;
2335
+
2336
+ function parkForCohort(entry, until) {
2337
+ cohortParked.push({ entry, until });
2338
+ armCohortRelease();
2339
+ }
2340
+
2341
+ function armCohortRelease() {
2342
+ if (cohortParkTimer) { clearTimeout(cohortParkTimer); cohortParkTimer = null; }
2343
+ const timed = cohortParked.filter((p) => p.until !== null);
2344
+ if (timed.length === 0) return;
2345
+ const earliest = Math.min(...timed.map((p) => p.until));
2346
+ const delay = Math.max(COHORT_PARK_RECHECK_MIN_MS, Math.min(COHORT_PARK_RECHECK_MAX_MS, earliest - Date.now()));
2347
+ cohortParkTimer = setTimeout(() => {
2348
+ cohortParkTimer = null;
2349
+ try { releaseCohortParked(); } catch (err) { console.warn(`[dispatcher] cohort-llm release failed: ${err.message}`); }
2350
+ }, delay);
2351
+ if (cohortParkTimer && typeof cohortParkTimer.unref === "function") cohortParkTimer.unref();
2352
+ }
2353
+
2354
+ /**
2355
+ * Put parked entries back at the head of their queues (original order) and
2356
+ * drain: `cold:true` releases the entries waiting on a prime; otherwise the
2357
+ * ones whose deferral has lifted. They are gated again on the way out.
2358
+ */
2359
+ function releaseCohortParked({ cold = false } = {}) {
2360
+ const now = Date.now();
2361
+ const back = [];
2362
+ const keep = [];
2363
+ for (const p of cohortParked) {
2364
+ const due = cold ? p.until === null : p.until !== null && p.until <= now;
2365
+ (due ? back : keep).push(p);
2366
+ }
2367
+ cohortParked.length = 0;
2368
+ cohortParked.push(...keep);
2369
+ for (let i = back.length - 1; i >= 0; i--) {
2370
+ const e = back[i].entry;
2371
+ const high = e.classResult && (e.classResult.priority === "critical" || e.classResult.priority === "high");
2372
+ (high ? priorityQueue : normalQueue).unshift(e);
2373
+ }
2374
+ armCohortRelease();
2375
+ if (back.length > 0) drainQueue();
2376
+ }
2377
+
2378
+ /** Fetch a quota reading, then retry the deferred work once. */
2379
+ function primeCohortQuota(entry) {
2380
+ const clients = getCohortClients();
2381
+ if (!clients || !clients.quota) return;
2382
+ if (entry.source !== "backlog") {
2383
+ if (cohortInboxPrimeInFlight) return; // one read releases every cold-parked entry
2384
+ cohortInboxPrimeInFlight = true;
2385
+ }
2386
+ Promise.resolve()
2387
+ .then(() => clients.quota.canAdmit())
2388
+ .catch(() => null)
2389
+ .then(() => {
2390
+ try {
2391
+ if (entry.source === "backlog") {
2392
+ // One re-dispatch per prime. A failed read is cached too, so the
2393
+ // second gate answers with a real deferral instead of cold again.
2394
+ if (entry.cohortPrimed) return;
2395
+ dispatch(entry.prompt, entry.item, entry.classResult, "backlog", { obligationKey: entry.obligationKey, onClose: entry.onClose, onSpawn: entry.onSpawn, cohortPrimed: true });
2396
+ } else {
2397
+ cohortInboxPrimeInFlight = false;
2398
+ releaseCohortParked({ cold: true });
2399
+ }
2400
+ } catch (err) {
2401
+ cohortInboxPrimeInFlight = false;
2402
+ console.warn(`[dispatcher] cohort-llm retry after quota prime failed: ${err.message}`);
2403
+ }
2404
+ });
2405
+ }
2406
+
2407
+ /** Test seam: run one drain pass now (the queue is otherwise drained by closes and timers). */
2408
+ export function _drainQueueForTests() {
2409
+ drainQueue();
2410
+ }
2411
+
1968
2412
  /** Get current status for health checks */
1969
2413
  export function getStatus() {
1970
2414
  return {
@@ -1972,6 +2416,8 @@ export function getStatus() {
1972
2416
  max_concurrent: MAX_CONCURRENT,
1973
2417
  priority_queue_length: priorityQueue.length,
1974
2418
  normal_queue_length: normalQueue.length,
2419
+ // Inbox entries held off the queues by a cohort-llm deferral (never dropped).
2420
+ cohort_parked_length: cohortParked.length,
1975
2421
  sessions: Array.from(activeSessions.entries()).map(([id, s]) => ({
1976
2422
  id,
1977
2423
  model: s.model,