@vellumai/assistant 0.11.3 → 0.11.4-dev.202608190019.b94dbf2

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 (596) hide show
  1. package/AGENTS.md +9 -3
  2. package/ARCHITECTURE.md +32 -10
  3. package/docs/architecture/memory.md +43 -5
  4. package/docs/architecture/security.md +96 -152
  5. package/docs/architecture/turn-actor.md +70 -0
  6. package/docs/browser-use-architecture-phase2.md +128 -56
  7. package/docs/flux-turn-detection-spike.md +248 -0
  8. package/docs/guardian-request-flow.md +35 -0
  9. package/docs/stt-provider-onboarding.md +3 -1
  10. package/knip.json +3 -0
  11. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  12. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  13. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  17. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  18. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  23. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
  24. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
  25. package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
  26. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  27. package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
  28. package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
  29. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  30. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +60 -0
  31. package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
  32. package/openapi.yaml +289 -113
  33. package/package.json +1 -1
  34. package/scripts/voice-ttft-spike.ts +3 -3
  35. package/scripts/write-plugin-api-shim.ts +10 -0
  36. package/src/__tests__/app-compiler.test.ts +38 -3
  37. package/src/__tests__/app-control-flow.test.ts +1 -0
  38. package/src/__tests__/approval-routes-http.test.ts +2 -2
  39. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  40. package/src/__tests__/attachments-store.test.ts +21 -12
  41. package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
  42. package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
  43. package/src/__tests__/call-site-routing-provider.test.ts +1 -1
  44. package/src/__tests__/channel-availability-routes.test.ts +14 -1
  45. package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
  46. package/src/__tests__/channel-delivery-store.test.ts +14 -14
  47. package/src/__tests__/channel-inbound-disk-pressure.test.ts +4 -4
  48. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  49. package/src/__tests__/checker.test.ts +20 -314
  50. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
  51. package/src/__tests__/compaction-events.test.ts +8 -10
  52. package/src/__tests__/config-loader-backfill.test.ts +3 -3
  53. package/src/__tests__/config-schema.test.ts +25 -10
  54. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  55. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
  56. package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
  57. package/src/__tests__/conversation-agent-loop.test.ts +32 -21
  58. package/src/__tests__/conversation-attachments.test.ts +0 -1
  59. package/src/__tests__/conversation-attention-store.test.ts +63 -0
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  61. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -0
  63. package/src/__tests__/conversation-fork-referential.test.ts +67 -0
  64. package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
  65. package/src/__tests__/conversation-load-history-repair.test.ts +209 -0
  66. package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
  67. package/src/__tests__/conversation-queue.test.ts +154 -4
  68. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  69. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  71. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  72. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +187 -102
  74. package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
  75. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  76. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  77. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  78. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  79. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  80. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  82. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  83. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  85. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  86. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  87. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  88. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  89. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  90. package/src/__tests__/credential-prompt-route.test.ts +7 -10
  91. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  92. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  93. package/src/__tests__/custom-profile-ensure.test.ts +5 -1
  94. package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
  95. package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
  96. package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
  97. package/src/__tests__/document-append-idempotency.test.ts +233 -0
  98. package/src/__tests__/document-sync-tags.test.ts +0 -75
  99. package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
  100. package/src/__tests__/edit-propagation.test.ts +107 -11
  101. package/src/__tests__/file-ops-service.test.ts +163 -30
  102. package/src/__tests__/filesystem-tools.test.ts +23 -24
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  104. package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
  105. package/src/__tests__/helpers/mock-actor-context.ts +49 -0
  106. package/src/__tests__/helpers/mock-conversation.ts +13 -1
  107. package/src/__tests__/host-file-read-tool.test.ts +16 -19
  108. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  109. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  110. package/src/__tests__/injector-chain.test.ts +63 -41
  111. package/src/__tests__/injector-disk-pressure.test.ts +11 -23
  112. package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
  113. package/src/__tests__/llm-context-resolution.test.ts +73 -1
  114. package/src/__tests__/llm-schema.test.ts +5 -2
  115. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  116. package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
  117. package/src/__tests__/media-generate-image.test.ts +131 -21
  118. package/src/__tests__/memory-retrieval-hook.test.ts +100 -7
  119. package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
  120. package/src/__tests__/mtime-cache.test.ts +3 -1
  121. package/src/__tests__/non-member-access-request.test.ts +0 -20
  122. package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
  123. package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
  124. package/src/__tests__/plugin-api-store-credential.test.ts +261 -0
  125. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  126. package/src/__tests__/plugin-disabled-state.test.ts +2 -0
  127. package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
  128. package/src/__tests__/plugin-execution-context.test.ts +73 -0
  129. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  130. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -4
  131. package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
  132. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
  133. package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
  134. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  135. package/src/__tests__/qdrant-manager.test.ts +14 -1
  136. package/src/__tests__/reaction-persistence.test.ts +200 -7
  137. package/src/__tests__/require-fresh-approval.test.ts +0 -4
  138. package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
  139. package/src/__tests__/run-conversation-turn-persistence.test.ts +460 -105
  140. package/src/__tests__/run-due-schedules.test.ts +21 -0
  141. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  142. package/src/__tests__/schedule-routes.test.ts +23 -0
  143. package/src/__tests__/schedule-store.test.ts +17 -0
  144. package/src/__tests__/scoped-approval-grants.test.ts +11 -6
  145. package/src/__tests__/secret-ingress-channel.test.ts +0 -1
  146. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  147. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  148. package/src/__tests__/skills.test.ts +32 -0
  149. package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
  150. package/src/__tests__/starter-task-flow.test.ts +5 -4
  151. package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
  152. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  153. package/src/__tests__/subagent-spawn-and-await.test.ts +18 -17
  154. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  155. package/src/__tests__/subagent-tools.test.ts +81 -101
  156. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  157. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  158. package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
  159. package/src/__tests__/tool-executor.test.ts +68 -39
  160. package/src/__tests__/turn-events-store.test.ts +43 -0
  161. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  162. package/src/__tests__/ui-shape-teaching.test.ts +33 -0
  163. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  164. package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
  165. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  166. package/src/__tests__/user-plugin-loader.test.ts +3 -1
  167. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  168. package/src/__tests__/visible-app-context.test.ts +16 -9
  169. package/src/__tests__/voice-config-update.test.ts +40 -0
  170. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  171. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  172. package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
  173. package/src/__tests__/worker-plugin-surface.test.ts +77 -0
  174. package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
  175. package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
  176. package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
  177. package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
  178. package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
  179. package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
  180. package/src/acp/__tests__/auth-required.test.ts +161 -0
  181. package/src/acp/acp-claude-oauth.ts +19 -2
  182. package/src/acp/agent-process.test.ts +100 -0
  183. package/src/acp/agent-process.ts +29 -26
  184. package/src/acp/auth-required.ts +102 -0
  185. package/src/acp/session-manager.test.ts +119 -0
  186. package/src/acp/session-manager.ts +76 -3
  187. package/src/api/events/acp-auth-required.ts +55 -0
  188. package/src/api/events/host-file.ts +2 -2
  189. package/src/api/index.ts +7 -0
  190. package/src/api/surfaces.ts +12 -3
  191. package/src/apps/app-store.ts +3 -0
  192. package/src/bundler/package-resolver.ts +2 -30
  193. package/src/calls/__tests__/voice-session-bridge.test.ts +194 -11
  194. package/src/calls/__tests__/voice-triage-escalate.test.ts +102 -0
  195. package/src/calls/call-controller.ts +19 -3
  196. package/src/calls/call-setup-flow.ts +0 -1
  197. package/src/calls/media-stream-stt-session.ts +15 -0
  198. package/src/calls/voice-session-bridge.ts +115 -41
  199. package/src/calls/voice-triage-escalate.ts +105 -2
  200. package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
  201. package/src/channels/config.ts +13 -0
  202. package/src/channels/plugin-channel-declarations.ts +108 -0
  203. package/src/channels/types.ts +30 -0
  204. package/src/cli/AGENTS.md +5 -2
  205. package/src/cli/bundled-modules.ts +29 -0
  206. package/src/cli/commands/credentials.help.ts +2 -2
  207. package/src/cli/commands/db/repair.ts +4 -8
  208. package/src/cli/commands/domain.ts +6 -3
  209. package/src/cli/commands/email.ts +6 -3
  210. package/src/cli/commands/inference-providers.ts +1 -1
  211. package/src/cli/commands/keys.ts +8 -3
  212. package/src/cli/commands/mcp.help.ts +13 -4
  213. package/src/cli/commands/mcp.ts +9 -0
  214. package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
  215. package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
  216. package/src/cli/commands/memory/index.help.ts +84 -28
  217. package/src/cli/commands/memory/index.ts +2 -0
  218. package/src/cli/commands/memory/memory-v2.ts +58 -54
  219. package/src/cli/commands/memory/memory-v3.ts +64 -0
  220. package/src/cli/commands/memory/memory-validate.ts +18 -0
  221. package/src/cli/commands/plugins.ts +85 -36
  222. package/src/cli/commands/schedules.ts +35 -1
  223. package/src/cli/commands/stt.help.ts +27 -2
  224. package/src/cli/commands/trust.ts +3 -14
  225. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
  226. package/src/cli/lib/bundled-marketplace.json +14 -1
  227. package/src/cli/lib/upgrade-plugin.ts +42 -0
  228. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  229. package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
  230. package/src/config/__tests__/default-provider.test.ts +6 -1
  231. package/src/config/__tests__/profile-materialization.test.ts +75 -19
  232. package/src/config/assistant-feature-flags.ts +36 -15
  233. package/src/config/balanced-model-experiment.ts +35 -0
  234. package/src/config/bundled-skills/acp/SKILL.md +6 -7
  235. package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
  236. package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
  237. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  238. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  239. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  240. package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
  241. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  242. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
  243. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
  244. package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
  245. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
  246. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  247. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  248. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  249. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
  250. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
  251. package/src/config/call-site-defaults.ts +11 -5
  252. package/src/config/default-profile-catalog.ts +179 -16
  253. package/src/config/default-profile-names.ts +4 -1
  254. package/src/config/default-provider-resolution.ts +4 -0
  255. package/src/config/feature-flag-registry.json +12 -12
  256. package/src/config/llm-context-resolution.ts +11 -3
  257. package/src/config/llm-resolver.ts +28 -1
  258. package/src/config/profile-materialization.ts +70 -22
  259. package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
  260. package/src/config/schemas/call-site-catalog.ts +4 -4
  261. package/src/config/schemas/live-voice.ts +57 -23
  262. package/src/config/schemas/llm.ts +59 -32
  263. package/src/config/schemas/mcp.ts +23 -0
  264. package/src/config/schemas/plugin-updates.ts +6 -2
  265. package/src/config/schemas/stt.ts +1 -0
  266. package/src/config/skills.ts +9 -2
  267. package/src/context/outbound-sanitize.ts +96 -1
  268. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  269. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
  270. package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
  271. package/src/daemon/conversation-agent-loop.ts +31 -19
  272. package/src/daemon/conversation-messaging.ts +5 -1
  273. package/src/daemon/conversation-notifiers.ts +20 -10
  274. package/src/daemon/conversation-process.ts +20 -6
  275. package/src/daemon/conversation-runtime-assembly.ts +12 -6
  276. package/src/daemon/conversation-store.ts +14 -4
  277. package/src/daemon/conversation-surfaces.ts +75 -26
  278. package/src/daemon/conversation-tool-setup.ts +3 -6
  279. package/src/daemon/conversation.ts +154 -51
  280. package/src/daemon/doordash-steps.ts +2 -2
  281. package/src/daemon/lifecycle.ts +14 -1
  282. package/src/daemon/mcp-reload-service.ts +36 -6
  283. package/src/daemon/process-message.ts +13 -26
  284. package/src/daemon/providers-setup.ts +6 -3
  285. package/src/daemon/trust-context-types.ts +29 -0
  286. package/src/daemon/trust-context.ts +12 -7
  287. package/src/daemon/wake-conversation-ops.ts +3 -2
  288. package/src/daemon/windows-compiled-entry.ts +4 -0
  289. package/src/documents/document-store.ts +143 -240
  290. package/src/hooks/hook-loader.ts +3 -3
  291. package/src/hooks/registry.ts +103 -45
  292. package/src/hooks/types.ts +5 -0
  293. package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
  294. package/src/inbound/oauth-callback-url.ts +61 -0
  295. package/src/ipc/gateway-client.test.ts +1 -1
  296. package/src/ipc/gateway-client.ts +14 -21
  297. package/src/ipc/gateway-flag-listener.ts +17 -3
  298. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
  299. package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
  300. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +1050 -0
  301. package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
  302. package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
  303. package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
  304. package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
  305. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
  306. package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
  307. package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
  308. package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
  309. package/src/live-voice/live-voice-archive.ts +2 -0
  310. package/src/live-voice/live-voice-manager.ts +16 -3
  311. package/src/live-voice/live-voice-metrics.ts +57 -32
  312. package/src/live-voice/live-voice-photo.ts +1 -2
  313. package/src/live-voice/live-voice-session.ts +598 -323
  314. package/src/live-voice/progress-narration.ts +277 -0
  315. package/src/live-voice/protocol.ts +21 -1
  316. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  317. package/src/mcp/__tests__/effective-config.test.ts +238 -0
  318. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
  319. package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
  320. package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
  321. package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
  322. package/src/mcp/client.ts +16 -11
  323. package/src/mcp/effective-config.ts +113 -0
  324. package/src/mcp/manager.ts +11 -6
  325. package/src/mcp/mcp-auth-orchestrator.ts +13 -22
  326. package/src/mcp/mcp-oauth-provider.ts +205 -240
  327. package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
  328. package/src/monitoring/control.ts +1 -0
  329. package/src/monitoring/db-integrity-sample.ts +4 -5
  330. package/src/monitoring/plugin-auto-update.ts +128 -24
  331. package/src/notifications/AGENTS.md +2 -0
  332. package/src/notifications/approval-card-data.ts +33 -0
  333. package/src/notifications/signal.ts +1 -0
  334. package/src/permissions/AGENTS.md +16 -0
  335. package/src/permissions/checker.test.ts +75 -122
  336. package/src/permissions/checker.ts +44 -560
  337. package/src/permissions/confirmation-guardian-request.test.ts +15 -11
  338. package/src/permissions/confirmation-guardian-request.ts +2 -2
  339. package/src/permissions/prompter.ts +1 -5
  340. package/src/permissions/question-guardian-request.test.ts +14 -6
  341. package/src/permissions/question-guardian-request.ts +1 -2
  342. package/src/persistence/attachments-store.ts +8 -1
  343. package/src/persistence/bookmark-crud.ts +3 -7
  344. package/src/persistence/conversation-attention-store.ts +16 -45
  345. package/src/persistence/conversation-crud.ts +33 -4
  346. package/src/persistence/conversation-lineage.ts +9 -0
  347. package/src/persistence/conversation-queries.ts +182 -52
  348. package/src/persistence/delivery-crud.ts +150 -36
  349. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  350. package/src/persistence/external-conversation-store.ts +32 -4
  351. package/src/persistence/llm-request-log-store.ts +4 -10
  352. package/src/persistence/llm-usage-store.ts +8 -3
  353. package/src/persistence/message-reads.test.ts +197 -0
  354. package/src/persistence/message-reads.ts +211 -0
  355. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  356. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
  357. package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
  358. package/src/persistence/real-user-turn-filter.ts +27 -3
  359. package/src/persistence/schema/documents.ts +4 -4
  360. package/src/persistence/steps.ts +9 -0
  361. package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
  362. package/src/plugin-api/constants.ts +8 -0
  363. package/src/plugin-api/conversation-turn.ts +195 -5
  364. package/src/plugin-api/index.ts +38 -6
  365. package/src/plugin-api/resolve-credential.ts +3 -2
  366. package/src/plugin-api/store-credential.ts +146 -0
  367. package/src/plugin-api/vision-support.test.ts +1 -1
  368. package/src/plugin-api/webhook-url.ts +13 -11
  369. package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
  370. package/src/plugins/defaults/main.ts +21 -7
  371. package/src/plugins/defaults/memory/AGENTS.md +15 -3
  372. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
  373. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
  374. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
  375. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
  376. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
  377. package/src/plugins/defaults/memory/buffer-format.ts +165 -0
  378. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
  379. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  380. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  381. package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
  382. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
  383. package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
  384. package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +17 -24
  385. package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
  386. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  387. package/src/plugins/defaults/memory/indexer.ts +3 -1
  388. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
  389. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  390. package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
  391. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
  392. package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
  393. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +147 -0
  394. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
  395. package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
  396. package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
  397. package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
  398. package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
  399. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
  400. package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +195 -58
  401. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +93 -24
  402. package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
  403. package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
  404. package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
  405. package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
  406. package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
  407. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
  408. package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
  409. package/src/plugins/defaults/memory/substrate/static-context.ts +160 -33
  410. package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
  411. package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
  412. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  413. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  414. package/src/plugins/defaults/memory/v3/card.ts +9 -9
  415. package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
  416. package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
  417. package/src/plugins/defaults/memory/v3/edge.ts +13 -57
  418. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  419. package/src/plugins/defaults/memory/v3/prune.ts +2 -0
  420. package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
  421. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  422. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  423. package/src/plugins/defaults/memory/worker.ts +6 -3
  424. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  425. package/src/plugins/external-plugin-loader.ts +47 -0
  426. package/src/plugins/mcp-servers.ts +361 -0
  427. package/src/plugins/mtime-cache.ts +37 -46
  428. package/src/plugins/pipeline.ts +14 -7
  429. package/src/plugins/plugin-execution-context.ts +35 -11
  430. package/src/plugins/worker-plugin-surface.ts +33 -0
  431. package/src/prompts/templates/system-sections.ts +0 -7
  432. package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
  433. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  434. package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
  435. package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
  436. package/src/providers/__tests__/retry-callsite.test.ts +25 -2
  437. package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
  438. package/src/providers/anthropic/client.ts +332 -241
  439. package/src/providers/call-site-routing.ts +30 -3
  440. package/src/providers/connection-resolution.ts +194 -11
  441. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +4 -0
  442. package/src/providers/inference/adapter-factory.ts +3 -0
  443. package/src/providers/inference/auth.ts +6 -0
  444. package/src/providers/inference/connection-availability.ts +24 -2
  445. package/src/providers/inference/connections.ts +2 -0
  446. package/src/providers/model-catalog.ts +3 -3
  447. package/src/providers/model-intents.ts +28 -8
  448. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +163 -40
  449. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
  450. package/src/providers/openai/chat-completions-provider.ts +133 -36
  451. package/src/providers/openai/codex-models.ts +2 -1
  452. package/src/providers/provider-send-message.ts +32 -3
  453. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
  454. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +625 -0
  455. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
  456. package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
  457. package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
  458. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +679 -0
  459. package/src/providers/speech-to-text/provider-catalog.ts +99 -8
  460. package/src/providers/speech-to-text/resolve.ts +25 -2
  461. package/src/routes/control.ts +1 -0
  462. package/src/routes/route-host-client.ts +1 -0
  463. package/src/routes/route-host-protocol.ts +7 -0
  464. package/src/routes/worker.ts +48 -15
  465. package/src/runtime/AGENTS.md +16 -17
  466. package/src/runtime/access-request-helper.ts +9 -12
  467. package/src/runtime/agent-wake.ts +18 -15
  468. package/src/runtime/effective-capabilities.test.ts +43 -0
  469. package/src/runtime/effective-capabilities.ts +27 -0
  470. package/src/runtime/guardian-reply-router.ts +6 -2
  471. package/src/runtime/pre-first-message-gate.ts +4 -0
  472. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
  473. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +522 -2
  474. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
  475. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
  476. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
  477. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
  478. package/src/runtime/routes/__tests__/plugins-routes.test.ts +132 -35
  479. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  480. package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
  481. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
  482. package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
  483. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +133 -1
  484. package/src/runtime/routes/channel-availability-routes.ts +32 -14
  485. package/src/runtime/routes/channel-route-shared.ts +0 -6
  486. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
  487. package/src/runtime/routes/conversation-list-routes.ts +181 -23
  488. package/src/runtime/routes/conversation-management-routes.ts +17 -12
  489. package/src/runtime/routes/conversation-query-routes.ts +40 -27
  490. package/src/runtime/routes/conversation-routes.ts +43 -29
  491. package/src/runtime/routes/credential-prompt-routes.ts +4 -7
  492. package/src/runtime/routes/credential-routes.ts +26 -82
  493. package/src/runtime/routes/default-provider-routes.ts +15 -0
  494. package/src/runtime/routes/documents-routes.ts +3 -222
  495. package/src/runtime/routes/inbound-message-handler.ts +21 -45
  496. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
  497. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
  498. package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
  499. package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
  500. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
  501. package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -79
  502. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
  503. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -7
  504. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -8
  505. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -79
  506. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
  507. package/src/runtime/routes/inference-profiles-routes.ts +20 -11
  508. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
  509. package/src/runtime/routes/log-export-routes.ts +3 -0
  510. package/src/runtime/routes/mcp-auth-routes.ts +148 -57
  511. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  512. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  513. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  514. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  515. package/src/runtime/routes/plugins-routes.ts +38 -8
  516. package/src/runtime/routes/schedule-routes.ts +94 -7
  517. package/src/runtime/routes/settings-routes.ts +6 -6
  518. package/src/runtime/routes/stt-routes.ts +31 -25
  519. package/src/runtime/routes/surface-action-routes.ts +20 -19
  520. package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
  521. package/src/runtime/routes/user-route-dispatcher.ts +69 -15
  522. package/src/runtime/routes/user-route-import.ts +108 -0
  523. package/src/runtime/routes/user-route-resolution.ts +18 -1
  524. package/src/runtime/routes/workspace-routes.ts +0 -9
  525. package/src/runtime/routes/workspace-utils.ts +3 -13
  526. package/src/runtime/services/conversation-serializer.ts +7 -2
  527. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  528. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  529. package/src/schedule/plugin-schedule-availability.ts +58 -0
  530. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  531. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  532. package/src/schedule/schedule-store.ts +5 -1
  533. package/src/schedule/scheduler.ts +9 -4
  534. package/src/schedule/worker-control.ts +1 -0
  535. package/src/schedule/worker.ts +6 -0
  536. package/src/security/oauth2.ts +6 -22
  537. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
  538. package/src/stt/__tests__/types.test.ts +94 -0
  539. package/src/stt/daemon-batch-transcriber.ts +10 -0
  540. package/src/stt/stt-stream-session.ts +8 -4
  541. package/src/stt/types.ts +103 -0
  542. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  543. package/src/subagent/consult-context.ts +11 -11
  544. package/src/subagent/consult-prompt.ts +26 -35
  545. package/src/subagent/manager.ts +21 -40
  546. package/src/subagent/notify.ts +7 -1
  547. package/src/subagent/types.ts +15 -12
  548. package/src/tools/__tests__/tool-input-schemas.test.ts +7 -7
  549. package/src/tools/acp/spawn.test.ts +97 -0
  550. package/src/tools/acp/spawn.ts +38 -4
  551. package/src/tools/credentials/ref-parse.ts +35 -0
  552. package/src/tools/credentials/resolve.ts +4 -10
  553. package/src/tools/credentials/store.ts +168 -0
  554. package/src/tools/document/document-tool.ts +12 -3
  555. package/src/tools/executor.ts +53 -15
  556. package/src/tools/filesystem/read.ts +27 -10
  557. package/src/tools/host-filesystem/read.ts +27 -15
  558. package/src/tools/network/url-safety.ts +9 -16
  559. package/src/tools/permission-checker.ts +43 -52
  560. package/src/tools/registry.ts +2 -1
  561. package/src/tools/shared/filesystem/file-ops-service.ts +63 -35
  562. package/src/tools/shared/filesystem/legacy-read-args.ts +22 -0
  563. package/src/tools/shared/filesystem/types.ts +5 -5
  564. package/src/tools/skills/scaffold-managed.ts +25 -7
  565. package/src/tools/subagent/spawn.ts +28 -88
  566. package/src/tools/tool-types.ts +26 -13
  567. package/src/tools/types.ts +6 -6
  568. package/src/tools/ui-surface/surface-shape-docs.ts +12 -1
  569. package/src/tools/workflows/run-workflow.ts +1 -2
  570. package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
  571. package/src/tts/reasoning-tag-filter.ts +89 -0
  572. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  573. package/src/util/logger.ts +16 -0
  574. package/src/util/think-tag-stream.ts +95 -0
  575. package/src/util/worker-process.ts +37 -4
  576. package/src/windows-compiled-cli.ts +32 -0
  577. package/src/windows-compiled-entry.ts +4 -0
  578. package/src/windows-compiled-logger.ts +6 -0
  579. package/src/windows-compiled-worker-entry.ts +29 -0
  580. package/src/workspace/byok-default-profile-ensure.ts +76 -24
  581. package/src/workspace/custom-profile-ensure.ts +4 -24
  582. package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
  583. package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
  584. package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
  585. package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
  586. package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
  587. package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
  588. package/src/workspace/migrations/registry.ts +10 -0
  589. package/src/workspace/provider-commit-message-generator.ts +7 -5
  590. package/src/__tests__/document-workspace-file.test.ts +0 -467
  591. package/src/live-voice/__tests__/front-decision.test.ts +0 -645
  592. package/src/live-voice/front-decision.ts +0 -476
  593. package/src/permissions/ipc-risk-types.ts +0 -143
  594. package/src/permissions/risk-types.ts +0 -76
  595. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  596. package/src/subagent/consult-transcript.ts +0 -90
