@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
@@ -6,16 +6,22 @@ import { setConfig } from "../../../../__tests__/helpers/set-config.js";
6
6
  // pipeline (both flags default true under the real loader).
7
7
  setConfig("memory", { enabled: false, v2: { enabled: false } });
8
8
 
9
+ import { createConversation } from "../../../../persistence/conversation-crud.js";
9
10
  import {
10
11
  getDb,
11
12
  getMemorySqlite,
13
+ resetDb,
12
14
  } from "../../../../persistence/db-connection.js";
13
15
  import { initializeDb } from "../../../../persistence/db-init.js";
16
+ import { messages } from "../../../../persistence/schema/index.js";
14
17
  import {
18
+ _resetRetrospectiveCursorColumnForTests,
15
19
  appendToRememberedLog,
16
20
  bumpRetrospectiveLastRunAt,
21
+ ensureRetrospectiveCursorColumn,
17
22
  forkRetrospectiveState,
18
23
  getRetrospectiveState,
24
+ listRetrospectiveStates,
19
25
  REMEMBERED_LOG_MAX_BYTES,
20
26
  REMEMBERED_LOG_MAX_ENTRIES,
21
27
  upsertRetrospectiveState,
@@ -176,6 +182,219 @@ describe("memory-retrospective-state remembered log persistence", () => {
176
182
  });
177
183
  });
178
184
 
