@cohortapp/agent-sdk 2.3.0

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 (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Maestro — Voice session rotation
3
+ *
4
+ * OpenAI Realtime has a documented latency-drift bug on long sessions:
5
+ * median turn latency climbs from ~800 ms to >2 s after ~20 turns. The
6
+ * supported mitigation is to **rotate the session** every N turns or M
7
+ * minutes — summarise the conversation, close the WebSocket, open a new
8
+ * one, and re-seed the new session with a system message containing the
9
+ * running summary.
10
+ *
11
+ * This module provides:
12
+ * shouldRotate(state, opts) → boolean
13
+ * rotateSessionIfStale(provider, state, opts) → { rotated, newProvider }
14
+ * makeSummary(transcript) → short text summary for re-seeding
15
+ *
16
+ * The voice worker calls rotateSessionIfStale() after each TURN_DONE.
17
+ * If rotation is needed it spins up a fresh provider with the same
18
+ * sessionConfig, plays the summary into it via sendText(), and the
19
+ * worker swaps providers transparently. The transcript + audit log
20
+ * persist across the rotation in state/voice/sessions/.
21
+ */
22
+
23
+ import { VOICE_EVENTS } from "./provider.mjs";
24
+
25
+ export const SESSION_ROTATION_DEFAULTS = Object.freeze({
26
+ max_turns: 12, // rotate after this many user→agent turn-pairs
27
+ max_duration_ms: 4 * 60 * 1000, // rotate after this much wall-clock
28
+ max_p95_latency_ms: 2000, // rotate if last 5 turns averaged > this
29
+ });
30
+
31
+ /**
32
+ * Decide whether the session is stale enough to rotate.
33
+ *
34
+ * @param {object} state Tracking state from the voice worker:
35
+ * {
36
+ * turns: number,
37
+ * started_at: number (ms epoch),
38
+ * recent_latencies_ms: number[], // last N turn latencies
39
+ * }
40
+ * @param {object} opts Optional overrides for the defaults.
41
+ * @returns {{ rotate: boolean, reason?: string }}
42
+ */
43
+ export function shouldRotate(state, opts = {}) {
44
+ const cfg = { ...SESSION_ROTATION_DEFAULTS, ...opts };
45
+ if (!state) return { rotate: false };
46
+
47
+ if ((state.turns || 0) >= cfg.max_turns) {
48
+ return { rotate: true, reason: "max_turns" };
49
+ }
50
+ const ageMs = Date.now() - (state.started_at || Date.now());
51
+ if (ageMs >= cfg.max_duration_ms) {
52
+ return { rotate: true, reason: "max_duration" };
53
+ }
54
+ const recent = state.recent_latencies_ms || [];
55
+ if (recent.length >= 5) {
56
+ const sorted = [...recent].sort((a, b) => a - b);
57
+ const p95 = sorted[Math.max(0, Math.floor(sorted.length * 0.95) - 1)];
58
+ if (p95 >= cfg.max_p95_latency_ms) {
59
+ return { rotate: true, reason: "latency_drift" };
60
+ }
61
+ }
62
+ return { rotate: false };
63
+ }
64
+
65
+ /**
66
+ * Build a compact summary of a transcript for re-seeding into a fresh
67
+ * session. We keep this dumb-on-purpose: deterministic, no LLM round-trip,
68
+ * preserves the most recent N exchanges and the most recent tool calls.
69
+ *
70
+ * @param {Array<{role: string, text?: string, tool?: object}>} transcript
71
+ * @param {object} [opts]
72
+ * @returns {string}
73
+ */
74
+ export function makeSummary(transcript, opts = {}) {
75
+ const keepExchanges = opts.keepExchanges ?? 4;
76
+ const keepTools = opts.keepTools ?? 3;
77
+
78
+ if (!Array.isArray(transcript) || !transcript.length) {
79
+ return "Conversation continues. No prior context available.";
80
+ }
81
+
82
+ const exchanges = transcript.filter((t) => t.role === "user" || t.role === "assistant");
83
+ const recentExchanges = exchanges.slice(-keepExchanges * 2);
84
+ const recentTools = transcript
85
+ .filter((t) => t.tool)
86
+ .slice(-keepTools)
87
+ .map((t) => ({
88
+ name: t.tool.name,
89
+ ok: t.tool.error ? false : true,
90
+ summary: summariseToolCall(t.tool),
91
+ }));
92
+
93
+ const lines = [
94
+ "[Session rotated for latency reasons — running summary follows.]",
95
+ "",
96
+ "Recent dialogue:",
97
+ ...recentExchanges.map((t) => `- ${t.role}: ${truncate(t.text || "", 240)}`),
98
+ ];
99
+ if (recentTools.length) {
100
+ lines.push("", "Recent tool activity:");
101
+ for (const r of recentTools) {
102
+ lines.push(`- ${r.name} → ${r.ok ? "ok" : "error"}${r.summary ? ` (${r.summary})` : ""}`);
103
+ }
104
+ }
105
+ lines.push("", "Continue the conversation from this state. Do not re-introduce yourself.");
106
+ return lines.join("\n");
107
+ }
108
+
109
+ function summariseToolCall(tool) {
110
+ if (!tool) return "";
111
+ if (tool.error) return `error: ${truncate(String(tool.error), 80)}`;
112
+ if (tool.args?.channel) return `to ${tool.args.channel}`;
113
+ if (tool.args?.to) return `to ${tool.args.to}`;
114
+ if (tool.args?.query) return `query="${truncate(tool.args.query, 60)}"`;
115
+ return "";
116
+ }
117
+
118
+ function truncate(s, n) {
119
+ if (!s || s.length <= n) return s;
120
+ return s.slice(0, n - 1) + "…";
121
+ }
122
+
123
+ /**
124
+ * Default time (ms) to wait for a freshly-spun provider to confirm it is
125
+ * live before we treat the rotation as failed and keep the old session.
126
+ */
127
+ export const ROTATION_CONFIRM_TIMEOUT_MS = 5000;
128
+
129
+ /**
130
+ * Wait until a freshly-connected provider is observably live, or fail.
131
+ *
132
+ * "Live" means either:
133
+ * - its connect() promise resolved (already awaited by the caller), AND
134
+ * - it has reached a non-idle/non-closed state, OR emitted CONNECTED /
135
+ * SESSION_CREATED within `timeoutMs`.
136
+ *
137
+ * connect() resolving is necessary but not sufficient: a provider can
138
+ * resolve connect() and then immediately drop the upstream socket. We
139
+ * therefore confirm against the observable signals the provider exposes.
140
+ *
141
+ * @param {VoiceProvider} provider The newly-connected provider.
142
+ * @param {number} timeoutMs
143
+ * @returns {Promise<void>} Resolves when live; rejects on timeout/error.
144
+ */
145
+ function confirmProviderLive(provider, timeoutMs) {
146
+ const healthy = (s) => s && s !== "idle" && s !== "closed";
147
+
148
+ return new Promise((resolve, reject) => {
149
+ // Fast path: if the provider already reports a healthy state (e.g.
150
+ // connect() resolved and set state="connected"), trust it. Evaluated
151
+ // inside the executor so it reflects state AFTER connect() resolved,
152
+ // not the state captured when this promise was constructed.
153
+ if (healthy(provider?.state)) { resolve(); return; }
154
+
155
+ // No event surface to observe and not already healthy → cannot confirm.
156
+ if (typeof provider?.once !== "function") {
157
+ reject(new Error("rotation: new provider is not observable and not in a live state"));
158
+ return;
159
+ }
160
+ let settled = false;
161
+ const finish = (fn, arg) => {
162
+ if (settled) return;
163
+ settled = true;
164
+ clearTimeout(timer);
165
+ try { provider.removeListener?.(VOICE_EVENTS.CONNECTED, onUp); } catch { /* */ }
166
+ try { provider.removeListener?.(VOICE_EVENTS.SESSION_CREATED, onUp); } catch { /* */ }
167
+ try { provider.removeListener?.(VOICE_EVENTS.CLOSED, onDown); } catch { /* */ }
168
+ try { provider.removeListener?.(VOICE_EVENTS.ERROR, onErr); } catch { /* */ }
169
+ fn(arg);
170
+ };
171
+ const onUp = () => finish(resolve);
172
+ const onDown = (info) => finish(reject, new Error(`rotation: new provider closed before confirmed live${info?.reason ? ` (${info.reason})` : ""}`));
173
+ const onErr = (err) => finish(reject, err instanceof Error ? err : new Error(String(err?.message || err)));
174
+ const timer = setTimeout(
175
+ () => finish(reject, new Error(`rotation: new provider did not confirm live within ${timeoutMs}ms`)),
176
+ timeoutMs,
177
+ );
178
+ if (typeof timer.unref === "function") timer.unref();
179
+
180
+ provider.once(VOICE_EVENTS.CONNECTED, onUp);
181
+ provider.once(VOICE_EVENTS.SESSION_CREATED, onUp);
182
+ provider.once(VOICE_EVENTS.CLOSED, onDown);
183
+ provider.once(VOICE_EVENTS.ERROR, onErr);
184
+
185
+ // Re-check state after wiring listeners to avoid a race where the
186
+ // provider became healthy between the fast-path check and now.
187
+ if (healthy(provider.state)) finish(resolve);
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Rotate the session if stale. Returns the (possibly-new) provider plus
193
+ * a flag indicating whether rotation actually happened.
194
+ *
195
+ * The new session is brought up and CONFIRMED LIVE before we touch the
196
+ * old one. If the bring-up rejects (or never confirms live), we keep the
197
+ * old provider running, best-effort close the half-open new provider, and
198
+ * return { rotated: false, provider: <old> } so the caller keeps serving
199
+ * the call instead of cutting over to a dead session. The old provider is
200
+ * only closed AFTER the new one is confirmed live (M10).
201
+ *
202
+ * Caller must:
203
+ * - Provide `createNewProvider`: a factory the worker uses to spin up
204
+ * a fresh provider with identical session config.
205
+ * - Provide `state.transcript` so the summary can be built.
206
+ *
207
+ * @param {VoiceProvider} provider Current live provider
208
+ * @param {object} state
209
+ * @param {object} opts
210
+ * @param {number} [opts.confirmTimeoutMs] Override the liveness timeout.
211
+ */
212
+ export async function rotateSessionIfStale(provider, state, opts = {}) {
213
+ const decision = shouldRotate(state, opts.thresholds);
214
+ if (!decision.rotate) return { rotated: false, provider };
215
+
216
+ if (typeof opts.createNewProvider !== "function") {
217
+ throw new Error("rotateSessionIfStale requires opts.createNewProvider");
218
+ }
219
+
220
+ const summary = makeSummary(state.transcript || [], opts.summary);
221
+ const oldProvider = provider;
222
+ const sessionConfig = state.sessionConfig || provider.sessionConfig;
223
+ const confirmTimeoutMs = opts.confirmTimeoutMs ?? ROTATION_CONFIRM_TIMEOUT_MS;
224
+
225
+ let next;
226
+ try {
227
+ next = opts.createNewProvider({ reason: decision.reason });
228
+ // Bring up the new session, then confirm it is actually live before we
229
+ // touch the old one. confirmProviderLive checks the post-connect state
230
+ // (a well-behaved provider sets state="connected"/emits CONNECTED from
231
+ // connect()) and, if not yet healthy, waits for CONNECTED/SESSION_CREATED
232
+ // with a timeout — rejecting on CLOSED/ERROR. A provider that resolves
233
+ // connect() but drops (or never reaches a live state) fails confirmation
234
+ // and we keep the old session running rather than cutting over.
235
+ await Promise.resolve(next.connect(sessionConfig));
236
+ await confirmProviderLive(next, confirmTimeoutMs);
237
+ // New session is live — seed it with the running summary.
238
+ next.sendText(`(System) ${summary}`);
239
+ } catch (err) {
240
+ // Bring-up failed: do NOT cut over. Tear down the half-open new
241
+ // provider so it can't leak, keep the old provider serving the call.
242
+ if (next) { try { next.close?.(`rotation_failed: ${decision.reason}`); } catch { /* */ } }
243
+ return {
244
+ rotated: false,
245
+ provider: oldProvider,
246
+ reason: decision.reason,
247
+ error: err instanceof Error ? err.message : String(err),
248
+ };
249
+ }
250
+
251
+ // Only now that the new session is confirmed live do we close the old.
252
+ try { oldProvider.close(`rotated: ${decision.reason}`); } catch { /* */ }
253
+
254
+ return { rotated: true, provider: next, reason: decision.reason };
255
+ }
256
+
257
+ export default { shouldRotate, rotateSessionIfStale, makeSummary, SESSION_ROTATION_DEFAULTS };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Maestro — Speech-to-text helper for inbound audio files.
3
+ *
4
+ * Wraps OpenAI's (and any OpenAI-compatible) `/v1/audio/transcriptions`
5
+ * endpoint. Used by the voice channel adapter's receive() path to turn an
6
+ * inbound voice note (WhatsApp PTT, Slack audio clip, a recorded Twilio
7
+ * call leg, an uploaded recording) into text the agent can reason over.
8
+ *
9
+ * Counterpart to lib/voice/tts.mjs (which goes text → audio). This goes
10
+ * audio file → text and mirrors its conventions: module-level defaults,
11
+ * apiKey from arg or OPENAI_API_KEY, clear HTTP errors, a default export.
12
+ *
13
+ * Why a separate helper from the Realtime API:
14
+ * - Asynchronous transcription of a *file* is much cheaper than a live
15
+ * Realtime session and is the right primitive for store-and-forward
16
+ * channels (WhatsApp / Slack / SMS) that hand you a file, not a stream.
17
+ * - whisper-1 / gpt-4o-transcribe accept multipart/form-data uploads;
18
+ * no streaming session required.
19
+ *
20
+ * Ported and adapted from OpenClaw
21
+ * (src/media-understanding/openai-compatible-audio.ts —
22
+ * transcribeOpenAiCompatibleAudio) for the maestro autonomy model. The
23
+ * OpenClaw-specific provider-config / DNS-pinning / dispatcher plumbing is
24
+ * dropped in favour of plain global fetch + FormData/Blob (Node 18+) with
25
+ * an AbortController timeout.
26
+ *
27
+ * Public API:
28
+ * transcribeAudioFile({ filePath | buffer, apiKey, model, baseUrl,
29
+ * language, prompt, fileName, mime, timeoutMs })
30
+ * → Promise<{ text, raw }>
31
+ *
32
+ * Node builtins only (node:fs/promises, node:path); global fetch / FormData
33
+ * / Blob / AbortController are Node 18+ globals.
34
+ */
35
+
36
+ import { readFile } from "node:fs/promises";
37
+ import { basename } from "node:path";
38
+
39
+ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
40
+ const DEFAULT_MODEL = "whisper-1";
41
+ const DEFAULT_TIMEOUT_MS = 120000; // 2 min — large files / cold models
42
+ const DEFAULT_FILE_NAME = "audio";
43
+
44
+ /**
45
+ * Strip a single trailing slash so `${baseUrl}/audio/transcriptions` never
46
+ * doubles up. Mirrors OpenClaw's resolveProviderHttpRequestConfig behaviour
47
+ * of treating baseUrl as a root without a trailing slash.
48
+ * @param {string} url
49
+ * @returns {string}
50
+ */
51
+ function normalizeBaseUrl(url) {
52
+ return String(url || "").replace(/\/+$/, "");
53
+ }
54
+
55
+ /**
56
+ * @typedef {Object} TranscribeAudioFileOptions
57
+ * @property {string} [filePath] Path to the audio file to transcribe. Mutually
58
+ * sufficient with `buffer`; one of the two is required.
59
+ * @property {Buffer|Uint8Array|ArrayBuffer} [buffer] Raw audio bytes. Used instead of
60
+ * reading from disk when supplied.
61
+ * @property {string} [apiKey] Override OPENAI_API_KEY.
62
+ * @property {string} [model] Transcription model id (default: whisper-1).
63
+ * @property {string} [baseUrl] OpenAI-compatible API root (default:
64
+ * https://api.openai.com/v1). No trailing "/audio/...".
65
+ * @property {string} [language] ISO-639-1 language hint (e.g. "en"); improves accuracy.
66
+ * @property {string} [prompt] Optional prompt to steer style / spelling.
67
+ * @property {string} [fileName] Filename to send in the multipart part. Defaults to the
68
+ * basename of `filePath`, else "audio".
69
+ * @property {string} [mime] Content-Type for the audio part (e.g. "audio/mpeg").
70
+ * @property {number} [timeoutMs] Request timeout in ms (default: 120000).
71
+ */
72
+
73
+ /**
74
+ * Transcribe a single audio file via an OpenAI-compatible
75
+ * `/audio/transcriptions` endpoint.
76
+ *
77
+ * @param {TranscribeAudioFileOptions} [opts]
78
+ * @returns {Promise<{ text: string, raw: any }>} The transcript text plus the
79
+ * raw parsed JSON payload from the provider.
80
+ */
81
+ export async function transcribeAudioFile(opts = {}) {
82
+ const apiKey = opts.apiKey || process.env.OPENAI_API_KEY;
83
+ if (!apiKey) throw new Error("OPENAI_API_KEY not set");
84
+
85
+ // Resolve the audio bytes: explicit buffer wins, else read from disk.
86
+ let bytes;
87
+ let fileName = opts.fileName;
88
+ if (opts.buffer != null) {
89
+ bytes =
90
+ opts.buffer instanceof ArrayBuffer ? Buffer.from(opts.buffer) : Buffer.from(opts.buffer);
91
+ } else if (opts.filePath) {
92
+ bytes = await readFile(opts.filePath);
93
+ if (!fileName) fileName = basename(opts.filePath);
94
+ } else {
95
+ throw new Error("transcribeAudioFile: provide either filePath or buffer");
96
+ }
97
+ if (!fileName) fileName = DEFAULT_FILE_NAME;
98
+
99
+ const model = (opts.model && String(opts.model).trim()) || DEFAULT_MODEL;
100
+ const baseUrl = normalizeBaseUrl(opts.baseUrl || DEFAULT_BASE_URL);
101
+ const url = `${baseUrl}/audio/transcriptions`;
102
+ const timeoutMs =
103
+ typeof opts.timeoutMs === "number" && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
104
+
105
+ // Build multipart/form-data. fetch sets the Content-Type (with boundary)
106
+ // automatically when the body is a FormData instance — do NOT set it by hand.
107
+ const form = new FormData();
108
+ const blob = opts.mime ? new Blob([bytes], { type: opts.mime }) : new Blob([bytes]);
109
+ form.append("file", blob, fileName);
110
+ form.append("model", model);
111
+ if (opts.language) form.append("language", String(opts.language));
112
+ if (opts.prompt) form.append("prompt", String(opts.prompt));
113
+
114
+ const controller = new AbortController();
115
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
116
+
117
+ let r;
118
+ try {
119
+ r = await fetch(url, {
120
+ method: "POST",
121
+ headers: {
122
+ Authorization: `Bearer ${apiKey}`,
123
+ },
124
+ body: form,
125
+ signal: controller.signal,
126
+ });
127
+ } catch (err) {
128
+ if (err && err.name === "AbortError") {
129
+ throw new Error(`stt failed: request timed out after ${timeoutMs}ms`);
130
+ }
131
+ throw err;
132
+ } finally {
133
+ clearTimeout(timer);
134
+ }
135
+
136
+ if (!r.ok) {
137
+ let errText = "";
138
+ try {
139
+ errText = await r.text();
140
+ } catch {
141
+ errText = "";
142
+ }
143
+ throw new Error(`stt failed: HTTP ${r.status} ${errText.slice(0, 300)}`);
144
+ }
145
+
146
+ let payload;
147
+ try {
148
+ payload = await r.json();
149
+ } catch (err) {
150
+ throw new Error(`stt failed: invalid JSON response (${err && err.message})`);
151
+ }
152
+
153
+ const text = payload && typeof payload.text === "string" ? payload.text : undefined;
154
+ if (typeof text !== "string") {
155
+ throw new Error("stt failed: response missing text");
156
+ }
157
+
158
+ return { text, raw: payload };
159
+ }
160
+
161
+ export default { transcribeAudioFile };
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Tests for lib/voice/stt.mjs — speech-to-text over an OpenAI-compatible
3
+ * /audio/transcriptions endpoint.
4
+ *
5
+ * Network is fully stubbed: global.fetch is replaced per-test so no real
6
+ * HTTP call is ever made. We assert the request URL, the Authorization
7
+ * header, the multipart field names/values (model, language, file), the
8
+ * parsed result shape, default model/baseUrl, the filePath read path, and
9
+ * error handling on a 401 stub.
10
+ */
11
+
12
+ import { test } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
15
+ import { tmpdir } from "node:os";
16
+ import { join } from "node:path";
17
+
18
+ import { transcribeAudioFile } from "./stt.mjs";
19
+
20
+ const realFetch = global.fetch;
21
+
22
+ function jsonResponse(obj, { ok = true, status = 200 } = {}) {
23
+ return {
24
+ ok,
25
+ status,
26
+ async json() {
27
+ return obj;
28
+ },
29
+ async text() {
30
+ return JSON.stringify(obj);
31
+ },
32
+ };
33
+ }
34
+
35
+ function installFetchStub(handler) {
36
+ const calls = [];
37
+ global.fetch = async (url, init) => {
38
+ calls.push({ url, init });
39
+ return handler(url, init, calls);
40
+ };
41
+ return calls;
42
+ }
43
+
44
+ function restoreFetch() {
45
+ global.fetch = realFetch;
46
+ }
47
+
48
+ // Read multipart fields from a FormData body without making a network call.
49
+ async function readForm(init) {
50
+ assert.ok(init && init.body instanceof FormData, "body must be a FormData instance");
51
+ const form = init.body;
52
+ return {
53
+ model: form.get("model"),
54
+ language: form.get("language"),
55
+ prompt: form.get("prompt"),
56
+ file: form.get("file"),
57
+ };
58
+ }
59
+
60
+ test("buffer path: posts multipart to default URL with auth + field names, parses result", async () => {
61
+ const calls = installFetchStub(() => jsonResponse({ text: "hello world", language: "en" }));
62
+ try {
63
+ const out = await transcribeAudioFile({
64
+ buffer: Buffer.from("FAKEAUDIOBYTES"),
65
+ apiKey: "sk-test-123",
66
+ language: "en",
67
+ fileName: "note.mp3",
68
+ mime: "audio/mpeg",
69
+ });
70
+
71
+ assert.equal(calls.length, 1, "exactly one fetch call");
72
+ const { url, init } = calls[0];
73
+
74
+ // URL + method
75
+ assert.equal(url, "https://api.openai.com/v1/audio/transcriptions");
76
+ assert.equal(init.method, "POST");
77
+
78
+ // Auth header
79
+ assert.equal(init.headers.Authorization, "Bearer sk-test-123");
80
+ // Caller must NOT hand-set Content-Type for FormData (fetch adds boundary).
81
+ assert.ok(
82
+ !init.headers["Content-Type"] && !init.headers["content-type"],
83
+ "Content-Type must not be set manually for multipart",
84
+ );
85
+
86
+ // Multipart fields
87
+ const fields = await readForm(init);
88
+ assert.equal(fields.model, "whisper-1", "default model");
89
+ assert.equal(fields.language, "en");
90
+ assert.equal(fields.prompt, null, "no prompt field when not provided");
91
+ assert.ok(fields.file instanceof Blob, "file part is a Blob");
92
+ assert.equal(fields.file.type, "audio/mpeg", "blob carries supplied mime");
93
+
94
+ // Parsed result
95
+ assert.equal(out.text, "hello world");
96
+ assert.deepEqual(out.raw, { text: "hello world", language: "en" });
97
+ } finally {
98
+ restoreFetch();
99
+ }
100
+ });
101
+
102
+ test("filePath path: reads file, derives fileName, honours baseUrl + model overrides", async () => {
103
+ const calls = installFetchStub(() => jsonResponse({ text: "from disk" }));
104
+ const dir = await mkdtemp(join(tmpdir(), "stt-test-"));
105
+ const fp = join(dir, "voicemail.wav");
106
+ await writeFile(fp, Buffer.from("RIFFfakewav"));
107
+ try {
108
+ const out = await transcribeAudioFile({
109
+ filePath: fp,
110
+ apiKey: "sk-abc",
111
+ model: "gpt-4o-transcribe",
112
+ baseUrl: "https://compat.example.com/v1/", // trailing slash should be normalized
113
+ prompt: "Northwind",
114
+ });
115
+
116
+ const { url, init } = calls[0];
117
+ assert.equal(url, "https://compat.example.com/v1/audio/transcriptions", "trailing slash trimmed");
118
+
119
+ const fields = await readForm(init);
120
+ assert.equal(fields.model, "gpt-4o-transcribe");
121
+ assert.equal(fields.prompt, "Northwind");
122
+ assert.ok(fields.file instanceof Blob);
123
+ // fileName defaults to the basename of filePath; assert via the part name.
124
+ assert.equal(fields.file.name, "voicemail.wav", "fileName derived from filePath basename");
125
+
126
+ assert.equal(out.text, "from disk");
127
+ } finally {
128
+ restoreFetch();
129
+ await rm(dir, { recursive: true, force: true });
130
+ }
131
+ });
132
+
133
+ test("apiKey falls back to process.env.OPENAI_API_KEY", async () => {
134
+ const calls = installFetchStub(() => jsonResponse({ text: "env key works" }));
135
+ const prev = process.env.OPENAI_API_KEY;
136
+ process.env.OPENAI_API_KEY = "sk-from-env";
137
+ try {
138
+ const out = await transcribeAudioFile({ buffer: Buffer.from("x") });
139
+ assert.equal(calls[0].init.headers.Authorization, "Bearer sk-from-env");
140
+ assert.equal(out.text, "env key works");
141
+ } finally {
142
+ restoreFetch();
143
+ if (prev === undefined) delete process.env.OPENAI_API_KEY;
144
+ else process.env.OPENAI_API_KEY = prev;
145
+ }
146
+ });
147
+
148
+ test("throws clear error when no apiKey available", async () => {
149
+ const prev = process.env.OPENAI_API_KEY;
150
+ delete process.env.OPENAI_API_KEY;
151
+ // fetch must never be called.
152
+ const calls = installFetchStub(() => {
153
+ throw new Error("fetch should not be called");
154
+ });
155
+ try {
156
+ await assert.rejects(
157
+ () => transcribeAudioFile({ buffer: Buffer.from("x") }),
158
+ /OPENAI_API_KEY not set/,
159
+ );
160
+ assert.equal(calls.length, 0, "no network call without a key");
161
+ } finally {
162
+ restoreFetch();
163
+ if (prev !== undefined) process.env.OPENAI_API_KEY = prev;
164
+ }
165
+ });
166
+
167
+ test("throws when neither filePath nor buffer is provided", async () => {
168
+ const calls = installFetchStub(() => jsonResponse({ text: "nope" }));
169
+ try {
170
+ await assert.rejects(
171
+ () => transcribeAudioFile({ apiKey: "sk-x" }),
172
+ /provide either filePath or buffer/,
173
+ );
174
+ assert.equal(calls.length, 0);
175
+ } finally {
176
+ restoreFetch();
177
+ }
178
+ });
179
+
180
+ test("error handling: non-2xx (401) surfaces status + body", async () => {
181
+ installFetchStub(() =>
182
+ jsonResponse({ error: { message: "Invalid API key" } }, { ok: false, status: 401 }),
183
+ );
184
+ try {
185
+ await assert.rejects(
186
+ () => transcribeAudioFile({ buffer: Buffer.from("x"), apiKey: "sk-bad" }),
187
+ (err) => {
188
+ assert.match(err.message, /stt failed: HTTP 401/);
189
+ assert.match(err.message, /Invalid API key/);
190
+ return true;
191
+ },
192
+ );
193
+ } finally {
194
+ restoreFetch();
195
+ }
196
+ });
197
+
198
+ test("error handling: 2xx but missing text field", async () => {
199
+ installFetchStub(() => jsonResponse({ language: "en" }));
200
+ try {
201
+ await assert.rejects(
202
+ () => transcribeAudioFile({ buffer: Buffer.from("x"), apiKey: "sk-x" }),
203
+ /response missing text/,
204
+ );
205
+ } finally {
206
+ restoreFetch();
207
+ }
208
+ });
209
+
210
+ test("timeout: AbortError is reported as a timeout", async () => {
211
+ global.fetch = async (_url, init) => {
212
+ // Simulate the runtime aborting the request.
213
+ const err = new Error("aborted");
214
+ err.name = "AbortError";
215
+ void init;
216
+ throw err;
217
+ };
218
+ try {
219
+ await assert.rejects(
220
+ () => transcribeAudioFile({ buffer: Buffer.from("x"), apiKey: "sk-x", timeoutMs: 5 }),
221
+ /request timed out after 5ms/,
222
+ );
223
+ } finally {
224
+ restoreFetch();
225
+ }
226
+ });