@vellumai/assistant 0.11.3 → 0.11.4-staging.1

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 (324) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/docs/architecture/memory.md +11 -0
  3. package/docs/architecture/turn-actor.md +70 -0
  4. package/docs/flux-turn-detection-spike.md +243 -0
  5. package/docs/stt-provider-onboarding.md +3 -1
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  8. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  10. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  11. package/openapi.yaml +140 -38
  12. package/package.json +1 -1
  13. package/scripts/voice-ttft-spike.ts +3 -3
  14. package/src/__tests__/app-compiler.test.ts +38 -3
  15. package/src/__tests__/attachments-store.test.ts +21 -12
  16. package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
  17. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  18. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  19. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  20. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  21. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  22. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  23. package/src/__tests__/config-schema.test.ts +25 -10
  24. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  26. package/src/__tests__/conversation-agent-loop.test.ts +28 -20
  27. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  28. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  29. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  30. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  31. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  32. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  33. package/src/__tests__/conversation-queue.test.ts +55 -4
  34. package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
  35. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  36. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  37. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  38. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  39. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  40. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  41. package/src/__tests__/edit-propagation.test.ts +0 -7
  42. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  43. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  44. package/src/__tests__/injector-chain.test.ts +63 -41
  45. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  46. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  47. package/src/__tests__/llm-schema.test.ts +5 -2
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  49. package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
  50. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  51. package/src/__tests__/mtime-cache.test.ts +1 -1
  52. package/src/__tests__/non-member-access-request.test.ts +0 -20
  53. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  54. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
  55. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  56. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  57. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  58. package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
  59. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  60. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  61. package/src/__tests__/skills.test.ts +32 -0
  62. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  63. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  64. package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
  65. package/src/__tests__/turn-events-store.test.ts +43 -0
  66. package/src/__tests__/user-plugin-loader.test.ts +1 -1
  67. package/src/__tests__/visible-app-context.test.ts +16 -9
  68. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  69. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  70. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  71. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  72. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  73. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  74. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  75. package/src/acp/__tests__/auth-required.test.ts +161 -0
  76. package/src/acp/acp-claude-oauth.ts +19 -2
  77. package/src/acp/agent-process.test.ts +100 -0
  78. package/src/acp/agent-process.ts +29 -26
  79. package/src/acp/auth-required.ts +102 -0
  80. package/src/acp/session-manager.test.ts +119 -0
  81. package/src/acp/session-manager.ts +68 -2
  82. package/src/api/events/acp-auth-required.ts +55 -0
  83. package/src/api/index.ts +7 -0
  84. package/src/apps/app-store.ts +3 -0
  85. package/src/bundler/package-resolver.ts +2 -30
  86. package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
  87. package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
  88. package/src/calls/call-controller.ts +9 -2
  89. package/src/calls/call-setup-flow.ts +0 -1
  90. package/src/calls/media-stream-stt-session.ts +15 -0
  91. package/src/calls/voice-session-bridge.ts +71 -16
  92. package/src/calls/voice-triage-escalate.ts +104 -2
  93. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  94. package/src/channels/config.ts +13 -0
  95. package/src/channels/plugin-channel-declarations.ts +108 -0
  96. package/src/channels/types.ts +30 -0
  97. package/src/cli/AGENTS.md +5 -2
  98. package/src/cli/commands/credentials.help.ts +2 -2
  99. package/src/cli/commands/inference-providers.ts +1 -1
  100. package/src/cli/commands/mcp.help.ts +13 -4
  101. package/src/cli/commands/mcp.ts +9 -0
  102. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  103. package/src/cli/commands/memory/index.help.ts +43 -1
  104. package/src/cli/commands/memory/memory-v3.ts +64 -0
  105. package/src/cli/commands/stt.help.ts +27 -2
  106. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  107. package/src/cli/lib/bundled-marketplace.json +13 -0
  108. package/src/cli/lib/upgrade-plugin.ts +42 -0
  109. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  110. package/src/config/__tests__/default-provider.test.ts +6 -1
  111. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  112. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  113. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  114. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  115. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  116. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  117. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  118. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  119. package/src/config/call-site-defaults.ts +4 -5
  120. package/src/config/default-profile-catalog.ts +82 -11
  121. package/src/config/default-profile-names.ts +4 -1
  122. package/src/config/default-provider-resolution.ts +4 -0
  123. package/src/config/llm-context-resolution.ts +11 -3
  124. package/src/config/llm-resolver.ts +28 -1
  125. package/src/config/profile-materialization.ts +70 -22
  126. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  127. package/src/config/schemas/call-site-catalog.ts +4 -4
  128. package/src/config/schemas/live-voice.ts +57 -23
  129. package/src/config/schemas/llm.ts +59 -32
  130. package/src/config/schemas/mcp.ts +23 -0
  131. package/src/config/schemas/plugin-updates.ts +6 -2
  132. package/src/config/schemas/stt.ts +1 -0
  133. package/src/context/outbound-sanitize.ts +96 -1
  134. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  135. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  136. package/src/daemon/conversation-agent-loop.ts +17 -6
  137. package/src/daemon/conversation-messaging.ts +5 -1
  138. package/src/daemon/conversation-notifiers.ts +9 -1
  139. package/src/daemon/conversation-process.ts +9 -6
  140. package/src/daemon/conversation-runtime-assembly.ts +3 -4
  141. package/src/daemon/conversation-tool-setup.ts +1 -2
  142. package/src/daemon/conversation.ts +48 -0
  143. package/src/daemon/mcp-reload-service.ts +36 -6
  144. package/src/daemon/process-message.ts +13 -3
  145. package/src/daemon/providers-setup.ts +6 -3
  146. package/src/daemon/trust-context-types.ts +29 -0
  147. package/src/daemon/wake-conversation-ops.ts +3 -2
  148. package/src/documents/document-store.ts +138 -5
  149. package/src/hooks/hook-loader.ts +3 -3
  150. package/src/hooks/registry.ts +50 -6
  151. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  152. package/src/inbound/oauth-callback-url.ts +61 -0
  153. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  154. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  155. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
  156. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  157. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  158. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  159. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  160. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  161. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  162. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  163. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  164. package/src/live-voice/live-voice-archive.ts +2 -0
  165. package/src/live-voice/live-voice-metrics.ts +57 -32
  166. package/src/live-voice/live-voice-photo.ts +1 -2
  167. package/src/live-voice/live-voice-session.ts +535 -314
  168. package/src/live-voice/progress-narration.ts +277 -0
  169. package/src/live-voice/protocol.ts +21 -1
  170. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  171. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  172. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  173. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  174. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  175. package/src/mcp/client.ts +16 -11
  176. package/src/mcp/effective-config.ts +113 -0
  177. package/src/mcp/manager.ts +11 -6
  178. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  179. package/src/mcp/mcp-oauth-provider.ts +205 -240
  180. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  181. package/src/monitoring/plugin-auto-update.ts +128 -24
  182. package/src/notifications/signal.ts +1 -0
  183. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  184. package/src/permissions/confirmation-guardian-request.ts +2 -2
  185. package/src/permissions/question-guardian-request.test.ts +14 -6
  186. package/src/permissions/question-guardian-request.ts +1 -2
  187. package/src/persistence/attachments-store.ts +8 -1
  188. package/src/persistence/bookmark-crud.ts +3 -7
  189. package/src/persistence/conversation-attention-store.ts +16 -45
  190. package/src/persistence/conversation-crud.ts +33 -4
  191. package/src/persistence/conversation-lineage.ts +9 -0
  192. package/src/persistence/conversation-queries.ts +108 -41
  193. package/src/persistence/delivery-crud.ts +38 -29
  194. package/src/persistence/external-conversation-store.ts +32 -4
  195. package/src/persistence/llm-request-log-store.ts +4 -10
  196. package/src/persistence/llm-usage-store.ts +8 -3
  197. package/src/persistence/message-reads.test.ts +197 -0
  198. package/src/persistence/message-reads.ts +211 -0
  199. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  200. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  201. package/src/persistence/real-user-turn-filter.ts +27 -3
  202. package/src/persistence/steps.ts +9 -0
  203. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  204. package/src/plugin-api/conversation-turn.ts +168 -5
  205. package/src/plugin-api/index.ts +21 -5
  206. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  207. package/src/plugins/defaults/memory/AGENTS.md +4 -0
  208. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  209. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  210. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  211. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  212. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  213. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  214. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  215. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  216. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  217. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
  218. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  219. package/src/plugins/defaults/memory/indexer.ts +3 -1
  220. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  221. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  222. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  223. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
  224. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
  225. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
  226. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  227. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
  228. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  229. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  230. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  231. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  232. package/src/plugins/defaults/memory/worker.ts +6 -3
  233. package/src/plugins/external-plugin-loader.ts +47 -0
  234. package/src/plugins/mcp-servers.ts +361 -0
  235. package/src/plugins/mtime-cache.ts +23 -49
  236. package/src/plugins/worker-plugin-surface.ts +33 -0
  237. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  238. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  239. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  240. package/src/providers/__tests__/retry-callsite.test.ts +5 -2
  241. package/src/providers/call-site-routing.ts +30 -3
  242. package/src/providers/connection-resolution.ts +194 -11
  243. package/src/providers/inference/auth.ts +6 -0
  244. package/src/providers/inference/connection-availability.ts +24 -2
  245. package/src/providers/inference/connections.ts +2 -0
  246. package/src/providers/model-intents.ts +26 -6
  247. package/src/providers/openai/codex-models.ts +2 -1
  248. package/src/providers/provider-send-message.ts +32 -3
  249. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  250. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
  251. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  252. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  253. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  254. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
  255. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  256. package/src/providers/speech-to-text/resolve.ts +25 -2
  257. package/src/routes/worker.ts +17 -5
  258. package/src/runtime/access-request-helper.ts +9 -12
  259. package/src/runtime/agent-wake.ts +3 -3
  260. package/src/runtime/pre-first-message-gate.ts +4 -0
  261. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  262. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
  263. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  264. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  265. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  266. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  267. package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
  268. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  269. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
  270. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  271. package/src/runtime/routes/channel-route-shared.ts +0 -6
  272. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  273. package/src/runtime/routes/conversation-list-routes.ts +112 -1
  274. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  275. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  276. package/src/runtime/routes/default-provider-routes.ts +15 -0
  277. package/src/runtime/routes/inbound-message-handler.ts +17 -41
  278. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  279. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  280. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  281. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  282. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  283. package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
  284. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  285. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
  286. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
  287. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
  288. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  289. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  291. package/src/runtime/routes/log-export-routes.ts +3 -0
  292. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  293. package/src/runtime/routes/plugins-routes.ts +21 -3
  294. package/src/runtime/routes/stt-routes.ts +31 -25
  295. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  296. package/src/runtime/routes/user-route-dispatcher.ts +39 -14
  297. package/src/runtime/routes/user-route-import.ts +108 -0
  298. package/src/schedule/worker.ts +6 -0
  299. package/src/security/oauth2.ts +6 -22
  300. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  301. package/src/stt/__tests__/types.test.ts +94 -0
  302. package/src/stt/daemon-batch-transcriber.ts +10 -0
  303. package/src/stt/stt-stream-session.ts +8 -4
  304. package/src/stt/types.ts +103 -0
  305. package/src/subagent/manager.ts +1 -3
  306. package/src/subagent/types.ts +7 -6
  307. package/src/tools/acp/spawn.test.ts +97 -0
  308. package/src/tools/acp/spawn.ts +32 -0
  309. package/src/tools/document/document-tool.ts +12 -3
  310. package/src/tools/registry.ts +2 -1
  311. package/src/tools/workflows/run-workflow.ts +1 -2
  312. package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
  313. package/src/tts/reasoning-tag-filter.ts +110 -0
  314. package/src/workspace/byok-default-profile-ensure.ts +61 -18
  315. package/src/workspace/custom-profile-ensure.ts +4 -24
  316. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  317. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  318. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  319. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  320. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  321. package/src/workspace/migrations/registry.ts +8 -0
  322. package/src/workspace/provider-commit-message-generator.ts +7 -5
  323. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  324. package/src/live-voice/front-decision.ts +0 -476
