@vellumai/assistant 0.8.5 → 0.8.6

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 (544) hide show
  1. package/AGENTS.md +33 -1
  2. package/ARCHITECTURE.md +1 -1
  3. package/bunfig.toml +6 -1
  4. package/docs/credential-execution-service.md +6 -6
  5. package/docs/plugins.md +4 -3
  6. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +12 -13
  7. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +4 -1
  8. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
  9. package/openapi.yaml +1900 -166
  10. package/package.json +1 -1
  11. package/src/__tests__/actor-token-service.test.ts +3 -2
  12. package/src/__tests__/agent-loop-exit-reason.test.ts +102 -9
  13. package/src/__tests__/agent-loop-override-profile.test.ts +2 -1
  14. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  15. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  16. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  17. package/src/__tests__/annotate-risk-options.test.ts +1 -0
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/approval-routes-http.test.ts +9 -13
  20. package/src/__tests__/assert-not-live-db.ts +79 -0
  21. package/src/__tests__/assistant-feature-flags-integration.test.ts +9 -25
  22. package/src/__tests__/audit-log-rotation.test.ts +2 -2
  23. package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
  24. package/src/__tests__/background-workers-disk-pressure.test.ts +5 -8
  25. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  26. package/src/__tests__/btw-routes.test.ts +3 -2
  27. package/src/__tests__/call-controller.test.ts +3 -2
  28. package/src/__tests__/channel-approval-routes.test.ts +3 -2
  29. package/src/__tests__/channel-guardian.test.ts +3 -2
  30. package/src/__tests__/channel-readiness-slack-remote.test.ts +175 -0
  31. package/src/__tests__/channel-reply-delivery.test.ts +35 -0
  32. package/src/__tests__/channel-retry-sweep.test.ts +320 -3
  33. package/src/__tests__/checker.test.ts +12 -12
  34. package/src/__tests__/compaction-events.test.ts +1 -0
  35. package/src/__tests__/compaction-trail-store.test.ts +264 -0
  36. package/src/__tests__/compactor-call-site-logging.test.ts +1 -0
  37. package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
  38. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +7 -5
  39. package/src/__tests__/computer-use-tools.test.ts +12 -14
  40. package/src/__tests__/config-loader-backfill.test.ts +13 -28
  41. package/src/__tests__/config-loader-corrupt.test.ts +5 -5
  42. package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
  43. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
  44. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
  45. package/src/__tests__/config-schema.test.ts +10 -10
  46. package/src/__tests__/connection-model-compat.test.ts +83 -0
  47. package/src/__tests__/contacts-tools.test.ts +3 -2
  48. package/src/__tests__/context-token-estimator.test.ts +22 -0
  49. package/src/__tests__/conversation-abort-tool-results.test.ts +5 -0
  50. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
  52. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +34 -0
  54. package/src/__tests__/conversation-agent-loop.test.ts +488 -2
  55. package/src/__tests__/conversation-analysis-routes.test.ts +1 -0
  56. package/src/__tests__/conversation-app-control-instantiation.test.ts +29 -19
  57. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -0
  58. package/src/__tests__/conversation-attention-store.test.ts +101 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  61. package/src/__tests__/conversation-error.test.ts +30 -0
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -8
  63. package/src/__tests__/conversation-fork-route.test.ts +3 -2
  64. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  65. package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
  66. package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
  67. package/src/__tests__/conversation-lifecycle.test.ts +1 -0
  68. package/src/__tests__/conversation-list-source.test.ts +3 -2
  69. package/src/__tests__/conversation-load-history-repair.test.ts +2 -1
  70. package/src/__tests__/conversation-load-history-stripped.test.ts +1 -0
  71. package/src/__tests__/conversation-pairing.test.ts +53 -0
  72. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +26 -7
  73. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  74. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -0
  75. package/src/__tests__/conversation-queue.test.ts +333 -291
  76. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -18
  77. package/src/__tests__/conversation-routes-guardian-reply.test.ts +33 -8
  78. package/src/__tests__/conversation-routes-slash-commands.test.ts +33 -2
  79. package/src/__tests__/conversation-runtime-assembly.test.ts +78 -0
  80. package/src/__tests__/conversation-skill-tools.test.ts +38 -142
  81. package/src/__tests__/conversation-slash-queue.test.ts +84 -32
  82. package/src/__tests__/conversation-slash-unknown.test.ts +5 -0
  83. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +46 -0
  85. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
  86. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
  87. package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
  88. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -3
  89. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -17
  90. package/src/__tests__/conversation-sync-tags.test.ts +128 -12
  91. package/src/__tests__/conversation-title-service.test.ts +1 -0
  92. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +30 -0
  93. package/src/__tests__/conversation-usage.test.ts +1 -0
  94. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  95. package/src/__tests__/conversation-workspace-injection.test.ts +5 -0
  96. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -0
  97. package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
  98. package/src/__tests__/credential-broker-server-use.test.ts +5 -5
  99. package/src/__tests__/credential-execution-client.test.ts +72 -1
  100. package/src/__tests__/credential-execution-feature-gates.test.ts +10 -12
  101. package/src/__tests__/credential-health-service.test.ts +252 -3
  102. package/src/__tests__/credential-security-invariants.test.ts +5 -5
  103. package/src/__tests__/credential-vault-unit.test.ts +19 -19
  104. package/src/__tests__/credential-vault.test.ts +5 -5
  105. package/src/__tests__/cross-provider-web-search.test.ts +56 -2
  106. package/src/__tests__/db-connection-isolation.test.ts +7 -6
  107. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
  108. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
  109. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
  110. package/src/__tests__/db-test-helpers.ts +58 -0
  111. package/src/__tests__/disk-pressure-guard.test.ts +58 -41
  112. package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
  113. package/src/__tests__/disk-pressure-routes.test.ts +0 -33
  114. package/src/__tests__/disk-pressure-tools.test.ts +0 -4
  115. package/src/__tests__/dm-persistence.test.ts +26 -40
  116. package/src/__tests__/document-create-dedupe.test.ts +189 -0
  117. package/src/__tests__/document-find-replace.test.ts +3 -2
  118. package/src/__tests__/document-tool-security.test.ts +81 -2
  119. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
  120. package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
  121. package/src/__tests__/encrypted-store.test.ts +11 -9
  122. package/src/__tests__/feature-flag-test-helpers.ts +53 -0
  123. package/src/__tests__/filing-service.test.ts +1 -0
  124. package/src/__tests__/first-greeting.test.ts +62 -12
  125. package/src/__tests__/gateway-flag-listener.test.ts +0 -1
  126. package/src/__tests__/gemini-provider.test.ts +26 -0
  127. package/src/__tests__/guardian-action-sweep.test.ts +3 -2
  128. package/src/__tests__/guardian-outbound-http.test.ts +3 -2
  129. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
  130. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -0
  131. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  132. package/src/__tests__/heartbeat-service.test.ts +1 -0
  133. package/src/__tests__/helpers/mock-logger.ts +26 -0
  134. package/src/__tests__/host-bash-routes.test.ts +1 -0
  135. package/src/__tests__/host-cu-routes-targeted.test.ts +1 -0
  136. package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
  137. package/src/__tests__/host-shell-tool.test.ts +5 -4
  138. package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
  139. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  140. package/src/__tests__/http-user-message-parity.test.ts +29 -7
  141. package/src/__tests__/identity-intro-cache.test.ts +133 -22
  142. package/src/__tests__/inbound-slack-persistence.test.ts +44 -72
  143. package/src/__tests__/inference-profile-reaper.test.ts +3 -2
  144. package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
  145. package/src/__tests__/injector-disk-pressure.test.ts +3 -17
  146. package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
  147. package/src/__tests__/list-messages-hidden-metadata.test.ts +80 -0
  148. package/src/__tests__/llm-context-normalization.test.ts +42 -0
  149. package/src/__tests__/llm-resolver.test.ts +331 -0
  150. package/src/__tests__/llm-schema.test.ts +1 -1
  151. package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
  152. package/src/__tests__/mcp-abort-signal.test.ts +14 -0
  153. package/src/__tests__/mcp-client-auth.test.ts +14 -0
  154. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  155. package/src/__tests__/migration-import-from-url.test.ts +3 -3
  156. package/src/__tests__/mock-gateway-ipc.ts +18 -2
  157. package/src/__tests__/model-intents.test.ts +3 -3
  158. package/src/__tests__/native-web-search.test.ts +30 -2
  159. package/src/__tests__/notification-deep-link.test.ts +62 -0
  160. package/src/__tests__/oauth-commands-routes.test.ts +37 -0
  161. package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
  162. package/src/__tests__/oauth-store.test.ts +3 -2
  163. package/src/__tests__/onboarding-template-contract.test.ts +3 -2
  164. package/src/__tests__/openai-provider.test.ts +8 -9
  165. package/src/__tests__/openai-responses-provider.test.ts +70 -10
  166. package/src/__tests__/openrouter-provider-only.test.ts +27 -5
  167. package/src/__tests__/outbound-slack-persistence.test.ts +46 -1
  168. package/src/__tests__/persistence-pipeline.test.ts +139 -1
  169. package/src/__tests__/persistence-secret-redaction.test.ts +83 -12
  170. package/src/__tests__/plugin-bootstrap.test.ts +9 -11
  171. package/src/__tests__/plugin-tool-contribution.test.ts +41 -38
  172. package/src/__tests__/process-message-background-slack.test.ts +21 -16
  173. package/src/__tests__/process-message-display-content.test.ts +19 -22
  174. package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
  175. package/src/__tests__/provider-platform-proxy-integration.test.ts +216 -4
  176. package/src/__tests__/provider-registry-ollama.test.ts +45 -22
  177. package/src/__tests__/recording-handler.test.ts +1 -0
  178. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  179. package/src/__tests__/registry.test.ts +82 -76
  180. package/src/__tests__/relay-server.test.ts +10 -10
  181. package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
  182. package/src/__tests__/schedule-store.test.ts +16 -1
  183. package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
  184. package/src/__tests__/secret-ingress-http.test.ts +5 -1
  185. package/src/__tests__/secure-keys.test.ts +3 -3
  186. package/src/__tests__/send-endpoint-busy.test.ts +81 -42
  187. package/src/__tests__/server-history-render.test.ts +4 -1
  188. package/src/__tests__/skill-feature-flags-integration.test.ts +8 -10
  189. package/src/__tests__/skill-feature-flags.test.ts +14 -16
  190. package/src/__tests__/skill-load-feature-flag.test.ts +5 -5
  191. package/src/__tests__/skill-projection-feature-flag.test.ts +44 -30
  192. package/src/__tests__/skill-projection.benchmark.test.ts +5 -7
  193. package/src/__tests__/skill-tool-factory.test.ts +96 -95
  194. package/src/__tests__/slack-channel-config.test.ts +3 -3
  195. package/src/__tests__/subagent-call-site-routing.test.ts +11 -3
  196. package/src/__tests__/subagent-disposal.test.ts +27 -8
  197. package/src/__tests__/subagent-fork-notifications.test.ts +24 -9
  198. package/src/__tests__/subagent-fork-spawn.test.ts +13 -4
  199. package/src/__tests__/subagent-manager-notify.test.ts +20 -8
  200. package/src/__tests__/subagent-notify-parent.test.ts +5 -4
  201. package/src/__tests__/subagent-spawn-tool-fork.test.ts +58 -0
  202. package/src/__tests__/subagent-tools.test.ts +2 -1
  203. package/src/__tests__/suggestion-routes.test.ts +1 -0
  204. package/src/__tests__/system-prompt.test.ts +38 -0
  205. package/src/__tests__/test-preload-verifier.ts +68 -0
  206. package/src/__tests__/test-preload.ts +32 -39
  207. package/src/__tests__/tool-executor-lifecycle-events.test.ts +20 -7
  208. package/src/__tests__/tool-executor.test.ts +55 -10
  209. package/src/__tests__/tool-preview-lifecycle.test.ts +1 -0
  210. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  211. package/src/__tests__/twilio-routes.test.ts +3 -2
  212. package/src/__tests__/validate-input.test.ts +381 -0
  213. package/src/__tests__/verification-control-plane-policy.test.ts +1 -0
  214. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -1
  215. package/src/__tests__/voice-session-bridge.test.ts +37 -28
  216. package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
  217. package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
  218. package/src/acp/session-manager.ts +5 -6
  219. package/src/agent/loop.ts +80 -0
  220. package/src/api/README.md +124 -2
  221. package/src/api/constants/call-sites.ts +27 -0
  222. package/src/api/events/assistant-outbound-attachment.ts +51 -0
  223. package/src/api/events/assistant-text-delta.ts +32 -0
  224. package/src/api/events/assistant-turn-start.ts +33 -0
  225. package/src/api/events/document-comment-created.ts +48 -0
  226. package/src/api/events/document-comment-deleted.ts +24 -0
  227. package/src/api/events/document-comment-reopened.ts +25 -0
  228. package/src/api/events/document-comment-resolved.ts +27 -0
  229. package/src/api/events/generation-cancelled.ts +24 -0
  230. package/src/api/events/generation-handoff.ts +41 -0
  231. package/src/api/events/message-complete.ts +42 -0
  232. package/src/api/events/open-url.ts +30 -0
  233. package/src/{events → api/events}/relationship-state-updated.ts +3 -3
  234. package/src/api/events/tool-use-start.ts +32 -0
  235. package/src/api/index.ts +128 -3
  236. package/src/api/responses/llm-context-response.ts +39 -0
  237. package/src/api/responses/llm-request-log-entry.ts +93 -0
  238. package/src/api/responses/memory-recall-log.ts +65 -0
  239. package/src/api/responses/memory-v2-activation-log.ts +78 -0
  240. package/src/background-wake/background-wake-routes.test.ts +687 -52
  241. package/src/background-wake/platform-client.test.ts +308 -0
  242. package/src/background-wake/platform-client.ts +167 -0
  243. package/src/background-wake/publisher.ts +91 -0
  244. package/src/background-wake/runtime-registry.ts +2 -2
  245. package/src/background-wake/wake-intent-hooks.test.ts +282 -0
  246. package/src/calls/guardian-dispatch.ts +1 -0
  247. package/src/calls/voice-session-bridge.ts +4 -4
  248. package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
  249. package/src/cli/commands/__tests__/notifications.test.ts +184 -40
  250. package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
  251. package/src/cli/commands/channels/index.ts +229 -0
  252. package/src/cli/commands/memory-v3-render.ts +147 -0
  253. package/src/cli/commands/memory-v3.ts +255 -4
  254. package/src/cli/commands/notifications.ts +365 -55
  255. package/src/cli/lib/open-browser.ts +7 -2
  256. package/src/cli/program.ts +2 -0
  257. package/src/config/assistant-feature-flags.ts +23 -42
  258. package/src/config/bundled-skills/document-editor/SKILL.md +5 -1
  259. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  260. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  261. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
  262. package/src/config/call-site-defaults.ts +1 -1
  263. package/src/config/feature-flag-cache.ts +86 -0
  264. package/src/config/feature-flag-registry.json +17 -17
  265. package/src/config/llm-context-resolution.ts +10 -1
  266. package/src/config/llm-resolver.ts +121 -15
  267. package/src/config/loader.ts +4 -5
  268. package/src/config/schemas/__tests__/memory-v2.test.ts +15 -0
  269. package/src/config/schemas/heartbeat.ts +1 -1
  270. package/src/config/schemas/llm.ts +90 -1
  271. package/src/config/schemas/memory-v2.ts +26 -0
  272. package/src/config/schemas/services.ts +6 -2
  273. package/src/config/seed-inference-profiles.ts +36 -16
  274. package/src/context/token-estimator.ts +10 -5
  275. package/src/credential-execution/executable-discovery.ts +40 -0
  276. package/src/credential-execution/process-manager.ts +6 -2
  277. package/src/credential-health/credential-health-service.ts +125 -40
  278. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -6
  279. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +13 -15
  280. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -2
  281. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
  282. package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
  283. package/src/daemon/__tests__/native-web-search-metadata.test.ts +1 -0
  284. package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
  285. package/src/daemon/__tests__/web-search-status-text.test.ts +1 -0
  286. package/src/daemon/conversation-agent-loop-handlers.ts +389 -68
  287. package/src/daemon/conversation-agent-loop.ts +132 -28
  288. package/src/daemon/conversation-error.ts +33 -5
  289. package/src/daemon/conversation-messaging.ts +84 -43
  290. package/src/daemon/conversation-process.ts +74 -37
  291. package/src/daemon/conversation-runtime-assembly.ts +29 -9
  292. package/src/daemon/conversation-skill-tools.ts +14 -30
  293. package/src/daemon/conversation-surfaces.ts +69 -34
  294. package/src/daemon/conversation-tool-setup.ts +33 -48
  295. package/src/daemon/conversation.ts +26 -46
  296. package/src/daemon/daemon-control.ts +1 -1
  297. package/src/daemon/daemon-skill-host.ts +9 -2
  298. package/src/daemon/disk-pressure-guard.ts +27 -29
  299. package/src/daemon/first-greeting.ts +31 -13
  300. package/src/daemon/handlers/shared.ts +6 -1
  301. package/src/daemon/lifecycle.ts +12 -12
  302. package/src/daemon/mcp-reload-service.ts +1 -1
  303. package/src/daemon/meet-manifest-loader.ts +10 -17
  304. package/src/daemon/message-types/conversations.ts +20 -22
  305. package/src/daemon/message-types/document-comments.ts +8 -44
  306. package/src/daemon/message-types/home.ts +2 -2
  307. package/src/daemon/message-types/integrations.ts +2 -7
  308. package/src/daemon/message-types/messages.ts +23 -38
  309. package/src/daemon/message-types/subagents.ts +6 -0
  310. package/src/daemon/process-message.ts +9 -9
  311. package/src/daemon/providers-setup.ts +1 -1
  312. package/src/daemon/server.ts +16 -0
  313. package/src/daemon/switch-inference-profile-tool.ts +13 -3
  314. package/src/daemon/tool-setup-types.ts +0 -6
  315. package/src/daemon/wake-target-adapter.ts +10 -0
  316. package/src/documents/document-store.ts +38 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +1 -0
  318. package/src/heartbeat/__tests__/heartbeat-service.test.ts +29 -0
  319. package/src/heartbeat/heartbeat-service.ts +63 -0
  320. package/src/home/__tests__/feed-writer.test.ts +161 -0
  321. package/src/home/__tests__/post-connect-feed.test.ts +1 -0
  322. package/src/home/__tests__/suggested-prompts.test.ts +55 -59
  323. package/src/home/feed-writer.ts +146 -7
  324. package/src/home/suggested-prompts.ts +27 -145
  325. package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
  326. package/src/ipc/gateway-client.test.ts +4 -1
  327. package/src/ipc/skill-routes/__tests__/memory.test.ts +1 -0
  328. package/src/ipc/skill-routes/__tests__/registries.test.ts +36 -7
  329. package/src/ipc/skill-routes/memory.ts +4 -3
  330. package/src/ipc/skill-routes/registries.ts +28 -29
  331. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +1 -0
  332. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +26 -5
  333. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
  334. package/src/memory/__tests__/memory-retrospective-job.test.ts +1 -0
  335. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
  336. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
  337. package/src/memory/conversation-attention-store.ts +17 -3
  338. package/src/memory/conversation-crud.ts +352 -112
  339. package/src/memory/db-connection.ts +29 -19
  340. package/src/memory/db-init.ts +4 -0
  341. package/src/memory/db-singleton.ts +77 -0
  342. package/src/memory/delivery-channels.ts +82 -0
  343. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
  344. package/src/memory/graph/retriever.test.ts +3 -3
  345. package/src/memory/job-handlers/embedding.test.ts +3 -2
  346. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
  347. package/src/memory/jobs-worker.ts +12 -1
  348. package/src/memory/llm-request-log-source-clickhouse.ts +80 -0
  349. package/src/memory/llm-request-log-source-local.ts +24 -0
  350. package/src/memory/llm-request-log-source.ts +31 -0
  351. package/src/memory/llm-request-log-store.ts +188 -3
  352. package/src/memory/memory-v2-activation-log-store.ts +95 -1
  353. package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
  354. package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
  355. package/src/memory/migrations/index.ts +2 -0
  356. package/src/memory/schema/conversations.ts +9 -1
  357. package/src/memory/schema/inference.ts +0 -1
  358. package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
  359. package/src/memory/v2/__tests__/harness-metrics.test.ts +9 -0
  360. package/src/memory/v2/__tests__/harness-replay-input.test.ts +9 -4
  361. package/src/memory/v2/__tests__/harness-runner.test.ts +26 -0
  362. package/src/memory/v2/__tests__/sweep-job.test.ts +6 -3
  363. package/src/memory/v2/harness/metrics.ts +5 -1
  364. package/src/memory/v2/harness/replay-input.ts +19 -3
  365. package/src/memory/v2/harness/runner.ts +6 -0
  366. package/src/memory/v2/harness/trace.ts +6 -0
  367. package/src/memory/v3/__tests__/consolidation-job.test.ts +2 -4
  368. package/src/memory/v3/__tests__/coretrieval-seed.test.ts +270 -0
  369. package/src/memory/v3/__tests__/edges.test.ts +144 -1
  370. package/src/memory/v3/__tests__/filter.test.ts +48 -0
  371. package/src/memory/v3/__tests__/gate.test.ts +96 -33
  372. package/src/memory/v3/__tests__/index-composition.test.ts +58 -0
  373. package/src/memory/v3/__tests__/loop.test.ts +250 -5
  374. package/src/memory/v3/__tests__/scouts.test.ts +49 -0
  375. package/src/memory/v3/__tests__/shadow-diff.test.ts +225 -0
  376. package/src/memory/v3/__tests__/shadow-middleware.test.ts +88 -2
  377. package/src/memory/v3/__tests__/traversal.test.ts +39 -0
  378. package/src/memory/v3/__tests__/tree-walk.test.ts +77 -0
  379. package/src/memory/v3/__tests__/validate.test.ts +32 -0
  380. package/src/memory/v3/coretrieval-seed.ts +240 -0
  381. package/src/memory/v3/edges.ts +58 -21
  382. package/src/memory/v3/filter.ts +27 -22
  383. package/src/memory/v3/gate.ts +51 -36
  384. package/src/memory/v3/index-composition.ts +18 -5
  385. package/src/memory/v3/loop.ts +65 -17
  386. package/src/memory/v3/scouts.ts +15 -4
  387. package/src/memory/v3/shadow-diff.ts +287 -0
  388. package/src/memory/v3/shadow-middleware.ts +44 -2
  389. package/src/memory/v3/traversal.ts +6 -1
  390. package/src/memory/v3/tree-walk.ts +6 -1
  391. package/src/memory/v3/validate.ts +56 -33
  392. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  393. package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
  394. package/src/notifications/adapters/slack.ts +45 -11
  395. package/src/notifications/broadcaster.ts +114 -63
  396. package/src/notifications/conversation-pairing.ts +23 -3
  397. package/src/notifications/decisions-store.ts +32 -1
  398. package/src/notifications/deliveries-store.ts +45 -0
  399. package/src/notifications/edit-notification.ts +201 -0
  400. package/src/notifications/emit-signal.ts +11 -1
  401. package/src/notifications/signal.ts +10 -0
  402. package/src/notifications/types.ts +37 -0
  403. package/src/oauth/byo-connection.test.ts +67 -3
  404. package/src/oauth/byo-connection.ts +32 -5
  405. package/src/oauth/connect-orchestrator.ts +9 -0
  406. package/src/oauth/connection-resolver.test.ts +76 -0
  407. package/src/oauth/connection-resolver.ts +49 -10
  408. package/src/oauth/manual-token-connection.ts +51 -3
  409. package/src/oauth/seed-providers.ts +3 -0
  410. package/src/permissions/approval-policy.test.ts +19 -5
  411. package/src/permissions/approval-policy.ts +14 -3
  412. package/src/permissions/checker.ts +21 -8
  413. package/src/platform/client.test.ts +24 -1
  414. package/src/platform/client.ts +8 -0
  415. package/src/platform/feature-gate.ts +15 -0
  416. package/src/plugins/defaults/injectors.ts +2 -8
  417. package/src/plugins/defaults/persistence.ts +25 -6
  418. package/src/plugins/types.ts +57 -13
  419. package/src/proactive-artifact/job.test.ts +1 -0
  420. package/src/prompts/__tests__/system-prompt.test.ts +4 -4
  421. package/src/prompts/system-prompt.ts +38 -40
  422. package/src/prompts/template-detection.ts +10 -4
  423. package/src/prompts/templates/BOOTSTRAP.md +7 -11
  424. package/src/prompts/templates/IDENTITY.md +0 -2
  425. package/src/providers/__tests__/connection-model-compat.test.ts +3 -4
  426. package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
  427. package/src/providers/call-site-routing.ts +33 -9
  428. package/src/providers/connection-model-compat.ts +23 -0
  429. package/src/providers/connection-resolution.ts +39 -20
  430. package/src/providers/fireworks/client.ts +1 -0
  431. package/src/providers/gemini/client.ts +24 -3
  432. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
  433. package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
  434. package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
  435. package/src/providers/inference/auth.ts +0 -8
  436. package/src/providers/inference/connections.ts +3 -66
  437. package/src/providers/inference/resolve-auth.ts +2 -3
  438. package/src/providers/model-catalog.ts +35 -1
  439. package/src/providers/model-intents.ts +3 -3
  440. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  441. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +157 -5
  442. package/src/providers/openai/chat-completions-provider.ts +110 -12
  443. package/src/providers/openai/codex-models.ts +2 -0
  444. package/src/providers/openai/responses-provider.ts +53 -53
  445. package/src/providers/openrouter/client.ts +13 -8
  446. package/src/providers/provider-send-message.ts +18 -9
  447. package/src/providers/registry.ts +48 -8
  448. package/src/providers/retry.ts +16 -4
  449. package/src/providers/search-provider-catalog.ts +17 -9
  450. package/src/providers/types.ts +9 -0
  451. package/src/runtime/__tests__/agent-wake.test.ts +1 -0
  452. package/src/runtime/__tests__/background-job-runner.test.ts +1 -0
  453. package/src/runtime/access-request-helper.ts +1 -0
  454. package/src/runtime/auth/route-policy.ts +10 -0
  455. package/src/runtime/channel-readiness-service.ts +68 -0
  456. package/src/runtime/channel-reply-delivery.ts +23 -0
  457. package/src/runtime/channel-retry-sweep.ts +47 -14
  458. package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
  459. package/src/runtime/migrations/vbundle-builder.ts +3 -2
  460. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
  461. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +406 -0
  462. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  463. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  464. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
  465. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +13 -50
  466. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +51 -3
  467. package/src/runtime/routes/__tests__/memory-v3-simulate-params.test.ts +35 -0
  468. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
  469. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +294 -0
  470. package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
  471. package/src/runtime/routes/acp-routes-list.test.ts +3 -0
  472. package/src/runtime/routes/app-management-routes.ts +111 -4
  473. package/src/runtime/routes/background-wake-routes.ts +188 -20
  474. package/src/runtime/routes/btw-routes.ts +4 -4
  475. package/src/runtime/routes/conversation-analysis-routes.ts +6 -0
  476. package/src/runtime/routes/conversation-compaction-routes.ts +263 -0
  477. package/src/runtime/routes/conversation-list-routes.ts +147 -0
  478. package/src/runtime/routes/conversation-management-routes.ts +39 -14
  479. package/src/runtime/routes/conversation-query-routes.ts +60 -10
  480. package/src/runtime/routes/conversation-routes.ts +186 -140
  481. package/src/runtime/routes/conversations-import-routes.ts +19 -6
  482. package/src/runtime/routes/documents-routes.ts +10 -1
  483. package/src/runtime/routes/group-routes.ts +11 -0
  484. package/src/runtime/routes/home-feed-routes.ts +129 -0
  485. package/src/runtime/routes/identity-intro-cache.ts +61 -16
  486. package/src/runtime/routes/identity-routes.ts +30 -9
  487. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +530 -6
  488. package/src/runtime/routes/inbound-stages/background-dispatch.ts +57 -8
  489. package/src/runtime/routes/index.ts +2 -0
  490. package/src/runtime/routes/inference-provider-connection-routes.ts +5 -26
  491. package/src/runtime/routes/integrations/vercel.ts +15 -0
  492. package/src/runtime/routes/llm-context-normalization.ts +7 -2
  493. package/src/runtime/routes/memory-v3-routes.ts +160 -2
  494. package/src/runtime/routes/migration-routes.ts +20 -13
  495. package/src/runtime/routes/notification-routes.ts +63 -1
  496. package/src/runtime/routes/oauth-commands-routes.ts +6 -1
  497. package/src/runtime/routes/surface-action-routes.ts +1 -38
  498. package/src/runtime/routes/surface-content-routes.ts +12 -5
  499. package/src/runtime/routes/surface-conversation-resolver.ts +65 -0
  500. package/src/runtime/routes/wipe-conversation-routes.ts +3 -0
  501. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -0
  502. package/src/runtime/slack-dm-text-delivery.ts +177 -0
  503. package/src/runtime/sync/resource-sync-events.ts +1 -1
  504. package/src/runtime/tool-grant-request-helper.ts +1 -0
  505. package/src/schedule/schedule-store.ts +8 -1
  506. package/src/schedule/scheduler.ts +111 -15
  507. package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
  508. package/src/security/encrypted-store.ts +7 -16
  509. package/src/security/store-path-override.ts +61 -0
  510. package/src/signals/user-message.ts +5 -8
  511. package/src/skills/validate-input.ts +177 -0
  512. package/src/subagent/manager.ts +13 -13
  513. package/src/subagent/types.ts +6 -0
  514. package/src/tasks/tool-sanitizer.ts +2 -2
  515. package/src/tools/apps/definitions.ts +35 -21
  516. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
  517. package/src/tools/computer-use/definitions.ts +268 -266
  518. package/src/tools/document/document-tool.ts +131 -8
  519. package/src/tools/execution-target.ts +2 -5
  520. package/src/tools/executor.ts +18 -55
  521. package/src/tools/host-filesystem/edit.test.ts +1 -0
  522. package/src/tools/host-filesystem/read.test.ts +1 -0
  523. package/src/tools/host-filesystem/transfer.test.ts +31 -6
  524. package/src/tools/host-filesystem/write.test.ts +1 -0
  525. package/src/tools/mcp/mcp-tool-factory.ts +0 -2
  526. package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
  527. package/src/tools/network/__tests__/web-search.test.ts +211 -3
  528. package/src/tools/network/managed-search-proxy.ts +183 -0
  529. package/src/tools/network/web-search.ts +199 -44
  530. package/src/tools/policy-context.ts +3 -1
  531. package/src/tools/registry.ts +146 -103
  532. package/src/tools/schedule/create.ts +1 -1
  533. package/src/tools/skills/skill-tool-factory.ts +17 -36
  534. package/src/tools/subagent/spawn.ts +3 -0
  535. package/src/tools/tool-approval-handler.ts +10 -4
  536. package/src/tools/tool-name-aliases.ts +72 -14
  537. package/src/tools/types.ts +17 -15
  538. package/src/tools/ui-surface/definitions.ts +98 -86
  539. package/src/types/onboarding-context.ts +6 -0
  540. package/src/usage/attribution.ts +32 -1
  541. package/src/util/browser.ts +7 -2
  542. package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
  543. package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
  544. package/src/workspace/migrations/registry.ts +4 -0