185
+ describe("memory-retrospective-state cursor timestamp", () => {
186
+ beforeEach(() => {
187
+ resetTables();
188
+ });
189
+
190
+ test("upsert persists lastProcessedCreatedAt and both reads return it", async () => {
191
+ await upsertRetrospectiveState({
192
+ conversationId: "conv-ts",
193
+ lastProcessedMessageId: "m1",
194
+ lastProcessedCreatedAt: 1_234,
195
+ lastRunAt: 1000,
196
+ });
197
+
198
+ expect(getRetrospectiveState("conv-ts")?.lastProcessedCreatedAt).toBe(
199
+ 1_234,
200
+ );
201
+ expect(
202
+ listRetrospectiveStates(10).find((s) => s.conversationId === "conv-ts")
203
+ ?.lastProcessedCreatedAt,
204
+ ).toBe(1_234);
205
+ });
206
+
207
+ test("an upsert that omits the timestamp records unknown for the new pointer", async () => {
208
+ await upsertRetrospectiveState({
209
+ conversationId: "conv-ts",
210
+ lastProcessedMessageId: "m1",
211
+ lastProcessedCreatedAt: 1_234,
212
+ lastRunAt: 1000,
213
+ });
214
+ await upsertRetrospectiveState({
215
+ conversationId: "conv-ts",
216
+ lastProcessedMessageId: "m2",
217
+ lastRunAt: 2000,
218
+ });
219
+
220
+ const state = getRetrospectiveState("conv-ts");
221
+ expect(state?.lastProcessedMessageId).toBe("m2");
222
+ expect(state?.lastProcessedCreatedAt).toBeNull();
223
+ });
224
+
225
+ test("bumpRetrospectiveLastRunAt leaves the timestamp untouched", async () => {
226
+ await upsertRetrospectiveState({
227
+ conversationId: "conv-ts",
228
+ lastProcessedMessageId: "m1",
229
+ lastProcessedCreatedAt: 1_234,
230
+ lastRunAt: 1000,
231
+ });
232
+ await bumpRetrospectiveLastRunAt("conv-ts", 5000);
233
+
234
+ const state = getRetrospectiveState("conv-ts");
235
+ expect(state?.lastRunAt).toBe(5000);
236
+ expect(state?.lastProcessedCreatedAt).toBe(1_234);
237
+ });
238
+
239
+ test("forking carries the forked pointer's createdAt, read through the main handle", async () => {
240
+ const db = getDb();
241
+ createConversation({ id: "conv-fork-ts-src" });
242
+ createConversation({ id: "conv-fork-ts-dst" });
243
+ for (const [id, conversationId] of [
244
+ ["src-m1", "conv-fork-ts-src"],
245
+ ["dst-m1", "conv-fork-ts-dst"],
246
+ ] as const) {
247
+ db.insert(messages)
248
+ .values({
249
+ id,
250
+ conversationId,
251
+ role: "user",
252
+ content: "[]",
253
+ createdAt: 4_000,
254
+ metadata: null,
255
+ })
256
+ .run();
257
+ }
258
+ await upsertRetrospectiveState({
259
+ conversationId: "conv-fork-ts-src",
260
+ lastProcessedMessageId: "src-m1",
261
+ lastProcessedCreatedAt: 4_000,
262
+ lastRunAt: 1,
263
+ });
264
+
265
+ forkRetrospectiveState({
266
+ database: db,
267
+ sourceConversationId: "conv-fork-ts-src",
268
+ forkedConversationId: "conv-fork-ts-dst",
269
+ forkedMessageIds: new Map([["src-m1", "dst-m1"]]),
270
+ lastCopiedSourceMessageId: "src-m1",
271
+ });
272
+
273
+ const child = getRetrospectiveState("conv-fork-ts-dst");
274
+ expect(child?.lastProcessedMessageId).toBe("dst-m1");
275
+ expect(child?.lastProcessedCreatedAt).toBe(4_000);
276
+
277
+ db.run(`DELETE FROM messages`);
278
+ db.run(`DELETE FROM conversations`);
279
+ });
280
+
281
+ test("a source cursor whose row was regenerated away is placed by its stored bound, not clamped past the copies", async () => {
282
+ const db = getDb();
283
+ createConversation({ id: "conv-fork-bound-src" });
284
+ createConversation({ id: "conv-fork-bound-dst" });
285
+ // src-m2 (createdAt 2_000) was the cursor; a regenerate deleted it and
286
+ // src-m3 replaced it. The fork copies src-m1 and src-m3.
287
+ for (const [id, conversationId, createdAt] of [
288
+ ["src-m1", "conv-fork-bound-src", 1_000],
289
+ ["src-m3", "conv-fork-bound-src", 3_000],
290
+ ["dst-m1", "conv-fork-bound-dst", 1_000],
291
+ ["dst-m3", "conv-fork-bound-dst", 3_000],
292
+ ] as const) {
293
+ db.insert(messages)
294
+ .values({
295
+ id,
296
+ conversationId,
297
+ role: "user",
298
+ content: "[]",
299
+ createdAt,
300
+ metadata: null,
301
+ })
302
+ .run();
303
+ }
304
+ await upsertRetrospectiveState({
305
+ conversationId: "conv-fork-bound-src",
306
+ lastProcessedMessageId: "src-m2",
307
+ lastProcessedCreatedAt: 2_000,
308
+ lastRunAt: 1,
309
+ });
310
+
311
+ forkRetrospectiveState({
312
+ database: db,
313
+ sourceConversationId: "conv-fork-bound-src",
314
+ forkedConversationId: "conv-fork-bound-dst",
315
+ forkedMessageIds: new Map([
316
+ ["src-m1", "dst-m1"],
317
+ ["src-m3", "dst-m3"],
318
+ ]),
319
+ lastCopiedSourceMessageId: "src-m3",
320
+ });
321
+
322
+ const child = getRetrospectiveState("conv-fork-bound-dst");
323
+ expect(child?.lastProcessedMessageId).toBe("dst-m1");
324
+ expect(child?.lastProcessedCreatedAt).toBe(1_000);
325
+
326
+ // A bound before every copied row leaves the child with nothing processed.
327
+ await upsertRetrospectiveState({
328
+ conversationId: "conv-fork-bound-src",
329
+ lastProcessedMessageId: "src-m0",
330
+ lastProcessedCreatedAt: 500,
331
+ lastRunAt: 1,
332
+ });
333
+ forkRetrospectiveState({
334
+ database: db,
335
+ sourceConversationId: "conv-fork-bound-src",
336
+ forkedConversationId: "conv-fork-bound-dst",
337
+ forkedMessageIds: new Map([
338
+ ["src-m1", "dst-m1"],
339
+ ["src-m3", "dst-m3"],
340
+ ]),
341
+ lastCopiedSourceMessageId: "src-m3",
342
+ });
343
+ expect(
344
+ getRetrospectiveState("conv-fork-bound-dst")?.lastProcessedMessageId,
345
+ ).toBe("");
346
+
347
+ db.run(`DELETE FROM messages`);
348
+ db.run(`DELETE FROM conversations`);
349
+ });
350
+
351
+ test("ensureRetrospectiveCursorColumn adds the column to a legacy table once and is a no-op after", () => {
352
+ const raw = getMemorySqlite()!;
353
+ raw.exec("DROP TABLE memory_retrospective_state");
354
+ raw.exec(`
355
+ CREATE TABLE memory_retrospective_state (
356
+ conversation_id TEXT PRIMARY KEY,
357
+ last_processed_message_id TEXT NOT NULL,
358
+ last_run_at INTEGER NOT NULL,
359
+ remembered_log TEXT
360
+ )
361
+ `);
362
+ _resetRetrospectiveCursorColumnForTests();
363
+
364
+ expect(ensureRetrospectiveCursorColumn("test")).toBe(true);
365
+ expect(ensureRetrospectiveCursorColumn("test")).toBe(true);
366
+
367
+ const columns = raw
368
+ .query("PRAGMA table_info(memory_retrospective_state)")
369
+ .all() as Array<{ name: string }>;
370
+ expect(
371
+ columns.filter((c) => c.name === "last_processed_created_at"),
372
+ ).toHaveLength(1);
373
+ });
374
+
375
+ test("a replaced memory connection is probed again, so a restored database without the column gets it", () => {
376
+ expect(ensureRetrospectiveCursorColumn("test")).toBe(true);
377
+
378
+ // What a restore or import does: the stored connections are dropped and
379
+ // reopened. Strip the column on the reopened file to stand in for an
380
+ // older database.
381
+ resetDb();
382
+ const reopened = getMemorySqlite()!;
383
+ reopened.exec(
384
+ "ALTER TABLE memory_retrospective_state DROP COLUMN last_processed_created_at",
385
+ );
386
+
387
+ expect(getRetrospectiveState("conv-none")).toBeNull();
388
+ expect(ensureRetrospectiveCursorColumn("test")).toBe(true);
389
+ const columns = reopened
390
+ .query("PRAGMA table_info(memory_retrospective_state)")
391
+ .all() as Array<{ name: string }>;
392
+ expect(columns.some((c) => c.name === "last_processed_created_at")).toBe(
393
+ true,
394
+ );
395
+ });
396
+ });
397
+
179
398
  describe("fail-soft when the underlying statement fails", () => {
180
399
  // The memory connection is present, but the relocated table is gone (a
181
400
  // corrupt/dropped table, SQLITE_FULL, I/O error, or SQLITE_BUSY after
@@ -412,3 +412,34 @@ describe("listSweepCandidateConversationIds", () => {
412
412
  ]);
413
413
  });
