@cohortapp/agent-sdk 2.17.0 → 2.18.5

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