@vellumai/assistant 0.5.16 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (592) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +69 -16
  3. package/Dockerfile +2 -5
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/knip.json +2 -1
  9. package/openapi.yaml +1198 -83
  10. package/package.json +5 -1
  11. package/src/__tests__/actor-token-service.test.ts +68 -0
  12. package/src/__tests__/agent-loop.test.ts +0 -32
  13. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  14. package/src/__tests__/anthropic-provider.test.ts +217 -98
  15. package/src/__tests__/app-compiler.test.ts +120 -0
  16. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  17. package/src/__tests__/app-executors.test.ts +47 -1
  18. package/src/__tests__/app-source-watcher.test.ts +159 -0
  19. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  20. package/src/__tests__/call-conversation-messages.test.ts +2 -6
  21. package/src/__tests__/call-domain.test.ts +2 -6
  22. package/src/__tests__/call-pointer-messages.test.ts +2 -14
  23. package/src/__tests__/call-recovery.test.ts +2 -6
  24. package/src/__tests__/call-routes-http.test.ts +2 -6
  25. package/src/__tests__/call-store.test.ts +2 -6
  26. package/src/__tests__/cancel-resolves-conversation-key.test.ts +2 -6
  27. package/src/__tests__/canonical-guardian-store.test.ts +2 -6
  28. package/src/__tests__/channel-delivery-store.test.ts +2 -6
  29. package/src/__tests__/channel-retry-sweep.test.ts +2 -6
  30. package/src/__tests__/checker.test.ts +63 -9
  31. package/src/__tests__/clawhub.test.ts +54 -24
  32. package/src/__tests__/cli-command-risk-guard.test.ts +14 -0
  33. package/src/__tests__/config-schema.test.ts +6 -1
  34. package/src/__tests__/config-set-platform-guard.test.ts +302 -0
  35. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -6
  36. package/src/__tests__/contacts-tools.test.ts +31 -0
  37. package/src/__tests__/context-overflow-reducer.test.ts +86 -0
  38. package/src/__tests__/context-token-estimator.test.ts +175 -10
  39. package/src/__tests__/conversation-agent-loop-overflow.test.ts +13 -6
  40. package/src/__tests__/conversation-agent-loop.test.ts +13 -51
  41. package/src/__tests__/conversation-attachments.test.ts +2 -6
  42. package/src/__tests__/conversation-attention-store.test.ts +2 -6
  43. package/src/__tests__/conversation-clear-safety.test.ts +2 -6
  44. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +4 -10
  45. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -6
  46. package/src/__tests__/conversation-disk-view.test.ts +2 -6
  47. package/src/__tests__/conversation-error.test.ts +33 -2
  48. package/src/__tests__/conversation-fork-crud.test.ts +2 -6
  49. package/src/__tests__/conversation-history-web-search.test.ts +6 -1
  50. package/src/__tests__/conversation-load-history-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-media-retry.test.ts +91 -0
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  53. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  54. package/src/__tests__/conversation-starter-routes.test.ts +20 -11
  55. package/src/__tests__/conversation-store.test.ts +2 -6
  56. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  57. package/src/__tests__/conversation-usage.test.ts +2 -6
  58. package/src/__tests__/conversation-wipe.test.ts +13 -414
  59. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  60. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  61. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  62. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  63. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
  64. package/src/__tests__/credential-execution-shell-lockdown.test.ts +2 -2
  65. package/src/__tests__/credential-security-e2e.test.ts +2 -0
  66. package/src/__tests__/date-context.test.ts +76 -210
  67. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  68. package/src/__tests__/file-list-tool.test.ts +219 -0
  69. package/src/__tests__/first-greeting.test.ts +1 -1
  70. package/src/__tests__/followup-tools.test.ts +2 -6
  71. package/src/__tests__/graph-extraction-event-date.test.ts +186 -0
  72. package/src/__tests__/guardian-action-conversation-turn.test.ts +2 -6
  73. package/src/__tests__/guardian-action-followup-executor.test.ts +2 -6
  74. package/src/__tests__/guardian-action-followup-store.test.ts +2 -6
  75. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +2 -6
  76. package/src/__tests__/guardian-action-late-reply.test.ts +2 -6
  77. package/src/__tests__/guardian-action-store.test.ts +2 -6
  78. package/src/__tests__/guardian-binding-drift-heal.test.ts +2 -6
  79. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +8 -8
  80. package/src/__tests__/guardian-dispatch.test.ts +2 -6
  81. package/src/__tests__/guardian-grant-minting.test.ts +2 -14
  82. package/src/__tests__/guardian-principal-id-roundtrip.test.ts +2 -6
  83. package/src/__tests__/guardian-routing-invariants.test.ts +192 -6
  84. package/src/__tests__/guardian-routing-state.test.ts +2 -6
  85. package/src/__tests__/guardian-verification-voice-binding.test.ts +2 -6
  86. package/src/__tests__/heartbeat-service.test.ts +180 -3
  87. package/src/__tests__/identity-routes.test.ts +328 -0
  88. package/src/__tests__/inbound-invite-redemption.test.ts +2 -6
  89. package/src/__tests__/injection-block.test.ts +178 -0
  90. package/src/__tests__/install-meta.test.ts +506 -0
  91. package/src/__tests__/install-skill-routing.test.ts +293 -0
  92. package/src/__tests__/invite-redemption-service.test.ts +2 -6
  93. package/src/__tests__/invite-routes-http.test.ts +2 -6
  94. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +17 -28
  95. package/src/__tests__/list-messages-attachments.test.ts +2 -6
  96. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  97. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  98. package/src/__tests__/llm-context-route-provider.test.ts +103 -6
  99. package/src/__tests__/llm-request-log-turn-query.test.ts +164 -6
  100. package/src/__tests__/llm-usage-store.test.ts +2 -6
  101. package/src/__tests__/log-export-workspace.test.ts +74 -111
  102. package/src/__tests__/managed-store.test.ts +38 -11
  103. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  104. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  105. package/src/__tests__/memory-jobs-worker-backoff.test.ts +2 -8
  106. package/src/__tests__/memory-recall-log-store.test.ts +134 -6
  107. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -112
  108. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  109. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  110. package/src/__tests__/mock-fetch.ts +87 -0
  111. package/src/__tests__/non-member-access-request.test.ts +2 -6
  112. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  113. package/src/__tests__/notification-guardian-path.test.ts +2 -6
  114. package/src/__tests__/oauth-cli.test.ts +364 -2
  115. package/src/__tests__/oauth2-gateway-transport.test.ts +18 -3
  116. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  117. package/src/__tests__/outlook-attachments.test.ts +301 -0
  118. package/src/__tests__/outlook-automation-tools.test.ts +425 -0
  119. package/src/__tests__/outlook-categories.test.ts +212 -0
  120. package/src/__tests__/outlook-client-automation.test.ts +246 -0
  121. package/src/__tests__/outlook-compose-tools.test.ts +325 -0
  122. package/src/__tests__/outlook-declutter-tools.test.ts +585 -0
  123. package/src/__tests__/outlook-email-watcher.test.ts +322 -0
  124. package/src/__tests__/outlook-follow-up.test.ts +196 -0
  125. package/src/__tests__/outlook-messaging-provider.test.ts +498 -3
  126. package/src/__tests__/outlook-trash.test.ts +77 -0
  127. package/src/__tests__/outlook-unsubscribe.test.ts +250 -0
  128. package/src/__tests__/parser.test.ts +32 -0
  129. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  130. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  131. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  132. package/src/__tests__/permission-mode-store.test.ts +277 -0
  133. package/src/__tests__/permission-mode.test.ts +101 -0
  134. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  135. package/src/__tests__/platform-callback-registration.test.ts +4 -4
  136. package/src/__tests__/playbook-execution.test.ts +76 -80
  137. package/src/__tests__/playbook-tools.test.ts +5 -7
  138. package/src/__tests__/profiler-routes.test.ts +502 -0
  139. package/src/__tests__/profiler-run-store.test.ts +441 -0
  140. package/src/__tests__/provider-error-scenarios.test.ts +21 -0
  141. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  142. package/src/__tests__/rebuild-index-graph-nodes.test.ts +273 -0
  143. package/src/__tests__/registry.test.ts +3 -3
  144. package/src/__tests__/require-fresh-approval.test.ts +64 -2
  145. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -6
  146. package/src/__tests__/runtime-events-sse.test.ts +2 -6
  147. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  148. package/src/__tests__/schedule-store.test.ts +2 -6
  149. package/src/__tests__/schedule-tools.test.ts +2 -6
  150. package/src/__tests__/scheduler-recurrence.test.ts +1 -5
  151. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  152. package/src/__tests__/scoped-approval-grants.test.ts +2 -6
  153. package/src/__tests__/scoped-grant-security-matrix.test.ts +2 -6
  154. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  155. package/src/__tests__/search-skills-unified.test.ts +422 -0
  156. package/src/__tests__/secret-onetime-send.test.ts +2 -0
  157. package/src/__tests__/send-endpoint-busy.test.ts +44 -9
  158. package/src/__tests__/sequence-store.test.ts +2 -6
  159. package/src/__tests__/server-history-render.test.ts +2 -6
  160. package/src/__tests__/set-permission-mode.test.ts +274 -0
  161. package/src/__tests__/skill-feature-flags-integration.test.ts +38 -31
  162. package/src/__tests__/skill-feature-flags.test.ts +6 -6
  163. package/src/__tests__/skill-load-feature-flag.test.ts +23 -11
  164. package/src/__tests__/skill-memory.test.ts +2 -741
  165. package/src/__tests__/skills-uninstall.test.ts +2 -2
  166. package/src/__tests__/skills.test.ts +1 -1
  167. package/src/__tests__/slack-inbound-verification.test.ts +2 -6
  168. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  169. package/src/__tests__/subagent-detail.test.ts +84 -0
  170. package/src/__tests__/subagent-disposal.test.ts +308 -0
  171. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  172. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  173. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  174. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  175. package/src/__tests__/subagent-tools.test.ts +464 -4
  176. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  177. package/src/__tests__/task-compiler.test.ts +2 -6
  178. package/src/__tests__/task-management-tools.test.ts +2 -6
  179. package/src/__tests__/task-memory-cleanup.test.ts +185 -241
  180. package/src/__tests__/task-runner.test.ts +2 -6
  181. package/src/__tests__/task-scheduler.test.ts +2 -6
  182. package/src/__tests__/terminal-tools.test.ts +17 -27
  183. package/src/__tests__/test-preload.ts +7 -0
  184. package/src/__tests__/tool-approval-handler.test.ts +2 -6
  185. package/src/__tests__/tool-executor.test.ts +4 -26
  186. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -6
  187. package/src/__tests__/tool-side-effects-slack-dm.test.ts +277 -0
  188. package/src/__tests__/top-level-renderer.test.ts +10 -13
  189. package/src/__tests__/trust-store.test.ts +1 -1
  190. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -6
  191. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +118 -8
  192. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -6
  193. package/src/__tests__/trusted-contact-verification.test.ts +2 -6
  194. package/src/__tests__/turn-boundary-resolution.test.ts +2 -6
  195. package/src/__tests__/usage-cache-backfill-migration.test.ts +1 -6
  196. package/src/__tests__/usage-routes.test.ts +2 -6
  197. package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
  198. package/src/__tests__/voice-invite-redemption.test.ts +2 -6
  199. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -6
  200. package/src/__tests__/voice-session-bridge.test.ts +2 -6
  201. package/src/__tests__/volume-security-guard.test.ts +2 -0
  202. package/src/__tests__/workspace-lifecycle.test.ts +29 -1
  203. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -6
  204. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -6
  205. package/src/__tests__/workspace-migration-026-backfill-install-meta.test.ts +558 -0
  206. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  207. package/src/__tests__/workspace-policy.test.ts +1 -1
  208. package/src/agent/attachments.ts +7 -2
  209. package/src/agent/image-optimize.ts +165 -0
  210. package/src/agent/loop.ts +7 -15
  211. package/src/approvals/guardian-request-resolvers.ts +24 -0
  212. package/src/avatar/traits-png-sync.ts +3 -3
  213. package/src/bundler/app-compiler.ts +179 -2
  214. package/src/bundler/package-resolver.ts +3 -5
  215. package/src/cli/__tests__/notifications.test.ts +1 -2
  216. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  217. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  218. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  219. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  220. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  221. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  222. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  223. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  224. package/src/cli/commands/avatar.ts +3 -3
  225. package/src/cli/commands/config.ts +26 -13
  226. package/src/cli/commands/conversations.ts +1 -8
  227. package/src/cli/commands/doctor.ts +2 -2
  228. package/src/cli/commands/email.ts +584 -835
  229. package/src/cli/commands/memory.ts +37 -84
  230. package/src/cli/commands/notifications.ts +7 -2
  231. package/src/cli/commands/oauth/__tests__/connect.test.ts +2 -2
  232. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +2 -2
  233. package/src/cli/commands/oauth/__tests__/mode.test.ts +8 -1
  234. package/src/cli/commands/oauth/__tests__/status.test.ts +2 -2
  235. package/src/cli/commands/oauth/connect.ts +25 -11
  236. package/src/cli/commands/oauth/mode.ts +7 -0
  237. package/src/cli/commands/oauth/shared.ts +39 -3
  238. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  239. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  240. package/src/cli/commands/platform/__tests__/status.test.ts +5 -5
  241. package/src/cli/commands/platform/index.ts +16 -16
  242. package/src/cli/commands/routes.ts +396 -0
  243. package/src/cli/commands/skills.ts +218 -36
  244. package/src/cli/commands/trust.ts +2 -2
  245. package/src/cli/lib/daemon-credential-client.ts +2 -3
  246. package/src/cli/program.ts +2 -0
  247. package/src/cli.ts +1 -120
  248. package/src/config/bundled-skills/acp/TOOLS.json +1 -1
  249. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  250. package/src/config/bundled-skills/contacts/SKILL.md +0 -1
  251. package/src/config/bundled-skills/contacts/TOOLS.json +0 -8
  252. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -4
  253. package/src/config/bundled-skills/gmail/SKILL.md +4 -12
  254. package/src/config/bundled-skills/google-calendar/SKILL.md +1 -9
  255. package/src/config/bundled-skills/messaging/SKILL.md +17 -18
  256. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +40 -33
  257. package/src/config/bundled-skills/outlook/SKILL.md +189 -0
  258. package/src/config/bundled-skills/outlook/TOOLS.json +530 -0
  259. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +85 -0
  260. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +77 -0
  261. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +84 -0
  262. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +94 -0
  263. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +49 -0
  264. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +237 -0
  265. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +161 -0
  266. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +32 -0
  267. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +272 -0
  268. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +29 -0
  269. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +129 -0
  270. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +87 -0
  271. package/src/config/bundled-skills/outlook/tools/shared.ts +20 -0
  272. package/src/config/bundled-skills/outlook-calendar/SKILL.md +51 -0
  273. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +221 -0
  274. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +252 -0
  275. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +53 -0
  276. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +74 -0
  277. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +18 -0
  278. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +46 -0
  279. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +36 -0
  280. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +17 -0
  281. package/src/config/bundled-skills/outlook-calendar/types.ts +120 -0
  282. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +47 -40
  283. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +16 -29
  284. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +16 -18
  285. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +39 -47
  286. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  287. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  288. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  289. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  290. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  291. package/src/config/bundled-skills/slack/SKILL.md +3 -7
  292. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  293. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  294. package/src/config/bundled-tool-registry.ts +56 -4
  295. package/src/config/env-registry.ts +78 -8
  296. package/src/config/feature-flag-registry.json +38 -125
  297. package/src/config/schema.ts +8 -0
  298. package/src/config/schemas/filing.ts +51 -0
  299. package/src/config/schemas/heartbeat.ts +15 -12
  300. package/src/config/schemas/memory-lifecycle.ts +12 -0
  301. package/src/config/schemas/platform.ts +8 -0
  302. package/src/config/schemas/security.ts +14 -0
  303. package/src/config/schemas/timeouts.ts +1 -1
  304. package/src/config/skills.ts +18 -7
  305. package/src/context/token-estimator.ts +25 -18
  306. package/src/context/window-manager.ts +6 -2
  307. package/src/credential-execution/process-manager.ts +3 -1
  308. package/src/daemon/app-source-watcher.ts +93 -0
  309. package/src/daemon/config-watcher.ts +79 -1
  310. package/src/daemon/context-overflow-reducer.ts +46 -2
  311. package/src/daemon/conversation-agent-loop-handlers.ts +143 -82
  312. package/src/daemon/conversation-agent-loop.ts +236 -108
  313. package/src/daemon/conversation-error.ts +31 -8
  314. package/src/daemon/conversation-history.ts +4 -19
  315. package/src/daemon/conversation-lifecycle.ts +36 -9
  316. package/src/daemon/conversation-media-retry.ts +85 -7
  317. package/src/daemon/conversation-notifiers.ts +4 -1
  318. package/src/daemon/conversation-process.ts +13 -7
  319. package/src/daemon/conversation-runtime-assembly.ts +305 -306
  320. package/src/daemon/conversation-tool-setup.ts +44 -14
  321. package/src/daemon/conversation-workspace.ts +1 -2
  322. package/src/daemon/conversation.ts +59 -2
  323. package/src/daemon/daemon-control.ts +8 -2
  324. package/src/daemon/date-context.ts +26 -53
  325. package/src/daemon/first-greeting.ts +1 -1
  326. package/src/daemon/handlers/conversations.ts +4 -7
  327. package/src/daemon/handlers/shared.test.ts +143 -0
  328. package/src/daemon/handlers/shared.ts +85 -17
  329. package/src/daemon/handlers/skills.ts +416 -209
  330. package/src/daemon/lifecycle.ts +212 -131
  331. package/src/daemon/main.ts +5 -1
  332. package/src/daemon/message-types/conversations.ts +29 -7
  333. package/src/daemon/message-types/messages.ts +12 -2
  334. package/src/daemon/message-types/schedules.ts +1 -0
  335. package/src/daemon/message-types/settings.ts +6 -0
  336. package/src/daemon/message-types/skills.ts +97 -36
  337. package/src/daemon/profiler-run-store.ts +557 -0
  338. package/src/daemon/providers-setup.ts +5 -0
  339. package/src/daemon/server.ts +100 -11
  340. package/src/daemon/shutdown-handlers.ts +5 -0
  341. package/src/daemon/tool-side-effects.ts +50 -8
  342. package/src/export/transcript-formatter.ts +148 -0
  343. package/src/filing/filing-service.ts +228 -0
  344. package/src/heartbeat/heartbeat-service.ts +97 -7
  345. package/src/hooks/cli.ts +2 -2
  346. package/src/hooks/runner.ts +15 -38
  347. package/src/inbound/platform-callback-registration.ts +14 -14
  348. package/src/mcp/client.ts +6 -0
  349. package/src/mcp/mcp-oauth-provider.ts +149 -27
  350. package/src/memory/admin.ts +42 -75
  351. package/src/memory/app-store.ts +69 -0
  352. package/src/memory/conversation-bootstrap.ts +3 -1
  353. package/src/memory/conversation-crud.ts +211 -288
  354. package/src/memory/conversation-group-migration.ts +157 -0
  355. package/src/memory/conversation-queries.ts +61 -13
  356. package/src/memory/conversation-title-service.ts +1 -0
  357. package/src/memory/db-init.ts +194 -361
  358. package/src/memory/embed.ts +73 -0
  359. package/src/memory/embedding-backend.ts +8 -14
  360. package/src/memory/embedding-runtime-manager.ts +12 -114
  361. package/src/memory/fingerprint.ts +2 -2
  362. package/src/memory/graph/bootstrap.ts +521 -0
  363. package/src/memory/graph/capability-seed.ts +449 -0
  364. package/src/memory/graph/consolidation.ts +725 -0
  365. package/src/memory/graph/conversation-graph-memory.ts +659 -0
  366. package/src/memory/graph/decay.test.ts +208 -0
  367. package/src/memory/graph/decay.ts +195 -0
  368. package/src/memory/graph/extraction-job.ts +74 -0
  369. package/src/memory/graph/extraction.test.ts +936 -0
  370. package/src/memory/graph/extraction.ts +1297 -0
  371. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  372. package/src/memory/graph/graph-search.ts +280 -0
  373. package/src/memory/graph/image-ref-utils.ts +29 -0
  374. package/src/memory/graph/injection.test.ts +513 -0
  375. package/src/memory/graph/injection.ts +469 -0
  376. package/src/memory/graph/inspect.ts +543 -0
  377. package/src/memory/graph/narrative.ts +267 -0
  378. package/src/memory/graph/pattern-scan.ts +269 -0
  379. package/src/memory/graph/retriever.ts +1111 -0
  380. package/src/memory/graph/scoring.test.ts +548 -0
  381. package/src/memory/graph/scoring.ts +232 -0
  382. package/src/memory/graph/serendipity.ts +65 -0
  383. package/src/memory/graph/store.test.ts +1098 -0
  384. package/src/memory/graph/store.ts +838 -0
  385. package/src/memory/graph/tool-handlers.ts +301 -0
  386. package/src/memory/graph/tools.ts +97 -0
  387. package/src/memory/graph/triggers.test.ts +487 -0
  388. package/src/memory/graph/triggers.ts +223 -0
  389. package/src/memory/graph/types.ts +295 -0
  390. package/src/memory/group-crud.ts +191 -0
  391. package/src/memory/indexer.ts +37 -19
  392. package/src/memory/job-handlers/cleanup.ts +32 -42
  393. package/src/memory/job-handlers/conversation-starters.ts +91 -53
  394. package/src/memory/job-handlers/embedding.ts +5 -31
  395. package/src/memory/job-handlers/index-maintenance.ts +23 -11
  396. package/src/memory/job-handlers/summarization.ts +32 -17
  397. package/src/memory/job-utils.ts +1 -1
  398. package/src/memory/jobs-store.ts +21 -31
  399. package/src/memory/jobs-worker.ts +180 -129
  400. package/src/memory/llm-request-log-store.ts +96 -12
  401. package/src/memory/memory-recall-log-store.ts +49 -5
  402. package/src/memory/message-content.ts +1 -0
  403. package/src/memory/migrations/202-memory-graph-tables.ts +130 -0
  404. package/src/memory/migrations/203-drop-memory-items-tables.ts +55 -0
  405. package/src/memory/migrations/204-rename-memory-graph-type-values.ts +46 -0
  406. package/src/memory/migrations/205-memory-graph-image-refs.ts +11 -0
  407. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  408. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  409. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  410. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  411. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  412. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  413. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  414. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  415. package/src/memory/migrations/index.ts +12 -0
  416. package/src/memory/migrations/registry.ts +16 -0
  417. package/src/memory/qdrant-client.ts +44 -17
  418. package/src/memory/schema/conversations.ts +14 -0
  419. package/src/memory/schema/index.ts +1 -0
  420. package/src/memory/schema/infrastructure.ts +8 -1
  421. package/src/memory/schema/memory-core.ts +0 -51
  422. package/src/memory/schema/memory-graph.ts +154 -0
  423. package/src/memory/search/semantic.ts +47 -91
  424. package/src/memory/task-memory-cleanup.ts +58 -61
  425. package/src/messaging/providers/outlook/adapter.ts +8 -1
  426. package/src/messaging/providers/outlook/client.ts +299 -0
  427. package/src/messaging/providers/outlook/types.ts +118 -0
  428. package/src/notifications/adapters/macos.ts +1 -0
  429. package/src/notifications/copy-composer.ts +95 -0
  430. package/src/notifications/decision-engine.ts +35 -0
  431. package/src/notifications/signal.ts +16 -0
  432. package/src/oauth/seed-providers.ts +2 -1
  433. package/src/permissions/checker.ts +36 -4
  434. package/src/permissions/defaults.ts +4 -4
  435. package/src/permissions/permission-mode-store.ts +180 -0
  436. package/src/permissions/permission-mode.ts +31 -0
  437. package/src/permissions/workspace-policy.ts +10 -1
  438. package/src/playbooks/playbook-compiler.ts +19 -18
  439. package/src/playbooks/types.ts +4 -3
  440. package/src/prompts/system-prompt.ts +62 -36
  441. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  442. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  443. package/src/prompts/templates/HEARTBEAT.md +3 -1
  444. package/src/prompts/templates/SOUL.md +25 -4
  445. package/src/prompts/templates/UPDATES.md +8 -0
  446. package/src/providers/anthropic/client.ts +136 -220
  447. package/src/providers/gemini/client.ts +1 -1
  448. package/src/providers/openai/client.ts +1 -1
  449. package/src/providers/registry.ts +1 -1
  450. package/src/providers/retry.ts +19 -3
  451. package/src/runtime/actor-trust-resolver.ts +5 -1
  452. package/src/runtime/auth/route-policy.ts +30 -0
  453. package/src/runtime/guardian-reply-router.ts +5 -1
  454. package/src/runtime/http-server.ts +55 -5
  455. package/src/runtime/http-types.ts +12 -1
  456. package/src/runtime/middleware/auth.ts +20 -0
  457. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  458. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  459. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  460. package/src/runtime/routes/app-management-routes.ts +1 -11
  461. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  462. package/src/runtime/routes/archive-utils.ts +29 -0
  463. package/src/runtime/routes/attachment-routes.test.ts +106 -0
  464. package/src/runtime/routes/attachment-routes.ts +106 -16
  465. package/src/runtime/routes/avatar-routes.ts +2 -9
  466. package/src/runtime/routes/brain-graph-routes.ts +21 -22
  467. package/src/runtime/routes/btw-routes.ts +22 -1
  468. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  469. package/src/runtime/routes/conversation-management-routes.ts +3 -14
  470. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  471. package/src/runtime/routes/conversation-routes.ts +264 -44
  472. package/src/runtime/routes/conversation-starter-routes.ts +2 -2
  473. package/src/runtime/routes/debug-routes.ts +1 -1
  474. package/src/runtime/routes/global-search-routes.ts +21 -19
  475. package/src/runtime/routes/group-routes.ts +207 -0
  476. package/src/runtime/routes/guardian-action-routes.ts +21 -10
  477. package/src/runtime/routes/guardian-bootstrap-routes.ts +23 -19
  478. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  479. package/src/runtime/routes/identity-routes.ts +53 -18
  480. package/src/runtime/routes/inbound-message-handler.ts +19 -0
  481. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +292 -0
  482. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +207 -0
  483. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  484. package/src/runtime/routes/log-export-routes.ts +23 -275
  485. package/src/runtime/routes/memory-item-routes.test.ts +170 -247
  486. package/src/runtime/routes/memory-item-routes.ts +341 -388
  487. package/src/runtime/routes/migration-routes.ts +18 -7
  488. package/src/runtime/routes/profiler-routes.ts +350 -0
  489. package/src/runtime/routes/schedule-routes.ts +28 -11
  490. package/src/runtime/routes/settings-routes.ts +95 -8
  491. package/src/runtime/routes/skills-routes.ts +103 -37
  492. package/src/runtime/routes/subagents-routes.ts +28 -7
  493. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  494. package/src/runtime/routes/user-routes.ts +41 -0
  495. package/src/runtime/routes/work-items-routes.test.ts +2 -6
  496. package/src/runtime/routes/workspace-routes.ts +0 -1
  497. package/src/schedule/schedule-store.ts +30 -0
  498. package/src/schedule/scheduler.ts +52 -18
  499. package/src/security/oauth2.ts +1 -1
  500. package/src/security/secure-keys.ts +4 -8
  501. package/src/shared/provider-env-vars.ts +19 -0
  502. package/src/skills/catalog-cache.ts +5 -0
  503. package/src/skills/catalog-install.ts +25 -16
  504. package/src/skills/clawhub.ts +134 -154
  505. package/src/skills/install-meta.ts +208 -0
  506. package/src/skills/managed-store.ts +29 -18
  507. package/src/skills/skill-memory.ts +12 -229
  508. package/src/skills/skillssh-registry.ts +19 -17
  509. package/src/subagent/index.ts +13 -3
  510. package/src/subagent/manager.ts +308 -29
  511. package/src/subagent/types.ts +68 -0
  512. package/src/tasks/task-runner.ts +7 -5
  513. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -5
  514. package/src/tools/apps/executors.ts +29 -4
  515. package/src/tools/browser/runtime-check.ts +3 -1
  516. package/src/tools/filesystem/list.ts +93 -0
  517. package/src/tools/memory/register.ts +63 -46
  518. package/src/tools/permission-checker.ts +85 -1
  519. package/src/tools/registry.ts +4 -0
  520. package/src/tools/schedule/create.ts +3 -0
  521. package/src/tools/schedule/list.ts +1 -0
  522. package/src/tools/schedule/update.ts +6 -0
  523. package/src/tools/shared/filesystem/errors.ts +5 -0
  524. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  525. package/src/tools/shared/filesystem/image-read.ts +22 -85
  526. package/src/tools/shared/filesystem/types.ts +17 -0
  527. package/src/tools/shared/shell-output.ts +31 -2
  528. package/src/tools/subagent/abort.ts +12 -2
  529. package/src/tools/subagent/message.ts +9 -2
  530. package/src/tools/subagent/notify-parent.ts +79 -0
  531. package/src/tools/subagent/read.ts +29 -8
  532. package/src/tools/subagent/resolve.ts +21 -0
  533. package/src/tools/subagent/spawn.ts +2 -0
  534. package/src/tools/subagent/status.ts +11 -1
  535. package/src/tools/system/avatar-generator.ts +3 -3
  536. package/src/tools/system/register.ts +23 -0
  537. package/src/tools/system/set-permission-mode.ts +103 -0
  538. package/src/tools/terminal/parser.ts +30 -5
  539. package/src/tools/terminal/safe-env.ts +17 -1
  540. package/src/tools/tool-manifest.ts +9 -3
  541. package/src/tools/types.ts +2 -0
  542. package/src/util/browser.ts +25 -10
  543. package/src/util/bun-runtime.ts +172 -0
  544. package/src/util/logger.ts +1 -1
  545. package/src/util/platform.ts +50 -17
  546. package/src/watcher/providers/outlook-calendar.ts +343 -0
  547. package/src/watcher/providers/outlook.ts +198 -0
  548. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  549. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  550. package/src/workspace/migrations/025-remove-oauth-app-setup-skills.ts +76 -0
  551. package/src/workspace/migrations/026-backfill-install-meta.ts +325 -0
  552. package/src/workspace/migrations/027-remove-orphaned-optimized-images-cache.ts +42 -0
  553. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  554. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  555. package/src/workspace/migrations/registry.ts +10 -0
  556. package/src/workspace/top-level-renderer.ts +5 -9
  557. package/src/__tests__/cli-memory.test.ts +0 -372
  558. package/src/__tests__/clipboard.test.ts +0 -88
  559. package/src/__tests__/context-memory-e2e.test.ts +0 -415
  560. package/src/__tests__/journal-context.test.ts +0 -268
  561. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -297
  562. package/src/__tests__/memory-lifecycle-e2e.test.ts +0 -459
  563. package/src/__tests__/memory-query-builder.test.ts +0 -59
  564. package/src/__tests__/memory-recall-quality.test.ts +0 -1046
  565. package/src/__tests__/memory-regressions.experimental.test.ts +0 -629
  566. package/src/__tests__/memory-regressions.test.ts +0 -3696
  567. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -295
  568. package/src/cli/cli-memory.ts +0 -176
  569. package/src/daemon/conversation-memory.ts +0 -207
  570. package/src/memory/conversation-starters-cadence.ts +0 -74
  571. package/src/memory/items-extractor.ts +0 -860
  572. package/src/memory/job-handlers/batch-extraction.ts +0 -753
  573. package/src/memory/job-handlers/extraction.ts +0 -40
  574. package/src/memory/job-handlers/journal-carry-forward.test.ts +0 -355
  575. package/src/memory/job-handlers/journal-carry-forward.ts +0 -255
  576. package/src/memory/journal-memory.ts +0 -224
  577. package/src/memory/query-builder.ts +0 -47
  578. package/src/memory/query-expansion.ts +0 -83
  579. package/src/memory/retriever.test.ts +0 -1592
  580. package/src/memory/retriever.ts +0 -1331
  581. package/src/memory/search/formatting.test.ts +0 -140
  582. package/src/memory/search/formatting.ts +0 -262
  583. package/src/memory/search/mmr.ts +0 -139
  584. package/src/memory/search/ranking.ts +0 -15
  585. package/src/memory/search/staleness.ts +0 -40
  586. package/src/memory/search/tier-classifier.ts +0 -18
  587. package/src/memory/search/types.ts +0 -121
  588. package/src/prompts/journal-context.ts +0 -154
  589. package/src/tools/memory/definitions.ts +0 -69
  590. package/src/tools/memory/handlers.test.ts +0 -562
  591. package/src/tools/memory/handlers.ts +0 -434
  592. package/src/util/clipboard.ts +0 -34
