@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
@@ -5,7 +5,11 @@
5
5
  // Two pointers move independently:
6
6
  // - `lastProcessedMessageId` advances ONLY when a retrospective run
7
7
  // completes successfully (correctness invariant — failures must
8
- // re-process the same messages on the next attempt).
8
+ // re-process the same messages on the next attempt). Its companion
9
+ // `lastProcessedCreatedAt` is that row's `createdAt` at write time, so the
10
+ // `(createdAt, id)` cursor stays reconstructible after the row itself is
11
+ // deleted: a regenerated reply discards the latest assistant message,
12
+ // which is exactly where the cursor tends to sit.
9
13
  // - `lastRunAt` advances at the end of every job that actually attempted a
10
14
  // run (success or failure). Drives the per-conversation cooldown gate in
11
15
  // the trigger-check helper so failing jobs can't loop in tight retries
@@ -25,20 +29,39 @@
25
29
  // that connection is unavailable. The memory database has no `conversations`
26
30
  // table, so there is no FK cascade — the `conversation-deleted` hook purges the
27
31
  // row explicitly instead.
32
+ //
33
+ // `last_processed_created_at` is added to the table by
34
+ // `ensureRetrospectiveCursorColumn`: an idempotent, fail-open ALTER run lazily
35
+ // on first use in each process (daemon, memory worker, CLI) rather than by the
36
+ // global migration chain, which must not gate DB readiness on the memory
37
+ // database. The probe is memoized per memory connection, so a connection
38
+ // replaced by `resetDb()` (restores, imports) is probed again before its
39
+ // first read. When the column cannot be added, reads and writes fall back to
40
+ // the id-only cursor shape and the probe is retried after a short backoff.
28
41
 
29
42
  import { desc, eq } from "drizzle-orm";
30
43
 
31
44
  import type { DrizzleDb } from "../../../persistence/db-connection.js";
45
+ import { messageCreatedAtByIds } from "../../../persistence/message-reads.js";
32
46
  import { memoryRetrospectiveState } from "../../../persistence/schema/index.js";
33
47
  import { withSqliteRetry } from "./host-utils.js";
34
48
  import { getLogger } from "./logging.js";
35
- import { memoryDbOrNull } from "./memory-db.js";
49
+ import { memoryDbOrNull, memorySqliteOrNull } from "./memory-db.js";
36
50
 
37
51
  const log = getLogger("memory-retrospective-state");
38
52
 
