@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
@@ -0,0 +1,349 @@
1
+ /**
2
+ * lib/engine/wire/stall.mjs — inter-event idle timeout and stall diagnostics
3
+ * for a gateway stream (CF-156, client half).
4
+ *
5
+ * WHY THIS EXISTS. The W13 live measurement (docs/engine/eval.md §3) found
6
+ * `cohort-reason` stalling twice in 25 requests: one engine call hung past a
7
+ * 120 s client timeout (`req_fd528e0c1ab3f8947a77bb66`) and one raw Anthropic
8
+ * call took 45.5 s (`req_449cafa064e187c22993991e`), against ~1.5 s normally.
9
+ * Root cause was NOT diagnosed, and a stall left almost nothing behind to
10
+ * diagnose it with. Two occurrences in 25 requests is not a reproduction
11
+ * budget, so this module's job is to make the THIRD occurrence self-evident,
12
+ * and to end it inside the task budget it runs under instead of eating all of
13
+ * it. (Which budget, and why 180 s is not the dispatcher's, is set out on
14
+ * DEFAULT_STREAM_IDLE_TIMEOUT_MS below.)
15
+ *
16
+ * Two pieces, both usable without a socket:
17
+ *
18
+ * · `withIdleTimeout` — wraps an async iterable of SSE frames and ends it
19
+ * when no frame arrives for `idleMs`. The clock is INTER-EVENT, not
20
+ * total: a long completion that keeps streaming is never cut off, and a
21
+ * stream that goes quiet is. It closes the underlying iterator on the way
22
+ * out, so the fetch body is released rather than left reading.
23
+ *
24
+ * · `createStallWatch` — counts what a diagnosis needs (time to first byte,
25
+ * time to first event, time since the last event, the last event type,
26
+ * whether the terminal `cohort` frame had arrived) and builds a record
27
+ * ONLY when asked. Per frame it touches three numbers and a string; the
28
+ * record is built on trouble, never on the happy path.
29
+ *
30
+ * On keep-alive pings. The gateway sends `:` comment lines every 15 s and
31
+ * `readSse` drops them, by design — they are the SSE grammar's liveness
32
+ * filler, not events. So this timer measures silence in EVENTS, which is the
33
+ * thing that actually stalled: a gateway that keeps pinging while no content
34
+ * ever arrives is precisely the failure W13 could not tell apart from a slow
35
+ * model. The cost of that choice is that a tier which legitimately thinks for
36
+ * longer than `idleMs` without emitting anything would be cut off, which is
37
+ * why the default sits far above every latency yet measured and why it is
38
+ * named configuration rather than a constant.
39
+ *
40
+ * @module lib/engine/wire/stall
41
+ */
42
+
43
+ /**
44
+ * Longest silence before a stream is called stalled — and, spent across a whole
45
+ * call rather than re-armed per attempt, the client's bound on a gateway that
46
+ * never speaks.
47
+ *
48
+ * 60 s, derived rather than chosen — and derived from a number this repo owns,
49
+ * which the first cut of this module did not:
50
+ *
51
+ * · WHAT ACTUALLY BOUNDS A TURN. Earlier drafts of this module, and
52
+ * docs/engine/eval.md §3, called 180 s "the dispatcher's task timeout". It
53
+ * is not. The live dispatcher times a task out at 10 min to 12 h depending
54
+ * on model and source (`scripts/daemon/dispatcher.mjs`,
55
+ * `SONNET_INBOX_TIMEOUT` … `OPUS_BACKLOG_TIMEOUT`). The real 180 s in this
56
+ * repo is `DEFAULT_TASK_TIMEOUT_MS` (`scripts/eval/replay/lib/config.mjs`),
57
+ * the per-task budget of the design §8.4.2 replay eval — and the TIGHTEST
58
+ * budget the engine runs a task under here. Deriving against the tightest
59
+ * is the conservative choice, and unlike "the dispatcher's 180 s" it is a
60
+ * constant this repo owns, so the test pins it by import instead of
61
+ * restating a literal.
62
+ * · THE BOUND THIS BUYS. A stalled call ends within 2 × this value, never
63
+ * more: either the silence budget is spent before acceptance and the call
64
+ * returns without resending, or the gateway accepts inside it and then one
65
+ * silent inter-event gap ends the stream. 120 s at the default — inside the
66
+ * 180 s replay budget, and far inside the dispatcher's 10-minute floor.
67
+ * · ABOVE EVERY REAL LATENCY. The worst legitimate latency ever recorded
68
+ * against this gateway is W13's 45.5 s WHOLE call, and this budget is per
69
+ * inter-event gap, which is strictly smaller. It is also well under the
70
+ * 120 s client timeout that a real stall already ran past.
71
+ *
72
+ * It is NOT a bound on a healthy call's total duration, by design: a stream
73
+ * that keeps emitting events is never cut off, however long it runs.
74
+ */
75
+ export const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 60_000;
76
+
77
+ /**
78
+ * The band a configured budget is held to.
79
+ *
80
+ * Below the floor the guard would cut off healthy streams; above the ceiling it
81
+ * would exceed the very task budget the default is derived from, while still
82
+ * looking armed. Both are silent failures, so a value outside the band is
83
+ * clamped and `idleTimeoutNote` says so out loud. `0` (explicitly off) is not
84
+ * clamped — disabling the guard is a deliberate, documented escape hatch.
85
+ */
86
+ export const MIN_STREAM_IDLE_TIMEOUT_MS = 5_000;
87
+ /** @see MIN_STREAM_IDLE_TIMEOUT_MS */
88
+ export const MAX_STREAM_IDLE_TIMEOUT_MS = 120_000;
89
+
90
+ /**
91
+ * A stream that finished but took longer than this is recorded as `slow`.
92
+ *
93
+ * 20 s: above the ~1.5 s typical turn and above the 7.9 s raw
94
+ * `cohort-reason`/OpenAI call W13 saw, but below the 45.5 s outlier — so the
95
+ * outlier would have left a record, and ordinary traffic leaves none.
96
+ */
97
+ export const DEFAULT_SLOW_STREAM_MS = 20_000;
98
+
99
+ /**
100
+ * Thrown by `withIdleTimeout` when the silence budget is spent. A marker type
101
+ * so the HTTP edge can tell a stall from a socket fault without matching on a
102
+ * message.
103
+ */
104
+ export class StreamIdleTimeoutSignal extends Error {
105
+ /** @param {number} idleMs */
106
+ constructor(idleMs) {
107
+ super(`no SSE event for ${idleMs}ms`);
108
+ this.name = "StreamIdleTimeoutSignal";
109
+ /** @type {number} */
110
+ this.idleMs = idleMs;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Parse a configured idle timeout. Accepts milliseconds; `0`, `off`, `false`
116
+ * and `none` disable the timer (0). Anything unparseable keeps the default, so
117
+ * a typo in the environment loosens nothing silently — it just does not apply.
118
+ * A finite value outside {@link MIN_STREAM_IDLE_TIMEOUT_MS} …
119
+ * {@link MAX_STREAM_IDLE_TIMEOUT_MS} is CLAMPED into the band: a 1 ms budget
120
+ * that cuts off every healthy stream and a 10-minute one that outlives the task
121
+ * it is protecting both look armed and are both useless. Pair with
122
+ * {@link idleTimeoutNote} so the adjustment is visible rather than silent.
123
+ *
124
+ * @param {unknown} value
125
+ * @param {number} [fallback]
126
+ * @returns {number} milliseconds, or 0 for "no timeout"
127
+ */
128
+ export function parseIdleTimeoutMs(value, fallback = DEFAULT_STREAM_IDLE_TIMEOUT_MS) {
129
+ const raw = String(value ?? "").trim().toLowerCase();
130
+ if (raw === "") return fallback;
131
+ if (["0", "off", "false", "no", "none"].includes(raw)) return 0;
132
+ const n = Number(raw);
133
+ if (!Number.isFinite(n) || n <= 0) return fallback;
134
+ return Math.min(MAX_STREAM_IDLE_TIMEOUT_MS, Math.max(MIN_STREAM_IDLE_TIMEOUT_MS, Math.round(n)));
135
+ }
136
+
137
+ /**
138
+ * The one line to print when a configured budget was not taken at face value —
139
+ * either it did not parse, or it was clamped into the band. `null` when the
140
+ * value was empty, an explicit "off", or applied exactly as written.
141
+ *
142
+ * Separate from {@link parseIdleTimeoutMs} so that function stays pure and
143
+ * total: the parse has no seam to warn through, and the CLI does.
144
+ *
145
+ * @param {unknown} value the raw configured value
146
+ * @param {number} [applied] what {@link parseIdleTimeoutMs} returned for it
147
+ * @returns {string|null}
148
+ */
149
+ export function idleTimeoutNote(value, applied = parseIdleTimeoutMs(value)) {
150
+ const raw = String(value ?? "").trim();
151
+ if (raw === "") return null;
152
+ const lower = raw.toLowerCase();
153
+ if (["0", "off", "false", "no", "none"].includes(lower)) return null;
154
+ const n = Number(lower);
155
+ if (!Number.isFinite(n) || n <= 0) {
156
+ return `COHORT_LLM_STREAM_IDLE_TIMEOUT_MS=${raw} is not a number of milliseconds; using ${applied}ms`;
157
+ }
158
+ if (Math.round(n) === applied) return null;
159
+ return (
160
+ `COHORT_LLM_STREAM_IDLE_TIMEOUT_MS=${raw} is outside the ` +
161
+ `${MIN_STREAM_IDLE_TIMEOUT_MS}–${MAX_STREAM_IDLE_TIMEOUT_MS}ms band; using ${applied}ms`
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Yield an async iterable's values, failing with {@link StreamIdleTimeoutSignal}
167
+ * when more than `idleMs` passes between them.
168
+ *
169
+ * The underlying iterator is closed in a `finally`, so a stalled fetch body is
170
+ * cancelled rather than left pending. A non-positive or non-finite `idleMs`
171
+ * passes straight through with no timer at all.
172
+ *
173
+ * @template T
174
+ * @param {AsyncIterable<T>} source
175
+ * @param {number} idleMs
176
+ * @param {{ setTimer?: typeof setTimeout, clearTimer?: typeof clearTimeout }} [deps]
177
+ * @returns {AsyncGenerator<T>}
178
+ */
179
+ export async function* withIdleTimeout(source, idleMs, deps = {}) {
180
+ if (!Number.isFinite(idleMs) || idleMs <= 0) {
181
+ yield* source;
182
+ return;
183
+ }
184
+ const setTimer = deps.setTimer ?? setTimeout;
185
+ const clearTimer = deps.clearTimer ?? clearTimeout;
186
+ const it = source[Symbol.asyncIterator]();
187
+ try {
188
+ for (;;) {
189
+ const next = it.next();
190
+ // The loser of the race is orphaned: keep it from becoming an unhandled
191
+ // rejection when the timer wins and the socket errors afterwards.
192
+ next.catch(() => {});
193
+ /** @type {any} */
194
+ let timer;
195
+ const idle = new Promise((_resolve, reject) => {
196
+ timer = setTimer(() => reject(new StreamIdleTimeoutSignal(idleMs)), idleMs);
197
+ });
198
+ idle.catch(() => {});
199
+ let step;
200
+ try {
201
+ step = await Promise.race([next, idle]);
202
+ } finally {
203
+ clearTimer(timer);
204
+ }
205
+ if (step.done) return;
206
+ yield step.value;
207
+ }
208
+ } finally {
209
+ // NEVER await this. An async generator suspended at an un-resolving
210
+ // `await` — which is exactly what a stalled fetch body is — only processes
211
+ // `return()` when it next resumes, so awaiting here would hang on the very
212
+ // stall just detected. Ask it to close and move on; releasing the socket
213
+ // is the caller's job (wire/http.mjs cancels the response body).
214
+ try {
215
+ void Promise.resolve(it.return?.()).catch(() => {});
216
+ } catch {
217
+ /* a source with no return() needs no closing */
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * @typedef {Object} StreamDiagnostic
224
+ * @property {'stalled'|'failed'|'slow'} outcome
225
+ * @property {string|null} requestId x-cohort-request-id (or the cohort frame's)
226
+ * @property {string|null} wire 'openai' | 'anthropic'
227
+ * @property {string|null} tier the model tier asked for
228
+ * @property {boolean} accepted the gateway had returned 2xx
229
+ * @property {number|null} ttfbMs request start → response headers
230
+ * @property {number|null} firstEventMs response headers → first SSE event
231
+ * @property {number|null} sinceLastEventMs last SSE event → now (the silence)
232
+ * @property {number} events SSE events seen (the cohort frame included)
233
+ * @property {string|null} lastEventType the last event name seen
234
+ * @property {boolean} cohortFrameSeen the terminal `event: cohort` frame had arrived
235
+ * @property {number} totalMs THIS ATTEMPT's start → now
236
+ * @property {number} callMs the whole call's start → now (every attempt and backoff sleep included)
237
+ * @property {number} attempts attempts made on this call (1 unless the gateway was retried)
238
+ * @property {string|null} code the wire error's code, when it failed
239
+ * @property {string|null} message the wire error's message, when it failed.
240
+ * MAY CONTAIN CONTENT: a protocol error quotes the offending stream chunk
241
+ * (see `openai-chat.mjs` / `anthropic-messages.mjs`), which is up to 120
242
+ * characters of a tenant's completion. `formatStreamDiagnostic` omits it; any
243
+ * other observer that forwards a record off this machine must redact it.
244
+ */
245
+
246
+ /**
247
+ * Count what diagnosing a stall needs. Cheap per frame; the record is built
248
+ * only when `report` is called, which the HTTP edge does on a stall, on a
249
+ * mid-stream fault, and on a stream slower than its `slow` threshold.
250
+ *
251
+ * `startedAt` is THIS ATTEMPT's start, so every timing in the record describes
252
+ * the attempt that actually ran; `callStartedAt` (defaulting to it) is the
253
+ * whole call's, so a record can never be read as describing more than it does.
254
+ * The two differ only after a retry, and `attempts` says when that happened —
255
+ * a record labelled `slow` that describes a 3 ms stream was exactly the
256
+ * confusion this separation removes.
257
+ *
258
+ * @param {{ now: () => number, startedAt: number, callStartedAt?: number, attempts?: number, wire?: string|null, tier?: string|null }} p
259
+ */
260
+ export function createStallWatch({ now, startedAt, callStartedAt = startedAt, attempts: attemptCount = 1, wire = null, tier = null }) {
261
+ let acceptedAt = /** @type {number|null} */ (null);
262
+ let firstEventAt = /** @type {number|null} */ (null);
263
+ let lastEventAt = /** @type {number|null} */ (null);
264
+ let lastEventType = /** @type {string|null} */ (null);
265
+ let events = 0;
266
+ let cohortFrameSeen = false;
267
+
268
+ return {
269
+ /** The gateway answered 2xx: headers are in, the body is open. */
270
+ accepted() {
271
+ acceptedAt = now();
272
+ },
273
+ /** One SSE frame arrived. @param {{event?:string}} frame */
274
+ event(frame) {
275
+ events++;
276
+ lastEventAt = now();
277
+ if (firstEventAt === null) firstEventAt = lastEventAt;
278
+ lastEventType = frame?.event || "message";
279
+ },
280
+ /** The terminal `event: cohort` frame arrived. */
281
+ cohortFrame() {
282
+ cohortFrameSeen = true;
283
+ },
284
+ get cohortFrameSeen() {
285
+ return cohortFrameSeen;
286
+ },
287
+ /**
288
+ * @param {{ outcome:'stalled'|'failed'|'slow', requestId?:string|null, accepted?:boolean, attempts?:number, error?:{code?:string, message?:string}|null }} p
289
+ * @returns {StreamDiagnostic}
290
+ */
291
+ report({ outcome, requestId = null, accepted = acceptedAt !== null, attempts = attemptCount, error = null }) {
292
+ const at = now();
293
+ return {
294
+ outcome,
295
+ requestId,
296
+ wire,
297
+ tier,
298
+ accepted,
299
+ ttfbMs: acceptedAt === null ? null : acceptedAt - startedAt,
300
+ firstEventMs: firstEventAt === null || acceptedAt === null ? null : firstEventAt - acceptedAt,
301
+ sinceLastEventMs: lastEventAt === null ? (acceptedAt === null ? null : at - acceptedAt) : at - lastEventAt,
302
+ events,
303
+ lastEventType,
304
+ cohortFrameSeen,
305
+ totalMs: at - startedAt,
306
+ callMs: at - callStartedAt,
307
+ attempts,
308
+ code: error?.code ?? null,
309
+ message: error?.message ?? null,
310
+ };
311
+ },
312
+ };
313
+ }
314
+
315
+ /**
316
+ * One stderr line for a diagnostic.
317
+ *
318
+ * THIS LINE carries only ids, timings and event names — never a token, a prompt
319
+ * or a completion — because it goes to a seat's log. That promise is this
320
+ * function's, not the record's: `StreamDiagnostic.message` can quote a stream
321
+ * chunk, so `message` is deliberately omitted here and any other observer must
322
+ * redact it itself.
323
+ *
324
+ * `total` is this attempt; `call` and `attempts` appear only when the gateway
325
+ * was retried, so a record can never be read as describing more than it does.
326
+ *
327
+ * @param {StreamDiagnostic} d
328
+ * @returns {string}
329
+ */
330
+ export function formatStreamDiagnostic(d) {
331
+ const retried = (d.attempts ?? 1) > 1;
332
+ const parts = [
333
+ `stream ${d.outcome}`,
334
+ d.wire ? `wire=${d.wire}` : null,
335
+ d.tier ? `tier=${d.tier}` : null,
336
+ `request=${d.requestId ?? "unknown"}`,
337
+ d.ttfbMs == null ? null : `ttfb=${d.ttfbMs}ms`,
338
+ d.firstEventMs == null ? null : `first-event=${d.firstEventMs}ms`,
339
+ d.sinceLastEventMs == null ? null : `silent=${d.sinceLastEventMs}ms`,
340
+ `events=${d.events}`,
341
+ `last-event=${d.lastEventType ?? "none"}`,
342
+ `cohort-frame=${d.cohortFrameSeen ? "yes" : "no"}`,
343
+ `total=${d.totalMs}ms`,
344
+ retried ? `call=${d.callMs}ms` : null,
345
+ retried ? `attempts=${d.attempts}` : null,
346
+ d.code ? `code=${d.code}` : null,
347
+ ].filter(Boolean);
348
+ return parts.join(" ");
349
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * lib/engine/wire/token-provider.mjs — the gateway credential for a run that
3
+ * may outlive its seat token.
4
+ *
5
+ * A seat token (`cst_…`) is short-lived. A headless run finishes well inside
6
+ * its life, but a `cohort session` front door runs for days, so the engine
7
+ * must not read `COHORT_LLM_TOKEN` once and keep it. In order:
8
+ *
9
+ * COHORT_LLM_TOKEN_HELPER a shell command that prints a fresh token on its
10
+ * first stdout line (maestro sets it to
11
+ * scripts/cohort-llm/api-key-helper.mjs). Its token
12
+ * is cached for COHORT_LLM_TOKEN_TTL_MS (default 10
13
+ * minutes) and the command runs again when the
14
+ * cache has aged out or the gateway answered 401
15
+ * (`{refresh:true}` from wire/http.mjs).
16
+ * COHORT_LLM_TOKEN the token itself. With a helper it seeds the cache
17
+ * (the adapter minted it at spawn), so a short run
18
+ * never forks the helper; without one it is used as
19
+ * given, as before.
20
+ *
21
+ * The provider returns what the wire takes: a string (env only) or a token
22
+ * function (helper). Concurrent refreshes share one helper run. The helper's
23
+ * output never reaches an error message; only its exit and its stderr's first
24
+ * line do.
25
+ *
26
+ * Re-mints after a 401 are bounded (CF-136): a refresh naming a `rejected`
27
+ * token that the cache has already replaced returns the replacement with no
28
+ * helper run; the helper is told the rejected token's fingerprint
29
+ * (COHORT_LLM_TOKEN_REJECTED_FP) so the seat's manager mints only when its own
30
+ * token is the refused one; and at most MAX_REFRESHES_PER_WINDOW re-mints run
31
+ * per REFRESH_WINDOW_MS — past that the token function throws, which the wire
32
+ * returns as a non-retryable `token_unavailable`. Only a 401 asks for a
33
+ * refresh: a 429 or a 503 never reaches this provider's refresh path.
34
+ *
35
+ * @module lib/engine/wire/token-provider
36
+ */
37
+
38
+ import { execFile } from "node:child_process";
39
+ import { createHash } from "node:crypto";
40
+
41
+ export const DEFAULT_TOKEN_TTL_MS = 10 * 60 * 1000;
42
+ export const TOKEN_HELPER_TIMEOUT_MS = 20_000;
43
+
44
+ /** @param {Record<string,string|undefined>} env */
45
+ export function tokenTtlMs(env) {
46
+ const n = Number(env.COHORT_LLM_TOKEN_TTL_MS);
47
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_TOKEN_TTL_MS;
48
+ }
49
+
50
+ /**
51
+ * Run a helper command in /bin/sh and resolve its stdout.
52
+ * @param {string} command
53
+ * @param {{env:Record<string,string|undefined>, timeoutMs?:number}} o
54
+ * @returns {Promise<string>}
55
+ */
56
+ export function runShellTokenHelper(command, { env, timeoutMs = TOKEN_HELPER_TIMEOUT_MS }) {
57
+ return new Promise((resolve, reject) => {
58
+ execFile("/bin/sh", ["-c", command], { env: /** @type any */ (env), timeout: timeoutMs, maxBuffer: 64 * 1024, encoding: "utf8" }, (err, stdout, stderr) => {
59
+ if (err) {
60
+ const why = String(stderr || "").split(/\r?\n/).find((l) => l.trim() !== "") ?? (/** @type any */ (err).killed ? "timed out" : err.message);
61
+ reject(new Error(`COHORT_LLM_TOKEN_HELPER failed: ${why.slice(0, 300)}`));
62
+ return;
63
+ }
64
+ resolve(String(stdout));
65
+ });
66
+ });
67
+ }
68
+
69
+ /**
70
+ * @typedef {{refresh?:boolean, rejected?:string}} TokenRequest
71
+ * @typedef {{ok:true, source:'env'|'helper', token:string|((o?:TokenRequest)=>Promise<string>), helperRuns:()=>number}
72
+ * | {ok:false, error:{code:'missing_token', message:string}}} TokenProvider
73
+ */
74
+
75
+ /**
76
+ * @param {object} p
77
+ * @param {Record<string,string|undefined>} p.env
78
+ * @param {() => number} [p.now]
79
+ * @param {(command:string, o:{env:Record<string,string|undefined>}) => Promise<string>} [p.runHelper]
80
+ * @param {{max?:number, windowMs?:number}} [p.refreshCaps] overrides DEFAULT_REFRESH_CAPS
81
+ * @returns {TokenProvider}
82
+ */
83
+ export function createTokenProvider({ env, now = Date.now, runHelper = runShellTokenHelper, refreshCaps = {} }) {
84
+ const caps = { ...DEFAULT_REFRESH_CAPS, ...refreshCaps };
85
+ const helper = String(env.COHORT_LLM_TOKEN_HELPER ?? "").trim();
86
+ const envToken = String(env.COHORT_LLM_TOKEN ?? "").trim();
87
+ if (!helper && !envToken) {
88
+ return { ok: false, error: { code: "missing_token", message: "no gateway credential: set COHORT_LLM_TOKEN (or COHORT_LLM_TOKEN_HELPER)" } };
89
+ }
90
+ if (!helper) return { ok: true, source: "env", token: envToken, helperRuns: () => 0 };
91
+
92
+ const ttl = tokenTtlMs(env);
93
+ /** @type {{token:string, at:number}|null} */
94
+ let cached = envToken ? { token: envToken, at: now() } : null;
95
+ /** @type {Promise<string>|null} */
96
+ let inflight = null;
97
+ let runs = 0;
98
+ /** @type {number[]} when each 401-driven re-mint started (the refresh window) */
99
+ let refreshes = [];
100
+
101
+ /** @param {string|null} rejectedToken */
102
+ const mint = async (rejectedToken) => {
103
+ runs++;
104
+ // The helper learns WHICH token was refused (a fingerprint, never the token),
105
+ // so its seat manager re-mints only when its cached token is that one.
106
+ const helperEnv = rejectedToken ? { ...env, [REJECTED_TOKEN_FP_ENV]: tokenFingerprint(rejectedToken) } : env;
107
+ const out = await runHelper(helper, { env: helperEnv });
108
+ const fresh = String(out).split(/\r?\n/)[0].trim();
109
+ if (!fresh) throw new Error("COHORT_LLM_TOKEN_HELPER printed no token");
110
+ cached = { token: fresh, at: now() };
111
+ return fresh;
112
+ };
113
+
114
+ return {
115
+ ok: true,
116
+ source: "helper",
117
+ helperRuns: () => runs,
118
+ /** @param {TokenRequest} [o] */
119
+ token: async ({ refresh = false, rejected } = {}) => {
120
+ if (!refresh) {
121
+ if (cached && now() - cached.at < ttl) return cached.token;
122
+ if (!inflight) inflight = mint(null).finally(() => (inflight = null));
123
+ return inflight;
124
+ }
125
+ // A 401. A concurrent request may already have replaced the rejected token.
126
+ if (rejected && cached && cached.token !== rejected && now() - cached.at < ttl) return cached.token;
127
+ if (inflight) return inflight;
128
+ const t = now();
129
+ const budget = refreshBudget(refreshes, t, caps);
130
+ refreshes = budget.recent;
131
+ if (!budget.allowed) {
132
+ throw new Error(`COHORT_LLM_TOKEN_HELPER re-mint capped: ${caps.max} refreshes after a 401 within ${Math.round(caps.windowMs / 1000)}s; the gateway keeps refusing fresh tokens`);
133
+ }
134
+ refreshes.push(t);
135
+ const rejectedToken = rejected || (cached ? cached.token : null);
136
+ cached = null; // a rejected token is never served again
137
+ inflight = mint(rejectedToken).finally(() => (inflight = null));
138
+ return inflight;
139
+ },
140
+ };
141
+ }
142
+
143
+ /** Env var naming the fingerprint of the token a 401 rejected (read by maestro's api-key-helper). */
144
+ export const REJECTED_TOKEN_FP_ENV = "COHORT_LLM_TOKEN_REJECTED_FP";
145
+ /** A 401-driven re-mint may run at most MAX_REFRESHES_PER_WINDOW times per REFRESH_WINDOW_MS. */
146
+ export const REFRESH_WINDOW_MS = 60_000;
147
+ export const MAX_REFRESHES_PER_WINDOW = 3;
148
+ /**
149
+ * The engine's re-mint cap as one named config (CF-143). The values are the W7
150
+ * defaults, pending owner confirmation; `createTokenProvider({ refreshCaps })`
151
+ * overrides them. docs/engine/README.md "Retry caps" lists every cap.
152
+ */
153
+ export const DEFAULT_REFRESH_CAPS = Object.freeze({ max: MAX_REFRESHES_PER_WINDOW, windowMs: REFRESH_WINDOW_MS });
154
+
155
+ /**
156
+ * sha256 of a token, first 16 hex chars — the same shape as lib/org/llm-token.mjs
157
+ * `keyFingerprint`, so the helper compares like with like.
158
+ * @param {string} token
159
+ */
160
+ export function tokenFingerprint(token) {
161
+ return createHash("sha256").update(String(token || "")).digest("hex").slice(0, 16);
162
+ }
163
+
164
+ /**
165
+ * The sliding-window re-mint budget. PURE.
166
+ * @param {number[]} history start times of earlier re-mints
167
+ * @param {number} nowMs
168
+ * @param {{max?:number, windowMs?:number}} [o]
169
+ * @returns {{allowed:boolean, recent:number[], retryAt:number|null}}
170
+ */
171
+ export function refreshBudget(history, nowMs, { max = MAX_REFRESHES_PER_WINDOW, windowMs = REFRESH_WINDOW_MS } = {}) {
172
+ const recent = (Array.isArray(history) ? history : []).filter((at) => Number.isFinite(at) && at > nowMs - windowMs && at <= nowMs).sort((a, b) => a - b);
173
+ if (recent.length < max) return { allowed: true, recent, retryAt: null };
174
+ return { allowed: false, recent, retryAt: recent[recent.length - max] + windowMs };
175
+ }