@vellumai/assistant 0.12.1 → 0.12.2-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (548) 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/openapi.yaml +327 -52
  6. package/package.json +3 -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__/anthropic-provider.test.ts +0 -63
  10. package/src/__tests__/app-compiler.test.ts +6 -0
  11. package/src/__tests__/app-control-tool-schemas.test.ts +5 -2
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -3
  13. package/src/__tests__/assistant-event-hub-machine-name.test.ts +6 -7
  14. package/src/__tests__/assistant-event-hub.test.ts +44 -0
  15. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  16. package/src/__tests__/computer-use-skill-proxy-bridge.test.ts +5 -2
  17. package/src/__tests__/computer-use-tools.test.ts +19 -7
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +28 -29
  20. package/src/__tests__/config-watcher.test.ts +23 -8
  21. package/src/__tests__/connection-model-compat.test.ts +2 -2
  22. package/src/__tests__/conversation-key-store-disk-view.test.ts +1 -1
  23. package/src/__tests__/conversation-pairing.test.ts +26 -0
  24. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -10
  25. package/src/__tests__/conversation-queue.test.ts +48 -207
  26. package/src/__tests__/conversation-retry-discard.test.ts +54 -1
  27. package/src/__tests__/conversation-skill-tools.test.ts +27 -0
  28. package/src/__tests__/conversation-surfaces-app-control.test.ts +8 -3
  29. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  30. package/src/__tests__/conversation-title-service.test.ts +14 -10
  31. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +3 -5
  32. package/src/__tests__/conversation-tool-setup-attribution.test.ts +51 -0
  33. package/src/__tests__/credential-record-retire.test.ts +171 -1
  34. package/src/__tests__/credential-routes.test.ts +212 -3
  35. package/src/__tests__/credential-security-invariants.test.ts +4 -2
  36. package/src/__tests__/cu-unified-flow.test.ts +8 -2
  37. package/src/__tests__/document-sync-tags.test.ts +25 -0
  38. package/src/__tests__/drain-kick-guard.test.ts +0 -2
  39. package/src/__tests__/drain-requeue-on-contention.test.ts +0 -6
  40. package/src/__tests__/evict-conversations-for-reload.test.ts +1 -1
  41. package/src/__tests__/git-porcelain.test.ts +47 -0
  42. package/src/__tests__/heartbeat-service.test.ts +56 -40
  43. package/src/__tests__/helpers/set-workspace-mcp.ts +11 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +26 -45
  45. package/src/__tests__/host-browser-proxy.test.ts +6 -17
  46. package/src/__tests__/host-cu-proxy.test.ts +539 -0
  47. package/src/__tests__/host-file-proxy-targeted.test.ts +23 -0
  48. package/src/__tests__/host-file-proxy.test.ts +11 -23
  49. package/src/__tests__/host-transfer-proxy-targeted.test.ts +30 -9
  50. package/src/__tests__/list-messages-camera-frame.test.ts +121 -0
  51. package/src/__tests__/llm-catalog-parity.test.ts +25 -0
  52. package/src/__tests__/mcp-auth-routes.test.ts +6 -9
  53. package/src/__tests__/mcp-config-secret-boundary.test.ts +78 -136
  54. package/src/__tests__/mcp-health-check.test.ts +8 -11
  55. package/src/__tests__/mcp-list-plugin-servers.test.ts +8 -10
  56. package/src/__tests__/message-deleted-hook-dispatch.test.ts +119 -0
  57. package/src/__tests__/messages-after-tiebreaker.test.ts +91 -0
  58. package/src/__tests__/messages-read-boundary-guard.test.ts +1 -1
  59. package/src/__tests__/messaging-send-tool.test.ts +355 -548
  60. package/src/__tests__/model-intents.test.ts +3 -0
  61. package/src/__tests__/notification-vellum-adapter.test.ts +85 -2
  62. package/src/__tests__/oauth-commands-routes.test.ts +113 -0
  63. package/src/__tests__/oauth-provider-profiles.test.ts +18 -0
  64. package/src/__tests__/openai-provider.test.ts +1 -0
  65. package/src/__tests__/outlook-messaging-provider.test.ts +6 -6
  66. package/src/__tests__/platform-callback-registration.test.ts +8 -16
  67. package/src/__tests__/platform-client-verify-credential.test.ts +2 -0
  68. package/src/__tests__/plugin-import-boundary-guard.test.ts +14 -0
  69. package/src/__tests__/qdrant-manager.test.ts +132 -0
  70. package/src/__tests__/resolve-app.test.ts +135 -0
  71. package/src/__tests__/scaffold-managed-skill-tool.test.ts +229 -6
  72. package/src/__tests__/script-proxy-certs.test.ts +35 -0
  73. package/src/__tests__/service-account-token.test.ts +247 -0
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +1 -1
  75. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  76. package/src/__tests__/skills.test.ts +9 -4
  77. package/src/__tests__/source-fingerprint.test.ts +184 -0
  78. package/src/__tests__/subagent-tool-gate-mode.test.ts +19 -3
  79. package/src/__tests__/subagent-tools.test.ts +3 -2
  80. package/src/__tests__/telephony-tts-guard.test.ts +38 -3
  81. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  82. package/src/__tests__/unicode.test.ts +13 -0
  83. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +4 -0
  84. package/src/__tests__/watch-retro-tool-availability.test.ts +8 -17
  85. package/src/__tests__/workspace-git-service.test.ts +26 -0
  86. package/src/__tests__/workspace-migration-154-repair-retired-codex-gpt-5-4-model-ids.test.ts +336 -0
  87. package/src/acp/__tests__/agent-process.test.ts +258 -4
  88. package/src/acp/__tests__/auth-required.test.ts +96 -0
  89. package/src/acp/__tests__/client-handler.test.ts +90 -2
  90. package/src/acp/__tests__/helpers/acp-model-option.ts +51 -0
  91. package/src/acp/__tests__/session-manager-persistence.test.ts +5 -1
  92. package/src/acp/__tests__/session-manager-resume.test.ts +270 -6
  93. package/src/acp/__tests__/session-manager-usage.test.ts +4 -2
  94. package/src/acp/__tests__/session-manager.test.ts +895 -4
  95. package/src/acp/agent-process.test.ts +4 -1
  96. package/src/acp/agent-process.ts +96 -10
  97. package/src/acp/auth-required.ts +50 -1
  98. package/src/acp/auto-install.test.ts +9 -7
  99. package/src/acp/client-handler.ts +16 -3
  100. package/src/acp/model-config.test.ts +166 -0
  101. package/src/acp/model-config.ts +138 -0
  102. package/src/acp/resolve-agent.test.ts +220 -6
  103. package/src/acp/resolve-agent.ts +88 -23
  104. package/src/acp/session-manager.test.ts +196 -3
  105. package/src/acp/session-manager.ts +441 -18
  106. package/src/acp/types.ts +50 -5
  107. package/src/agent/loop.ts +1 -0
  108. package/src/api/events/acp-session-model-update.test.ts +88 -0
  109. package/src/api/events/acp-session-model-update.ts +46 -0
  110. package/src/api/events/app-files-changed.ts +4 -3
  111. package/src/api/events/notification-intent.ts +2 -0
  112. package/src/api/events/user-message-echo.ts +7 -1
  113. package/src/api/index.ts +6 -0
  114. package/src/api/responses/conversation-message.ts +5 -0
  115. package/src/apps/app-store.ts +1 -1
  116. package/src/apps/resolve-app.ts +81 -0
  117. package/src/apps/source-fingerprint.ts +266 -0
  118. package/src/avatar/identity-avatar.ts +7 -2
  119. package/src/bundler/app-compiler.ts +15 -0
  120. package/src/calls/__tests__/barge-in-guard.test.ts +81 -0
  121. package/src/{__tests__ → calls/__tests__}/call-controller.test.ts +503 -82
  122. package/src/{__tests__ → calls/__tests__}/media-stream-audio-transcode.test.ts +1 -1
  123. package/src/{__tests__ → calls/__tests__}/media-stream-output.test.ts +37 -8
  124. package/src/{__tests__ → calls/__tests__}/media-stream-parser.test.ts +1 -1
  125. package/src/{__tests__ → calls/__tests__}/media-stream-server-integration.test.ts +150 -57
  126. package/src/{__tests__ → calls/__tests__}/media-stream-stt-session.test.ts +5 -5
  127. package/src/{__tests__ → calls/__tests__}/media-turn-detector.test.ts +1 -1
  128. package/src/{live-voice → calls}/__tests__/progress-narration.test.ts +4 -4
  129. package/src/{live-voice → calls}/__tests__/progress-phrases.test.ts +1 -1
  130. package/src/calls/__tests__/voice-control-protocol.test.ts +58 -25
  131. package/src/calls/__tests__/voice-leg-coordinator.test.ts +229 -0
  132. package/src/calls/__tests__/voice-progress-cadence.test.ts +274 -0
  133. package/src/{__tests__/voice-session-bridge.test.ts → calls/__tests__/voice-session-bridge-turn-flow.test.ts} +20 -12
  134. package/src/calls/__tests__/voice-session-bridge.test.ts +37 -1
  135. package/src/calls/__tests__/voice-triage-escalate.test.ts +105 -0
  136. package/src/calls/barge-in-guard.ts +118 -0
  137. package/src/calls/call-constants.ts +24 -4
  138. package/src/calls/call-controller.ts +499 -199
  139. package/src/calls/call-transport.ts +16 -0
  140. package/src/calls/media-stream-output.ts +32 -0
  141. package/src/calls/media-stream-server.ts +82 -30
  142. package/src/calls/media-stream-stt-session.ts +15 -0
  143. package/src/{live-voice → calls}/progress-narration.ts +2 -2
  144. package/src/calls/resolve-call-tts-provider.ts +33 -2
  145. package/src/calls/tts-call-strategy.ts +9 -5
  146. package/src/calls/types.ts +2 -0
  147. package/src/calls/voice-control-protocol.ts +53 -21
  148. package/src/calls/voice-leg-coordinator.ts +189 -0
  149. package/src/calls/voice-progress-cadence.ts +423 -0
  150. package/src/calls/voice-session-bridge.ts +59 -46
  151. package/src/calls/voice-triage-escalate.ts +175 -56
  152. package/src/cli/commands/__tests__/apps.test.ts +245 -0
  153. package/src/cli/commands/__tests__/bash.test.ts +53 -0
  154. package/src/cli/commands/__tests__/roadmap.help.test.ts +19 -0
  155. package/src/cli/commands/apps.help.ts +67 -13
  156. package/src/cli/commands/apps.ts +183 -0
  157. package/src/cli/commands/bash.help.ts +8 -13
  158. package/src/cli/commands/bash.ts +7 -25
  159. package/src/cli/commands/channels/__tests__/send.test.ts +218 -0
  160. package/src/cli/commands/channels/index.help.ts +70 -2
  161. package/src/cli/commands/channels/index.ts +16 -3
  162. package/src/cli/commands/channels/send.ts +128 -0
  163. package/src/cli/commands/credentials.help.ts +6 -2
  164. package/src/cli/commands/credentials.ts +2 -2
  165. package/src/cli/commands/mcp.help.ts +8 -8
  166. package/src/cli/commands/mcp.ts +1 -1
  167. package/src/cli/commands/roadmap.help.ts +5 -1
  168. package/src/cli/commands/roadmap.ts +1 -1
  169. package/src/cli/lib/bundled-marketplace.json +103 -90
  170. package/src/config/__tests__/default-profile-catalog.test.ts +22 -0
  171. package/src/config/__tests__/memory-retrospective-schema.test.ts +15 -1
  172. package/src/config/__tests__/platform-identity.test.ts +255 -0
  173. package/src/config/__tests__/platform-rehydration.test.ts +99 -9
  174. package/src/config/acp-agent-write.ts +43 -0
  175. package/src/config/acp-defaults.test.ts +13 -3
  176. package/src/config/acp-defaults.ts +23 -6
  177. package/src/config/acp-schema.test.ts +81 -0
  178. package/src/config/acp-schema.ts +30 -2
  179. package/src/config/bundled-skills/acp/SKILL.md +22 -14
  180. package/src/config/bundled-skills/acp/TOOLS.json +5 -1
  181. package/src/config/bundled-skills/app-builder/SKILL.md +12 -9
  182. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +1 -1
  183. package/src/config/bundled-skills/computer-use/SKILL.md +20 -2
  184. package/src/config/bundled-skills/computer-use/TOOLS.json +99 -0
  185. package/src/config/bundled-skills/computer-use/tools/computer-use-sequence.ts +12 -0
  186. package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -1
  187. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -1
  188. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -1
  189. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  190. package/src/config/bundled-skills/messaging/TOOLS.json +3 -3
  191. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +67 -139
  192. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +1 -1
  193. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -1
  194. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -1
  195. package/src/config/bundled-skills/schedule/SKILL.md +3 -4
  196. package/src/config/bundled-skills/screen-annotation/SKILL.md +8 -1
  197. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +4 -1
  198. package/src/config/bundled-skills/skill-management/TOOLS.json +6 -2
  199. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -2
  200. package/src/config/default-profile-catalog.ts +1 -1
  201. package/src/config/env.ts +2 -2
  202. package/src/config/feature-flag-registry.json +24 -12
  203. package/src/config/loader.ts +1 -0
  204. package/src/config/memory-v3-gate.test.ts +31 -0
  205. package/src/config/memory-v3-gate.ts +20 -16
  206. package/src/config/platform-identity.ts +201 -0
  207. package/src/config/platform-rehydration.ts +11 -27
  208. package/src/config/schema.ts +2 -2
  209. package/src/config/schemas/__tests__/live-voice.test.ts +8 -158
  210. package/src/config/schemas/__tests__/voice.test.ts +148 -0
  211. package/src/config/schemas/calls.ts +1 -28
  212. package/src/config/schemas/live-voice.ts +1 -173
  213. package/src/config/schemas/mcp.ts +1 -1
  214. package/src/config/schemas/memory-retrospective.ts +10 -0
  215. package/src/config/schemas/services.ts +8 -0
  216. package/src/config/schemas/skills.ts +0 -14
  217. package/src/config/schemas/voice.ts +150 -0
  218. package/src/credential-execution/process-manager.test.ts +18 -19
  219. package/src/credential-health/credential-health-service.ts +11 -17
  220. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -121
  221. package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +4 -1
  222. package/src/daemon/__tests__/skill-memory-refresh-mtime.test.ts +133 -0
  223. package/src/daemon/app-change-notify.ts +45 -0
  224. package/src/daemon/config-watcher.ts +13 -194
  225. package/src/daemon/conversation-error.ts +3 -2
  226. package/src/daemon/conversation-media-retry.ts +2 -1
  227. package/src/daemon/conversation-process.ts +35 -64
  228. package/src/daemon/conversation-runtime-assembly.ts +2 -1
  229. package/src/daemon/conversation-skill-tools.ts +13 -31
  230. package/src/daemon/conversation-store.ts +1 -1
  231. package/src/daemon/conversation-surfaces.ts +15 -9
  232. package/src/daemon/conversation-tool-setup.ts +37 -43
  233. package/src/daemon/conversation.ts +0 -3
  234. package/src/daemon/dictation-profile-store.ts +2 -1
  235. package/src/daemon/handlers/shared.ts +2 -1
  236. package/src/daemon/handlers/skills.ts +6 -3
  237. package/src/daemon/host-bash-proxy.ts +8 -12
  238. package/src/daemon/host-browser-proxy.ts +15 -1
  239. package/src/daemon/host-cu-proxy.ts +186 -19
  240. package/src/daemon/host-file-proxy.ts +8 -10
  241. package/src/daemon/host-proxy-preactivation.ts +2 -1
  242. package/src/daemon/host-transfer-proxy.ts +14 -18
  243. package/src/daemon/lifecycle.ts +0 -5
  244. package/src/daemon/mcp-reload-service.ts +5 -5
  245. package/src/daemon/message-types/sync.ts +1 -0
  246. package/src/daemon/orphan-reaper.ts +1 -1
  247. package/src/daemon/providers-setup.ts +1 -1
  248. package/src/daemon/reaction-history-render.ts +2 -1
  249. package/src/daemon/shutdown-handlers.ts +1 -9
  250. package/src/daemon/skill-memory-refresh.ts +64 -0
  251. package/src/daemon/tool-side-effects.ts +7 -18
  252. package/src/desktop/__tests__/fake-desktop.ts +122 -0
  253. package/src/desktop/desktop-chrome-policy.test.ts +79 -0
  254. package/src/desktop/desktop-chrome-policy.ts +47 -0
  255. package/src/desktop/desktop-chrome-session.ts +37 -0
  256. package/src/desktop/desktop-dependencies.test.ts +102 -0
  257. package/src/desktop/desktop-dependencies.ts +308 -0
  258. package/src/desktop/desktop-feature.test.ts +18 -0
  259. package/src/desktop/desktop-feature.ts +19 -0
  260. package/src/desktop/desktop-panel-config.test.ts +135 -0
  261. package/src/desktop/desktop-panel-config.ts +149 -0
  262. package/src/desktop/desktop-session-manager.test.ts +730 -0
  263. package/src/desktop/desktop-session-manager.ts +850 -0
  264. package/src/desktop/desktop-stream-bridge.test.ts +395 -0
  265. package/src/desktop/desktop-stream-bridge.ts +214 -0
  266. package/src/desktop/desktop-wallpaper.test.ts +191 -0
  267. package/src/desktop/desktop-wallpaper.ts +113 -0
  268. package/src/desktop/desktop-window-drag.ts +177 -0
  269. package/src/desktop/desktop-window-manager-config.test.ts +230 -0
  270. package/src/desktop/desktop-window-manager-config.ts +238 -0
  271. package/src/desktop/desktop-window-theme.ts +170 -0
  272. package/src/heartbeat/heartbeat-service.ts +21 -3
  273. package/src/home/__tests__/feed-writer.test.ts +22 -0
  274. package/src/home/feed-writer.ts +11 -2
  275. package/src/hooks/types.ts +33 -0
  276. package/src/i18n/AGENTS.md +22 -0
  277. package/src/i18n/__tests__/i18n.test.ts +108 -0
  278. package/src/i18n/index.ts +16 -0
  279. package/src/i18n/locales.ts +93 -0
  280. package/src/i18n/messages.ts +61 -0
  281. package/src/i18n/title.ts +25 -0
  282. package/src/inbound/platform-callback-registration.ts +4 -5
  283. package/src/live-voice/__tests__/live-voice-metrics.test.ts +0 -3
  284. package/src/live-voice/__tests__/live-voice-progress.test.ts +15 -15
  285. package/src/live-voice/__tests__/live-voice-sight-frame-echo.test.ts +238 -0
  286. package/src/live-voice/__tests__/live-voice-vad.test.ts +2 -2
  287. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +13 -69
  288. package/src/live-voice/live-voice-photo.ts +25 -6
  289. package/src/live-voice/live-voice-session.ts +213 -693
  290. package/src/live-voice/protocol.ts +0 -2
  291. package/src/mcp/__tests__/effective-config.test.ts +2 -2
  292. package/src/mcp/__tests__/workspace-mcp-config.test.ts +135 -0
  293. package/src/mcp/effective-config.ts +3 -3
  294. package/src/mcp/mcp-header-store.ts +29 -45
  295. package/src/mcp/spec-schema.ts +111 -0
  296. package/src/mcp/startup.ts +8 -4
  297. package/src/mcp/workspace-mcp-config.ts +188 -0
  298. package/src/messaging/provider.ts +10 -3
  299. package/src/messaging/providers/__tests__/proactive-address.test.ts +95 -0
  300. package/src/messaging/providers/callback-routing.ts +18 -0
  301. package/src/messaging/providers/channel-transport.ts +56 -0
  302. package/src/messaging/providers/discord/transport.ts +17 -0
  303. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +6 -17
  304. package/src/messaging/providers/slack/adapter.ts +7 -44
  305. package/src/messaging/providers/slack/transport.ts +16 -0
  306. package/src/messaging/providers/telegram-bot/adapter.ts +1 -48
  307. package/src/messaging/providers/telegram-bot/client.ts +0 -26
  308. package/src/messaging/providers/telegram-bot/transport.ts +20 -0
  309. package/src/messaging/providers/whatsapp/adapter.ts +1 -48
  310. package/src/messaging/providers/whatsapp/transport.ts +18 -0
  311. package/src/monitoring/__tests__/workspace-git-heartbeat.test.ts +8 -0
  312. package/src/notifications/__tests__/assistant-reply-producer.test.ts +55 -0
  313. package/src/notifications/__tests__/guardian-feed-projection.test.ts +158 -2
  314. package/src/notifications/__tests__/home-feed-side-effect.test.ts +153 -3
  315. package/src/notifications/__tests__/schedule-result-producer.test.ts +99 -3
  316. package/src/notifications/adapters/macos.ts +3 -0
  317. package/src/notifications/assistant-reply-producer.ts +8 -3
  318. package/src/notifications/conversation-pairing.ts +36 -16
  319. package/src/notifications/delivered-post-record.ts +11 -1
  320. package/src/notifications/guardian-feed-projection.ts +125 -5
  321. package/src/notifications/home-feed-side-effect.ts +55 -7
  322. package/src/notifications/schedule-result-producer.ts +66 -22
  323. package/src/oauth/AGENTS.md +2 -0
  324. package/src/oauth/__tests__/seed-providers-managed.test.ts +54 -0
  325. package/src/oauth/byo-connection.test.ts +105 -0
  326. package/src/oauth/byo-connection.ts +36 -6
  327. package/src/oauth/connection-resolver.test.ts +73 -0
  328. package/src/oauth/connection-resolver.ts +95 -2
  329. package/src/oauth/scope-utils.ts +24 -0
  330. package/src/oauth/seed-providers.ts +204 -25
  331. package/src/outbound-proxy/certs.ts +3 -5
  332. package/src/permissions/checker.test.ts +11 -6
  333. package/src/persistence/compaction-log-store-clickhouse.ts +3 -3
  334. package/src/persistence/conversation-crud.ts +71 -63
  335. package/src/persistence/conversation-lineage.ts +12 -4
  336. package/src/persistence/conversation-queries.ts +2 -2
  337. package/src/persistence/conversation-title-placeholders.ts +23 -0
  338. package/src/persistence/conversation-title-service.ts +9 -26
  339. package/src/persistence/embeddings/qdrant-manager.ts +163 -18
  340. package/src/persistence/llm-request-log-sink-clickhouse.ts +3 -3
  341. package/src/persistence/llm-request-log-source-clickhouse.ts +3 -3
  342. package/src/persistence/message-cursor.ts +78 -0
  343. package/src/persistence/message-reads.ts +30 -0
  344. package/src/persistence/migrations/157-invite-contact-id.ts +18 -6
  345. package/src/persistence/migrations/__tests__/157-invite-contact-id.test.ts +149 -0
  346. package/src/persistence/schema/memory-core.ts +5 -0
  347. package/src/platform/client.test.ts +4 -2
  348. package/src/platform/client.ts +2 -10
  349. package/src/plugin-api/constants.ts +2 -0
  350. package/src/plugin-api/index.ts +1 -0
  351. package/src/plugin-api/types.ts +2 -0
  352. package/src/plugins/defaults/index.ts +2 -0
  353. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +46 -0
  354. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-backfill.test.ts +118 -0
  355. package/src/plugins/defaults/memory/__tests__/memory-retrospective-cursor-preserve.test.ts +83 -0
  356. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +9 -5
  357. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +20 -10
  358. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +5 -0
  359. package/src/plugins/defaults/memory/__tests__/memory-retrospective-state.test.ts +219 -0
  360. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +31 -0
  361. package/src/plugins/defaults/memory/__tests__/memory-retrospective-trigger-check.test.ts +63 -0
  362. package/src/plugins/defaults/memory/__tests__/shutdown-hook.test.ts +4 -0
  363. package/src/plugins/defaults/memory/context-search/agent-runner.ts +3 -2
  364. package/src/plugins/defaults/memory/context-search/format.ts +3 -2
  365. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +6 -3
  366. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +3 -2
  367. package/src/plugins/defaults/memory/graph/capability-seed.ts +6 -7
  368. package/src/plugins/defaults/memory/hooks/init.ts +10 -0
  369. package/src/plugins/defaults/memory/hooks/message-deleted.ts +21 -0
  370. package/src/plugins/defaults/memory/hooks/shutdown.ts +2 -0
  371. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  372. package/src/plugins/defaults/memory/jobs-worker.ts +13 -0
  373. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +34 -60
  374. package/src/plugins/defaults/memory/memory-retrospective-cursor-backfill.ts +126 -0
  375. package/src/plugins/defaults/memory/memory-retrospective-cursor-preserve.ts +79 -0
  376. package/src/plugins/defaults/memory/memory-retrospective-cursor.ts +28 -0
  377. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +3 -5
  378. package/src/plugins/defaults/memory/memory-retrospective-job.ts +12 -3
  379. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
  380. package/src/plugins/defaults/memory/memory-retrospective-state.ts +312 -71
  381. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +2 -1
  382. package/src/plugins/defaults/memory/memory-retrospective-trigger-check.ts +5 -3
  383. package/src/plugins/defaults/memory/skill-md-mtime-poll.ts +9 -0
  384. package/src/plugins/defaults/memory/startup.ts +28 -2
  385. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +7 -9
  386. package/src/plugins/defaults/memory/substrate/page-index.ts +6 -1
  387. package/src/plugins/defaults/memory/substrate/skill-content.ts +2 -2
  388. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +4 -2
  389. package/src/plugins/defaults/memory/v1/graph/extraction.ts +4 -2
  390. package/src/plugins/defaults/memory/v1/graph/retriever.ts +4 -1
  391. package/src/plugins/defaults/memory/v2/reranker.ts +6 -1
  392. package/src/plugins/defaults/memory/v3/__tests__/card.test.ts +24 -0
  393. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +125 -0
  394. package/src/plugins/defaults/memory/v3/__tests__/sections.test.ts +26 -0
  395. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +30 -0
  396. package/src/plugins/defaults/memory/v3/card.ts +3 -2
  397. package/src/plugins/defaults/memory/v3/pool-select.ts +5 -3
  398. package/src/plugins/defaults/memory/v3/sections.ts +9 -3
  399. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +5 -2
  400. package/src/plugins/defaults/memory/worker.ts +6 -0
  401. package/src/plugins/mcp-servers.ts +15 -75
  402. package/src/providers/__tests__/connection-model-compat.test.ts +3 -3
  403. package/src/providers/__tests__/retry-callsite.test.ts +41 -0
  404. package/src/providers/__tests__/retry-outbound-sanitize.test.ts +176 -0
  405. package/src/providers/anthropic/client.ts +0 -47
  406. package/src/providers/gemini/client.ts +9 -23
  407. package/src/providers/gemini-thought-signature.test.ts +63 -0
  408. package/src/providers/gemini-thought-signature.ts +44 -0
  409. package/src/providers/inference/__tests__/endpoint-probe.test.ts +42 -3
  410. package/src/providers/inference/auth.ts +2 -3
  411. package/src/providers/inference/connection-availability.ts +27 -8
  412. package/src/providers/inference/endpoint-probe.ts +10 -1
  413. package/src/providers/inference/resolve-auth.ts +22 -4
  414. package/src/providers/inference/service-account-token.ts +248 -0
  415. package/src/providers/model-catalog.ts +34 -0
  416. package/src/providers/model-intents.ts +10 -5
  417. package/src/providers/openai/__tests__/chat-completions-output-token-limit.test.ts +141 -0
  418. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +146 -18
  419. package/src/providers/openai/__tests__/chat-completions-provider-thought-signature.test.ts +613 -0
  420. package/src/providers/openai/__tests__/google-thought-signature.test.ts +235 -0
  421. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +83 -1
  422. package/src/providers/openai/chat-completions-provider.ts +212 -79
  423. package/src/providers/openai/codex-models.ts +0 -4
  424. package/src/providers/openai/google-thought-signature.ts +320 -0
  425. package/src/providers/opencode/client.test.ts +73 -4
  426. package/src/providers/opencode/client.ts +60 -3
  427. package/src/providers/openrouter/client.ts +20 -0
  428. package/src/providers/outbound-request-sanitize.ts +96 -0
  429. package/src/providers/retry.ts +30 -19
  430. package/src/providers/unparseable-tool-args.ts +5 -1
  431. package/src/runtime/AGENTS.md +3 -1
  432. package/src/runtime/__tests__/channel-send.test.ts +443 -0
  433. package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +89 -0
  434. package/src/runtime/__tests__/host-observe.test.ts +40 -1
  435. package/src/runtime/__tests__/live-voice-upgrade.test.ts +58 -0
  436. package/src/runtime/__tests__/runtime-ws-test-utils.ts +72 -0
  437. package/src/runtime/__tests__/watch-stream-upgrade.test.ts +59 -0
  438. package/src/runtime/agent-wake.ts +2 -1
  439. package/src/runtime/assistant-event-hub.ts +21 -3
  440. package/src/runtime/auth/same-actor.ts +38 -3
  441. package/src/runtime/channel-send.ts +316 -0
  442. package/src/runtime/host-observe.ts +11 -1
  443. package/src/runtime/http-server.ts +160 -151
  444. package/src/runtime/routes/__tests__/acp-routes.test.ts +159 -4
  445. package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +205 -0
  446. package/src/runtime/routes/__tests__/channel-send-routes.test.ts +127 -0
  447. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +108 -1
  448. package/src/runtime/routes/__tests__/default-provider-routes.test.ts +2 -2
  449. package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +107 -0
  450. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +16 -12
  451. package/src/runtime/routes/acp-routes.test.ts +15 -0
  452. package/src/runtime/routes/acp-routes.ts +72 -9
  453. package/src/runtime/routes/app-management-routes.ts +73 -0
  454. package/src/runtime/routes/app-routes.ts +1 -1
  455. package/src/runtime/routes/channel-send-routes.ts +85 -0
  456. package/src/runtime/routes/conversation-list-routes.ts +14 -3
  457. package/src/runtime/routes/conversation-query-routes.ts +40 -0
  458. package/src/runtime/routes/conversation-routes.ts +16 -3
  459. package/src/runtime/routes/credential-routes.ts +19 -10
  460. package/src/runtime/routes/desktop-setup-routes.test.ts +74 -0
  461. package/src/runtime/routes/desktop-setup-routes.ts +35 -0
  462. package/src/runtime/routes/diagnostics-routes.ts +61 -4
  463. package/src/runtime/routes/events-routes.ts +4 -0
  464. package/src/runtime/routes/host-cu-routes.ts +6 -0
  465. package/src/runtime/routes/index.ts +4 -2
  466. package/src/runtime/routes/log-export-routes.ts +2 -0
  467. package/src/runtime/routes/mcp-auth-routes.ts +39 -47
  468. package/src/runtime/routes/migration-routes.ts +6 -20
  469. package/src/runtime/routes/oauth-commands-routes.ts +36 -0
  470. package/src/runtime/routes/platform-routes.ts +18 -15
  471. package/src/runtime/routes/workspace-routes.test.ts +55 -26
  472. package/src/runtime/routes/workspace-routes.ts +46 -63
  473. package/src/runtime/routes/workspace-tree-walk.test.ts +244 -0
  474. package/src/runtime/routes/workspace-tree-walk.ts +240 -0
  475. package/src/runtime/routes/workspace-utils.ts +17 -0
  476. package/src/runtime/services/__tests__/conversation-serializer.test.ts +28 -0
  477. package/src/runtime/services/conversation-serializer.ts +16 -3
  478. package/src/runtime/sync/resource-sync-events.ts +2 -1
  479. package/src/schedule/run-script.ts +2 -1
  480. package/src/schedule/schedule-store.ts +9 -2
  481. package/src/schedule/worker.ts +1 -1
  482. package/src/security/__tests__/ces-http-record-backend.test.ts +137 -0
  483. package/src/security/ces-http-record-backend.ts +120 -0
  484. package/src/security/ces-rpc-record-backend.ts +7 -0
  485. package/src/security/untrusted-content.ts +3 -1
  486. package/src/skills/__tests__/workspace-skill-md-signature.test.ts +60 -0
  487. package/src/skills/workspace-skill-md-signature.ts +50 -0
  488. package/src/subagent/consult-context.ts +12 -25
  489. package/src/tools/__tests__/side-effect-abort-guard.test.ts +1 -0
  490. package/src/tools/acp/list-agents.test.ts +3 -1
  491. package/src/tools/acp/spawn.test.ts +146 -11
  492. package/src/tools/acp/spawn.ts +21 -3
  493. package/src/tools/acp/status.test.ts +94 -0
  494. package/src/tools/acp/status.ts +40 -3
  495. package/src/tools/app-control/skill-proxy-bridge.ts +2 -1
  496. package/src/tools/browser/browser-execution.ts +1 -1
  497. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +9 -3
  498. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +2 -1
  499. package/src/tools/browser/cdp-client/extension-cdp-client.ts +3 -2
  500. package/src/tools/browser/network-recorder.ts +2 -1
  501. package/src/tools/capability-offer.test.ts +22 -2
  502. package/src/tools/capability-offer.ts +16 -1
  503. package/src/tools/computer-use/definitions.ts +121 -2
  504. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -1
  505. package/src/tools/credentials/metadata-store.ts +100 -0
  506. package/src/tools/filesystem/search.ts +2 -1
  507. package/src/tools/host-filesystem/edit.test.ts +2 -1
  508. package/src/tools/host-filesystem/edit.ts +2 -2
  509. package/src/tools/host-filesystem/read.test.ts +2 -1
  510. package/src/tools/host-filesystem/read.ts +2 -2
  511. package/src/tools/host-filesystem/transfer.test.ts +2 -1
  512. package/src/tools/host-filesystem/transfer.ts +2 -2
  513. package/src/tools/host-filesystem/write.test.ts +2 -1
  514. package/src/tools/host-filesystem/write.ts +2 -2
  515. package/src/tools/host-terminal/host-shell.test.ts +16 -0
  516. package/src/tools/host-terminal/host-shell.ts +4 -20
  517. package/src/tools/network/web-search.ts +6 -1
  518. package/src/tools/policy-context.ts +6 -7
  519. package/src/tools/skills/scaffold-managed.ts +61 -3
  520. package/src/tools/subagent/spawn.ts +5 -8
  521. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +57 -0
  522. package/src/tools/terminal/sanitized-bash.ts +105 -0
  523. package/src/tools/watch/watch-retro-report.ts +0 -8
  524. package/src/types/host-capabilities.ts +5 -3
  525. package/src/util/__tests__/language-subtag.test.ts +23 -1
  526. package/src/util/ca-bundle.ts +13 -0
  527. package/src/util/language-subtag.ts +16 -0
  528. package/src/util/platform.ts +8 -0
  529. package/src/util/unicode.ts +15 -1
  530. package/src/watch/watch-timeline.ts +2 -2
  531. package/src/workflows/run-manager.ts +2 -1
  532. package/src/workspace/git-porcelain.ts +66 -0
  533. package/src/workspace/git-service.ts +6 -55
  534. package/src/workspace/migrations/154-repair-retired-codex-gpt-5-4-model-ids.ts +258 -0
  535. package/src/workspace/migrations/155-move-front-model-config-to-voice.ts +78 -0
  536. package/src/workspace/migrations/156-extract-workspace-mcp-json.ts +214 -0
  537. package/src/workspace/migrations/__tests__/155-move-front-model-config-to-voice.test.ts +102 -0
  538. package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +199 -0
  539. package/src/workspace/migrations/registry.ts +6 -0
  540. package/src/__tests__/app-source-watcher.test.ts +0 -218
  541. package/src/__tests__/config-watcher-skill-reseed.test.ts +0 -359
  542. package/src/__tests__/speaker-identification.test.ts +0 -56
  543. package/src/calls/speaker-identification.ts +0 -233
  544. package/src/daemon/app-source-watcher.ts +0 -218
  545. package/src/messaging/providers/whatsapp/client.ts +0 -29
  546. package/src/runtime/routes/debug-bash-routes.ts +0 -173
  547. package/src/util/fs-watcher-error.ts +0 -36
  548. /package/src/{live-voice → calls}/progress-phrases.ts +0 -0
@@ -115,6 +115,11 @@ For everything else in your review window, use the \`remember\` tool on facts, p
115
115
  expect(out).toContain(
116
116
  "\n---\n\nIf your review window contains a PROCEDURE you actually carried out",
117
117
  );
118
+ // An UPDATE is announced by a notice whose only account of the change is
119
+ // what the pass passes here, so the instruction has to ask for it.
120
+ expect(out).toContain(
121
+ "and pass `change_summary` (the update is rejected without it): one or two short sentences (under 200 characters)",
122
+ );
118
123
  expect(
119
124
  out.endsWith(
120
125
  "skills are for executed, reusable procedures, not for facts.\n",
@@ -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", () => {
@@ -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