@vellumai/assistant 0.12.1 → 0.12.2-staging.2

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 (577) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +20 -21
  3. package/docs/architecture/turn-actor.md +15 -8
  4. package/docs/guardian-request-flow.md +16 -0
  5. package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
  6. package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
  7. package/openapi.yaml +360 -51
  8. package/package.json +3 -1
  9. package/scripts/smoke-desktop-window-theme.ts +186 -0
  10. package/src/__tests__/always-loaded-tools-guard.test.ts +18 -10
  11. package/src/__tests__/anthropic-provider.test.ts +0 -63
  12. package/src/__tests__/app-compiler.test.ts +6 -0
  13. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  14. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  15. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  16. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  17. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  18. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  19. package/src/__tests__/computer-use-tools.test.ts +19 -7
  20. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  21. package/src/__tests__/config-schema.test.ts +28 -29
  22. package/src/__tests__/config-watcher.test.ts +23 -8
  23. package/src/__tests__/connection-model-compat.test.ts +2 -2
  24. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  25. package/src/__tests__/conversation-pairing.test.ts +26 -0
  26. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  27. package/src/__tests__/conversation-queue.test.ts +48 -207
  28. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  29. package/src/__tests__/conversation-runtime-assembly.test.ts +28 -0
  30. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  31. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  32. package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  35. package/src/__tests__/conversation-title-service.test.ts +14 -10
  36. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  37. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  38. package/src/__tests__/credential-record-retire.test.ts +171 -1
  39. package/src/__tests__/credential-routes.test.ts +250 -3
  40. package/src/__tests__/credential-security-invariants.test.ts +5 -3
  41. package/src/__tests__/cu-unified-flow.test.ts +14 -4
  42. package/src/__tests__/document-sync-tags.test.ts +25 -0
  43. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  44. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  45. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  46. package/src/__tests__/git-porcelain.test.ts +47 -0
  47. package/src/__tests__/heartbeat-service.test.ts +56 -40
  48. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  49. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  50. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  51. package/src/__tests__/host-cu-proxy.test.ts +596 -0
  52. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  53. package/src/__tests__/host-file-proxy.test.ts +11 -23
  54. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  55. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  56. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  57. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  58. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  59. package/src/__tests__/mcp-health-check.test.ts +8 -11
  60. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  61. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  62. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  63. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  64. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  65. package/src/__tests__/model-intents.test.ts +3 -0
  66. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  67. package/src/__tests__/oauth-commands-routes.test.ts +168 -0
  68. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  69. package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
  70. package/src/__tests__/oauth-providers-routes.test.ts +1 -0
  71. package/src/__tests__/openai-provider.test.ts +1 -0
  72. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  73. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  74. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  75. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  76. package/src/__tests__/qdrant-manager.test.ts +132 -0
  77. package/src/__tests__/resolve-app.test.ts +135 -0
  78. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  79. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  80. package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
  81. package/src/__tests__/service-account-token.test.ts +247 -0
  82. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  83. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +9 -4
  85. package/src/__tests__/source-fingerprint.test.ts +184 -0
  86. package/src/__tests__/subagent-tool-gate-mode.test.ts +70 -3
  87. package/src/__tests__/subagent-tools.test.ts +3 -2
  88. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  89. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  90. package/src/__tests__/ui-channel-variants.test.ts +1 -56
  91. package/src/__tests__/unicode.test.ts +13 -0
  92. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  93. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  94. package/src/__tests__/workspace-git-service.test.ts +26 -0
  95. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  96. package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
  97. package/src/acp/__tests__/acp-credentials.test.ts +13 -0
  98. package/src/acp/__tests__/agent-process.test.ts +258 -4
  99. package/src/acp/__tests__/auth-required.test.ts +96 -0
  100. package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
  101. package/src/acp/__tests__/client-handler.test.ts +90 -2
  102. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  103. package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
  104. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  105. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  106. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  107. package/src/acp/__tests__/session-manager.test.ts +895 -4
  108. package/src/acp/acp-claude-oauth.ts +328 -14
  109. package/src/acp/acp-credentials.ts +19 -0
  110. package/src/acp/agent-process.test.ts +4 -1
  111. package/src/acp/agent-process.ts +96 -10
  112. package/src/acp/auth-required.ts +50 -1
  113. package/src/acp/auto-install.test.ts +9 -7
  114. package/src/acp/claude-token-refresh.ts +150 -0
  115. package/src/acp/client-handler.ts +16 -3
  116. package/src/acp/model-config.test.ts +166 -0
  117. package/src/acp/model-config.ts +138 -0
  118. package/src/acp/prepare-agent-env.ts +21 -6
  119. package/src/acp/resolve-agent.test.ts +220 -6
  120. package/src/acp/resolve-agent.ts +88 -23
  121. package/src/acp/session-manager.test.ts +196 -3
  122. package/src/acp/session-manager.ts +441 -18
  123. package/src/acp/types.ts +50 -5
  124. package/src/agent/loop.ts +1 -0
  125. package/src/api/events/acp-session-model-update.test.ts +88 -0
  126. package/src/api/events/acp-session-model-update.ts +46 -0
  127. package/src/api/events/app-files-changed.ts +4 -3
  128. package/src/api/events/notification-intent.ts +2 -0
  129. package/src/api/events/user-message-echo.ts +7 -1
  130. package/src/api/index.ts +6 -0
  131. package/src/api/responses/conversation-message.ts +5 -0
  132. package/src/apps/app-store.ts +1 -1
  133. package/src/apps/resolve-app.ts +81 -0
  134. package/src/apps/source-fingerprint.ts +266 -0
  135. package/src/avatar/identity-avatar.ts +7 -2
  136. package/src/bundler/app-compiler.ts +15 -0
  137. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  138. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  139. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  140. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  141. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  142. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  143. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  144. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  145. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  146. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  147. package/src/calls/__tests__/voice-control-protocol.test.ts +120 -25
  148. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  149. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  150. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  151. package/src/calls/__tests__/voice-session-bridge.test.ts +56 -1
  152. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  153. package/src/calls/barge-in-guard.ts +118 -0
  154. package/src/calls/call-constants.ts +24 -4
  155. package/src/calls/call-controller.ts +499 -199
  156. package/src/calls/call-transport.ts +16 -0
  157. package/src/calls/media-stream-output.ts +32 -0
  158. package/src/calls/media-stream-server.ts +82 -30
  159. package/src/calls/media-stream-stt-session.ts +15 -0
  160. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  161. package/src/calls/resolve-call-tts-provider.ts +33 -2
  162. package/src/calls/tts-call-strategy.ts +9 -5
  163. package/src/calls/types.ts +2 -0
  164. package/src/calls/voice-control-protocol.ts +155 -21
  165. package/src/calls/voice-leg-coordinator.ts +189 -0
  166. package/src/calls/voice-progress-cadence.ts +423 -0
  167. package/src/calls/voice-session-bridge.ts +92 -72
  168. package/src/calls/voice-triage-escalate.ts +178 -59
  169. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  170. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  171. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  172. package/src/cli/commands/apps.help.ts +67 -13
  173. package/src/cli/commands/apps.ts +183 -0
  174. package/src/cli/commands/bash.help.ts +8 -13
  175. package/src/cli/commands/bash.ts +7 -25
  176. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  177. package/src/cli/commands/channels/index.help.ts +70 -2
  178. package/src/cli/commands/channels/index.ts +16 -3
  179. package/src/cli/commands/channels/send.ts +128 -0
  180. package/src/cli/commands/credentials.help.ts +6 -2
  181. package/src/cli/commands/credentials.ts +2 -2
  182. package/src/cli/commands/mcp.help.ts +8 -8
  183. package/src/cli/commands/mcp.ts +1 -1
  184. package/src/cli/commands/oauth/status.ts +5 -0
  185. package/src/cli/commands/roadmap.help.ts +5 -1
  186. package/src/cli/commands/roadmap.ts +1 -1
  187. package/src/cli/lib/bundled-marketplace.json +103 -90
  188. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  189. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  190. package/src/config/__tests__/platform-identity.test.ts +255 -0
  191. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  192. package/src/config/acp-agent-write.ts +43 -0
  193. package/src/config/acp-defaults.test.ts +13 -3
  194. package/src/config/acp-defaults.ts +23 -6
  195. package/src/config/acp-schema.test.ts +81 -0
  196. package/src/config/acp-schema.ts +30 -2
  197. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  198. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  199. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  200. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  201. package/src/config/bundled-skills/computer-use/SKILL.md +29 -2
  202. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  203. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  204. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  205. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  206. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  207. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  208. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  209. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  210. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  211. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  212. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  213. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  214. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  215. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  216. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  217. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  218. package/src/config/default-profile-catalog.ts +1 -1
  219. package/src/config/env.ts +2 -2
  220. package/src/config/feature-flag-registry.json +33 -13
  221. package/src/config/loader.ts +2 -0
  222. package/src/config/memory-v3-gate.test.ts +31 -0
  223. package/src/config/memory-v3-gate.ts +20 -16
  224. package/src/config/platform-identity.ts +201 -0
  225. package/src/config/platform-rehydration.ts +11 -27
  226. package/src/config/schema.ts +2 -2
  227. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  228. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  229. package/src/config/schemas/calls.ts +1 -28
  230. package/src/config/schemas/live-voice.ts +1 -173
  231. package/src/config/schemas/mcp.ts +1 -1
  232. package/src/config/schemas/memory-retrospective.ts +10 -0
  233. package/src/config/schemas/services.ts +18 -0
  234. package/src/config/schemas/skills.ts +0 -14
  235. package/src/config/schemas/voice.ts +150 -0
  236. package/src/credential-execution/process-manager.test.ts +18 -19
  237. package/src/credential-health/credential-health-service.ts +11 -17
  238. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  239. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  240. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  241. package/src/daemon/app-change-notify.ts +45 -0
  242. package/src/daemon/config-watcher.ts +13 -194
  243. package/src/daemon/conversation-error.ts +3 -2
  244. package/src/daemon/conversation-media-retry.ts +2 -1
  245. package/src/daemon/conversation-process.ts +35 -64
  246. package/src/daemon/conversation-runtime-assembly.ts +20 -7
  247. package/src/daemon/conversation-skill-tools.ts +13 -31
  248. package/src/daemon/conversation-store.ts +1 -1
  249. package/src/daemon/conversation-surfaces.ts +21 -10
  250. package/src/daemon/conversation-tool-setup.ts +45 -59
  251. package/src/daemon/conversation.ts +0 -3
  252. package/src/daemon/dictation-profile-store.ts +2 -1
  253. package/src/daemon/handlers/shared.ts +2 -1
  254. package/src/daemon/handlers/skills.ts +6 -3
  255. package/src/daemon/host-bash-proxy.ts +8 -12
  256. package/src/daemon/host-browser-proxy.ts +15 -1
  257. package/src/daemon/host-cu-proxy.ts +224 -19
  258. package/src/daemon/host-file-proxy.ts +8 -10
  259. package/src/daemon/host-proxy-preactivation.ts +2 -1
  260. package/src/daemon/host-transfer-proxy.ts +14 -18
  261. package/src/daemon/lifecycle.ts +0 -5
  262. package/src/daemon/mcp-reload-service.ts +5 -5
  263. package/src/daemon/message-types/sync.ts +1 -0
  264. package/src/daemon/orphan-reaper.ts +1 -1
  265. package/src/daemon/providers-setup.ts +1 -1
  266. package/src/daemon/reaction-history-render.ts +2 -1
  267. package/src/daemon/shutdown-handlers.ts +1 -9
  268. package/src/daemon/skill-memory-refresh.ts +64 -0
  269. package/src/daemon/tool-side-effects.ts +7 -18
  270. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  271. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  272. package/src/desktop/desktop-chrome-policy.ts +47 -0
  273. package/src/desktop/desktop-chrome-session.ts +37 -0
  274. package/src/desktop/desktop-dependencies.test.ts +102 -0
  275. package/src/desktop/desktop-dependencies.ts +308 -0
  276. package/src/desktop/desktop-feature.test.ts +18 -0
  277. package/src/desktop/desktop-feature.ts +19 -0
  278. package/src/desktop/desktop-panel-config.test.ts +135 -0
  279. package/src/desktop/desktop-panel-config.ts +149 -0
  280. package/src/desktop/desktop-session-manager.test.ts +730 -0
  281. package/src/desktop/desktop-session-manager.ts +850 -0
  282. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  283. package/src/desktop/desktop-stream-bridge.ts +214 -0
  284. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  285. package/src/desktop/desktop-wallpaper.ts +113 -0
  286. package/src/desktop/desktop-window-drag.ts +177 -0
  287. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  288. package/src/desktop/desktop-window-manager-config.ts +238 -0
  289. package/src/desktop/desktop-window-theme.ts +170 -0
  290. package/src/heartbeat/heartbeat-service.ts +21 -3
  291. package/src/home/__tests__/feed-writer.test.ts +22 -0
  292. package/src/home/feed-writer.ts +11 -2
  293. package/src/hooks/types.ts +33 -0
  294. package/src/i18n/AGENTS.md +22 -0
  295. package/src/i18n/__tests__/i18n.test.ts +108 -0
  296. package/src/i18n/index.ts +16 -0
  297. package/src/i18n/locales.ts +93 -0
  298. package/src/i18n/messages.ts +61 -0
  299. package/src/i18n/title.ts +25 -0
  300. package/src/inbound/platform-callback-registration.ts +4 -5
  301. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +121 -0
  302. package/src/live-voice/__tests__/live-voice-events.test.ts +8 -7
  303. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  304. package/src/live-voice/__tests__/live-voice-progress.test.ts +94 -15
  305. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  306. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  307. package/src/live-voice/__tests__/protocol.test.ts +39 -0
  308. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  309. package/src/live-voice/__tests__/session-controls.test.ts +79 -0
  310. package/src/live-voice/live-voice-photo.ts +25 -6
  311. package/src/live-voice/live-voice-session.ts +297 -699
  312. package/src/live-voice/protocol.ts +60 -2
  313. package/src/live-voice/session-controls.ts +111 -0
  314. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  315. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  316. package/src/mcp/effective-config.ts +3 -3
  317. package/src/mcp/mcp-header-store.ts +29 -45
  318. package/src/mcp/spec-schema.ts +111 -0
  319. package/src/mcp/startup.ts +8 -4
  320. package/src/mcp/workspace-mcp-config.ts +188 -0
  321. package/src/messaging/provider.ts +10 -3
  322. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  323. package/src/messaging/providers/callback-routing.ts +18 -0
  324. package/src/messaging/providers/channel-transport.ts +56 -0
  325. package/src/messaging/providers/discord/transport.ts +17 -0
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  327. package/src/messaging/providers/slack/adapter.ts +7 -44
  328. package/src/messaging/providers/slack/transport.ts +16 -0
  329. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  330. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  331. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  332. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  333. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  334. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  335. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  336. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  337. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  338. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  339. package/src/notifications/adapters/macos.ts +3 -0
  340. package/src/notifications/assistant-reply-producer.ts +8 -3
  341. package/src/notifications/conversation-pairing.ts +36 -16
  342. package/src/notifications/delivered-post-record.ts +11 -1
  343. package/src/notifications/guardian-feed-projection.ts +125 -5
  344. package/src/notifications/home-feed-side-effect.ts +55 -7
  345. package/src/notifications/schedule-result-producer.ts +66 -22
  346. package/src/oauth/AGENTS.md +2 -0
  347. package/src/oauth/__tests__/seed-providers-managed.test.ts +149 -0
  348. package/src/oauth/byo-connection.test.ts +105 -0
  349. package/src/oauth/byo-connection.ts +36 -6
  350. package/src/oauth/connection-resolver.test.ts +100 -0
  351. package/src/oauth/connection-resolver.ts +120 -3
  352. package/src/oauth/provider-serializer.ts +9 -0
  353. package/src/oauth/scope-utils.ts +24 -0
  354. package/src/oauth/seed-providers.ts +314 -27
  355. package/src/outbound-proxy/certs.ts +3 -5
  356. package/src/permissions/checker.test.ts +11 -6
  357. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  358. package/src/persistence/conversation-crud.ts +71 -63
  359. package/src/persistence/conversation-lineage.ts +12 -4
  360. package/src/persistence/conversation-queries.ts +2 -2
  361. package/src/persistence/conversation-title-placeholders.ts +23 -0
  362. package/src/persistence/conversation-title-service.ts +9 -26
  363. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  364. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  365. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  366. package/src/persistence/message-cursor.ts +78 -0
  367. package/src/persistence/message-reads.ts +30 -0
  368. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  369. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  370. package/src/persistence/schema/memory-core.ts +5 -0
  371. package/src/platform/client.test.ts +4 -2
  372. package/src/platform/client.ts +2 -10
  373. package/src/plugin-api/constants.ts +2 -0
  374. package/src/plugin-api/index.ts +1 -0
  375. package/src/plugin-api/types.ts +2 -0
  376. package/src/plugins/defaults/index.ts +2 -0
  377. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  378. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  379. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  380. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  381. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  382. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  383. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  384. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  385. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  386. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  387. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  388. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  389. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  390. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  391. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  392. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  393. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  394. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  395. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  396. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  397. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  398. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  399. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  400. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  401. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  402. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  403. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  404. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  405. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  406. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  407. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  408. package/src/plugins/defaults/memory/startup.ts +28 -2
  409. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  410. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  411. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  412. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  413. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  414. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  415. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  416. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  417. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  418. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  419. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  420. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  421. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  422. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  423. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  424. package/src/plugins/defaults/memory/worker.ts +6 -0
  425. package/src/plugins/mcp-servers.ts +15 -75
  426. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  427. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  428. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  429. package/src/providers/anthropic/client.ts +0 -47
  430. package/src/providers/gemini/client.ts +9 -23
  431. package/src/providers/gemini-thought-signature.test.ts +63 -0
  432. package/src/providers/gemini-thought-signature.ts +44 -0
  433. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  434. package/src/providers/inference/auth.ts +2 -3
  435. package/src/providers/inference/connection-availability.ts +27 -8
  436. package/src/providers/inference/endpoint-probe.ts +10 -1
  437. package/src/providers/inference/resolve-auth.ts +22 -4
  438. package/src/providers/inference/service-account-token.ts +248 -0
  439. package/src/providers/model-catalog.ts +34 -0
  440. package/src/providers/model-intents.ts +10 -5
  441. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  442. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  443. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  444. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  445. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  446. package/src/providers/openai/chat-completions-provider.ts +212 -79
  447. package/src/providers/openai/codex-models.ts +0 -4
  448. package/src/providers/openai/google-thought-signature.ts +320 -0
  449. package/src/providers/opencode/client.test.ts +73 -4
  450. package/src/providers/opencode/client.ts +60 -3
  451. package/src/providers/openrouter/client.ts +20 -0
  452. package/src/providers/outbound-request-sanitize.ts +96 -0
  453. package/src/providers/retry.ts +30 -19
  454. package/src/providers/unparseable-tool-args.ts +5 -1
  455. package/src/runtime/AGENTS.md +3 -1
  456. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  457. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  458. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  459. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  460. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  461. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  462. package/src/runtime/agent-wake.ts +2 -1
  463. package/src/runtime/assistant-event-hub.ts +21 -3
  464. package/src/runtime/auth/same-actor.ts +38 -3
  465. package/src/runtime/channel-send.ts +316 -0
  466. package/src/runtime/host-observe.ts +11 -1
  467. package/src/runtime/http-server.ts +160 -151
  468. package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
  469. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  470. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +275 -0
  471. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  472. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  473. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  474. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  475. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  476. package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
  477. package/src/runtime/routes/acp-routes.test.ts +15 -0
  478. package/src/runtime/routes/acp-routes.ts +72 -9
  479. package/src/runtime/routes/app-management-routes.ts +90 -2
  480. package/src/runtime/routes/app-routes.ts +1 -1
  481. package/src/runtime/routes/channel-send-routes.ts +85 -0
  482. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  483. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  484. package/src/runtime/routes/conversation-routes.ts +16 -3
  485. package/src/runtime/routes/credential-routes.ts +20 -14
  486. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  487. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  488. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  489. package/src/runtime/routes/events-routes.ts +4 -0
  490. package/src/runtime/routes/host-cu-routes.ts +6 -0
  491. package/src/runtime/routes/index.ts +4 -2
  492. package/src/runtime/routes/log-export-routes.ts +2 -0
  493. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  494. package/src/runtime/routes/migration-routes.ts +6 -20
  495. package/src/runtime/routes/oauth-commands-routes.ts +50 -0
  496. package/src/runtime/routes/oauth-providers.ts +7 -0
  497. package/src/runtime/routes/platform-routes.ts +18 -15
  498. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  499. package/src/runtime/routes/workspace-routes.ts +46 -63
  500. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  501. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  502. package/src/runtime/routes/workspace-utils.ts +17 -0
  503. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  504. package/src/runtime/services/conversation-serializer.ts +16 -3
  505. package/src/runtime/sync/resource-sync-events.ts +2 -1
  506. package/src/schedule/run-script.ts +2 -1
  507. package/src/schedule/schedule-store.ts +9 -2
  508. package/src/schedule/worker.ts +1 -1
  509. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  510. package/src/security/ces-http-record-backend.ts +120 -0
  511. package/src/security/ces-rpc-record-backend.ts +7 -0
  512. package/src/security/untrusted-content.ts +3 -1
  513. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  514. package/src/skills/workspace-skill-md-signature.ts +50 -0
  515. package/src/subagent/consult-context.ts +12 -25
  516. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  517. package/src/tools/acp/list-agents.test.ts +3 -1
  518. package/src/tools/acp/spawn.test.ts +146 -11
  519. package/src/tools/acp/spawn.ts +21 -3
  520. package/src/tools/acp/status.test.ts +94 -0
  521. package/src/tools/acp/status.ts +40 -3
  522. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  523. package/src/tools/browser/browser-execution.ts +1 -1
  524. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  525. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  526. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  527. package/src/tools/browser/network-recorder.ts +2 -1
  528. package/src/tools/capability-offer.test.ts +22 -2
  529. package/src/tools/capability-offer.ts +16 -1
  530. package/src/tools/computer-use/definitions.ts +121 -2
  531. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  532. package/src/tools/credentials/metadata-store.ts +100 -0
  533. package/src/tools/filesystem/search.ts +2 -1
  534. package/src/tools/host-filesystem/edit.test.ts +2 -1
  535. package/src/tools/host-filesystem/edit.ts +2 -2
  536. package/src/tools/host-filesystem/read.test.ts +2 -1
  537. package/src/tools/host-filesystem/read.ts +2 -2
  538. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  539. package/src/tools/host-filesystem/transfer.ts +2 -2
  540. package/src/tools/host-filesystem/write.test.ts +2 -1
  541. package/src/tools/host-filesystem/write.ts +2 -2
  542. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  543. package/src/tools/host-terminal/host-shell.ts +4 -20
  544. package/src/tools/network/web-search.ts +6 -1
  545. package/src/tools/policy-context.ts +6 -7
  546. package/src/tools/skills/scaffold-managed.ts +61 -3
  547. package/src/tools/subagent/spawn.ts +5 -8
  548. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  549. package/src/tools/terminal/sanitized-bash.ts +105 -0
  550. package/src/tools/ui-surface/channel-variants.ts +10 -59
  551. package/src/tools/watch/watch-retro-report.ts +0 -8
  552. package/src/types/host-capabilities.ts +5 -3
  553. package/src/util/__tests__/language-subtag.test.ts +23 -1
  554. package/src/util/ca-bundle.ts +13 -0
  555. package/src/util/language-subtag.ts +16 -0
  556. package/src/util/platform.ts +8 -0
  557. package/src/util/unicode.ts +15 -1
  558. package/src/watch/watch-retro.ts +6 -8
  559. package/src/watch/watch-timeline.ts +2 -2
  560. package/src/workflows/run-manager.ts +2 -1
  561. package/src/workspace/git-porcelain.ts +66 -0
  562. package/src/workspace/git-service.ts +6 -55
  563. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  564. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  565. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  566. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  567. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  568. package/src/workspace/migrations/registry.ts +6 -0
  569. package/src/__tests__/app-source-watcher.test.ts +0 -218
  570. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  571. package/src/__tests__/speaker-identification.test.ts +0 -56
  572. package/src/calls/speaker-identification.ts +0 -233
  573. package/src/daemon/app-source-watcher.ts +0 -218
  574. package/src/messaging/providers/whatsapp/client.ts +0 -29
  575. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  576. package/src/util/fs-watcher-error.ts +0 -36
  577. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -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", () => {
