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