@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
@@ -123,7 +123,9 @@ export async function indexMessageNow(
123
123
  // memory_segments has no cross-file FK to messages, so this call must not
124
124
  // leave pieces for a message with no row. Skip early when the source
125
125
  // row is already gone, the common case of a backfill job running after the
126
- // message was deleted. A post-write re-check below closes the narrower window
126
+ // message was deleted. Any-state existence check, deliberately: a
127
+ // streaming row exists, and orphan prevention is about deletion, not
128
+ // completeness. A post-write re-check below closes the narrower window
127
129
  // where the delete lands mid-transaction.
128
130
  const sourceMessage = getDb()
129
131
  .select({ id: messages.id })
@@ -52,18 +52,30 @@ export function isSkillCardMessage(row: { metadata: string | null }): boolean {
52
52
  }
53
53
 
54
54
  /**
55
- * `getMessagesAfter` minus skill-card rows. The retrospective job's
56
- * new-message slice: a card-only tail yields an empty slice (the job's
57
- * `no_new_messages` early return), and a mixed tail's cutoff lands on the
58
- * last real message rather than the card.
55
+ * `getMessagesAfter` truncated at the first unfinalized row, minus skill-card
56
+ * rows. The retrospective job's new-message slice: a card-only tail yields an
57
+ * empty slice (the job's `no_new_messages` early return), and a mixed tail's
58
+ * cutoff lands on the last real message rather than the card.
59
+ *
60
+ * The slice STOPS BEFORE the first unfinalized row rather than filtering it
61
+ * out. The job's cutoff (and therefore `lastProcessedMessageId` on success)
62
+ * is chosen from this slice, while the fork the job reviews skips unfinalized
63
+ * rows, so the cursor must never move past one: filtering through a stale
64
+ * mid-slice `finalized = 0` row would advance the cursor beyond it, and once
65
+ * the row later finalizes its `(createdAt, id)` sits behind the cursor,
66
+ * unreviewed forever. Truncating parks the cursor before the unfinalized row;
67
+ * the rows behind it are reviewed by the retrospective that runs after the
68
+ * row resolves (finalizes, or is repaired away by crash recovery).
59
69
  */
60
70
  export function getRetrospectiveMessagesAfter(
61
71
  conversationId: string,
62
72
  afterMessageId: string | null,
63
73
  ): MessageRow[] {
64
- return getMessagesAfter(conversationId, afterMessageId).filter(
65
- (row) => !isSkillCardMessage(row),
66
- );
74
+ const rows = getMessagesAfter(conversationId, afterMessageId);
75
+ const firstUnfinalized = rows.findIndex((row) => row.finalized !== 1);
76
+ const bounded =
77
+ firstUnfinalized === -1 ? rows : rows.slice(0, firstUnfinalized);
78
+ return bounded.filter((row) => !isSkillCardMessage(row));
67
79
  }
68
80
 
69
81
  /**
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Tests for `handleMemoryV3GateStats` in `memory-v3-routes.ts`.
3
+ *
4
+ * The handler aggregates gate telemetry from the telemetry DB and buckets it
5
+ * by concept page count. These tests cover:
6
+ * - empty DB → zero counts, all buckets present, scoredPassRate null
7
+ * - mixed rows land in the correct buckets by real_concept_page_count
8
+ * - scored vs unscored runs are counted separately
9
+ * - rows with no real_concept_page_count go to unknownPageCount
10
+ * - lookbackDays is clamped to [1, 90]
11
+ * - malformed JSON rows are skipped
12
+ * - null/absent DB returns a zero response
13
+ */
14
+
15
+ import type { Database } from "bun:sqlite";
16
+ import { describe, expect, test } from "bun:test";
17
+
18
+ import { handleMemoryV3GateStats } from "../memory-v3-routes.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Minimal fake SQLite DB that returns pre-set rows
22
+ // ---------------------------------------------------------------------------
23
+
24
+ type FakeRow = { payload: string };
25
+
26
+ function makePayload(detail: Record<string, unknown>): string {
27
+ return JSON.stringify({
28
+ type: "watchdog",
29
+ daemon_event_id: "test-id",
30
+ recorded_at: Date.now(),
31
+ check_name: "memory_v3_injection_gate",
32
+ value: 1,
33
+ detail,
34
+ });
35
+ }
36
+
37
+ function fakeDb(rows: FakeRow[]): Database {
38
+ return {
39
+ query: (_sql: string) => ({
40
+ all: (..._args: unknown[]) => rows,
41
+ }),
42
+ } as unknown as Database;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Tests
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe("handleMemoryV3GateStats", () => {
50
+ test("returns zero counts with all buckets when DB is null", () => {
51
+ const result = handleMemoryV3GateStats(30, null);
52
+ expect(result.totalRuns).toBe(0);
53
+ expect(result.buckets).toHaveLength(4);
54
+ for (const bucket of result.buckets) {
55
+ expect(bucket.total).toBe(0);
56
+ expect(bucket.scored).toBe(0);
57
+ expect(bucket.passed).toBe(0);
58
+ expect(bucket.scoredPassRate).toBeNull();
59
+ expect(bucket.reasons).toEqual({});
60
+ }
61
+ expect(result.unknownPageCount).toEqual({
62
+ total: 0,
63
+ passed: 0,
64
+ reasons: {},
65
+ });
66
+ });
67
+
68
+ test("returns zero counts with all buckets when DB returns no rows", () => {
69
+ const result = handleMemoryV3GateStats(30, fakeDb([]));
70
+ expect(result.totalRuns).toBe(0);
71
+ expect(result.buckets[0]!.pageCountRange).toBe("0–9");
72
+ expect(result.buckets[3]!.pageCountRange).toBe("200+");
73
+ for (const b of result.buckets) {
74
+ expect(b.scoredPassRate).toBeNull();
75
+ }
76
+ });
77
+
78
+ test("buckets a dense_pass row into 10–49 bucket", () => {
79
+ const rows = [
80
+ {
81
+ payload: makePayload({
82
+ pass: true,
83
+ reason: "dense_pass",
84
+ scored: true,
85
+ real_concept_page_count: 25,
86
+ }),
87
+ },
88
+ ];
89
+ const result = handleMemoryV3GateStats(30, fakeDb(rows));
90
+ expect(result.totalRuns).toBe(1);
91
+ const bucket = result.buckets.find((b) => b.pageCountRange === "10–49")!;
92
+ expect(bucket.total).toBe(1);
93
+ expect(bucket.scored).toBe(1);
94
+ expect(bucket.passed).toBe(1);
95
+ expect(bucket.scoredPassRate).toBe(1);
96
+ expect(bucket.reasons).toEqual({ dense_pass: 1 });
97
+ });
98
+
99
+ test("buckets a fail row into 200+ bucket", () => {
100
+ const rows = [
101
+ {
102
+ payload: makePayload({
103
+ pass: false,
104
+ reason: "fail_no_signal",
105
+ scored: true,
106
+ real_concept_page_count: 350,
107
+ }),
108
+ },
109
+ ];
110
+ const result = handleMemoryV3GateStats(30, fakeDb(rows));
111
+ const bucket = result.buckets.find((b) => b.pageCountRange === "200+")!;
112
+ expect(bucket.total).toBe(1);
113
+ expect(bucket.passed).toBe(0);
114
+ expect(bucket.scoredPassRate).toBe(0);
115
+ expect(bucket.reasons).toEqual({ fail_no_signal: 1 });
116
+ });
117
+
118
+ test("unscored pass-open rows count in total but not scored", () => {
119
+ const rows = [
120
+ {
121
+ payload: makePayload({
122
+ pass: true,
123
+ reason: "dense_unavailable",
124
+ scored: false,
125
+ real_concept_page_count: 5,
126
+ }),
127
+ },
128
+ ];
129
+ const result = handleMemoryV3GateStats(30, fakeDb(rows));
130
+ const bucket = result.buckets.find((b) => b.pageCountRange === "0–9")!;
131
+ expect(bucket.total).toBe(1);
132
+ expect(bucket.scored).toBe(0);
133
+ expect(bucket.passed).toBe(1);
134
+ // No scored runs → scoredPassRate is null (unscored pass does not count)
135
+ expect(bucket.scoredPassRate).toBeNull();
136
+ });
137
+
138
+ test("unscored pass mixed with scored fail does not inflate scoredPassRate", () => {
139
+ // Regression: before fix, pass-open + scored-fail in same bucket gave 100%
140
+ const rows = [
141
+ {
142
+ payload: makePayload({
143
+ pass: true,
144
+ reason: "dense_unavailable",
145
+ scored: false,
146
+ real_concept_page_count: 30,
147
+ }),
148
+ },
149
+ {
150
+ payload: makePayload({
151
+ pass: false,
152
+ reason: "fail_no_signal",
153
+ scored: true,
154
+ real_concept_page_count: 40,
155
+ }),
156
+ },
157
+ ];
158
+ const result = handleMemoryV3GateStats(30, fakeDb(rows));
159
+ const bucket = result.buckets.find((b) => b.pageCountRange === "10–49")!;
160
+ expect(bucket.total).toBe(2);
161
+ expect(bucket.scored).toBe(1);
162
+ expect(bucket.passed).toBe(1); // total passes (scored + unscored)
163
+ // Only the scored fail counts toward the rate — no scored passes
164
+ expect(bucket.scoredPassRate).toBe(0);
165
+ });
166
+
167
+ test("rows with no real_concept_page_count go to unknownPageCount", () => {
168
+ const rows = [
169
+ {
170
+ payload: makePayload({
171
+ pass: false,
172
+ reason: "fail_no_signal",
173
+ scored: true,
174
+ }),
175
+ },
176
+ ];
177
+ const result = handleMemoryV3GateStats(30, fakeDb(rows));
178
+ expect(result.unknownPageCount.total).toBe(1);
179
+ expect(result.unknownPageCount.passed).toBe(0);
180
+ expect(result.unknownPageCount.reasons).toEqual({ fail_no_signal: 1 });
181
+ for (const b of result.buckets) {
182
+ expect(b.total).toBe(0);
183
+ }
184
+ });
185
+
186
+ test("mixed rows across multiple buckets are aggregated correctly", () => {
187
+ const rows: FakeRow[] = [
188
+ // 0–9 bucket: 1 unscored pass
189
+ {
190
+ payload: makePayload({
191
+ pass: true,
192
+ reason: "dense_disabled",
193
+ scored: false,
194
+ real_concept_page_count: 3,
195
+ }),
196
+ },
197
+ // 10–49 bucket: 1 pass, 1 fail (both scored)
198
+ {
199
+ payload: makePayload({
200
+ pass: true,
201
+ reason: "dense_pass",
202
+ scored: true,
203
+ real_concept_page_count: 20,
204
+ }),
205
+ },
206
+ {
207
+ payload: makePayload({
208
+ pass: false,
209
+ reason: "fail_no_signal",
210
+ scored: true,
211
+ real_concept_page_count: 45,
212
+ }),
213
+ },
214
+ // 50–199 bucket: 2 passes
215
+ {
216
+ payload: makePayload({
217
+ pass: true,
218
+ reason: "sparse_only_strong",
219
+ scored: true,
220
+ real_concept_page_count: 100,
221
+ }),
222
+ },
223
+ {
224
+ payload: makePayload({
225
+ pass: true,
226
+ reason: "dense_pass",
227
+ scored: true,
228
+ real_concept_page_count: 150,
229
+ }),
230
+ },
231
+ ];
232
+
233
+ const result = handleMemoryV3GateStats(30, fakeDb(rows));
234
+ expect(result.totalRuns).toBe(5);
235
+
236
+ const b09 = result.buckets.find((b) => b.pageCountRange === "0–9")!;
237
+ expect(b09.total).toBe(1);
238
+ expect(b09.scored).toBe(0);
239
+ expect(b09.scoredPassRate).toBeNull();
240
+
241
+ const b1049 = result.buckets.find((b) => b.pageCountRange === "10–49")!;
242
+ expect(b1049.total).toBe(2);
243
+ expect(b1049.scored).toBe(2);
244
+ expect(b1049.passed).toBe(1);
245
+ expect(b1049.scoredPassRate).toBe(0.5);
246
+ expect(b1049.reasons).toEqual({ dense_pass: 1, fail_no_signal: 1 });
247
+
248
+ const b50199 = result.buckets.find((b) => b.pageCountRange === "50–199")!;
249
+ expect(b50199.scored).toBe(2);
250
+ expect(b50199.passed).toBe(2);
251
+ expect(b50199.scoredPassRate).toBe(1);
252
+ });
253
+
254
+ test("lookbackDays is clamped to 1–90", () => {
255
+ const r1 = handleMemoryV3GateStats(0, fakeDb([]));
256
+ expect(r1.lookbackDays).toBe(1);
257
+ const r2 = handleMemoryV3GateStats(999, fakeDb([]));
258
+ expect(r2.lookbackDays).toBe(90);
259
+ const r3 = handleMemoryV3GateStats(30, fakeDb([]));
260
+ expect(r3.lookbackDays).toBe(30);
261
+ });
262
+
263
+ test("malformed JSON payload rows are skipped", () => {
264
+ const rows: FakeRow[] = [
265
+ { payload: "not-valid-json" },
266
+ {
267
+ payload: makePayload({
268
+ pass: true,
269
+ reason: "dense_pass",
270
+ scored: true,
271
+ real_concept_page_count: 25,
272
+ }),
273
+ },
274
+ ];
275
+ const result = handleMemoryV3GateStats(30, fakeDb(rows));
276
+ // Only the valid row is counted
277
+ expect(result.totalRuns).toBe(2); // totalRuns counts raw rows before parse
278
+ const bucket = result.buckets.find((b) => b.pageCountRange === "10–49")!;
279
+ expect(bucket.total).toBe(1);
280
+ });
281
+ });
@@ -19,6 +19,7 @@ import { z } from "zod";
19
19
 
20
20
  import { getConfig } from "../../../../config/loader.js";
21
21
  import type { AssistantConfig } from "../../../../config/types.js";
22
+ import { getTelemetrySqlite } from "../../../../persistence/db-connection.js";
22
23
  import {
23
24
  ACTOR_PRINCIPALS,
24
25
  type RoutePolicy,
@@ -26,6 +27,7 @@ import {
26
27
  import type { RouteDefinition } from "../../../../runtime/routes/types.js";
27
28
  import { getLogger } from "../logging.js";
28
29
  import { backfillAllSections } from "../v3/maintain-job.js";
30
+ import { MEMORY_V3_INJECTION_GATE_CHECK_NAME } from "../v3/orchestrate.js";
29
31
  import { invalidateLanes } from "../v3/shadow-plugin.js";
30
32
 
31
33
  const log = getLogger("memory-v3-routes");
@@ -101,6 +103,211 @@ const WRITE_POLICY: RoutePolicy = {
101
103
  allowedPrincipalTypes: ACTOR_PRINCIPALS,
102
104
  };
103
105
 
106
+ // ---------------------------------------------------------------------------
107
+ // gate-stats
108
+ // ---------------------------------------------------------------------------
109
+
110
+ const DEFAULT_LOOKBACK_DAYS = 30;
111
+ const MAX_LOOKBACK_DAYS = 90;
112
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
113
+
114
+ /** Corpus size buckets for gate-stats aggregation (in concept pages). */
115
+ const CORPUS_BUCKETS = [
116
+ { label: "0–9", min: 0, max: 9 },
117
+ { label: "10–49", min: 10, max: 49 },
118
+ { label: "50–199", min: 50, max: 199 },
119
+ { label: "200+", min: 200, max: Infinity },
120
+ ] as const;
121
+
122
+ type BucketLabel = (typeof CORPUS_BUCKETS)[number]["label"];
123
+
124
+ interface BucketAcc {
125
+ total: number;
126
+ scored: number;
127
+ passed: number;
128
+ /** Passes that came from scored runs — the numerator for scoredPassRate. */
129
+ scoredPassed: number;
130
+ reasons: Record<string, number>;
131
+ }
132
+
133
+ function corpusBucketLabel(pageCount: number): BucketLabel {
134
+ for (const b of CORPUS_BUCKETS) {
135
+ if (pageCount <= b.max) {
136
+ return b.label;
137
+ }
138
+ }
139
+ return "200+";
140
+ }
141
+
142
+ interface GatePayloadDetail {
143
+ pass?: boolean;
144
+ reason?: string;
145
+ scored?: boolean;
146
+ real_concept_page_count?: number;
147
+ }
148
+
149
+ const GateStatsBucketSchema = z.object({
150
+ pageCountRange: z
151
+ .string()
152
+ .describe("Concept page count range (e.g. '10–49')"),
153
+ total: z
154
+ .number()
155
+ .describe("All gate runs in this bucket (scored + pass-open)"),
156
+ scored: z
157
+ .number()
158
+ .describe("Runs that weighed scores (dense lane available)"),
159
+ passed: z.number().describe("Runs that passed the gate"),
160
+ scoredPassRate: z
161
+ .number()
162
+ .nullable()
163
+ .describe(
164
+ "scoredPasses / scored (contested decisions only); null when no scored runs in bucket",
165
+ ),
166
+ reasons: z
167
+ .record(z.string(), z.number())
168
+ .describe("Gate reason code → run count (dense_pass, fail_no_signal, …)"),
169
+ });
170
+
171
+ export const GateStatsResponseSchema = z.object({
172
+ lookbackDays: z.number().describe("Lookback window applied"),
173
+ totalRuns: z.number().describe("Total gate runs found in the window"),
174
+ buckets: z
175
+ .array(GateStatsBucketSchema)
176
+ .describe("Stats by concept page count range, lean→large"),
177
+ unknownPageCount: z
178
+ .object({
179
+ total: z.number(),
180
+ passed: z.number(),
181
+ reasons: z.record(z.string(), z.number()),
182
+ })
183
+ .describe("Runs with no real_concept_page_count in telemetry detail"),
184
+ });
185
+
186
+ export type GateStatsResponse = z.infer<typeof GateStatsResponseSchema>;
187
+
188
+ /**
189
+ * Aggregate memory v3 injection gate runs from the telemetry outbox for the
190
+ * given lookback window. Groups runs by concept page count bucket and computes
191
+ * pass rates and reason distributions per bucket.
192
+ *
193
+ * Pass-open shortcuts (dense disabled/unavailable, gate threw) are included in
194
+ * `total` but not in `scored`, so `scoredPassRate` reflects only contested gate
195
+ * decisions — the signal that actually reveals whether the gate thresholds are
196
+ * calibrated correctly.
197
+ */
198
+ export function handleMemoryV3GateStats(
199
+ lookbackDays: number = DEFAULT_LOOKBACK_DAYS,
200
+ db: ReturnType<typeof getTelemetrySqlite> = getTelemetrySqlite(),
201
+ ): GateStatsResponse {
202
+ const days = Math.min(
203
+ Math.max(1, Math.floor(lookbackDays)),
204
+ MAX_LOOKBACK_DAYS,
205
+ );
206
+ const since = Date.now() - days * MS_PER_DAY;
207
+
208
+ const emptyBuckets = (): GateStatsResponse["buckets"] =>
209
+ CORPUS_BUCKETS.map((b) => ({
210
+ pageCountRange: b.label,
211
+ total: 0,
212
+ scored: 0,
213
+ passed: 0,
214
+ scoredPassRate: null,
215
+ reasons: {},
216
+ }));
217
+ if (!db) {
218
+ return {
219
+ lookbackDays: days,
220
+ totalRuns: 0,
221
+ buckets: emptyBuckets(),
222
+ unknownPageCount: { total: 0, passed: 0, reasons: {} },
223
+ };
224
+ }
225
+
226
+ const rows = db
227
+ .query(
228
+ /*sql*/ `
229
+ SELECT payload FROM telemetry_events
230
+ WHERE name = 'watchdog'
231
+ AND created_at >= ?
232
+ AND json_extract(payload, '$.check_name') = ?
233
+ `,
234
+ )
235
+ .all(since, MEMORY_V3_INJECTION_GATE_CHECK_NAME) as Array<{
236
+ payload: string;
237
+ }>;
238
+
239
+ const bucketMap = new Map<BucketLabel, BucketAcc>(
240
+ CORPUS_BUCKETS.map((b) => [
241
+ b.label,
242
+ { total: 0, scored: 0, passed: 0, scoredPassed: 0, reasons: {} },
243
+ ]),
244
+ );
245
+ const unknown: BucketAcc = {
246
+ total: 0,
247
+ scored: 0,
248
+ passed: 0,
249
+ scoredPassed: 0,
250
+ reasons: {},
251
+ };
252
+
253
+ for (const row of rows) {
254
+ let detail: GatePayloadDetail;
255
+ try {
256
+ const parsed = JSON.parse(row.payload) as { detail?: GatePayloadDetail };
257
+ detail = parsed.detail ?? {};
258
+ } catch {
259
+ continue;
260
+ }
261
+
262
+ const pageCount =
263
+ typeof detail.real_concept_page_count === "number"
264
+ ? detail.real_concept_page_count
265
+ : null;
266
+
267
+ const acc =
268
+ pageCount !== null
269
+ ? (bucketMap.get(corpusBucketLabel(pageCount)) ?? unknown)
270
+ : unknown;
271
+
272
+ acc.total += 1;
273
+ if (detail.scored === true) {
274
+ acc.scored += 1;
275
+ if (detail.pass === true) {
276
+ acc.scoredPassed += 1;
277
+ }
278
+ }
279
+ if (detail.pass === true) {
280
+ acc.passed += 1;
281
+ }
282
+ const reason =
283
+ typeof detail.reason === "string" ? detail.reason : "unknown";
284
+ acc.reasons[reason] = (acc.reasons[reason] ?? 0) + 1;
285
+ }
286
+
287
+ const buckets = CORPUS_BUCKETS.map((b) => {
288
+ const acc = bucketMap.get(b.label)!;
289
+ return {
290
+ pageCountRange: b.label,
291
+ total: acc.total,
292
+ scored: acc.scored,
293
+ passed: acc.passed,
294
+ scoredPassRate: acc.scored > 0 ? acc.scoredPassed / acc.scored : null,
295
+ reasons: acc.reasons,
296
+ };
297
+ });
298
+
299
+ return {
300
+ lookbackDays: days,
301
+ totalRuns: rows.length,
302
+ buckets,
303
+ unknownPageCount: {
304
+ total: unknown.total,
305
+ passed: unknown.passed,
306
+ reasons: unknown.reasons,
307
+ },
308
+ };
309
+ }
310
+
104
311
  export const ROUTES: RouteDefinition[] = [
105
312
  {
106
313
  operationId: "memory_v3_rebuild_index",
@@ -197,6 +197,7 @@ const { memoryV2ConsolidateJob, CONSOLIDATION_FAILURE_CHECKPOINT_KEY } =
197
197
  const { CUTOFF_PLACEHOLDER, CONSOLIDATION_PROMPT } =
198
198
  await import("../prompts/consolidation.js");
199
199
  const { invalidatePageIndex } = await import("../page-index.js");
200
+ const { formatRememberEntry } = await import("../../buffer-format.js");
200
201
 
201
202
  // The handler only reads `config.memory.enabled`, `config.memory.v2.enabled`,
202
203
  // `config.memory.v2.consolidation_prompt_path`, and
@@ -437,6 +438,38 @@ describe("memoryV2ConsolidateJob — chunked cutoff (consolidation_max_entries_p
437
438
  expect(outcome.cutoff).not.toBe("[meeting-notes]");
438
439
  });
439
440
 
441
+ test("a fact whose body holds a canonical entry line counts once and never supplies the cutoff", async () => {
442
+ // Written through the real writer, so this covers the round trip rather
443
+ // than a hand-built buffer: the body line carries the exact shape an entry
444
+ // opening has. Counting it would both inflate the per-run budget and let
445
+ // the cutoff be drawn from inside a fact, deferring entries against a
446
+ // timestamp the user never filed anything at.
447
+ writeFileSync(
448
+ bufferPath(),
449
+ formatRememberEntry(
450
+ "Alice's rollout plan:\n- [Apr 28, 9:00 AM] cutover",
451
+ new Date(2026, 3, 27, 9, 0),
452
+ ) +
453
+ formatRememberEntry(
454
+ "Bob takes his coffee black.",
455
+ new Date(2026, 3, 27, 9, 1),
456
+ ),
457
+ );
458
+
459
+ const outcome = await memoryV2ConsolidateJob(
460
+ makeJob(),
461
+ configWithMaxEntries(2),
462
+ );
463
+
464
+ expect(outcome.kind).toBe("invoked");
465
+ if (outcome.kind !== "invoked") {
466
+ throw new Error("unreachable");
467
+ }
468
+ // Two facts, cap 2 → no chunking. Three would chunk and defer one.
469
+ expect(outcome.deferredEntries).toBe(0);
470
+ expect(outcome.cutoff).not.toBe("Apr 28, 9:00 AM");
471
+ });
472
+
440
473
  test("multi-line entries under the cap → full-buffer cutoff even when raw line count exceeds it", async () => {
441
474
  writeFileSync(
442
475
  bufferPath(),