@vellumai/assistant 0.12.0 → 0.12.1-dev.202609141913.5e16607

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 (441) hide show
  1. package/AGENTS.md +5 -1
  2. package/ARCHITECTURE.md +11 -12
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/openapi.yaml +292 -50
  6. package/package.json +1 -1
  7. package/scripts/smoke-desktop-window-theme.ts +186 -0
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  9. package/src/__tests__/app-compiler.test.ts +6 -0
  10. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  12. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  13. package/src/__tests__/computer-use-tools.test.ts +17 -5
  14. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  15. package/src/__tests__/config-schema.test.ts +28 -29
  16. package/src/__tests__/config-watcher.test.ts +23 -8
  17. package/src/__tests__/connection-model-compat.test.ts +2 -2
  18. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  19. package/src/__tests__/conversation-pairing.test.ts +26 -0
  20. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  21. package/src/__tests__/conversation-queue.test.ts +48 -207
  22. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  23. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  24. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  25. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  26. package/src/__tests__/conversation-title-service.test.ts +14 -10
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  28. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  29. package/src/__tests__/credential-record-retire.test.ts +171 -1
  30. package/src/__tests__/credential-routes.test.ts +212 -3
  31. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  32. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  33. package/src/__tests__/document-sync-tags.test.ts +25 -0
  34. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  35. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  36. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  37. package/src/__tests__/git-porcelain.test.ts +47 -0
  38. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  39. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  40. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  41. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  42. package/src/__tests__/host-file-proxy.test.ts +11 -23
  43. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  44. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  45. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  46. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  47. package/src/__tests__/mcp-health-check.test.ts +8 -11
  48. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  49. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  50. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  51. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  52. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  53. package/src/__tests__/model-intents.test.ts +3 -0
  54. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  55. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  56. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  57. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  58. package/src/__tests__/plugin-import-boundary-guard.test.ts +13 -0
  59. package/src/__tests__/resolve-app.test.ts +135 -0
  60. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  61. package/src/__tests__/service-account-token.test.ts +247 -0
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  63. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  64. package/src/__tests__/source-fingerprint.test.ts +184 -0
  65. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  66. package/src/__tests__/subagent-tools.test.ts +3 -2
  67. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  68. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  69. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  70. package/src/__tests__/workspace-git-service.test.ts +26 -0
  71. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  72. package/src/acp/__tests__/agent-process.test.ts +258 -4
  73. package/src/acp/__tests__/auth-required.test.ts +96 -0
  74. package/src/acp/__tests__/client-handler.test.ts +90 -2
  75. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  76. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  77. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  78. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  79. package/src/acp/__tests__/session-manager.test.ts +895 -4
  80. package/src/acp/agent-process.test.ts +4 -1
  81. package/src/acp/agent-process.ts +94 -9
  82. package/src/acp/auth-required.ts +50 -1
  83. package/src/acp/auto-install.test.ts +9 -7
  84. package/src/acp/client-handler.ts +16 -3
  85. package/src/acp/model-config.test.ts +166 -0
  86. package/src/acp/model-config.ts +138 -0
  87. package/src/acp/resolve-agent.test.ts +220 -6
  88. package/src/acp/resolve-agent.ts +88 -23
  89. package/src/acp/session-manager.test.ts +196 -3
  90. package/src/acp/session-manager.ts +441 -18
  91. package/src/acp/types.ts +50 -5
  92. package/src/api/events/acp-session-model-update.test.ts +88 -0
  93. package/src/api/events/acp-session-model-update.ts +46 -0
  94. package/src/api/events/app-files-changed.ts +4 -3
  95. package/src/api/events/notification-intent.ts +2 -0
  96. package/src/api/index.ts +6 -0
  97. package/src/apps/app-store.ts +1 -1
  98. package/src/apps/resolve-app.ts +81 -0
  99. package/src/apps/source-fingerprint.ts +266 -0
  100. package/src/bundler/app-compiler.ts +15 -0
  101. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  102. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  103. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  104. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  105. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  106. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  107. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  108. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  109. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  110. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  111. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  112. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  113. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  114. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  115. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  116. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  117. package/src/calls/barge-in-guard.ts +118 -0
  118. package/src/calls/call-constants.ts +24 -4
  119. package/src/calls/call-controller.ts +499 -199
  120. package/src/calls/call-transport.ts +16 -0
  121. package/src/calls/media-stream-output.ts +32 -0
  122. package/src/calls/media-stream-server.ts +82 -30
  123. package/src/calls/media-stream-stt-session.ts +15 -0
  124. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  125. package/src/calls/resolve-call-tts-provider.ts +33 -2
  126. package/src/calls/tts-call-strategy.ts +9 -5
  127. package/src/calls/types.ts +2 -0
  128. package/src/calls/voice-control-protocol.ts +53 -21
  129. package/src/calls/voice-leg-coordinator.ts +189 -0
  130. package/src/calls/voice-progress-cadence.ts +423 -0
  131. package/src/calls/voice-session-bridge.ts +59 -46
  132. package/src/calls/voice-triage-escalate.ts +175 -56
  133. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  134. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  135. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  136. package/src/cli/commands/apps.help.ts +67 -13
  137. package/src/cli/commands/apps.ts +183 -0
  138. package/src/cli/commands/bash.help.ts +8 -13
  139. package/src/cli/commands/bash.ts +7 -25
  140. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  141. package/src/cli/commands/channels/index.help.ts +70 -2
  142. package/src/cli/commands/channels/index.ts +16 -3
  143. package/src/cli/commands/channels/send.ts +128 -0
  144. package/src/cli/commands/credentials.help.ts +6 -2
  145. package/src/cli/commands/credentials.ts +2 -2
  146. package/src/cli/commands/mcp.help.ts +8 -8
  147. package/src/cli/commands/mcp.ts +1 -1
  148. package/src/cli/commands/roadmap.help.ts +5 -1
  149. package/src/cli/commands/roadmap.ts +1 -1
  150. package/src/cli/lib/bundled-marketplace.json +103 -90
  151. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  152. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  153. package/src/config/__tests__/platform-identity.test.ts +255 -0
  154. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  155. package/src/config/acp-agent-write.ts +43 -0
  156. package/src/config/acp-defaults.test.ts +13 -3
  157. package/src/config/acp-defaults.ts +23 -6
  158. package/src/config/acp-schema.test.ts +81 -0
  159. package/src/config/acp-schema.ts +30 -2
  160. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  161. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  162. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  163. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  164. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  165. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  166. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  167. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  168. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  169. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  170. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  171. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  172. package/src/config/default-profile-catalog.ts +1 -1
  173. package/src/config/env.ts +2 -2
  174. package/src/config/feature-flag-registry.json +24 -12
  175. package/src/config/loader.ts +1 -0
  176. package/src/config/memory-v3-gate.test.ts +31 -0
  177. package/src/config/memory-v3-gate.ts +20 -16
  178. package/src/config/platform-identity.ts +201 -0
  179. package/src/config/platform-rehydration.ts +11 -27
  180. package/src/config/schema.ts +2 -2
  181. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  182. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  183. package/src/config/schemas/calls.ts +1 -28
  184. package/src/config/schemas/live-voice.ts +1 -173
  185. package/src/config/schemas/mcp.ts +1 -1
  186. package/src/config/schemas/memory-retrospective.ts +10 -0
  187. package/src/config/schemas/services.ts +8 -0
  188. package/src/config/schemas/skills.ts +0 -14
  189. package/src/config/schemas/voice.ts +150 -0
  190. package/src/credential-execution/process-manager.test.ts +18 -19
  191. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  192. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  193. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  194. package/src/daemon/app-change-notify.ts +45 -0
  195. package/src/daemon/config-watcher.ts +13 -194
  196. package/src/daemon/conversation-process.ts +35 -64
  197. package/src/daemon/conversation-skill-tools.ts +13 -31
  198. package/src/daemon/conversation-store.ts +1 -1
  199. package/src/daemon/conversation-surfaces.ts +10 -8
  200. package/src/daemon/conversation-tool-setup.ts +37 -43
  201. package/src/daemon/conversation.ts +0 -3
  202. package/src/daemon/host-bash-proxy.ts +8 -12
  203. package/src/daemon/host-browser-proxy.ts +15 -1
  204. package/src/daemon/host-file-proxy.ts +8 -10
  205. package/src/daemon/host-transfer-proxy.ts +14 -18
  206. package/src/daemon/lifecycle.ts +0 -5
  207. package/src/daemon/mcp-reload-service.ts +5 -5
  208. package/src/daemon/message-types/sync.ts +1 -0
  209. package/src/daemon/orphan-reaper.ts +1 -1
  210. package/src/daemon/providers-setup.ts +1 -1
  211. package/src/daemon/shutdown-handlers.ts +1 -9
  212. package/src/daemon/skill-memory-refresh.ts +64 -0
  213. package/src/daemon/tool-side-effects.ts +7 -18
  214. package/src/desktop/__tests__/fake-desktop.ts +119 -0
  215. package/src/desktop/desktop-chrome-policy.test.ts +71 -0
  216. package/src/desktop/desktop-chrome-policy.ts +42 -0
  217. package/src/desktop/desktop-dependencies.test.ts +102 -0
  218. package/src/desktop/desktop-dependencies.ts +306 -0
  219. package/src/desktop/desktop-feature.test.ts +18 -0
  220. package/src/desktop/desktop-feature.ts +19 -0
  221. package/src/desktop/desktop-panel-config.test.ts +135 -0
  222. package/src/desktop/desktop-panel-config.ts +149 -0
  223. package/src/desktop/desktop-session-manager.test.ts +627 -0
  224. package/src/desktop/desktop-session-manager.ts +814 -0
  225. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  226. package/src/desktop/desktop-stream-bridge.ts +214 -0
  227. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  228. package/src/desktop/desktop-wallpaper.ts +113 -0
  229. package/src/desktop/desktop-window-theme.ts +170 -0
  230. package/src/home/__tests__/feed-writer.test.ts +22 -0
  231. package/src/home/feed-writer.ts +11 -2
  232. package/src/hooks/types.ts +33 -0
  233. package/src/i18n/AGENTS.md +22 -0
  234. package/src/i18n/__tests__/i18n.test.ts +108 -0
  235. package/src/i18n/index.ts +16 -0
  236. package/src/i18n/locales.ts +93 -0
  237. package/src/i18n/messages.ts +61 -0
  238. package/src/i18n/title.ts +25 -0
  239. package/src/inbound/platform-callback-registration.ts +4 -5
  240. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  241. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  242. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  243. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  244. package/src/live-voice/live-voice-session.ts +213 -693
  245. package/src/live-voice/protocol.ts +0 -2
  246. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  247. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  248. package/src/mcp/effective-config.ts +3 -3
  249. package/src/mcp/mcp-header-store.ts +29 -45
  250. package/src/mcp/spec-schema.ts +111 -0
  251. package/src/mcp/startup.ts +8 -4
  252. package/src/mcp/workspace-mcp-config.ts +188 -0
  253. package/src/messaging/provider.ts +10 -3
  254. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  255. package/src/messaging/providers/callback-routing.ts +18 -0
  256. package/src/messaging/providers/channel-transport.ts +56 -0
  257. package/src/messaging/providers/discord/transport.ts +17 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  259. package/src/messaging/providers/slack/adapter.ts +7 -44
  260. package/src/messaging/providers/slack/transport.ts +16 -0
  261. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  262. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  263. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  264. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  265. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  266. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  267. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  268. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  269. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  270. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  271. package/src/notifications/adapters/macos.ts +3 -0
  272. package/src/notifications/assistant-reply-producer.ts +8 -3
  273. package/src/notifications/conversation-pairing.ts +36 -16
  274. package/src/notifications/delivered-post-record.ts +11 -1
  275. package/src/notifications/guardian-feed-projection.ts +125 -5
  276. package/src/notifications/home-feed-side-effect.ts +55 -7
  277. package/src/notifications/schedule-result-producer.ts +66 -22
  278. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  279. package/src/oauth/byo-connection.test.ts +45 -0
  280. package/src/oauth/byo-connection.ts +36 -6
  281. package/src/oauth/connection-resolver.test.ts +73 -0
  282. package/src/oauth/connection-resolver.ts +95 -2
  283. package/src/oauth/seed-providers.ts +165 -0
  284. package/src/outbound-proxy/certs.ts +3 -5
  285. package/src/permissions/checker.test.ts +11 -6
  286. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  287. package/src/persistence/conversation-crud.ts +71 -63
  288. package/src/persistence/conversation-lineage.ts +12 -4
  289. package/src/persistence/conversation-title-placeholders.ts +23 -0
  290. package/src/persistence/conversation-title-service.ts +9 -26
  291. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  292. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  293. package/src/persistence/message-cursor.ts +78 -0
  294. package/src/persistence/message-reads.ts +30 -0
  295. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  296. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  297. package/src/persistence/schema/memory-core.ts +5 -0
  298. package/src/platform/client.test.ts +4 -2
  299. package/src/platform/client.ts +2 -10
  300. package/src/plugin-api/constants.ts +2 -0
  301. package/src/plugin-api/index.ts +1 -0
  302. package/src/plugin-api/types.ts +2 -0
  303. package/src/plugins/defaults/index.ts +2 -0
  304. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  305. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  306. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  307. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  308. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  309. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  310. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  311. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  312. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  313. package/src/plugins/defaults/memory/graph/capability-seed.ts +4 -6
  314. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  315. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  316. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  317. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  318. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  319. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  320. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  321. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  322. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  323. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  324. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  325. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  326. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  327. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  328. package/src/plugins/defaults/memory/startup.ts +6 -1
  329. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  330. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  331. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +89 -0
  332. package/src/plugins/defaults/memory/worker.ts +6 -0
  333. package/src/plugins/mcp-servers.ts +15 -75
  334. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  335. package/src/providers/gemini/client.ts +9 -23
  336. package/src/providers/gemini-thought-signature.test.ts +63 -0
  337. package/src/providers/gemini-thought-signature.ts +44 -0
  338. package/src/providers/inference/auth.ts +2 -3
  339. package/src/providers/inference/connection-availability.ts +27 -8
  340. package/src/providers/inference/resolve-auth.ts +22 -4
  341. package/src/providers/inference/service-account-token.ts +248 -0
  342. package/src/providers/model-catalog.ts +34 -0
  343. package/src/providers/model-intents.ts +10 -5
  344. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  345. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  346. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  347. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  348. package/src/providers/openai/chat-completions-provider.ts +192 -78
  349. package/src/providers/openai/codex-models.ts +0 -4
  350. package/src/providers/openai/google-thought-signature.ts +320 -0
  351. package/src/runtime/AGENTS.md +2 -0
  352. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  353. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  354. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  355. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  356. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  357. package/src/runtime/auth/same-actor.ts +38 -3
  358. package/src/runtime/channel-send.ts +316 -0
  359. package/src/runtime/http-server.ts +160 -151
  360. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  361. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  362. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  363. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  364. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  365. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  366. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  367. package/src/runtime/routes/acp-routes.test.ts +15 -0
  368. package/src/runtime/routes/acp-routes.ts +72 -9
  369. package/src/runtime/routes/app-management-routes.ts +73 -0
  370. package/src/runtime/routes/app-routes.ts +1 -1
  371. package/src/runtime/routes/channel-send-routes.ts +85 -0
  372. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  373. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  374. package/src/runtime/routes/credential-routes.ts +19 -10
  375. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  376. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  377. package/src/runtime/routes/diagnostics-routes.ts +55 -3
  378. package/src/runtime/routes/index.ts +4 -2
  379. package/src/runtime/routes/log-export-routes.ts +2 -0
  380. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  381. package/src/runtime/routes/migration-routes.ts +6 -20
  382. package/src/runtime/routes/platform-routes.ts +18 -15
  383. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  384. package/src/runtime/services/conversation-serializer.ts +16 -3
  385. package/src/runtime/sync/resource-sync-events.ts +2 -1
  386. package/src/schedule/worker.ts +1 -1
  387. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  388. package/src/security/ces-http-record-backend.ts +120 -0
  389. package/src/security/ces-rpc-record-backend.ts +7 -0
  390. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  391. package/src/skills/workspace-skill-md-signature.ts +50 -0
  392. package/src/subagent/consult-context.ts +12 -25
  393. package/src/tools/acp/list-agents.test.ts +3 -1
  394. package/src/tools/acp/spawn.test.ts +146 -11
  395. package/src/tools/acp/spawn.ts +21 -3
  396. package/src/tools/acp/status.test.ts +94 -0
  397. package/src/tools/acp/status.ts +40 -3
  398. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  399. package/src/tools/capability-offer.test.ts +22 -2
  400. package/src/tools/capability-offer.ts +16 -1
  401. package/src/tools/computer-use/definitions.ts +4 -2
  402. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  403. package/src/tools/credentials/metadata-store.ts +100 -0
  404. package/src/tools/host-filesystem/edit.test.ts +2 -1
  405. package/src/tools/host-filesystem/edit.ts +2 -2
  406. package/src/tools/host-filesystem/read.test.ts +2 -1
  407. package/src/tools/host-filesystem/read.ts +2 -2
  408. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  409. package/src/tools/host-filesystem/transfer.ts +2 -2
  410. package/src/tools/host-filesystem/write.test.ts +2 -1
  411. package/src/tools/host-filesystem/write.ts +2 -2
  412. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  413. package/src/tools/host-terminal/host-shell.ts +4 -20
  414. package/src/tools/policy-context.ts +6 -7
  415. package/src/tools/subagent/spawn.ts +5 -8
  416. package/src/tools/terminal/__tests__/safe-env.test.ts +6 -6
  417. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  418. package/src/tools/terminal/safe-env.ts +23 -3
  419. package/src/tools/terminal/sanitized-bash.ts +105 -0
  420. package/src/tools/watch/watch-retro-report.ts +0 -8
  421. package/src/util/__tests__/language-subtag.test.ts +23 -1
  422. package/src/util/ca-bundle.ts +13 -0
  423. package/src/util/language-subtag.ts +16 -0
  424. package/src/util/platform.ts +8 -0
  425. package/src/workspace/git-porcelain.ts +66 -0
  426. package/src/workspace/git-service.ts +5 -54
  427. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  428. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  429. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  430. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  431. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  432. package/src/workspace/migrations/registry.ts +6 -0
  433. package/src/__tests__/app-source-watcher.test.ts +0 -218
  434. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  435. package/src/__tests__/speaker-identification.test.ts +0 -56
  436. package/src/calls/speaker-identification.ts +0 -233
  437. package/src/daemon/app-source-watcher.ts +0 -218
  438. package/src/messaging/providers/whatsapp/client.ts +0 -29
  439. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  440. package/src/util/fs-watcher-error.ts +0 -36
  441. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -1,4 +1,4 @@
