@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f

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 (866) hide show
  1. package/ARCHITECTURE.md +94 -91
  2. package/Dockerfile +5 -16
  3. package/README.md +7 -10
  4. package/bun.lock +0 -4
  5. package/docs/architecture/security.md +20 -20
  6. package/docs/stt-provider-onboarding.md +4 -5
  7. package/docs/trusted-contact-access.md +7 -7
  8. package/eslint-rules/cli-no-daemon-internals.js +4 -0
  9. package/eslint.config.mjs +7 -1
  10. package/knip.json +0 -1
  11. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
  12. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
  13. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
  14. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
  15. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  16. package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
  17. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
  18. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
  19. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
  21. package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
  22. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
  23. package/openapi.yaml +1073 -225
  24. package/package.json +1 -4
  25. package/scripts/test.sh +35 -13
  26. package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
  27. package/src/__tests__/acp-session.test.ts +4 -1
  28. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
  29. package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
  30. package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
  31. package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
  32. package/src/__tests__/app-source-watcher.test.ts +11 -0
  33. package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
  34. package/src/__tests__/assistant-attachments.test.ts +62 -4
  35. package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
  36. package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
  37. package/src/__tests__/attachments-store.test.ts +3 -0
  38. package/src/__tests__/background-shell-bash.test.ts +34 -0
  39. package/src/__tests__/background-shell-host-bash.test.ts +12 -0
  40. package/src/__tests__/background-tool-registry.test.ts +97 -0
  41. package/src/__tests__/background-tool-routes.test.ts +72 -1
  42. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  43. package/src/__tests__/btw-routes.test.ts +57 -3
  44. package/src/__tests__/call-controller.test.ts +474 -84
  45. package/src/__tests__/call-domain.test.ts +48 -1
  46. package/src/__tests__/call-pointer-messages.test.ts +57 -19
  47. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
  48. package/src/__tests__/call-routes-http.test.ts +15 -14
  49. package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
  50. package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
  51. package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
  52. package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
  53. package/src/__tests__/call-setup-flow.test.ts +311 -0
  54. package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
  55. package/src/__tests__/channel-guardian.test.ts +4 -7
  56. package/src/__tests__/channel-readiness-service.test.ts +0 -6
  57. package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
  58. package/src/__tests__/channel-retry-sweep.test.ts +76 -64
  59. package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
  60. package/src/__tests__/clear-all-lexical.test.ts +90 -0
  61. package/src/__tests__/compactor-media-strip.test.ts +177 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +5 -5
  63. package/src/__tests__/config-schema.test.ts +17 -0
  64. package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
  65. package/src/__tests__/contacts-relay-reads.test.ts +188 -23
  66. package/src/__tests__/contacts-tools.test.ts +42 -1
  67. package/src/__tests__/contacts-write.test.ts +181 -0
  68. package/src/__tests__/context-search-conversations-source.test.ts +220 -81
  69. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  70. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  71. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  72. package/src/__tests__/conversation-agent-loop.test.ts +39 -0
  73. package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
  74. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
  75. package/src/__tests__/conversation-clear-safety.test.ts +0 -1
  76. package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
  77. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
  78. package/src/__tests__/conversation-disk-view.test.ts +1 -0
  79. package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
  80. package/src/__tests__/conversation-error.test.ts +0 -18
  81. package/src/__tests__/conversation-fork-crud.test.ts +67 -0
  82. package/src/__tests__/conversation-history-web-search.test.ts +7 -137
  83. package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
  84. package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
  85. package/src/__tests__/conversation-process-callsite.test.ts +37 -0
  86. package/src/__tests__/conversation-queue.test.ts +149 -0
  87. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
  88. package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
  89. package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
  90. package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
  91. package/src/__tests__/conversation-skill-tools.test.ts +73 -0
  92. package/src/__tests__/conversation-wipe.test.ts +20 -5
  93. package/src/__tests__/credential-prompt-route.test.ts +69 -0
  94. package/src/__tests__/credential-security-invariants.test.ts +1 -3
  95. package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
  96. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
  97. package/src/__tests__/edit-propagation.test.ts +56 -2
  98. package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
  99. package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
  100. package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
  101. package/src/__tests__/guardian-question-mode.test.ts +57 -0
  102. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
  103. package/src/__tests__/guardian-wait-controller.test.ts +619 -0
  104. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
  105. package/src/__tests__/heic-fixture.ts +55 -0
  106. package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
  107. package/src/__tests__/http-conversation-lineage.test.ts +6 -1
  108. package/src/__tests__/image-conversion.test.ts +170 -0
  109. package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
  110. package/src/__tests__/introduction-card-resolver.test.ts +354 -0
  111. package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
  112. package/src/__tests__/invite-routes-http.test.ts +246 -420
  113. package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
  114. package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
  115. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
  116. package/src/__tests__/list-messages-attachments.test.ts +159 -0
  117. package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
  118. package/src/__tests__/list-messages-queued.test.ts +25 -0
  119. package/src/__tests__/managed-store.test.ts +129 -1
  120. package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
  121. package/src/__tests__/media-stream-output.test.ts +350 -19
  122. package/src/__tests__/media-stream-server-integration.test.ts +943 -169
  123. package/src/__tests__/media-stream-stt-session.test.ts +498 -1
  124. package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
  125. package/src/__tests__/model-intents.test.ts +1 -1
  126. package/src/__tests__/mtime-cache.test.ts +19 -0
  127. package/src/__tests__/non-member-access-request.test.ts +349 -3
  128. package/src/__tests__/notification-decision-fallback.test.ts +104 -8
  129. package/src/__tests__/notification-decision-strategy.test.ts +57 -8
  130. package/src/__tests__/onboarding-template-contract.test.ts +27 -43
  131. package/src/__tests__/openai-provider.test.ts +33 -0
  132. package/src/__tests__/openai-responses-provider.test.ts +40 -0
  133. package/src/__tests__/persistence-layering-guard.test.ts +29 -20
  134. package/src/__tests__/persona-resolver.test.ts +56 -2
  135. package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
  136. package/src/__tests__/phone.test.ts +1 -15
  137. package/src/__tests__/plugin-bootstrap.test.ts +25 -196
  138. package/src/__tests__/plugin-disabled-state.test.ts +33 -0
  139. package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
  140. package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
  141. package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
  142. package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
  143. package/src/__tests__/plugin-pipeline.test.ts +63 -1
  144. package/src/__tests__/plugin-route-contribution.test.ts +62 -115
  145. package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
  146. package/src/__tests__/plugin-types.test.ts +0 -1
  147. package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
  148. package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
  149. package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
  150. package/src/__tests__/public-ingress-urls.test.ts +0 -48
  151. package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
  152. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
  153. package/src/__tests__/resolve-trust-class.test.ts +28 -10
  154. package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
  155. package/src/__tests__/schedule-retry.test.ts +38 -14
  156. package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
  157. package/src/__tests__/schedule-routes.test.ts +14 -6
  158. package/src/__tests__/schedule-store.test.ts +3 -1
  159. package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
  160. package/src/__tests__/scheduler-recurrence.test.ts +57 -107
  161. package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
  162. package/src/__tests__/scheduler-wake.test.ts +13 -23
  163. package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
  164. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  165. package/src/__tests__/shell-observability.test.ts +1 -0
  166. package/src/__tests__/skill-boundary-guard.test.ts +3 -4
  167. package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
  168. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
  169. package/src/__tests__/skill-script-runner-host.test.ts +53 -0
  170. package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
  171. package/src/__tests__/skill-tool-factory.test.ts +73 -1
  172. package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
  173. package/src/__tests__/slack-inbound-verification.test.ts +36 -0
  174. package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
  175. package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
  176. package/src/__tests__/stt-hints.test.ts +2 -256
  177. package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
  178. package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
  179. package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
  180. package/src/__tests__/subagent-persistence.test.ts +122 -0
  181. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  182. package/src/__tests__/subagent-terminal-message.test.ts +99 -0
  183. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
  184. package/src/__tests__/system-prompt.test.ts +17 -118
  185. package/src/__tests__/task-scheduler.test.ts +68 -89
  186. package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
  187. package/src/__tests__/telephony-tts-guard.test.ts +538 -0
  188. package/src/__tests__/title-generate-hook.test.ts +16 -0
  189. package/src/__tests__/tool-approval-handler.test.ts +87 -1
  190. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
  191. package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
  192. package/src/__tests__/trust-context-guards.test.ts +21 -10
  193. package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
  194. package/src/__tests__/twilio-provider.test.ts +17 -17
  195. package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
  196. package/src/__tests__/twilio-routes.test.ts +257 -387
  197. package/src/__tests__/twilio-validation.test.ts +1 -1
  198. package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
  199. package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
  200. package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
  201. package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
  202. package/src/__tests__/voice-quality.test.ts +2 -2
  203. package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
  204. package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
  205. package/src/__tests__/voice-session-bridge.test.ts +1 -10
  206. package/src/__tests__/workspace-git-service.test.ts +1 -1
  207. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
  208. package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
  209. package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
  210. package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
  211. package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
  212. package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
  213. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
  214. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
  215. package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
  216. package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
  217. package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
  218. package/src/acp/__tests__/session-manager.test.ts +6 -0
  219. package/src/acp/agent-process.test.ts +116 -0
  220. package/src/acp/agent-process.ts +71 -0
  221. package/src/acp/auto-install.ts +12 -8
  222. package/src/acp/failure-error.test.ts +109 -0
  223. package/src/acp/failure-error.ts +111 -0
  224. package/src/acp/prepare-agent-env.ts +0 -12
  225. package/src/acp/resolve-agent.test.ts +6 -45
  226. package/src/acp/resolve-agent.ts +2 -4
  227. package/src/acp/session-manager.test.ts +312 -0
  228. package/src/acp/session-manager.ts +102 -46
  229. package/src/agent/ax-tree-compaction.test.ts +4 -1
  230. package/src/agent/image-optimize.ts +17 -127
  231. package/src/agent/loop.ts +45 -192
  232. package/src/api/events/background-tool-completed.test.ts +51 -0
  233. package/src/api/events/background-tool-completed.ts +29 -0
  234. package/src/api/events/background-tool-started.test.ts +36 -0
  235. package/src/api/events/background-tool-started.ts +28 -0
  236. package/src/api/events/conversation-error.ts +0 -1
  237. package/src/api/events/open-panel.ts +33 -0
  238. package/src/api/events/subagent-status-changed.ts +4 -1
  239. package/src/api/index.ts +19 -0
  240. package/src/api/responses/conversation-message.ts +34 -0
  241. package/src/approvals/guardian-decision-primitive.ts +74 -24
  242. package/src/approvals/guardian-request-resolvers.ts +450 -122
  243. package/src/apps/app-store.ts +1 -0
  244. package/src/apps/shared-app-links-store.ts +1 -0
  245. package/src/avatar/identity-avatar.ts +3 -7
  246. package/src/background-wake/wake-intent-hooks.test.ts +1 -1
  247. package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
  248. package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
  249. package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
  250. package/src/calls/call-controller.ts +134 -48
  251. package/src/calls/call-domain.ts +18 -42
  252. package/src/calls/call-pointer-messages.ts +51 -67
  253. package/src/calls/call-setup-flow-types.ts +76 -0
  254. package/src/calls/call-setup-flow.ts +1743 -0
  255. package/src/calls/call-speech-output.ts +87 -23
  256. package/src/calls/call-store.ts +4 -0
  257. package/src/calls/call-transport.ts +22 -36
  258. package/src/calls/fish-audio-client.ts +3 -0
  259. package/src/calls/gateway-invite-reader.ts +98 -0
  260. package/src/calls/guardian-wait-controller.ts +451 -0
  261. package/src/calls/media-stream-audio-transcode.ts +82 -2
  262. package/src/calls/media-stream-output.ts +142 -36
  263. package/src/calls/media-stream-server.ts +297 -178
  264. package/src/calls/media-stream-stt-session.ts +393 -50
  265. package/src/calls/media-turn-detector.ts +4 -6
  266. package/src/calls/relay-access-wait.ts +3 -4
  267. package/src/calls/relay-setup-router.ts +22 -43
  268. package/src/calls/relay-verification.ts +17 -24
  269. package/src/calls/resolve-call-tts-provider.ts +96 -29
  270. package/src/calls/stt-hints.ts +24 -87
  271. package/src/calls/telephony-credential-preflight.ts +159 -0
  272. package/src/calls/telephony-tts-capability.ts +183 -0
  273. package/src/calls/tts-call-strategy.ts +18 -20
  274. package/src/calls/twilio-provider.ts +3 -2
  275. package/src/calls/twilio-routes.ts +47 -351
  276. package/src/calls/types.ts +5 -3
  277. package/src/calls/voice-ingress-preflight.ts +9 -0
  278. package/src/calls/voice-quality.ts +13 -18
  279. package/src/calls/voice-session-bridge.ts +25 -35
  280. package/src/channels/__tests__/types.test.ts +28 -0
  281. package/src/channels/channel-verification-sessions.ts +7 -2
  282. package/src/channels/config.ts +0 -42
  283. package/src/channels/slack-thread-store.ts +12 -0
  284. package/src/channels/types.ts +23 -0
  285. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
  286. package/src/cli/commands/__tests__/schedules.test.ts +83 -34
  287. package/src/cli/commands/__tests__/status.test.ts +42 -2
  288. package/src/cli/commands/contacts.ts +2 -2
  289. package/src/cli/commands/credentials.ts +17 -0
  290. package/src/cli/commands/monitoring.ts +191 -0
  291. package/src/cli/commands/schedules-worker.ts +175 -0
  292. package/src/cli/commands/schedules.ts +169 -43
  293. package/src/cli/commands/status.ts +12 -1
  294. package/src/cli/commands/tools.ts +5 -1
  295. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
  296. package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
  297. package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
  298. package/src/cli/lib/search-plugins.ts +6 -0
  299. package/src/cli/lib/toggle-plugin.ts +21 -2
  300. package/src/cli/program.ts +3 -1
  301. package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
  302. package/src/config/acp-defaults.test.ts +0 -10
  303. package/src/config/acp-defaults.ts +0 -6
  304. package/src/config/bundled-skills/acp/SKILL.md +13 -45
  305. package/src/config/bundled-skills/acp/TOOLS.json +3 -3
  306. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  307. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
  308. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
  309. package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
  310. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  311. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
  312. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
  313. package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
  314. package/src/config/call-site-defaults.ts +0 -2
  315. package/src/config/feature-flag-registry.json +16 -8
  316. package/src/config/loader.ts +11 -0
  317. package/src/config/schema.ts +9 -1
  318. package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
  319. package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
  320. package/src/config/schemas/call-site-catalog.ts +0 -12
  321. package/src/config/schemas/calls.ts +8 -0
  322. package/src/config/schemas/elevenlabs.ts +1 -1
  323. package/src/config/schemas/llm.ts +0 -2
  324. package/src/config/schemas/memory-v2.ts +1 -1
  325. package/src/config/schemas/memory-v3.ts +82 -0
  326. package/src/config/schemas/monitoring.ts +61 -0
  327. package/src/config/schemas/schedules.ts +35 -0
  328. package/src/config/seed-inference-profiles.ts +2 -2
  329. package/src/config/skills-plugin-filter.test.ts +96 -0
  330. package/src/config/skills.ts +26 -0
  331. package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
  332. package/src/contacts/canonical-guardian-store.ts +5 -0
  333. package/src/contacts/contact-store.ts +81 -35
  334. package/src/contacts/contacts-write.ts +50 -25
  335. package/src/contacts/member-write-relay.ts +115 -31
  336. package/src/contacts/types.ts +0 -8
  337. package/src/context/compactor.ts +29 -15
  338. package/src/context/outbound-sanitize.ts +202 -0
  339. package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
  340. package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
  341. package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
  342. package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
  343. package/src/daemon/abort-watchdog.ts +7 -0
  344. package/src/daemon/app-source-watcher.ts +9 -0
  345. package/src/daemon/assistant-attachments.ts +42 -0
  346. package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
  347. package/src/daemon/conversation-agent-loop.ts +61 -68
  348. package/src/daemon/conversation-error.ts +1 -15
  349. package/src/daemon/conversation-history.ts +32 -236
  350. package/src/daemon/conversation-initial-prompt.ts +77 -0
  351. package/src/daemon/conversation-lifecycle.ts +25 -1
  352. package/src/daemon/conversation-messaging.ts +2 -2
  353. package/src/daemon/conversation-plugin-scope.ts +38 -0
  354. package/src/daemon/conversation-process.ts +50 -14
  355. package/src/daemon/conversation-runtime-assembly.ts +36 -6
  356. package/src/daemon/conversation-skill-tools.ts +19 -3
  357. package/src/daemon/conversation-store.ts +3 -3
  358. package/src/daemon/conversation-surfaces.ts +171 -0
  359. package/src/daemon/conversation-tool-setup.test.ts +95 -0
  360. package/src/daemon/conversation-tool-setup.ts +116 -1
  361. package/src/daemon/conversation-turn-finalize.ts +211 -0
  362. package/src/daemon/conversation.ts +92 -1
  363. package/src/daemon/disk-pressure-policy.ts +2 -1
  364. package/src/daemon/event-loop-watchdog.ts +22 -6
  365. package/src/daemon/external-plugins-bootstrap.ts +47 -134
  366. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
  367. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
  368. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
  369. package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
  370. package/src/daemon/handlers/config-a2a.ts +7 -14
  371. package/src/daemon/handlers/config-channels.ts +2 -10
  372. package/src/daemon/handlers/conversation-history.ts +3 -1
  373. package/src/daemon/handlers/conversations.ts +0 -52
  374. package/src/daemon/handlers/shared.ts +4 -24
  375. package/src/daemon/handlers/skills.ts +4 -1
  376. package/src/daemon/host-cu-proxy.ts +1 -1
  377. package/src/daemon/lifecycle.ts +59 -472
  378. package/src/daemon/message-protocol.ts +3 -3
  379. package/src/daemon/message-provenance.ts +12 -14
  380. package/src/daemon/message-types/background-tools.ts +28 -0
  381. package/src/daemon/message-types/contacts.ts +2 -10
  382. package/src/daemon/message-types/conversations.ts +10 -0
  383. package/src/daemon/message-types/integrations.ts +2 -0
  384. package/src/daemon/message-types/surfaces.ts +1 -0
  385. package/src/daemon/message-types/sync.ts +1 -0
  386. package/src/daemon/pointer-conversation-trust.ts +23 -9
  387. package/src/daemon/pointer-turn-runner.ts +174 -0
  388. package/src/daemon/providers-setup.ts +16 -0
  389. package/src/daemon/shutdown-handlers.ts +139 -47
  390. package/src/daemon/skill-memory-refresh.ts +2 -2
  391. package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
  392. package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
  393. package/src/daemon/tool-setup-types.ts +8 -0
  394. package/src/daemon/trust-context.ts +21 -6
  395. package/src/daemon/turn-latency-tracker.test.ts +93 -0
  396. package/src/daemon/turn-latency-tracker.ts +38 -0
  397. package/src/daemon/wake-conversation-ops.ts +10 -8
  398. package/src/documents/document-comments-store.test.ts +7 -1
  399. package/src/documents/document-comments-store.ts +8 -0
  400. package/src/documents/document-store.ts +15 -0
  401. package/src/export/__tests__/transcript-formatter.test.ts +11 -0
  402. package/src/export/transcript-formatter.ts +49 -67
  403. package/src/home/__tests__/feed-writer.test.ts +247 -1
  404. package/src/home/conversation-starter-checkpoints.ts +1 -0
  405. package/src/home/feed-writer.ts +61 -0
  406. package/src/home/job-handlers/conversation-starters.ts +1 -0
  407. package/src/hooks/hook-loader.ts +12 -0
  408. package/src/hooks/registry.ts +32 -4
  409. package/src/inbound/public-ingress-urls.ts +0 -17
  410. package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
  411. package/src/ipc/assistant-server.ts +28 -7
  412. package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
  413. package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
  414. package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
  415. package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
  416. package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
  417. package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
  418. package/src/ipc/routes/db-proxy.ts +5 -2
  419. package/src/ipc/routes/invite-ipc-routes.ts +53 -39
  420. package/src/jobs/worker.ts +9 -2
  421. package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
  422. package/src/live-voice/live-voice-archive.ts +5 -0
  423. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
  424. package/src/messaging/providers/channel-transport.ts +6 -0
  425. package/src/messaging/providers/index.ts +3 -0
  426. package/src/messaging/providers/slack/api.test.ts +49 -1
  427. package/src/messaging/providers/slack/api.ts +110 -0
  428. package/src/messaging/providers/slack/send.test.ts +85 -0
  429. package/src/messaging/providers/slack/send.ts +100 -28
  430. package/src/messaging/providers/slack/transport.ts +7 -13
  431. package/src/messaging/providers/whatsapp/send.ts +29 -9
  432. package/src/monitoring/__tests__/control.test.ts +235 -0
  433. package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
  434. package/src/monitoring/control.ts +105 -0
  435. package/src/monitoring/resource-sampler.ts +258 -0
  436. package/src/monitoring/sample-ring-buffer.ts +108 -0
  437. package/src/monitoring/worker.ts +94 -0
  438. package/src/notifications/README.md +1 -1
  439. package/src/notifications/access-request-copy.ts +94 -23
  440. package/src/notifications/adapters/slack.ts +89 -21
  441. package/src/notifications/approval-card-builder.ts +33 -18
  442. package/src/notifications/approval-card-data.ts +48 -15
  443. package/src/notifications/broadcaster.ts +25 -16
  444. package/src/notifications/decision-engine.ts +131 -32
  445. package/src/notifications/guardian-question-mode.ts +58 -10
  446. package/src/permissions/approval-policy.ts +8 -1
  447. package/src/permissions/checker.ts +2 -0
  448. package/src/permissions/gateway-threshold-reader.ts +7 -6
  449. package/src/permissions/ipc-risk-types.ts +1 -0
  450. package/src/permissions/secret-prompter.ts +11 -3
  451. package/src/permissions/types.ts +2 -2
  452. package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
  453. package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
  454. package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
  455. package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
  456. package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
  457. package/src/persistence/a2a-invite-store.ts +205 -0
  458. package/src/persistence/attachments-store.ts +94 -10
  459. package/src/persistence/checkpoints.ts +45 -0
  460. package/src/persistence/conversation-crud.ts +453 -260
  461. package/src/persistence/conversation-display-order-migration.ts +8 -2
  462. package/src/persistence/conversation-group-migration.test.ts +8 -0
  463. package/src/persistence/conversation-group-migration.ts +9 -0
  464. package/src/persistence/conversation-queries.ts +244 -125
  465. package/src/persistence/conversation-row-batch-delete.ts +9 -5
  466. package/src/persistence/conversation-search-lexical.ts +49 -0
  467. package/src/persistence/db-async-query.ts +42 -0
  468. package/src/persistence/db-connection.ts +13 -0
  469. package/src/persistence/delivery-channels.ts +0 -82
  470. package/src/persistence/delivery-crud.ts +27 -6
  471. package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
  472. package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
  473. package/src/persistence/embeddings/embedding-identity.ts +17 -5
  474. package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
  475. package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
  476. package/src/persistence/embeddings/qdrant-client.ts +81 -5
  477. package/src/persistence/group-crud.ts +13 -1
  478. package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
  479. package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
  480. package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
  481. package/src/persistence/job-handlers/cleanup.ts +57 -9
  482. package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
  483. package/src/persistence/job-handlers/message-lexical.ts +251 -0
  484. package/src/persistence/jobs-store.ts +102 -8
  485. package/src/persistence/jobs-worker.ts +68 -28
  486. package/src/persistence/llm-request-log-store.ts +35 -19
  487. package/src/persistence/llm-usage-store.ts +11 -0
  488. package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
  489. package/src/persistence/memory-lifecycle-hooks.ts +55 -1
  490. package/src/persistence/migrations/028-call-session-mode.ts +1 -1
  491. package/src/persistence/migrations/116-messages-fts.ts +8 -1
  492. package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
  493. package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
  494. package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
  495. package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
  496. package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
  497. package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
  498. package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
  499. package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
  500. package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
  501. package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
  502. package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
  503. package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
  504. package/src/persistence/raw-query.ts +50 -11
  505. package/src/persistence/schema/a2a.ts +14 -0
  506. package/src/persistence/schema/contacts.ts +0 -62
  507. package/src/persistence/schema/conversation-groups.ts +12 -0
  508. package/src/persistence/schema/conversations.ts +2 -0
  509. package/src/persistence/schema/documents.ts +65 -0
  510. package/src/persistence/schema/guardian.ts +4 -0
  511. package/src/persistence/schema/index.ts +4 -0
  512. package/src/persistence/schema/memory-injection.ts +62 -0
  513. package/src/persistence/schema/workflows.ts +55 -0
  514. package/src/persistence/slow-query-log.ts +437 -0
  515. package/src/persistence/slow-sync-log.ts +204 -0
  516. package/src/persistence/steps.ts +16 -0
  517. package/src/persistence/subagent-store.ts +125 -0
  518. package/src/persistence/worker-control.ts +45 -238
  519. package/src/plugin-api/event-hub-facade.ts +172 -0
  520. package/src/plugin-api/index.ts +9 -1
  521. package/src/plugin-api/types.ts +40 -9
  522. package/src/plugins/defaults/default-plugin-names.ts +60 -0
  523. package/src/plugins/defaults/index.ts +19 -29
  524. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
  525. package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
  526. package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
  527. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
  528. package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
  529. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
  530. package/src/plugins/defaults/memory/config.ts +11 -0
  531. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
  532. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
  533. package/src/plugins/defaults/memory/embeddings.ts +77 -0
  534. package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
  535. package/src/plugins/defaults/memory/frontmatter.ts +50 -0
  536. package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
  537. package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
  538. package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
  539. package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
  540. package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
  541. package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
  542. package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
  543. package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
  544. package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
  545. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  546. package/src/plugins/defaults/memory/hooks/init.ts +31 -5
  547. package/src/plugins/defaults/memory/indexer.ts +2 -3
  548. package/src/plugins/defaults/memory/injectors.ts +2 -2
  549. package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
  550. package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
  551. package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
  552. package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
  553. package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
  554. package/src/plugins/defaults/memory/job-handlers.ts +13 -12
  555. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
  556. package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
  557. package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
  558. package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
  559. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
  560. package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
  561. package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
  562. package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
  563. package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
  564. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
  565. package/src/plugins/defaults/memory/search/semantic.ts +2 -2
  566. package/src/plugins/defaults/memory/startup.ts +244 -0
  567. package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
  568. package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
  569. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
  570. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
  571. package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
  572. package/src/plugins/defaults/memory/v2/activation.ts +2 -4
  573. package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
  574. package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
  575. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
  576. package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
  577. package/src/plugins/defaults/memory/v2/migration.ts +2 -5
  578. package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
  579. package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
  580. package/src/plugins/defaults/memory/v2/router.ts +5 -5
  581. package/src/plugins/defaults/memory/v2/sim.ts +2 -4
  582. package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
  583. package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
  584. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
  585. package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
  586. package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
  587. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
  588. package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
  589. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
  590. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
  591. package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
  592. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
  593. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
  594. package/src/plugins/defaults/memory/v3/card.ts +1 -4
  595. package/src/plugins/defaults/memory/v3/dense.ts +60 -25
  596. package/src/plugins/defaults/memory/v3/edge.ts +1 -1
  597. package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
  598. package/src/plugins/defaults/memory/v3/gate.ts +158 -0
  599. package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
  600. package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
  601. package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
  602. package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
  603. package/src/plugins/defaults/memory/v3/prune.ts +2 -2
  604. package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
  605. package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
  606. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
  607. package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
  608. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
  609. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  610. package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
  611. package/src/plugins/injector-registry.ts +13 -2
  612. package/src/plugins/mtime-cache.ts +36 -47
  613. package/src/plugins/pipeline.ts +15 -1
  614. package/src/plugins/types.ts +14 -23
  615. package/src/prompts/__tests__/system-prompt.test.ts +170 -32
  616. package/src/prompts/persona-resolver.ts +13 -9
  617. package/src/prompts/system-prompt.ts +30 -9
  618. package/src/prompts/templates/BOOTSTRAP.md +16 -119
  619. package/src/prompts/templates/SOUL.md +43 -13
  620. package/src/prompts/templates/system-sections.ts +32 -27
  621. package/src/prompts/templates/users/default.md +19 -0
  622. package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
  623. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
  624. package/src/providers/model-intents.ts +2 -2
  625. package/src/providers/openai/chat-completions-provider.ts +14 -1
  626. package/src/providers/openai/responses-provider.ts +18 -0
  627. package/src/providers/placeholder-sentinels.ts +18 -8
  628. package/src/providers/retry.ts +1 -0
  629. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
  630. package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
  631. package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
  632. package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
  633. package/src/providers/speech-to-text/provider-catalog.ts +0 -89
  634. package/src/providers/speech-to-text/resolve.ts +55 -0
  635. package/src/providers/tool-progress-events.test.ts +50 -0
  636. package/src/providers/tool-progress-events.ts +99 -0
  637. package/src/providers/types.ts +1 -1
  638. package/src/providers/vellum-model-routing.test.ts +51 -0
  639. package/src/providers/vellum-model-routing.ts +84 -0
  640. package/src/runtime/AGENTS.md +5 -4
  641. package/src/runtime/__tests__/agent-wake.test.ts +158 -1
  642. package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
  643. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
  644. package/src/runtime/access-request-helper.ts +154 -3
  645. package/src/runtime/actor-trust-resolver.ts +7 -21
  646. package/src/runtime/agent-wake.ts +41 -1
  647. package/src/runtime/assistant-event.ts +86 -12
  648. package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
  649. package/src/runtime/channel-approval-types.ts +52 -2
  650. package/src/runtime/channel-approvals.ts +23 -8
  651. package/src/runtime/channel-invite-transport.ts +2 -2
  652. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  653. package/src/runtime/channel-readiness-service.ts +3 -5
  654. package/src/runtime/channel-retry-sweep.ts +18 -24
  655. package/src/runtime/channel-verification-service.ts +9 -11
  656. package/src/runtime/finalize-event-delivery.ts +30 -16
  657. package/src/runtime/guardian-action-service.ts +10 -11
  658. package/src/runtime/guardian-reply-router.ts +122 -21
  659. package/src/runtime/http-server.ts +44 -101
  660. package/src/runtime/introduction-policy.test.ts +214 -0
  661. package/src/runtime/introduction-policy.ts +284 -0
  662. package/src/runtime/invite-service.ts +104 -361
  663. package/src/runtime/local-actor-identity.test.ts +38 -0
  664. package/src/runtime/local-actor-identity.ts +14 -0
  665. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
  666. package/src/runtime/middleware/rate-limiter.ts +30 -0
  667. package/src/runtime/middleware/twilio-validation.ts +5 -7
  668. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
  669. package/src/runtime/migrations/origin-mode.ts +6 -6
  670. package/src/runtime/pre-first-message-gate.ts +1 -0
  671. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
  672. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
  673. package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
  674. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
  675. package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
  676. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
  677. package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
  678. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
  679. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
  680. package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
  681. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
  682. package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
  683. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
  684. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
  685. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
  686. package/src/runtime/routes/acp-routes.ts +15 -3
  687. package/src/runtime/routes/app-management-routes.ts +4 -7
  688. package/src/runtime/routes/approval-routes.ts +4 -0
  689. package/src/runtime/routes/attachment-routes.ts +54 -3
  690. package/src/runtime/routes/avatar-routes.ts +0 -4
  691. package/src/runtime/routes/background-tool-routes.ts +34 -5
  692. package/src/runtime/routes/btw-routes.ts +32 -4
  693. package/src/runtime/routes/channel-route-shared.ts +18 -14
  694. package/src/runtime/routes/channel-verification-routes.ts +0 -3
  695. package/src/runtime/routes/contact-routes.ts +298 -117
  696. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  697. package/src/runtime/routes/conversation-management-routes.ts +74 -37
  698. package/src/runtime/routes/conversation-query-routes.ts +5 -2
  699. package/src/runtime/routes/conversation-routes.ts +222 -82
  700. package/src/runtime/routes/conversations-import-routes.ts +8 -1
  701. package/src/runtime/routes/credential-prompt-routes.ts +20 -5
  702. package/src/runtime/routes/debug-routes.ts +1 -0
  703. package/src/runtime/routes/documents-routes.ts +5 -2
  704. package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
  705. package/src/runtime/routes/global-search-routes.ts +2 -1
  706. package/src/runtime/routes/guardian-approval-interception.ts +51 -0
  707. package/src/runtime/routes/home-feed-routes.ts +60 -1
  708. package/src/runtime/routes/identity-routes.ts +4 -87
  709. package/src/runtime/routes/inbound-message-handler.ts +82 -41
  710. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
  711. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
  712. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
  713. package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
  714. package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
  715. package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
  716. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
  717. package/src/runtime/routes/index.ts +6 -0
  718. package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
  719. package/src/runtime/routes/internal-twilio-routes.ts +0 -15
  720. package/src/runtime/routes/messages-lexical-routes.ts +75 -0
  721. package/src/runtime/routes/migration-routes.ts +69 -6
  722. package/src/runtime/routes/monitoring-routes.ts +237 -0
  723. package/src/runtime/routes/plugins-routes.ts +329 -8
  724. package/src/runtime/routes/schedule-routes.ts +49 -8
  725. package/src/runtime/routes/schedule-worker-routes.ts +210 -0
  726. package/src/runtime/routes/secret-routes.ts +1 -1
  727. package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
  728. package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
  729. package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
  730. package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
  731. package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
  732. package/src/runtime/services/analyze-conversation.ts +14 -4
  733. package/src/runtime/services/conversation-serializer.ts +5 -0
  734. package/src/runtime/slack-no-response.ts +47 -0
  735. package/src/runtime/slack-reply-session.test.ts +633 -0
  736. package/src/runtime/slack-reply-session.ts +382 -0
  737. package/src/runtime/slack-task-progress.ts +95 -0
  738. package/src/runtime/sync/resource-sync-events.ts +25 -0
  739. package/src/runtime/tool-grant-request-helper.ts +17 -1
  740. package/src/runtime/trust-class.test.ts +141 -0
  741. package/src/runtime/trust-class.ts +121 -0
  742. package/src/runtime/trust-verdict-consumer.ts +7 -6
  743. package/src/schedule/__tests__/run-script.test.ts +21 -0
  744. package/src/schedule/__tests__/worker-control.test.ts +241 -0
  745. package/src/schedule/run-script.ts +13 -6
  746. package/src/schedule/schedule-usage-store.ts +1 -0
  747. package/src/schedule/scheduler.ts +178 -127
  748. package/src/schedule/worker-control.ts +118 -0
  749. package/src/schedule/worker.ts +131 -0
  750. package/src/skills/__tests__/categories-cache.test.ts +38 -0
  751. package/src/skills/categories-cache.ts +40 -5
  752. package/src/skills/managed-store.ts +41 -6
  753. package/src/skills/validate-input.ts +47 -0
  754. package/src/stt/types.ts +5 -9
  755. package/src/subagent/manager.ts +267 -39
  756. package/src/subagent/types.ts +6 -2
  757. package/src/subagent/validate-allowlists.ts +65 -0
  758. package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
  759. package/src/telemetry/watchdog-direct-emit.ts +89 -0
  760. package/src/tools/acp/list-agents.test.ts +0 -6
  761. package/src/tools/background-tool-registry.ts +61 -0
  762. package/src/tools/document/document-comment-tool.test.ts +4 -0
  763. package/src/tools/host-terminal/host-shell.test.ts +397 -0
  764. package/src/tools/host-terminal/host-shell.ts +203 -14
  765. package/src/tools/skills/find-similar-skills.test.ts +152 -1
  766. package/src/tools/skills/find-similar-skills.ts +30 -4
  767. package/src/tools/skills/load.ts +36 -0
  768. package/src/tools/skills/scaffold-managed.ts +66 -34
  769. package/src/tools/skills/skill-script-runner.ts +17 -0
  770. package/src/tools/skills/skill-tool-factory.ts +5 -1
  771. package/src/tools/terminal/safe-env.ts +3 -0
  772. package/src/tools/terminal/shell.test.ts +177 -0
  773. package/src/tools/terminal/shell.ts +98 -4
  774. package/src/tools/tool-approval-handler.ts +106 -29
  775. package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
  776. package/src/tools/types.ts +29 -17
  777. package/src/tools/ui-surface/definitions.ts +3 -1
  778. package/src/tts/__tests__/provider-adapters.test.ts +37 -0
  779. package/src/tts/__tests__/provider-catalog.test.ts +21 -0
  780. package/src/tts/provider-catalog.ts +40 -9
  781. package/src/tts/providers/fish-audio-provider.ts +19 -10
  782. package/src/tts/types.ts +3 -4
  783. package/src/util/__tests__/process-tree.test.ts +10 -2
  784. package/src/util/__tests__/text-spacing.test.ts +53 -0
  785. package/src/util/cgroup-memory.ts +142 -0
  786. package/src/util/image-conversion.ts +281 -0
  787. package/src/util/phone.ts +1 -8
  788. package/src/util/platform.ts +33 -0
  789. package/src/util/process-tree.ts +10 -2
  790. package/src/util/text-spacing.ts +53 -0
  791. package/src/util/worker-process.ts +271 -0
  792. package/src/workflows/journal-store.ts +16 -1
  793. package/src/workflows/library.test.ts +35 -20
  794. package/src/workflows/library.ts +105 -66
  795. package/src/workspace/git-service.ts +1 -0
  796. package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
  797. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
  798. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
  799. package/src/workspace/migrations/118-seed-now-md.ts +64 -0
  800. package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
  801. package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
  802. package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
  803. package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
  804. package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
  805. package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
  806. package/src/workspace/migrations/registry.ts +12 -0
  807. package/drizzle/0000_dizzy_maggott.sql +0 -301
  808. package/drizzle/meta/0000_snapshot.json +0 -1933
  809. package/drizzle/meta/_journal.json +0 -13
  810. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
  811. package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
  812. package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
  813. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
  814. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
  815. package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
  816. package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
  817. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
  818. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
  819. package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
  820. package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
  821. package/src/__tests__/invite-redemption-service.test.ts +0 -1180
  822. package/src/__tests__/invite-service-ipc.test.ts +0 -350
  823. package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
  824. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
  825. package/src/__tests__/relay-server.test.ts +0 -6153
  826. package/src/__tests__/telephony-stt-routing.test.ts +0 -329
  827. package/src/__tests__/voice-invite-redemption.test.ts +0 -702
  828. package/src/calls/relay-server.ts +0 -2463
  829. package/src/calls/telephony-stt-routing.ts +0 -145
  830. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
  831. package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
  832. package/src/daemon/daemon-skill-host.ts +0 -262
  833. package/src/daemon/message-types/meet.ts +0 -143
  834. package/src/daemon/shutdown-registry.ts +0 -40
  835. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
  836. package/src/ipc/__tests__/skill-server.test.ts +0 -182
  837. package/src/ipc/skill-ipc-types.ts +0 -54
  838. package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
  839. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
  840. package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
  841. package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
  842. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
  843. package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
  844. package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
  845. package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
  846. package/src/ipc/skill-routes/config.ts +0 -47
  847. package/src/ipc/skill-routes/events.ts +0 -147
  848. package/src/ipc/skill-routes/identity.ts +0 -21
  849. package/src/ipc/skill-routes/index.ts +0 -37
  850. package/src/ipc/skill-routes/log.ts +0 -40
  851. package/src/ipc/skill-routes/memory.ts +0 -92
  852. package/src/ipc/skill-routes/platform.ts +0 -39
  853. package/src/ipc/skill-routes/providers.ts +0 -162
  854. package/src/ipc/skill-routes/registries.ts +0 -338
  855. package/src/ipc/skill-server.ts +0 -831
  856. package/src/ipc/skill-socket-path.ts +0 -20
  857. package/src/jobs/register-job-handlers.ts +0 -74
  858. package/src/persistence/invite-store.ts +0 -481
  859. package/src/plugins/job-handler-registry.ts +0 -94
  860. package/src/providers/cache-control.ts +0 -26
  861. package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
  862. package/src/runtime/invite-redemption-service.ts +0 -828
  863. package/src/runtime/invite-redemption-templates.ts +0 -46
  864. package/src/runtime/runtime-mode.ts +0 -40
  865. package/src/runtime/slack-dm-text-delivery.ts +0 -177
  866. package/src/util/voice-code.ts +0 -31