@@ -1,5 +1,7 @@
1
1
  import { beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
+ import { eq } from "drizzle-orm";
4
+
3
5
  import {
4
6
  createConversation,
5
7
  type MessageRow,
@@ -8,6 +10,7 @@ import { getDb } from "../../../../persistence/db-connection.js";
8
10
  import { initializeDb } from "../../../../persistence/db-init.js";
9
11
  import { messages } from "../../../../persistence/schema/index.js";
10
12
  import {
13
+ getRetrospectiveMessagesAfter,
11
14
  hasQualifyingUserMessageAfter,
12
15
  messagesHaveUserActivity,
13
16
  } from "../memory-retrospective-accounting.js";
@@ -197,3 +200,72 @@ describe("messagesHaveUserActivity", () => {
197
200
  expect(messagesHaveUserActivity([])).toBe(false);
198
201
  });
199
202
  });
203
+
204
+ describe("getRetrospectiveMessagesAfter", () => {
205
+ beforeEach(() => {
206
+ const db = getDb();
207
+ db.run(`DELETE FROM messages`);
208
+ db.run(`DELETE FROM conversations`);
209
+ createConversation({ id: CONV });
210
+ });
211
+
212
+ test("truncates at a stale mid-slice unfinalized row so the cursor never passes it", () => {
213
+ const first = insertRaw({ role: "user", content: TEXT, createdAt: 1_000 });
214
+ const stale = insertRaw({
215
+ role: "assistant",
216
+ content: TEXT,
217
+ createdAt: 2_000,
218
+ });
219
+ insertRaw({ role: "user", content: TEXT, createdAt: 3_000 });
220
+ insertRaw({ role: "assistant", content: TEXT, createdAt: 4_000 });
221
+ getDb()
222
+ .update(messages)
223
+ .set({ finalized: 0 })
224
+ .where(eq(messages.id, stale))
225
+ .run();
226
+
227
+ const slice = getRetrospectiveMessagesAfter(CONV, null);
228
+
229
+ // The slice stops BEFORE the stale row: taking the later finalized rows
230
+ // as the cutoff would advance the cursor past the stale row, and once it
231
+ // finalizes it would sit behind the cursor, unreviewed forever.
232
+ expect(slice.map((row) => row.id)).toEqual([first]);
233
+
234
+ getDb()
235
+ .update(messages)
236
+ .set({ finalized: 1 })
237
+ .where(eq(messages.id, stale))
238
+ .run();
239
+
240
+ // Once the row resolves, the slice continues past it.
241
+ expect(getRetrospectiveMessagesAfter(CONV, null)).toHaveLength(4);
242
+ });
243
+
244
+ test("excludes unfinalized rows so the cutoff stays on rows a fork holds", () => {
245
+ insertRaw({ role: "user", content: TEXT, createdAt: 1_000 });
246
+ const lastFinalized = insertRaw({
247
+ role: "assistant",
248
+ content: TEXT,
249
+ createdAt: 2_000,
250
+ });
251
+ const streaming = insertRaw({
252
+ role: "assistant",
253
+ content: TEXT,
254
+ createdAt: 3_000,
255
+ });
256
+ getDb()
257
+ .update(messages)
258
+ .set({ finalized: 0 })
259
+ .where(eq(messages.id, streaming))
260
+ .run();
261
+
262
+ const slice = getRetrospectiveMessagesAfter(CONV, null);
263
+
264
+ // The invariant: no slice member is unfinalized, and the cutoff the job
265
+ // would take (the last slice row) is a row the retrospective fork
266
+ // actually contains.
267
+ expect(slice.some((row) => row.finalized === 0)).toBe(false);
268
+ expect(slice.at(-1)?.id).toBe(lastFinalized);
269
+ expect(slice.map((row) => row.id)).not.toContain(streaming);
270
+ });
271
+ });
@@ -168,7 +168,10 @@ mock.module("../find-most-recent-retrospective-for.js", () => ({
168
168
  mock.module("../../../../persistence/conversation-crud.js", () => ({
169
169
  getMessagesAfter: (id: string, afterId: string | null) => {
170
170
  messagesAfterCalls.push({ conversationId: id, afterId });
171
- return newMessages;
171
+ // Real rows always carry `finalized` (NOT NULL, default 1); the slice
172
+ // filter drops unfinalized rows, so mock rows default to finalized
173
+ // unless a test explicitly stages a streaming row.
174
+ return newMessages.map((row) => ({ finalized: 1, ...row }));
172
175
  },
173
176
  getMessages: (id: string) => {
174
177
  if (messagesByConversationId[id]) {
@@ -96,7 +96,10 @@ mock.module("../find-most-recent-retrospective-for.js", () => ({
96
96
  }));
97
97
 
98
98
  mock.module("../../../../persistence/conversation-crud.js", () => ({
99
- getMessagesAfter: (_id: string, _afterId: string | null) => newMessages,
99
+ // Real rows always carry `finalized` (NOT NULL, default 1); the slice
100
+ // filter drops unfinalized rows, so mock rows default to finalized.
101
+ getMessagesAfter: (_id: string, _afterId: string | null) =>
102
+ newMessages.map((row) => ({ finalized: 1, ...row })),
100
103
  getMessages: (id: string) => messageStores[id] ?? [],
101
104
  // `defaultResolveTarget` in the REAL wake reads the FORK id's row for the
102
105
  // archived check, so both rows carry archivedAt/createdAt. The fork's
@@ -151,7 +151,6 @@ const HOST_TIER_IMPORT_ALLOWLIST: readonly HostTierImportExemption[] = [
151
151
  { path: "src/daemon/embedding-reconcile.ts", tiers: ["substrate", "v3"] },
152
152
  { path: "src/daemon/skill-memory-refresh.ts", tiers: ["substrate"] },
153
153
  { path: "src/daemon/tool-side-effects.ts", tiers: ["substrate"] },
154
- { path: "src/daemon/trust-context.ts", tiers: ["substrate"] },
155
154
  { path: "src/persistence/steps.ts", tiers: ["v1"] },
156
155
  { path: "src/runtime/routes/consolidation-routes.ts", tiers: ["substrate"] },
157
156
  {
@@ -0,0 +1,165 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory buffer entry format: the single owner
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // `memory/buffer.md` is the append-only staging log that `remember()` writes
6
+ // and consolidation drains. Its line format is written here and recognized
7
+ // here, by one matcher, so the writer and every reader agree by construction.
8
+ //
9
+ // This module deliberately sits at the plugin root (shared infra, not a tier)
10
+ // and imports nothing. `substrate/` is the bottom tier and may not import a
11
+ // tier, but plugin-root infra is fair game, so all readers can reach this one
12
+ // without inverting the layering.
13
+ //
14
+ // A second matcher for this format anywhere in the tree is a bug. Two copies
15
+ // disagree on the shapes nobody thinks to test (indentation, spacing after the
16
+ // bullet dash, spacing inside the date), and each disagreement splits or merges
17
+ // somebody's fact.
18
+
19
+ /**
20
+ * A parsed buffer entry opening line.
21
+ */
22
+ export interface BufferEntryStart {
23
+ /**
24
+ * The bracketed timestamp, verbatim. Returned unparsed so it can serve
25
+ * directly as a consolidation cutoff: both sides of the agent's
26
+ * "timestamp >= cutoff" comparison then share this exact shape.
27
+ */
28
+ timestamp: string;
29
+ /** Everything after the timestamp bracket, trimmed. */
30
+ text: string;
31
+ }
32
+
33
+ /**
34
+ * Matches an entry opening line: `- [Mon D, h:mm AM/PM] fact`, the shape
35
+ * {@link formatRememberEntry} writes.
36
+ *
37
+ * Anchored at column 0, requiring a space after the dash, and spelling the
38
+ * timestamp with the single spaces {@link formatBufferTimestamp} emits.
39
+ * Everything else in the file is a continuation line belonging to the entry
40
+ * above it, which is how a multiline fact keeps its body: `remember()` writes
41
+ * only the opening line at column 0 and nests the body beneath it. That rule
42
+ * is what makes a bullet carrying other bracketed text (a `- [ ]` checklist
43
+ * item, a `- [[wikilink]]` bullet) read as continuation rather than as a new
44
+ * fact, and it is why a body line can never imitate an opening.
45
+ *
46
+ * Matching the writer's exact shape is the conservative choice in both
47
+ * directions: a line the writer could not have produced stays body text, so an
48
+ * odd hand-edit merges into the fact around it instead of splitting that fact
49
+ * or supplying a malformed timestamp to a caller that treats one as a cutoff.
50
+ *
51
+ * Group 1 is the timestamp, group 2 the fact text.
52
+ */
53
+ const BUFFER_ENTRY_REGEX =
54
+ /^- \[([A-Z][a-z]{2} \d{1,2}, \d{1,2}:\d{2} [AP]M)\]\s*(.*)$/;
55
+
56
+ /**
57
+ * Parse `line` as an entry opening, or `null` if it is a continuation line.
58
+ *
59
+ * Pass the raw line. Callers must not trim it first: leading whitespace is
60
+ * exactly what distinguishes an indented body line from a real entry, so a
61
+ * trimmed line reads an indented body bullet as a fresh fact and splits one
62
+ * entry into two.
63
+ */
64
+ export function matchBufferEntryStart(line: string): BufferEntryStart | null {
65
+ const match = BUFFER_ENTRY_REGEX.exec(line.trimEnd());
66
+ if (match === null) {
67
+ return null;
68
+ }
69
+ return { timestamp: match[1]!, text: match[2]!.trim() };
70
+ }
71
+
72
+ /** Whether `line` opens a buffer entry. See {@link matchBufferEntryStart}. */
73
+ export function isBufferEntryStart(line: string): boolean {
74
+ return BUFFER_ENTRY_REGEX.test(line.trimEnd());
75
+ }
76
+
77
+ /**
78
+ * Format `now` as a buffer-entry timestamp (`Mon D, h:mm AM/PM`). Exported so
79
+ * the consolidation job can present its cutoff in the same shape the buffer
80
+ * entries use, making the agent's "timestamp >= cutoff" comparison
81
+ * unambiguous at minute precision.
82
+ */
83
+ export function formatBufferTimestamp(now: Date): string {
84
+ const month = now.toLocaleString("en-US", { month: "short" });
85
+ const day = now.getDate();
86
+ const hours = now.getHours();
87
+ const minutes = String(now.getMinutes()).padStart(2, "0");
88
+ const ampm = hours >= 12 ? "PM" : "AM";
89
+ const displayHour = hours % 12 || 12;
90
+ return `${month} ${day}, ${displayHour}:${minutes} ${ampm}`;
91
+ }
92
+
93
+ /**
94
+ * Indent applied to a fact's continuation lines. Two spaces is the markdown
95
+ * convention for content nested under a `- ` bullet, so an entry stays
96
+ * readable as a list item.
97
+ */
98
+ const CONTINUATION_INDENT = " ";
99
+
100
+ /**
101
+ * Build a timestamped bullet entry for `buffer.md` / `archive/<date>.md`.
102
+ *
103
+ * Format mirrors the long-standing v1 PKB layout so buffers stay
104
+ * human-readable and downstream consumers (sweep, consolidation, the graph
105
+ * view) can parse the same shape regardless of which path produced the entry.
106
+ *
107
+ * A fact's body is indented, which is what makes the format round-trip. The
108
+ * buffer is line-delimited and the delimiter is "canonical entry shape at
109
+ * column 0", so a body line carrying that exact shape would otherwise be
110
+ * indistinguishable from the start of the next fact and split one entry into
111
+ * two. Indenting moves every body line off column 0, so no fact content can
112
+ * imitate a delimiter no matter what the user dictated. Blank lines stay
113
+ * genuinely blank rather than becoming whitespace.
114
+ *
115
+ * Readers strip the indent: {@link matchBufferEntryStart} classifies on the
116
+ * raw line while `parseBufferEntries` trims continuation lines, so the fact
117
+ * text a caller gets back is unchanged by this.
118
+ *
119
+ * Exported so memory sweep / extractor jobs format their auto-remembered
120
+ * entries identically to user-facing `remember()` calls.
121
+ */
122
+ export function formatRememberEntry(content: string, now: Date): string {
123
+ const [opening = "", ...body] = content.split("\n");
124
+ const nested = body.map((line) =>
125
+ line.trim().length === 0 ? "" : `${CONTINUATION_INDENT}${line}`,
126
+ );
127
+ return `- [${formatBufferTimestamp(now)}] ${[opening, ...nested].join("\n")}\n`;
128
+ }
129
+
130
+ /**
131
+ * Split buffer content into entries, each keeping its lines verbatim.
132
+ *
133
+ * The unit callers almost always want. Counting or trimming a buffer by raw
134
+ * lines conflates a five-line fact with five facts, and cuts a fact in half
135
+ * wherever the count runs out.
136
+ *
137
+ * Leading lines that precede the first entry opening (a hand-written buffer,
138
+ * or stray prose) are returned as a headless first group with
139
+ * `start === null`, so callers can decide what to do with them rather than
140
+ * silently dropping content.
141
+ */
142
+ export function splitBufferEntries(
143
+ lines: readonly string[],
144
+ ): BufferEntryLines[] {
145
+ const groups: BufferEntryLines[] = [];
146
+ for (const [index, line] of lines.entries()) {
147
+ const start = matchBufferEntryStart(line);
148
+ if (start !== null || groups.length === 0) {
149
+ groups.push({ start, firstLine: index, lines: [line] });
150
+ continue;
151
+ }
152
+ groups[groups.length - 1]!.lines.push(line);
153
+ }
154
+ return groups;
155
+ }
156
+
157
+ /** One entry's verbatim lines, as returned by {@link splitBufferEntries}. */
158
+ export interface BufferEntryLines {
159
+ /** `null` for the headless group before the first entry opening. */
160
+ start: BufferEntryStart | null;
161
+ /** Index of this group's first line in the input array. */
162
+ firstLine: number;
163
+ /** The group's lines, verbatim and in order. */
164
+ lines: string[];
165
+ }
@@ -224,6 +224,12 @@ async function gatherCandidateRowsFromQdrant(
224
224
  * the source/type/excluded-conversation predicates in SQL (Qdrant has no
225
225
  * visibility filtering). The app-side scorer determines final ordering, so
226
226
  * the SQL tie-break falls back to recency.
227
+ *
228
+ * No `finalized = 1` predicate, deliberately: every id in `messageIds` comes
229
+ * from the lexical or vector index, and indexing filters `finalized = 1` at
230
+ * index time (`message-lexical.ts`), so an unfinalized row can never be a
231
+ * candidate here. Do not copy this query shape to a reader whose id set has a
232
+ * different provenance; those need the filter themselves.
227
233
  */
228
234
  function searchByIds(
229
235
  messageIds: readonly string[],
@@ -371,6 +371,38 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (per-turn path)",
371
371
  expect(result.runMessages).toEqual(messages);
372
372
  });
373
373
 
374
+ test("separate routing history directs v1 retrieval without replacing model history", async () => {
375
+ const memory = makeMemory();
376
+ const config = makeConfig(false);
377
+ const callerText = "What is my preferred editor?";
378
+ const continuationText = "Continue the answer.";
379
+ const routingMessages = makeMessages(callerText);
380
+ const messages: Message[] = [
381
+ ...routingMessages,
382
+ {
383
+ role: "assistant",
384
+ content: [{ type: "text", text: "Let me check that." }],
385
+ },
386
+ {
387
+ role: "user",
388
+ content: [{ type: "text", text: continuationText }],
389
+ },
390
+ ];
391
+
392
+ const result = await memory.prepareMemory(
393
+ messages,
394
+ config,
395
+ new AbortController().signal,
396
+ noopEvent,
397
+ routingMessages,
398
+ );
399
+
400
+ expect(retrieveForTurnMock).toHaveBeenCalledWith(
401
+ expect.objectContaining({ userLastMessage: callerText }),
402
+ );
403
+ expect(result.runMessages).toEqual(messages);
404
+ });
405
+
374
406
  test("config on → v2 block prepended, mode is per-turn", async () => {
375
407
  stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
376
408
 
@@ -498,6 +530,51 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (per-turn path)",
498
530
  ).toBe(false);
499
531
  });
500
532
 
533
+ test("separate routing history directs v2 retrieval while injection stays on model history", async () => {
534
+ stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
535
+
536
+ const memory = makeMemory();
537
+ const config = makeConfig(true);
538
+ const callerText = "What is my preferred editor?";
539
+ const continuationText = "Continue the answer.";
540
+ const routingMessages = makeMessages(callerText);
541
+ const messages: Message[] = [
542
+ ...routingMessages,
543
+ {
544
+ role: "assistant",
545
+ content: [{ type: "text", text: "Let me check that." }],
546
+ },
547
+ {
548
+ role: "user",
549
+ content: [{ type: "text", text: continuationText }],
550
+ },
551
+ ];
552
+
553
+ const result = await memory.prepareMemory(
554
+ messages,
555
+ config,
556
+ new AbortController().signal,
557
+ noopEvent,
558
+ routingMessages,
559
+ );
560
+
561
+ expect(generateSparseEmbeddingMock.mock.calls).toContainEqual([callerText]);
562
+ expect(generateSparseEmbeddingMock.mock.calls).not.toContainEqual([
563
+ continuationText,
564
+ ]);
565
+ const tail = result.runMessages[result.runMessages.length - 1];
566
+ expect(tail?.role).toBe("user");
567
+ expect(tail?.content).toContainEqual({
568
+ type: "text",
569
+ text: continuationText,
570
+ });
571
+ expect(tail?.content[0]).toEqual(expect.objectContaining({ type: "text" }));
572
+ if (tail?.content[0]?.type !== "text") {
573
+ throw new Error("unexpected block type");
574
+ }
575
+ expect(tail.content[0].text).toContain("# memory/concepts/alice-vscode.md");
576
+ });
577
+
501
578
  test("config on with empty Qdrant hits → no retrieved concepts in the v2 block, v1 fallback skipped", async () => {
502
579
  // No `stageTurn` call — every channel returns `{ points: [] }` so the
503
580
  // candidate set is empty and retrieval contributes nothing. Always-candidate
@@ -512,6 +512,10 @@ export class ConversationGraphMemory {
512
512
  * - Turn 1 (or after compaction): full context load
513
513
  * - Every other turn: per-turn injection
514
514
  *
515
+ * `routingMessages` may end before an internal continuation message. The
516
+ * retrievers derive their query from that view, while memory is still
517
+ * injected into `messages`, which is the history sent to the model.
518
+ *
515
519
  * Returns augmented messages with memory context prepended to the last
516
520
  * user message, following the same injection pattern as the old system.
517
521
  */
@@ -520,6 +524,7 @@ export class ConversationGraphMemory {
520
524
  config: AssistantConfig,
521
525
  abortSignal: AbortSignal,
522
526
  onEvent: (msg: AssistantEvent) => void,
527
+ routingMessages: Message[] = messages,
523
528
  ): Promise<{
524
529
  runMessages: Message[];
525
530
  injectedTokens: number;
@@ -579,7 +584,7 @@ export class ConversationGraphMemory {
579
584
 
580
585
  // Gate: skip for empty/tool-result-only messages — unless we need to
581
586
  // reload after compaction (needsReload) or haven't initialized yet.
582
- const lastMessage = messages[messages.length - 1];
587
+ const lastMessage = routingMessages[routingMessages.length - 1];
583
588
  if (!lastMessage || lastMessage.role !== "user") {
584
589
  return noopResult;
585
590
  }
@@ -598,6 +603,7 @@ export class ConversationGraphMemory {
598
603
 
599
604
  return await this.runContextLoad(
600
605
  messages,
606
+ routingMessages,
601
607
  config,
602
608
  recentSummaries,
603
609
  firstUserText ?? undefined,
@@ -606,7 +612,12 @@ export class ConversationGraphMemory {
606
612
  );
607
613
  }
608
614
 
609
- return await this.runPerTurn(messages, config, abortSignal);
615
+ return await this.runPerTurn(
616
+ messages,
617
+ routingMessages,
618
+ config,
619
+ abortSignal,
620
+ );
610
621
  } catch (err) {
611
622
  const errCode =
612
623
  err instanceof z.ZodError ? err.issues[0]?.code : undefined;
@@ -629,6 +640,7 @@ export class ConversationGraphMemory {
629
640
 
630
641
  private async runContextLoad(
631
642
  messages: Message[],
643
+ routingMessages: Message[],
632
644
  config: AssistantConfig,
633
645
  recentSummaries: string[],
634
646
  userQuery: string | undefined,
@@ -640,9 +652,12 @@ export class ConversationGraphMemory {
640
652
  // The activation pipeline is robust to weak ANN signal — it falls back
641
653
  // to spreading + nowText to surface candidates.
642
654
  const startedAt = Date.now();
643
- const rawUserText = readRawUserText(messages[messages.length - 1]);
655
+ const rawUserText = readRawUserText(
656
+ routingMessages[routingMessages.length - 1],
657
+ );
644
658
  const v2 = await this.maybeRouteV2Injection(
645
659
  messages,
660
+ routingMessages,
646
661
  config,
647
662
  "context-load",
648
663
  // Context-load runs before the messages array necessarily contains
@@ -780,6 +795,7 @@ export class ConversationGraphMemory {
780
795
 
781
796
  private async runPerTurn(
782
797
  messages: Message[],
798
+ routingMessages: Message[],
783
799
  config: AssistantConfig,
784
800
  signal: AbortSignal,
785
801
  ) {
@@ -788,8 +804,8 @@ export class ConversationGraphMemory {
788
804
  let userLast = "";
789
805
  let userLastBlocks: ContentBlock[] = [];
790
806
 
791
- for (let i = messages.length - 1; i >= 0; i--) {
792
- const msg = messages[i];
807
+ for (let i = routingMessages.length - 1; i >= 0; i--) {
808
+ const msg = routingMessages[i];
793
809
  const text = msg.content
794
810
  .filter(
795
811
  (b): b is Extract<typeof b, { type: "text" }> => b.type === "text",
@@ -815,6 +831,7 @@ export class ConversationGraphMemory {
815
831
  const startedAt = Date.now();
816
832
  const v2 = await this.maybeRouteV2Injection(
817
833
  messages,
834
+ routingMessages,
818
835
  config,
819
836
  "per-turn",
820
837
  null,
@@ -978,6 +995,7 @@ export class ConversationGraphMemory {
978
995
  */
979
996
  private async maybeRouteV2Injection(
980
997
  messages: Message[],
998
+ routingMessages: Message[],
981
999
  config: AssistantConfig,
982
1000
  mode: InjectMemoryV2Mode,
983
1001
  /**
@@ -1004,7 +1022,7 @@ export class ConversationGraphMemory {
1004
1022
  const recentTurnPairs =
1005
1023
  userMessageOverride !== null
1006
1024
  ? [{ assistantMessage: "", userMessage: userMessageOverride }]
1007
- : extractRecentTurnPairs(messages, historicalPairs);
1025
+ : extractRecentTurnPairs(routingMessages, historicalPairs);
1008
1026
 
1009
1027
  const result = await injectMemoryV2Block({
1010
1028
  conversationId: this.conversationId,
@@ -17,6 +17,9 @@ export async function loadImageRefData(
17
17
  const msg = db
18
18
  .select({ content: messages.content })
19
19
  .from(messages)
20
+ // No completeness predicate: refs come from the extracted memory graph,
21
+ // and extraction reads finalized rows only, so an unfinalized id cannot
22
+ // reach here. Do not copy this shape for ids with other provenance.
20
23
  .where(eq(messages.id, ref.messageId))
21
24
  .get();
22
25
  if (!msg) {
@@ -15,6 +15,7 @@ import {
15
15
  } from "../../../../config/memory-v3-gate.js";
16
16
  import type { AssistantConfig } from "../../../../config/types.js";
17
17
  import { enqueueMemoryJob } from "../../../../persistence/jobs-store.js";
18
+ import { formatRememberEntry } from "../buffer-format.js";
18
19
  import { getWorkspaceDir } from "../paths.js";
19
20
  import type { GraphStats } from "./store.js";
20
21
  import {
@@ -98,36 +99,6 @@ export function handleRemember(
98
99
  return { success: true, message };
99
100
  }
100
101
 
101
- /**
102
- * Format `now` as a buffer-entry timestamp (`Mon D, h:mm AM/PM`). Exported so
103
- * the memory v2 consolidation job can present its cutoff in the same shape
104
- * the buffer entries use, making the agent's "timestamp ≥ cutoff" comparison
105
- * unambiguous at minute precision.
106
- */
107
- export function formatBufferTimestamp(now: Date): string {
108
- const month = now.toLocaleString("en-US", { month: "short" });
109
- const day = now.getDate();
110
- const hours = now.getHours();
111
- const minutes = String(now.getMinutes()).padStart(2, "0");
112
- const ampm = hours >= 12 ? "PM" : "AM";
113
- const displayHour = hours % 12 || 12;
114
- return `${month} ${day}, ${displayHour}:${minutes} ${ampm}`;
115
- }
116
-
117
- /**
118
- * Build a timestamped bullet entry for `buffer.md` / `archive/<date>.md`.
119
- *
120
- * Format mirrors the long-standing v1 PKB layout so v2 buffers stay
121
- * human-readable and downstream consumers (sweep, consolidation) can parse
122
- * the same shape regardless of which path produced the entry.
123
- *
124
- * Exported so memory v2 sweep / extractor jobs format their auto-remembered
125
- * entries identically to user-facing `remember()` calls.
126
- */
127
- export function formatRememberEntry(content: string, now: Date): string {
128
- return `- [${formatBufferTimestamp(now)}] ${content}\n`;
129
- }
130
-
131
102
  /**
132
103
  * Append `entry` to `<rootDir>/buffer.md` and `<rootDir>/archive/<today>.md`,
133
104
  * creating the archive directory and seeding the archive header if missing.
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
+ import { formatRememberEntry } from "../buffer-format.js";
3
4
  import {
4
5
  buildPendingGraph,
5
6
  findPendingEntryForContent,
@@ -42,6 +43,39 @@ describe("parseBufferEntries", () => {
42
43
  expect(entries[1]!.text).toBe("separate fact");
43
44
  });
44
45
 
46
+ test("a written fact stays one node even when its body is entry-shaped at column 0", () => {
47
+ // Round trip through the real writer. A user can dictate a fact whose body
48
+ // is itself a dated bullet, and without the writer nesting the body those
49
+ // lines are byte-identical to the start of the next fact, so one memory
50
+ // renders as several pending nodes in the Memory tab.
51
+ const entries = parseBufferEntries(
52
+ formatRememberEntry(
53
+ "Rollout plan:\n- [Jul 21, 9:00 AM] cutover window opens\n- [Jul 22, 9:00 AM] rollback deadline",
54
+ new Date(2026, 6, 20, 15, 15),
55
+ ),
56
+ );
57
+
58
+ expect(entries).toHaveLength(1);
59
+ expect(entries[0]!.text).toBe(
60
+ "Rollout plan:\n- [Jul 21, 9:00 AM] cutover window opens\n- [Jul 22, 9:00 AM] rollback deadline",
61
+ );
62
+ });
63
+
64
+ test("an indented entry-shaped line stays inside the fact above it", () => {
65
+ // Only an entry's opening line sits at column 0, so an indented bullet
66
+ // that happens to carry a timestamp is body text the user wrote. Reading
67
+ // it as an entry splits one fact into two nodes in the Memory tab.
68
+ const entries = parseBufferEntries(
69
+ "- [Jul 20, 3:15 PM] Timeline for the migration:\n" +
70
+ " - [Jul 21, 9:00 AM] cutover window opens\n" +
71
+ " - [Jul 22, 9:00 AM] rollback deadline\n",
72
+ );
73
+ expect(entries).toHaveLength(1);
74
+ expect(entries[0]!.text).toBe(
75
+ "Timeline for the migration:\n- [Jul 21, 9:00 AM] cutover window opens\n- [Jul 22, 9:00 AM] rollback deadline",
76
+ );
77
+ });
78
+
45
79
  test("keeps interior blank lines inside a multiline fact", () => {
46
80
  const entries = parseBufferEntries(
47
81
  "- [Jul 20, 3:15 PM] first paragraph\n\nsecond paragraph\n- [Jul 20, 3:16 PM] next\n",