@@ -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,
@@ -72,7 +70,7 @@ const interfaceIdSchema = z.enum(INTERFACE_IDS);
72
70
  const subagentNotificationSchema = z.object({
73
71
  subagentId: z.string(),
74
72
  label: z.string(),
75
- status: z.enum(["completed", "failed", "aborted"]),
73
+ status: z.enum(["running", "completed", "failed", "aborted"]),
76
74
  error: z.string().optional(),
77
75
  conversationId: z.string().optional(),
78
76
  });
@@ -174,6 +172,7 @@ export interface ConversationRow {
174
172
  forkParentMessageId: string | null;
175
173
  isAutoTitle: number;
176
174
  scheduleJobId: string | null;
175
+ lastMessageAt: number | null;
177
176
  }
178
177
 
179
178
  export const parseConversation = createRowMapper<
@@ -199,6 +198,7 @@ export const parseConversation = createRowMapper<
199
198
  forkParentMessageId: "forkParentMessageId",
200
199
  isAutoTitle: "isAutoTitle",
201
200
  scheduleJobId: "scheduleJobId",
201
+ lastMessageAt: "lastMessageAt",
202
202
  });
203
203
 
204
204
  export interface MessageRow {
@@ -241,9 +241,10 @@ export function createConversation(
241
241
  | string
242
242
  | {
243
243
  title?: string;
244
- conversationType?: "standard" | "private" | "background";
244
+ conversationType?: "standard" | "private" | "background" | "scheduled";
245
245
  source?: string;
246
246
  scheduleJobId?: string;
247
+ groupId?: string;
247
248
  },
248
249
  ) {
249
250
  const db = getDb();
@@ -254,9 +255,16 @@ export function createConversation(
254
255
  : (titleOrOpts ?? {});
255
256
  const conversationType = opts.conversationType ?? "standard";
256
257
  const source = opts.source ?? "user";
258
+ const groupId = opts.groupId;
257
259
  const id = uuid();
258
260
  const memoryScopeId =
259
261
  conversationType === "private" ? `private:${id}` : "default";
262
+
263
+ // Ensure group_id column exists for deterministic schema readiness,
264
+ // even when this conversation has no groupId (a subsequent query or
265
+ // reorder may reference the column).
266
+ ensureGroupMigration();
267
+
260
268
  const conversation = {
261
269
  id,
262
270
  title: opts.title ?? null,
@@ -276,6 +284,12 @@ export function createConversation(
276
284
 
277
285
  // Retry on SQLITE_BUSY and SQLITE_IOERR — transient disk I/O errors or WAL
278
286
  // contention can cause the first attempt to fail even under normal load.
287
+ // INSERT and group_id UPDATE are retried independently so a transient failure
288
+ // on the UPDATE doesn't re-execute the already-succeeded INSERT (which would
289
+ // hit a unique constraint violation).
290
+ // No explicit BEGIN/COMMIT here — callers that need atomicity (e.g.
291
+ // forkConversation) wrap in their own transaction, and nesting raw BEGIN
292
+ // inside Drizzle's db.transaction() would crash SQLite.
279
293
  const MAX_RETRIES = 3;
280
294
  for (let attempt = 0; ; attempt++) {
281
295
  try {
@@ -289,10 +303,8 @@ export function createConversation(
289
303
  ) {
290
304
  log.warn(
291
305
  { attempt, conversationId: id, code },
292
- "createConversation: transient SQLite error, retrying",
306
+ "createConversation: INSERT transient error, retrying",
293
307
  );
294
- // Synchronous sleep — createConversation is synchronous and the
295
- // retry window is short (50-150ms), so Bun.sleepSync is appropriate.
296
308
  Bun.sleepSync(50 * (attempt + 1));
297
309
  continue;
298
310
  }
@@ -300,6 +312,35 @@ export function createConversation(
300
312
  }
301
313
  }
302
314
 
315
+ // group_id is NOT in the Drizzle schema (raw-query-only pattern).
316
+ // Set via raw SQL after the INSERT succeeds.
317
+ if (groupId) {
318
+ for (let attempt = 0; ; attempt++) {
319
+ try {
320
+ rawRun(
321
+ "UPDATE conversations SET group_id = ? WHERE id = ?",
322
+ groupId,
323
+ id,
324
+ );
325
+ break;
326
+ } catch (err) {
327
+ const code = (err as { code?: string }).code ?? "";
328
+ if (
329
+ attempt < MAX_RETRIES &&
330
+ (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_IOERR"))
331
+ ) {
332
+ log.warn(
333
+ { attempt, conversationId: id, code },
334
+ "createConversation: group_id UPDATE transient error, retrying",
335
+ );
336
+ Bun.sleepSync(50 * (attempt + 1));
337
+ continue;
338
+ }
339
+ throw err;
340
+ }
341
+ }
342
+ }
343
+
303
344
  initConversationDir({ ...conversation, originChannel: null });
304
345
 
305
346
  return conversation;
@@ -344,6 +385,20 @@ export function getConversationMemoryScopeId(conversationId: string): string {
344
385
  return conv?.memoryScopeId ?? "default";
345
386
  }
346
387
 
388
+ /**
389
+ * Fetch group_id for a conversation via raw SQL. group_id is NOT in the
390
+ * Drizzle schema (raw-query-only pattern), so ConversationRow doesn't
391
+ * include it. This helper is used by forkConversation to inherit group_id.
392
+ */
393
+ export function getConversationGroupId(conversationId: string): string | null {
394
+ ensureGroupMigration();
395
+ const row = rawGet<{ group_id: string | null }>(
396
+ "SELECT group_id FROM conversations WHERE id = ?",
397
+ conversationId,
398
+ );
399
+ return row?.group_id ?? null;
400
+ }
401
+
347
402
  export function forkConversation(params: {
348
403
  conversationId: string;
349
404
  throughMessageId?: string;
@@ -407,10 +462,14 @@ export function forkConversation(params: {
407
462
  // (linkAttachmentToMessage, relinkAttachments, seedForkedConversationAttention)
408
463
  // use the same underlying bun:sqlite connection, so their writes participate
409
464
  // in this transaction automatically.
465
+ // Inherit group_id from parent via raw SQL helper (group_id is not in Drizzle schema)
466
+ const parentGroupId = getConversationGroupId(conversationId);
467
+
410
468
  const forkedConversation = db.transaction(() => {
411
469
  const fc = createConversation({
412
470
  title: forkTitle,
413
471
  conversationType: "standard",
472
+ groupId: parentGroupId ?? undefined,
414
473
  });
415
474
 
416
475
  db.update(conversations)
@@ -526,6 +585,16 @@ export function forkConversation(params: {
526
585
  });
527
586
  }
528
587
 
588
+ // Set lastMessageAt to the max createdAt of copied messages so the
589
+ // forked conversation sorts correctly by message recency.
590
+ const lastCopiedMessage = messagesToCopy.at(-1);
591
+ if (lastCopiedMessage) {
592
+ db.update(conversations)
593
+ .set({ lastMessageAt: lastCopiedMessage.createdAt })
594
+ .where(eq(conversations.id, fc.id))
595
+ .run();
596
+ }
597
+
529
598
  seedForkedConversationAttention({
530
599
  conversationId: fc.id,
531
600
  latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
@@ -553,14 +622,13 @@ export function forkConversation(params: {
553
622
 
554
623
  /**
555
624
  * Delete a conversation and all its messages, cleaning up orphaned memory
556
- * artifacts (items, embeddings). Returns segment and orphaned item IDs so
557
- * callers can clean up the corresponding Qdrant vector entries.
625
+ * artifacts (embeddings). Returns segment IDs so callers can clean up
626
+ * the corresponding Qdrant vector entries.
558
627
  */
559
628
  export function deleteConversation(id: string): DeletedMemoryIds {
560
629
  const db = getDb();
561
630
  const result: DeletedMemoryIds = {
562
631
  segmentIds: [],
563
- orphanedItemIds: [],
564
632
  deletedSummaryIds: [],
565
633
  };
566
634
 
@@ -589,16 +657,6 @@ export function deleteConversation(id: string): DeletedMemoryIds {
589
657
  .all();
590
658
  result.segmentIds = linkedSegments.map((r) => r.id);
591
659
 
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
660
  // Delete non-cascading tables first.
603
661
  tx.delete(llmRequestLogs)
604
662
  .where(eq(llmRequestLogs.conversationId, id))
@@ -606,7 +664,7 @@ export function deleteConversation(id: string): DeletedMemoryIds {
606
664
  tx.delete(toolInvocations)
607
665
  .where(eq(toolInvocations.conversationId, id))
608
666
  .run();
609
- // Cascade deletes memory_segments, memory_item_sources, message_attachments.
667
+ // Cascade deletes memory_segments, message_attachments.
610
668
  tx.delete(messages).where(eq(messages.conversationId, id)).run();
611
669
 
612
670
  // Clean up segment embeddings.
@@ -620,34 +678,6 @@ export function deleteConversation(id: string): DeletedMemoryIds {
620
678
  )
621
679
  .run();
622
680
  }
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
681
  } else {
652
682
  // No messages — just clean up non-message tables.
653
683
  tx.delete(llmRequestLogs)
@@ -659,35 +689,6 @@ export function deleteConversation(id: string): DeletedMemoryIds {
659
689
  }
660
690
 
661
691
  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
692
  // Sweep memory summaries with this private scopeId.
692
693
  const scopeSummaries = tx
693
694
  .select({ id: memorySummaries.id })
@@ -733,79 +734,16 @@ export function deleteConversation(id: string): DeletedMemoryIds {
733
734
  *
734
735
  * Extends `deleteConversation` with:
735
736
  * - Cancelling pending memory jobs before deletion
736
- * - Restoring memory items that were explicitly superseded by items from this conversation
737
- * - Restoring orphaned subject-match superseded items after deletion
738
737
  * - Deleting conversation-scoped memory summaries and their embeddings
739
- * - Enqueuing `embed_item` jobs for all restored items
740
738
  */
741
739
  export function wipeConversation(id: string): WipeConversationResult {
742
740
  const db = getDb();
743
- const unsupersededItemIds: string[] = [];
744
741
  const deletedSummaryIds: string[] = [];
745
742
 
746
743
  // Step A — Cancel pending memory jobs (before deleting messages, since
747
744
  // the cancellation queries join on `messages`).
748
745
  const cancelledJobCount = cancelPendingJobsForConversation(id);
749
746
 
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
747
  // Step C — Delete conversation-scoped memory summaries and their embeddings.
810
748
  const summaryRows = db
811
749
  .select({ id: memorySummaries.id })
@@ -833,82 +771,14 @@ export function wipeConversation(id: string): WipeConversationResult {
833
771
  }
834
772
  deletedSummaryIds.push(...summaryIds);
835
773
 
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.
774
+ // Step D — Delegate to deleteConversation which handles messages (cascade
775
+ // segments, attachments), llmRequestLogs, toolInvocations,
776
+ // embeddings, and the conversation row.
864
777
  const deletedMemoryIds = deleteConversation(id);
865
778
 
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.
779
+ // Step EReturn the combined result.
909
780
  return {
910
781
  ...deletedMemoryIds,
911
- unsupersededItemIds,
912
782
  deletedSummaryIds: [
913
783
  ...deletedSummaryIds,
914
784
  ...deletedMemoryIds.deletedSummaryIds,
@@ -938,20 +808,17 @@ export function purgePrivateConversations(): {
938
808
  count: 0,
939
809
  deletedMemory: {
940
810
  segmentIds: [],
941
- orphanedItemIds: [],
942
811
  deletedSummaryIds: [],
943
812
  },
944
813
  };
945
814
  }
946
815
 
947
816
  const allSegmentIds: string[] = [];
948
- const allOrphanedItemIds: string[] = [];
949
817
  const allDeletedSummaryIds: string[] = [];
950
818
 
951
819
  for (const conv of privateConvs) {
952
820
  const deleted = deleteConversation(conv.id);
953
821
  allSegmentIds.push(...deleted.segmentIds);
954
- allOrphanedItemIds.push(...deleted.orphanedItemIds);
955
822
  allDeletedSummaryIds.push(...deleted.deletedSummaryIds);
956
823
  }
957
824
 
@@ -959,7 +826,6 @@ export function purgePrivateConversations(): {
959
826
  count: privateConvs.length,
960
827
  deletedMemory: {
961
828
  segmentIds: allSegmentIds,
962
- orphanedItemIds: allOrphanedItemIds,
963
829
  deletedSummaryIds: allDeletedSummaryIds,
964
830
  },
965
831
  };
@@ -1021,7 +887,7 @@ export async function addMessage(
1021
887
  .run();
1022
888
  }
1023
889
  tx.update(conversations)
1024
- .set({ updatedAt: now })
890
+ .set({ updatedAt: now, lastMessageAt: now })
1025
891
  .where(eq(conversations.id, conversationId))
1026
892
  .run();
1027
893
  });
@@ -1265,18 +1131,16 @@ export function clearAll(): { conversations: number; messages: number } {
1265
1131
  const convCount =
1266
1132
  rawGet<{ c: number }>("SELECT COUNT(*) AS c FROM conversations")?.c ?? 0;
1267
1133
 
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.
1134
+ // Delete in dependency order. Cascades handle memory_segments and
1135
+ // tool_invocations, but we explicitly clear non-cascading memory
1136
+ // tables too.
1271
1137
  //
1272
1138
  // FTS virtual tables are cleared before their base tables. If an FTS
1273
1139
  // table is corrupted, the DELETE will fail — we drop the associated
1274
1140
  // triggers so that the subsequent base-table DELETEs don't also fail
1275
1141
  // (SQLite triggers are atomic with the triggering statement, so a
1276
1142
  // corrupted FTS table would roll back every base-table DELETE).
1277
- rawExec("DELETE FROM memory_item_sources");
1278
1143
  rawExec("DELETE FROM memory_segments");
1279
- rawExec("DELETE FROM memory_items");
1280
1144
  rawExec("DELETE FROM memory_summaries");
1281
1145
  rawExec("DELETE FROM memory_embeddings");
1282
1146
  rawExec("DELETE FROM memory_jobs");
@@ -1398,8 +1262,16 @@ export function deleteLastExchange(conversationId: string): number {
1398
1262
 
1399
1263
  db.transaction((tx) => {
1400
1264
  tx.delete(messages).where(condition).run();
1265
+ const maxResult = tx
1266
+ .select({ maxCreatedAt: sql<number | null>`MAX(${messages.createdAt})` })
1267
+ .from(messages)
1268
+ .where(eq(messages.conversationId, conversationId))
1269
+ .get();
1401
1270
  tx.update(conversations)
1402
- .set({ updatedAt: Date.now() })
1271
+ .set({
1272
+ updatedAt: Date.now(),
1273
+ lastMessageAt: maxResult?.maxCreatedAt ?? null,
1274
+ })
1403
1275
  .where(eq(conversations.id, conversationId))
1404
1276
  .run();
1405
1277
  });
@@ -1416,12 +1288,10 @@ export function deleteLastExchange(conversationId: string): number {
1416
1288
  */
1417
1289
  export interface DeletedMemoryIds {
1418
1290
  segmentIds: string[];
1419
- orphanedItemIds: string[];
1420
1291
  deletedSummaryIds: string[];
1421
1292
  }
1422
1293
 
1423
1294
  export interface WipeConversationResult extends DeletedMemoryIds {
1424
- unsupersededItemIds: string[];
1425
1295
  cancelledJobCount: number;
1426
1296
  }
1427
1297
 
@@ -1440,6 +1310,27 @@ export function updateMessageContent(
1440
1310
  .run();
1441
1311
  }
1442
1312
 
1313
+ /**
1314
+ * Merge `updates` into the metadata JSON of an existing message.
1315
+ * Reads the current metadata, shallow-merges the new fields, and writes back.
1316
+ */
1317
+ export function updateMessageMetadata(
1318
+ messageId: string,
1319
+ updates: Record<string, unknown>,
1320
+ ): void {
1321
+ const db = getDb();
1322
+ const row = db
1323
+ .select({ metadata: messages.metadata })
1324
+ .from(messages)
1325
+ .where(eq(messages.id, messageId))
1326
+ .get();
1327
+ const existing = row?.metadata ? JSON.parse(row.metadata) : {};
1328
+ db.update(messages)
1329
+ .set({ metadata: JSON.stringify({ ...existing, ...updates }) })
1330
+ .where(eq(messages.id, messageId))
1331
+ .run();
1332
+ }
1333
+
1443
1334
  /**
1444
1335
  * Re-link all attachments from a set of source messages to a target message.
1445
1336
  * Used during message consolidation so that attachments linked to deleted
@@ -1475,21 +1366,13 @@ export function relinkAttachments(
1475
1366
  * NULL before the message row is removed, so associated run and event
1476
1367
  * records survive.
1477
1368
  *
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.
1369
+ * Returns segment IDs so the caller can clean up the corresponding
1370
+ * Qdrant vector entries.
1487
1371
  */
1488
1372
  export function deleteMessageById(messageId: string): DeletedMemoryIds {
1489
1373
  const db = getDb();
1490
1374
  const result: DeletedMemoryIds = {
1491
1375
  segmentIds: [],
1492
- orphanedItemIds: [],
1493
1376
  deletedSummaryIds: [],
1494
1377
  };
1495
1378
 
@@ -1503,6 +1386,13 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1503
1386
  .map((r) => r.attachmentId)
1504
1387
  .filter((id): id is string => id !== undefined);
1505
1388
 
1389
+ // Look up the conversation before the transaction so we can recalculate lastMessageAt.
1390
+ const msgRow = db
1391
+ .select({ conversationId: messages.conversationId })
1392
+ .from(messages)
1393
+ .where(eq(messages.id, messageId))
1394
+ .get();
1395
+
1506
1396
  db.transaction((tx) => {
1507
1397
  // Collect memory segment IDs linked to this message before cascade.
1508
1398
  const linkedSegments = tx
@@ -1512,24 +1402,31 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1512
1402
  .all();
1513
1403
  result.segmentIds = linkedSegments.map((r) => r.id);
1514
1404
 
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
1405
  // Detach nullable FK references so the cascade doesn't destroy them.
1524
1406
  tx.update(channelInboundEvents)
1525
1407
  .set({ messageId: null })
1526
1408
  .where(eq(channelInboundEvents.messageId, messageId))
1527
1409
  .run();
1528
1410
 
1529
- // Now safe to delete — NOT NULL cascades remove memory_item_sources,
1530
- // memory_segments, and message_attachments.
1411
+ // Now safe to delete — NOT NULL cascades remove memory_segments
1412
+ // and message_attachments.
1531
1413
  tx.delete(messages).where(eq(messages.id, messageId)).run();
1532
1414
 
1415
+ // Recalculate lastMessageAt after deletion.
1416
+ if (msgRow) {
1417
+ const maxResult = tx
1418
+ .select({
1419
+ maxCreatedAt: sql<number | null>`MAX(${messages.createdAt})`,
1420
+ })
1421
+ .from(messages)
1422
+ .where(eq(messages.conversationId, msgRow.conversationId))
1423
+ .get();
1424
+ tx.update(conversations)
1425
+ .set({ lastMessageAt: maxResult?.maxCreatedAt ?? null })
1426
+ .where(eq(conversations.id, msgRow.conversationId))
1427
+ .run();
1428
+ }
1429
+
1533
1430
  // Clean up segment embeddings from SQLite (Qdrant cleanup is the caller's job).
1534
1431
  if (result.segmentIds.length > 0) {
1535
1432
  tx.delete(memoryEmbeddings)
@@ -1541,37 +1438,6 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1541
1438
  )
1542
1439
  .run();
1543
1440
  }
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
1441
  });
1576
1442
 
1577
1443
  deleteOrphanAttachments(candidateAttachmentIds);
@@ -1670,18 +1536,66 @@ export function batchSetDisplayOrders(
1670
1536
  id: string;
1671
1537
  displayOrder: number | null;
1672
1538
  isPinned: boolean;
1539
+ groupId?: string | null;
1673
1540
  }>,
1674
1541
  ): void {
1675
1542
  ensureDisplayOrderMigration();
1543
+ ensureGroupMigration();
1676
1544
  rawExec("BEGIN");
1677
1545
  try {
1678
1546
  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
- );
1547
+ if (update.groupId !== undefined) {
1548
+ // New client: groupId is authoritative.
1549
+ // Derive is_pinned from groupId.
1550
+ // Sanitize: if groupId references a deleted/unknown group, fall back
1551
+ // to NULL to avoid FK violation that would roll back the entire batch.
1552
+ let safeGroupId = update.groupId;
1553
+ if (
1554
+ safeGroupId !== null &&
1555
+ !rawGet<{ id: string }>(
1556
+ "SELECT id FROM conversation_groups WHERE id = ?",
1557
+ safeGroupId,
1558
+ )
1559
+ ) {
1560
+ safeGroupId = null;
1561
+ }
1562
+ rawRun(
1563
+ "UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ? WHERE id = ?",
1564
+ update.displayOrder,
1565
+ safeGroupId === "system:pinned" ? 1 : 0,
1566
+ safeGroupId,
1567
+ update.id,
1568
+ );
1569
+ } else {
1570
+ // Old client: no groupId in payload
1571
+ // isPinned true -> set group_id = system:pinned
1572
+ // isPinned false -> clear group_id ONLY IF currently system:pinned
1573
+ // otherwise preserve existing group_id
1574
+ if (update.isPinned) {
1575
+ rawRun(
1576
+ "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
1577
+ update.displayOrder,
1578
+ update.id,
1579
+ );
1580
+ } else {
1581
+ // Restore system group from source/conversationType when old clients
1582
+ // unpin, instead of clearing to NULL (which would lose provenance).
1583
+ rawRun(
1584
+ `UPDATE conversations SET display_order = ?, is_pinned = 0,
1585
+ group_id = CASE WHEN group_id = 'system:pinned' THEN
1586
+ CASE
1587
+ WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
1588
+ WHEN source IN ('heartbeat', 'task') THEN 'system:background'
1589
+ WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
1590
+ ELSE NULL
1591
+ END
1592
+ ELSE group_id END
1593
+ WHERE id = ?`,
1594
+ update.displayOrder,
1595
+ update.id,
1596
+ );
1597
+ }
1598
+ }
1685
1599
  }
1686
1600
  rawExec("COMMIT");
1687
1601
  } catch (err) {
@@ -1692,21 +1606,30 @@ export function batchSetDisplayOrders(
1692
1606
 
1693
1607
  export function getDisplayMetaForConversations(
1694
1608
  conversationIds: string[],
1695
- ): Map<string, { displayOrder: number | null; isPinned: boolean }> {
1609
+ ): Map<
1610
+ string,
1611
+ { displayOrder: number | null; isPinned: boolean; groupId: string | null }
1612
+ > {
1696
1613
  ensureDisplayOrderMigration();
1614
+ ensureGroupMigration();
1697
1615
  const result = new Map<
1698
1616
  string,
1699
- { displayOrder: number | null; isPinned: boolean }
1617
+ { displayOrder: number | null; isPinned: boolean; groupId: string | null }
1700
1618
  >();
1701
1619
  if (conversationIds.length === 0) return result;
1702
1620
  for (const id of conversationIds) {
1703
1621
  const row = rawGet<{
1704
1622
  display_order: number | null;
1705
1623
  is_pinned: number | null;
1706
- }>("SELECT display_order, is_pinned FROM conversations WHERE id = ?", id);
1624
+ group_id: string | null;
1625
+ }>(
1626
+ "SELECT display_order, is_pinned, group_id FROM conversations WHERE id = ?",
1627
+ id,
1628
+ );
1707
1629
  result.set(id, {
1708
1630
  displayOrder: row?.display_order ?? null,
1709
1631
  isPinned: (row?.is_pinned ?? 0) === 1,
1632
+ groupId: row?.group_id ?? null,
1710
1633
  });
1711
1634
  }
1712
1635
  return result;