@@ -7,6 +7,7 @@ import type {
7
7
  import { getConfiguredProvider } from "@vellumai/plugin-api";
8
8
 
9
9
  import { redactSecrets } from "../../../../security/secret-scanner.js";
10
+ import { safeStringSlice } from "../host-utils.js";
10
11
  import {
11
12
  buildRecallAgentPromptBundle,
12
13
  FINISH_RECALL_TOOL_DEFINITION,
@@ -862,9 +863,9 @@ function compactText(text: string, maxChars: number): string {
862
863
  return compacted;
863
864
  }
864
865
  if (maxChars <= 3) {
865
- return compacted.slice(0, maxChars);
866
+ return safeStringSlice(compacted, 0, maxChars);
866
867
  }
867
- return `${compacted.slice(0, maxChars - 3).trimEnd()}...`;
868
+ return `${safeStringSlice(compacted, 0, maxChars - 3).trimEnd()}...`;
868
869
  }
869
870
 
870
871
  function dedupeEvidenceById(
@@ -1,3 +1,4 @@
1
+ import { safeStringSlice } from "../host-utils.js";
1
2
  import type {
2
3
  DeterministicRecallSearchResult,
3
4
  DeterministicRecallSourceNote,
@@ -139,9 +140,9 @@ function compactText(text: string, maxChars: number): string {
139
140
  return compacted;
140
141
  }
141
142
  if (maxChars <= 3) {
142
- return compacted.slice(0, maxChars);
143
+ return safeStringSlice(compacted, 0, maxChars);
143
144
  }
144
- return `${compacted.slice(0, maxChars - 3).trimEnd()}...`;
145
+ return `${safeStringSlice(compacted, 0, maxChars - 3).trimEnd()}...`;
145
146
  }
146
147
 
147
148
  function dedupeStrings(values: readonly string[]): string[] {
@@ -27,6 +27,7 @@ import { readdir, readFile, realpath, stat } from "node:fs/promises";
27
27
  import { extname, join } from "node:path";
28
28
 
29
29
  import { embedWithRetry } from "../../../../../persistence/embeddings/embed.js";
30
+ import { safeStringSlice } from "../../host-utils.js";
30
31
  import { getLogger } from "../../logging.js";
31
32
  import { isPathInsideRoot } from "../../path-containment.js";
32
33
  import { getEdgeIndex } from "../../substrate/edge-index.js";
@@ -518,9 +519,11 @@ function buildLexicalExcerpt(
518
519
  if (focusedLine.length <= MEMORY_V2_LEXICAL_EXCERPT_MAX_CHARS) {
519
520
  return focusedLine;
520
521
  }
521
- return `${focusedLine
522
- .slice(0, MEMORY_V2_LEXICAL_EXCERPT_MAX_CHARS - 3)
523
- .trimEnd()}...`;
522
+ return `${safeStringSlice(
523
+ focusedLine,
524
+ 0,
525
+ MEMORY_V2_LEXICAL_EXCERPT_MAX_CHARS - 3,
526
+ ).trimEnd()}...`;
524
527
  }
525
528
 
526
529
  function toLexicalEvidence(match: MemoryV2LexicalMatch): RecallEvidence {
@@ -1,6 +1,7 @@
1
1
  import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { extname, isAbsolute, join, relative, sep } from "node:path";
3
3
 
4
+ import { safeStringSlice } from "../../host-utils.js";
4
5
  import { isPathInsideRoot } from "../../path-containment.js";
5
6
  import type {
6
7
  RecallEvidence,
@@ -924,7 +925,7 @@ function buildExcerpt(lines: readonly string[], lineIndex: number): string {
924
925
  return focusedLine;
925
926
  }
926
927
 
927
- return `${focusedLine.slice(0, EXCERPT_MAX_CHARS - 3).trimEnd()}...`;
928
+ return `${safeStringSlice(focusedLine, 0, EXCERPT_MAX_CHARS - 3).trimEnd()}...`;
928
929
  }
929
930
 
930
931
  function buildSectionExcerpt(
@@ -942,7 +943,7 @@ function buildSectionExcerpt(
942
943
  return excerpt;
943
944
  }
944
945
 
945
- return `${excerpt.slice(0, SECTION_EXCERPT_MAX_CHARS - 3).trimEnd()}...`;
946
+ return `${safeStringSlice(excerpt, 0, SECTION_EXCERPT_MAX_CHARS - 3).trimEnd()}...`;
946
947
  }
947
948
 
948
949
  function buildStructuredJsonExcerpt(contents: string): string {
@@ -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,
@@ -25,6 +26,7 @@ import {
25
26
  getCachedCatalogSync,
26
27
  getCatalog,
27
28
  } from "../../../../skills/catalog-cache.js";
29
+ import { safeStringSlice } from "../host-utils.js";
28
30
  import { getLogger } from "../logging.js";
29
31
  import { memoryDbOrNull } from "../memory-db.js";
30
32
  import type { SkillCapabilityInput } from "../substrate/skill-content.js";
@@ -131,12 +133,9 @@ export function seedSkillGraphNodes(): void {
131
133
  const input = fromSkillSummary(summary);
132
134
 
133
135
  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
- }
136
+ const names = Object.keys(loadWorkspaceMcpConfig().servers);
137
+ if (names.length > 0) {
138
+ input.description += ` Configured: ${names.join(", ")}`;
140
139
  }
141
140
  }
142
141
 
@@ -247,7 +246,7 @@ function buildSkillContent(input: SkillCapabilityInput): string {
247
246
  content += ` Avoid when: ${input.avoidWhen.join("; ")}.`;
248
247
  }
249
248
  if (content.length > 500) {
250
- content = content.slice(0, 500);
249
+ content = safeStringSlice(content, 0, 500);
251
250
  }
252
251
  return content;
253
252
  }
@@ -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
 
@@ -25,6 +25,7 @@ import type { SqliteRetryOptions } from "../../../util/sqlite-retry.js";
25
25
  import * as hostSqliteRetry from "../../../util/sqlite-retry.js";
26
26
  import * as hostStripCommentLines from "../../../util/strip-comment-lines.js";
27
27
  import * as hostTruncate from "../../../util/truncate.js";
28
+ import * as hostUnicode from "../../../util/unicode.js";
28
29
  import * as hostWorkerCompute from "../../../util/worker-compute.js";
29
30
  import * as hostWorkerMemory from "../../../util/worker-memory.js";
30
31
 
@@ -76,6 +77,15 @@ export function truncate(str: string, maxLen: number, suffix?: string): string {
76
77
  : hostTruncate.truncate(str, maxLen, suffix);
77
78
  }
78
79
 
80
+ /** `String.prototype.slice` that never cuts a UTF-16 surrogate pair in half. */
81
+ export function safeStringSlice(
82
+ str: string,
83
+ start?: number,
84
+ end?: number,
85
+ ): string {
86
+ return hostUnicode.safeStringSlice(str, start, end);
87
+ }
88
+
79
89
  export function workerMemoryEnv(): Record<string, string | undefined> {
80
90
  return hostWorkerMemory.workerMemoryEnv();
81
91
  }
@@ -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
  }
@@ -0,0 +1,126 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory retrospective: cursor timestamp backfill.
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // State rows written before `last_processed_created_at` existed carry an
6
+ // id-only cursor, which stops bounding reads the moment its row is deleted
7
+ // (see `memory-retrospective-state.ts`). This pass copies `createdAt` from
8
+ // each such row's message while the message still exists. Rows whose message
9
+ // is already gone stay NULL: nothing remains to recover, and they are counted
10
+ // so the stall is visible in the log. Runs detached at daemon init (ahead of
11
+ // any client regenerating a reply) and again at memory-worker startup;
12
+ // idempotent, keyset-paginated by conversation id, and yielding between pages.
13
+
14
+ import { and, asc, eq, gt, isNull, ne } from "drizzle-orm";
15
+
16
+ import { messageCreatedAtByIds } from "../../../persistence/message-reads.js";
17
+ import { memoryRetrospectiveState } from "../../../persistence/schema/index.js";
18
+ import { withSqliteRetry } from "./host-utils.js";
19
+ import { getLogger } from "./logging.js";
20
+ import { memoryDbOrNull } from "./memory-db.js";
21
+ import { ensureRetrospectiveCursorColumn } from "./memory-retrospective-state.js";
22
+
23
+ const log = getLogger("memory-retrospective-cursor-backfill");
24
+
25
+ const DEFAULT_PAGE_SIZE = 500;
26
+
27
+ export interface CursorBackfillResult {
28
+ /** Rows with a cursor id but no timestamp. */
29
+ scanned: number;
30
+ /** Rows whose message still exists and received its `createdAt`. */
31
+ backfilled: number;
32
+ /** Rows whose cursor message no longer exists; left NULL. */
33
+ unrecoverable: number;
34
+ }
35
+
36
+ /**
37
+ * Fill `last_processed_created_at` on every state row that has a cursor id
38
+ * but no timestamp. Best-effort: a missing memory connection or column is a
39
+ * no-op, and a row the job rewrites mid-pass is skipped by the update's
40
+ * `IS NULL` guard rather than stomped. Pages by conversation id so a large
41
+ * backlog never materializes at once or holds the event loop.
42
+ */
43
+ export async function backfillRetrospectiveCursorTimestamps(opts?: {
44
+ pageSize?: number;
45
+ }): Promise<CursorBackfillResult> {
46
+ const pageSize = opts?.pageSize ?? DEFAULT_PAGE_SIZE;
47
+ const result: CursorBackfillResult = {
48
+ scanned: 0,
49
+ backfilled: 0,
50
+ unrecoverable: 0,
51
+ };
52
+ const context = "backfillRetrospectiveCursorTimestamps";
53
+ const mdb = memoryDbOrNull(context);
54
+ if (!mdb || !ensureRetrospectiveCursorColumn(context)) {
55
+ return result;
56
+ }
57
+
58
+ let afterConversationId = "";
59
+ for (;;) {
60
+ const page = mdb
61
+ .select({
62
+ conversationId: memoryRetrospectiveState.conversationId,
63
+ messageId: memoryRetrospectiveState.lastProcessedMessageId,
64
+ })
65
+ .from(memoryRetrospectiveState)
66
+ .where(
67
+ and(
68
+ isNull(memoryRetrospectiveState.lastProcessedCreatedAt),
69
+ ne(memoryRetrospectiveState.lastProcessedMessageId, ""),
70
+ gt(memoryRetrospectiveState.conversationId, afterConversationId),
71
+ ),
72
+ )
73
+ .orderBy(asc(memoryRetrospectiveState.conversationId))
74
+ .limit(pageSize)
75
+ .all();
76
+ if (page.length === 0) {
77
+ break;
78
+ }
79
+ afterConversationId = page[page.length - 1]!.conversationId;
80
+ result.scanned += page.length;
81
+
82
+ const createdAtById = messageCreatedAtByIds(
83
+ page.map((row) => row.messageId),
84
+ );
85
+ const recoverable = page.flatMap((row) => {
86
+ const createdAt = createdAtById.get(row.messageId);
87
+ return createdAt === undefined ? [] : [{ ...row, createdAt }];
88
+ });
89
+ result.unrecoverable += page.length - recoverable.length;
90
+
91
+ if (recoverable.length > 0) {
92
+ await withSqliteRetry(
93
+ () => {
94
+ mdb.transaction((tx) => {
95
+ for (const row of recoverable) {
96
+ tx.update(memoryRetrospectiveState)
97
+ .set({ lastProcessedCreatedAt: row.createdAt })
98
+ .where(
99
+ and(
100
+ eq(
101
+ memoryRetrospectiveState.conversationId,
102
+ row.conversationId,
103
+ ),
104
+ eq(
105
+ memoryRetrospectiveState.lastProcessedMessageId,
106
+ row.messageId,
107
+ ),
108
+ isNull(memoryRetrospectiveState.lastProcessedCreatedAt),
109
+ ),
110
+ )
111
+ .run();
112
+ }
113
+ });
114
+ },
115
+ { op: context, context: { afterConversationId } },
116
+ );
117
+ result.backfilled += recoverable.length;
118
+ }
119
+ await Bun.sleep(0);
120
+ }
121
+
122
+ if (result.backfilled > 0 || result.unrecoverable > 0) {
123
+ log.info(result, "Retrospective cursor timestamps backfilled");
124
+ }
125
+ return result;
126
+ }