@vellumai/assistant 0.5.6 → 0.5.8

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 (442) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +3 -2
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docker-entrypoint.sh +9 -0
  7. package/docs/architecture/keychain-broker.md +45 -240
  8. package/docs/architecture/memory.md +13 -11
  9. package/docs/architecture/security.md +0 -17
  10. package/docs/credential-execution-service.md +2 -2
  11. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  13. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  14. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  15. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  16. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
  17. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  18. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  19. package/package.json +2 -3
  20. package/src/__tests__/actor-token-service.test.ts +0 -114
  21. package/src/__tests__/approval-cascade.test.ts +0 -1
  22. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  23. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  24. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  25. package/src/__tests__/btw-routes.test.ts +0 -39
  26. package/src/__tests__/call-controller.test.ts +0 -1
  27. package/src/__tests__/call-domain.test.ts +0 -128
  28. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  29. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  31. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  32. package/src/__tests__/checker.test.ts +4 -2
  33. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  34. package/src/__tests__/config-schema-cmd.test.ts +0 -2
  35. package/src/__tests__/config-schema.test.ts +3 -1
  36. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  37. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  38. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  39. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  40. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  41. package/src/__tests__/conversation-error.test.ts +15 -1
  42. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  43. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  44. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  45. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  46. package/src/__tests__/conversation-queue.test.ts +0 -1
  47. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  48. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  49. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  50. package/src/__tests__/conversation-title-service.test.ts +87 -0
  51. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  52. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  53. package/src/__tests__/credential-execution-client.test.ts +5 -2
  54. package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
  55. package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
  56. package/src/__tests__/credential-security-e2e.test.ts +1 -67
  57. package/src/__tests__/credential-security-invariants.test.ts +6 -50
  58. package/src/__tests__/credentials-cli.test.ts +82 -3
  59. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  60. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  61. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  62. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  63. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  64. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  65. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  66. package/src/__tests__/host-shell-tool.test.ts +6 -7
  67. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  68. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  69. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  70. package/src/__tests__/intent-routing.test.ts +0 -13
  71. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  72. package/src/__tests__/journal-context.test.ts +335 -0
  73. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  74. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  75. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  76. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  77. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  78. package/src/__tests__/memory-regressions.test.ts +408 -363
  79. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  80. package/src/__tests__/migration-export-http.test.ts +2 -2
  81. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  82. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  83. package/src/__tests__/migration-validate-http.test.ts +2 -2
  84. package/src/__tests__/non-member-access-request.test.ts +2 -7
  85. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  86. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  87. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  88. package/src/__tests__/oauth-cli.test.ts +5 -1
  89. package/src/__tests__/permission-types.test.ts +1 -0
  90. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  91. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  93. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  94. package/src/__tests__/qdrant-manager.test.ts +28 -2
  95. package/src/__tests__/registry.test.ts +0 -6
  96. package/src/__tests__/relay-server.test.ts +1 -2
  97. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  98. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  99. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  100. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  101. package/src/__tests__/secure-keys.test.ts +95 -272
  102. package/src/__tests__/shell-identity.test.ts +96 -6
  103. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  104. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  105. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  106. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  107. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  108. package/src/__tests__/skill-load-tool.test.ts +0 -2
  109. package/src/__tests__/skill-memory.test.ts +17 -3
  110. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  111. package/src/__tests__/skills.test.ts +0 -2
  112. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  113. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  114. package/src/__tests__/stt-hints.test.ts +437 -0
  115. package/src/__tests__/suggestion-routes.test.ts +1 -32
  116. package/src/__tests__/system-prompt.test.ts +0 -1
  117. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  118. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  119. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  120. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  121. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  122. package/src/__tests__/update-bulletin.test.ts +0 -2
  123. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  124. package/src/__tests__/voice-quality.test.ts +58 -0
  125. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
  126. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  127. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
  128. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  129. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  130. package/src/acp/agent-process.ts +9 -1
  131. package/src/agent/loop.ts +1 -1
  132. package/src/approvals/guardian-request-resolvers.ts +164 -38
  133. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  134. package/src/calls/audio-store.test.ts +97 -0
  135. package/src/calls/audio-store.ts +205 -0
  136. package/src/calls/call-controller.ts +90 -8
  137. package/src/calls/call-domain.ts +3 -0
  138. package/src/calls/call-store.ts +10 -3
  139. package/src/calls/fish-audio-client.ts +129 -0
  140. package/src/calls/relay-server.ts +27 -0
  141. package/src/calls/stt-hints.ts +189 -0
  142. package/src/calls/tts-text-sanitizer.ts +61 -0
  143. package/src/calls/twilio-routes.ts +34 -5
  144. package/src/calls/types.ts +1 -0
  145. package/src/calls/voice-ingress-preflight.ts +0 -42
  146. package/src/calls/voice-quality.ts +38 -5
  147. package/src/calls/voice-session-bridge.ts +7 -12
  148. package/src/cli/commands/avatar.ts +2 -2
  149. package/src/cli/commands/config.ts +1 -4
  150. package/src/cli/commands/credentials.ts +128 -82
  151. package/src/cli/commands/doctor.ts +2 -2
  152. package/src/cli/commands/keys.ts +7 -7
  153. package/src/cli/commands/memory.ts +1 -1
  154. package/src/cli/commands/oauth/connections.ts +11 -29
  155. package/src/cli/commands/oauth/index.ts +7 -0
  156. package/src/cli/commands/oauth/platform.ts +525 -0
  157. package/src/cli/commands/platform.ts +3 -3
  158. package/src/cli/lib/daemon-credential-client.ts +284 -0
  159. package/src/cli.ts +1 -1
  160. package/src/config/assistant-feature-flags.ts +186 -5
  161. package/src/config/bundled-skills/AGENTS.md +34 -0
  162. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  163. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  164. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  165. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  166. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  167. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  168. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  169. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  170. package/src/config/bundled-skills/settings/TOOLS.json +47 -2
  171. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  172. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  173. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  174. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  175. package/src/config/bundled-tool-registry.ts +5 -11
  176. package/src/config/defaults.ts +0 -2
  177. package/src/config/env-registry.ts +5 -5
  178. package/src/config/env.ts +21 -14
  179. package/src/config/feature-flag-registry.json +49 -9
  180. package/src/config/loader.ts +106 -42
  181. package/src/config/schema.ts +9 -29
  182. package/src/config/schemas/calls.ts +30 -0
  183. package/src/config/schemas/fish-audio.ts +39 -0
  184. package/src/config/schemas/inference.ts +2 -2
  185. package/src/config/schemas/journal.ts +16 -0
  186. package/src/config/schemas/memory-processing.ts +2 -2
  187. package/src/config/schemas/security.ts +0 -4
  188. package/src/config/types.ts +1 -1
  189. package/src/contacts/contact-store.ts +39 -0
  190. package/src/contacts/types.ts +2 -0
  191. package/src/credential-execution/approval-bridge.ts +1 -0
  192. package/src/credential-execution/executable-discovery.ts +28 -4
  193. package/src/credential-execution/feature-gates.ts +16 -0
  194. package/src/credential-execution/process-manager.ts +38 -0
  195. package/src/credential-execution/startup-timeout.ts +36 -0
  196. package/src/daemon/approval-generators.ts +3 -9
  197. package/src/daemon/assistant-attachments.ts +9 -0
  198. package/src/daemon/config-watcher.ts +5 -0
  199. package/src/daemon/conversation-error.ts +13 -1
  200. package/src/daemon/conversation-memory.ts +1 -2
  201. package/src/daemon/conversation-process.ts +18 -1
  202. package/src/daemon/conversation-surfaces.ts +30 -1
  203. package/src/daemon/conversation-tool-setup.ts +0 -105
  204. package/src/daemon/conversation.ts +21 -1
  205. package/src/daemon/guardian-action-generators.ts +3 -9
  206. package/src/daemon/handlers/config-vercel.ts +92 -0
  207. package/src/daemon/handlers/skills.ts +2 -15
  208. package/src/daemon/install-symlink.ts +195 -0
  209. package/src/daemon/lifecycle.ts +234 -51
  210. package/src/daemon/message-types/conversations.ts +4 -4
  211. package/src/daemon/message-types/diagnostics.ts +3 -22
  212. package/src/daemon/message-types/messages.ts +0 -2
  213. package/src/daemon/message-types/upgrades.ts +8 -0
  214. package/src/daemon/server.ts +32 -95
  215. package/src/events/domain-events.ts +2 -1
  216. package/src/inbound/platform-callback-registration.ts +3 -3
  217. package/src/instrument.ts +8 -5
  218. package/src/memory/app-store.ts +31 -0
  219. package/src/memory/conversation-title-service.ts +50 -1
  220. package/src/memory/db-init.ts +16 -0
  221. package/src/memory/indexer.ts +19 -10
  222. package/src/memory/items-extractor.ts +328 -321
  223. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  224. package/src/memory/job-handlers/summarization.ts +26 -16
  225. package/src/memory/jobs-store.ts +63 -6
  226. package/src/memory/jobs-worker.ts +31 -7
  227. package/src/memory/journal-memory.ts +214 -0
  228. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  229. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  230. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  231. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  232. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  233. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  234. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  235. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  236. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  237. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  238. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  239. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  240. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  241. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  242. package/src/memory/migrations/116-messages-fts.ts +106 -1
  243. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  244. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  245. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  246. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  247. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  248. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  249. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  250. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  251. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  252. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  253. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  254. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  255. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  256. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  257. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  258. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  259. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  260. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  261. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  262. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  263. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  264. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  265. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  266. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  267. package/src/memory/migrations/index.ts +5 -0
  268. package/src/memory/migrations/registry.ts +98 -0
  269. package/src/memory/migrations/validate-migration-state.ts +137 -11
  270. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  271. package/src/memory/qdrant-manager.ts +64 -7
  272. package/src/memory/retriever.test.ts +37 -25
  273. package/src/memory/retriever.ts +24 -49
  274. package/src/memory/schema/calls.ts +1 -0
  275. package/src/memory/schema/contacts.ts +1 -0
  276. package/src/memory/schema/memory-core.ts +2 -0
  277. package/src/memory/search/formatting.ts +7 -44
  278. package/src/memory/search/staleness.ts +4 -0
  279. package/src/memory/search/tier-classifier.ts +10 -2
  280. package/src/memory/search/types.ts +2 -5
  281. package/src/memory/task-memory-cleanup.ts +4 -3
  282. package/src/notifications/adapters/slack.ts +168 -6
  283. package/src/notifications/broadcaster.ts +1 -0
  284. package/src/notifications/copy-composer.ts +59 -2
  285. package/src/notifications/decision-engine.ts +4 -1
  286. package/src/notifications/signal.ts +2 -0
  287. package/src/notifications/types.ts +2 -0
  288. package/src/oauth/connection-resolver.ts +6 -4
  289. package/src/permissions/checker.ts +0 -38
  290. package/src/permissions/shell-identity.ts +76 -22
  291. package/src/permissions/types.ts +4 -2
  292. package/src/platform/client.ts +35 -7
  293. package/src/prompts/journal-context.ts +133 -0
  294. package/src/prompts/persona-resolver.ts +194 -0
  295. package/src/prompts/system-prompt.ts +44 -4
  296. package/src/prompts/templates/SOUL.md +10 -0
  297. package/src/prompts/templates/users/default.md +1 -0
  298. package/src/providers/provider-send-message.ts +3 -32
  299. package/src/providers/registry.ts +29 -179
  300. package/src/providers/types.ts +1 -1
  301. package/src/runtime/access-request-helper.ts +4 -0
  302. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  303. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  304. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  305. package/src/runtime/auth/external-assistant-id.ts +13 -59
  306. package/src/runtime/auth/route-policy.ts +17 -1
  307. package/src/runtime/auth/token-service.ts +43 -138
  308. package/src/runtime/channel-readiness-service.ts +1 -16
  309. package/src/runtime/gateway-client.ts +47 -4
  310. package/src/runtime/guardian-decision-types.ts +45 -4
  311. package/src/runtime/http-server.ts +31 -3
  312. package/src/runtime/middleware/error-handler.ts +1 -9
  313. package/src/runtime/routes/access-request-decision.ts +2 -2
  314. package/src/runtime/routes/app-management-routes.ts +2 -1
  315. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  316. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  317. package/src/runtime/routes/audio-routes.ts +40 -0
  318. package/src/runtime/routes/btw-routes.ts +0 -17
  319. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  320. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  321. package/src/runtime/routes/conversation-routes.ts +4 -44
  322. package/src/runtime/routes/debug-routes.ts +12 -9
  323. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  324. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  325. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  326. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  327. package/src/runtime/routes/identity-routes.ts +19 -30
  328. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  329. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  330. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  331. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  332. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  333. package/src/runtime/routes/integrations/twilio.ts +52 -10
  334. package/src/runtime/routes/integrations/vercel.ts +89 -0
  335. package/src/runtime/routes/log-export-routes.ts +5 -0
  336. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  337. package/src/runtime/routes/memory-item-routes.ts +46 -14
  338. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  339. package/src/runtime/routes/migration-routes.ts +17 -1
  340. package/src/runtime/routes/notification-routes.ts +58 -0
  341. package/src/runtime/routes/schedule-routes.ts +65 -0
  342. package/src/runtime/routes/secret-routes.ts +141 -10
  343. package/src/runtime/routes/settings-routes.ts +41 -1
  344. package/src/runtime/routes/tts-routes.ts +96 -0
  345. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  346. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  347. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  348. package/src/runtime/routes/workspace-routes.ts +1 -1
  349. package/src/runtime/routes/workspace-utils.ts +86 -2
  350. package/src/security/ces-credential-client.ts +75 -29
  351. package/src/security/ces-rpc-credential-backend.ts +86 -0
  352. package/src/security/credential-backend.ts +22 -92
  353. package/src/security/keychain-broker-client.ts +10 -2
  354. package/src/security/secure-keys.ts +113 -115
  355. package/src/skills/catalog-install.ts +6 -32
  356. package/src/skills/skill-memory.ts +1 -0
  357. package/src/subagent/manager.ts +2 -5
  358. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  359. package/src/tools/acp/spawn.ts +78 -1
  360. package/src/tools/calls/call-start.ts +1 -0
  361. package/src/tools/credentials/vault.ts +5 -3
  362. package/src/tools/executor.ts +0 -4
  363. package/src/tools/memory/definitions.ts +3 -2
  364. package/src/tools/memory/handlers.ts +10 -7
  365. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  366. package/src/tools/network/web-fetch.ts +3 -1
  367. package/src/tools/skills/execute.ts +1 -1
  368. package/src/tools/terminal/safe-env.ts +1 -0
  369. package/src/tools/types.ts +0 -8
  370. package/src/util/browser.ts +15 -0
  371. package/src/util/errors.ts +0 -12
  372. package/src/util/platform.ts +4 -51
  373. package/src/workspace/git-service.ts +5 -2
  374. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  375. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  376. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  377. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  378. package/src/workspace/migrations/006-services-config.ts +49 -0
  379. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  380. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  381. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  382. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  383. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  384. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  385. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  386. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  387. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  388. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  389. package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
  390. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  391. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  392. package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
  393. package/src/workspace/migrations/registry.ts +12 -0
  394. package/src/workspace/migrations/runner.ts +106 -2
  395. package/src/workspace/migrations/types.ts +4 -0
  396. package/src/workspace/provider-commit-message-generator.ts +12 -21
  397. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  398. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  399. package/src/__tests__/diagnostics-export.test.ts +0 -288
  400. package/src/__tests__/local-gateway-health.test.ts +0 -209
  401. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  402. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  403. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  404. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  405. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  406. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  407. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  408. package/src/__tests__/swarm-recursion.test.ts +0 -197
  409. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  410. package/src/__tests__/swarm-tool.test.ts +0 -185
  411. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  412. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  413. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  414. package/src/commands/cc-command-registry.ts +0 -248
  415. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  416. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  417. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  418. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  419. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  420. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  421. package/src/config/schemas/swarm.ts +0 -82
  422. package/src/logfire.ts +0 -135
  423. package/src/memory/search/lexical.ts +0 -48
  424. package/src/providers/failover.ts +0 -186
  425. package/src/runtime/local-gateway-health.ts +0 -275
  426. package/src/security/secret-ingress.ts +0 -68
  427. package/src/swarm/backend-claude-code.ts +0 -225
  428. package/src/swarm/checkpoint.ts +0 -137
  429. package/src/swarm/graph-utils.ts +0 -53
  430. package/src/swarm/index.ts +0 -55
  431. package/src/swarm/limits.ts +0 -66
  432. package/src/swarm/orchestrator.ts +0 -424
  433. package/src/swarm/plan-validator.ts +0 -117
  434. package/src/swarm/router-planner.ts +0 -162
  435. package/src/swarm/router-prompts.ts +0 -39
  436. package/src/swarm/synthesizer.ts +0 -81
  437. package/src/swarm/types.ts +0 -72
  438. package/src/swarm/worker-backend.ts +0 -131
  439. package/src/swarm/worker-prompts.ts +0 -80
  440. package/src/swarm/worker-runner.ts +0 -170
  441. package/src/tools/claude-code/claude-code.ts +0 -610
  442. package/src/tools/swarm/delegate.ts +0 -205