414
414
  });
415
+
416
+ describe("runRetrospectiveSweep: cursor survives a regenerated reply", () => {
417
+ beforeEach(() => {
418
+ resetTables();
419
+ enqueueCalls = [];
420
+ enqueueDeclines = new Set();
421
+ });
422
+
423
+ test("a stale conversation whose cursor row was regenerated away is still swept", async () => {
424
+ const conv = createConversation({ id: "conv-a" });
425
+ insertMessage(conv.id, { createdAt: 1_000 });
426
+ const reply = insertMessage(conv.id, {
427
+ role: "assistant",
428
+ createdAt: 2_000,
429
+ });
430
+ await upsertRetrospectiveState({
431
+ conversationId: conv.id,
432
+ lastProcessedMessageId: reply,
433
+ lastProcessedCreatedAt: 2_000,
434
+ lastRunAt: Date.now() - SWEEP_INTERVAL_MS - 60_000,
435
+ });
436
+ getDb().delete(messages).where(eq(messages.id, reply)).run();
437
+ insertMessage(conv.id, { role: "assistant", createdAt: 3_000 });
438
+
439
+ await runRetrospectiveSweep(makeConfig());
440
+
441
+ expect(enqueueCalls).toEqual([
442
+ { conversationId: conv.id, trigger: "sweep" },
443
+ ]);
444
+ });
445
+ });
@@ -12,6 +12,8 @@ mock.module("../memory-retrospective-enqueue.js", () => ({
12
12
  },
13
13
  }));
14
14
 
15
+ import { eq } from "drizzle-orm";
16
+
15
17
  import type { AssistantConfig } from "../../../../config/types.js";