53
+ const TABLE = "memory_retrospective_state";
54
+ const CURSOR_CREATED_AT_COLUMN = "last_processed_created_at";
55
+
39
56
  export interface MemoryRetrospectiveState {
40
57
  conversationId: string;
41
58
  lastProcessedMessageId: string;
59
+ /**
60
+ * `createdAt` of the `lastProcessedMessageId` row when the pointer was
61
+ * written, or null when unknown: rows written before the column existed,
62
+ * and the `""` sentinel. Lets the cursor bound a read after its row is gone.
63
+ */
64
+ lastProcessedCreatedAt: number | null;
42
65
  lastRunAt: number;
43
66
  /**
44
67
  * Cumulative `remember` contents from prior retrospective passes, oldest
@@ -99,6 +122,124 @@ function serializeRememberedLog(log: string[]): string | null {
99
122
  return log.length === 0 ? null : JSON.stringify(log);
100
123
  }
101
124
 
125
+ // ---------------------------------------------------------------------------
126
+ // Cursor timestamp column
127
+ // ---------------------------------------------------------------------------
128
+
129
+ type MemorySqlite = NonNullable<ReturnType<typeof memorySqliteOrNull>>;
130
+
131
+ /** The memory connection the column was confirmed on; any other re-probes. */
132
+ let cursorColumnEnsuredOn: MemorySqlite | null = null;
133
+ /**
134
+ * The connection the last probe failed on and when. The backoff applies to
135
+ * that connection only: a replaced connection is probed at once, so a
136
+ * restore right after a failure still gets the column before its first read.
137
+ */
138
+ let cursorColumnFailedOn: MemorySqlite | null = null;
139
+ let lastCursorColumnFailureAt = 0;
140
+ /** Backoff before re-probing the same connection after a failed ALTER. */
141
+ const CURSOR_COLUMN_RETRY_MS = 60_000;
142
+
143
+ function isDuplicateColumnError(err: unknown): boolean {
144
+ return err instanceof Error && /duplicate column name/i.test(err.message);
145
+ }
146
+
147
+ /**
148
+ * Add `last_processed_created_at` to the state table when it is missing.
149
+ * Idempotent; memoized per memory connection, so a connection replaced by
150
+ * `resetDb()` is probed again and an imported database that lacks the column
151
+ * gets it before its first read. Fail-open: when the ALTER cannot run, the
152
+ * failure is logged, every read and write falls back to the id-only shape,
153
+ * and that connection is probed again after {@link CURSOR_COLUMN_RETRY_MS}.
154
+ * A table the migration chain has not created yet is neither memoized nor a
155
+ * failure.
156
+ */
157
+ export function ensureRetrospectiveCursorColumn(context: string): boolean {
158
+ const raw = memorySqliteOrNull(context);
159
+ if (!raw) {
160
+ return false;
161
+ }
162
+ if (cursorColumnEnsuredOn === raw) {
163
+ return true;
164
+ }
165
+ if (
166
+ cursorColumnFailedOn === raw &&
167
+ Date.now() - lastCursorColumnFailureAt < CURSOR_COLUMN_RETRY_MS
168
+ ) {
169
+ return false;
170
+ }
171
+ try {
172
+ const columns = raw.query(`PRAGMA table_info(${TABLE})`).all() as Array<{
173
+ name: string;
174
+ }>;
175
+ if (columns.length === 0) {
176
+ return false;
177
+ }
178
+ if (!columns.some((column) => column.name === CURSOR_CREATED_AT_COLUMN)) {
179
+ raw.exec(
180
+ `ALTER TABLE ${TABLE} ADD COLUMN ${CURSOR_CREATED_AT_COLUMN} INTEGER`,
181
+ );
182
+ }
183
+ } catch (err) {
184
+ if (!isDuplicateColumnError(err)) {
185
+ cursorColumnFailedOn = raw;
186
+ lastCursorColumnFailureAt = Date.now();
187
+ log.warn(
188
+ { err, context },
189
+ "could not add the retrospective cursor timestamp column; cursors fall back to id-only until the next probe",
190
+ );
191
+ return false;
192
+ }
193
+ }
194
+ cursorColumnEnsuredOn = raw;
195
+ cursorColumnFailedOn = null;
196
+ return true;
197
+ }
198
+
199
+ /**
200
+ * Forget the memoized column probe so the next call re-runs it. Intended
201
+ * ONLY for tests that recreate the memory database mid-process.
202
+ */
203
+ export function _resetRetrospectiveCursorColumnForTests(): void {
204
+ cursorColumnEnsuredOn = null;
205
+ cursorColumnFailedOn = null;
206
+ lastCursorColumnFailureAt = 0;
207
+ }
208
+
209
+ // ---------------------------------------------------------------------------
210
+ // Reads
211
+ // ---------------------------------------------------------------------------
212
+
213
+ const BASE_COLUMNS = {
214
+ conversationId: memoryRetrospectiveState.conversationId,
215
+ lastProcessedMessageId: memoryRetrospectiveState.lastProcessedMessageId,
216
+ lastRunAt: memoryRetrospectiveState.lastRunAt,
217
+ rememberedLog: memoryRetrospectiveState.rememberedLog,
218
+ };
219
+
220
+ const CURSOR_COLUMNS = {
221
+ ...BASE_COLUMNS,
222
+ lastProcessedCreatedAt: memoryRetrospectiveState.lastProcessedCreatedAt,
223
+ };
224
+
225
+ interface StateRow {
226
+ conversationId: string;
227
+ lastProcessedMessageId: string;
228
+ lastRunAt: number;
229
+ rememberedLog: string | null;
230
+ lastProcessedCreatedAt?: number | null;
231
+ }
232
+
233
+ function toState(row: StateRow): MemoryRetrospectiveState {
234
+ return {
235
+ conversationId: row.conversationId,
236
+ lastProcessedMessageId: row.lastProcessedMessageId,
237
+ lastProcessedCreatedAt: row.lastProcessedCreatedAt ?? null,
238
+ lastRunAt: row.lastRunAt,
239
+ rememberedLog: parseRememberedLog(row.rememberedLog),
240
+ };
241
+ }
242
+
102
243
  /**
103
244
  * Return the `limit` most-recently-run retrospective state rows, newest first.
104
245
  */
@@ -109,23 +250,22 @@ export function listRetrospectiveStates(
109
250
  if (!mdb) {
110
251
  return [];
111
252
  }
112
- const rows = mdb
113
- .select({
114
- conversationId: memoryRetrospectiveState.conversationId,
115
- lastProcessedMessageId: memoryRetrospectiveState.lastProcessedMessageId,
116
- lastRunAt: memoryRetrospectiveState.lastRunAt,
117
- rememberedLog: memoryRetrospectiveState.rememberedLog,
118
- })
119
- .from(memoryRetrospectiveState)
120
- .orderBy(desc(memoryRetrospectiveState.lastRunAt))
121
- .limit(limit)
122
- .all();
123
- return rows.map((row) => ({
124
- conversationId: row.conversationId,
125
- lastProcessedMessageId: row.lastProcessedMessageId,
126
- lastRunAt: row.lastRunAt,
127
- rememberedLog: parseRememberedLog(row.rememberedLog),
128
- }));
253
+ const rows: StateRow[] = ensureRetrospectiveCursorColumn(
254
+ "listRetrospectiveStates",
255
+ )
256
+ ? mdb
257
+ .select(CURSOR_COLUMNS)
258
+ .from(memoryRetrospectiveState)
259
+ .orderBy(desc(memoryRetrospectiveState.lastRunAt))
260
+ .limit(limit)
261
+ .all()
262
+ : mdb
263
+ .select(BASE_COLUMNS)
264
+ .from(memoryRetrospectiveState)
265
+ .orderBy(desc(memoryRetrospectiveState.lastRunAt))
266
+ .limit(limit)
267
+ .all();
268
+ return rows.map(toState);
129
269
  }
130
270
 
131
271
  /**
@@ -138,37 +278,45 @@ export function getRetrospectiveState(
138
278
  if (!mdb) {
139
279
  return null;
140
280
  }
141
- const row = mdb
142
- .select({
143
- conversationId: memoryRetrospectiveState.conversationId,
144
- lastProcessedMessageId: memoryRetrospectiveState.lastProcessedMessageId,
145
- lastRunAt: memoryRetrospectiveState.lastRunAt,
146
- rememberedLog: memoryRetrospectiveState.rememberedLog,
147
- })
148
- .from(memoryRetrospectiveState)
149
- .where(eq(memoryRetrospectiveState.conversationId, conversationId))
150
- .get();
151
- if (!row) {
152
- return null;
153
- }
154
- return {
155
- conversationId: row.conversationId,
156
- lastProcessedMessageId: row.lastProcessedMessageId,
157
- lastRunAt: row.lastRunAt,
158
- rememberedLog: parseRememberedLog(row.rememberedLog),
159
- };
281
+ const where = eq(memoryRetrospectiveState.conversationId, conversationId);
282
+ const row: StateRow | undefined = ensureRetrospectiveCursorColumn(
283
+ "getRetrospectiveState",
284
+ )
285
+ ? mdb
286
+ .select(CURSOR_COLUMNS)
287
+ .from(memoryRetrospectiveState)
288
+ .where(where)
289
+ .get()
290
+ : mdb
291
+ .select(BASE_COLUMNS)
292
+ .from(memoryRetrospectiveState)
293
+ .where(where)
294
+ .get();
295
+ return row ? toState(row) : null;
160
296
  }
161
297
 
298
+ // ---------------------------------------------------------------------------
299
+ // Writes
300
+ // ---------------------------------------------------------------------------
301
+
162
302
  /**
163
303
  * Upsert both pointers atomically. Used on successful retrospective runs.
164
304
  *
305
+ * `lastProcessedCreatedAt` is the id's companion and is rewritten with every
306
+ * pointer write: an omitted value records "unknown" for the new id rather
307
+ * than keeping the previous row's timestamp.
308
+ *
165
309
  * `rememberedLog`, when provided, is written in the same statement so the
166
310
  * cumulative dedup log can never drift from the pointer it was computed
167
311
  * against. When omitted, the stored log is left untouched (and seeded NULL on
168
312
  * first insert).
169
313
  */
170
314
  export async function upsertRetrospectiveState(
171
- args: Omit<MemoryRetrospectiveState, "rememberedLog"> & {
315
+ args: Omit<
316
+ MemoryRetrospectiveState,
317
+ "rememberedLog" | "lastProcessedCreatedAt"
318
+ > & {
319
+ lastProcessedCreatedAt?: number | null;
172
320
  rememberedLog?: string[];
173
321
  },
174
322
  ): Promise<void> {
@@ -176,6 +324,9 @@ export async function upsertRetrospectiveState(
176
324
  if (!mdb) {
177
325
  return;
178
326
  }
327
+ const withCursor = ensureRetrospectiveCursorColumn(
328
+ "upsertRetrospectiveState",
329
+ );
179
330
  const serializedLog =
180
331
  args.rememberedLog === undefined
181
332
  ? undefined
@@ -185,12 +336,16 @@ export async function upsertRetrospectiveState(
185
336
  // on first insert).
186
337
  const set: {
187
338
  lastProcessedMessageId: string;
339
+ lastProcessedCreatedAt?: number | null;
188
340
  lastRunAt: number;
189
341
  rememberedLog?: string | null;
190
342
  } = {
191
343
  lastProcessedMessageId: args.lastProcessedMessageId,
192
344
  lastRunAt: args.lastRunAt,
193
345
  };
346
+ if (withCursor) {
347
+ set.lastProcessedCreatedAt = args.lastProcessedCreatedAt ?? null;
348
+ }
194
349
  if (serializedLog !== undefined) {
195
350
  set.rememberedLog = serializedLog;
196
351
  }
@@ -203,6 +358,9 @@ export async function upsertRetrospectiveState(
203
358
  lastProcessedMessageId: args.lastProcessedMessageId,
204
359
  lastRunAt: args.lastRunAt,
205
360
  rememberedLog: serializedLog ?? null,
361
+ ...(withCursor
362
+ ? { lastProcessedCreatedAt: args.lastProcessedCreatedAt ?? null }
363
+ : {}),
206
364
  })
207
365
  .onConflictDoUpdate({
208
366
  target: memoryRetrospectiveState.conversationId,
@@ -216,6 +374,46 @@ export async function upsertRetrospectiveState(
216
374
  );
217
375
  }
218
376
 
377
+ /**
378
+ * The copied source row that sits latest at or before `bound` in
379
+ * `(createdAt, id)` order, as its forked id and `createdAt`, or null when
380
+ * every copied row lies after the bound. Reads the source rows through the
381
+ * fork transaction's handle.
382
+ */
383
+ function latestCopiedRowAtOrBefore(
384
+ database: DrizzleDb,
385
+ forkedMessageIds: Map<string, string>,
386
+ bound: { createdAt: number; id: string },
387
+ ): { forkedId: string; createdAt: number } | null {
388
+ const createdAtBySourceId = messageCreatedAtByIds(
389
+ [...forkedMessageIds.keys()],
390
+ { db: database },
391
+ );
392
+ let best: { sourceId: string; createdAt: number } | null = null;
393
+ for (const [sourceId, createdAt] of createdAtBySourceId) {
394
+ const atOrBefore =
395
+ createdAt < bound.createdAt ||
396
+ (createdAt === bound.createdAt && sourceId <= bound.id);
397
+ if (!atOrBefore) {
398
+ continue;
399
+ }
400
+ const later =
401
+ best === null ||
402
+ createdAt > best.createdAt ||
403
+ (createdAt === best.createdAt && sourceId > best.sourceId);
404
+ if (later) {
405
+ best = { sourceId, createdAt };
406
+ }
407
+ }
408
+ if (best === null) {
409
+ return null;
410
+ }
411
+ const forkedId = forkedMessageIds.get(best.sourceId);
412
+ return forkedId === undefined
413
+ ? null
414
+ : { forkedId, createdAt: best.createdAt };
415
+ }
416
+
219
417
  /**
220
418
  * Carry the source conversation's retrospective state into a forked child so
221
419
  * the fork doesn't re-process content the parent already covered. Synchronous
@@ -229,18 +427,23 @@ export async function upsertRetrospectiveState(
229
427
  * succeeded) → child pointer is also `""`.
230
428
  * - source pointer is within the copied range (`forkedMessageIds` has it) →
231
429
  * child pointer is the mapped forked message ID.
232
- * - source pointer is past the fork boundary (not in `forkedMessageIds`)
233
- * child pointer is the last copied message's mapped ID. All copied
234
- * messages have already been retro'd by the source, so the child should
235
- * wait for new post-fork messages before its first retro fires.
430
+ * - source pointer is not in `forkedMessageIds` (its row was deleted, or it
431
+ * lies past the copied range) and carries a stored `createdAt` → child
432
+ * pointer is the latest copied row at or before the source's
433
+ * `(createdAt, id)` bound; copied rows after the bound stay unprocessed.
434
+ * - source pointer is not in `forkedMessageIds` and has no stored
435
+ * `createdAt` → child pointer is the last copied message's mapped ID, on
436
+ * the assumption it lies past the fork boundary.
236
437
  *
438
+ * `lastProcessedCreatedAt` is read from the forked pointer's row through
439
+ * `database`, the fork transaction's main-DB handle, which is the only handle
440
+ * the freshly copied rows are visible through.
237
441
  * `lastRunAt` is copied verbatim — the cooldown gate inherits from source.
238
442
  * `rememberedLog` is copied verbatim — the parent's saves remain the child's
239
443
  * dedup baseline.
240
444
  *
241
- * The row lives on the memory connection, so this reads/writes there rather
242
- * than on the main fork transaction's handle — the `database` arg is unused
243
- * now, and an unavailable memory database is a best-effort no-op.
445
+ * The state row itself is written on the memory connection; an unavailable
446
+ * memory database is a best-effort no-op.
244
447
  */
245
448
  export function forkRetrospectiveState(args: {
246
449
  database: DrizzleDb;
@@ -250,6 +453,7 @@ export function forkRetrospectiveState(args: {
250
453
  lastCopiedSourceMessageId: string | null;
251
454
  }): void {
252
455
  const {
456
+ database,
253
457
  sourceConversationId,
254
458
  forkedConversationId,
255
459
  forkedMessageIds,
@@ -262,47 +466,84 @@ export function forkRetrospectiveState(args: {
262
466
  return;
263
467
  }
264
468
 
265
- const sourceRow = mdb
266
- .select({
267
- lastProcessedMessageId: memoryRetrospectiveState.lastProcessedMessageId,
268
- lastRunAt: memoryRetrospectiveState.lastRunAt,
269
- rememberedLog: memoryRetrospectiveState.rememberedLog,
270
- })
271
- .from(memoryRetrospectiveState)
272
- .where(eq(memoryRetrospectiveState.conversationId, sourceConversationId))
273
- .get();
469
+ const withCursor = ensureRetrospectiveCursorColumn(
470
+ "forkRetrospectiveState",
471
+ );
472
+ const sourceRow: StateRow | undefined = withCursor
473
+ ? mdb
474
+ .select(CURSOR_COLUMNS)
475
+ .from(memoryRetrospectiveState)
476
+ .where(
477
+ eq(memoryRetrospectiveState.conversationId, sourceConversationId),
478
+ )
479
+ .get()
480
+ : mdb
481
+ .select(BASE_COLUMNS)
482
+ .from(memoryRetrospectiveState)
483
+ .where(
484
+ eq(memoryRetrospectiveState.conversationId, sourceConversationId),
485
+ )
486
+ .get();
274
487
  if (!sourceRow) {
275
488
  return;
276
489
  }
277
490
 
491
+ const sourcePointer = sourceRow.lastProcessedMessageId;
492
+ const sourceCreatedAt = sourceRow.lastProcessedCreatedAt ?? null;
278
493
  let forkedPointer = "";
279
- if (sourceRow.lastProcessedMessageId !== "") {
280
- const mapped = forkedMessageIds.get(sourceRow.lastProcessedMessageId);
494
+ let forkedCreatedAt: number | null = null;
495
+ if (sourcePointer !== "") {
496
+ const mapped = forkedMessageIds.get(sourcePointer);
281
497
  if (mapped !== undefined) {
282
498
  forkedPointer = mapped;
499
+ forkedCreatedAt =
500
+ messageCreatedAtByIds([forkedPointer], { db: database }).get(
501
+ forkedPointer,
502
+ ) ?? null;
503
+ } else if (sourceCreatedAt !== null) {
504
+ // The source pointer is not among the copied rows: either its row
505
+ // was deleted (a regenerated reply) or it lies past the copied range.
506
+ // Its stored `(createdAt, id)` bound still says where it sat, so the
507
+ // child takes the latest copied row at or before that bound. Copied
508
+ // rows after it (a replacement reply, later turns) stay unprocessed;
509
+ // a bound before every copied row leaves the child at "nothing
510
+ // processed yet".
511
+ const placed = latestCopiedRowAtOrBefore(database, forkedMessageIds, {
512
+ createdAt: sourceCreatedAt,
513
+ id: sourcePointer,
514
+ });
515
+ if (placed) {
516
+ forkedPointer = placed.forkedId;
517
+ forkedCreatedAt = placed.createdAt;
518
+ }
283
519
  } else if (lastCopiedSourceMessageId !== null) {
284
- // Source pointer is past the fork boundary everything copied has
285
- // already been processed by the source, so clamp to the last copied
286
- // message so the fork waits for new post-fork messages.
520
+ // No stored bound to place a missing pointer by (a row written before
521
+ // timestamps were kept): treat it as past the fork boundary and clamp
522
+ // to the last copied message so the fork waits for new post-fork
523
+ // messages.
287
524
  forkedPointer = forkedMessageIds.get(lastCopiedSourceMessageId) ?? "";
525
+ forkedCreatedAt =
526
+ forkedPointer === ""
527
+ ? null
528
+ : (messageCreatedAtByIds([forkedPointer], { db: database }).get(
529
+ forkedPointer,
530
+ ) ?? null);
288
531
  }
289
532
  }
290
533
 
534
+ const values = {
535
+ lastProcessedMessageId: forkedPointer,
536
+ lastRunAt: sourceRow.lastRunAt,
537
+ rememberedLog: sourceRow.rememberedLog,
538
+ ...(withCursor ? { lastProcessedCreatedAt: forkedCreatedAt } : {}),
539
+ };
540
+
291
541
  mdb
292
542
  .insert(memoryRetrospectiveState)
293
- .values({
294
- conversationId: forkedConversationId,
295
- lastProcessedMessageId: forkedPointer,
296
- lastRunAt: sourceRow.lastRunAt,
297
- rememberedLog: sourceRow.rememberedLog,
298
- })
543
+ .values({ conversationId: forkedConversationId, ...values })
299
544
  .onConflictDoUpdate({
300
545
  target: memoryRetrospectiveState.conversationId,
301
- set: {
302
- lastProcessedMessageId: forkedPointer,
303
- lastRunAt: sourceRow.lastRunAt,
304
- rememberedLog: sourceRow.rememberedLog,
305
- },
546
+ set: values,
306
547
  })
307
548
  .run();
308
549
  } catch (err) {
@@ -78,6 +78,7 @@ import { conversations } from "../../../persistence/schema/index.js";
78
78
  import { getLogger } from "./logging.js";
79
79
  import { countRetrospectiveMessagesAfter } from "./memory-retrospective-accounting.js";
80
80
  import { MEMORY_RETROSPECTIVE_SOURCES } from "./memory-retrospective-constants.js";
81
+ import { retrospectiveCursor } from "./memory-retrospective-cursor.js";
81
82
  import { enqueueMemoryRetrospectiveIfEnabled } from "./memory-retrospective-enqueue.js";
82
83
  import { getRetrospectiveState } from "./memory-retrospective-state.js";
83
84
 
@@ -273,7 +274,7 @@ export async function runRetrospectiveSweep(
273
274
 
274
275
  const unprocessed = countRetrospectiveMessagesAfter(
275
276
  conversationId,
276
- state?.lastProcessedMessageId ?? null,
277
+ retrospectiveCursor(state),
277
278
  );
278
279
  if (unprocessed === 0) {
279
280
  continue;
@@ -9,8 +9,9 @@
9
9
  // (success or failure). Prevents tight retry loops across trigger
10
10
  // types.
11
11
  // 2. Interval threshold: time since last attempt >= `timeThresholdMs`.
12
- // 3. Message count threshold: new messages since `lastProcessedMessageId`
13
- // >= `messageThreshold`.
12
+ // 3. Message count threshold: new messages since the persisted cursor
13
+ // (`lastProcessedMessageId` with its `createdAt`, so the count survives
14
+ // the cursor's row being deleted) >= `messageThreshold`.
14
15
  //
15
16
  // First-run case (no state row) skips the cooldown — `lastRunAt = 0` so the
16
17
  // gap is effectively `Infinity`. The interval threshold trips immediately;
@@ -24,6 +25,7 @@
24
25
  import type { AssistantConfig } from "../../../config/types.js";
25
26
  import { getLogger } from "./logging.js";
26
27
  import { countRetrospectiveMessagesAfter } from "./memory-retrospective-accounting.js";
28
+ import { retrospectiveCursor } from "./memory-retrospective-cursor.js";
27
29
  import { enqueueMemoryRetrospectiveIfEnabled } from "./memory-retrospective-enqueue.js";
28
30
  import { getRetrospectiveState } from "./memory-retrospective-state.js";
29
31
 
@@ -83,7 +85,7 @@ export function maybeEnqueueRetrospective(
83
85
  const state = getRetrospectiveState(conversationId);
84
86
  const newMessageCount = countRetrospectiveMessagesAfter(
85
87
  conversationId,
86
- state?.lastProcessedMessageId ?? null,
88
+ retrospectiveCursor(state),
87
89
  );
88
90
  if (newMessageCount === 0) {
89
91
  return;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Plugin-local re-export of the workspace SKILL.md mtime poll so hook files
3
+ * under `hooks/` do not grow a new escaping specifier. The daemon module is
4
+ * already a memory-plugin baseline import from `startup.ts` / `worker.ts`.
5
+ */
6
+ export {
7
+ startWorkspaceSkillMdMtimePoll,
8
+ stopWorkspaceSkillMdMtimePoll,
9
+ } from "../../../daemon/skill-memory-refresh.js";
@@ -28,7 +28,10 @@ import { join } from "node:path";
28
28
  import { usesConceptPageMemory } from "../../../config/memory-v3-gate.js";
29
29
  import type { AssistantConfig } from "../../../config/schema.js";
30
30
  import { reconcileEmbeddingIdentity } from "../../../daemon/embedding-reconcile.js";
31
- import { refreshSkillCapabilityMemories } from "../../../daemon/skill-memory-refresh.js";
31
+ import {
32
+ refreshSkillCapabilityMemories,
33
+ startWorkspaceSkillMdMtimePoll,
34
+ } from "../../../daemon/skill-memory-refresh.js";
32
35
  import { selectEmbeddingBackend } from "../../../persistence/embeddings/embedding-backend.js";
33
36
  import {
34
37
  initMessagesLexicalIndex,
@@ -38,11 +41,15 @@ import {
38
41
  clearRebuildSentinel,
39
42
  initQdrantClient,
40
43
  } from "../../../persistence/embeddings/qdrant-client.js";
41
- import { createQdrantManager } from "../../../persistence/embeddings/qdrant-manager.js";
44
+ import {
45
+ createQdrantManager,
46
+ describeQdrantStartFailure,
47
+ } from "../../../persistence/embeddings/qdrant-manager.js";
42
48
  import {
43
49
  enqueueMemoryJob,
44
50
  isMemoryEnabled,
45
51
  } from "../../../persistence/jobs-store.js";
52
+ import { recordWatchdogEvent } from "../../../telemetry/watchdog-events-store.js";
46
53
  import { resolveQdrantUrl } from "./embeddings.js";
47
54
  import { startMemoryJobsWorker } from "./jobs-worker.js";
48
55
  import { getLogger } from "./logging.js";
@@ -89,6 +96,23 @@ export async function runMemoryStartup(config: AssistantConfig): Promise<void> {
89
96
  { err },
90
97
  "Qdrant failed to start after all attempts — memory features will be unavailable",
91
98
  );
99
+ // Fleet-wide record of the failure: the step, the exit code and
100
+ // Qdrant's own reason line, bounded to telemetry-safe metadata by
101
+ // `describeQdrantStartFailure`. Observational only: the outbox insert
102
+ // can throw on a locked or half-migrated telemetry DB, and this path
103
+ // must still reach the worker start below.
104
+ try {
105
+ recordWatchdogEvent({
106
+ checkName: "qdrant_start_failed",
107
+ value: attempt,
108
+ detail: { attempts: attempt, ...describeQdrantStartFailure(err) },
109
+ });
110
+ } catch (telemetryErr) {
111
+ log.warn(
112
+ { err: telemetryErr },
113
+ "Qdrant start failure could not be recorded to telemetry",
114
+ );
115
+ }
92
116
  }
93
117
  }
94
118
  }
@@ -263,6 +287,8 @@ export async function runMemoryStartup(config: AssistantConfig): Promise<void> {
263
287
  log.warn({ err }, "Graph capability seeding failed — continuing");
264
288
  }
265
289
 
290
+ startWorkspaceSkillMdMtimePoll();
291
+
266
292
  // ---- v1 (legacy engine) — delete with v1 ----
267
293
  // Auto-bootstrap: if the graph has no non-procedural nodes but historical
268
294
  // segments exist, enqueue a one-time graph_bootstrap job to populate the
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { describe, expect, test } from "bun:test";
6
6
 
7
- import { setConfig } from "../../../../../__tests__/helpers/set-config.js";
7
+ import { setWorkspaceMcp } from "../../../../../__tests__/helpers/set-workspace-mcp.js";
8
8
  import type { SkillCapabilityInput } from "../skill-content.js";
9
9
 
10
10
  describe("buildSkillContent", () => {
@@ -90,14 +90,12 @@ describe("augmentMcpSetupDescription", () => {
90
90
  });
91
91
 
92
92
  test("appends 'Configured: <names>' for mcp-setup with configured servers", async () => {
93
- setConfig("mcp", {
94
- servers: {
95
- "example-server": {
96
- transport: { type: "stdio", command: "example-cmd" },
97
- },
98
- "another-server": {
99
- transport: { type: "stdio", command: "another-cmd" },
100
- },
93
+ setWorkspaceMcp({
94
+ "example-server": {
95
+ transport: { type: "stdio", command: "example-cmd", args: [] },
96
+ },
97
+ "another-server": {
98
+ transport: { type: "stdio", command: "another-cmd", args: [] },
101
99
  },
102
100
  });
103
101
  const { augmentMcpSetupDescription } = await import("../skill-content.js");