@@ -0,0 +1,254 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { sanitizeForTts } from "../tts-text-sanitizer.js";
4
+
5
+ describe("sanitizeForTts", () => {
6
+ describe("markdown links", () => {
7
+ test("strips markdown links, keeping link text", () => {
8
+ expect(sanitizeForTts("Check [this link](https://example.com)")).toBe(
9
+ "Check this link",
10
+ );
11
+ });
12
+
13
+ test("handles multiple links", () => {
14
+ expect(
15
+ sanitizeForTts("See [foo](http://a.com) and [bar](http://b.com)"),
16
+ ).toBe("See foo and bar");
17
+ });
18
+
19
+ test("preserves Fish Audio S2 bracket annotations", () => {
20
+ expect(sanitizeForTts("Hello [laughter] world")).toBe(
21
+ "Hello [laughter] world",
22
+ );
23
+ expect(sanitizeForTts("[breath] ok")).toBe("[breath] ok");
24
+ });
25
+
26
+ test("handles URLs with balanced parentheses (e.g. Wikipedia)", () => {
27
+ expect(
28
+ sanitizeForTts(
29
+ "See [Function](https://en.wikipedia.org/wiki/Function_(mathematics))",
30
+ ),
31
+ ).toBe("See Function");
32
+ });
33
+
34
+ test("handles URLs with multiple balanced parentheses groups", () => {
35
+ expect(
36
+ sanitizeForTts("[link](http://example.com/a_(b)_c_(d))"),
37
+ ).toBe("link");
38
+ });
39
+ });
40
+
41
+ describe("bold and italic", () => {
42
+ test("strips bold (asterisks)", () => {
43
+ expect(sanitizeForTts("Hello **world**")).toBe("Hello world");
44
+ });
45
+
46
+ test("strips bold (underscores)", () => {
47
+ expect(sanitizeForTts("Hello __world__")).toBe("Hello world");
48
+ });
49
+
50
+ test("strips italic (asterisks)", () => {
51
+ expect(sanitizeForTts("Hello *world*")).toBe("Hello world");
52
+ });
53
+
54
+ test("strips italic (underscores)", () => {
55
+ expect(sanitizeForTts("Hello _world_")).toBe("Hello world");
56
+ });
57
+
58
+ test("strips bold+italic (asterisks)", () => {
59
+ expect(sanitizeForTts("Hello ***world***")).toBe("Hello world");
60
+ });
61
+
62
+ test("strips bold+italic (underscores)", () => {
63
+ expect(sanitizeForTts("Hello ___world___")).toBe("Hello world");
64
+ });
65
+
66
+ test("preserves arithmetic asterisks", () => {
67
+ expect(sanitizeForTts("5 * 3 = 15")).toBe("5 * 3 = 15");
68
+ });
69
+
70
+ test("preserves identifiers with underscores", () => {
71
+ expect(sanitizeForTts("The my_var variable")).toBe(
72
+ "The my_var variable",
73
+ );
74
+ });
75
+
76
+ test("preserves snake_case identifiers", () => {
77
+ expect(sanitizeForTts("use some_function_name here")).toBe(
78
+ "use some_function_name here",
79
+ );
80
+ });
81
+ });
82
+
83
+ describe("headers", () => {
84
+ test("strips h1", () => {
85
+ expect(sanitizeForTts("# Header\n\nSome text")).toBe(
86
+ "Header\n\nSome text",
87
+ );
88
+ });
89
+
90
+ test("strips h2", () => {
91
+ expect(sanitizeForTts("## Sub Header")).toBe("Sub Header");
92
+ });
93
+
94
+ test("strips h3 through h6", () => {
95
+ expect(sanitizeForTts("### H3")).toBe("H3");
96
+ expect(sanitizeForTts("#### H4")).toBe("H4");
97
+ expect(sanitizeForTts("##### H5")).toBe("H5");
98
+ expect(sanitizeForTts("###### H6")).toBe("H6");
99
+ });
100
+
101
+ test("does not strip # in middle of line", () => {
102
+ expect(sanitizeForTts("Issue #42")).toBe("Issue #42");
103
+ });
104
+ });
105
+
106
+ describe("code", () => {
107
+ test("strips code fences, keeping content", () => {
108
+ const input = "Here:\n```js\nconst x = 1;\n```\nDone.";
109
+ expect(sanitizeForTts(input)).toBe("Here:\nconst x = 1;\nDone.");
110
+ });
111
+
112
+ test("strips inline code backticks", () => {
113
+ expect(sanitizeForTts("Use `console.log` here")).toBe(
114
+ "Use console.log here",
115
+ );
116
+ });
117
+
118
+ test("preserves # comments inside code fences", () => {
119
+ const input = "Example:\n```python\n# This is a comment\nprint('hi')\n```\nDone.";
120
+ expect(sanitizeForTts(input)).toBe(
121
+ "Example:\n# This is a comment\nprint('hi')\nDone.",
122
+ );
123
+ });
124
+
125
+ test("preserves shell comments inside code fences", () => {
126
+ const input = "Run:\n```bash\n## Install deps\napt-get install curl\n```";
127
+ expect(sanitizeForTts(input)).toBe(
128
+ "Run:\n## Install deps\napt-get install curl\n",
129
+ );
130
+ });
131
+ });
132
+
133
+ describe("bullet markers", () => {
134
+ test("strips dash bullets", () => {
135
+ expect(sanitizeForTts("- First\n- Second")).toBe("First\nSecond");
136
+ });
137
+
138
+ test("strips asterisk bullets", () => {
139
+ expect(sanitizeForTts("* First\n* Second")).toBe("First\nSecond");
140
+ });
141
+
142
+ test("does not strip dashes mid-line", () => {
143
+ expect(sanitizeForTts("well-known fact")).toBe("well-known fact");
144
+ });
145
+ });
146
+
147
+ describe("emojis", () => {
148
+ test("strips simple emojis", () => {
149
+ expect(sanitizeForTts("Hello world 👋")).toBe("Hello world ");
150
+ });
151
+
152
+ test("strips compound emojis (ZWJ sequences)", () => {
153
+ // Family emoji: man + ZWJ + woman + ZWJ + girl
154
+ expect(sanitizeForTts("Family: 👨‍👩‍👧")).toBe("Family: ");
155
+ });
156
+
157
+ test("strips emojis with skin tone modifiers", () => {
158
+ expect(sanitizeForTts("Wave 👋🏽 hello")).toBe("Wave hello");
159
+ });
160
+
161
+ test("strips flag emojis", () => {
162
+ // Flags are regional indicator sequences (Extended_Pictographic)
163
+ expect(sanitizeForTts("Hello 🇺🇸 world")).toBe("Hello world");
164
+ });
165
+
166
+ test("strips emojis with variation selectors", () => {
167
+ // Heart with variation selector
168
+ expect(sanitizeForTts("Love ❤️ you")).toBe("Love you");
169
+ });
170
+
171
+ test("preserves numbers and currency", () => {
172
+ expect(sanitizeForTts("$100.50 and €200")).toBe("$100.50 and €200");
173
+ });
174
+
175
+ test("preserves punctuation", () => {
176
+ expect(sanitizeForTts("Hello, world! How are you?")).toBe(
177
+ "Hello, world! How are you?",
178
+ );
179
+ });
180
+ });
181
+
182
+ describe("whitespace collapsing", () => {
183
+ test("collapses multiple spaces to single space", () => {
184
+ expect(sanitizeForTts("Hello world")).toBe("Hello world");
185
+ });
186
+
187
+ test("collapses multiple blank lines to single newline", () => {
188
+ expect(sanitizeForTts("Hello\n\n\n\nworld")).toBe("Hello\n\nworld");
189
+ });
190
+ });
191
+
192
+ describe("combined transformations", () => {
193
+ test("acceptance: Hello **world** with emoji", () => {
194
+ expect(sanitizeForTts("Hello **world** 👋")).toBe("Hello world ");
195
+ });
196
+
197
+ test("acceptance: markdown link", () => {
198
+ expect(
199
+ sanitizeForTts("Check [this link](https://example.com)"),
200
+ ).toBe("Check this link");
201
+ });
202
+
203
+ test("acceptance: arithmetic preserved", () => {
204
+ expect(sanitizeForTts("5 * 3 = 15")).toBe("5 * 3 = 15");
205
+ });
206
+
207
+ test("acceptance: header with text", () => {
208
+ expect(sanitizeForTts("# Header\n\nSome text")).toBe(
209
+ "Header\n\nSome text",
210
+ );
211
+ });
212
+
213
+ test("complex mixed markdown and emojis", () => {
214
+ const input =
215
+ "# Welcome 🎉\n\nHello **world**! Check [docs](http://x.com).\n\n- Item *one*\n- Item `two`";
216
+ const expected =
217
+ "Welcome \n\nHello world! Check docs.\n\nItem one\nItem two";
218
+ expect(sanitizeForTts(input)).toBe(expected);
219
+ });
220
+ });
221
+
222
+ describe("edge cases", () => {
223
+ test("empty string", () => {
224
+ expect(sanitizeForTts("")).toBe("");
225
+ });
226
+
227
+ test("already-clean text", () => {
228
+ const clean = "Hello, this is plain text.";
229
+ expect(sanitizeForTts(clean)).toBe(clean);
230
+ });
231
+
232
+ test("nested markdown (bold inside italic)", () => {
233
+ expect(sanitizeForTts("*Hello **world***")).toBe("Hello world");
234
+ });
235
+
236
+ test("partial markdown (unmatched asterisks)", () => {
237
+ expect(sanitizeForTts("Hello **world")).toBe("Hello **world");
238
+ });
239
+
240
+ test("idempotency: applying twice gives same result", () => {
241
+ const input = "# Hello **world** 👋\n\n- Item *one*\n- [link](http://x.com)";
242
+ const once = sanitizeForTts(input);
243
+ const twice = sanitizeForTts(once);
244
+ expect(twice).toBe(once);
245
+ });
246
+
247
+ test("preserves trailing whitespace for streaming chunks", () => {
248
+ // Streaming chunks must keep trailing spaces so word boundaries survive
249
+ // concatenation: "Hello " + "world" = "Hello world", not "Helloworld"
250
+ expect(sanitizeForTts("Hello ")).toBe("Hello ");
251
+ expect(sanitizeForTts("the quick ")).toBe("the quick ");
252
+ });
253
+ });
254
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { getAudio, storeAudio } from "./audio-store.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Reset module-level state between tests by re-importing.
11
+ * Since the store uses module-level variables, we isolate via fresh imports
12
+ * where needed, but for most tests the shared module state is fine as long
13
+ * as we account for it.
14
+ */
15
+
16
+ function makeBuffer(sizeBytes: number): Buffer {
17
+ return Buffer.alloc(sizeBytes, 0x42);
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Tests
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe("audio-store", () => {
25
+ describe("storeAudio / getAudio", () => {
26
+ test("stores and retrieves audio by id", () => {
27
+ const buf = makeBuffer(1024);
28
+ const id = storeAudio(buf, "mp3");
29
+ const result = getAudio(id);
30
+ expect(result).not.toBeNull();
31
+ expect(result!.type).toBe("buffer");
32
+ if (result!.type === "buffer") {
33
+ expect(result!.buffer).toEqual(buf);
34
+ }
35
+ expect(result!.contentType).toBe("audio/mpeg");
36
+ });
37
+
38
+ test("returns correct content type for each format", () => {
39
+ const buf = makeBuffer(64);
40
+
41
+ const mp3Id = storeAudio(buf, "mp3");
42
+ expect(getAudio(mp3Id)!.contentType).toBe("audio/mpeg");
43
+
44
+ const wavId = storeAudio(buf, "wav");
45
+ expect(getAudio(wavId)!.contentType).toBe("audio/wav");
46
+
47
+ const opusId = storeAudio(buf, "opus");
48
+ expect(getAudio(opusId)!.contentType).toBe("audio/opus");
49
+ });
50
+
51
+ test("returns null for unknown id", () => {
52
+ expect(getAudio("nonexistent-id")).toBeNull();
53
+ });
54
+ });
55
+
56
+ describe("TTL expiration", () => {
57
+ test("expired entries return null", () => {
58
+ const buf = makeBuffer(128);
59
+ const id = storeAudio(buf, "wav");
60
+
61
+ // Fast-forward time past TTL (60s)
62
+ const originalNow = Date.now;
63
+ Date.now = () => originalNow() + 61_000;
64
+ try {
65
+ const result = getAudio(id);
66
+ expect(result).toBeNull();
67
+ } finally {
68
+ Date.now = originalNow;
69
+ }
70
+ });
71
+ });
72
+
73
+ describe("capacity eviction", () => {
74
+ test("evicts oldest entries when capacity is exceeded", () => {
75
+ // The store has a 50MB cap. Fill it with entries, then add one more
76
+ // that would exceed the cap. The oldest should be evicted.
77
+ const chunkSize = 10 * 1024 * 1024; // 10MB per chunk
78
+ const ids: string[] = [];
79
+
80
+ // Store 5 x 10MB = 50MB (at capacity)
81
+ for (let i = 0; i < 5; i++) {
82
+ ids.push(storeAudio(makeBuffer(chunkSize), "opus"));
83
+ }
84
+
85
+ // All 5 should be retrievable
86
+ for (const id of ids) {
87
+ expect(getAudio(id)).not.toBeNull();
88
+ }
89
+
90
+ // Add one more 10MB entry — should evict the oldest
91
+ const newId = storeAudio(makeBuffer(chunkSize), "mp3");
92
+ expect(getAudio(newId)).not.toBeNull();
93
+ // The first entry should have been evicted
94
+ expect(getAudio(ids[0]!)).toBeNull();
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,205 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ interface AudioEntry {
4
+ buffer: Buffer;
5
+ contentType: string;
6
+ expiresAt: number;
7
+ }
8
+
9
+ interface StreamingAudioEntry {
10
+ contentType: string;
11
+ expiresAt: number;
12
+ chunks: Uint8Array[];
13
+ totalBytes: number;
14
+ complete: boolean;
15
+ subscribers: Set<ReadableStreamDefaultController<Uint8Array>>;
16
+ }
17
+
18
+ const store = new Map<string, AudioEntry>();
19
+ const streamingStore = new Map<string, StreamingAudioEntry>();
20
+ const MAX_STORE_BYTES = 50 * 1024 * 1024; // 50MB cap
21
+ const TTL_MS = 60_000; // 60 seconds
22
+
23
+ let currentBytes = 0;
24
+
25
+ export function storeAudio(
26
+ buffer: Buffer,
27
+ format: "mp3" | "wav" | "opus",
28
+ ): string {
29
+ evictExpired();
30
+ // Evict oldest if over capacity
31
+ while (currentBytes + buffer.length > MAX_STORE_BYTES && store.size > 0) {
32
+ const oldest = store.keys().next().value;
33
+ if (oldest) removeEntry(oldest);
34
+ }
35
+ const id = randomUUID();
36
+ const contentType = contentTypeForFormat(format);
37
+ store.set(id, { buffer, contentType, expiresAt: Date.now() + TTL_MS });
38
+ currentBytes += buffer.length;
39
+ return id;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Streaming entries — audio is pushed chunk-by-chunk while being served
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export interface StreamingAudioHandle {
47
+ audioId: string;
48
+ push: (chunk: Uint8Array) => void;
49
+ finalize: () => void;
50
+ }
51
+
52
+ export function createStreamingEntry(
53
+ format: "mp3" | "wav" | "opus",
54
+ ): StreamingAudioHandle {
55
+ evictExpired();
56
+ const id = randomUUID();
57
+ const contentType = contentTypeForFormat(format);
58
+ const entry: StreamingAudioEntry = {
59
+ contentType,
60
+ expiresAt: Date.now() + TTL_MS,
61
+ chunks: [],
62
+ totalBytes: 0,
63
+ complete: false,
64
+ subscribers: new Set(),
65
+ };
66
+ streamingStore.set(id, entry);
67
+
68
+ return {
69
+ audioId: id,
70
+ push(chunk: Uint8Array) {
71
+ entry.chunks.push(chunk);
72
+ entry.totalBytes += chunk.byteLength;
73
+ for (const controller of entry.subscribers) {
74
+ try {
75
+ controller.enqueue(chunk);
76
+ } catch {
77
+ entry.subscribers.delete(controller);
78
+ }
79
+ }
80
+ },
81
+ finalize() {
82
+ entry.complete = true;
83
+ for (const controller of entry.subscribers) {
84
+ try {
85
+ controller.close();
86
+ } catch {
87
+ // Already closed
88
+ }
89
+ }
90
+ entry.subscribers.clear();
91
+ },
92
+ };
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Retrieval — handles both regular and streaming entries
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export type AudioResult =
100
+ | { type: "buffer"; buffer: Buffer; contentType: string }
101
+ | { type: "stream"; stream: ReadableStream<Uint8Array>; contentType: string };
102
+
103
+ export function getAudio(id: string): AudioResult | null {
104
+ evictExpired();
105
+
106
+ // Check streaming store first
107
+ const streamingEntry = streamingStore.get(id);
108
+ if (streamingEntry) {
109
+ if (Date.now() > streamingEntry.expiresAt) {
110
+ streamingStore.delete(id);
111
+ return null;
112
+ }
113
+
114
+ if (streamingEntry.complete) {
115
+ // Synthesis finished — serve the complete buffer
116
+ const merged = mergeChunks(streamingEntry.chunks);
117
+ return {
118
+ type: "buffer",
119
+ buffer: Buffer.from(merged),
120
+ contentType: streamingEntry.contentType,
121
+ };
122
+ }
123
+
124
+ // Still streaming — return a ReadableStream that replays existing
125
+ // chunks and subscribes for future ones.
126
+ let ctrl: ReadableStreamDefaultController<Uint8Array>;
127
+ const stream = new ReadableStream<Uint8Array>({
128
+ start(controller) {
129
+ ctrl = controller;
130
+ for (const chunk of streamingEntry.chunks) {
131
+ controller.enqueue(chunk);
132
+ }
133
+ if (streamingEntry.complete) {
134
+ controller.close();
135
+ } else {
136
+ streamingEntry.subscribers.add(controller);
137
+ }
138
+ },
139
+ cancel() {
140
+ streamingEntry.subscribers.delete(ctrl);
141
+ },
142
+ });
143
+
144
+ return { type: "stream", stream, contentType: streamingEntry.contentType };
145
+ }
146
+
147
+ // Check regular store
148
+ const entry = store.get(id);
149
+ if (!entry) return null;
150
+ if (Date.now() > entry.expiresAt) {
151
+ removeEntry(id);
152
+ return null;
153
+ }
154
+ return { type: "buffer", buffer: entry.buffer, contentType: entry.contentType };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Internal helpers
159
+ // ---------------------------------------------------------------------------
160
+
161
+ function contentTypeForFormat(format: "mp3" | "wav" | "opus"): string {
162
+ return format === "mp3"
163
+ ? "audio/mpeg"
164
+ : format === "wav"
165
+ ? "audio/wav"
166
+ : "audio/opus";
167
+ }
168
+
169
+ function mergeChunks(chunks: Uint8Array[]): Uint8Array {
170
+ const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
171
+ const merged = new Uint8Array(totalLength);
172
+ let offset = 0;
173
+ for (const chunk of chunks) {
174
+ merged.set(chunk, offset);
175
+ offset += chunk.byteLength;
176
+ }
177
+ return merged;
178
+ }
179
+
180
+ function removeEntry(id: string): void {
181
+ const entry = store.get(id);
182
+ if (entry) {
183
+ currentBytes -= entry.buffer.length;
184
+ store.delete(id);
185
+ }
186
+ }
187
+
188
+ function evictExpired(): void {
189
+ const now = Date.now();
190
+ for (const [id, entry] of store) {
191
+ if (now > entry.expiresAt) removeEntry(id);
192
+ }
193
+ for (const [id, entry] of streamingStore) {
194
+ if (now > entry.expiresAt) {
195
+ for (const controller of entry.subscribers) {
196
+ try {
197
+ controller.close();
198
+ } catch {
199
+ // noop
200
+ }
201
+ }
202
+ streamingStore.delete(id);
203
+ }
204
+ }
205
+ }