16
18
  import { createConversation } from "../../../../persistence/conversation-crud.js";
17
19
  import {
@@ -307,3 +309,64 @@ describe("maybeEnqueueRetrospective — kind-aware accounting", () => {
307
309
  ]);
308
310
  });
309
311
  });
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // maybeEnqueueRetrospective: the cursor survives a regenerated reply.
315
+ //
316
+ // The cursor normally lands on the latest assistant reply, and a regenerate
317
+ // deletes exactly that row. With its `createdAt` persisted the cursor still
318
+ // bounds the count, so the conversation keeps getting retrospectives.
319
+ // ---------------------------------------------------------------------------
320
+
321
+ describe("maybeEnqueueRetrospective: cursor survives a regenerated reply", () => {
322
+ beforeEach(() => {
323
+ resetTables();
324
+ enqueueCalls = [];
325
+ });
326
+
327
+ test("a regenerate that deletes the cursor's row does not stall the conversation", async () => {
328
+ const conv = createConversation("conv");
329
+ insertMessage(conv.id, { createdAt: 1_000 });
330
+ const reply = insertMessage(conv.id, {
331
+ role: "assistant",
332
+ createdAt: 2_000,
333
+ });
334
+ await upsertRetrospectiveState({
335
+ conversationId: conv.id,
336
+ lastProcessedMessageId: reply,
337
+ lastProcessedCreatedAt: 2_000,
338
+ lastRunAt: Date.now() - 24 * 60 * 60_000,
339
+ });
340
+ // Regenerate: the retry route discards the latest assistant turn, then
341
+ // a fresh reply and the next user message land.
342
+ getDb().delete(messages).where(eq(messages.id, reply)).run();
343
+ insertMessage(conv.id, { role: "assistant", createdAt: 3_000 });
344
+ insertMessage(conv.id, { createdAt: 4_000 });
345
+
346
+ maybeEnqueueRetrospective(conv.id, makeConfig());
347
+
348
+ expect(enqueueCalls).toEqual([
349
+ { conversationId: conv.id, trigger: "interval" },
350
+ ]);
351
+ });
352
+
353
+ test("a cursor written without a timestamp still goes quiet once its row is gone", async () => {
354
+ const conv = createConversation("conv");
355
+ insertMessage(conv.id, { createdAt: 1_000 });
356
+ const reply = insertMessage(conv.id, {
357
+ role: "assistant",
358
+ createdAt: 2_000,
359
+ });
360
+ await upsertRetrospectiveState({
361
+ conversationId: conv.id,
362
+ lastProcessedMessageId: reply,
363
+ lastRunAt: Date.now() - 24 * 60 * 60_000,
364
+ });
365
+ getDb().delete(messages).where(eq(messages.id, reply)).run();
366
+ insertMessage(conv.id, { createdAt: 4_000 });
367
+
368
+ maybeEnqueueRetrospective(conv.id, makeConfig());
369
+
370
+ expect(enqueueCalls).toEqual([]);
371
+ });
372
+ });
@@ -14,6 +14,10 @@ mock.module("../worker-control.js", () => ({
14
14
  },
15
15
  }));
16
16
 
17
+ mock.module("../skill-md-mtime-poll.js", () => ({
18
+ stopWorkspaceSkillMdMtimePoll: () => {},
19
+ }));
20
+
17
21
  const memoryShutdown = (await import("../hooks/shutdown.js")).default;
18
22
 
