@vellumai/assistant 0.5.16 → 0.6.0

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 (407) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +0 -3
  3. package/knip.json +2 -1
  4. package/openapi.yaml +660 -80
  5. package/package.json +1 -1
  6. package/src/__tests__/actor-token-service.test.ts +68 -0
  7. package/src/__tests__/agent-loop.test.ts +0 -32
  8. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  9. package/src/__tests__/anthropic-provider.test.ts +57 -3
  10. package/src/__tests__/app-compiler.test.ts +120 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  12. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  13. package/src/__tests__/call-domain.test.ts +2 -6
  14. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  15. package/src/__tests__/call-recovery.test.ts +2 -6
  16. package/src/__tests__/call-routes-http.test.ts +2 -6
  17. package/src/__tests__/call-store.test.ts +2 -6
  18. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  19. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  20. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  21. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  22. package/src/__tests__/checker.test.ts +25 -3
  23. package/src/__tests__/clawhub.test.ts +54 -24
  24. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  25. package/src/__tests__/cli-memory.test.ts +74 -69
  26. package/src/__tests__/config-schema.test.ts +1 -1
  27. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  28. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  29. package/src/__tests__/contacts-tools.test.ts +31 -0
  30. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  31. package/src/__tests__/context-token-estimator.test.ts +175 -10
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +9 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +9 -0
  34. package/src/__tests__/conversation-attachments.test.ts +2 -6
  35. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  36. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  37. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  38. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  39. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  40. package/src/__tests__/conversation-error.test.ts +33 -2
  41. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  42. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  43. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  44. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  45. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  46. package/src/__tests__/conversation-store.test.ts +2 -6
  47. package/src/__tests__/conversation-usage.test.ts +2 -6
  48. package/src/__tests__/conversation-wipe.test.ts +11 -408
  49. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  50. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  51. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  52. package/src/__tests__/followup-tools.test.ts +2 -6
  53. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  54. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  55. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  56. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  57. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  58. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  59. package/src/__tests__/guardian-action-store.test.ts +2 -6
  60. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  61. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  62. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  63. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  64. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  65. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  66. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  67. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  68. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  69. package/src/__tests__/injection-block.test.ts +154 -0
  70. package/src/__tests__/install-meta.test.ts +506 -0
  71. package/src/__tests__/install-skill-routing.test.ts +292 -0
  72. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  73. package/src/__tests__/invite-routes-http.test.ts +2 -6
  74. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +2 -14
  75. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  76. package/src/__tests__/llm-context-route-provider.test.ts +2 -6
  77. package/src/__tests__/llm-request-log-turn-query.test.ts +2 -6
  78. package/src/__tests__/llm-usage-store.test.ts +2 -6
  79. package/src/__tests__/log-export-workspace.test.ts +2 -6
  80. package/src/__tests__/managed-store.test.ts +38 -11
  81. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  82. package/src/__tests__/memory-recall-log-store.test.ts +2 -6
  83. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  84. package/src/__tests__/non-member-access-request.test.ts +2 -6
  85. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  86. package/src/__tests__/oauth-cli.test.ts +364 -2
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  88. package/src/__tests__/outlook-attachments.test.ts +301 -0
  89. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  90. package/src/__tests__/outlook-categories.test.ts +212 -0
  91. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  92. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  93. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  94. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  95. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  96. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  97. package/src/__tests__/outlook-trash.test.ts +77 -0
  98. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  99. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  100. package/src/__tests__/playbook-execution.test.ts +76 -80
  101. package/src/__tests__/playbook-tools.test.ts +5 -7
  102. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  103. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  104. package/src/__tests__/registry.test.ts +2 -2
  105. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  106. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  107. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  108. package/src/__tests__/schedule-store.test.ts +2 -6
  109. package/src/__tests__/schedule-tools.test.ts +2 -6
  110. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  111. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  112. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  113. package/src/__tests__/search-skills-unified.test.ts +421 -0
  114. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  115. package/src/__tests__/send-endpoint-busy.test.ts +2 -6
  116. package/src/__tests__/sequence-store.test.ts +2 -6
  117. package/src/__tests__/server-history-render.test.ts +2 -6
  118. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  119. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  120. package/src/__tests__/skill-load-feature-flag.test.ts +11 -11
  121. package/src/__tests__/skill-memory.test.ts +140 -98
  122. package/src/__tests__/skills-uninstall.test.ts +2 -2
  123. package/src/__tests__/skills.test.ts +1 -1
  124. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  125. package/src/__tests__/task-compiler.test.ts +2 -6
  126. package/src/__tests__/task-management-tools.test.ts +2 -6
  127. package/src/__tests__/task-memory-cleanup.test.ts +173 -229
  128. package/src/__tests__/task-runner.test.ts +2 -6
  129. package/src/__tests__/task-scheduler.test.ts +2 -6
  130. package/src/__tests__/test-preload.ts +3 -0
  131. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  132. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  133. package/src/__tests__/tool-side-effects-slack-dm.test.ts +276 -0
  134. package/src/__tests__/trust-store.test.ts +1 -1
  135. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -6
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  138. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  139. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  140. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  141. package/src/__tests__/usage-routes.test.ts +2 -6
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  143. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  145. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  146. package/src/__tests__/volume-security-guard.test.ts +2 -0
  147. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  148. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  149. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  150. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  151. package/src/__tests__/workspace-policy.test.ts +1 -1
  152. package/src/agent/attachments.ts +7 -2
  153. package/src/agent/image-optimize.ts +165 -0
  154. package/src/agent/loop.ts +1 -15
  155. package/src/bundler/app-compiler.ts +179 -2
  156. package/src/bundler/package-resolver.ts +3 -5
  157. package/src/cli/__tests__/notifications.test.ts +1 -2
  158. package/src/cli/cli-memory.ts +67 -64
  159. package/src/cli/commands/avatar.ts +3 -3
  160. package/src/cli/commands/config.ts +26 -13
  161. package/src/cli/commands/doctor.ts +2 -2
  162. package/src/cli/commands/memory.ts +41 -55
  163. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  164. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  165. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  166. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  167. package/src/cli/commands/oauth/connect.ts +11 -6
  168. package/src/cli/commands/oauth/mode.ts +7 -0
  169. package/src/cli/commands/oauth/shared.ts +39 -3
  170. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  171. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  172. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  173. package/src/cli/commands/platform/index.ts +16 -16
  174. package/src/cli/commands/skills.ts +88 -16
  175. package/src/cli/commands/trust.ts +2 -2
  176. package/src/cli/lib/daemon-credential-client.ts +2 -3
  177. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  178. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  179. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  180. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  181. package/src/config/bundled-skills/gmail/SKILL.md +2 -10
  182. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  183. package/src/config/bundled-skills/messaging/SKILL.md +10 -18
  184. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  185. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  186. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  187. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  188. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  189. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  190. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  191. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  192. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  193. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  194. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  195. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  196. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  197. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  198. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  199. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  200. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  201. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  202. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  203. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  204. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  205. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  206. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  207. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  208. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  209. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  210. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  211. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  212. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  213. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  214. package/src/config/bundled-skills/slack/SKILL.md +1 -7
  215. package/src/config/bundled-tool-registry.ts +56 -4
  216. package/src/config/env-registry.ts +15 -8
  217. package/src/config/feature-flag-registry.json +21 -124
  218. package/src/config/schemas/platform.ts +8 -0
  219. package/src/config/schemas/timeouts.ts +1 -1
  220. package/src/config/skills.ts +18 -7
  221. package/src/context/token-estimator.ts +25 -18
  222. package/src/context/window-manager.ts +6 -2
  223. package/src/credential-execution/process-manager.ts +3 -1
  224. package/src/daemon/context-overflow-reducer.ts +46 -2
  225. package/src/daemon/conversation-agent-loop-handlers.ts +123 -82
  226. package/src/daemon/conversation-agent-loop.ts +96 -61
  227. package/src/daemon/conversation-error.ts +31 -8
  228. package/src/daemon/conversation-lifecycle.ts +33 -0
  229. package/src/daemon/conversation-media-retry.ts +85 -7
  230. package/src/daemon/conversation-notifiers.ts +4 -1
  231. package/src/daemon/conversation-runtime-assembly.ts +5 -0
  232. package/src/daemon/conversation.ts +41 -2
  233. package/src/daemon/daemon-control.ts +8 -2
  234. package/src/daemon/handlers/shared.ts +22 -12
  235. package/src/daemon/handlers/skills.ts +416 -202
  236. package/src/daemon/lifecycle.ts +40 -1
  237. package/src/daemon/main.ts +5 -1
  238. package/src/daemon/message-types/conversations.ts +4 -1
  239. package/src/daemon/message-types/messages.ts +3 -1
  240. package/src/daemon/message-types/skills.ts +97 -36
  241. package/src/daemon/providers-setup.ts +5 -0
  242. package/src/daemon/server.ts +11 -2
  243. package/src/daemon/tool-side-effects.ts +27 -5
  244. package/src/heartbeat/heartbeat-service.ts +1 -0
  245. package/src/hooks/cli.ts +2 -2
  246. package/src/hooks/runner.ts +15 -38
  247. package/src/inbound/platform-callback-registration.ts +14 -14
  248. package/src/memory/admin.ts +11 -45
  249. package/src/memory/conversation-bootstrap.ts +2 -0
  250. package/src/memory/conversation-crud.ts +242 -348
  251. package/src/memory/conversation-group-migration.ts +157 -0
  252. package/src/memory/conversation-queries.ts +4 -2
  253. package/src/memory/db-init.ts +30 -3
  254. package/src/memory/embed.ts +73 -0
  255. package/src/memory/embedding-backend.ts +8 -14
  256. package/src/memory/embedding-runtime-manager.ts +12 -114
  257. package/src/memory/fingerprint.ts +2 -2
  258. package/src/memory/graph/bootstrap.ts +512 -0
  259. package/src/memory/graph/capability-seed.ts +297 -0
  260. package/src/memory/graph/consolidation.ts +691 -0
  261. package/src/memory/graph/conversation-graph-memory.ts +630 -0
  262. package/src/memory/graph/decay.test.ts +208 -0
  263. package/src/memory/graph/decay.ts +195 -0
  264. package/src/memory/graph/extraction-job.ts +69 -0
  265. package/src/memory/graph/extraction.test.ts +936 -0
  266. package/src/memory/graph/extraction.ts +1254 -0
  267. package/src/memory/graph/graph-search.ts +266 -0
  268. package/src/memory/graph/image-ref-utils.ts +29 -0
  269. package/src/memory/graph/injection.test.ts +513 -0
  270. package/src/memory/graph/injection.ts +439 -0
  271. package/src/memory/graph/inspect.ts +534 -0
  272. package/src/memory/graph/narrative.ts +267 -0
  273. package/src/memory/graph/pattern-scan.ts +269 -0
  274. package/src/memory/graph/retriever.ts +1008 -0
  275. package/src/memory/graph/scoring.test.ts +548 -0
  276. package/src/memory/graph/scoring.ts +232 -0
  277. package/src/memory/graph/serendipity.ts +65 -0
  278. package/src/memory/graph/store.test.ts +1050 -0
  279. package/src/memory/graph/store.ts +699 -0
  280. package/src/memory/graph/tool-handlers.ts +426 -0
  281. package/src/memory/graph/tools.ts +141 -0
  282. package/src/memory/graph/triggers.test.ts +487 -0
  283. package/src/memory/graph/triggers.ts +223 -0
  284. package/src/memory/graph/types.ts +271 -0
  285. package/src/memory/group-crud.ts +191 -0
  286. package/src/memory/indexer.ts +37 -19
  287. package/src/memory/job-handlers/cleanup.ts +0 -53
  288. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  289. package/src/memory/job-handlers/embedding.ts +5 -31
  290. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  291. package/src/memory/job-handlers/summarization.ts +32 -17
  292. package/src/memory/job-utils.ts +1 -1
  293. package/src/memory/jobs-store.ts +50 -70
  294. package/src/memory/jobs-worker.ts +147 -112
  295. package/src/memory/message-content.ts +1 -0
  296. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  297. package/src/memory/migrations/203-drop-memory-items-tables.ts +23 -0
  298. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  299. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  300. package/src/memory/migrations/index.ts +4 -0
  301. package/src/memory/migrations/registry.ts +8 -0
  302. package/src/memory/qdrant-client.ts +44 -17
  303. package/src/memory/schema/index.ts +1 -0
  304. package/src/memory/schema/memory-graph.ts +139 -0
  305. package/src/memory/search/semantic.ts +47 -91
  306. package/src/memory/task-memory-cleanup.ts +28 -50
  307. package/src/messaging/providers/outlook/adapter.ts +8 -1
  308. package/src/messaging/providers/outlook/client.ts +299 -0
  309. package/src/messaging/providers/outlook/types.ts +118 -0
  310. package/src/notifications/adapters/macos.ts +1 -0
  311. package/src/notifications/copy-composer.ts +9 -0
  312. package/src/notifications/signal.ts +16 -0
  313. package/src/oauth/seed-providers.ts +2 -1
  314. package/src/permissions/checker.ts +24 -3
  315. package/src/permissions/defaults.ts +4 -4
  316. package/src/permissions/workspace-policy.ts +1 -1
  317. package/src/playbooks/playbook-compiler.ts +19 -18
  318. package/src/playbooks/types.ts +4 -3
  319. package/src/prompts/system-prompt.ts +3 -29
  320. package/src/providers/anthropic/client.ts +47 -19
  321. package/src/providers/gemini/client.ts +1 -1
  322. package/src/providers/openai/client.ts +1 -1
  323. package/src/providers/registry.ts +1 -1
  324. package/src/providers/retry.ts +19 -3
  325. package/src/runtime/actor-trust-resolver.ts +5 -1
  326. package/src/runtime/auth/route-policy.ts +7 -0
  327. package/src/runtime/guardian-reply-router.ts +5 -1
  328. package/src/runtime/http-server.ts +23 -3
  329. package/src/runtime/middleware/auth.ts +20 -0
  330. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  331. package/src/runtime/routes/attachment-routes.ts +106 -16
  332. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  333. package/src/runtime/routes/btw-routes.ts +8 -0
  334. package/src/runtime/routes/conversation-management-routes.ts +2 -0
  335. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  336. package/src/runtime/routes/debug-routes.ts +1 -1
  337. package/src/runtime/routes/global-search-routes.ts +21 -19
  338. package/src/runtime/routes/group-routes.ts +207 -0
  339. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  340. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  341. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  342. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  343. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +2 -14
  345. package/src/runtime/routes/memory-item-routes.ts +341 -388
  346. package/src/runtime/routes/schedule-routes.ts +2 -0
  347. package/src/runtime/routes/skills-routes.ts +103 -37
  348. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  349. package/src/schedule/scheduler.ts +8 -1
  350. package/src/security/oauth2.ts +1 -1
  351. package/src/security/secure-keys.ts +4 -8
  352. package/src/shared/provider-env-vars.ts +19 -0
  353. package/src/skills/catalog-cache.ts +5 -0
  354. package/src/skills/catalog-install.ts +15 -14
  355. package/src/skills/clawhub.ts +134 -154
  356. package/src/skills/install-meta.ts +208 -0
  357. package/src/skills/managed-store.ts +27 -16
  358. package/src/skills/skill-memory.ts +152 -77
  359. package/src/skills/skillssh-registry.ts +19 -17
  360. package/src/tasks/task-runner.ts +3 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  362. package/src/tools/browser/runtime-check.ts +3 -1
  363. package/src/tools/memory/register.ts +63 -46
  364. package/src/tools/permission-checker.ts +7 -1
  365. package/src/tools/shared/filesystem/image-read.ts +22 -85
  366. package/src/tools/terminal/safe-env.ts +1 -0
  367. package/src/tools/tool-manifest.ts +3 -3
  368. package/src/util/browser.ts +25 -10
  369. package/src/util/bun-runtime.ts +172 -0
  370. package/src/watcher/providers/outlook-calendar.ts +343 -0
  371. package/src/watcher/providers/outlook.ts +198 -0
  372. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  373. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  374. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  375. package/src/workspace/migrations/registry.ts +6 -0
  376. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  377. package/src/__tests__/journal-context.test.ts +0 -268
  378. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  379. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  380. package/src/__tests__/memory-query-builder.test.ts +0 -59
  381. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  382. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  383. package/src/__tests__/memory-regressions.test.ts +0 -3696
  384. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  385. package/src/daemon/conversation-memory.ts +0 -207
  386. package/src/memory/conversation-starters-cadence.ts +0 -74
  387. package/src/memory/items-extractor.ts +0 -860
  388. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  389. package/src/memory/job-handlers/extraction.ts +0 -40
  390. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  391. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  392. package/src/memory/journal-memory.ts +0 -224
  393. package/src/memory/query-builder.ts +0 -47
  394. package/src/memory/query-expansion.ts +0 -83
  395. package/src/memory/retriever.test.ts +0 -1592
  396. package/src/memory/retriever.ts +0 -1331
  397. package/src/memory/search/formatting.test.ts +0 -140
  398. package/src/memory/search/formatting.ts +0 -262
  399. package/src/memory/search/mmr.ts +0 -139
  400. package/src/memory/search/ranking.ts +0 -15
  401. package/src/memory/search/staleness.ts +0 -40
  402. package/src/memory/search/tier-classifier.ts +0 -18
  403. package/src/memory/search/types.ts +0 -121
  404. package/src/prompts/journal-context.ts +0 -154
  405. package/src/tools/memory/definitions.ts +0 -69
  406. package/src/tools/memory/handlers.test.ts +0 -562
  407. package/src/tools/memory/handlers.ts +0 -434
