@vellumai/assistant 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -70,7 +70,7 @@ const interfaceIdSchema = z.enum(INTERFACE_IDS);
70
70
  const subagentNotificationSchema = z.object({
71
71
  subagentId: z.string(),
72
72
  label: z.string(),
73
- status: z.enum(["completed", "failed", "aborted"]),
73
+ status: z.enum(["running", "completed", "failed", "aborted"]),
74
74
  error: z.string().optional(),
75
75
  conversationId: z.string().optional(),
76
76
  });
@@ -108,7 +108,7 @@ export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
108
108
 
109
109
  function cloneForkMessageMetadata(
110
110
  metadata: string | null,
111
- sourceMessageId: string
111
+ sourceMessageId: string,
112
112
  ): string {
113
113
  if (!metadata) {
114
114
  return JSON.stringify({ forkSourceMessageId: sourceMessageId });
@@ -141,7 +141,7 @@ function cloneForkMessageMetadata(
141
141
  * callers with actual guardian trust should always supply a real context.
142
142
  */
143
143
  export function provenanceFromTrustContext(
144
- ctx: TrustContext | null | undefined
144
+ ctx: TrustContext | null | undefined,
145
145
  ): Record<string, unknown> {
146
146
  if (!ctx) return { provenanceTrustClass: "unknown" };
147
147
  return {
@@ -172,6 +172,7 @@ export interface ConversationRow {
172
172
  forkParentMessageId: string | null;
173
173
  isAutoTitle: number;
174
174
  scheduleJobId: string | null;
175
+ lastMessageAt: number | null;
175
176
  }
176
177
 
177
178
  export const parseConversation = createRowMapper<
@@ -197,6 +198,7 @@ export const parseConversation = createRowMapper<
197
198
  forkParentMessageId: "forkParentMessageId",
198
199
  isAutoTitle: "isAutoTitle",
199
200
  scheduleJobId: "scheduleJobId",
201
+ lastMessageAt: "lastMessageAt",
200
202
  });
201
203
 
202
204
  export interface MessageRow {
@@ -239,18 +241,18 @@ export function createConversation(
239
241
  | string
240
242
  | {
241
243
  title?: string;
242
- conversationType?: "standard" | "private" | "background";
244
+ conversationType?: "standard" | "private" | "background" | "scheduled";
243
245
  source?: string;
244
246
  scheduleJobId?: string;
245
247
  groupId?: string;
246
- }
248
+ },
247
249
  ) {
248
250
  const db = getDb();
249
251
  const now = Date.now();
250
252
  const opts =
251
253
  typeof titleOrOpts === "string"
252
254
  ? { title: titleOrOpts }
253
- : titleOrOpts ?? {};
255
+ : (titleOrOpts ?? {});
254
256
  const conversationType = opts.conversationType ?? "standard";
255
257
  const source = opts.source ?? "user";
256
258
  const groupId = opts.groupId;
@@ -301,7 +303,7 @@ export function createConversation(
301
303
  ) {
302
304
  log.warn(
303
305
  { attempt, conversationId: id, code },
304
- "createConversation: INSERT transient error, retrying"
306
+ "createConversation: INSERT transient error, retrying",
305
307
  );
306
308
  Bun.sleepSync(50 * (attempt + 1));
307
309
  continue;
@@ -312,13 +314,16 @@ export function createConversation(
312
314
 
313
315
  // group_id is NOT in the Drizzle schema (raw-query-only pattern).
314
316
  // Set via raw SQL after the INSERT succeeds.
315
- if (groupId) {
317
+ // Always set group_id — default to "system:all" when none provided.
318
+ {
319
+ const effectiveGroupId = groupId ?? "system:all";
316
320
  for (let attempt = 0; ; attempt++) {
317
321
  try {
318
322
  rawRun(
319
- "UPDATE conversations SET group_id = ? WHERE id = ?",
320
- groupId,
321
- id
323
+ "UPDATE conversations SET group_id = ?, is_pinned = ? WHERE id = ?",
324
+ effectiveGroupId,
325
+ effectiveGroupId === "system:pinned" ? 1 : 0,
326
+ id,
322
327
  );
323
328
  break;
324
329
  } catch (err) {
@@ -329,7 +334,7 @@ export function createConversation(
329
334
  ) {
330
335
  log.warn(
331
336
  { attempt, conversationId: id, code },
332
- "createConversation: group_id UPDATE transient error, retrying"
337
+ "createConversation: group_id UPDATE transient error, retrying",
333
338
  );
334
339
  Bun.sleepSync(50 * (attempt + 1));
335
340
  continue;
@@ -360,18 +365,18 @@ export function getConversation(id: string): ConversationRow | null {
360
365
  * (i.e. no other conversations still reference it).
361
366
  */
362
367
  export function countConversationsByScheduleJobId(
363
- scheduleJobId: string
368
+ scheduleJobId: string,
364
369
  ): number {
365
370
  return (
366
371
  rawGet<{ c: number }>(
367
372
  "SELECT COUNT(*) AS c FROM conversations WHERE schedule_job_id = ?",
368
- scheduleJobId
373
+ scheduleJobId,
369
374
  )?.c ?? 0
370
375
  );
371
376
  }
372
377
 
373
378
  export function getConversationType(
374
- conversationId: string
379
+ conversationId: string,
375
380
  ): "standard" | "private" {
376
381
  const conv = getConversation(conversationId);
377
382
  const raw = conv?.conversationType;
@@ -392,7 +397,7 @@ export function getConversationGroupId(conversationId: string): string | null {
392
397
  ensureGroupMigration();
393
398
  const row = rawGet<{ group_id: string | null }>(
394
399
  "SELECT group_id FROM conversations WHERE id = ?",
395
- conversationId
400
+ conversationId,
396
401
  );
397
402
  return row?.group_id ?? null;
398
403
  }
@@ -416,7 +421,7 @@ export function forkConversation(params: {
416
421
 
417
422
  if (sourceMessages.length === 0) {
418
423
  throw new UserError(
419
- `Conversation ${conversationId} has no persisted messages to fork`
424
+ `Conversation ${conversationId} has no persisted messages to fork`,
420
425
  );
421
426
  }
422
427
 
@@ -427,7 +432,7 @@ export function forkConversation(params: {
427
432
 
428
433
  if (throughMessageId != null && copyBoundaryIndex === -1) {
429
434
  throw new UserError(
430
- `Message ${throughMessageId} does not belong to conversation ${conversationId}`
435
+ `Message ${throughMessageId} does not belong to conversation ${conversationId}`,
431
436
  );
432
437
  }
433
438
 
@@ -435,8 +440,8 @@ export function forkConversation(params: {
435
440
  0,
436
441
  Math.min(
437
442
  sourceConversation.contextCompactedMessageCount,
438
- sourceMessages.length
439
- )
443
+ sourceMessages.length,
444
+ ),
440
445
  );
441
446
  const preserveSourceCompactionState =
442
447
  copyBoundaryIndex >= visibleWindowStartIndex;
@@ -467,7 +472,7 @@ export function forkConversation(params: {
467
472
  const fc = createConversation({
468
473
  title: forkTitle,
469
474
  conversationType: "standard",
470
- groupId: parentGroupId ?? undefined,
475
+ groupId: parentGroupId ?? "system:all",
471
476
  });
472
477
 
473
478
  db.update(conversations)
@@ -530,7 +535,7 @@ export function forkConversation(params: {
530
535
  .orderBy(messageAttachments.position)
531
536
  .all();
532
537
  const uncachedAttachmentLinks = attachmentLinks.filter(
533
- (link) => !attachmentIdMap.has(link.attachmentId)
538
+ (link) => !attachmentIdMap.has(link.attachmentId),
534
539
  );
535
540
  const stagingMessageId =
536
541
  uncachedAttachmentLinks.length > 0 ? uuid() : null;
@@ -566,7 +571,7 @@ export function forkConversation(params: {
566
571
  const scopedAttachmentId = linkAttachmentToMessage(
567
572
  stagingMessageId ?? forkedMessageId,
568
573
  link.attachmentId,
569
- link.position
574
+ link.position,
570
575
  );
571
576
  attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
572
577
  }
@@ -583,6 +588,16 @@ export function forkConversation(params: {
583
588
  });
584
589
  }
585
590
 
591
+ // Set lastMessageAt to the max createdAt of copied messages so the
592
+ // forked conversation sorts correctly by message recency.
593
+ const lastCopiedMessage = messagesToCopy.at(-1);
594
+ if (lastCopiedMessage) {
595
+ db.update(conversations)
596
+ .set({ lastMessageAt: lastCopiedMessage.createdAt })
597
+ .where(eq(conversations.id, fc.id))
598
+ .run();
599
+ }
600
+
586
601
  seedForkedConversationAttention({
587
602
  conversationId: fc.id,
588
603
  latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
@@ -601,7 +616,7 @@ export function forkConversation(params: {
601
616
  const persistedFork = getConversation(forkedConversation.id);
602
617
  if (!persistedFork) {
603
618
  throw new Error(
604
- `Failed to load forked conversation ${forkedConversation.id} after creation`
619
+ `Failed to load forked conversation ${forkedConversation.id} after creation`,
605
620
  );
606
621
  }
607
622
 
@@ -610,14 +625,13 @@ export function forkConversation(params: {
610
625
 
611
626
  /**
612
627
  * Delete a conversation and all its messages, cleaning up orphaned memory
613
- * artifacts (items, embeddings). Returns segment and orphaned item IDs so
614
- * callers can clean up the corresponding Qdrant vector entries.
628
+ * artifacts (embeddings). Returns segment IDs so callers can clean up
629
+ * the corresponding Qdrant vector entries.
615
630
  */
616
631
  export function deleteConversation(id: string): DeletedMemoryIds {
617
632
  const db = getDb();
618
633
  const result: DeletedMemoryIds = {
619
634
  segmentIds: [],
620
- orphanedItemIds: [],
621
635
  deletedSummaryIds: [],
622
636
  };
623
637
 
@@ -662,8 +676,8 @@ export function deleteConversation(id: string): DeletedMemoryIds {
662
676
  .where(
663
677
  and(
664
678
  eq(memoryEmbeddings.targetType, "segment"),
665
- inArray(memoryEmbeddings.targetId, result.segmentIds)
666
- )
679
+ inArray(memoryEmbeddings.targetId, result.segmentIds),
680
+ ),
667
681
  )
668
682
  .run();
669
683
  }
@@ -691,8 +705,8 @@ export function deleteConversation(id: string): DeletedMemoryIds {
691
705
  .where(
692
706
  and(
693
707
  eq(memoryEmbeddings.targetType, "summary"),
694
- inArray(memoryEmbeddings.targetId, scopeSummaryIds)
695
- )
708
+ inArray(memoryEmbeddings.targetId, scopeSummaryIds),
709
+ ),
696
710
  )
697
711
  .run();
698
712
  tx.delete(memorySummaries)
@@ -723,14 +737,10 @@ export function deleteConversation(id: string): DeletedMemoryIds {
723
737
  *
724
738
  * Extends `deleteConversation` with:
725
739
  * - Cancelling pending memory jobs before deletion
726
- * - Restoring memory items that were explicitly superseded by items from this conversation
727
- * - Restoring orphaned subject-match superseded items after deletion
728
740
  * - Deleting conversation-scoped memory summaries and their embeddings
729
- * - Enqueuing `embed_item` jobs for all restored items
730
741
  */
731
742
  export function wipeConversation(id: string): WipeConversationResult {
732
743
  const db = getDb();
733
- const unsupersededItemIds: string[] = [];
734
744
  const deletedSummaryIds: string[] = [];
735
745
 
736
746
  // Step A — Cancel pending memory jobs (before deleting messages, since
@@ -744,8 +754,8 @@ export function wipeConversation(id: string): WipeConversationResult {
744
754
  .where(
745
755
  and(
746
756
  eq(memorySummaries.scope, "conversation"),
747
- eq(memorySummaries.scopeKey, id)
748
- )
757
+ eq(memorySummaries.scopeKey, id),
758
+ ),
749
759
  )
750
760
  .all();
751
761
  const summaryIds = summaryRows.map((r) => r.id);
@@ -754,8 +764,8 @@ export function wipeConversation(id: string): WipeConversationResult {
754
764
  .where(
755
765
  and(
756
766
  eq(memoryEmbeddings.targetType, "summary"),
757
- inArray(memoryEmbeddings.targetId, summaryIds)
758
- )
767
+ inArray(memoryEmbeddings.targetId, summaryIds),
768
+ ),
759
769
  )
760
770
  .run();
761
771
  db.delete(memorySummaries)
@@ -772,7 +782,6 @@ export function wipeConversation(id: string): WipeConversationResult {
772
782
  // Step E — Return the combined result.
773
783
  return {
774
784
  ...deletedMemoryIds,
775
- unsupersededItemIds,
776
785
  deletedSummaryIds: [
777
786
  ...deletedSummaryIds,
778
787
  ...deletedMemoryIds.deletedSummaryIds,
@@ -802,20 +811,17 @@ export function purgePrivateConversations(): {
802
811
  count: 0,
803
812
  deletedMemory: {
804
813
  segmentIds: [],
805
- orphanedItemIds: [],
806
814
  deletedSummaryIds: [],
807
815
  },
808
816
  };
809
817
  }
810
818
 
811
819
  const allSegmentIds: string[] = [];
812
- const allOrphanedItemIds: string[] = [];
813
820
  const allDeletedSummaryIds: string[] = [];
814
821
 
815
822
  for (const conv of privateConvs) {
816
823
  const deleted = deleteConversation(conv.id);
817
824
  allSegmentIds.push(...deleted.segmentIds);
818
- allOrphanedItemIds.push(...deleted.orphanedItemIds);
819
825
  allDeletedSummaryIds.push(...deleted.deletedSummaryIds);
820
826
  }
821
827
 
@@ -823,7 +829,6 @@ export function purgePrivateConversations(): {
823
829
  count: privateConvs.length,
824
830
  deletedMemory: {
825
831
  segmentIds: allSegmentIds,
826
- orphanedItemIds: allOrphanedItemIds,
827
832
  deletedSummaryIds: allDeletedSummaryIds,
828
833
  },
829
834
  };
@@ -834,7 +839,7 @@ export async function addMessage(
834
839
  role: string,
835
840
  content: string,
836
841
  metadata?: Record<string, unknown>,
837
- opts?: { skipIndexing?: boolean }
842
+ opts?: { skipIndexing?: boolean },
838
843
  ) {
839
844
  const db = getDb();
840
845
  const messageId = uuid();
@@ -844,7 +849,7 @@ export async function addMessage(
844
849
  if (!result.success) {
845
850
  log.warn(
846
851
  { conversationId, messageId, issues: result.error.issues },
847
- "Invalid message metadata, storing as-is"
852
+ "Invalid message metadata, storing as-is",
848
853
  );
849
854
  }
850
855
  }
@@ -879,13 +884,13 @@ export async function addMessage(
879
884
  .where(
880
885
  and(
881
886
  eq(conversations.id, conversationId),
882
- isNull(conversations.originChannel)
883
- )
887
+ isNull(conversations.originChannel),
888
+ ),
884
889
  )
885
890
  .run();
886
891
  }
887
892
  tx.update(conversations)
888
- .set({ updatedAt: now })
893
+ .set({ updatedAt: now, lastMessageAt: now })
889
894
  .where(eq(conversations.id, conversationId))
890
895
  .run();
891
896
  });
@@ -899,7 +904,7 @@ export async function addMessage(
899
904
  ) {
900
905
  log.warn(
901
906
  { attempt, conversationId, code: errCode },
902
- "addMessage: transient SQLite error, retrying"
907
+ "addMessage: transient SQLite error, retrying",
903
908
  );
904
909
  await Bun.sleep(50 * (attempt + 1));
905
910
  continue;
@@ -938,12 +943,12 @@ export async function addMessage(
938
943
  provenanceTrustClass,
939
944
  automated,
940
945
  },
941
- config.memory
946
+ config.memory,
942
947
  );
943
948
  } catch (err) {
944
949
  log.warn(
945
950
  { err, conversationId, messageId: message.id },
946
- "Failed to index message for memory"
951
+ "Failed to index message for memory",
947
952
  );
948
953
  }
949
954
  }
@@ -958,7 +963,7 @@ export async function addMessage(
958
963
  } catch (err) {
959
964
  log.warn(
960
965
  { err, conversationId, messageId: message.id },
961
- "Failed to project assistant message for attention tracking"
966
+ "Failed to project assistant message for attention tracking",
962
967
  );
963
968
  }
964
969
  }
@@ -985,7 +990,7 @@ export interface PaginatedMessagesResult {
985
990
  export function getMessagesPaginated(
986
991
  conversationId: string,
987
992
  limit: number | undefined,
988
- beforeTimestamp?: number
993
+ beforeTimestamp?: number,
989
994
  ): PaginatedMessagesResult {
990
995
  const db = getDb();
991
996
 
@@ -1029,7 +1034,7 @@ export function getMessagesPaginated(
1029
1034
 
1030
1035
  export function getLastAssistantTimestampBefore(
1031
1036
  conversationId: string,
1032
- beforeTimestamp: number
1037
+ beforeTimestamp: number,
1033
1038
  ): number {
1034
1039
  const db = getDb();
1035
1040
  const row = db
@@ -1039,8 +1044,8 @@ export function getLastAssistantTimestampBefore(
1039
1044
  and(
1040
1045
  eq(messages.conversationId, conversationId),
1041
1046
  eq(messages.role, "assistant"),
1042
- lt(messages.createdAt, beforeTimestamp)
1043
- )
1047
+ lt(messages.createdAt, beforeTimestamp),
1048
+ ),
1044
1049
  )
1045
1050
  .orderBy(desc(messages.createdAt))
1046
1051
  .limit(1)
@@ -1051,7 +1056,7 @@ export function getLastAssistantTimestampBefore(
1051
1056
  /** Fetch a single message by ID, optionally scoped to a specific conversation. */
1052
1057
  export function getMessageById(
1053
1058
  messageId: string,
1054
- conversationId?: string
1059
+ conversationId?: string,
1055
1060
  ): MessageRow | null {
1056
1061
  const db = getDb();
1057
1062
  const conditions = [eq(messages.id, messageId)];
@@ -1069,7 +1074,7 @@ export function getMessageById(
1069
1074
  export function updateConversationTitle(
1070
1075
  id: string,
1071
1076
  title: string,
1072
- isAutoTitle?: number
1077
+ isAutoTitle?: number,
1073
1078
  ): void {
1074
1079
  const db = getDb();
1075
1080
  const set: Record<string, unknown> = { title, updatedAt: Date.now() };
@@ -1087,7 +1092,7 @@ export function updateConversationUsage(
1087
1092
  id: string,
1088
1093
  totalInputTokens: number,
1089
1094
  totalOutputTokens: number,
1090
- totalEstimatedCost: number
1095
+ totalEstimatedCost: number,
1091
1096
  ): void {
1092
1097
  const db = getDb();
1093
1098
  db.update(conversations)
@@ -1104,7 +1109,7 @@ export function updateConversationUsage(
1104
1109
  export function updateConversationContextWindow(
1105
1110
  id: string,
1106
1111
  contextSummary: string,
1107
- contextCompactedMessageCount: number
1112
+ contextCompactedMessageCount: number,
1108
1113
  ): void {
1109
1114
  const db = getDb();
1110
1115
  db.update(conversations)
@@ -1154,7 +1159,7 @@ export function clearAll(): { conversations: number; messages: number } {
1154
1159
  } catch (err) {
1155
1160
  log.warn(
1156
1161
  { err },
1157
- "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed"
1162
+ "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed",
1158
1163
  );
1159
1164
  rawExec("DROP TRIGGER IF EXISTS messages_fts_ai");
1160
1165
  rawExec("DROP TRIGGER IF EXISTS messages_fts_ad");
@@ -1170,7 +1175,7 @@ export function clearAll(): { conversations: number; messages: number } {
1170
1175
  `INSERT INTO lifecycle_events (id, event_name, created_at) VALUES (?, ?, ?)`,
1171
1176
  uuid(),
1172
1177
  "conversations_clear_all",
1173
- Date.now()
1178
+ Date.now(),
1174
1179
  );
1175
1180
 
1176
1181
  // Rebuild corrupted FTS tables and restore triggers after all base-table
@@ -1180,16 +1185,16 @@ export function clearAll(): { conversations: number; messages: number } {
1180
1185
  if (messagesFtsCorrupted) {
1181
1186
  rawExec("DROP TABLE IF EXISTS messages_fts");
1182
1187
  rawExec(
1183
- `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`
1188
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`,
1184
1189
  );
1185
1190
  rawExec(
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`
1191
+ `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`,
1187
1192
  );
1188
1193
  rawExec(
1189
- `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`
1194
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`,
1190
1195
  );
1191
1196
  rawExec(
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`
1197
+ `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`,
1193
1198
  );
1194
1199
  }
1195
1200
 
@@ -1214,8 +1219,8 @@ export function deleteLastExchange(conversationId: string): number {
1214
1219
  .where(
1215
1220
  and(
1216
1221
  eq(messages.conversationId, conversationId),
1217
- eq(messages.role, "user")
1218
- )
1222
+ eq(messages.role, "user"),
1223
+ ),
1219
1224
  )
1220
1225
  .orderBy(sql`rowid DESC`)
1221
1226
  .limit(1)
@@ -1229,7 +1234,7 @@ export function deleteLastExchange(conversationId: string): number {
1229
1234
  const rowidSubquery = sql`(SELECT rowid FROM messages WHERE id = ${lastUserMsg.id})`;
1230
1235
  const condition = and(
1231
1236
  eq(messages.conversationId, conversationId),
1232
- sql`rowid >= ${rowidSubquery}`
1237
+ sql`rowid >= ${rowidSubquery}`,
1233
1238
  );
1234
1239
 
1235
1240
  const [{ deleted }] = db
@@ -1260,8 +1265,16 @@ export function deleteLastExchange(conversationId: string): number {
1260
1265
 
1261
1266
  db.transaction((tx) => {
1262
1267
  tx.delete(messages).where(condition).run();
1268
+ const maxResult = tx
1269
+ .select({ maxCreatedAt: sql<number | null>`MAX(${messages.createdAt})` })
1270
+ .from(messages)
1271
+ .where(eq(messages.conversationId, conversationId))
1272
+ .get();
1263
1273
  tx.update(conversations)
1264
- .set({ updatedAt: Date.now() })
1274
+ .set({
1275
+ updatedAt: Date.now(),
1276
+ lastMessageAt: maxResult?.maxCreatedAt ?? null,
1277
+ })
1265
1278
  .where(eq(conversations.id, conversationId))
1266
1279
  .run();
1267
1280
  });
@@ -1278,12 +1291,10 @@ export function deleteLastExchange(conversationId: string): number {
1278
1291
  */
1279
1292
  export interface DeletedMemoryIds {
1280
1293
  segmentIds: string[];
1281
- orphanedItemIds: string[];
1282
1294
  deletedSummaryIds: string[];
1283
1295
  }
1284
1296
 
1285
1297
  export interface WipeConversationResult extends DeletedMemoryIds {
1286
- unsupersededItemIds: string[];
1287
1298
  cancelledJobCount: number;
1288
1299
  }
1289
1300
 
@@ -1293,7 +1304,7 @@ export interface WipeConversationResult extends DeletedMemoryIds {
1293
1304
  */
1294
1305
  export function updateMessageContent(
1295
1306
  messageId: string,
1296
- newContent: string
1307
+ newContent: string,
1297
1308
  ): void {
1298
1309
  const db = getDb();
1299
1310
  db.update(messages)
@@ -1330,7 +1341,7 @@ export function updateMessageMetadata(
1330
1341
  */
1331
1342
  export function relinkAttachments(
1332
1343
  fromMessageIds: string[],
1333
- toMessageId: string
1344
+ toMessageId: string,
1334
1345
  ): number {
1335
1346
  if (fromMessageIds.length === 0) return 0;
1336
1347
  const db = getDb();
@@ -1365,7 +1376,6 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1365
1376
  const db = getDb();
1366
1377
  const result: DeletedMemoryIds = {
1367
1378
  segmentIds: [],
1368
- orphanedItemIds: [],
1369
1379
  deletedSummaryIds: [],
1370
1380
  };
1371
1381
 
@@ -1379,6 +1389,13 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1379
1389
  .map((r) => r.attachmentId)
1380
1390
  .filter((id): id is string => id !== undefined);
1381
1391
 
1392
+ // Look up the conversation before the transaction so we can recalculate lastMessageAt.
1393
+ const msgRow = db
1394
+ .select({ conversationId: messages.conversationId })
1395
+ .from(messages)
1396
+ .where(eq(messages.id, messageId))
1397
+ .get();
1398
+
1382
1399
  db.transaction((tx) => {
1383
1400
  // Collect memory segment IDs linked to this message before cascade.
1384
1401
  const linkedSegments = tx
@@ -1398,14 +1415,29 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1398
1415
  // and message_attachments.
1399
1416
  tx.delete(messages).where(eq(messages.id, messageId)).run();
1400
1417
 
1418
+ // Recalculate lastMessageAt after deletion.
1419
+ if (msgRow) {
1420
+ const maxResult = tx
1421
+ .select({
1422
+ maxCreatedAt: sql<number | null>`MAX(${messages.createdAt})`,
1423
+ })
1424
+ .from(messages)
1425
+ .where(eq(messages.conversationId, msgRow.conversationId))
1426
+ .get();
1427
+ tx.update(conversations)
1428
+ .set({ lastMessageAt: maxResult?.maxCreatedAt ?? null })
1429
+ .where(eq(conversations.id, msgRow.conversationId))
1430
+ .run();
1431
+ }
1432
+
1401
1433
  // Clean up segment embeddings from SQLite (Qdrant cleanup is the caller's job).
1402
1434
  if (result.segmentIds.length > 0) {
1403
1435
  tx.delete(memoryEmbeddings)
1404
1436
  .where(
1405
1437
  and(
1406
1438
  eq(memoryEmbeddings.targetType, "segment"),
1407
- inArray(memoryEmbeddings.targetId, result.segmentIds)
1408
- )
1439
+ inArray(memoryEmbeddings.targetId, result.segmentIds),
1440
+ ),
1409
1441
  )
1410
1442
  .run();
1411
1443
  }
@@ -1418,7 +1450,7 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1418
1450
 
1419
1451
  export function setConversationOriginChannelIfUnset(
1420
1452
  conversationId: string,
1421
- channel: ChannelId
1453
+ channel: ChannelId,
1422
1454
  ): void {
1423
1455
  const db = getDb();
1424
1456
  db.update(conversations)
@@ -1426,14 +1458,14 @@ export function setConversationOriginChannelIfUnset(
1426
1458
  .where(
1427
1459
  and(
1428
1460
  eq(conversations.id, conversationId),
1429
- isNull(conversations.originChannel)
1430
- )
1461
+ isNull(conversations.originChannel),
1462
+ ),
1431
1463
  )
1432
1464
  .run();
1433
1465
  }
1434
1466
 
1435
1467
  export function getConversationOriginChannel(
1436
- conversationId: string
1468
+ conversationId: string,
1437
1469
  ): ChannelId | null {
1438
1470
  const db = getDb();
1439
1471
  const row = db
@@ -1446,7 +1478,7 @@ export function getConversationOriginChannel(
1446
1478
 
1447
1479
  export function setConversationOriginInterfaceIfUnset(
1448
1480
  conversationId: string,
1449
- interfaceId: InterfaceId
1481
+ interfaceId: InterfaceId,
1450
1482
  ): void {
1451
1483
  const db = getDb();
1452
1484
  db.update(conversations)
@@ -1454,14 +1486,14 @@ export function setConversationOriginInterfaceIfUnset(
1454
1486
  .where(
1455
1487
  and(
1456
1488
  eq(conversations.id, conversationId),
1457
- isNull(conversations.originInterface)
1458
- )
1489
+ isNull(conversations.originInterface),
1490
+ ),
1459
1491
  )
1460
1492
  .run();
1461
1493
  }
1462
1494
 
1463
1495
  export function getConversationOriginInterface(
1464
- conversationId: string
1496
+ conversationId: string,
1465
1497
  ): InterfaceId | null {
1466
1498
  const db = getDb();
1467
1499
  const row = db
@@ -1481,13 +1513,13 @@ export function getConversationOriginInterface(
1481
1513
  * conversation itself isn't a desktop-origin private conversation).
1482
1514
  */
1483
1515
  export function getConversationRecentProvenanceTrustClass(
1484
- conversationId: string
1516
+ conversationId: string,
1485
1517
  ): "guardian" | "trusted_contact" | "unknown" | undefined {
1486
1518
  const row = rawGet<{ metadata: string | null }>(
1487
1519
  `SELECT metadata FROM messages
1488
1520
  WHERE conversation_id = ? AND role = 'user' AND metadata IS NOT NULL
1489
1521
  ORDER BY created_at DESC LIMIT 1`,
1490
- conversationId
1522
+ conversationId,
1491
1523
  );
1492
1524
  if (!row?.metadata) return undefined;
1493
1525
  try {
@@ -1508,7 +1540,7 @@ export function batchSetDisplayOrders(
1508
1540
  displayOrder: number | null;
1509
1541
  isPinned: boolean;
1510
1542
  groupId?: string | null;
1511
- }>
1543
+ }>,
1512
1544
  ): void {
1513
1545
  ensureDisplayOrderMigration();
1514
1546
  ensureGroupMigration();
@@ -1518,24 +1550,26 @@ export function batchSetDisplayOrders(
1518
1550
  if (update.groupId !== undefined) {
1519
1551
  // New client: groupId is authoritative.
1520
1552
  // 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.
1553
+ // Sanitize: if groupId is null or references a deleted/unknown group,
1554
+ // fall back to "system:all" to avoid FK violation that would roll back
1555
+ // the entire batch.
1523
1556
  let safeGroupId = update.groupId;
1524
- if (
1525
- safeGroupId !== null &&
1557
+ if (safeGroupId === null) {
1558
+ safeGroupId = "system:all";
1559
+ } else if (
1526
1560
  !rawGet<{ id: string }>(
1527
1561
  "SELECT id FROM conversation_groups WHERE id = ?",
1528
- safeGroupId
1562
+ safeGroupId,
1529
1563
  )
1530
1564
  ) {
1531
- safeGroupId = null;
1565
+ safeGroupId = "system:all";
1532
1566
  }
1533
1567
  rawRun(
1534
1568
  "UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ? WHERE id = ?",
1535
1569
  update.displayOrder,
1536
1570
  safeGroupId === "system:pinned" ? 1 : 0,
1537
1571
  safeGroupId,
1538
- update.id
1572
+ update.id,
1539
1573
  );
1540
1574
  } else {
1541
1575
  // Old client: no groupId in payload
@@ -1546,7 +1580,7 @@ export function batchSetDisplayOrders(
1546
1580
  rawRun(
1547
1581
  "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
1548
1582
  update.displayOrder,
1549
- update.id
1583
+ update.id,
1550
1584
  );
1551
1585
  } else {
1552
1586
  // Restore system group from source/conversationType when old clients
@@ -1558,12 +1592,12 @@ export function batchSetDisplayOrders(
1558
1592
  WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
1559
1593
  WHEN source IN ('heartbeat', 'task') THEN 'system:background'
1560
1594
  WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
1561
- ELSE NULL
1595
+ ELSE 'system:all'
1562
1596
  END
1563
1597
  ELSE group_id END
1564
1598
  WHERE id = ?`,
1565
1599
  update.displayOrder,
1566
- update.id
1600
+ update.id,
1567
1601
  );
1568
1602
  }
1569
1603
  }
@@ -1576,7 +1610,7 @@ export function batchSetDisplayOrders(
1576
1610
  }
1577
1611
 
1578
1612
  export function getDisplayMetaForConversations(
1579
- conversationIds: string[]
1613
+ conversationIds: string[],
1580
1614
  ): Map<
1581
1615
  string,
1582
1616
  { displayOrder: number | null; isPinned: boolean; groupId: string | null }
@@ -1595,7 +1629,7 @@ export function getDisplayMetaForConversations(
1595
1629
  group_id: string | null;
1596
1630
  }>(
1597
1631
  "SELECT display_order, is_pinned, group_id FROM conversations WHERE id = ?",
1598
- id
1632
+ id,
1599
1633
  );
1600
1634
  result.set(id, {
1601
1635
  displayOrder: row?.display_order ?? null,
@@ -1624,7 +1658,7 @@ function isToolResultMessage(role: string, content: string): boolean {
1624
1658
  (block: unknown) =>
1625
1659
  block != null &&
1626
1660
  typeof block === "object" &&
1627
- (block as Record<string, unknown>).type === "tool_result"
1661
+ (block as Record<string, unknown>).type === "tool_result",
1628
1662
  );
1629
1663
  } catch {
1630
1664
  return false;
@@ -1644,7 +1678,7 @@ function isToolResultMessage(role: string, content: string): boolean {
1644
1678
  */
1645
1679
  export function getTurnTimeBounds(
1646
1680
  conversationId: string,
1647
- messageCreatedAt: number
1681
+ messageCreatedAt: number,
1648
1682
  ): { startTime: number; endTime: number } | null {
1649
1683
  const db = getDb();
1650
1684
 
@@ -1666,8 +1700,8 @@ export function getTurnTimeBounds(
1666
1700
  .where(
1667
1701
  and(
1668
1702
  eq(messages.conversationId, conversationId),
1669
- sql`rowid <= ${rowidSubquery}`
1670
- )
1703
+ sql`rowid <= ${rowidSubquery}`,
1704
+ ),
1671
1705
  )
1672
1706
  .orderBy(sql`rowid DESC`)
1673
1707
  .limit(50)
@@ -1698,8 +1732,8 @@ export function getTurnTimeBounds(
1698
1732
  .where(
1699
1733
  and(
1700
1734
  eq(messages.conversationId, conversationId),
1701
- sql`rowid > ${forwardRowidSubquery}`
1702
- )
1735
+ sql`rowid > ${forwardRowidSubquery}`,
1736
+ ),
1703
1737
  )
1704
1738
  .orderBy(sql`rowid ASC`)
1705
1739
  .limit(50)
@@ -1740,8 +1774,8 @@ export function getTurnTimeBounds(
1740
1774
  and(
1741
1775
  eq(llmRequestLogs.conversationId, conversationId),
1742
1776
  gte(llmRequestLogs.createdAt, startTime),
1743
- lte(llmRequestLogs.createdAt, hardCeiling)
1744
- )
1777
+ lte(llmRequestLogs.createdAt, hardCeiling),
1778
+ ),
1745
1779
  )
1746
1780
  .orderBy(desc(llmRequestLogs.createdAt))
1747
1781
  .limit(1)
@@ -1789,8 +1823,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1789
1823
  .where(
1790
1824
  and(
1791
1825
  eq(messages.conversationId, target.conversationId),
1792
- lte(messages.createdAt, target.createdAt)
1793
- )
1826
+ lte(messages.createdAt, target.createdAt),
1827
+ ),
1794
1828
  )
1795
1829
  .orderBy(desc(messages.createdAt))
1796
1830
  .limit(50)
@@ -1827,8 +1861,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1827
1861
  .where(
1828
1862
  and(
1829
1863
  eq(messages.conversationId, target.conversationId),
1830
- gt(messages.createdAt, target.createdAt)
1831
- )
1864
+ gt(messages.createdAt, target.createdAt),
1865
+ ),
1832
1866
  )
1833
1867
  .orderBy(asc(messages.createdAt))
1834
1868
  .limit(50)
@@ -1864,8 +1898,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1864
1898
  and(
1865
1899
  eq(messages.conversationId, target.conversationId),
1866
1900
  gt(messages.createdAt, boundaryCreatedAt),
1867
- lte(messages.createdAt, target.createdAt)
1868
- )
1901
+ lte(messages.createdAt, target.createdAt),
1902
+ ),
1869
1903
  )
1870
1904
  .orderBy(asc(messages.createdAt))
1871
1905
  .all();
@@ -1888,8 +1922,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1888
1922
  .where(
1889
1923
  and(
1890
1924
  eq(messages.conversationId, target.conversationId),
1891
- inArray(messages.id, [...idSet])
1892
- )
1925
+ inArray(messages.id, [...idSet]),
1926
+ ),
1893
1927
  )
1894
1928
  .orderBy(asc(messages.createdAt))
1895
1929
  .all();