1
- import { readFileSync, utimesSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readFileSync, rmSync, utimesSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { beforeEach, describe, expect, mock, test } from "bun:test";
4
4
 
@@ -14,14 +14,6 @@ mock.module("../util/logger.js", () => ({
14
14
  truncateForLog: (value: string, maxLen = 500) => value.slice(0, maxLen),
15
15
  }));
16
16
 
17
- // The config routes under test read and write the workspace config.json via
18
- // the real loader (`loadRawConfig`/`saveRawConfig`). Tests seed the raw file
19
- // directly (the fixtures are raw-file shapes, including deliberately
20
- // malformed trees, so the whole file is replaced rather than composed via
21
- // `setConfig`) and detect commits by comparing the on-disk text against the
22
- // seeded snapshot — `saveRawConfig` pretty-prints, so any commit changes the
23
- // text.
24
- let rawConfig: Record<string, unknown> = {};
25
17
  let seededRawText = "";
26
18
  let mtimeSeq = 0;
27
19
 
@@ -29,19 +21,23 @@ function configJsonPath(): string {
29
21
  return join(process.env.VELLUM_WORKSPACE_DIR!, "config.json");
30
22
  }
31
23
 
32
- /** Write `raw` to the workspace config.json as the seeded pre-test state. */
24
+ function mcpJsonPath(): string {
25
+ return join(process.env.VELLUM_WORKSPACE_DIR!, "mcp.json");
26
+ }
27
+
33
28
  function seedRawConfig(raw: Record<string, unknown>): void {
34
- rawConfig = raw;
35
29
  seededRawText = JSON.stringify(raw);
30
+ mkdirSync(process.env.VELLUM_WORKSPACE_DIR!, { recursive: true });
36
31
  writeFileSync(configJsonPath(), seededRawText);
37
- // Distinct mtime per write so the loader's file-signature cache can never
38
- // read two consecutive seeds as identical.
39
32
  mtimeSeq += 1;
40
33
  const stamp = new Date(Date.now() + mtimeSeq);
41
34
  utimesSync(configJsonPath(), stamp, stamp);
42
35
  }
43
36
 
44
- /** The raw config a route commit persisted, or null when nothing was saved. */
37
+ function seedWorkspaceMcp(document: unknown): void {
38
+ writeFileSync(mcpJsonPath(), JSON.stringify(document, null, 2) + "\n");
39
+ }
40
+
45
41
  function committedRaw(): Record<string, unknown> | null {
46
42
  const text = readFileSync(configJsonPath(), "utf8");
47
43
  if (text === seededRawText) {
@@ -119,11 +115,6 @@ const configGetRoute = findRoute("config_get");
119
115
  const configPatchRoute = findRoute("config_patch");
120
116
  const configSetRoute = findRoute("config_set");
121
117
 
122
- /**
123
- * Config responses inject the code-catalog default profiles into
124
- * `llm.profiles` (the effective wire view). These tests pin the MCP secret
125
- * boundary, so drop the injected block before whole-response comparisons.
126
- */
127
118
  function withoutWireProfiles(
128
119
  result: Record<string, unknown>,
129
120
  ): Record<string, unknown> {
@@ -134,21 +125,18 @@ function withoutWireProfiles(
134
125
  describe("MCP config secret boundary", () => {
135
126
  beforeEach(() => {
136
127
  seedRawConfig({});
128
+ rmSync(mcpJsonPath(), { force: true });
137
129
  });
138
130
 
139
- test("config_get omits legacy MCP transport headers from settings-read responses", () => {
140
- seedRawConfig({
141
- mcp: {
142
- servers: {
143
- remote: {
144
- transport: {
145
- type: "streamable-http",
146
- url: "https://mcp.example.com",
147
- headers: {
148
- Authorization: "Bearer mcp-secret",
149
- "X-API-Key": "mcp-api-secret",
150
- },
151
- },
131
+ test("config_get omits MCP headers from the mcp.json overlay", () => {
132
+ seedWorkspaceMcp({
133
+ mcpServers: {
134
+ remote: {
135
+ type: "streamable-http",
136
+ url: "https://mcp.example.com",
137
+ headers: {
138
+ Authorization: "Bearer mcp-secret",
139
+ "X-API-Key": "mcp-api-secret",
152
140
  },
153
141
  },
154
142
  },
@@ -167,64 +155,61 @@ describe("MCP config secret boundary", () => {
167
155
  });
168
156
  });
169
157
 
170
- test("config_get omits headers inside malformed MCP server trees", () => {
158
+ test("config_get ignores leftover config.json mcp so its headers cannot leak", () => {
171
159
  seedRawConfig({
172
160
  mcp: {
173
- servers: [
174
- {
161
+ servers: {
162
+ remote: {
175
163
  transport: {
176
- headers: { Authorization: "Bearer malformed-secret" },
164
+ type: "streamable-http",
165
+ url: "https://mcp.example.com",
166
+ headers: { Authorization: "Bearer leftover-secret" },
177
167
  },
178
168
  },
179
- ],
169
+ },
180
170
  },
181
171
  });
182
172
 
183
173
  const result = configGetRoute.handler({}) as Record<string, unknown>;
184
174
 
185
- expect(JSON.stringify(result)).not.toContain("malformed-secret");
186
- expect(withoutWireProfiles(result)).toEqual({
187
- mcp: {
188
- servers: [
189
- {
190
- transport: {},
191
- },
192
- ],
193
- },
194
- });
175
+ expect(JSON.stringify(result)).not.toContain("leftover-secret");
176
+ expect(
177
+ (result.mcp as { servers: Record<string, unknown> }).servers,
178
+ ).toEqual({});
195
179
  });
196
180
 
197
181
  test("config_get preserves an MCP server named headers", () => {
198
- seedRawConfig({
199
- mcp: {
200
- servers: {
201
- headers: {
202
- transport: {
203
- type: "streamable-http",
204
- url: "https://mcp.example.com",
205
- },
206
- },
182
+ seedWorkspaceMcp({
183
+ mcpServers: {
184
+ headers: {
185
+ type: "streamable-http",
186
+ url: "https://mcp.example.com",
207
187
  },
208
188
  },
209
189
  });
210
190
 
211
191
  const result = configGetRoute.handler({}) as Record<string, unknown>;
212
192
 
213
- expect(withoutWireProfiles(result)).toEqual(rawConfig);
193
+ expect(withoutWireProfiles(result).mcp).toEqual({
194
+ servers: {
195
+ headers: {
196
+ transport: {
197
+ type: "streamable-http",
198
+ url: "https://mcp.example.com",
199
+ },
200
+ },
201
+ },
202
+ });
214
203
  });
215
204
 
216
205
  test("config_get preserves non-credential headers env vars", () => {
217
- seedRawConfig({
218
- mcp: {
219
- servers: {
220
- local: {
221
- transport: {
222
- type: "stdio",
223
- command: "npx",
224
- env: {
225
- headers: "not-a-transport-header",
226
- },
227
- },
206
+ seedWorkspaceMcp({
207
+ mcpServers: {
208
+ local: {
209
+ type: "stdio",
210
+ command: "npx",
211
+ env: {
212
+ headers: "not-a-transport-header",
228
213
  },
229
214
  },
230
215
  },
@@ -232,7 +217,20 @@ describe("MCP config secret boundary", () => {
232
217
 
233
218
  const result = configGetRoute.handler({}) as Record<string, unknown>;
234
219
 
235
- expect(withoutWireProfiles(result)).toEqual(rawConfig);
220
+ expect(withoutWireProfiles(result).mcp).toEqual({
221
+ servers: {
222
+ local: {
223
+ transport: {
224
+ type: "stdio",
225
+ command: "npx",
226
+ args: [],
227
+ env: {
228
+ headers: "not-a-transport-header",
229
+ },
230
+ },
231
+ },
232
+ },
233
+ });
236
234
  });
237
235
 
238
236
  test("config_patch rejects MCP transport headers so generic writes cannot reintroduce plaintext credentials", async () => {
@@ -256,7 +254,7 @@ describe("MCP config secret boundary", () => {
256
254
  expect(committedRaw()).toBeNull();
257
255
  });
258
256
 
259
- test("config_patch allows an MCP server named headers when its value has no header credentials", async () => {
257
+ test("config_patch strips mcp so a full-config round trip cannot write it back", async () => {
260
258
  const result = await configPatchRoute.handler({
261
259
  body: {
262
260
  mcp: {
@@ -272,68 +270,25 @@ describe("MCP config secret boundary", () => {
272
270
  },
273
271
  });
274
272
 
275
- expect(withoutWireProfiles(result as Record<string, unknown>)).toEqual({
276
- mcp: {
277
- servers: {
278
- headers: {
279
- transport: {
280
- type: "streamable-http",
281
- url: "https://mcp.example.com",
282
- },
283
- },
284
- },
285
- },
286
- });
287
- });
288
-
289
- test("config_patch allows non-credential headers env vars", async () => {
290
- const result = await configPatchRoute.handler({
291
- body: {
292
- mcp: {
293
- servers: {
294
- local: {
295
- transport: {
296
- type: "stdio",
297
- command: "npx",
298
- env: {
299
- headers: "not-a-transport-header",
300
- },
301
- },
302
- },
303
- },
304
- },
305
- },
306
- });
307
-
308
- expect(withoutWireProfiles(result as Record<string, unknown>)).toEqual({
309
- mcp: {
310
- servers: {
311
- local: {
312
- transport: {
313
- type: "stdio",
314
- command: "npx",
315
- env: {
316
- headers: "not-a-transport-header",
317
- },
318
- },
319
- },
320
- },
321
- },
322
- });
273
+ expect(committedRaw()?.mcp).toBeUndefined();
274
+ expect(
275
+ (result as { mcp: { servers: Record<string, unknown> } }).mcp.servers,
276
+ ).toEqual({});
323
277
  });
324
278
 
325
- test("config_set rejects malformed MCP server trees containing headers", async () => {
279
+ test("config_set rejects MCP server writes", async () => {
326
280
  await expect(
327
281
  configSetRoute.handler({
328
282
  body: {
329
283
  path: "mcp.servers",
330
- value: [
331
- {
284
+ value: {
285
+ remote: {
332
286
  transport: {
333
- headers: { Authorization: "Bearer malformed-secret" },
287
+ type: "streamable-http",
288
+ url: "https://mcp.example.com",
334
289
  },
335
290
  },
336
- ],
291
+ },
337
292
  },
338
293
  }),
339
294
  ).rejects.toThrow(BadRequestError);
@@ -341,19 +296,6 @@ describe("MCP config secret boundary", () => {
341
296
  });
342
297
 
343
298
  test("config_set rejects direct MCP transport header paths", async () => {
344
- seedRawConfig({
345
- mcp: {
346
- servers: {
347
- remote: {
348
- transport: {
349
- type: "streamable-http",
350
- url: "https://mcp.example.com",
351
- },
352
- },
353
- },
354
- },
355
- });
356
-
357
299
  await expect(
358
300
  configSetRoute.handler({
359
301
  body: {
@@ -18,17 +18,14 @@ mock.module("../mcp/client.js", () => ({
18
18
  },
19
19
  }));
20
20
 
21
- import { setConfig } from "./helpers/set-config.js";
22
-
23
- // Seed the MCP server the list route reads via `loadRawConfig()` into the
24
- // workspace config for real.
25
- setConfig("mcp", {
26
- servers: {
27
- test: {
28
- transport: {
29
- type: "streamable-http",
30
- url: "https://example.com/mcp",
31
- },
21
+ import { setWorkspaceMcp } from "./helpers/set-workspace-mcp.js";
22
+
23
+ // Seed the MCP server the list route reads from workspace mcp.json.
24
+ setWorkspaceMcp({
25
+ test: {
26
+ transport: {
27
+ type: "streamable-http",
28
+ url: "https://example.com/mcp",
32
29
  },
33
30
  },
34
31
  });
@@ -68,17 +68,15 @@ mock.module("../daemon/mcp-reload-service.js", () => ({
68
68
  reloadMcpServers: async () => {},
69
69
  }));
70
70
 
71
- import { setConfig } from "./helpers/set-config.js";
71
+ import { setWorkspaceMcp } from "./helpers/set-workspace-mcp.js";
72
72
 
73
- setConfig("mcp", {
74
- servers: {
75
- "from-workspace": {
76
- transport: { type: "streamable-http", url: "https://config.example/mcp" },
77
- },
78
- // Deliberately shares an id with the `shadowed` plugin below.
79
- shadowed: {
80
- transport: { type: "streamable-http", url: "https://wins.example/mcp" },
81
- },
73
+ setWorkspaceMcp({
74
+ "from-workspace": {
75
+ transport: { type: "streamable-http", url: "https://config.example/mcp" },
76
+ },
77
+ // Deliberately shares an id with the `shadowed` plugin below.
78
+ shadowed: {
79
+ transport: { type: "streamable-http", url: "https://wins.example/mcp" },
82
80
  },
83
81
  });
84
82
 
@@ -0,0 +1,119 @@
1
+ /**
2
+ * The `message-deleted` hook is dispatched by every primitive that removes
3
+ * individual message rows, carrying each row's `(createdAt, id)`, so a plugin
4
+ * keeping a cursor on a row learns where it sat once the row is gone.
5
+ * DB-backed: exercises the real delete primitives against the test database.
6
+ */
7
+ import { beforeEach, describe, expect, test } from "bun:test";
8
+
9
+ import { registerPluginHooks } from "../hooks/registry.js";
10
+ import type { MessageDeletedContext } from "../hooks/types.js";
11
+ import {
12
+ addMessage,
13
+ deleteLastExchange,
14
+ deleteMessageById,
15
+ getMessages,
16
+ } from "../persistence/conversation-crud.js";
17
+ import { getDb } from "../persistence/db-connection.js";
18
+ import { initializeDb } from "../persistence/db-init.js";
19
+ import { conversations, messages } from "../persistence/schema/index.js";
20
+ import type { PluginHooks } from "../plugins/types.js";
21
+
22
+ await initializeDb();
23
+
24
+ const CONV_ID = "conv-message-deleted-hook";
25
+
26
+ const received: Array<{
27
+ conversationId: string;
28
+ messageId: string;
29
+ createdAt: number;
30
+ }> = [];
31
+ registerPluginHooks("test-message-deleted-spy", {
32
+ "message-deleted": async (ctx: MessageDeletedContext) => {
33
+ received.push({
34
+ conversationId: ctx.conversationId,
35
+ messageId: ctx.messageId,
36
+ createdAt: ctx.createdAt,
37
+ });
38
+ },
39
+ } as PluginHooks);
40
+
41
+ /** Poll until the fire-and-forget hook chain has delivered `count` calls. */
42
+ async function waitForCalls(count: number): Promise<void> {
43
+ const deadline = Date.now() + 2_000;
44
+ while (received.length < count && Date.now() < deadline) {
45
+ await Bun.sleep(10);
46
+ }
47
+ }
48
+
49
+ const text = (t: string) => JSON.stringify([{ type: "text", text: t }]);
50
+
51
+ beforeEach(() => {
52
+ received.length = 0;
53
+ getDb().delete(messages).run();
54
+ getDb().delete(conversations).run();
55
+ const now = Date.now();
56
+ getDb()
57
+ .insert(conversations)
58
+ .values({
59
+ id: CONV_ID,
60
+ title: "message-deleted hook",
61
+ createdAt: now,
62
+ updatedAt: now,
63
+ source: "test",
64
+ conversationType: "standard",
65
+ })
66
+ .run();
67
+ });
68
+
69
+ describe("message-deleted hook dispatch", () => {
70
+ test("deleteMessageById dispatches once with the row's position", async () => {
71
+ await addMessage(CONV_ID, "user", text("hello"));
72
+ const reply = await addMessage(CONV_ID, "assistant", text("hi"));
73
+ const replyCreatedAt = getMessages(CONV_ID).find(
74
+ (m) => m.id === reply.id,
75
+ )!.createdAt;
76
+
77
+ deleteMessageById(reply.id);
78
+ await waitForCalls(1);
79
+
80
+ expect(received).toEqual([
81
+ {
82
+ conversationId: CONV_ID,
83
+ messageId: reply.id,
84
+ createdAt: replyCreatedAt,
85
+ },
86
+ ]);
87
+ });
88
+
89
+ test("deleteLastExchange dispatches for every row of the undone tail", async () => {
90
+ await addMessage(CONV_ID, "user", text("first"));
91
+ await addMessage(CONV_ID, "assistant", text("one"));
92
+ const user2 = await addMessage(CONV_ID, "user", text("second"));
93
+ const reply2 = await addMessage(CONV_ID, "assistant", text("two"));
94
+ const byId = new Map(getMessages(CONV_ID).map((m) => [m.id, m.createdAt]));
95
+
96
+ expect(deleteLastExchange(CONV_ID)).toBe(2);
97
+ await waitForCalls(2);
98
+
99
+ expect([...received].sort((a, b) => a.createdAt - b.createdAt)).toEqual([
100
+ {
101
+ conversationId: CONV_ID,
102
+ messageId: user2.id,
103
+ createdAt: byId.get(user2.id)!,
104
+ },
105
+ {
106
+ conversationId: CONV_ID,
107
+ messageId: reply2.id,
108
+ createdAt: byId.get(reply2.id)!,
109
+ },
110
+ ]);
111
+ });
112
+
113
+ test("a delete of a missing row dispatches nothing", async () => {
114
+ deleteMessageById("no-such-row");
115
+ await Bun.sleep(50);
116
+
117
+ expect(received).toEqual([]);
118
+ });
119
+ });
@@ -1,11 +1,14 @@
1
1
  import { beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
+ import { eq } from "drizzle-orm";
4
+
3
5
  import {
4
6
  countMessagesAfter,
5
7
  getMessagesAfter,
6
8
  } from "../persistence/conversation-crud.js";
7
9
  import { getDb } from "../persistence/db-connection.js";
8
10
  import { initializeDb } from "../persistence/db-init.js";
11
+ import { resolveMessagesAfterBound } from "../persistence/message-cursor.js";
9
12
  import { conversations, messages } from "../persistence/schema/index.js";
10
13
 
11
14
  await initializeDb();
@@ -113,3 +116,91 @@ describe("countMessagesAfter / getMessagesAfter — millisecond-collision tie-br
113
116
  expect(getMessagesAfter(CONV_ID, "")).toHaveLength(2);
114
117
  });
115
118
  });
119
+
120
+ describe("countMessagesAfter / getMessagesAfter: a cursor survives its row's deletion", () => {
121
+ beforeEach(() => {
122
+ clearDb();
123
+ seedConversation();
124
+ });
125
+
126
+ test("a cursor carrying createdAt keeps bounding the read after its row is deleted", () => {
127
+ const ts = 1_700_000_000_000;
128
+ insertMessage("a", ts);
129
+ // The row the cursor points at: a reply the user then regenerates.
130
+ insertMessage("b", ts + 1);
131
+ insertMessage("c", ts + 2);
132
+ getDb().delete(messages).where(eq(messages.id, "b")).run();
133
+ // The regenerated reply.
134
+ insertMessage("d", ts + 3);
135
+
136
+ const cursor = { id: "b", createdAt: ts + 1 };
137
+ expect(countMessagesAfter(CONV_ID, cursor)).toBe(2);
138
+ expect(getMessagesAfter(CONV_ID, cursor).map((m) => m.id)).toEqual([
139
+ "c",
140
+ "d",
141
+ ]);
142
+ });
143
+
144
+ test("a cursor without createdAt keeps the conservative id-only semantics once its row is gone", () => {
145
+ insertMessage("a", 1_700_000_000_000);
146
+
147
+ const cursor = { id: "nonexistent", createdAt: null };
148
+ expect(countMessagesAfter(CONV_ID, cursor)).toBe(0);
149
+ expect(getMessagesAfter(CONV_ID, cursor)).toEqual([]);
150
+ });
151
+
152
+ test("the live row wins over the timestamp the cursor carries", () => {
153
+ const ts = 1_700_000_000_000;
154
+ insertMessage("a", ts);
155
+ insertMessage("b", ts + 10);
156
+
157
+ // A stale cursor timestamp must not pull processed rows back in, nor
158
+ // hide unprocessed ones, while the row itself can still be read.
159
+ expect(countMessagesAfter(CONV_ID, { id: "b", createdAt: ts - 100 })).toBe(
160
+ 0,
161
+ );
162
+ expect(countMessagesAfter(CONV_ID, { id: "a", createdAt: ts + 999 })).toBe(
163
+ 1,
164
+ );
165
+ });
166
+
167
+ test("a cursor with an empty id reads everything, like null and the empty string", () => {
168
+ insertMessage("a", 1_700_000_000_000);
169
+
170
+ expect(countMessagesAfter(CONV_ID, { id: "", createdAt: 5 })).toBe(1);
171
+ expect(getMessagesAfter(CONV_ID, { id: "", createdAt: 5 })).toHaveLength(1);
172
+ });
173
+ });
174
+
175
+ describe("resolveMessagesAfterBound", () => {
176
+ beforeEach(() => {
177
+ clearDb();
178
+ seedConversation();
179
+ });
180
+
181
+ test("resolves the unbounded, live-row, stored-timestamp, and vanished cases", () => {
182
+ insertMessage("a", 1_000);
183
+
184
+ expect(resolveMessagesAfterBound(null)).toEqual({ kind: "all" });
185
+ expect(resolveMessagesAfterBound("")).toEqual({ kind: "all" });
186
+ expect(resolveMessagesAfterBound({ id: "", createdAt: 3 })).toEqual({
187
+ kind: "all",
188
+ });
189
+ expect(resolveMessagesAfterBound("a")).toEqual({
190
+ kind: "after",
191
+ bound: { createdAt: 1_000, id: "a" },
192
+ });
193
+ expect(resolveMessagesAfterBound({ id: "a", createdAt: 99 })).toEqual({
194
+ kind: "after",
195
+ bound: { createdAt: 1_000, id: "a" },
196
+ });
197
+ expect(resolveMessagesAfterBound("gone")).toEqual({ kind: "vanished" });
198
+ expect(resolveMessagesAfterBound({ id: "gone", createdAt: null })).toEqual({
199
+ kind: "vanished",
200
+ });
201
+ expect(resolveMessagesAfterBound({ id: "gone", createdAt: 7 })).toEqual({
202
+ kind: "after",
203
+ bound: { createdAt: 7, id: "gone" },
204
+ });
205
+ });
206
+ });
@@ -52,7 +52,7 @@ const BASELINE: Readonly<Record<string, number>> = {
52
52
  "src/plugins/defaults/memory/context-search/sources/conversations.ts": 1,
53
53
  "src/plugins/defaults/memory/graph/image-ref-utils.ts": 1,
54
54
  "src/plugins/defaults/memory/indexer.ts": 2,
55
- "src/plugins/defaults/memory/memory-retrospective-accounting.ts": 4,
55
+ "src/plugins/defaults/memory/memory-retrospective-accounting.ts": 2,
56
56
  "src/plugins/defaults/memory/substrate/sweep-job.ts": 1,
57
57
  "src/plugins/defaults/memory/v1/graph/extraction.ts": 3,
58
58
  "src/plugins/defaults/memory/v1/job-handlers/backfill.ts": 1,