@@ -109,7 +109,7 @@ export function projectAssistantMessage(params: {
109
109
  conversationId: string;
110
110
  messageId: string;
111
111
  messageAt: number;
112
- }): void {
112
+ }): boolean {
113
113
  const { conversationId, messageId, messageAt } = params;
114
114
  const db = getDb();
115
115
  const now = Date.now();
@@ -140,7 +140,7 @@ export function projectAssistantMessage(params: {
140
140
  updatedAt: now,
141
141
  })
142
142
  .run();
143
- return;
143
+ return true;
144
144
  }
145
145
 
146
146
  // Monotonic: only advance if the new message is strictly later
@@ -148,9 +148,21 @@ export function projectAssistantMessage(params: {
148
148
  existing.latestAssistantMessageAt != null &&
149
149
  messageAt <= existing.latestAssistantMessageAt
150
150
  ) {
151
- return;
151
+ return false;
152
152
  }
153
153
 
154
+ // Determine whether the conversation was previously in a "seen" state.
155
+ // Two cases count as seen:
156
+ // 1. latestAssistantMessageAt is null — no prior assistant message existed,
157
+ // so there was nothing unseen. The first assistant message transitions
158
+ // the conversation to unseen.
159
+ // 2. lastSeenAssistantMessageAt >= latestAssistantMessageAt — the user saw
160
+ // the most recent assistant message.
161
+ const wasSeen =
162
+ existing.latestAssistantMessageAt == null ||
163
+ (existing.lastSeenAssistantMessageAt != null &&
164
+ existing.lastSeenAssistantMessageAt >= existing.latestAssistantMessageAt);
165
+
154
166
  db.update(conversationAssistantAttentionState)
155
167
  .set({
156
168
  latestAssistantMessageId: messageId,
@@ -161,6 +173,8 @@ export function projectAssistantMessage(params: {
161
173
  eq(conversationAssistantAttentionState.conversationId, conversationId),
162
174
  )
163
175
  .run();
176
+
177
+ return wasSeen;
164
178
  }
165
179
 
166
180
  /**
@@ -25,7 +25,10 @@ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
25
25
  import { CHANNEL_IDS, isChannelId } from "../channels/types.js";
26
26
  import { getConfig } from "../config/loader.js";
27
27
  import { findDisplayTurnEndIndex } from "../conversations/message-consolidation.js";
28
+ import { conversationMetadataSyncTag } from "../daemon/message-types/sync.js";
28
29
  import type { TrustContext } from "../daemon/trust-context.js";
30
+ import { clearAllConversationIds } from "../home/feed-writer.js";
31
+ import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
29
32
  import { UserError } from "../util/errors.js";
30
33
  import { safeParseRecord } from "../util/json.js";
31
34
  import { getLogger } from "../util/logger.js";
@@ -181,6 +184,24 @@ export function provenanceFromTrustContext(
181
184
  };
182
185
  }
183
186
 
187
+ /** Extract image file paths from resolved attachments for message metadata. */
188
+ export function extractImageSourcePaths(
189
+ attachments: ReadonlyArray<{
190
+ filename: string;
191
+ mimeType: string;
192
+ filePath?: string;
193
+ }>,
194
+ ): Record<string, string> | undefined {
195
+ const paths: Record<string, string> = {};
196
+ for (let i = 0; i < attachments.length; i++) {
197
+ const a = attachments[i];
198
+ if (a.filePath && a.mimeType.toLowerCase().startsWith("image/")) {
199
+ paths[`${i}:${a.filename}`] = a.filePath;
200
+ }
201
+ }
202
+ return Object.keys(paths).length > 0 ? paths : undefined;
203
+ }
204
+
184
205
  export interface ConversationRow {
185
206
  id: string;
186
207
  title: string | null;
@@ -246,6 +267,9 @@ export const parseConversation = createRowMapper<
246
267
  lastNotifiedInferenceProfile: "lastNotifiedInferenceProfile",
247
268
  });
248
269
 
270
+ /** Allowed values for the `role` column on `messages`. */
271
+ export type MessageRole = "user" | "assistant" | "system";
272
+
249
273
  export interface MessageRow {
250
274
  id: string;
251
275
  conversationId: string;
@@ -280,6 +304,193 @@ function monotonicNow(): number {
280
304
  return lastTimestamp;
281
305
  }
282
306
 
307
+ // ── insertMessageCore ─────────────────────────────────────────────────
308
+
309
+ /** Shape returned by {@link insertMessageCore} and its public wrappers. */
310
+ interface InsertedMessage {
311
+ id: string;
312
+ conversationId: string;
313
+ role: MessageRole;
314
+ content: string;
315
+ createdAt: number;
316
+ metadata?: string;
317
+ clientMessageId?: string;
318
+ deduplicated: boolean;
319
+ }
320
+
321
+ interface InsertMessageCoreParams {
322
+ conversationId: string;
323
+ role: MessageRole;
324
+ content: string;
325
+ metadata?: Record<string, unknown>;
326
+ clientMessageId?: string;
327
+ }
328
+
329
+ /**
330
+ * Core message persistence primitive shared by {@link addMessage} and
331
+ * {@link reserveMessage}.
332
+ *
333
+ * Inserts a message row inside a transaction that atomically bumps the
334
+ * parent conversation's `updatedAt` / `lastMessageAt` timestamps and
335
+ * conditionally sets the conversation's `originChannel` when the first
336
+ * channel-originated message arrives.
337
+ *
338
+ * When a `clientMessageId` is provided the insert runs inside a
339
+ * SAVEPOINT. If the partial unique index on
340
+ * `(conversation_id, client_message_id)` raises
341
+ * `SQLITE_CONSTRAINT_UNIQUE`, the SAVEPOINT is rolled back, the
342
+ * existing row is fetched, and returned with `deduplicated: true`.
343
+ * This makes the operation idempotent for client-generated
344
+ * correlation nonces.
345
+ *
346
+ * Retries up to 3 times on `SQLITE_BUSY*` / `SQLITE_IOERR*` to handle
347
+ * WAL contention. The timestamp is recomputed each attempt so a late
348
+ * retry doesn't persist a stale `updatedAt`.
349
+ */
350
+ async function insertMessageCore(
351
+ params: InsertMessageCoreParams,
352
+ ): Promise<InsertedMessage> {
353
+ const { conversationId, role, content, metadata, clientMessageId } = params;
354
+ const db = getDb();
355
+ const messageId = uuid();
356
+
357
+ if (metadata) {
358
+ const result = messageMetadataSchema.safeParse(metadata);
359
+ if (!result.success) {
360
+ log.warn(
361
+ { conversationId, messageId, issues: result.error.issues },
362
+ "Invalid message metadata, storing as-is",
363
+ );
364
+ }
365
+ }
366
+
367
+ const metadataStr = metadata ? JSON.stringify(metadata) : undefined;
368
+ const originChannelCandidate =
369
+ metadata && isChannelId(metadata.userMessageChannel)
370
+ ? metadata.userMessageChannel
371
+ : null;
372
+
373
+ const MAX_RETRIES = 3;
374
+ let now!: number;
375
+ for (let attempt = 0; ; attempt++) {
376
+ now = monotonicNow();
377
+ try {
378
+ const values = {
379
+ id: messageId,
380
+ conversationId,
381
+ role,
382
+ content,
383
+ createdAt: now,
384
+ ...(metadataStr ? { metadata: metadataStr } : {}),
385
+ ...(clientMessageId ? { clientMessageId } : {}),
386
+ };
387
+
388
+ if (clientMessageId) {
389
+ // Idempotent insert: skip silently if this clientMessageId was
390
+ // already persisted for the conversation.
391
+ const raw = getSqliteFrom(db);
392
+ raw.exec("SAVEPOINT insert_msg");
393
+ try {
394
+ db.insert(messages).values(values).run();
395
+ if (originChannelCandidate) {
396
+ db.update(conversations)
397
+ .set({ originChannel: originChannelCandidate })
398
+ .where(
399
+ and(
400
+ eq(conversations.id, conversationId),
401
+ isNull(conversations.originChannel),
402
+ ),
403
+ )
404
+ .run();
405
+ }
406
+ db.update(conversations)
407
+ .set({ updatedAt: now, lastMessageAt: now })
408
+ .where(eq(conversations.id, conversationId))
409
+ .run();
410
+ raw.exec("RELEASE insert_msg");
411
+ } catch (insertErr) {
412
+ raw.exec("ROLLBACK TO insert_msg");
413
+ raw.exec("RELEASE insert_msg");
414
+ const code = (insertErr as { code?: string }).code ?? "";
415
+ if (code === "SQLITE_CONSTRAINT_UNIQUE") {
416
+ // Duplicate clientMessageId — return the existing row.
417
+ const existing = db
418
+ .select()
419
+ .from(messages)
420
+ .where(
421
+ and(
422
+ eq(messages.conversationId, conversationId),
423
+ eq(messages.clientMessageId, clientMessageId),
424
+ ),
425
+ )
426
+ .get();
427
+ if (existing) {
428
+ return {
429
+ id: existing.id,
430
+ conversationId: existing.conversationId,
431
+ role: existing.role as MessageRole,
432
+ content: existing.content,
433
+ createdAt: existing.createdAt,
434
+ ...(existing.metadata ? { metadata: existing.metadata } : {}),
435
+ clientMessageId: existing.clientMessageId ?? undefined,
436
+ deduplicated: true,
437
+ };
438
+ }
439
+ }
440
+ throw insertErr;
441
+ }
442
+ } else {
443
+ // No clientMessageId — standard insert inside a transaction.
444
+ db.transaction((tx) => {
445
+ tx.insert(messages).values(values).run();
446
+ if (originChannelCandidate) {
447
+ tx.update(conversations)
448
+ .set({ originChannel: originChannelCandidate })
449
+ .where(
450
+ and(
451
+ eq(conversations.id, conversationId),
452
+ isNull(conversations.originChannel),
453
+ ),
454
+ )
455
+ .run();
456
+ }
457
+ tx.update(conversations)
458
+ .set({ updatedAt: now, lastMessageAt: now })
459
+ .where(eq(conversations.id, conversationId))
460
+ .run();
461
+ });
462
+ }
463
+ break;
464
+ } catch (err) {
465
+ const errCode = (err as { code?: string }).code ?? "";
466
+ if (
467
+ attempt < MAX_RETRIES &&
468
+ (errCode.startsWith("SQLITE_BUSY") ||
469
+ errCode.startsWith("SQLITE_IOERR"))
470
+ ) {
471
+ log.warn(
472
+ { attempt, conversationId, code: errCode },
473
+ "insertMessageCore: transient SQLite error, retrying",
474
+ );
475
+ await Bun.sleep(50 * (attempt + 1));
476
+ continue;
477
+ }
478
+ throw err;
479
+ }
480
+ }
481
+
482
+ return {
483
+ id: messageId,
484
+ conversationId,
485
+ role,
486
+ content,
487
+ createdAt: now,
488
+ ...(metadataStr ? { metadata: metadataStr } : {}),
489
+ ...(clientMessageId ? { clientMessageId } : {}),
490
+ deduplicated: false,
491
+ };
492
+ }
493
+
283
494
  export function createConversation(
284
495
  titleOrOpts?:
285
496
  | string
@@ -576,6 +787,20 @@ export function forkConversation(params: {
576
787
  throw new UserError(`Conversation ${conversationId} not found`);
577
788
  }
578
789
  const sourceMessages = getMessages(conversationId);
790
+ if (throughMessageId != null) {
791
+ // `getMessages` orders by `createdAt` only; when rows share an identical
792
+ // millisecond timestamp the tie order is unspecified. Callers that pin the
793
+ // fork to a cutoff choose it from a `(createdAt, id)` cursor (e.g. the
794
+ // memory-retrospective job, via `getMessagesAfter`), so slicing through
795
+ // `throughMessageId` under the unstable order could include same-timestamp
796
+ // siblings the cursor considers *after* the cutoff (reprocessed next run)
797
+ // or exclude ones it considers *before* it (skipped forever). Re-sort on
798
+ // `(createdAt, id)` so the slice agrees with the cutoff. The unpinned full
799
+ // fork copies every row regardless of order, so it keeps source order.
800
+ sourceMessages.sort(
801
+ (a, b) => a.createdAt - b.createdAt || a.id.localeCompare(b.id),
802
+ );
803
+ }
579
804
 
580
805
  if (sourceMessages.length === 0) {
581
806
  throw new UserError(
@@ -970,92 +1195,36 @@ export function wipeConversation(id: string): WipeConversationResult {
970
1195
  };
971
1196
  }
972
1197
 
1198
+ /**
1199
+ * Persist a message and run post-insert side effects (memory indexing,
1200
+ * attention projection). Delegates the core insert + retry logic to
1201
+ * {@link insertMessageCore}.
1202
+ *
1203
+ * @param clientMessageId Optional client-generated nonce. When
1204
+ * provided, duplicate inserts for the same `(conversationId,
1205
+ * clientMessageId)` pair are silently skipped (idempotent).
1206
+ */
973
1207
  export async function addMessage(
974
1208
  conversationId: string,
975
- role: string,
1209
+ role: MessageRole,
976
1210
  content: string,
977
1211
  metadata?: Record<string, unknown>,
978
1212
  opts?: { skipIndexing?: boolean },
1213
+ clientMessageId?: string,
979
1214
  ) {
980
- const db = getDb();
981
- const messageId = uuid();
982
-
983
- if (metadata) {
984
- const result = messageMetadataSchema.safeParse(metadata);
985
- if (!result.success) {
986
- log.warn(
987
- { conversationId, messageId, issues: result.error.issues },
988
- "Invalid message metadata, storing as-is",
989
- );
990
- }
991
- }
992
-
993
- const metadataStr = metadata ? JSON.stringify(metadata) : undefined;
994
- const originChannelCandidate =
995
- metadata && isChannelId(metadata.userMessageChannel)
996
- ? metadata.userMessageChannel
997
- : null;
998
- // Wrap insert + updatedAt bump in a transaction so they're atomic.
999
- // Retry on SQLITE_BUSY* and SQLITE_IOERR* — covers WAL contention variants
1000
- // (SQLITE_BUSY_SNAPSHOT, SQLITE_BUSY_RECOVERY) and transient disk I/O errors.
1001
- // Timestamp is recomputed each attempt so a late retry doesn't persist a stale updatedAt.
1002
- const MAX_RETRIES = 3;
1003
- let now!: number;
1004
- for (let attempt = 0; ; attempt++) {
1005
- now = monotonicNow();
1006
- try {
1007
- const values = {
1008
- id: messageId,
1009
- conversationId,
1010
- role,
1011
- content,
1012
- createdAt: now,
1013
- ...(metadataStr ? { metadata: metadataStr } : {}),
1014
- };
1015
- db.transaction((tx) => {
1016
- tx.insert(messages).values(values).run();
1017
- if (originChannelCandidate) {
1018
- tx.update(conversations)
1019
- .set({ originChannel: originChannelCandidate })
1020
- .where(
1021
- and(
1022
- eq(conversations.id, conversationId),
1023
- isNull(conversations.originChannel),
1024
- ),
1025
- )
1026
- .run();
1027
- }
1028
- tx.update(conversations)
1029
- .set({ updatedAt: now, lastMessageAt: now })
1030
- .where(eq(conversations.id, conversationId))
1031
- .run();
1032
- });
1033
- break;
1034
- } catch (err) {
1035
- const errCode = (err as { code?: string }).code ?? "";
1036
- if (
1037
- attempt < MAX_RETRIES &&
1038
- (errCode.startsWith("SQLITE_BUSY") ||
1039
- errCode.startsWith("SQLITE_IOERR"))
1040
- ) {
1041
- log.warn(
1042
- { attempt, conversationId, code: errCode },
1043
- "addMessage: transient SQLite error, retrying",
1044
- );
1045
- await Bun.sleep(50 * (attempt + 1));
1046
- continue;
1047
- }
1048
- throw err;
1049
- }
1050
- }
1051
- const message = {
1052
- id: messageId,
1215
+ const inserted = await insertMessageCore({
1053
1216
  conversationId,
1054
1217
  role,
1055
1218
  content,
1056
- createdAt: now,
1057
- ...(metadataStr ? { metadata: metadataStr } : {}),
1058
- };
1219
+ metadata,
1220
+ clientMessageId,
1221
+ });
1222
+
1223
+ if (inserted.deduplicated) {
1224
+ return inserted;
1225
+ }
1226
+
1227
+ const message = inserted;
1059
1228
 
1060
1229
  if (!opts?.skipIndexing) {
1061
1230
  try {
@@ -1090,11 +1259,16 @@ export async function addMessage(
1090
1259
 
1091
1260
  if (role === "assistant") {
1092
1261
  try {
1093
- projectAssistantMessage({
1262
+ const attentionStateChanged = projectAssistantMessage({
1094
1263
  conversationId,
1095
1264
  messageId: message.id,
1096
1265
  messageAt: message.createdAt,
1097
1266
  });
1267
+ if (attentionStateChanged) {
1268
+ void publishSyncInvalidation([
1269
+ conversationMetadataSyncTag(conversationId),
1270
+ ]);
1271
+ }
1098
1272
  } catch (err) {
1099
1273
  log.warn(
1100
1274
  { err, conversationId, messageId: message.id },
@@ -1288,11 +1462,30 @@ export function hasMessages(conversationId: string): boolean {
1288
1462
  interface PaginatedMessagesResult {
1289
1463
  messages: MessageRow[];
1290
1464
  hasMore: boolean;
1465
+ /**
1466
+ * Position of the last row scanned when the loop stops on
1467
+ * `PAGINATION_SCAN_CAP` rather than DB exhaustion. Callers derive their
1468
+ * client cursor from the visible page's oldest row, but a cap-truncated
1469
+ * page can be empty (a contiguous block of filtered-out rows longer than
1470
+ * the cap), leaving nothing to resume from. Surfacing the last scanned
1471
+ * `(createdAt, id)` lets the caller hand the client a cursor so it can
1472
+ * request the next window and keep draining instead of stalling.
1473
+ */
1474
+ nextCursor?: { createdAt: number; id: string };
1291
1475
  }
1292
1476
 
1293
1477
  const PAGINATION_CHUNK_MIN = 50;
1294
1478
  const PAGINATION_SCAN_CAP = 10_000;
1295
1479
 
1480
+ // Test-only override for PAGINATION_SCAN_CAP so tests can exercise the
1481
+ // cap-truncation branch with a small cap instead of seeding >10k rows (which
1482
+ // makes the suite slow and the post-test DELETE flaky under parallel CI load).
1483
+ // `undefined` restores the production cap.
1484
+ let paginationScanCapOverride: number | undefined;
1485
+ export function _setPaginationScanCapForTesting(cap: number | undefined): void {
1486
+ paginationScanCapOverride = cap;
1487
+ }
1488
+
1296
1489
  export function getMessagesPaginated(
1297
1490
  conversationId: string,
1298
1491
  limit: number | undefined,
@@ -1333,8 +1526,19 @@ export function getMessagesPaginated(
1333
1526
  // row — otherwise a pathological filter against a huge conversation would
1334
1527
  // tie up a connection for thousands of roundtrips.
1335
1528
  let rowsScanned = 0;
1336
-
1337
- while (visible.length < limit + 1 && rowsScanned < PAGINATION_SCAN_CAP) {
1529
+ // Distinguish "stopped because we hit the scan cap" from "stopped because the
1530
+ // DB ran out of rows". On a cap-truncated stop there may be more visible rows
1531
+ // past the scanned window, so `hasMore` must stay true and we record the last
1532
+ // scanned position as a resume cursor (the visible page may be empty).
1533
+ let scanCapTruncated = false;
1534
+ let lastScanned: { createdAt: number; id: string } | undefined;
1535
+ const scanCap = paginationScanCapOverride ?? PAGINATION_SCAN_CAP;
1536
+
1537
+ while (visible.length < limit + 1) {
1538
+ if (rowsScanned >= scanCap) {
1539
+ scanCapTruncated = true;
1540
+ break;
1541
+ }
1338
1542
  const cursorPredicate =
1339
1543
  cursorCreatedAt === undefined
1340
1544
  ? undefined
@@ -1367,15 +1571,25 @@ export function getMessagesPaginated(
1367
1571
 
1368
1572
  if (chunk.length < chunkSize) break;
1369
1573
  const lastRow = chunk[chunk.length - 1];
1574
+ lastScanned = { createdAt: lastRow.createdAt, id: lastRow.id };
1370
1575
  cursorCreatedAt = lastRow.createdAt;
1371
1576
  cursorMessageId = lastRow.id;
1372
1577
  }
1373
1578
 
1374
- const hasMore = visible.length > limit;
1375
- if (hasMore) visible.splice(limit);
1579
+ const filledPage = visible.length > limit;
1580
+ // A cap-truncated stop means the DB may still hold older visible rows past
1581
+ // the scanned window, so report `hasMore: true` to keep the client draining
1582
+ // — returning `false` here is the stall this loop exists to prevent.
1583
+ const hasMore = filledPage || scanCapTruncated;
1584
+ if (filledPage) visible.splice(limit);
1376
1585
  visible.reverse();
1377
1586
 
1378
- return { messages: visible, hasMore };
1587
+ // Only hand back a resume cursor when the cap (not DB exhaustion) cut the
1588
+ // search short; callers fall back to it when the visible page came back
1589
+ // empty and has no oldest row to anchor the next request.
1590
+ const nextCursor = scanCapTruncated ? lastScanned : undefined;
1591
+
1592
+ return { messages: visible, hasMore, nextCursor };
1379
1593
  }
1380
1594
 
1381
1595
  export function getLastUserTimestampBefore(
@@ -1821,6 +2035,8 @@ export async function clearAll(): Promise<{
1821
2035
  log.warn({ err }, "clearAll: failed to reset conversations directory");
1822
2036
  }
1823
2037
 
2038
+ void clearAllConversationIds();
2039
+
1824
2040
  return { conversations: convCount, messages: msgCount };
1825
2041
  }
1826
2042
 
@@ -1913,6 +2129,29 @@ interface WipeConversationResult extends DeletedMemoryIds {
1913
2129
  cancelledJobCount: number;
1914
2130
  }
1915
2131
 
2132
+ /**
2133
+ * Reserve an empty message row so the agent loop can stamp outbound
2134
+ * streaming events with a stable identity before content is produced.
2135
+ *
2136
+ * Intentionally skips Qdrant indexing and attention projection — an empty
2137
+ * placeholder is meaningless for either. The caller writes final content
2138
+ * via {@link updateMessageContent} and handles indexing/projection itself.
2139
+ *
2140
+ * Delegates the core insert + retry logic to {@link insertMessageCore}.
2141
+ */
2142
+ export async function reserveMessage(
2143
+ conversationId: string,
2144
+ role: MessageRole,
2145
+ metadata?: Record<string, unknown>,
2146
+ ) {
2147
+ return insertMessageCore({
2148
+ conversationId,
2149
+ role,
2150
+ content: "[]",
2151
+ metadata,
2152
+ });
2153
+ }
2154
+
1916
2155
  /**
1917
2156
  * Update the content of an existing message. Used when consolidating
1918
2157
  * multiple assistant messages into one.
@@ -2186,7 +2425,7 @@ export function batchSetDisplayOrders(
2186
2425
  updates: Array<{
2187
2426
  id: string;
2188
2427
  displayOrder: number | null;
2189
- isPinned: boolean;
2428
+ isPinned?: boolean;
2190
2429
  groupId?: string | null;
2191
2430
  }>,
2192
2431
  ): void {
@@ -2219,35 +2458,36 @@ export function batchSetDisplayOrders(
2219
2458
  safeGroupId,
2220
2459
  update.id,
2221
2460
  );
2461
+ } else if (update.isPinned === undefined) {
2462
+ // Only displayOrder provided — preserve existing pin state and group.
2463
+ rawRun(
2464
+ "UPDATE conversations SET display_order = ? WHERE id = ?",
2465
+ update.displayOrder,
2466
+ update.id,
2467
+ );
2468
+ } else if (update.isPinned) {
2469
+ rawRun(
2470
+ "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
2471
+ update.displayOrder,
2472
+ update.id,
2473
+ );
2222
2474
  } else {
2223
- // Old client: no groupId in payload
2224
- // isPinned true -> set group_id = system:pinned
2225
- // isPinned false -> clear group_id ONLY IF currently system:pinned
2226
- // otherwise preserve existing group_id
2227
- if (update.isPinned) {
2228
- rawRun(
2229
- "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
2230
- update.displayOrder,
2231
- update.id,
2232
- );
2233
- } else {
2234
- // Restore system group from source/conversationType when old clients
2235
- // unpin, instead of clearing to NULL (which would lose provenance).
2236
- rawRun(
2237
- `UPDATE conversations SET display_order = ?, is_pinned = 0,
2238
- group_id = CASE WHEN group_id = 'system:pinned' THEN
2239
- CASE
2240
- WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
2241
- WHEN source IN ('heartbeat', 'task') THEN 'system:background'
2242
- WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
2243
- ELSE 'system:all'
2244
- END
2245
- ELSE group_id END
2246
- WHERE id = ?`,
2247
- update.displayOrder,
2248
- update.id,
2249
- );
2250
- }
2475
+ // Restore system group from source/conversationType when unpinning,
2476
+ // instead of clearing to NULL (which would lose provenance).
2477
+ rawRun(
2478
+ `UPDATE conversations SET display_order = ?, is_pinned = 0,
2479
+ group_id = CASE WHEN group_id = 'system:pinned' THEN
2480
+ CASE
2481
+ WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
2482
+ WHEN source IN ('heartbeat', 'task') THEN 'system:background'
2483
+ WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
2484
+ ELSE 'system:all'
2485
+ END
2486
+ ELSE group_id END
2487
+ WHERE id = ?`,
2488
+ update.displayOrder,
2489
+ update.id,
2490
+ );
2251
2491
  }
2252
2492
  }
2253
2493
  rawExec("COMMIT");