@@ -14,6 +14,7 @@ mock.module("../daemon/date-context.js", () => ({
14
14
  formatTurnTimestamp: () => FIXED_TURN_TIMESTAMP,
15
15
  }));
16
16
 
17
+ import type { Conversation } from "../daemon/conversation.js";
17
18
  import {
18
19
  clearConversations,
19
20
  setConversation,
@@ -39,6 +40,7 @@ import {
39
40
  } from "../plugins/defaults/workspace/injectors.js";
40
41
  import type { Injector, TurnContext } from "../plugins/types.js";
41
42
  import type { Message } from "../providers/types.js";
43
+ import { asConversation } from "./helpers/mock-conversation.js";
42
44
 
43
45
  // `applyRuntimeInjections` self-resolves the Slack active-thread focus block
44
46
  // from the persisted message rows, so the schema must exist for Slack-channel
@@ -90,38 +92,23 @@ const diskPressureInjector = findInjector("disk-pressure-warning");
90
92
  // turn can exercise both; the seed helpers mutate and re-register the same
91
93
  // instance. An empty (non-dirty) workspace cache resolves to no block, so
92
94
  // disk-pressure-only tests don't trigger a directory scan.
93
- let liveConversation: {
94
- conversationId: string;
95
- workingDir: string;
96
- workspaceTopLevelContext: string | null;
97
- workspaceTopLevelDirty: boolean;
98
- diskPressureCleanupModeActive: boolean;
99
- channelCapabilities?: ChannelCapabilities;
100
- trustContext?: { trustClass: string };
101
- currentTurnTemporalSnapshot?: {
102
- clientTimezone: string | null;
103
- };
104
- currentTurnInterfaceContext?: {
105
- userMessageInterface: string;
106
- assistantMessageInterface: string;
107
- };
108
- };
95
+ let liveConversation: Conversation;
109
96
 
110
97
  function resetLiveConversation(): void {
111
- liveConversation = {
98
+ liveConversation = asConversation({
112
99
  conversationId: TEST_CONVERSATION_ID,
113
100
  workingDir: "/workspace",
114
101
  workspaceTopLevelContext: "",
115
102
  workspaceTopLevelDirty: false,
116
103
  diskPressureCleanupModeActive: false,
117
- trustContext: { trustClass: "guardian" },
104
+ trustContext: { trustClass: "guardian", sourceChannel: "vellum" },
118
105
  // The unified-turn-context injector sources the interface label from the
119
106
  // live conversation's turn interface context; match the expected blocks.
120
107
  currentTurnInterfaceContext: {
121
108
  userMessageInterface: "macos",
122
109
  assistantMessageInterface: "macos",
123
110
  },
124
- };
111
+ });
125
112
  }
126
113
 
127
114
  // `applyRuntimeInjections` gates the `<turn_context>` block on the live
@@ -130,24 +117,25 @@ function resetLiveConversation(): void {
130
117
  function seedTemporalSnapshot(): void {
131
118
  liveConversation.currentTurnTemporalSnapshot = {
132
119
  clientTimezone: null,
120
+ timeSinceLastMessage: null,
133
121
  };
134
- setConversation(TEST_CONVERSATION_ID, liveConversation as never);
122
+ setConversation(TEST_CONVERSATION_ID, liveConversation);
135
123
  }
136
124
 
137
125
  function seedChannelCapabilities(caps: ChannelCapabilities): void {
138
126
  liveConversation.channelCapabilities = caps;
139
- setConversation(TEST_CONVERSATION_ID, liveConversation as never);
127
+ setConversation(TEST_CONVERSATION_ID, liveConversation);
140
128
  }
141
129
 
142
130
  function seedDiskPressure(cleanupModeActive: boolean): void {
143
131
  liveConversation.diskPressureCleanupModeActive = cleanupModeActive;
144
- setConversation(TEST_CONVERSATION_ID, liveConversation as never);
132
+ setConversation(TEST_CONVERSATION_ID, liveConversation);
145
133
  }
146
134
 
147
135
  function seedWorkspaceContext(text: string): void {
148
136
  liveConversation.workspaceTopLevelContext = text;
149
137
  liveConversation.workspaceTopLevelDirty = false;
150
- setConversation(TEST_CONVERSATION_ID, liveConversation as never);
138
+ setConversation(TEST_CONVERSATION_ID, liveConversation);
151
139
  }
152
140
 
153
141
  // Persist Slack-channel rows for the turn conversation so
@@ -1,4 +1,21 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ // Entry-name providers resolve context limits through their row's kind, so
4
+ // the tests control the row store directly. A dangling label (no row) falls
5
+ // back to the model's catalog owner.
6
+ const connectionRows = new Map<string, { name: string; provider: string }>([
7
+ ["openai-work", { name: "openai-work", provider: "openai" }],
8
+ ["my-endpoint", { name: "my-endpoint", provider: "openai-compatible" }],
9
+ ]);
10
+ mock.module("../persistence/db-connection.js", () => ({
11
+ getDb: () => ({}),
12
+ }));
13
+ mock.module("../providers/inference/connections.js", () => ({
14
+ getConnection: (_db: unknown, name: string) =>
15
+ connectionRows.get(name) ?? null,
16
+ listConnections: () => [],
17
+ canonicalVellumConnection: () => null,
18
+ }));
2
19
 
3
20
  import { resolveEffectiveContextWindow } from "../config/llm-context-resolution.js";
4
21
  import { LLMSchema } from "../config/schemas/llm.js";
@@ -131,6 +148,61 @@ describe("resolveEffectiveContextWindow", () => {
131
148
  expect(resolved.modelMaxInputTokens).toBe(1050000);
132
149
  });
133
150
 
151
+ test("an entry of a catalog kind resolves the model's own context limits", () => {
152
+ // The entries collapse stores connection names in the provider field;
153
+ // the row's kind carries the model's limits, so an entry label must not
154
+ // fall back to the 200k default when its kind serves the model.
155
+ const llm = LLMSchema.parse({
156
+ profiles: {
157
+ work: { provider: "openai-work", model: "gpt-5.5" },
158
+ },
159
+ activeProfile: "work",
160
+ });
161
+
162
+ const resolved = resolveEffectiveContextWindow({
163
+ llm,
164
+ callSite: "mainAgent",
165
+ });
166
+
167
+ expect(resolved.modelMaxInputTokens).toBe(1050000);
168
+ expect(resolved.maxOutputTokens).toBeDefined();
169
+ });
170
+
171
+ test("a custom-endpoint entry keeps the conservative default for a catalog-colliding model id", () => {
172
+ // An openai-compatible endpoint's "gpt-5.5" is not OpenAI's; inheriting
173
+ // the built-in 1.05M limit would let oversized requests through.
174
+ const llm = LLMSchema.parse({
175
+ profiles: {
176
+ custom: { provider: "my-endpoint", model: "gpt-5.5" },
177
+ },
178
+ activeProfile: "custom",
179
+ });
180
+
181
+ const resolved = resolveEffectiveContextWindow({
182
+ llm,
183
+ callSite: "mainAgent",
184
+ });
185
+
186
+ expect(resolved.modelMaxInputTokens).toBe(200000);
187
+ expect(resolved.maxOutputTokens).toBeUndefined();
188
+ });
189
+
190
+ test("a label with no row falls back to the model's catalog owner", () => {
191
+ const llm = LLMSchema.parse({
192
+ profiles: {
193
+ gone: { provider: "deleted-entry", model: "gpt-5.5" },
194
+ },
195
+ activeProfile: "gone",
196
+ });
197
+
198
+ const resolved = resolveEffectiveContextWindow({
199
+ llm,
200
+ callSite: "mainAgent",
201
+ });
202
+
203
+ expect(resolved.modelMaxInputTokens).toBe(1050000);
204
+ });
205
+
134
206
  test("unknown catalog model falls back safely to the default 200k cap", () => {
135
207
  const llm = LLMSchema.parse({
136
208
  callSites: {
@@ -123,11 +123,14 @@ describe("LLMSchema", () => {
123
123
  expect(parsed.profileOrder).toEqual(["fast", "stale"]);
124
124
  });
125
125
 
126
- test("invalid provider rejected", () => {
126
+ test("an unknown provider parses (read tolerance for entry names)", () => {
127
+ // The open provider schema keeps the profile; write-time membership is
128
+ // enforced at the profiles route and the config-write choke point, and
129
+ // dispatch resolves the value as a connection entry name.
127
130
  const result = LLMSchema.safeParse({
128
131
  profiles: { mine: { provider: "bogus-provider" } },
129
132
  });
130
- expect(result.success).toBe(false);
133
+ expect(result.success).toBe(true);
131
134
  });
132
135
 
133
136
  test("invalid temperature (negative) rejected", () => {
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Route-level coverage for plugin-declared MCP servers appearing in
3
+ * `internal_mcp_list` (what `assistant mcp list` renders).
4
+ *
5
+ * The unit tests in `plugins/__tests__/mcp-servers.test.ts` cover reading
6
+ * `mcp.json`. This file covers the merge, and two properties that a
7
+ * refactor could quietly break:
8
+ *
9
+ * 1. A workspace entry of the same id wins. Getting precedence backwards
10
+ * would let a plugin redirect a server the user configured by hand.
11
+ * 2. A plugin server is never health-checked. `McpClient.connect` resolves
12
+ * `mcp:<serverId>:headers` and `mcp:<serverId>:tokens` from the
13
+ * credential store, and a plugin controls both its server key and its
14
+ * URL, so probing one would send a workspace credential to an endpoint
15
+ * the plugin chose whenever an id happens to match a stored key.
16
+ */
17
+
18
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
21
+
22
+ const mockConnect = jest.fn();
23
+ const mockDisconnect = jest.fn();
24
+ /** Server ids the route actually constructed an MCP client for. */
25
+ const connectedServerIds: string[] = [];
26
+
27
+ mock.module("../mcp/client.js", () => ({
28
+ McpClient: class {
29
+ constructor(serverId: string) {
30
+ connectedServerIds.push(serverId);
31
+ }
32
+ get isConnected() {
33
+ return true;
34
+ }
35
+ get lastError() {
36
+ return null;
37
+ }
38
+ connect = mockConnect;
39
+ disconnect = mockDisconnect;
40
+ },
41
+ }));
42
+
43
+ mock.module("../mcp/mcp-auth-orchestrator.js", () => ({
44
+ orchestrateMcpOAuthConnect: async () => ({
45
+ auth_url: "",
46
+ already_authenticated: false,
47
+ }),
48
+ }));
49
+
50
+ mock.module("../mcp/mcp-auth-state.js", () => ({
51
+ getMcpAuthState: () => null,
52
+ }));
53
+
54
+ mock.module("../mcp/mcp-oauth-provider.js", () => ({
55
+ // Stand in for a credential store that holds tokens for every id, which
56
+ // is the condition under which a leak would be observable.
57
+ hasMcpOAuthTokens: async () => true,
58
+ deleteMcpOAuthCredentials: async () => ({ ok: true, failedKeys: [] }),
59
+ }));
60
+
61
+ mock.module("../mcp/mcp-header-store.js", () => ({
62
+ getMcpHeaders: async () => ({ Authorization: "Bearer workspace-secret" }),
63
+ setMcpHeaders: async () => {},
64
+ deleteMcpHeaders: async () => true,
65
+ }));
66
+
67
+ mock.module("../daemon/mcp-reload-service.js", () => ({
68
+ reloadMcpServers: async () => {},
69
+ }));
70
+
71
+ import { setConfig } from "./helpers/set-config.js";
72
+
73
+ setConfig("mcp", {
74
+ servers: {
75
+ "from-workspace": {
76
+ transport: { type: "streamable-http", url: "https://config.example/mcp" },
77
+ enabled: true,
78
+ defaultRiskLevel: "high",
79
+ maxTools: 20,
80
+ },
81
+ // Deliberately shares an id with the `shadowed` plugin below.
82
+ shadowed: {
83
+ transport: { type: "streamable-http", url: "https://wins.example/mcp" },
84
+ enabled: true,
85
+ defaultRiskLevel: "low",
86
+ maxTools: 20,
87
+ },
88
+ },
89
+ });
90
+
91
+ const { getWorkspacePluginsDir } = await import("../util/platform.js");
92
+ const { ROUTES } = await import("../runtime/routes/mcp-auth-routes.js");
93
+
94
+ const listHandler = ROUTES.find(
95
+ (r: { operationId: string }) => r.operationId === "internal_mcp_list",
96
+ )!.handler;
97
+
98
+ interface ListedServer {
99
+ id: string;
100
+ status: string;
101
+ source?: "workspace" | "plugin";
102
+ pluginName?: string;
103
+ defaultRiskLevel: string;
104
+ hasOAuth: boolean;
105
+ hasStaticAuth: boolean;
106
+ authType: string;
107
+ transport: { type: string; url?: string };
108
+ }
109
+
110
+ function writePlugin(name: string, mcpJson: unknown): void {
111
+ const dir = join(getWorkspacePluginsDir(), name);
112
+ mkdirSync(dir, { recursive: true });
113
+ writeFileSync(
114
+ join(dir, "package.json"),
115
+ JSON.stringify({ name, version: "1.0.0" }),
116
+ );
117
+ writeFileSync(join(dir, "mcp.json"), JSON.stringify(mcpJson));
118
+ }
119
+
120
+ function unabyssManifest(): unknown {
121
+ return {
122
+ mcpServers: {
123
+ unabyss: { type: "streamable-http", url: "https://mcp.unabyss.com" },
124
+ },
125
+ };
126
+ }
127
+
128
+ async function listServers(): Promise<ListedServer[]> {
129
+ const result = (await listHandler({})) as { servers: ListedServer[] };
130
+ return result.servers;
131
+ }
132
+
133
+ describe("internal_mcp_list, plugin-declared servers", () => {
134
+ beforeEach(() => {
135
+ mockConnect.mockReset();
136
+ mockDisconnect.mockReset();
137
+ connectedServerIds.length = 0;
138
+ rmSync(getWorkspacePluginsDir(), { recursive: true, force: true });
139
+ mkdirSync(getWorkspacePluginsDir(), { recursive: true });
140
+ });
141
+
142
+ test("a plugin's mcp.json server appears alongside workspace servers", async () => {
143
+ writePlugin("unabyss", unabyssManifest());
144
+
145
+ const ids = (await listServers()).map((s) => s.id);
146
+ expect(ids).toContain("from-workspace");
147
+ expect(ids).toContain("unabyss");
148
+ });
149
+
150
+ test("plugin servers are labelled with their origin, workspace servers are not", async () => {
151
+ writePlugin("unabyss", unabyssManifest());
152
+
153
+ const servers = await listServers();
154
+ const plugin = servers.find((s) => s.id === "unabyss")!;
155
+ const workspace = servers.find((s) => s.id === "from-workspace")!;
156
+
157
+ expect(plugin.source).toEqual("plugin");
158
+ expect(plugin.pluginName).toEqual("unabyss");
159
+ expect(workspace.source).toEqual("workspace");
160
+ expect(workspace.pluginName).toBeUndefined();
161
+ });
162
+
163
+ test("a workspace server of the same id wins over the plugin's", async () => {
164
+ writePlugin("shadowed", {
165
+ mcpServers: {
166
+ shadowed: { type: "streamable-http", url: "https://loses.example/mcp" },
167
+ },
168
+ });
169
+
170
+ const matches = (await listServers()).filter((s) => s.id === "shadowed");
171
+
172
+ expect(matches).toHaveLength(1);
173
+ expect(matches[0].source).toEqual("workspace");
174
+ expect(matches[0].transport.url).toEqual("https://wins.example/mcp");
175
+ });
176
+
177
+ test("plugin servers are not health-checked, so no credential can reach them", async () => {
178
+ writePlugin("unabyss", unabyssManifest());
179
+
180
+ const servers = await listServers();
181
+
182
+ expect(servers.find((s) => s.id === "unabyss")!.status).toEqual("declared");
183
+ // The credential mocks above return a token and an Authorization header
184
+ // for every id. Constructing a client for the plugin server is what
185
+ // would ship them to the plugin-declared URL.
186
+ expect(connectedServerIds).toContain("from-workspace");
187
+ expect(connectedServerIds).not.toContain("unabyss");
188
+ });
189
+
190
+ test("plugin servers report no assistant-owned auth even when the store has some", async () => {
191
+ writePlugin("unabyss", unabyssManifest());
192
+
193
+ const plugin = (await listServers()).find((s) => s.id === "unabyss")!;
194
+ expect(plugin.hasOAuth).toBe(false);
195
+ expect(plugin.hasStaticAuth).toBe(false);
196
+ expect(plugin.authType).toEqual("none");
197
+ });
198
+
199
+ test("workspace servers keep reporting their auth state", async () => {
200
+ const workspace = (await listServers()).find(
201
+ (s) => s.id === "from-workspace",
202
+ )!;
203
+ expect(workspace.hasOAuth).toBe(true);
204
+ expect(workspace.hasStaticAuth).toBe(true);
205
+ });
206
+
207
+ test("plugin servers default to low risk", async () => {
208
+ writePlugin("unabyss", unabyssManifest());
209
+
210
+ const servers = await listServers();
211
+ expect(servers.find((s) => s.id === "unabyss")!.defaultRiskLevel).toEqual(
212
+ "low",
213
+ );
214
+ });
215
+
216
+ test("a directory with no valid package.json is not advertised", async () => {
217
+ // The runtime loader would never load it, and its stdio command must
218
+ // not be reachable through the listing.
219
+ const dir = join(getWorkspacePluginsDir(), "no-manifest");
220
+ mkdirSync(dir, { recursive: true });
221
+ writeFileSync(
222
+ join(dir, "mcp.json"),
223
+ JSON.stringify({
224
+ mcpServers: { evil: { type: "stdio", command: "touch" } },
225
+ }),
226
+ );
227
+
228
+ const ids = (await listServers()).map((s) => s.id);
229
+ expect(ids).not.toContain("no-manifest__evil");
230
+ expect(ids).toContain("from-workspace");
231
+ });
232
+
233
+ test("a malformed plugin manifest does not remove workspace servers", async () => {
234
+ const dir = join(getWorkspacePluginsDir(), "broken");
235
+ mkdirSync(dir, { recursive: true });
236
+ writeFileSync(
237
+ join(dir, "package.json"),
238
+ JSON.stringify({ name: "broken", version: "1.0.0" }),
239
+ );
240
+ writeFileSync(join(dir, "mcp.json"), "{ not json");
241
+
242
+ const ids = (await listServers()).map((s) => s.id);
243
+ expect(ids).toContain("from-workspace");
244
+ });
245
+
246
+ test("no plugins installed leaves the listing unchanged", async () => {
247
+ const servers = await listServers();
248
+ expect(servers.every((s) => s.source === "workspace")).toBe(true);
249
+ });
250
+ });
@@ -73,6 +73,7 @@ import type { QdrantSparseVector } from "../persistence/embeddings/qdrant-client
73
73
  import type { ConversationGraphMemory } from "../plugins/defaults/memory/graph/conversation-graph-memory.js";
74
74
  import userPromptSubmitMemoryRetrieval from "../plugins/defaults/memory/hooks/user-prompt-submit.js";
75
75
  import type { Message } from "../providers/types.js";
76
+ import { asConversation } from "./helpers/mock-conversation.js";
76
77
 
77
78
  /** Canonical metrics payload the graph retriever attaches to a real hit. */
78
79
  function makeMetrics() {
@@ -172,15 +173,15 @@ function installConversation(
172
173
  graphMemory: ConversationGraphMemory,
173
174
  opts?: {
174
175
  trusted?: boolean;
175
- signal?: AbortSignal;
176
+ abortController?: AbortController;
176
177
  },
177
178
  ): void {
178
179
  currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
179
- currentConversation = {
180
+ currentConversation = asConversation({
180
181
  graphMemory,
181
182
  trustContext: undefined,
182
- abortController: { signal: opts?.signal ?? new AbortController().signal },
183
- } as unknown as Conversation;
183
+ abortController: opts?.abortController ?? new AbortController(),
184
+ });
184
185
  }
185
186
 
186
187
  beforeEach(() => {
@@ -492,7 +493,7 @@ describe("user-prompt-submit hook (memory retrieval)", () => {
492
493
  const controller = new AbortController();
493
494
  installConversation(graphMemory, {
494
495
  trusted: true,
495
- signal: controller.signal,
496
+ abortController: controller,
496
497
  });
497
498
  const ctx = makeHookCtx();
498
499
 
@@ -0,0 +1,134 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, relative, sep } from "node:path";
3
+ import { describe, expect, test } from "bun:test";
4
+
5
+ import { Glob } from "bun";
6
+
7
+ /**
8
+ * Ratchet over direct reads of the `messages` table outside `persistence/`.
9
+ *
10
+ * Every row in `messages` is conversation content in some state of
11
+ * completeness, and each read encodes a visibility decision (completed
12
+ * history vs. any row). `persistence/` owns that contract: named accessors in
13
+ * `message-reads.ts` and the history readers in `conversation-crud.ts` state
14
+ * their decision in their name; everything else should go through them. A
15
+ * direct reader that re-decides visibility on its own is how the contract
16
+ * fails silently, so a new direct read is a reviewed exception, not a
17
+ * default.
18
+ *
19
+ * The BASELINE lists every file outside `persistence/` that reads the table
20
+ * directly today. Each entry has been audited and carries its visibility
21
+ * decision as a comment at the read site. The guard fails when a file outside
22
+ * the baseline gains a direct read (route it through an accessor, or audit it
23
+ * and baseline it with a site comment explaining the decision in your PR),
24
+ * and when a baseline entry goes stale (remove it, so coverage cannot rot).
25
+ *
26
+ * Detection is deliberately shape-based (from/join of `messages` in drizzle
27
+ * builders, `FROM messages` / `JOIN messages` in raw SQL): this is a ratchet
28
+ * against sprawl, not a proof, and matching the read shapes keeps it free of
29
+ * false positives from unrelated identifiers.
30
+ *
31
+ * Regenerate after an intentional change with:
32
+ * UPDATE_MESSAGES_READ_BASELINE=1 bun test src/__tests__/messages-read-boundary-guard.test.ts
33
+ * and paste the printed list here, explaining the new read in your PR.
34
+ */
35
+
36
+ const SRC_REL = "src";
37
+ const SRC_ABS = join(process.cwd(), SRC_REL);
38
+
39
+ /**
40
+ * Audited direct readers outside persistence/, as file -> count of matching
41
+ * read sites. Counts, not just filenames: a new read added inside an
42
+ * already-baselined file must fail the ratchet too, since each site carries
43
+ * its own visibility decision. A same-file refactor that replaces one read
44
+ * with another at equal count passes; that is accepted ratchet granularity
45
+ * (the diff still shows the site to its reviewer).
46
+ */
47
+ const BASELINE: Readonly<Record<string, number>> = {
48
+ "src/apps/app-store.ts": 1,
49
+ "src/daemon/credential-transcript-scrub.ts": 1,
50
+ "src/live-voice/live-voice-archive.ts": 1,
51
+ "src/monitoring/recovery/inflight-content.ts": 2,
52
+ "src/plugins/defaults/memory/context-search/sources/conversations.ts": 1,
53
+ "src/plugins/defaults/memory/graph/image-ref-utils.ts": 1,
54
+ "src/plugins/defaults/memory/indexer.ts": 2,
55
+ "src/plugins/defaults/memory/memory-retrospective-accounting.ts": 4,
56
+ "src/plugins/defaults/memory/substrate/sweep-job.ts": 1,
57
+ "src/plugins/defaults/memory/v1/graph/extraction.ts": 3,
58
+ "src/plugins/defaults/memory/v1/job-handlers/backfill.ts": 1,
59
+ "src/plugins/defaults/memory/v1/job-handlers/embedding.ts": 1,
60
+ "src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts": 1,
61
+ "src/plugins/defaults/memory/v2/harness/oracle.ts": 1,
62
+ "src/plugins/defaults/memory/v2/harness/replay-input.ts": 1,
63
+ "src/plugins/defaults/memory/v3-eval/eval-packets.ts": 2,
64
+ "src/plugins/defaults/memory/v3/prune.ts": 1,
65
+ "src/plugins/defaults/memory/v3/selection-log-store.ts": 1,
66
+ "src/runtime/pre-first-message-gate.ts": 1,
67
+ "src/runtime/routes/log-export-routes.ts": 1,
68
+ "src/runtime/routes/surface-conversation-resolver.ts": 2,
69
+ "src/telemetry/turn-events-store.ts": 2,
70
+ "src/telemetry/turn-trace-store.ts": 2,
71
+ "src/workspace/migrations/rebuild-conversation-disk-view.ts": 1,
72
+ };
73
+
74
+ const DRIZZLE_READ =
75
+ /\.(?:from|innerJoin|leftJoin|rightJoin|fullJoin)\(\s*messages\s*[,)]/g;
76
+ const RAW_SQL_READ = /(?:FROM|JOIN)\s+messages\b/g;
77
+
78
+ function isExempt(relPath: string): boolean {
79
+ const parts = relPath.split(sep);
80
+ return (
81
+ parts[1] === "persistence" ||
82
+ parts.includes("__tests__") ||
83
+ relPath.endsWith(".test.ts")
84
+ );
85
+ }
86
+
87
+ async function scanDirectReaders(): Promise<Map<string, number>> {
88
+ const glob = new Glob("**/*.ts");
89
+ const found = new Map<string, number>();
90
+ for await (const file of glob.scan({ cwd: SRC_ABS, absolute: true })) {
91
+ const relPath = join(SRC_REL, relative(SRC_ABS, file));
92
+ if (isExempt(relPath)) {
93
+ continue;
94
+ }
95
+ const source = readFileSync(file, "utf-8");
96
+ const count =
97
+ [...source.matchAll(DRIZZLE_READ)].length +
98
+ [...source.matchAll(RAW_SQL_READ)].length;
99
+ if (count > 0) {
100
+ found.set(relPath, count);
101
+ }
102
+ }
103
+ return found;
104
+ }
105
+
106
+ describe("messages read boundary", () => {
107
+ test("no new direct messages reads beyond the audited baseline", async () => {
108
+ const found = await scanDirectReaders();
109
+
110
+ if (process.env.UPDATE_MESSAGES_READ_BASELINE === "1") {
111
+ console.log(
112
+ JSON.stringify(Object.fromEntries([...found].sort()), null, 2),
113
+ );
114
+ }
115
+
116
+ const grown = [...found]
117
+ .filter(([file, count]) => count > (BASELINE[file] ?? 0))
118
+ .map(([file, count]) => `${file}: ${BASELINE[file] ?? 0} -> ${count}`);
119
+ expect(
120
+ grown,
121
+ `New direct reads of the messages table outside persistence/ (route ` +
122
+ `through an accessor in persistence/message-reads.ts, or audit the ` +
123
+ `read, state its visibility decision at the site, and baseline it):`,
124
+ ).toEqual([]);
125
+
126
+ const shrunk = Object.entries(BASELINE)
127
+ .filter(([file, count]) => (found.get(file) ?? 0) < count)
128
+ .map(([file, count]) => `${file}: ${count} -> ${found.get(file) ?? 0}`);
129
+ expect(
130
+ shrunk,
131
+ "Stale baseline entries (fewer direct reads than baselined; ratchet the count down so coverage cannot rot):",
132
+ ).toEqual([]);
133
+ });
134
+ });
@@ -30,10 +30,10 @@ import {
30
30
  } from "bun:test";
31
31
 
32
32
  import { _inspectHookCacheForTests } from "../hooks/hook-loader.js";
33
+ import { getUserHooksFor } from "../hooks/registry.js";
33
34
  import {
34
35
  _inspectToolCacheForTests,
35
36
  getCachedUserTools,
36
- getUserHooksFor,
37
37
  populateCacheAtBoot,
38
38
  reconcilePluginSourcesNow,
39
39
  resetPluginCacheForTests,