19
23
  describe("memory shutdown hook", () => {
@@ -16,6 +16,7 @@ import {
16
16
  loadSkillCatalog,
17
17
  type SkillSummary,
18
18
  } from "../../../../config/skills.js";
19
+ import { loadWorkspaceMcpConfig } from "../../../../mcp/workspace-mcp-config.js";
19
20
  import {
20
21
  enqueueMemoryJob,
21
22
  upsertEmbedGraphNodeJob,
@@ -131,12 +132,9 @@ export function seedSkillGraphNodes(): void {
131
132
  const input = fromSkillSummary(summary);
132
133
 
133
134
  if (summary.id === "mcp-setup") {
134
- const servers = config.mcp?.servers;
135
- if (servers) {
136
- const names = Object.keys(servers);
137
- if (names.length > 0) {
138
- input.description += ` Configured: ${names.join(", ")}`;
139
- }
135
+ const names = Object.keys(loadWorkspaceMcpConfig().servers);
136
+ if (names.length > 0) {
137
+ input.description += ` Configured: ${names.join(", ")}`;
140
138
  }
141
139
  }
142
140
 
@@ -17,6 +17,7 @@ import type { HookFunction, InitContext } from "@vellumai/plugin-api";
17
17
  import { getConfig } from "../../../../config/loader.js";
18
18
  import { registerMemoryPluginJobHandlers } from "../job-handler-registration.js";
19
19
  import { getLogger } from "../logging.js";
20
+ import { backfillRetrospectiveCursorTimestamps } from "../memory-retrospective-cursor-backfill.js";
20
21
  import { runMemoryStartup } from "../startup.js";
21
22
  import { ensureMemoryV3PluginSchema } from "../v3/plugin-schema.js";
22
23
 
@@ -30,6 +31,15 @@ const init: HookFunction<InitContext> = async () => {
30
31
  // against an empty table and failed as an unknown type.
31
32
  registerMemoryPluginJobHandlers();
32
33
 
34
+ // Retrospective cursors written before the timestamp column existed can
35
+ // only be completed while their message rows still exist, so start the
36
+ // fill at boot, ahead of any client regenerating a reply. Detached and
37
+ // fail-open like the rest of memory startup; the memory worker runs the
38
+ // same idempotent pass as a second chance.
39
+ void backfillRetrospectiveCursorTimestamps().catch((err) =>
40
+ log.warn({ err }, "Retrospective cursor timestamp backfill failed"),
41
+ );
42
+
33
43
  // The memory-v3 plugin-owned schema on the memory connection
34
44
  // (`v3/plugin-schema.ts`): the pools and injected-sections tables and the
35
45
  // selection log's `section_key` column, ensured idempotently on every
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Default `memory` message-deleted hook.
3
+ *
4
+ * Stamps the conversation's retrospective cursor with the deleted row's
5
+ * `createdAt` when the cursor points at that row and was written without a
6
+ * timestamp (by a build before the column existed). The cursor usually sits
7
+ * on the latest assistant reply, which is exactly the row a regenerate
8
+ * deletes, and once the row is gone only the timestamp can place it.
9
+ */
10
+
11
+ import type { HookFunction, MessageDeletedContext } from "@vellumai/plugin-api";
12
+
13
+ import { preserveRetrospectiveCursorTimestamps } from "../memory-retrospective-cursor-preserve.js";
14
+
15
+ const messageDeleted: HookFunction<MessageDeletedContext> = async (ctx) => {
16
+ await preserveRetrospectiveCursorTimestamps(ctx.conversationId, [
17
+ { id: ctx.messageId, createdAt: ctx.createdAt },
18
+ ]);
19
+ };
20
+
21
+ export default messageDeleted;
@@ -9,9 +9,11 @@
9
9
 
10
10
  import type { HookFunction, ShutdownContext } from "@vellumai/plugin-api";
11
11
 
12
+ import { stopWorkspaceSkillMdMtimePoll } from "../skill-md-mtime-poll.js";
12
13
  import { stopMemoryWorkerProcess } from "../worker-control.js";
13
14
 
14
15
  const shutdown: HookFunction<ShutdownContext> = async () => {
16
+ stopWorkspaceSkillMdMtimePoll();
15
17
  stopMemoryWorkerProcess();
16
18
  };
17
19
 
@@ -62,6 +62,7 @@ import {
62
62
  import { isJobQueueResolution, type JobHandler } from "../../types.js";
63
63
  import { sweepOrphanConversationMemoryTables } from "./conversation-memory-orphan-sweep.js";
64
64
  import { getLogger } from "./logging.js";
65
+ import { backfillRetrospectiveCursorTimestamps } from "./memory-retrospective-cursor-backfill.js";
65
66
  import { sweepOrphanMemoryRetrospectiveConversations } from "./memory-retrospective-startup-cleanup.js";
66
67
  import { getWorkspaceDir } from "./paths.js";
67
68
  // SUBSTRATE (v2+v3) — feeds `enqueueSubstrateMaintenanceJobs`.
@@ -259,6 +260,18 @@ export function startMemoryJobsWorkerLoop(): MemoryJobsWorker {
259
260
  );
260
261
  });
261
262
 
263
+ // Second chance for the cursor timestamp backfill the daemon's memory init
264
+ // hook starts at boot: rows written before the column existed get their
265
+ // `createdAt` so a later deletion of the row they point at (a regenerated
266
+ // reply) cannot stall the conversation's retrospectives. Idempotent,
267
+ // detached, and best-effort, same as the sweeps.
268
+ void backfillRetrospectiveCursorTimestamps().catch((err: unknown) => {
269
+ log.warn(
270
+ { err },
271
+ "Retrospective cursor timestamp backfill failed; continuing worker startup",
272
+ );
273
+ });
274
+
262
275
  // Also catch up on relocated conversation-keyed tables orphaned while the
263
276
  // plugin was disabled: with memory off, the conversation-deleted hook never
264
277
  // fired, so deletes during that window left rows behind (the pre-Wave-2 main
@@ -31,7 +31,7 @@
31
31
  // accounting; once newer real messages arrive it is copied into the next
32
32
  // run's fork as inert prefix context.
33
33
 
34
- import { and, eq, gt, like, or } from "drizzle-orm";
34
+ import { and, eq, like, or } from "drizzle-orm";
35
35
 
36
36
  import {
37
37
  countMessagesAfter,
@@ -43,6 +43,11 @@ import {
43
43
  SIGHT_FRAME_ATTACHMENT_IDS_KEY,
44
44
  } from "../../../persistence/conversation-types.js";
45
45
  import { getDb } from "../../../persistence/db-connection.js";
46
+ import {
47
+ messagesAfterBoundFilter,
48
+ type MessagesAfterRef,
49
+ resolveMessagesAfterBound,
50
+ } from "../../../persistence/message-cursor.js";
46
51
  import { messages } from "../../../persistence/schema/index.js";
47
52
  import { SKILL_CARD_MESSAGE_KIND } from "./memory-retrospective-constants.js";
48
53
 
@@ -98,9 +103,9 @@ export function isSkillCardMessage(row: { metadata: string | null }): boolean {
98
103
  */
99
104
  export function getRetrospectiveMessagesAfter(
100
105
  conversationId: string,
101
- afterMessageId: string | null,
106
+ after: MessagesAfterRef,
102
107
  ): MessageRow[] {
103
- const rows = getMessagesAfter(conversationId, afterMessageId);
108
+ const rows = getMessagesAfter(conversationId, after);
104
109
  const firstUnfinalized = rows.findIndex((row) => row.finalized !== 1);
105
110
  const bounded =
106
111
  firstUnfinalized === -1 ? rows : rows.slice(0, firstUnfinalized);
@@ -116,13 +121,13 @@ export function getRetrospectiveMessagesAfter(
116
121
  */
117
122
  export function countRetrospectiveMessagesAfter(
118
123
  conversationId: string,
119
- afterMessageId: string | null,
124
+ after: MessagesAfterRef,
120
125
  ): number {
121
- const total = countMessagesAfter(conversationId, afterMessageId);
126
+ const total = countMessagesAfter(conversationId, after);
122
127
  if (total === 0) {
123
128
  return 0;
124
129
  }
125
- const excluded = countExcludedMessagesAfter(conversationId, afterMessageId);
130
+ const excluded = countExcludedMessagesAfter(conversationId, after);
126
131
  return Math.max(0, total - excluded);
127
132
  }
128
133
 
@@ -167,7 +172,8 @@ export function messagesHaveUserActivity(
167
172
  * a non-tool_result block. Powers the `memory.retrospective.requireUserActivity`
168
173
  * enqueue gate, so only user rows are loaded. Cursor semantics mirror
169
174
  * `countMessagesAfter`: a null/`""` reference scans the whole conversation,
170
- * and a vanished reference means no new work.
175
+ * a cursor whose row is gone falls back to the timestamp it carries, and a
176
+ * bare id whose row is gone means no new work.
171
177
  *
172
178
  * Operates on the raw `content` column: rows that do not parse to a block
173
179
  * array (file-backed `{ ref }` rows, legacy plain strings) count as
@@ -177,37 +183,15 @@ export function messagesHaveUserActivity(
177
183
  */
178
184
  export function hasQualifyingUserMessageAfter(
179
185
  conversationId: string,
180
- afterMessageId: string | null,
186
+ after: MessagesAfterRef,
181
187
  ): boolean {
182
- const cursorId =
183
- afterMessageId === null || afterMessageId === "" ? null : afterMessageId;
184
- const db = getDb();
185
-
186
- let ref: { createdAt: number } | undefined;
187
- if (cursorId !== null) {
188
- ref = db
189
- .select({ createdAt: messages.createdAt })
190
- .from(messages)
191
- .where(eq(messages.id, cursorId))
192
- .get();
193
- if (!ref) {
194
- return false;
195
- }
188
+ const start = resolveMessagesAfterBound(after);
189
+ if (start.kind === "vanished") {
190
+ return false;
196
191
  }
197
-
198
192
  const afterCursor =
199
- cursorId !== null && ref
200
- ? [
201
- or(
202
- gt(messages.createdAt, ref.createdAt),
203
- and(
204
- eq(messages.createdAt, ref.createdAt),
205
- gt(messages.id, cursorId),
206
- ),
207
- ),
208
- ]
209
- : [];
210
- const rows = db
193
+ start.kind === "after" ? [messagesAfterBoundFilter(start.bound)] : [];
194
+ const rows = getDb()
211
195
  .select({ content: messages.content, metadata: messages.metadata })
212
196
  .from(messages)
213
197
  .where(
@@ -248,18 +232,14 @@ function rawUserContentCarriesActivity(raw: string | null): boolean {
248
232
  * — at most one per successful retrospective run); each candidate is then
249
233
  * JSON-verified by {@link isSkillCardMessage} so an incidental substring
250
234
  * match in unrelated metadata can never exclude a real message from
251
- * accounting. The cursor comparison mirrors `countMessagesAfter`'s
252
- * tie-breaker semantics, including the null/`""` "count everything" cases
253
- * and the vanished-reference "no new work" case.
235
+ * accounting. The cursor resolves exactly as `countMessagesAfter`'s does,
236
+ * including the null/`""` "count everything" cases and the vanished-reference
237
+ * "no new work" case.
254
238
  */
255
239
  function countExcludedMessagesAfter(
256
240
  conversationId: string,
257
- afterMessageId: string | null,
241
+ after: MessagesAfterRef,
258
242
  ): number {
259
- // The `""` sentinel (failure-only state rows) counts everything, matching
260
- // `countMessagesAfter`.
261
- const cursorId =
262
- afterMessageId === null || afterMessageId === "" ? null : afterMessageId;
263
243
  const db = getDb();
264
244
  const candidates = db
265
245
  .select({
@@ -282,18 +262,11 @@ function countExcludedMessagesAfter(
282
262
  return 0;
283
263
  }
284
264
 
285
- let ref: { createdAt: number } | undefined;
286
- if (cursorId !== null) {
287
- ref = db
288
- .select({ createdAt: messages.createdAt })
289
- .from(messages)
290
- .where(eq(messages.id, cursorId))
291
- .get();
292
- // Vanished reference: `countMessagesAfter` reported 0, so there is
293
- // nothing to subtract from.
294
- if (!ref) {
295
- return 0;
296
- }
265
+ const start = resolveMessagesAfterBound(after);
266
+ // Vanished reference: `countMessagesAfter` reported 0, so there is nothing
267
+ // to subtract from.
268
+ if (start.kind === "vanished") {
269
+ return 0;
297
270
  }
298
271
 
299
272
  let count = 0;
@@ -301,11 +274,12 @@ function countExcludedMessagesAfter(
301
274
  if (!isExcludedFromRetrospectiveAccounting(row)) {
302
275
  continue;
303
276
  }
304
- if (cursorId !== null && ref) {
305
- const after =
306
- row.createdAt > ref.createdAt ||
307
- (row.createdAt === ref.createdAt && row.id > cursorId);
308
- if (!after) {
277
+ if (start.kind === "after") {
278
+ const { createdAt, id } = start.bound;
279
+ const isAfter =
280
+ row.createdAt > createdAt ||
281
+ (row.createdAt === createdAt && row.id > id);
282
+ if (!isAfter) {
309
283
  continue;
310
284
  }
311
285
  }