@@ -28,15 +28,15 @@ import { findConversation } from "../daemon/conversation-registry.js";
28
28
  import { conversationMetadataSyncTag } from "../daemon/message-types/sync.js";
29
29
  import type { TrustContext } from "../daemon/trust-context.js";
30
30
  import { clearAllConversationIds } from "../home/feed-writer.js";
31
- import { MEMORY_RETROSPECTIVE_SOURCES } from "../plugins/defaults/memory/memory-retrospective-constants.js";
32
31
  import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
33
32
  import { publishSyncInvalidation } from "../runtime/sync/sync-publisher.js";
33
+ import { trustClassSchema } from "../runtime/trust-class.js";
34
34
  import { UserError } from "../util/errors.js";
35
35
  import { safeParseRecord } from "../util/json.js";
36
36
  import { getLogger } from "../util/logger.js";
37
37
  import { getLogsDbPath } from "../util/logs-db-path.js";
38
38
  import { getConversationsDir } from "../util/platform.js";
39
- import { createRowMapper } from "../util/row-mapper.js";
39
+ import { createRowMapper, parseJsonNullable } from "../util/row-mapper.js";
40
40
  import { withSqliteRetry } from "../util/sqlite-retry.js";
41
41
  import {
42
42
  deleteOrphanAttachments,
@@ -72,6 +72,7 @@ import {
72
72
  copyForkMessagesViaSubprocess,
73
73
  type ForkIdPair,
74
74
  } from "./fork-message-copy.js";
75
+ import { enqueueLexicalIndexForMessage } from "./job-handlers/message-lexical.js";
75
76
  import { getMemoryPersistenceHooks } from "./memory-lifecycle-hooks.js";
76
77
  import {
77
78
  rawExec,
@@ -92,6 +93,7 @@ import {
92
93
  skillLoadedEvents,
93
94
  toolInvocations,
94
95
  } from "./schema/index.js";
96
+ import { timeSyncSection } from "./slow-sync-log.js";
95
97
 
96
98
  const log = getLogger("conversation-store");
97
99
 
@@ -134,6 +136,18 @@ const acpNotificationSchema = z.object({
134
136
  agent: z.string().optional(),
135
137
  });
136
138
 
139
+ const backgroundToolCompletionMetadataSchema = z.object({
140
+ id: z.string(),
141
+ toolName: z.string(),
142
+ conversationId: z.string(),
143
+ command: z.string(),
144
+ startedAt: z.number(),
145
+ status: z.enum(["completed", "failed", "cancelled"]),
146
+ exitCode: z.number().nullable(),
147
+ output: z.string(),
148
+ completedAt: z.number(),
149
+ });
150
+
137
151
  export const messageMetadataSchema = z
138
152
  .object({
139
153
  userMessageChannel: channelIdSchema.optional(),
@@ -158,13 +172,27 @@ export const messageMetadataSchema = z
158
172
  * trust status changes later. Used by the memory write gate (indexer)
159
173
  * and read gate (conversation history loading) to enforce trust-aware access.
160
174
  */
161
- provenanceTrustClass: z
162
- .enum(["guardian", "trusted_contact", "unverified_contact", "unknown"])
163
- .optional(),
175
+ provenanceTrustClass: trustClassSchema.optional(),
164
176
  provenanceSourceChannel: channelIdSchema.optional(),
165
177
  provenanceGuardianExternalUserId: z.string().optional(),
166
178
  provenanceRequesterIdentifier: z.string().optional(),
167
179
  automated: z.boolean().optional(),
180
+ /**
181
+ * Transcript-suppression flag: the row is a machine signal (e.g. the
182
+ * channel-setup wizard-close marker, the onboarding greeting kickoff),
183
+ * persisted and LLM-visible but never rendered as a user message. Test
184
+ * with {@link isHiddenMessageMetadata} — hidden rows are filtered from
185
+ * list-messages and queued snapshots, skip the user_message_echo, and
186
+ * are excluded from search/memory indexing and other consumers that
187
+ * treat message text as organic user input.
188
+ */
189
+ hidden: z.boolean().optional(),
190
+ /**
191
+ * Structured terminal record stamped onto a `<background_event
192
+ * source="background-tool">` wake so the web can rebuild the inline
193
+ * bash/host_bash card from history after a daemon restart.
194
+ */
195
+ backgroundToolCompletion: backgroundToolCompletionMetadataSchema.optional(),
168
196
  forkSourceMessageId: z.string().optional(),
169
197
  /** Image source paths from desktop attachments, keyed by filename. */
170
198
  imageSourcePaths: z.record(z.string(), z.string()).optional(),
@@ -191,6 +219,44 @@ export const messageMetadataSchema = z
191
219
  })
192
220
  .passthrough();
193
221
 
222
+ /** Validated shape of a persisted message's `metadata` column. */
223
+ export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
224
+
225
+ /**
226
+ * Shared predicate for the transcript-suppression flag on user-message
227
+ * metadata (see the `hidden` field on {@link messageMetadataSchema}). One
228
+ * definition so the sites that must agree — echo suppression, list-messages
229
+ * filtering, queued-snapshot filtering, indexing exclusion, and downstream
230
+ * consumers of message text — cannot drift.
231
+ */
232
+ export function isHiddenMessageMetadata(
233
+ metadata: Record<string, unknown> | null | undefined,
234
+ ): boolean {
235
+ return metadata?.hidden === true;
236
+ }
237
+
238
+ /**
239
+ * Parse a persisted message's metadata JSON against {@link messageMetadataSchema}
240
+ * — the single source of truth for its shape — returning the validated fields,
241
+ * or `undefined` when the column is absent, not valid JSON, or fails validation.
242
+ * The single place the raw JSON.parse + safeParse dance lives, so callers read
243
+ * typed fields (e.g. `provenanceTrustClass`, `automated`, `subagentNotification`)
244
+ * instead of re-implementing it.
245
+ */
246
+ export function parseMessageMetadata(
247
+ metadataJson: string | null,
248
+ ): MessageMetadata | undefined {
249
+ if (!metadataJson) {
250
+ return undefined;
251
+ }
252
+ try {
253
+ const parsed = messageMetadataSchema.safeParse(JSON.parse(metadataJson));
254
+ return parsed.success ? parsed.data : undefined;
255
+ } catch {
256
+ return undefined;
257
+ }
258
+ }
259
+
194
260
  function cloneForkMessageMetadata(
195
261
  metadata: string | null,
196
262
  sourceMessageId: string,
@@ -228,7 +294,9 @@ function cloneForkMessageMetadata(
228
294
  export function provenanceFromTrustContext(
229
295
  ctx: TrustContext | null | undefined,
230
296
  ): Record<string, unknown> {
231
- if (!ctx) return { provenanceTrustClass: "unknown" };
297
+ if (!ctx) {
298
+ return { provenanceTrustClass: "unknown" };
299
+ }
232
300
  return {
233
301
  provenanceTrustClass: ctx.trustClass,
234
302
  provenanceSourceChannel: ctx.sourceChannel,
@@ -282,6 +350,8 @@ export interface ConversationRow {
282
350
  archivedAt: number | null;
283
351
  surfacedAt: number | null;
284
352
  inferenceProfile: string | null;
353
+ /** Parsed plugin-id list scoping this chat; null = default (all globally-enabled). */
354
+ enabledPlugins: string[] | null;
285
355
  inferenceProfileSessionId: string | null;
286
356
  inferenceProfileExpiresAt: number | null;
287
357
  lastNotifiedInferenceProfile: string | null;
@@ -318,6 +388,10 @@ export const parseConversation = createRowMapper<
318
388
  archivedAt: "archivedAt",
319
389
  surfacedAt: "surfacedAt",
320
390
  inferenceProfile: "inferenceProfile",
391
+ enabledPlugins: {
392
+ from: "enabledPlugins",
393
+ transform: parseJsonNullable<string[]>(),
394
+ },
321
395
  inferenceProfileSessionId: "inferenceProfileSessionId",
322
396
  inferenceProfileExpiresAt: "inferenceProfileExpiresAt",
323
397
  lastNotifiedInferenceProfile: "lastNotifiedInferenceProfile",
@@ -383,6 +457,11 @@ interface InsertMessageCoreParams {
383
457
  content: string;
384
458
  metadata?: Record<string, unknown>;
385
459
  clientMessageId?: string;
460
+ /** Pre-assigned message ID. When omitted, one is generated via
461
+ * `uuid()`. Callers that already have a correlation ID (e.g.
462
+ * `requestId` for user turns) can pass it here so the persisted
463
+ * row ID matches the runtime request ID. */
464
+ id?: string;
386
465
  }
387
466
 
388
467
  /**
@@ -409,9 +488,10 @@ interface InsertMessageCoreParams {
409
488
  async function insertMessageCore(
410
489
  params: InsertMessageCoreParams,
411
490
  ): Promise<InsertedMessage> {
412
- const { conversationId, role, content, metadata, clientMessageId } = params;
491
+ const { conversationId, role, content, metadata, clientMessageId, id } =
492
+ params;
413
493
  const db = getDb();
414
- const messageId = uuid();
494
+ const messageId = id ?? uuid();
415
495
 
416
496
  if (metadata) {
417
497
  const result = messageMetadataSchema.safeParse(metadata);
@@ -432,105 +512,117 @@ async function insertMessageCore(
432
512
  // The timestamp is recomputed each attempt so a late retry doesn't persist a
433
513
  // stale `updatedAt`.
434
514
  return withSqliteRetry(
435
- (): InsertedMessage => {
436
- const now = monotonicNow();
437
- const values = {
438
- id: messageId,
439
- conversationId,
440
- role,
441
- content,
442
- createdAt: now,
443
- ...(metadataStr ? { metadata: metadataStr } : {}),
444
- ...(clientMessageId ? { clientMessageId } : {}),
445
- };
446
-
447
- if (clientMessageId) {
448
- // Idempotent insert: skip silently if this clientMessageId was
449
- // already persisted for the conversation.
450
- const raw = getSqliteFrom(db);
451
- raw.exec("SAVEPOINT insert_msg");
452
- try {
453
- db.insert(messages).values(values).run();
454
- if (originChannelCandidate) {
455
- db.update(conversations)
456
- .set({ originChannel: originChannelCandidate })
457
- .where(
458
- and(
459
- eq(conversations.id, conversationId),
460
- isNull(conversations.originChannel),
461
- ),
462
- )
463
- .run();
464
- }
465
- db.update(conversations)
466
- .set({ updatedAt: now, lastMessageAt: now })
467
- .where(eq(conversations.id, conversationId))
468
- .run();
469
- raw.exec("RELEASE insert_msg");
470
- } catch (insertErr) {
471
- raw.exec("ROLLBACK TO insert_msg");
472
- raw.exec("RELEASE insert_msg");
473
- const code = (insertErr as { code?: string }).code ?? "";
474
- if (code === "SQLITE_CONSTRAINT_UNIQUE") {
475
- // Duplicate clientMessageId — return the existing row.
476
- const existing = db
477
- .select()
478
- .from(messages)
479
- .where(
480
- and(
481
- eq(messages.conversationId, conversationId),
482
- eq(messages.clientMessageId, clientMessageId),
483
- ),
484
- )
485
- .get();
486
- if (existing) {
487
- return {
488
- id: existing.id,
489
- conversationId: existing.conversationId,
490
- role: existing.role as MessageRole,
491
- content: existing.content,
492
- createdAt: existing.createdAt,
493
- ...(existing.metadata ? { metadata: existing.metadata } : {}),
494
- clientMessageId: existing.clientMessageId ?? undefined,
495
- deduplicated: true,
496
- };
515
+ (): InsertedMessage =>
516
+ timeSyncSection(
517
+ "messages:insert",
518
+ (): InsertedMessage => {
519
+ const now = monotonicNow();
520
+ const values = {
521
+ id: messageId,
522
+ conversationId,
523
+ role,
524
+ content,
525
+ createdAt: now,
526
+ ...(metadataStr ? { metadata: metadataStr } : {}),
527
+ ...(clientMessageId ? { clientMessageId } : {}),
528
+ };
529
+
530
+ if (clientMessageId) {
531
+ // Idempotent insert: skip silently if this clientMessageId was
532
+ // already persisted for the conversation.
533
+ const raw = getSqliteFrom(db);
534
+ raw.exec("SAVEPOINT insert_msg");
535
+ try {
536
+ db.insert(messages).values(values).run();
537
+ if (originChannelCandidate) {
538
+ db.update(conversations)
539
+ .set({ originChannel: originChannelCandidate })
540
+ .where(
541
+ and(
542
+ eq(conversations.id, conversationId),
543
+ isNull(conversations.originChannel),
544
+ ),
545
+ )
546
+ .run();
547
+ }
548
+ db.update(conversations)
549
+ .set({ updatedAt: now, lastMessageAt: now })
550
+ .where(eq(conversations.id, conversationId))
551
+ .run();
552
+ raw.exec("RELEASE insert_msg");
553
+ } catch (insertErr) {
554
+ raw.exec("ROLLBACK TO insert_msg");
555
+ raw.exec("RELEASE insert_msg");
556
+ const code = (insertErr as { code?: string }).code ?? "";
557
+ if (code === "SQLITE_CONSTRAINT_UNIQUE") {
558
+ // Duplicate clientMessageId — return the existing row.
559
+ const existing = db
560
+ .select()
561
+ .from(messages)
562
+ .where(
563
+ and(
564
+ eq(messages.conversationId, conversationId),
565
+ eq(messages.clientMessageId, clientMessageId),
566
+ ),
567
+ )
568
+ .get();
569
+ if (existing) {
570
+ return {
571
+ id: existing.id,
572
+ conversationId: existing.conversationId,
573
+ role: existing.role as MessageRole,
574
+ content: existing.content,
575
+ createdAt: existing.createdAt,
576
+ ...(existing.metadata
577
+ ? { metadata: existing.metadata }
578
+ : {}),
579
+ clientMessageId: existing.clientMessageId ?? undefined,
580
+ deduplicated: true,
581
+ };
582
+ }
583
+ }
584
+ throw insertErr;
497
585
  }
586
+ } else {
587
+ // No clientMessageId — standard insert inside a transaction.
588
+ db.transaction((tx) => {
589
+ tx.insert(messages).values(values).run();
590
+ if (originChannelCandidate) {
591
+ tx.update(conversations)
592
+ .set({ originChannel: originChannelCandidate })
593
+ .where(
594
+ and(
595
+ eq(conversations.id, conversationId),
596
+ isNull(conversations.originChannel),
597
+ ),
598
+ )
599
+ .run();
600
+ }
601
+ tx.update(conversations)
602
+ .set({ updatedAt: now, lastMessageAt: now })
603
+ .where(eq(conversations.id, conversationId))
604
+ .run();
605
+ });
498
606
  }
499
- throw insertErr;
500
- }
501
- } else {
502
- // No clientMessageId — standard insert inside a transaction.
503
- db.transaction((tx) => {
504
- tx.insert(messages).values(values).run();
505
- if (originChannelCandidate) {
506
- tx.update(conversations)
507
- .set({ originChannel: originChannelCandidate })
508
- .where(
509
- and(
510
- eq(conversations.id, conversationId),
511
- isNull(conversations.originChannel),
512
- ),
513
- )
514
- .run();
515
- }
516
- tx.update(conversations)
517
- .set({ updatedAt: now, lastMessageAt: now })
518
- .where(eq(conversations.id, conversationId))
519
- .run();
520
- });
521
- }
522
607
 
523
- return {
524
- id: messageId,
525
- conversationId,
526
- role,
527
- content,
528
- createdAt: now,
529
- ...(metadataStr ? { metadata: metadataStr } : {}),
530
- ...(clientMessageId ? { clientMessageId } : {}),
531
- deduplicated: false,
532
- };
533
- },
608
+ return {
609
+ id: messageId,
610
+ conversationId,
611
+ role,
612
+ content,
613
+ createdAt: now,
614
+ ...(metadataStr ? { metadata: metadataStr } : {}),
615
+ ...(clientMessageId ? { clientMessageId } : {}),
616
+ deduplicated: false,
617
+ };
618
+ },
619
+ () => ({
620
+ conversationId,
621
+ role,
622
+ contentBytes:
623
+ typeof content === "string" ? content.length : undefined,
624
+ }),
625
+ ),
534
626
  { op: "insertMessageCore", context: { conversationId } },
535
627
  );
536
628
  }
@@ -616,6 +708,7 @@ export function createConversation(
616
708
  try {
617
709
  db.insert(conversations).values(conversation).run();
618
710
  rawRun(
711
+ "conversation:create:setGroup",
619
712
  "UPDATE conversations SET group_id = ?, is_pinned = ? WHERE id = ?",
620
713
  effectiveGroupId,
621
714
  effectiveGroupId === "system:pinned" ? 1 : 0,
@@ -653,6 +746,7 @@ export function countConversationsByScheduleJobId(
653
746
  ): number {
654
747
  return (
655
748
  rawGet<{ c: number }>(
749
+ "conversation:countByScheduleJobId",
656
750
  "SELECT COUNT(*) AS c FROM conversations WHERE schedule_job_id = ?",
657
751
  scheduleJobId,
658
752
  )?.c ?? 0
@@ -690,67 +784,6 @@ export function findAnalysisConversationFor(
690
784
  return row ? { id: row.id } : null;
691
785
  }
692
786
 
693
- /**
694
- * Find the most recent memory-retrospective background conversation rooted
695
- * at `parentConversationId`. Used by the memory-retrospective job handler
696
- * to load the prior retrospective's `remember` calls into the new run's
697
- * `<already_remembered>` block — bounded source-of-truth for "what the
698
- * prior pass already saved" that scales as the source conversation grows.
699
- *
700
- * Walks up `forkParentConversationId` when no retrospective exists at the
701
- * current level. This lets a forked conversation inherit dedup context from
702
- * its source's most recent retro on the fork's *first* retrospective —
703
- * otherwise the fork would re-save every fact the source already retro'd.
704
- * Once the fork accumulates its own retros, those are found at the first
705
- * iteration and we never walk up.
706
- *
707
- * The returned `forkParentConversationId` is the conversation the
708
- * retrospective row is rooted at — `parentConversationId` itself when it has
709
- * its own retros, or an ancestor when the chain walk found the row higher
710
- * up. Callers that mutate the prior (e.g. GC of superseded runs) must check
711
- * it against the source conversation: an ancestor's row is that ancestor's
712
- * dedup baseline, not the caller's to delete.
713
- *
714
- * Returns `null` when no prior retrospective exists anywhere in the fork
715
- * chain (true first-run case).
716
- *
717
- * Hits `idx_conversations_fork_parent_conversation_id` for the
718
- * `forkParentConversationId` lookup.
719
- */
720
- const MAX_FORK_CHAIN_DEPTH = 16;
721
-
722
- export function findMostRecentRetrospectiveFor(
723
- parentConversationId: string,
724
- ): { id: string; forkParentConversationId: string } | null {
725
- const db = getDb();
726
- let currentId: string | null = parentConversationId;
727
- for (let depth = 0; depth < MAX_FORK_CHAIN_DEPTH && currentId; depth++) {
728
- const row = db
729
- .select({ id: conversations.id })
730
- .from(conversations)
731
- .where(
732
- and(
733
- inArray(conversations.source, MEMORY_RETROSPECTIVE_SOURCES),
734
- eq(conversations.forkParentConversationId, currentId),
735
- ),
736
- )
737
- .orderBy(desc(conversations.createdAt))
738
- .limit(1)
739
- .get();
740
- if (row) return { id: row.id, forkParentConversationId: currentId };
741
-
742
- const parent = db
743
- .select({
744
- forkParentConversationId: conversations.forkParentConversationId,
745
- })
746
- .from(conversations)
747
- .where(eq(conversations.id, currentId))
748
- .get();
749
- currentId = parent?.forkParentConversationId ?? null;
750
- }
751
- return null;
752
- }
753
-
754
787
  /**
755
788
  * Returns the `source` column for the given conversation, or null if
756
789
  * not found. Tiny convenience used by the recursion guard in the
@@ -774,6 +807,7 @@ export function getConversationSource(conversationId: string): string | null {
774
807
  function getConversationGroupId(conversationId: string): string | null {
775
808
  ensureGroupMigration();
776
809
  const row = rawGet<{ group_id: string | null }>(
810
+ "conversation:getGroupId",
777
811
  "SELECT group_id FROM conversations WHERE id = ?",
778
812
  conversationId,
779
813
  );
@@ -940,6 +974,7 @@ export function forkConversation(params: {
940
974
  ? sourceHistoryStrippedAt
941
975
  : null,
942
976
  inferenceProfile: sourceConversation.inferenceProfile,
977
+ enabledPlugins: encodeEnabledPlugins(sourceConversation.enabledPlugins),
943
978
  })
944
979
  .where(eq(conversations.id, fc.id))
945
980
  .run();
@@ -950,18 +985,8 @@ export function forkConversation(params: {
950
985
  messageAt: number;
951
986
  } | null = null;
952
987
 
953
- for (const message of messagesToCopy) {
988
+ const forkedMessageValues = messagesToCopy.map((message) => {
954
989
  const forkedMessageId = uuid();
955
- db.insert(messages)
956
- .values({
957
- id: forkedMessageId,
958
- conversationId: fc.id,
959
- role: message.role,
960
- content: message.content,
961
- createdAt: message.createdAt,
962
- metadata: cloneForkMessageMetadata(message.metadata, message.id),
963
- })
964
- .run();
965
990
  forkedMessageIds.set(message.id, forkedMessageId);
966
991
 
967
992
  if (message.role === "assistant") {
@@ -970,6 +995,27 @@ export function forkConversation(params: {
970
995
  messageAt: message.createdAt,
971
996
  };
972
997
  }
998
+
999
+ return {
1000
+ id: forkedMessageId,
1001
+ conversationId: fc.id,
1002
+ role: message.role,
1003
+ content: message.content,
1004
+ createdAt: message.createdAt,
1005
+ metadata: cloneForkMessageMetadata(message.metadata, message.id),
1006
+ };
1007
+ });
1008
+
1009
+ // Insert in chunks of one multi-row statement each so a large fork takes
1010
+ // the SQLite write lock O(messages / chunk) times instead of once per row.
1011
+ const FORK_INSERT_CHUNK_SIZE = 200;
1012
+ for (
1013
+ let i = 0;
1014
+ i < forkedMessageValues.length;
1015
+ i += FORK_INSERT_CHUNK_SIZE
1016
+ ) {
1017
+ const chunk = forkedMessageValues.slice(i, i + FORK_INSERT_CHUNK_SIZE);
1018
+ db.insert(messages).values(chunk).run();
973
1019
  }
974
1020
 
975
1021
  populateForkContentsInProcess({
@@ -1059,7 +1105,9 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
1059
1105
  const attachmentIdMap = new Map<string, string>();
1060
1106
  for (const message of messagesToCopy) {
1061
1107
  const forkedMessageId = forkedMessageIds.get(message.id);
1062
- if (!forkedMessageId) continue;
1108
+ if (!forkedMessageId) {
1109
+ continue;
1110
+ }
1063
1111
 
1064
1112
  const attachmentLinks = db
1065
1113
  .select({
@@ -1173,7 +1221,9 @@ function latestForkedAssistantFrom(
1173
1221
  ): { messageId: string; messageAt: number } | null {
1174
1222
  for (let i = messagesToCopy.length - 1; i >= 0; i--) {
1175
1223
  const message = messagesToCopy[i]!;
1176
- if (message.role !== "assistant") continue;
1224
+ if (message.role !== "assistant") {
1225
+ continue;
1226
+ }
1177
1227
  const forkedMessageId = forkedMessageIds.get(message.id);
1178
1228
  if (forkedMessageId) {
1179
1229
  return { messageId: forkedMessageId, messageAt: message.createdAt };
@@ -1317,6 +1367,9 @@ export async function forkConversationForRetrospective(params: {
1317
1367
  ? sourceHistoryStrippedAt
1318
1368
  : null,
1319
1369
  inferenceProfile: sourceConversation.inferenceProfile,
1370
+ enabledPlugins: encodeEnabledPlugins(
1371
+ sourceConversation.enabledPlugins,
1372
+ ),
1320
1373
  })
1321
1374
  .where(eq(conversations.id, fc.id))
1322
1375
  .run();
@@ -1455,6 +1508,13 @@ export function deleteConversation(id: string): DeletedMemoryIds {
1455
1508
  removeConversationDir(id, createdAtForDiskCleanup);
1456
1509
  }
1457
1510
 
1511
+ // Let the memory feature purge the conversation's per-message index (e.g. its
1512
+ // lexical points). Fired from the shared primitive so every delete caller —
1513
+ // route, wipe, retrospective cleanup/GC — cleans up. Routed through the
1514
+ // persistence-hook seam so this layer stays decoupled from the memory plugin;
1515
+ // a no-op when memory is not present.
1516
+ getMemoryPersistenceHooks().onConversationDeleted(id);
1517
+
1458
1518
  return result;
1459
1519
  }
1460
1520
 
@@ -1561,6 +1621,12 @@ export async function deleteConversationGently(
1561
1621
  removeConversationDir(id, createdAtForDiskCleanup);
1562
1622
  }
1563
1623
 
1624
+ // Let the memory feature purge the conversation's per-message index — the
1625
+ // gentle path is the retrospective-GC caller, which would otherwise leak the
1626
+ // conversation's lexical points. Routed through the persistence-hook seam;
1627
+ // a no-op when memory is not present.
1628
+ getMemoryPersistenceHooks().onConversationDeleted(id);
1629
+
1564
1630
  return result;
1565
1631
  }
1566
1632
 
@@ -1632,6 +1698,10 @@ export interface AddMessageOptions {
1632
1698
  * duplicate inserts for the same `(conversationId, clientMessageId)`
1633
1699
  * pair are silently skipped. */
1634
1700
  clientMessageId?: string;
1701
+ /** Pre-assigned message ID. When omitted, one is generated
1702
+ * internally. Pass the same value as `requestId` for user turns so
1703
+ * the persisted row ID matches the runtime correlation ID. */
1704
+ id?: string;
1635
1705
  }
1636
1706
 
1637
1707
  /**
@@ -1645,13 +1715,14 @@ export async function addMessage(
1645
1715
  content: string,
1646
1716
  options?: AddMessageOptions,
1647
1717
  ) {
1648
- const { metadata, skipIndexing, clientMessageId } = options ?? {};
1718
+ const { metadata, skipIndexing, clientMessageId, id } = options ?? {};
1649
1719
  const inserted = await insertMessageCore({
1650
1720
  conversationId,
1651
1721
  role,
1652
1722
  content,
1653
1723
  metadata,
1654
1724
  clientMessageId,
1725
+ id,
1655
1726
  });
1656
1727
 
1657
1728
  if (inserted.deduplicated) {
@@ -1660,7 +1731,15 @@ export async function addMessage(
1660
1731
 
1661
1732
  const message = inserted;
1662
1733
 
1663
- if (!skipIndexing) {
1734
+ // Hidden rows are machine signals suppressed from the transcript — they
1735
+ // must not surface as search excerpts or be embedded into memory either.
1736
+ if (!skipIndexing && !isHiddenMessageMetadata(metadata)) {
1737
+ // Message-content lexical indexing is host infrastructure, invoked
1738
+ // directly (not through the memory hook seam, whose active side effects go
1739
+ // inert while the memory plugin is disabled — search indexing must not).
1740
+ // The direct write seams (streaming finalize, import, edit) enqueue for
1741
+ // themselves; this covers the plain addMessage path.
1742
+ enqueueLexicalIndexForMessage(message.id);
1664
1743
  try {
1665
1744
  const parsed = metadata
1666
1745
  ? messageMetadataSchema.safeParse(metadata)
@@ -1711,13 +1790,21 @@ export async function addMessage(
1711
1790
 
1712
1791
  export function getMessages(conversationId: string): MessageRow[] {
1713
1792
  const db = getDb();
1714
- return db
1715
- .select()
1716
- .from(messages)
1717
- .where(eq(messages.conversationId, conversationId))
1718
- .orderBy(asc(messages.createdAt))
1719
- .all()
1720
- .map(parseMessage);
1793
+ // Synchronous read of every row for the conversation — the dominant
1794
+ // per-turn main-thread cost on large conversations. Timed so a freeze the
1795
+ // event-loop watchdog detects can be attributed here (see slow-sync-log).
1796
+ return timeSyncSection(
1797
+ "conversation-crud:get-messages",
1798
+ () =>
1799
+ db
1800
+ .select()
1801
+ .from(messages)
1802
+ .where(eq(messages.conversationId, conversationId))
1803
+ .orderBy(asc(messages.createdAt))
1804
+ .all()
1805
+ .map(parseMessage),
1806
+ (rows) => ({ conversationId, rowCount: rows.length }),
1807
+ );
1721
1808
  }
1722
1809
 
1723
1810
  /**
@@ -1796,7 +1883,9 @@ export function countMessagesAfter(
1796
1883
  .from(messages)
1797
1884
  .where(eq(messages.id, afterMessageId))
1798
1885
  .get();
1799
- if (!ref) return 0;
1886
+ if (!ref) {
1887
+ return 0;
1888
+ }
1800
1889
  // Tie-breaker on `messages.id` so rows that share a millisecond timestamp
1801
1890
  // with the reference are not permanently skipped. Mirrors the
1802
1891
  // `(createdAt, id)` cursor pattern used by the backfill job-handler and
@@ -1849,7 +1938,9 @@ export function getMessagesAfter(
1849
1938
  .from(messages)
1850
1939
  .where(eq(messages.id, afterMessageId))
1851
1940
  .get();
1852
- if (!ref) return [];
1941
+ if (!ref) {
1942
+ return [];
1943
+ }
1853
1944
  // Same `(createdAt, id)` cursor as `countMessagesAfter` — rows sharing
1854
1945
  // the reference's millisecond timestamp would otherwise be skipped.
1855
1946
  return db
@@ -1990,15 +2081,23 @@ export function getMessagesPaginated(
1990
2081
  .all()
1991
2082
  .map(parseMessage);
1992
2083
 
1993
- if (chunk.length === 0) break;
2084
+ if (chunk.length === 0) {
2085
+ break;
2086
+ }
1994
2087
  rowsScanned += chunk.length;
1995
2088
 
1996
2089
  for (const row of chunk) {
1997
- if (!filter || filter(row)) visible.push(row);
1998
- if (visible.length >= limit + 1) break;
2090
+ if (!filter || filter(row)) {
2091
+ visible.push(row);
2092
+ }
2093
+ if (visible.length >= limit + 1) {
2094
+ break;
2095
+ }
1999
2096
  }
2000
2097
 
2001
- if (chunk.length < chunkSize) break;
2098
+ if (chunk.length < chunkSize) {
2099
+ break;
2100
+ }
2002
2101
  const lastRow = chunk[chunk.length - 1];
2003
2102
  lastScanned = { createdAt: lastRow.createdAt, id: lastRow.id };
2004
2103
  cursorCreatedAt = lastRow.createdAt;
@@ -2010,7 +2109,9 @@ export function getMessagesPaginated(
2010
2109
  // the scanned window, so report `hasMore: true` to keep the client draining
2011
2110
  // — returning `false` here is the stall this loop exists to prevent.
2012
2111
  const hasMore = filledPage || scanCapTruncated;
2013
- if (filledPage) visible.splice(limit);
2112
+ if (filledPage) {
2113
+ visible.splice(limit);
2114
+ }
2014
2115
  visible.reverse();
2015
2116
 
2016
2117
  // Only hand back a resume cursor when the cap (not DB exhaustion) cut the
@@ -2092,7 +2193,9 @@ export function updateConversationTitle(
2092
2193
  ): void {
2093
2194
  const db = getDb();
2094
2195
  const set: Record<string, unknown> = { title, updatedAt: Date.now() };
2095
- if (isAutoTitle !== undefined) set.isAutoTitle = isAutoTitle;
2196
+ if (isAutoTitle !== undefined) {
2197
+ set.isAutoTitle = isAutoTitle;
2198
+ }
2096
2199
  db.update(conversations).set(set).where(eq(conversations.id, id)).run();
2097
2200
 
2098
2201
  // Update disk view meta.json with the new title
@@ -2181,9 +2284,12 @@ export function updateConversationSlackContextWatermark(
2181
2284
 
2182
2285
  export function archiveConversation(id: string): boolean {
2183
2286
  const conv = getConversation(id);
2184
- if (!conv) return false;
2287
+ if (!conv) {
2288
+ return false;
2289
+ }
2185
2290
  const now = Date.now();
2186
2291
  rawRun(
2292
+ "conversation:archive",
2187
2293
  "UPDATE conversations SET archived_at = ?, updated_at = ? WHERE id = ?",
2188
2294
  now,
2189
2295
  now,
@@ -2194,9 +2300,12 @@ export function archiveConversation(id: string): boolean {
2194
2300
 
2195
2301
  export function unarchiveConversation(id: string): boolean {
2196
2302
  const conv = getConversation(id);
2197
- if (!conv) return false;
2303
+ if (!conv) {
2304
+ return false;
2305
+ }
2198
2306
  const now = Date.now();
2199
2307
  rawRun(
2308
+ "conversation:unarchive",
2200
2309
  "UPDATE conversations SET archived_at = NULL, updated_at = ? WHERE id = ?",
2201
2310
  now,
2202
2311
  id,
@@ -2215,6 +2324,7 @@ export function setConversationProcessingStartedAt(
2215
2324
  startedAt: number | null,
2216
2325
  ): void {
2217
2326
  rawRun(
2327
+ "conversation:setProcessingStartedAt",
2218
2328
  "UPDATE conversations SET processing_started_at = ? WHERE id = ?",
2219
2329
  startedAt,
2220
2330
  id,
@@ -2230,6 +2340,7 @@ export function setConversationProcessingStartedAt(
2230
2340
  */
2231
2341
  export function clearStaleProcessingFlags(): number {
2232
2342
  return rawRun(
2343
+ "conversation:clearStaleProcessingFlags",
2233
2344
  "UPDATE conversations SET processing_started_at = NULL WHERE processing_started_at IS NOT NULL",
2234
2345
  );
2235
2346
  }
@@ -2244,8 +2355,11 @@ export function clearStaleProcessingFlags(): number {
2244
2355
  */
2245
2356
  export function isConversationProcessing(id: string): boolean {
2246
2357
  const inMemory = findConversation(id)?.isProcessing();
2247
- if (inMemory != null) return inMemory;
2358
+ if (inMemory != null) {
2359
+ return inMemory;
2360
+ }
2248
2361
  const row = rawGet<{ processing_started_at: number | null }>(
2362
+ "conversation:isProcessing",
2249
2363
  "SELECT processing_started_at FROM conversations WHERE id = ?",
2250
2364
  id,
2251
2365
  );
@@ -2265,6 +2379,7 @@ export function isConversationProcessing(id: string): boolean {
2265
2379
  */
2266
2380
  export function getConversationPersistedSeq(id: string): number | null {
2267
2381
  const row = rawGet<{ seq: number | null }>(
2382
+ "conversation:getPersistedSeq",
2268
2383
  "SELECT seq FROM conversations WHERE id = ?",
2269
2384
  id,
2270
2385
  );
@@ -2282,8 +2397,11 @@ export function getConversationPersistedSeq(id: string): number | null {
2282
2397
  * Non-positive or non-finite `seq` values are ignored.
2283
2398
  */
2284
2399
  export function recordConversationPersistedSeq(id: string, seq: number): void {
2285
- if (!Number.isFinite(seq) || seq <= 0) return;
2400
+ if (!Number.isFinite(seq) || seq <= 0) {
2401
+ return;
2402
+ }
2286
2403
  rawRun(
2404
+ "conversation:recordPersistedSeq",
2287
2405
  "UPDATE conversations SET seq = ? WHERE id = ? AND (seq IS NULL OR seq < ?)",
2288
2406
  seq,
2289
2407
  id,
@@ -2309,10 +2427,13 @@ export function setConversationSurfaced(
2309
2427
  surfaced: boolean,
2310
2428
  ): { surfacedAt: number | null } | null {
2311
2429
  const conv = getConversation(id);
2312
- if (!conv) return null;
2430
+ if (!conv) {
2431
+ return null;
2432
+ }
2313
2433
  const now = Date.now();
2314
2434
  const surfacedAt = surfaced ? now : null;
2315
2435
  rawRun(
2436
+ "conversation:setSurfaced",
2316
2437
  "UPDATE conversations SET surfaced_at = ?, updated_at = ? WHERE id = ?",
2317
2438
  surfacedAt,
2318
2439
  now,
@@ -2346,6 +2467,51 @@ export function setConversationInferenceProfile(
2346
2467
  .run();
2347
2468
  }
2348
2469
 
2470
+ /**
2471
+ * Encode a plugin-id list for the `enabled_plugins` text column. Keeps a true
2472
+ * SQL NULL for `null` (rather than the JSON literal `"null"`) so it reads back
2473
+ * as "no per-chat restriction".
2474
+ */
2475
+ function encodeEnabledPlugins(plugins: string[] | null): string | null {
2476
+ return plugins === null ? null : JSON.stringify(plugins);
2477
+ }
2478
+
2479
+ /**
2480
+ * Read the per-conversation plugin scope. Returns the parsed `string[]` of
2481
+ * plugin ids, or `null` when the column is unset/empty (= no per-chat
2482
+ * restriction). Defensively returns `null` on a JSON parse failure.
2483
+ */
2484
+ export function getConversationEnabledPlugins(
2485
+ conversationId: string,
2486
+ ): string[] | null {
2487
+ const db = getDb();
2488
+ const row = db
2489
+ .select({ enabledPlugins: conversations.enabledPlugins })
2490
+ .from(conversations)
2491
+ .where(eq(conversations.id, conversationId))
2492
+ .get();
2493
+ return parseJsonNullable<string[]>()(row?.enabledPlugins);
2494
+ }
2495
+
2496
+ /**
2497
+ * Set or clear the per-conversation plugin scope. Pass a `string[]` to scope
2498
+ * the chat to those plugin ids, or `null` to clear the restriction and fall
2499
+ * back to all globally-enabled plugins.
2500
+ */
2501
+ export function setConversationEnabledPlugins(
2502
+ conversationId: string,
2503
+ plugins: string[] | null,
2504
+ ): void {
2505
+ const db = getDb();
2506
+ db.update(conversations)
2507
+ .set({
2508
+ enabledPlugins: encodeEnabledPlugins(plugins),
2509
+ updatedAt: Date.now(),
2510
+ })
2511
+ .where(eq(conversations.id, conversationId))
2512
+ .run();
2513
+ }
2514
+
2349
2515
  /**
2350
2516
  * Atomically set the inference profile, session id, and expiry timestamp for
2351
2517
  * a conversation. Pass `null` for all three to clear the session-backed
@@ -2391,7 +2557,9 @@ export function clearExpiredInferenceProfiles(
2391
2557
  )
2392
2558
  .all(now) as Array<{ conversationId: string; sessionId: string | null }>;
2393
2559
 
2394
- if (expired.length === 0) return [];
2560
+ if (expired.length === 0) {
2561
+ return [];
2562
+ }
2395
2563
 
2396
2564
  const ids = expired.map((r) => r.conversationId);
2397
2565
  const placeholders = ids.map(() => "?").join(", ");
@@ -2516,6 +2684,7 @@ export function setLastNotifiedInferenceProfile(
2516
2684
  profileKey: string | null,
2517
2685
  ): void {
2518
2686
  rawRun(
2687
+ "conversation:setLastNotifiedProfile",
2519
2688
  "UPDATE conversations SET last_notified_inference_profile = ? WHERE id = ?",
2520
2689
  profileKey,
2521
2690
  conversationId,
@@ -2539,9 +2708,15 @@ export async function clearAll(): Promise<{
2539
2708
  messages: number;
2540
2709
  }> {
2541
2710
  const msgCount =
2542
- rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM messages")?.c ?? 0;
2711
+ rawGet<{ c: number }>(
2712
+ "conversation:clearAll:countMessages",
2713
+ "SELECT COUNT(*) AS c FROM messages",
2714
+ )?.c ?? 0;
2543
2715
  const convCount =
2544
- rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM conversations")?.c ?? 0;
2716
+ rawGet<{ c: number }>(
2717
+ "conversation:clearAll:countConvs",
2718
+ "SELECT COUNT(*) AS c FROM conversations",
2719
+ )?.c ?? 0;
2545
2720
 
2546
2721
  // Each DELETE goes through `runAsyncSqlite`. The original code threw
2547
2722
  // on rawExec failure; mirror that here by throwing when the async
@@ -2561,71 +2736,48 @@ export async function clearAll(): Promise<{
2561
2736
  // Delete in dependency order. Cascades handle memory_segments and
2562
2737
  // tool_invocations, but we explicitly clear non-cascading memory
2563
2738
  // tables too.
2564
- //
2565
- // FTS virtual tables are cleared before their base tables. If an FTS
2566
- // table is corrupted, the DELETE will fail — we drop the associated
2567
- // triggers so that the subsequent base-table DELETEs don't also fail
2568
- // (SQLite triggers are atomic with the triggering statement, so a
2569
- // corrupted FTS table would roll back every base-table DELETE).
2570
2739
  await runOrThrow("DELETE FROM memory_segments");
2571
2740
  await runOrThrow("DELETE FROM memory_summaries");
2572
2741
  await runOrThrow("DELETE FROM memory_embeddings");
2573
2742
  // memory_jobs and llm_request_logs each live in their own dedicated
2574
2743
  // connection; clear them directly on those connections rather than through a
2575
2744
  // sqlite3 subprocess.
2576
- rawMemoryRun("DELETE FROM memory_jobs");
2745
+ rawMemoryRun("conversation:clearAll:memoryJobs", "DELETE FROM memory_jobs");
2577
2746
  await runOrThrow("DELETE FROM memory_checkpoints");
2578
- rawLogsRun("DELETE FROM llm_request_logs");
2747
+ rawLogsRun(
2748
+ "conversation:clearAll:requestLogs",
2749
+ "DELETE FROM llm_request_logs",
2750
+ );
2579
2751
  await runOrThrow("DELETE FROM llm_usage_events");
2580
2752
  await runOrThrow("DELETE FROM message_attachments");
2581
2753
  await runOrThrow("DELETE FROM attachments");
2582
2754
  await runOrThrow("DELETE FROM tool_invocations");
2583
2755
  await runOrThrow("DELETE FROM skill_loaded_events");
2584
- let messagesFtsCorrupted = false;
2585
- const ftsResult = await runAsyncSqlite(
2586
- "DELETE FROM messages_fts",
2587
- "clearAll:messages-fts",
2588
- );
2589
- if (!ftsResult.ok) {
2590
- log.warn(
2591
- { error: ftsResult.error, backend: ftsResult.backend },
2592
- "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed",
2593
- );
2594
- rawExec("DROP TRIGGER IF EXISTS messages_fts_ai");
2595
- rawExec("DROP TRIGGER IF EXISTS messages_fts_ad");
2596
- rawExec("DROP TRIGGER IF EXISTS messages_fts_au");
2597
- messagesFtsCorrupted = true;
2598
- }
2599
2756
  await runOrThrow("DELETE FROM messages");
2600
2757
  await runOrThrow("DELETE FROM conversations");
2601
2758
 
2602
2759
  // Record audit event — lifecycle_events is NOT deleted by clearAll(),
2603
2760
  // so this survives the wipe and provides a permanent trail.
2604
2761
  rawRun(
2762
+ "conversation:clearAll:auditEvent",
2605
2763
  `INSERT INTO lifecycle_events (id, event_name, created_at) VALUES (?, ?, ?)`,
2606
2764
  uuid(),
2607
2765
  "conversations_clear_all",
2608
2766
  Date.now(),
2609
2767
  );
2610
2768
 
2611
- // Rebuild corrupted FTS tables and restore triggers after all base-table
2612
- // DELETEs have completed. Dropping the virtual table clears the corruption,
2613
- // and recreating it + triggers means subsequent writes maintain FTS
2614
- // consistency without requiring a daemon restart.
2615
- if (messagesFtsCorrupted) {
2616
- rawExec("DROP TABLE IF EXISTS messages_fts");
2617
- rawExec(
2618
- `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`,
2619
- );
2620
- rawExec(
2621
- `CREATE TRIGGER IF NOT EXISTS messages_fts_ai AFTER INSERT ON messages BEGIN INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`,
2622
- );
2623
- rawExec(
2624
- `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`,
2625
- );
2626
- rawExec(
2627
- `CREATE TRIGGER IF NOT EXISTS messages_fts_au AFTER UPDATE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; INSERT INTO messages_fts(message_id, content) VALUES (new.id, new.content); END`,
2628
- );
2769
+ // Let the memory feature drop its bulk per-message index (e.g. the whole
2770
+ // lexical Qdrant collection) a "delete all" leaves no ids to key
2771
+ // per-message cleanup on. Routed through the persistence-hook seam so this
2772
+ // layer stays decoupled from the memory plugin; a no-op when memory is not
2773
+ // present. AWAITED so the drop completes before clear-all returns and writes
2774
+ // resume a message created right after must not upsert into a collection
2775
+ // that is about to be dropped. Best-effort — a failure must not fail the
2776
+ // whole clear-all.
2777
+ try {
2778
+ await getMemoryPersistenceHooks().onAllConversationsCleared();
2779
+ } catch (err) {
2780
+ log.warn({ err }, "clearAll: failed to clear memory per-message index");
2629
2781
  }
2630
2782
 
2631
2783
  // Clear the disk-view conversations directory and recreate it empty
@@ -2658,7 +2810,9 @@ export function deleteLastExchange(conversationId: string): number {
2658
2810
  .limit(1)
2659
2811
  .get();
2660
2812
 
2661
- if (!lastUserMsg) return 0;
2813
+ if (!lastUserMsg) {
2814
+ return 0;
2815
+ }
2662
2816
 
2663
2817
  // Use rowid to identify the last user message and everything after it.
2664
2818
  // rowid is monotonically increasing for inserts, so this is safe even if
@@ -2674,7 +2828,9 @@ export function deleteLastExchange(conversationId: string): number {
2674
2828
  .from(messages)
2675
2829
  .where(condition)
2676
2830
  .all();
2677
- if (deleted === 0) return 0;
2831
+ if (deleted === 0) {
2832
+ return 0;
2833
+ }
2678
2834
 
2679
2835
  // Collect attachment IDs linked to the messages being deleted so we can
2680
2836
  // scope orphan cleanup to only those candidates (not freshly uploaded ones).
@@ -2713,6 +2869,12 @@ export function deleteLastExchange(conversationId: string): number {
2713
2869
 
2714
2870
  deleteOrphanAttachments(candidateAttachmentIds);
2715
2871
 
2872
+ // Let the memory feature clean up the undone messages' per-message index
2873
+ // entries. This bulk delete bypasses `deleteMessageById`, so notify the seam
2874
+ // with the collected ids — after the transaction and off the write path. A
2875
+ // no-op when memory is not present.
2876
+ getMemoryPersistenceHooks().onMessagesDeleted(messageIds);
2877
+
2716
2878
  return deleted;
2717
2879
  }
2718
2880
 
@@ -2756,6 +2918,13 @@ export async function reserveMessage(
2756
2918
  /**
2757
2919
  * Update the content of an existing message. Used when consolidating
2758
2920
  * multiple assistant messages into one.
2921
+ *
2922
+ * This is a pure CRUD primitive: it does NOT enqueue a lexical reindex, because
2923
+ * it is also driven by mid-stream partial flushes and high-frequency tool-timing
2924
+ * stamps (`_startedAt`/`_previewStartedAt`) that either don't change searchable
2925
+ * text or would spam reindex jobs. Callers on genuine content-change seams
2926
+ * (streaming finalize, channel edits, consolidation) enqueue the reindex
2927
+ * themselves via `enqueueLexicalIndexForMessage`.
2759
2928
  */
2760
2929
  export function updateMessageContent(
2761
2930
  messageId: string,
@@ -2831,7 +3000,9 @@ export function relinkAttachments(
2831
3000
  fromMessageIds: string[],
2832
3001
  toMessageId: string,
2833
3002
  ): number {
2834
- if (fromMessageIds.length === 0) return 0;
3003
+ if (fromMessageIds.length === 0) {
3004
+ return 0;
3005
+ }
2835
3006
  const db = getDb();
2836
3007
 
2837
3008
  // Count how many links will be moved before updating.
@@ -2841,7 +3012,9 @@ export function relinkAttachments(
2841
3012
  .where(inArray(messageAttachments.messageId, fromMessageIds))
2842
3013
  .all();
2843
3014
 
2844
- if (total === 0) return 0;
3015
+ if (total === 0) {
3016
+ return 0;
3017
+ }
2845
3018
 
2846
3019
  db.update(messageAttachments)
2847
3020
  .set({ messageId: toMessageId })
@@ -2933,6 +3106,15 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
2933
3106
 
2934
3107
  deleteOrphanAttachments(candidateAttachmentIds);
2935
3108
 
3109
+ // Let the memory feature clean up the deleted message's per-message index
3110
+ // entry. Notified only when the row actually existed (`msgRow` set), after the
3111
+ // transaction and off the write path, via the persistence-hook seam (a no-op
3112
+ // when memory is not present). Covers single-message deletes (consolidation)
3113
+ // that do not go through the conversation-level purge.
3114
+ if (msgRow) {
3115
+ getMemoryPersistenceHooks().onMessagesDeleted([messageId]);
3116
+ }
3117
+
2936
3118
  return result;
2937
3119
  }
2938
3120
 
@@ -3009,18 +3191,13 @@ export function getConversationRecentProvenanceTrustClass(
3009
3191
  | "unknown"
3010
3192
  | undefined {
3011
3193
  const row = rawGet<{ metadata: string | null }>(
3194
+ "conversation:getProvenanceTrustClass",
3012
3195
  `SELECT metadata FROM messages
3013
3196
  WHERE conversation_id = ? AND role = 'user' AND metadata IS NOT NULL
3014
3197
  ORDER BY created_at DESC LIMIT 1`,
3015
3198
  conversationId,
3016
3199
  );
3017
- if (!row?.metadata) return undefined;
3018
- try {
3019
- const parsed = messageMetadataSchema.safeParse(JSON.parse(row.metadata));
3020
- return parsed.success ? parsed.data.provenanceTrustClass : undefined;
3021
- } catch {
3022
- return undefined;
3023
- }
3200
+ return parseMessageMetadata(row?.metadata ?? null)?.provenanceTrustClass;
3024
3201
  }
3025
3202
 
3026
3203
  // ---------------------------------------------------------------------------
@@ -3051,6 +3228,7 @@ export function batchSetDisplayOrders(
3051
3228
  safeGroupId = "system:all";
3052
3229
  } else if (
3053
3230
  !rawGet<{ id: string }>(
3231
+ "conversation:batchSetDisplayOrders:groupCheck",
3054
3232
  "SELECT id FROM conversation_groups WHERE id = ?",
3055
3233
  safeGroupId,
3056
3234
  )
@@ -3066,6 +3244,7 @@ export function batchSetDisplayOrders(
3066
3244
  safeGroupId === "system:background" ||
3067
3245
  safeGroupId === "system:scheduled";
3068
3246
  rawRun(
3247
+ "conversation:batchSetDisplayOrders:group",
3069
3248
  `UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ?${
3070
3249
  clearsSurfaced ? ", surfaced_at = NULL" : ""
3071
3250
  } WHERE id = ?`,
@@ -3077,12 +3256,14 @@ export function batchSetDisplayOrders(
3077
3256
  } else if (update.isPinned === undefined) {
3078
3257
  // Only displayOrder provided — preserve existing pin state and group.
3079
3258
  rawRun(
3259
+ "conversation:batchSetDisplayOrders:orderOnly",
3080
3260
  "UPDATE conversations SET display_order = ? WHERE id = ?",
3081
3261
  update.displayOrder,
3082
3262
  update.id,
3083
3263
  );
3084
3264
  } else if (update.isPinned) {
3085
3265
  rawRun(
3266
+ "conversation:batchSetDisplayOrders:pin",
3086
3267
  "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
3087
3268
  update.displayOrder,
3088
3269
  update.id,
@@ -3091,6 +3272,7 @@ export function batchSetDisplayOrders(
3091
3272
  // Restore system group from source/conversationType when unpinning,
3092
3273
  // instead of clearing to NULL (which would lose provenance).
3093
3274
  rawRun(
3275
+ "conversation:batchSetDisplayOrders:unpin",
3094
3276
  `UPDATE conversations SET display_order = ?, is_pinned = 0,
3095
3277
  group_id = CASE WHEN group_id = 'system:pinned' THEN
3096
3278
  CASE
@@ -3125,13 +3307,16 @@ export function getDisplayMetaForConversations(
3125
3307
  string,
3126
3308
  { displayOrder: number | null; isPinned: boolean; groupId: string | null }
3127
3309
  >();
3128
- if (conversationIds.length === 0) return result;
3310
+ if (conversationIds.length === 0) {
3311
+ return result;
3312
+ }
3129
3313
  for (const id of conversationIds) {
3130
3314
  const row = rawGet<{
3131
3315
  display_order: number | null;
3132
3316
  is_pinned: number | null;
3133
3317
  group_id: string | null;
3134
3318
  }>(
3319
+ "conversation:getDisplayMeta",
3135
3320
  "SELECT display_order, is_pinned, group_id FROM conversations WHERE id = ?",
3136
3321
  id,
3137
3322
  );
@@ -3154,10 +3339,14 @@ export function getDisplayMetaForConversations(
3154
3339
  * be treated as turn boundaries.
3155
3340
  */
3156
3341
  function isToolResultMessage(role: string, content: string): boolean {
3157
- if (role !== "user") return false;
3342
+ if (role !== "user") {
3343
+ return false;
3344
+ }
3158
3345
  try {
3159
3346
  const parsed = JSON.parse(content);
3160
- if (!Array.isArray(parsed) || parsed.length === 0) return false;
3347
+ if (!Array.isArray(parsed) || parsed.length === 0) {
3348
+ return false;
3349
+ }
3161
3350
  return parsed.every(
3162
3351
  (block: unknown) =>
3163
3352
  block != null &&
@@ -3313,7 +3502,9 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
3313
3502
 
3314
3503
  // Look up the target message to get its conversationId and createdAt.
3315
3504
  const target = getMessageById(messageId);
3316
- if (!target) return [messageId];
3505
+ if (!target) {
3506
+ return [messageId];
3507
+ }
3317
3508
 
3318
3509
  // Walk backward from the target message to find the turn boundary.
3319
3510
  // Limit to 50 rows — sufficient for even aggressive tool-use loops.
@@ -3418,7 +3609,9 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
3418
3609
 
3419
3610
  // Sort by createdAt to ensure stable ordering.
3420
3611
  // Re-fetch createdAt for all collected IDs so the sort is accurate.
3421
- if (assistantIds.length <= 1) return assistantIds;
3612
+ if (assistantIds.length <= 1) {
3613
+ return assistantIds;
3614
+ }
3422
3615
 
3423
3616
  const idSet = new Set(assistantIds);
3424
3617
  const sorted = db