@@ -41,17 +41,15 @@ import {
41
41
  updateMetaFile,
42
42
  } from "./conversation-disk-view.js";
43
43
  import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
44
- import { getDb, rawAll, rawExec, rawGet, rawRun } from "./db.js";
44
+ import { ensureGroupMigration } from "./conversation-group-migration.js";
45
+ import { getDb, rawExec, rawGet, rawRun } from "./db.js";
45
46
  import { indexMessageNow } from "./indexer.js";
46
- import { enqueueMemoryJob } from "./jobs-store.js";
47
47
  import {
48
48
  channelInboundEvents,
49
49
  conversations,
50
50
  conversationStarters,
51
51
  llmRequestLogs,
52
52
  memoryEmbeddings,
53
- memoryItems,
54
- memoryItemSources,
55
53
  memorySegments,
56
54
  memorySummaries,
57
55
  messageAttachments,
@@ -110,7 +108,7 @@ export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
110
108
 
111
109
  function cloneForkMessageMetadata(
112
110
  metadata: string | null,
113
- sourceMessageId: string,
111
+ sourceMessageId: string
114
112
  ): string {
115
113
  if (!metadata) {
116
114
  return JSON.stringify({ forkSourceMessageId: sourceMessageId });
@@ -143,7 +141,7 @@ function cloneForkMessageMetadata(
143
141
  * callers with actual guardian trust should always supply a real context.
144
142
  */
145
143
  export function provenanceFromTrustContext(
146
- ctx: TrustContext | null | undefined,
144
+ ctx: TrustContext | null | undefined
147
145
  ): Record<string, unknown> {
148
146
  if (!ctx) return { provenanceTrustClass: "unknown" };
149
147
  return {
@@ -244,19 +242,27 @@ export function createConversation(
244
242
  conversationType?: "standard" | "private" | "background";
245
243
  source?: string;
246
244
  scheduleJobId?: string;
247
- },
245
+ groupId?: string;
246
+ }
248
247
  ) {
249
248
  const db = getDb();
250
249
  const now = Date.now();
251
250
  const opts =
252
251
  typeof titleOrOpts === "string"
253
252
  ? { title: titleOrOpts }
254
- : (titleOrOpts ?? {});
253
+ : titleOrOpts ?? {};
255
254
  const conversationType = opts.conversationType ?? "standard";
256
255
  const source = opts.source ?? "user";
256
+ const groupId = opts.groupId;
257
257
  const id = uuid();
258
258
  const memoryScopeId =
259
259
  conversationType === "private" ? `private:${id}` : "default";
260
+
261
+ // Ensure group_id column exists for deterministic schema readiness,
262
+ // even when this conversation has no groupId (a subsequent query or
263
+ // reorder may reference the column).
264
+ ensureGroupMigration();
265
+
260
266
  const conversation = {
261
267
  id,
262
268
  title: opts.title ?? null,
@@ -276,6 +282,12 @@ export function createConversation(
276
282
 
277
283
  // Retry on SQLITE_BUSY and SQLITE_IOERR — transient disk I/O errors or WAL
278
284
  // contention can cause the first attempt to fail even under normal load.
285
+ // INSERT and group_id UPDATE are retried independently so a transient failure
286
+ // on the UPDATE doesn't re-execute the already-succeeded INSERT (which would
287
+ // hit a unique constraint violation).
288
+ // No explicit BEGIN/COMMIT here — callers that need atomicity (e.g.
289
+ // forkConversation) wrap in their own transaction, and nesting raw BEGIN
290
+ // inside Drizzle's db.transaction() would crash SQLite.
279
291
  const MAX_RETRIES = 3;
280
292
  for (let attempt = 0; ; attempt++) {
281
293
  try {
@@ -289,10 +301,8 @@ export function createConversation(
289
301
  ) {
290
302
  log.warn(
291
303
  { attempt, conversationId: id, code },
292
- "createConversation: transient SQLite error, retrying",
304
+ "createConversation: INSERT transient error, retrying"
293
305
  );
294
- // Synchronous sleep — createConversation is synchronous and the
295
- // retry window is short (50-150ms), so Bun.sleepSync is appropriate.
296
306
  Bun.sleepSync(50 * (attempt + 1));
297
307
  continue;
298
308
  }
@@ -300,6 +310,35 @@ export function createConversation(
300
310
  }
301
311
  }
302
312
 
313
+ // group_id is NOT in the Drizzle schema (raw-query-only pattern).
314
+ // Set via raw SQL after the INSERT succeeds.
315
+ if (groupId) {
316
+ for (let attempt = 0; ; attempt++) {
317
+ try {
318
+ rawRun(
319
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
320
+ groupId,
321
+ id
322
+ );
323
+ break;
324
+ } catch (err) {
325
+ const code = (err as { code?: string }).code ?? "";
326
+ if (
327
+ attempt < MAX_RETRIES &&
328
+ (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_IOERR"))
329
+ ) {
330
+ log.warn(
331
+ { attempt, conversationId: id, code },
332
+ "createConversation: group_id UPDATE transient error, retrying"
333
+ );
334
+ Bun.sleepSync(50 * (attempt + 1));
335
+ continue;
336
+ }
337
+ throw err;
338
+ }
339
+ }
340
+ }
341
+
303
342
  initConversationDir({ ...conversation, originChannel: null });
304
343
 
305
344
  return conversation;
@@ -321,18 +360,18 @@ export function getConversation(id: string): ConversationRow | null {
321
360
  * (i.e. no other conversations still reference it).
322
361
  */
323
362
  export function countConversationsByScheduleJobId(
324
- scheduleJobId: string,
363
+ scheduleJobId: string
325
364
  ): number {
326
365
  return (
327
366
  rawGet<{ c: number }>(
328
367
  "SELECT COUNT(*) AS c FROM conversations WHERE schedule_job_id = ?",
329
- scheduleJobId,
368
+ scheduleJobId
330
369
  )?.c ?? 0
331
370
  );
332
371
  }
333
372
 
334
373
  export function getConversationType(
335
- conversationId: string,
374
+ conversationId: string
336
375
  ): "standard" | "private" {
337
376
  const conv = getConversation(conversationId);
338
377
  const raw = conv?.conversationType;
@@ -344,6 +383,20 @@ export function getConversationMemoryScopeId(conversationId: string): string {
344
383
  return conv?.memoryScopeId ?? "default";
345
384
  }
346
385
 
386
+ /**
387
+ * Fetch group_id for a conversation via raw SQL. group_id is NOT in the
388
+ * Drizzle schema (raw-query-only pattern), so ConversationRow doesn't
389
+ * include it. This helper is used by forkConversation to inherit group_id.
390
+ */
391
+ export function getConversationGroupId(conversationId: string): string | null {
392
+ ensureGroupMigration();
393
+ const row = rawGet<{ group_id: string | null }>(
394
+ "SELECT group_id FROM conversations WHERE id = ?",
395
+ conversationId
396
+ );
397
+ return row?.group_id ?? null;
398
+ }
399
+
347
400
  export function forkConversation(params: {
348
401
  conversationId: string;
349
402
  throughMessageId?: string;
@@ -363,7 +416,7 @@ export function forkConversation(params: {
363
416
 
364
417
  if (sourceMessages.length === 0) {
365
418
  throw new UserError(
366
- `Conversation ${conversationId} has no persisted messages to fork`,
419
+ `Conversation ${conversationId} has no persisted messages to fork`
367
420
  );
368
421
  }
369
422
 
@@ -374,7 +427,7 @@ export function forkConversation(params: {
374
427
 
375
428
  if (throughMessageId != null && copyBoundaryIndex === -1) {
376
429
  throw new UserError(
377
- `Message ${throughMessageId} does not belong to conversation ${conversationId}`,
430
+ `Message ${throughMessageId} does not belong to conversation ${conversationId}`
378
431
  );
379
432
  }
380
433
 
@@ -382,8 +435,8 @@ export function forkConversation(params: {
382
435
  0,
383
436
  Math.min(
384
437
  sourceConversation.contextCompactedMessageCount,
385
- sourceMessages.length,
386
- ),
438
+ sourceMessages.length
439
+ )
387
440
  );
388
441
  const preserveSourceCompactionState =
389
442
  copyBoundaryIndex >= visibleWindowStartIndex;
@@ -407,10 +460,14 @@ export function forkConversation(params: {
407
460
  // (linkAttachmentToMessage, relinkAttachments, seedForkedConversationAttention)
408
461
  // use the same underlying bun:sqlite connection, so their writes participate
409
462
  // in this transaction automatically.
463
+ // Inherit group_id from parent via raw SQL helper (group_id is not in Drizzle schema)
464
+ const parentGroupId = getConversationGroupId(conversationId);
465
+
410
466
  const forkedConversation = db.transaction(() => {
411
467
  const fc = createConversation({
412
468
  title: forkTitle,
413
469
  conversationType: "standard",
470
+ groupId: parentGroupId ?? undefined,
414
471
  });
415
472
 
416
473
  db.update(conversations)
@@ -473,7 +530,7 @@ export function forkConversation(params: {
473
530
  .orderBy(messageAttachments.position)
474
531
  .all();
475
532
  const uncachedAttachmentLinks = attachmentLinks.filter(
476
- (link) => !attachmentIdMap.has(link.attachmentId),
533
+ (link) => !attachmentIdMap.has(link.attachmentId)
477
534
  );
478
535
  const stagingMessageId =
479
536
  uncachedAttachmentLinks.length > 0 ? uuid() : null;
@@ -509,7 +566,7 @@ export function forkConversation(params: {
509
566
  const scopedAttachmentId = linkAttachmentToMessage(
510
567
  stagingMessageId ?? forkedMessageId,
511
568
  link.attachmentId,
512
- link.position,
569
+ link.position
513
570
  );
514
571
  attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
515
572
  }
@@ -544,7 +601,7 @@ export function forkConversation(params: {
544
601
  const persistedFork = getConversation(forkedConversation.id);
545
602
  if (!persistedFork) {
546
603
  throw new Error(
547
- `Failed to load forked conversation ${forkedConversation.id} after creation`,
604
+ `Failed to load forked conversation ${forkedConversation.id} after creation`
548
605
  );
549
606
  }
550
607
 
@@ -589,16 +646,6 @@ export function deleteConversation(id: string): DeletedMemoryIds {
589
646
  .all();
590
647
  result.segmentIds = linkedSegments.map((r) => r.id);
591
648
 
592
- // Collect memory item IDs linked to these messages before cascade.
593
- const linkedItems = tx
594
- .select({ memoryItemId: memoryItemSources.memoryItemId })
595
- .from(memoryItemSources)
596
- .where(inArray(memoryItemSources.messageId, messageIds))
597
- .all();
598
- const candidateItemIds = [
599
- ...new Set(linkedItems.map((r) => r.memoryItemId)),
600
- ];
601
-
602
649
  // Delete non-cascading tables first.
603
650
  tx.delete(llmRequestLogs)
604
651
  .where(eq(llmRequestLogs.conversationId, id))
@@ -606,7 +653,7 @@ export function deleteConversation(id: string): DeletedMemoryIds {
606
653
  tx.delete(toolInvocations)
607
654
  .where(eq(toolInvocations.conversationId, id))
608
655
  .run();
609
- // Cascade deletes memory_segments, memory_item_sources, message_attachments.
656
+ // Cascade deletes memory_segments, message_attachments.
610
657
  tx.delete(messages).where(eq(messages.conversationId, id)).run();
611
658
 
612
659
  // Clean up segment embeddings.
@@ -615,39 +662,11 @@ export function deleteConversation(id: string): DeletedMemoryIds {
615
662
  .where(
616
663
  and(
617
664
  eq(memoryEmbeddings.targetType, "segment"),
618
- inArray(memoryEmbeddings.targetId, result.segmentIds),
619
- ),
665
+ inArray(memoryEmbeddings.targetId, result.segmentIds)
666
+ )
620
667
  )
621
668
  .run();
622
669
  }
623
-
624
- // Clean up orphaned memory items whose only sources were in this conversation.
625
- if (candidateItemIds.length > 0) {
626
- const surviving = tx
627
- .select({ memoryItemId: memoryItemSources.memoryItemId })
628
- .from(memoryItemSources)
629
- .where(inArray(memoryItemSources.memoryItemId, candidateItemIds))
630
- .all();
631
- const survivingIds = new Set(surviving.map((r) => r.memoryItemId));
632
- const orphanedIds = candidateItemIds.filter(
633
- (itemId) => !survivingIds.has(itemId),
634
- );
635
- result.orphanedItemIds = orphanedIds;
636
-
637
- if (orphanedIds.length > 0) {
638
- tx.delete(memoryEmbeddings)
639
- .where(
640
- and(
641
- eq(memoryEmbeddings.targetType, "item"),
642
- inArray(memoryEmbeddings.targetId, orphanedIds),
643
- ),
644
- )
645
- .run();
646
- tx.delete(memoryItems)
647
- .where(inArray(memoryItems.id, orphanedIds))
648
- .run();
649
- }
650
- }
651
670
  } else {
652
671
  // No messages — just clean up non-message tables.
653
672
  tx.delete(llmRequestLogs)
@@ -659,35 +678,6 @@ export function deleteConversation(id: string): DeletedMemoryIds {
659
678
  }
660
679
 
661
680
  if (isPrivateScope && memoryScopeId) {
662
- // Sweep remaining memory items with this private scopeId.
663
- const scopeItems = tx
664
- .select({ id: memoryItems.id })
665
- .from(memoryItems)
666
- .where(eq(memoryItems.scopeId, memoryScopeId))
667
- .all();
668
- const alreadyDeleted = new Set(result.orphanedItemIds);
669
- const scopeItemIds = scopeItems
670
- .map((r) => r.id)
671
- .filter((id) => !alreadyDeleted.has(id));
672
-
673
- if (scopeItemIds.length > 0) {
674
- tx.delete(memoryEmbeddings)
675
- .where(
676
- and(
677
- eq(memoryEmbeddings.targetType, "item"),
678
- inArray(memoryEmbeddings.targetId, scopeItemIds),
679
- ),
680
- )
681
- .run();
682
- tx.delete(memoryItemSources)
683
- .where(inArray(memoryItemSources.memoryItemId, scopeItemIds))
684
- .run();
685
- tx.delete(memoryItems)
686
- .where(inArray(memoryItems.id, scopeItemIds))
687
- .run();
688
- result.orphanedItemIds.push(...scopeItemIds);
689
- }
690
-
691
681
  // Sweep memory summaries with this private scopeId.
692
682
  const scopeSummaries = tx
693
683
  .select({ id: memorySummaries.id })
@@ -701,8 +691,8 @@ export function deleteConversation(id: string): DeletedMemoryIds {
701
691
  .where(
702
692
  and(
703
693
  eq(memoryEmbeddings.targetType, "summary"),
704
- inArray(memoryEmbeddings.targetId, scopeSummaryIds),
705
- ),
694
+ inArray(memoryEmbeddings.targetId, scopeSummaryIds)
695
+ )
706
696
  )
707
697
  .run();
708
698
  tx.delete(memorySummaries)
@@ -747,65 +737,6 @@ export function wipeConversation(id: string): WipeConversationResult {
747
737
  // the cancellation queries join on `messages`).
748
738
  const cancelledJobCount = cancelPendingJobsForConversation(id);
749
739
 
750
- // Step B — Un-supersede memory items with explicit `supersededBy` links.
751
- // Find memory items whose `superseded_by` points to an item sourced
752
- // exclusively from this conversation.
753
- const explicitSuperseded = rawAll<{ oldItemId: string }>(
754
- `SELECT DISTINCT mi_old.id AS oldItemId
755
- FROM memory_items mi_old
756
- JOIN memory_items mi_new ON mi_old.superseded_by = mi_new.id
757
- WHERE mi_old.status = 'superseded'
758
- AND mi_new.id IN (
759
- SELECT mis.memory_item_id
760
- FROM memory_item_sources mis
761
- JOIN messages m ON m.id = mis.message_id
762
- WHERE m.conversation_id = ?
763
- )
764
- AND NOT EXISTS (
765
- SELECT 1 FROM memory_item_sources mis2
766
- JOIN messages m2 ON m2.id = mis2.message_id
767
- WHERE mis2.memory_item_id = mi_new.id
768
- AND m2.conversation_id != ?
769
- )
770
- AND NOT EXISTS (
771
- SELECT 1 FROM memory_items mi_active
772
- WHERE mi_active.kind = mi_old.kind
773
- AND mi_active.subject = mi_old.subject
774
- AND mi_active.scope_id = mi_old.scope_id
775
- AND mi_active.status = 'active'
776
- AND mi_active.id != mi_old.id
777
- -- Exclude items sourced exclusively from the conversation being
778
- -- wiped — deleteConversation will remove them, so they should not
779
- -- block restoration of mi_old.
780
- AND NOT (
781
- EXISTS (
782
- SELECT 1 FROM memory_item_sources mis_a
783
- JOIN messages m_a ON m_a.id = mis_a.message_id
784
- WHERE mis_a.memory_item_id = mi_active.id
785
- AND m_a.conversation_id = ?
786
- )
787
- AND NOT EXISTS (
788
- SELECT 1 FROM memory_item_sources mis_b
789
- JOIN messages m_b ON m_b.id = mis_b.message_id
790
- WHERE mis_b.memory_item_id = mi_active.id
791
- AND m_b.conversation_id != ?
792
- )
793
- )
794
- )`,
795
- id,
796
- id,
797
- id,
798
- id,
799
- );
800
- for (const { oldItemId } of explicitSuperseded) {
801
- rawRun(
802
- "UPDATE memory_items SET status = 'active', superseded_by = NULL WHERE id = ?",
803
- oldItemId,
804
- );
805
- enqueueMemoryJob("embed_item", { itemId: oldItemId });
806
- unsupersededItemIds.push(oldItemId);
807
- }
808
-
809
740
  // Step C — Delete conversation-scoped memory summaries and their embeddings.
810
741
  const summaryRows = db
811
742
  .select({ id: memorySummaries.id })
@@ -813,8 +744,8 @@ export function wipeConversation(id: string): WipeConversationResult {
813
744
  .where(
814
745
  and(
815
746
  eq(memorySummaries.scope, "conversation"),
816
- eq(memorySummaries.scopeKey, id),
817
- ),
747
+ eq(memorySummaries.scopeKey, id)
748
+ )
818
749
  )
819
750
  .all();
820
751
  const summaryIds = summaryRows.map((r) => r.id);
@@ -823,8 +754,8 @@ export function wipeConversation(id: string): WipeConversationResult {
823
754
  .where(
824
755
  and(
825
756
  eq(memoryEmbeddings.targetType, "summary"),
826
- inArray(memoryEmbeddings.targetId, summaryIds),
827
- ),
757
+ inArray(memoryEmbeddings.targetId, summaryIds)
758
+ )
828
759
  )
829
760
  .run();
830
761
  db.delete(memorySummaries)
@@ -833,79 +764,12 @@ export function wipeConversation(id: string): WipeConversationResult {
833
764
  }
834
765
  deletedSummaryIds.push(...summaryIds);
835
766
 
836
- // Step D — Get the conversation's memoryScopeId before deletion.
837
- const scopeId = getConversationMemoryScopeId(id);
838
-
839
- // Step D.5 — Collect kind + subject pairs of items that will be orphaned
840
- // by deleteConversation. These are items sourced from this conversation's
841
- // messages that have NO sources from any other conversation. We need this
842
- // before deletion so we can scope Step F to only restore superseded items
843
- // matching the specific kind + subject pairs that just lost their active
844
- // replacement.
845
- const orphanedKindSubjects = rawAll<{ kind: string; subject: string }>(
846
- `SELECT DISTINCT mi.kind, mi.subject
847
- FROM memory_items mi
848
- JOIN memory_item_sources mis ON mis.memory_item_id = mi.id
849
- JOIN messages m ON m.id = mis.message_id
850
- WHERE m.conversation_id = ?
851
- AND NOT EXISTS (
852
- SELECT 1 FROM memory_item_sources mis2
853
- JOIN messages m2 ON m2.id = mis2.message_id
854
- WHERE mis2.memory_item_id = mi.id
855
- AND m2.conversation_id != ?
856
- )`,
857
- id,
858
- id,
859
- );
860
-
861
- // Step E — Delegate to deleteConversation which handles messages (cascade
862
- // segments, item_sources, attachments), llmRequestLogs, toolInvocations,
863
- // orphaned memory items + embeddings, and the conversation row.
767
+ // Step D — Delegate to deleteConversation which handles messages (cascade
768
+ // segments, attachments), llmRequestLogs, toolInvocations,
769
+ // embeddings, and the conversation row.
864
770
  const deletedMemoryIds = deleteConversation(id);
865
771
 
866
- // Step FRestore orphaned subject-match superseded items. After
867
- // deleteConversation removes superseding items, find superseded items
868
- // with no supersededBy link where no active item with the same
869
- // kind + subject + scope_id exists. Scoped to only the kind + subject
870
- // pairs of items that were just orphaned by deleteConversation, so we
871
- // don't accidentally restore items superseded by unrelated conversations.
872
- let orphanedSuperseded: Array<{ id: string }> = [];
873
- if (orphanedKindSubjects.length > 0) {
874
- const placeholders = orphanedKindSubjects.map(() => "(?, ?)").join(", ");
875
- const params: Array<string> = [scopeId];
876
- for (const { kind, subject } of orphanedKindSubjects) {
877
- params.push(kind, subject);
878
- }
879
- orphanedSuperseded = rawAll<{ id: string }>(
880
- `SELECT id FROM (
881
- SELECT id, ROW_NUMBER() OVER (
882
- PARTITION BY kind, subject, scope_id
883
- ORDER BY last_seen_at DESC
884
- ) AS rn
885
- FROM memory_items
886
- WHERE status = 'superseded'
887
- AND superseded_by IS NULL
888
- AND scope_id = ?
889
- AND (kind, subject) IN (VALUES ${placeholders})
890
- AND NOT EXISTS (
891
- SELECT 1 FROM memory_items mi2
892
- WHERE mi2.kind = memory_items.kind
893
- AND mi2.subject = memory_items.subject
894
- AND mi2.scope_id = memory_items.scope_id
895
- AND mi2.status = 'active'
896
- AND mi2.id != memory_items.id
897
- )
898
- ) WHERE rn = 1`,
899
- ...params,
900
- );
901
- }
902
- for (const { id: itemId } of orphanedSuperseded) {
903
- rawRun("UPDATE memory_items SET status = 'active' WHERE id = ?", itemId);
904
- enqueueMemoryJob("embed_item", { itemId });
905
- unsupersededItemIds.push(itemId);
906
- }
907
-
908
- // Step G — Return the combined result.
772
+ // Step EReturn the combined result.
909
773
  return {
910
774
  ...deletedMemoryIds,
911
775
  unsupersededItemIds,
@@ -970,7 +834,7 @@ export async function addMessage(
970
834
  role: string,
971
835
  content: string,
972
836
  metadata?: Record<string, unknown>,
973
- opts?: { skipIndexing?: boolean },
837
+ opts?: { skipIndexing?: boolean }
974
838
  ) {
975
839
  const db = getDb();
976
840
  const messageId = uuid();
@@ -980,7 +844,7 @@ export async function addMessage(
980
844
  if (!result.success) {
981
845
  log.warn(
982
846
  { conversationId, messageId, issues: result.error.issues },
983
- "Invalid message metadata, storing as-is",
847
+ "Invalid message metadata, storing as-is"
984
848
  );
985
849
  }
986
850
  }
@@ -1015,8 +879,8 @@ export async function addMessage(
1015
879
  .where(
1016
880
  and(
1017
881
  eq(conversations.id, conversationId),
1018
- isNull(conversations.originChannel),
1019
- ),
882
+ isNull(conversations.originChannel)
883
+ )
1020
884
  )
1021
885
  .run();
1022
886
  }
@@ -1035,7 +899,7 @@ export async function addMessage(
1035
899
  ) {
1036
900
  log.warn(
1037
901
  { attempt, conversationId, code: errCode },
1038
- "addMessage: transient SQLite error, retrying",
902
+ "addMessage: transient SQLite error, retrying"
1039
903
  );
1040
904
  await Bun.sleep(50 * (attempt + 1));
1041
905
  continue;
@@ -1074,12 +938,12 @@ export async function addMessage(
1074
938
  provenanceTrustClass,
1075
939
  automated,
1076
940
  },
1077
- config.memory,
941
+ config.memory
1078
942
  );
1079
943
  } catch (err) {
1080
944
  log.warn(
1081
945
  { err, conversationId, messageId: message.id },
1082
- "Failed to index message for memory",
946
+ "Failed to index message for memory"
1083
947
  );
1084
948
  }
1085
949
  }
@@ -1094,7 +958,7 @@ export async function addMessage(
1094
958
  } catch (err) {
1095
959
  log.warn(
1096
960
  { err, conversationId, messageId: message.id },
1097
- "Failed to project assistant message for attention tracking",
961
+ "Failed to project assistant message for attention tracking"
1098
962
  );
1099
963
  }
1100
964
  }
@@ -1121,7 +985,7 @@ export interface PaginatedMessagesResult {
1121
985
  export function getMessagesPaginated(
1122
986
  conversationId: string,
1123
987
  limit: number | undefined,
1124
- beforeTimestamp?: number,
988
+ beforeTimestamp?: number
1125
989
  ): PaginatedMessagesResult {
1126
990
  const db = getDb();
1127
991
 
@@ -1165,7 +1029,7 @@ export function getMessagesPaginated(
1165
1029
 
1166
1030
  export function getLastAssistantTimestampBefore(
1167
1031
  conversationId: string,
1168
- beforeTimestamp: number,
1032
+ beforeTimestamp: number
1169
1033
  ): number {
1170
1034
  const db = getDb();
1171
1035
  const row = db
@@ -1175,8 +1039,8 @@ export function getLastAssistantTimestampBefore(
1175
1039
  and(
1176
1040
  eq(messages.conversationId, conversationId),
1177
1041
  eq(messages.role, "assistant"),
1178
- lt(messages.createdAt, beforeTimestamp),
1179
- ),
1042
+ lt(messages.createdAt, beforeTimestamp)
1043
+ )
1180
1044
  )
1181
1045
  .orderBy(desc(messages.createdAt))
1182
1046
  .limit(1)
@@ -1187,7 +1051,7 @@ export function getLastAssistantTimestampBefore(
1187
1051
  /** Fetch a single message by ID, optionally scoped to a specific conversation. */
1188
1052
  export function getMessageById(
1189
1053
  messageId: string,
1190
- conversationId?: string,
1054
+ conversationId?: string
1191
1055
  ): MessageRow | null {
1192
1056
  const db = getDb();
1193
1057
  const conditions = [eq(messages.id, messageId)];
@@ -1205,7 +1069,7 @@ export function getMessageById(
1205
1069
  export function updateConversationTitle(
1206
1070
  id: string,
1207
1071
  title: string,
1208
- isAutoTitle?: number,
1072
+ isAutoTitle?: number
1209
1073
  ): void {
1210
1074
  const db = getDb();
1211
1075
  const set: Record<string, unknown> = { title, updatedAt: Date.now() };
@@ -1223,7 +1087,7 @@ export function updateConversationUsage(
1223
1087
  id: string,
1224
1088
  totalInputTokens: number,
1225
1089
  totalOutputTokens: number,
1226
- totalEstimatedCost: number,
1090
+ totalEstimatedCost: number
1227
1091
  ): void {
1228
1092
  const db = getDb();
1229
1093
  db.update(conversations)
@@ -1240,7 +1104,7 @@ export function updateConversationUsage(
1240
1104
  export function updateConversationContextWindow(
1241
1105
  id: string,
1242
1106
  contextSummary: string,
1243
- contextCompactedMessageCount: number,
1107
+ contextCompactedMessageCount: number
1244
1108
  ): void {
1245
1109
  const db = getDb();
1246
1110
  db.update(conversations)
@@ -1265,18 +1129,16 @@ export function clearAll(): { conversations: number; messages: number } {
1265
1129
  const convCount =
1266
1130
  rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM conversations")?.c ?? 0;
1267
1131
 
1268
- // Delete in dependency order. Cascades handle memory_segments,
1269
- // memory_item_sources, and tool_invocations, but we explicitly
1270
- // clear non-cascading memory tables too.
1132
+ // Delete in dependency order. Cascades handle memory_segments and
1133
+ // tool_invocations, but we explicitly clear non-cascading memory
1134
+ // tables too.
1271
1135
  //
1272
1136
  // FTS virtual tables are cleared before their base tables. If an FTS
1273
1137
  // table is corrupted, the DELETE will fail — we drop the associated
1274
1138
  // triggers so that the subsequent base-table DELETEs don't also fail
1275
1139
  // (SQLite triggers are atomic with the triggering statement, so a
1276
1140
  // corrupted FTS table would roll back every base-table DELETE).
1277
- rawExec("DELETE FROM memory_item_sources");
1278
1141
  rawExec("DELETE FROM memory_segments");
1279
- rawExec("DELETE FROM memory_items");
1280
1142
  rawExec("DELETE FROM memory_summaries");
1281
1143
  rawExec("DELETE FROM memory_embeddings");
1282
1144
  rawExec("DELETE FROM memory_jobs");
@@ -1292,7 +1154,7 @@ export function clearAll(): { conversations: number; messages: number } {
1292
1154
  } catch (err) {
1293
1155
  log.warn(
1294
1156
  { err },
1295
- "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed",
1157
+ "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed"
1296
1158
  );
1297
1159
  rawExec("DROP TRIGGER IF EXISTS messages_fts_ai");
1298
1160
  rawExec("DROP TRIGGER IF EXISTS messages_fts_ad");
@@ -1308,7 +1170,7 @@ export function clearAll(): { conversations: number; messages: number } {
1308
1170
  `INSERT INTO lifecycle_events (id, event_name, created_at) VALUES (?, ?, ?)`,
1309
1171
  uuid(),
1310
1172
  "conversations_clear_all",
1311
- Date.now(),
1173
+ Date.now()
1312
1174
  );
1313
1175
 
1314
1176
  // Rebuild corrupted FTS tables and restore triggers after all base-table
@@ -1318,16 +1180,16 @@ export function clearAll(): { conversations: number; messages: number } {
1318
1180
  if (messagesFtsCorrupted) {
1319
1181
  rawExec("DROP TABLE IF EXISTS messages_fts");
1320
1182
  rawExec(
1321
- `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`,
1183
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`
1322
1184
  );
1323
1185
  rawExec(
1324
- `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`,
1186
+ `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`
1325
1187
  );
1326
1188
  rawExec(
1327
- `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`,
1189
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`
1328
1190
  );
1329
1191
  rawExec(
1330
- `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`,
1192
+ `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`
1331
1193
  );
1332
1194
  }
1333
1195
 
@@ -1352,8 +1214,8 @@ export function deleteLastExchange(conversationId: string): number {
1352
1214
  .where(
1353
1215
  and(
1354
1216
  eq(messages.conversationId, conversationId),
1355
- eq(messages.role, "user"),
1356
- ),
1217
+ eq(messages.role, "user")
1218
+ )
1357
1219
  )
1358
1220
  .orderBy(sql`rowid DESC`)
1359
1221
  .limit(1)
@@ -1367,7 +1229,7 @@ export function deleteLastExchange(conversationId: string): number {
1367
1229
  const rowidSubquery = sql`(SELECT rowid FROM messages WHERE id = ${lastUserMsg.id})`;
1368
1230
  const condition = and(
1369
1231
  eq(messages.conversationId, conversationId),
1370
- sql`rowid >= ${rowidSubquery}`,
1232
+ sql`rowid >= ${rowidSubquery}`
1371
1233
  );
1372
1234
 
1373
1235
  const [{ deleted }] = db
@@ -1431,7 +1293,7 @@ export interface WipeConversationResult extends DeletedMemoryIds {
1431
1293
  */
1432
1294
  export function updateMessageContent(
1433
1295
  messageId: string,
1434
- newContent: string,
1296
+ newContent: string
1435
1297
  ): void {
1436
1298
  const db = getDb();
1437
1299
  db.update(messages)
@@ -1440,6 +1302,27 @@ export function updateMessageContent(
1440
1302
  .run();
1441
1303
  }
1442
1304
 
1305
+ /**
1306
+ * Merge `updates` into the metadata JSON of an existing message.
1307
+ * Reads the current metadata, shallow-merges the new fields, and writes back.
1308
+ */
1309
+ export function updateMessageMetadata(
1310
+ messageId: string,
1311
+ updates: Record<string, unknown>,
1312
+ ): void {
1313
+ const db = getDb();
1314
+ const row = db
1315
+ .select({ metadata: messages.metadata })
1316
+ .from(messages)
1317
+ .where(eq(messages.id, messageId))
1318
+ .get();
1319
+ const existing = row?.metadata ? JSON.parse(row.metadata) : {};
1320
+ db.update(messages)
1321
+ .set({ metadata: JSON.stringify({ ...existing, ...updates }) })
1322
+ .where(eq(messages.id, messageId))
1323
+ .run();
1324
+ }
1325
+
1443
1326
  /**
1444
1327
  * Re-link all attachments from a set of source messages to a target message.
1445
1328
  * Used during message consolidation so that attachments linked to deleted
@@ -1447,7 +1330,7 @@ export function updateMessageContent(
1447
1330
  */
1448
1331
  export function relinkAttachments(
1449
1332
  fromMessageIds: string[],
1450
- toMessageId: string,
1333
+ toMessageId: string
1451
1334
  ): number {
1452
1335
  if (fromMessageIds.length === 0) return 0;
1453
1336
  const db = getDb();
@@ -1475,15 +1358,8 @@ export function relinkAttachments(
1475
1358
  * NULL before the message row is removed, so associated run and event
1476
1359
  * records survive.
1477
1360
  *
1478
- * Also cleans up derived memory_items: if the memory worker has already
1479
- * processed an extract_items job for this message, deleting the message
1480
- * cascades memory_item_sources but leaves the memory_items active.
1481
- * Without cleanup, those items would leak into summaries and recall.
1482
- * We delete any memory_items that become orphaned (no remaining sources)
1483
- * after this message is removed.
1484
- *
1485
- * Returns segment and orphaned item IDs so the caller can clean up the
1486
- * corresponding Qdrant vector entries.
1361
+ * Returns segment IDs so the caller can clean up the corresponding
1362
+ * Qdrant vector entries.
1487
1363
  */
1488
1364
  export function deleteMessageById(messageId: string): DeletedMemoryIds {
1489
1365
  const db = getDb();
@@ -1512,22 +1388,14 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1512
1388
  .all();
1513
1389
  result.segmentIds = linkedSegments.map((r) => r.id);
1514
1390
 
1515
- // Collect memory item IDs linked to this message before cascade.
1516
- const linkedItems = tx
1517
- .select({ memoryItemId: memoryItemSources.memoryItemId })
1518
- .from(memoryItemSources)
1519
- .where(eq(memoryItemSources.messageId, messageId))
1520
- .all();
1521
- const candidateItemIds = linkedItems.map((r) => r.memoryItemId);
1522
-
1523
1391
  // Detach nullable FK references so the cascade doesn't destroy them.
1524
1392
  tx.update(channelInboundEvents)
1525
1393
  .set({ messageId: null })
1526
1394
  .where(eq(channelInboundEvents.messageId, messageId))
1527
1395
  .run();
1528
1396
 
1529
- // Now safe to delete — NOT NULL cascades remove memory_item_sources,
1530
- // memory_segments, and message_attachments.
1397
+ // Now safe to delete — NOT NULL cascades remove memory_segments
1398
+ // and message_attachments.
1531
1399
  tx.delete(messages).where(eq(messages.id, messageId)).run();
1532
1400
 
1533
1401
  // Clean up segment embeddings from SQLite (Qdrant cleanup is the caller's job).
@@ -1536,42 +1404,11 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1536
1404
  .where(
1537
1405
  and(
1538
1406
  eq(memoryEmbeddings.targetType, "segment"),
1539
- inArray(memoryEmbeddings.targetId, result.segmentIds),
1540
- ),
1407
+ inArray(memoryEmbeddings.targetId, result.segmentIds)
1408
+ )
1541
1409
  )
1542
1410
  .run();
1543
1411
  }
1544
-
1545
- // Clean up orphaned memory items whose only source was this message.
1546
- if (candidateItemIds.length > 0) {
1547
- // Find which items still have at least one remaining source.
1548
- const surviving = tx
1549
- .select({ memoryItemId: memoryItemSources.memoryItemId })
1550
- .from(memoryItemSources)
1551
- .where(inArray(memoryItemSources.memoryItemId, candidateItemIds))
1552
- .all();
1553
- const survivingIds = new Set(surviving.map((r) => r.memoryItemId));
1554
- const orphanedIds = candidateItemIds.filter(
1555
- (id) => !survivingIds.has(id),
1556
- );
1557
- result.orphanedItemIds = orphanedIds;
1558
-
1559
- if (orphanedIds.length > 0) {
1560
- // Delete embeddings referencing these items.
1561
- tx.delete(memoryEmbeddings)
1562
- .where(
1563
- and(
1564
- eq(memoryEmbeddings.targetType, "item"),
1565
- inArray(memoryEmbeddings.targetId, orphanedIds),
1566
- ),
1567
- )
1568
- .run();
1569
- // Delete the orphaned memory items themselves.
1570
- tx.delete(memoryItems)
1571
- .where(inArray(memoryItems.id, orphanedIds))
1572
- .run();
1573
- }
1574
- }
1575
1412
  });
1576
1413
 
1577
1414
  deleteOrphanAttachments(candidateAttachmentIds);
@@ -1581,7 +1418,7 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1581
1418
 
1582
1419
  export function setConversationOriginChannelIfUnset(
1583
1420
  conversationId: string,
1584
- channel: ChannelId,
1421
+ channel: ChannelId
1585
1422
  ): void {
1586
1423
  const db = getDb();
1587
1424
  db.update(conversations)
@@ -1589,14 +1426,14 @@ export function setConversationOriginChannelIfUnset(
1589
1426
  .where(
1590
1427
  and(
1591
1428
  eq(conversations.id, conversationId),
1592
- isNull(conversations.originChannel),
1593
- ),
1429
+ isNull(conversations.originChannel)
1430
+ )
1594
1431
  )
1595
1432
  .run();
1596
1433
  }
1597
1434
 
1598
1435
  export function getConversationOriginChannel(
1599
- conversationId: string,
1436
+ conversationId: string
1600
1437
  ): ChannelId | null {
1601
1438
  const db = getDb();
1602
1439
  const row = db
@@ -1609,7 +1446,7 @@ export function getConversationOriginChannel(
1609
1446
 
1610
1447
  export function setConversationOriginInterfaceIfUnset(
1611
1448
  conversationId: string,
1612
- interfaceId: InterfaceId,
1449
+ interfaceId: InterfaceId
1613
1450
  ): void {
1614
1451
  const db = getDb();
1615
1452
  db.update(conversations)
@@ -1617,14 +1454,14 @@ export function setConversationOriginInterfaceIfUnset(
1617
1454
  .where(
1618
1455
  and(
1619
1456
  eq(conversations.id, conversationId),
1620
- isNull(conversations.originInterface),
1621
- ),
1457
+ isNull(conversations.originInterface)
1458
+ )
1622
1459
  )
1623
1460
  .run();
1624
1461
  }
1625
1462
 
1626
1463
  export function getConversationOriginInterface(
1627
- conversationId: string,
1464
+ conversationId: string
1628
1465
  ): InterfaceId | null {
1629
1466
  const db = getDb();
1630
1467
  const row = db
@@ -1644,13 +1481,13 @@ export function getConversationOriginInterface(
1644
1481
  * conversation itself isn't a desktop-origin private conversation).
1645
1482
  */
1646
1483
  export function getConversationRecentProvenanceTrustClass(
1647
- conversationId: string,
1484
+ conversationId: string
1648
1485
  ): "guardian" | "trusted_contact" | "unknown" | undefined {
1649
1486
  const row = rawGet<{ metadata: string | null }>(
1650
1487
  `SELECT metadata FROM messages
1651
1488
  WHERE conversation_id = ? AND role = 'user' AND metadata IS NOT NULL
1652
1489
  ORDER BY created_at DESC LIMIT 1`,
1653
- conversationId,
1490
+ conversationId
1654
1491
  );
1655
1492
  if (!row?.metadata) return undefined;
1656
1493
  try {
@@ -1670,18 +1507,66 @@ export function batchSetDisplayOrders(
1670
1507
  id: string;
1671
1508
  displayOrder: number | null;
1672
1509
  isPinned: boolean;
1673
- }>,
1510
+ groupId?: string | null;
1511
+ }>
1674
1512
  ): void {
1675
1513
  ensureDisplayOrderMigration();
1514
+ ensureGroupMigration();
1676
1515
  rawExec("BEGIN");
1677
1516
  try {
1678
1517
  for (const update of updates) {
1679
- rawRun(
1680
- "UPDATE conversations SET display_order = ?, is_pinned = ? WHERE id = ?",
1681
- update.displayOrder,
1682
- update.isPinned ? 1 : 0,
1683
- update.id,
1684
- );
1518
+ if (update.groupId !== undefined) {
1519
+ // New client: groupId is authoritative.
1520
+ // Derive is_pinned from groupId.
1521
+ // Sanitize: if groupId references a deleted/unknown group, fall back
1522
+ // to NULL to avoid FK violation that would roll back the entire batch.
1523
+ let safeGroupId = update.groupId;
1524
+ if (
1525
+ safeGroupId !== null &&
1526
+ !rawGet<{ id: string }>(
1527
+ "SELECT id FROM conversation_groups WHERE id = ?",
1528
+ safeGroupId
1529
+ )
1530
+ ) {
1531
+ safeGroupId = null;
1532
+ }
1533
+ rawRun(
1534
+ "UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ? WHERE id = ?",
1535
+ update.displayOrder,
1536
+ safeGroupId === "system:pinned" ? 1 : 0,
1537
+ safeGroupId,
1538
+ update.id
1539
+ );
1540
+ } else {
1541
+ // Old client: no groupId in payload
1542
+ // isPinned true -> set group_id = system:pinned
1543
+ // isPinned false -> clear group_id ONLY IF currently system:pinned
1544
+ // otherwise preserve existing group_id
1545
+ if (update.isPinned) {
1546
+ rawRun(
1547
+ "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
1548
+ update.displayOrder,
1549
+ update.id
1550
+ );
1551
+ } else {
1552
+ // Restore system group from source/conversationType when old clients
1553
+ // unpin, instead of clearing to NULL (which would lose provenance).
1554
+ rawRun(
1555
+ `UPDATE conversations SET display_order = ?, is_pinned = 0,
1556
+ group_id = CASE WHEN group_id = 'system:pinned' THEN
1557
+ CASE
1558
+ WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
1559
+ WHEN source IN ('heartbeat', 'task') THEN 'system:background'
1560
+ WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
1561
+ ELSE NULL
1562
+ END
1563
+ ELSE group_id END
1564
+ WHERE id = ?`,
1565
+ update.displayOrder,
1566
+ update.id
1567
+ );
1568
+ }
1569
+ }
1685
1570
  }
1686
1571
  rawExec("COMMIT");
1687
1572
  } catch (err) {
@@ -1691,22 +1576,31 @@ export function batchSetDisplayOrders(
1691
1576
  }
1692
1577
 
1693
1578
  export function getDisplayMetaForConversations(
1694
- conversationIds: string[],
1695
- ): Map<string, { displayOrder: number | null; isPinned: boolean }> {
1579
+ conversationIds: string[]
1580
+ ): Map<
1581
+ string,
1582
+ { displayOrder: number | null; isPinned: boolean; groupId: string | null }
1583
+ > {
1696
1584
  ensureDisplayOrderMigration();
1585
+ ensureGroupMigration();
1697
1586
  const result = new Map<
1698
1587
  string,
1699
- { displayOrder: number | null; isPinned: boolean }
1588
+ { displayOrder: number | null; isPinned: boolean; groupId: string | null }
1700
1589
  >();
1701
1590
  if (conversationIds.length === 0) return result;
1702
1591
  for (const id of conversationIds) {
1703
1592
  const row = rawGet<{
1704
1593
  display_order: number | null;
1705
1594
  is_pinned: number | null;
1706
- }>("SELECT display_order, is_pinned FROM conversations WHERE id = ?", id);
1595
+ group_id: string | null;
1596
+ }>(
1597
+ "SELECT display_order, is_pinned, group_id FROM conversations WHERE id = ?",
1598
+ id
1599
+ );
1707
1600
  result.set(id, {
1708
1601
  displayOrder: row?.display_order ?? null,
1709
1602
  isPinned: (row?.is_pinned ?? 0) === 1,
1603
+ groupId: row?.group_id ?? null,
1710
1604
  });
1711
1605
  }
1712
1606
  return result;
@@ -1730,7 +1624,7 @@ function isToolResultMessage(role: string, content: string): boolean {
1730
1624
  (block: unknown) =>
1731
1625
  block != null &&
1732
1626
  typeof block === "object" &&
1733
- (block as Record<string, unknown>).type === "tool_result",
1627
+ (block as Record<string, unknown>).type === "tool_result"
1734
1628
  );
1735
1629
  } catch {
1736
1630
  return false;
@@ -1750,7 +1644,7 @@ function isToolResultMessage(role: string, content: string): boolean {
1750
1644
  */
1751
1645
  export function getTurnTimeBounds(
1752
1646
  conversationId: string,
1753
- messageCreatedAt: number,
1647
+ messageCreatedAt: number
1754
1648
  ): { startTime: number; endTime: number } | null {
1755
1649
  const db = getDb();
1756
1650
 
@@ -1772,8 +1666,8 @@ export function getTurnTimeBounds(
1772
1666
  .where(
1773
1667
  and(
1774
1668
  eq(messages.conversationId, conversationId),
1775
- sql`rowid <= ${rowidSubquery}`,
1776
- ),
1669
+ sql`rowid <= ${rowidSubquery}`
1670
+ )
1777
1671
  )
1778
1672
  .orderBy(sql`rowid DESC`)
1779
1673
  .limit(50)
@@ -1804,8 +1698,8 @@ export function getTurnTimeBounds(
1804
1698
  .where(
1805
1699
  and(
1806
1700
  eq(messages.conversationId, conversationId),
1807
- sql`rowid > ${forwardRowidSubquery}`,
1808
- ),
1701
+ sql`rowid > ${forwardRowidSubquery}`
1702
+ )
1809
1703
  )
1810
1704
  .orderBy(sql`rowid ASC`)
1811
1705
  .limit(50)
@@ -1846,8 +1740,8 @@ export function getTurnTimeBounds(
1846
1740
  and(
1847
1741
  eq(llmRequestLogs.conversationId, conversationId),
1848
1742
  gte(llmRequestLogs.createdAt, startTime),
1849
- lte(llmRequestLogs.createdAt, hardCeiling),
1850
- ),
1743
+ lte(llmRequestLogs.createdAt, hardCeiling)
1744
+ )
1851
1745
  )
1852
1746
  .orderBy(desc(llmRequestLogs.createdAt))
1853
1747
  .limit(1)
@@ -1895,8 +1789,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1895
1789
  .where(
1896
1790
  and(
1897
1791
  eq(messages.conversationId, target.conversationId),
1898
- lte(messages.createdAt, target.createdAt),
1899
- ),
1792
+ lte(messages.createdAt, target.createdAt)
1793
+ )
1900
1794
  )
1901
1795
  .orderBy(desc(messages.createdAt))
1902
1796
  .limit(50)
@@ -1933,8 +1827,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1933
1827
  .where(
1934
1828
  and(
1935
1829
  eq(messages.conversationId, target.conversationId),
1936
- gt(messages.createdAt, target.createdAt),
1937
- ),
1830
+ gt(messages.createdAt, target.createdAt)
1831
+ )
1938
1832
  )
1939
1833
  .orderBy(asc(messages.createdAt))
1940
1834
  .limit(50)
@@ -1970,8 +1864,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1970
1864
  and(
1971
1865
  eq(messages.conversationId, target.conversationId),
1972
1866
  gt(messages.createdAt, boundaryCreatedAt),
1973
- lte(messages.createdAt, target.createdAt),
1974
- ),
1867
+ lte(messages.createdAt, target.createdAt)
1868
+ )
1975
1869
  )
1976
1870
  .orderBy(asc(messages.createdAt))
1977
1871
  .all();
@@ -1994,8 +1888,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1994
1888
  .where(
1995
1889
  and(
1996
1890
  eq(messages.conversationId, target.conversationId),
1997
- inArray(messages.id, [...idSet]),
1998
- ),
1891
+ inArray(messages.id, [...idSet])
1892
+ )
1999
1893
  )
2000
1894
  .orderBy(asc(messages.createdAt))
2001
1895
  .all();