@cohortapp/agent-sdk 2.17.0 → 2.18.5

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 (531) 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-slots.mjs +234 -0
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/config.mjs +2 -0
  25. package/lib/collective/global-config.mjs +63 -1
  26. package/lib/collective/loop-guard.mjs +155 -0
  27. package/lib/collective/presence.mjs +142 -5
  28. package/lib/comms/send-gate.mjs +559 -1
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/failover.mjs +67 -16
  138. package/lib/model-router/llm-task.mjs +39 -3
  139. package/lib/model-router/resolve.mjs +89 -3
  140. package/lib/model-router/spawn.mjs +46 -47
  141. package/lib/model-router/taxonomy.mjs +126 -4
  142. package/lib/org/cost-sync.mjs +141 -11
  143. package/lib/org/inbound/broadcast.mjs +289 -0
  144. package/lib/org/inbound/collective.mjs +375 -0
  145. package/lib/org/inbound/directedness.mjs +96 -8
  146. package/lib/org/inbound/facts.mjs +78 -2
  147. package/lib/org/inbound/project.mjs +22 -0
  148. package/lib/org/inbound/surfaces.mjs +14 -0
  149. package/lib/org/llm-token.mjs +879 -0
  150. package/lib/org/mesh.mjs +61 -0
  151. package/lib/org/messaging.mjs +3 -1
  152. package/lib/org/protocol.checksum +1 -1
  153. package/lib/org/protocol.mjs +15 -0
  154. package/lib/org/quota.mjs +520 -0
  155. package/lib/org/tool-surface.mjs +104 -16
  156. package/lib/org/ui-parity.mjs +16 -1
  157. package/lib/org/work-ledger.mjs +37 -6
  158. package/lib/rate-guard.mjs +114 -1
  159. package/lib/resource-governor.mjs +41 -6
  160. package/lib/runtime/adapter.mjs +833 -0
  161. package/lib/runtime/child-env.mjs +191 -0
  162. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  163. package/lib/runtime/seat-engine.mjs +162 -0
  164. package/lib/session/ask-ledger.mjs +271 -0
  165. package/lib/session/current-work.mjs +676 -0
  166. package/lib/session/feed-core.mjs +40 -3
  167. package/lib/session/launch-args.mjs +56 -4
  168. package/lib/session/status-summary.mjs +26 -9
  169. package/lib/session/upgrade-notice.mjs +42 -0
  170. package/lib/setup/claude-probe.mjs +117 -13
  171. package/lib/setup/enrich.mjs +13 -10
  172. package/lib/setup/sections/model.mjs +39 -13
  173. package/lib/telemetry/collect.mjs +229 -11
  174. package/lib/upgrade/ignored-drift.mjs +105 -0
  175. package/lib/voice/post-call-brief.mjs +30 -17
  176. package/package.json +13 -3
  177. package/plugins/maestro-skills/skills/board-work.md +5 -0
  178. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  179. package/plugins/maestro-skills/skills/main-session.md +18 -7
  180. package/scaffold/config/collective.yaml +7 -0
  181. package/scripts/ci/check-durable-write-seam.mjs +3 -1
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +142 -19
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +51 -37
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +595 -149
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/maestro-daemon.mjs +11 -0
  198. package/scripts/daemon/prompt-builder.mjs +24 -0
  199. package/scripts/daemon/responder.mjs +246 -79
  200. package/scripts/daemon/sdk-version.mjs +98 -16
  201. package/scripts/eval/probe-gateway.mjs +635 -0
  202. package/scripts/eval/replay/extract.mjs +270 -0
  203. package/scripts/eval/replay/grade.mjs +260 -0
  204. package/scripts/eval/replay/lib/config.mjs +50 -0
  205. package/scripts/eval/replay/lib/effects.mjs +65 -0
  206. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  207. package/scripts/eval/replay/lib/judge.mjs +72 -0
  208. package/scripts/eval/replay/lib/redact.mjs +136 -0
  209. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  210. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  211. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  212. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  213. package/scripts/eval/replay/report.mjs +185 -0
  214. package/scripts/eval/replay/run.mjs +404 -0
  215. package/scripts/fleet/rollout.mjs +1151 -0
  216. package/scripts/hooks/pre-send-audit.sh +36 -245
  217. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  218. package/scripts/hooks/validate-state-yaml.sh +190 -0
  219. package/scripts/huddle/huddle-llm.mjs +361 -0
  220. package/scripts/huddle/huddle-server.mjs +46 -121
  221. package/scripts/local-triggers/autoupdate.sh +465 -81
  222. package/scripts/local-triggers/run-trigger.sh +13 -0
  223. package/scripts/maintenance/pin-integrity.mjs +364 -0
  224. package/scripts/poll-slack-events.sh +41 -9
  225. package/scripts/poller/slack-socket-mode.mjs +28 -3
  226. package/scripts/session/supervisor.mjs +80 -13
  227. package/scripts/spawn-session.sh +13 -0
  228. package/bin/maestro.test.mjs +0 -1574
  229. package/lib/action-executor.test.mjs +0 -871
  230. package/lib/archetype.test.mjs +0 -132
  231. package/lib/assurance/plan-note.test.mjs +0 -234
  232. package/lib/assurance/room-budget.test.mjs +0 -486
  233. package/lib/assurance/tier.test.mjs +0 -174
  234. package/lib/autonomy.test.mjs +0 -66
  235. package/lib/backlog.test.mjs +0 -302
  236. package/lib/backup/policy.test.mjs +0 -305
  237. package/lib/budget-escalate.test.mjs +0 -232
  238. package/lib/budget-guard.envelope.test.mjs +0 -476
  239. package/lib/budget-guard.test.mjs +0 -427
  240. package/lib/cadence-bus-requeue.test.mjs +0 -83
  241. package/lib/cadence-bus-schedule.test.mjs +0 -194
  242. package/lib/cadence-bus.test.mjs +0 -720
  243. package/lib/cadences.test.mjs +0 -230
  244. package/lib/capability/inventory.test.mjs +0 -232
  245. package/lib/capability.test.mjs +0 -78
  246. package/lib/channels/base-adapter.test.mjs +0 -590
  247. package/lib/channels/channels.test.mjs +0 -371
  248. package/lib/channels/contract.test.mjs +0 -162
  249. package/lib/channels/inbox-item.test.mjs +0 -368
  250. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  251. package/lib/channels/pairing.test.mjs +0 -270
  252. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  253. package/lib/channels/slack-adapter.test.mjs +0 -212
  254. package/lib/channels/telegram-adapter.test.mjs +0 -306
  255. package/lib/channels/voice/adapter.test.mjs +0 -278
  256. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  257. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  258. package/lib/charter.test.mjs +0 -89
  259. package/lib/claude-bin.test.mjs +0 -131
  260. package/lib/cli/board.test.mjs +0 -227
  261. package/lib/cli/design.test.mjs +0 -270
  262. package/lib/cli/doctor-checks.test.mjs +0 -336
  263. package/lib/cli/global-setup-extras.test.mjs +0 -462
  264. package/lib/cli/inbox.test.mjs +0 -230
  265. package/lib/cli/session-ack.test.mjs +0 -63
  266. package/lib/cli/session.test.mjs +0 -613
  267. package/lib/collective/capture.test.mjs +0 -121
  268. package/lib/collective/cards.test.mjs +0 -114
  269. package/lib/collective/config.test.mjs +0 -123
  270. package/lib/collective/global-config.test.mjs +0 -220
  271. package/lib/collective/global-skills.test.mjs +0 -126
  272. package/lib/collective/presence.test.mjs +0 -95
  273. package/lib/collective/recall.test.mjs +0 -116
  274. package/lib/collective/vendor-skills.test.mjs +0 -306
  275. package/lib/comms/send-gate.test.mjs +0 -770
  276. package/lib/comms.test.mjs +0 -41
  277. package/lib/context/budget.test.mjs +0 -252
  278. package/lib/context/history-scope.test.mjs +0 -79
  279. package/lib/cost/ledger-row.test.mjs +0 -183
  280. package/lib/design/design-md.test.mjs +0 -318
  281. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  282. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  283. package/lib/design/fixtures/foundation.json +0 -133
  284. package/lib/design/refresh-gate.test.mjs +0 -144
  285. package/lib/design/write.test.mjs +0 -241
  286. package/lib/diagnostics/alerts.test.mjs +0 -318
  287. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  288. package/lib/diagnostics/counters.test.mjs +0 -206
  289. package/lib/diagnostics/events.test.mjs +0 -290
  290. package/lib/diagnostics/otel.test.mjs +0 -196
  291. package/lib/diagnostics/trace.test.mjs +0 -251
  292. package/lib/env-compat.test.mjs +0 -104
  293. package/lib/execution/disposition.test.mjs +0 -553
  294. package/lib/execution/drive.test.mjs +0 -270
  295. package/lib/execution/effects.test.mjs +0 -344
  296. package/lib/execution/intake.test.mjs +0 -389
  297. package/lib/execution/journal.test.mjs +0 -261
  298. package/lib/execution/match.test.mjs +0 -235
  299. package/lib/execution/pipeline.test.mjs +0 -392
  300. package/lib/execution/route.test.mjs +0 -186
  301. package/lib/execution/surface-policy.test.mjs +0 -162
  302. package/lib/fs-atomic.test.mjs +0 -72
  303. package/lib/fs-ownership.test.mjs +0 -158
  304. package/lib/goals/admission.test.mjs +0 -164
  305. package/lib/goals/classify.test.mjs +0 -167
  306. package/lib/goals/collaborate.test.mjs +0 -336
  307. package/lib/goals/gaps.test.mjs +0 -284
  308. package/lib/goals/loop.test.mjs +0 -845
  309. package/lib/hooks/bus.test.mjs +0 -387
  310. package/lib/identity/persona.test.mjs +0 -142
  311. package/lib/kpi-sensors.test.mjs +0 -278
  312. package/lib/kpi.test.mjs +0 -244
  313. package/lib/learning/config.test.mjs +0 -75
  314. package/lib/learning/counters.test.mjs +0 -69
  315. package/lib/learning/curator-consolidate.test.mjs +0 -238
  316. package/lib/learning/curator.test.mjs +0 -106
  317. package/lib/learning/reflect.test.mjs +0 -0
  318. package/lib/learning/session-index.test.mjs +0 -125
  319. package/lib/learning/skill-writer.test.mjs +0 -210
  320. package/lib/mandate/audit.test.mjs +0 -195
  321. package/lib/mandate/contract.test.mjs +0 -185
  322. package/lib/mandate/derive.test.mjs +0 -274
  323. package/lib/mandate/model.test.mjs +0 -164
  324. package/lib/mandate/refresh.test.mjs +0 -389
  325. package/lib/mcp/server.test.mjs +0 -426
  326. package/lib/model-router/auth-profiles.test.mjs +0 -580
  327. package/lib/model-router/catalog.test.mjs +0 -385
  328. package/lib/model-router/economics.test.mjs +0 -438
  329. package/lib/model-router/failover.test.mjs +0 -439
  330. package/lib/model-router/health.test.mjs +0 -338
  331. package/lib/model-router/integration-coverage.test.mjs +0 -831
  332. package/lib/model-router/integration.test.mjs +0 -564
  333. package/lib/model-router/ledger.test.mjs +0 -415
  334. package/lib/model-router/llm-task.test.mjs +0 -392
  335. package/lib/model-router/org-credentials.test.mjs +0 -265
  336. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  337. package/lib/model-router/reconcile.test.mjs +0 -316
  338. package/lib/model-router/repair.test.mjs +0 -180
  339. package/lib/model-router/spawn.test.mjs +0 -446
  340. package/lib/model-router/taxonomy.test.mjs +0 -410
  341. package/lib/model-router.test.mjs +0 -1207
  342. package/lib/org/activity.test.mjs +0 -134
  343. package/lib/org/approvals.test.mjs +0 -216
  344. package/lib/org/awareness.test.mjs +0 -159
  345. package/lib/org/board-mine-cache.test.mjs +0 -53
  346. package/lib/org/board.test.mjs +0 -187
  347. package/lib/org/bootstrap-context.test.mjs +0 -153
  348. package/lib/org/client.test.mjs +0 -1206
  349. package/lib/org/cohort-client.test.mjs +0 -126
  350. package/lib/org/cost-sync.test.mjs +0 -153
  351. package/lib/org/doctor.test.mjs +0 -346
  352. package/lib/org/engagement-ledger.test.mjs +0 -112
  353. package/lib/org/engagement.test.mjs +0 -739
  354. package/lib/org/handoff.test.mjs +0 -269
  355. package/lib/org/inbound/directedness.test.mjs +0 -668
  356. package/lib/org/inbound/facts.test.mjs +0 -471
  357. package/lib/org/inbound/hydrate.test.mjs +0 -908
  358. package/lib/org/inbound/index.test.mjs +0 -429
  359. package/lib/org/inbound/project.test.mjs +0 -287
  360. package/lib/org/integration-tools.test.mjs +0 -160
  361. package/lib/org/keys.test.mjs +0 -92
  362. package/lib/org/knowledge.test.mjs +0 -326
  363. package/lib/org/leases.test.mjs +0 -235
  364. package/lib/org/mesh-directives.test.mjs +0 -110
  365. package/lib/org/mesh-integration.test.mjs +0 -127
  366. package/lib/org/mesh.test.mjs +0 -400
  367. package/lib/org/messaging.test.mjs +0 -471
  368. package/lib/org/param-contract.test.mjs +0 -477
  369. package/lib/org/policy.test.mjs +0 -237
  370. package/lib/org/protocol.checksum.test.mjs +0 -90
  371. package/lib/org/protocol.test.mjs +0 -323
  372. package/lib/org/push.test.mjs +0 -792
  373. package/lib/org/registry.test.mjs +0 -100
  374. package/lib/org/resource-tools.test.mjs +0 -361
  375. package/lib/org/tool-access.test.mjs +0 -144
  376. package/lib/org/tool-surface-integration.test.mjs +0 -120
  377. package/lib/org/tool-surface.test.mjs +0 -1268
  378. package/lib/org/typing.test.mjs +0 -291
  379. package/lib/org/ui-parity.test.mjs +0 -560
  380. package/lib/org/verify.test.mjs +0 -194
  381. package/lib/org/work-ledger.test.mjs +0 -273
  382. package/lib/plan/adoption-e2e.test.mjs +0 -366
  383. package/lib/plan/budget-enforcement.test.mjs +0 -400
  384. package/lib/plan/compile.test.mjs +0 -382
  385. package/lib/plan/emit.test.mjs +0 -269
  386. package/lib/plan/explain.test.mjs +0 -188
  387. package/lib/prompts/parallelism.test.mjs +0 -177
  388. package/lib/rag/rag.test.mjs +0 -505
  389. package/lib/rate-guard.test.mjs +0 -272
  390. package/lib/reactive-gate.test.mjs +0 -57
  391. package/lib/render.test.mjs +0 -68
  392. package/lib/resource-governor.test.mjs +0 -488
  393. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  394. package/lib/scheduling/jitter.test.mjs +0 -140
  395. package/lib/secrets/broker.test.mjs +0 -280
  396. package/lib/secrets/providers.test.mjs +0 -274
  397. package/lib/security/audit-engine.test.mjs +0 -424
  398. package/lib/security/coerce-args.test.mjs +0 -281
  399. package/lib/security/dangerous-tools.test.mjs +0 -68
  400. package/lib/security/external-content.test.mjs +0 -84
  401. package/lib/security/redact.test.mjs +0 -441
  402. package/lib/security/secret-equal.test.mjs +0 -55
  403. package/lib/session/config.test.mjs +0 -92
  404. package/lib/session/feed-core.test.mjs +0 -198
  405. package/lib/session/first-run.test.mjs +0 -121
  406. package/lib/session/frontdoor.test.mjs +0 -205
  407. package/lib/session/handoffs.test.mjs +0 -183
  408. package/lib/session/identity.test.mjs +0 -180
  409. package/lib/session/inbox-claims.test.mjs +0 -286
  410. package/lib/session/launch-args.test.mjs +0 -157
  411. package/lib/session/liveness.test.mjs +0 -100
  412. package/lib/session/status-summary.test.mjs +0 -118
  413. package/lib/session-permissions.test.mjs +0 -120
  414. package/lib/setup/claude-probe.test.mjs +0 -187
  415. package/lib/setup/completeness.test.mjs +0 -110
  416. package/lib/setup/context-pack.test.mjs +0 -89
  417. package/lib/setup/enrich.test.mjs +0 -115
  418. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  419. package/lib/setup/integration.test.mjs +0 -162
  420. package/lib/setup/io.test.mjs +0 -77
  421. package/lib/setup/runner.test.mjs +0 -132
  422. package/lib/setup/sections/identity.test.mjs +0 -234
  423. package/lib/setup/sections/inventory.test.mjs +0 -198
  424. package/lib/setup/sections/learning.test.mjs +0 -81
  425. package/lib/setup/sections/mandate.test.mjs +0 -388
  426. package/lib/setup/sections/messaging.test.mjs +0 -127
  427. package/lib/setup/sections/model.test.mjs +0 -240
  428. package/lib/setup/sections/org.test.mjs +0 -346
  429. package/lib/setup/sections/orgmail.test.mjs +0 -118
  430. package/lib/setup/sections/recovery.test.mjs +0 -98
  431. package/lib/setup/sections/subagents.test.mjs +0 -429
  432. package/lib/setup/sections/verify.test.mjs +0 -175
  433. package/lib/setup/sot.test.mjs +0 -81
  434. package/lib/setup/state.test.mjs +0 -115
  435. package/lib/singleton.test.mjs +0 -151
  436. package/lib/subagents/cli.test.mjs +0 -389
  437. package/lib/subagents/client.test.mjs +0 -309
  438. package/lib/subagents/gap.test.mjs +0 -234
  439. package/lib/subagents/lock.test.mjs +0 -248
  440. package/lib/subagents/manifest.test.mjs +0 -175
  441. package/lib/subagents/refs.test.mjs +0 -204
  442. package/lib/subagents/resolve.test.mjs +0 -422
  443. package/lib/subagents/schema.test.mjs +0 -328
  444. package/lib/telemetry/alerts.test.mjs +0 -109
  445. package/lib/telemetry/collect.test.mjs +0 -1274
  446. package/lib/tool-definitions-integration.test.mjs +0 -83
  447. package/lib/tool-definitions.test.mjs +0 -437
  448. package/lib/upgrade/global-refresh.test.mjs +0 -65
  449. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  450. package/lib/upgrade/post-steps.test.mjs +0 -200
  451. package/lib/upgrade/verify.test.mjs +0 -164
  452. package/lib/util/fetch-timeout.test.mjs +0 -202
  453. package/lib/util/reconnect.test.mjs +0 -369
  454. package/lib/util/unhandled.test.mjs +0 -216
  455. package/lib/voice/outbound.test.mjs +0 -69
  456. package/lib/voice/session-rotation.test.mjs +0 -114
  457. package/lib/voice/stt.test.mjs +0 -226
  458. package/lib/voice/voice.test.mjs +0 -990
  459. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  460. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  461. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  462. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  463. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  464. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  465. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  466. package/scripts/ci/check.test.mjs +0 -194
  467. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  468. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  469. package/scripts/collective/hook-runner.test.mjs +0 -173
  470. package/scripts/cost/fleet-digest.test.mjs +0 -207
  471. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  472. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  473. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  474. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  475. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  476. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  477. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  478. package/scripts/daemon/assurance.test.mjs +0 -1791
  479. package/scripts/daemon/board-mirror.test.mjs +0 -165
  480. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  481. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  482. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  483. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  484. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  485. package/scripts/daemon/classifier.test.mjs +0 -266
  486. package/scripts/daemon/classify-kind.test.mjs +0 -40
  487. package/scripts/daemon/context-compiler.test.mjs +0 -406
  488. package/scripts/daemon/deliver.test.mjs +0 -564
  489. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  490. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  491. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  492. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  493. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  494. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  495. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  496. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  497. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  498. package/scripts/daemon/integration.test.mjs +0 -149
  499. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  500. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  501. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  502. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  503. package/scripts/daemon/responder-cost.test.mjs +0 -68
  504. package/scripts/daemon/responder-history.test.mjs +0 -221
  505. package/scripts/daemon/sdk-version.test.mjs +0 -31
  506. package/scripts/daemon/session-lock.test.mjs +0 -252
  507. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  508. package/scripts/daemon/typing-registry.test.mjs +0 -102
  509. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  510. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  511. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  512. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  513. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  514. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  515. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  516. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  517. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  518. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  519. package/scripts/poller-launchd/install.test.mjs +0 -243
  520. package/scripts/restore-from-backup.test.mjs +0 -181
  521. package/scripts/session/feed.test.mjs +0 -196
  522. package/scripts/session/supervisor-sh.test.mjs +0 -218
  523. package/scripts/session/supervisor.test.mjs +0 -482
  524. package/scripts/setup/configure-macos.test.mjs +0 -306
  525. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  526. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  527. package/scripts/setup/generate-capability.test.mjs +0 -134
  528. package/scripts/setup/init-agent.test.mjs +0 -370
  529. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  530. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  531. 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,