@vellumai/assistant 0.6.0 → 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 (285) 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 +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. 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;
@@ -318,7 +320,7 @@ export function createConversation(
318
320
  rawRun(
319
321
  "UPDATE conversations SET group_id = ? WHERE id = ?",
320
322
  groupId,
321
- id
323
+ id,
322
324
  );
323
325
  break;
324
326
  } catch (err) {
@@ -329,7 +331,7 @@ export function createConversation(
329
331
  ) {
330
332
  log.warn(
331
333
  { attempt, conversationId: id, code },
332
- "createConversation: group_id UPDATE transient error, retrying"
334
+ "createConversation: group_id UPDATE transient error, retrying",
333
335
  );
334
336
  Bun.sleepSync(50 * (attempt + 1));
335
337
  continue;
@@ -360,18 +362,18 @@ export function getConversation(id: string): ConversationRow | null {
360
362
  * (i.e. no other conversations still reference it).
361
363
  */
362
364
  export function countConversationsByScheduleJobId(
363
- scheduleJobId: string
365
+ scheduleJobId: string,
364
366
  ): number {
365
367
  return (
366
368
  rawGet<{ c: number }>(
367
369
  "SELECT COUNT(*) AS c FROM conversations WHERE schedule_job_id = ?",
368
- scheduleJobId
370
+ scheduleJobId,
369
371
  )?.c ?? 0
370
372
  );
371
373
  }
372
374
 
373
375
  export function getConversationType(
374
- conversationId: string
376
+ conversationId: string,
375
377
  ): "standard" | "private" {
376
378
  const conv = getConversation(conversationId);
377
379
  const raw = conv?.conversationType;
@@ -392,7 +394,7 @@ export function getConversationGroupId(conversationId: string): string | null {
392
394
  ensureGroupMigration();
393
395
  const row = rawGet<{ group_id: string | null }>(
394
396
  "SELECT group_id FROM conversations WHERE id = ?",
395
- conversationId
397
+ conversationId,
396
398
  );
397
399
  return row?.group_id ?? null;
398
400
  }
@@ -416,7 +418,7 @@ export function forkConversation(params: {
416
418
 
417
419
  if (sourceMessages.length === 0) {
418
420
  throw new UserError(
419
- `Conversation ${conversationId} has no persisted messages to fork`
421
+ `Conversation ${conversationId} has no persisted messages to fork`,
420
422
  );
421
423
  }
422
424
 
@@ -427,7 +429,7 @@ export function forkConversation(params: {
427
429
 
428
430
  if (throughMessageId != null && copyBoundaryIndex === -1) {
429
431
  throw new UserError(
430
- `Message ${throughMessageId} does not belong to conversation ${conversationId}`
432
+ `Message ${throughMessageId} does not belong to conversation ${conversationId}`,
431
433
  );
432
434
  }
433
435
 
@@ -435,8 +437,8 @@ export function forkConversation(params: {
435
437
  0,
436
438
  Math.min(
437
439
  sourceConversation.contextCompactedMessageCount,
438
- sourceMessages.length
439
- )
440
+ sourceMessages.length,
441
+ ),
440
442
  );
441
443
  const preserveSourceCompactionState =
442
444
  copyBoundaryIndex >= visibleWindowStartIndex;
@@ -530,7 +532,7 @@ export function forkConversation(params: {
530
532
  .orderBy(messageAttachments.position)
531
533
  .all();
532
534
  const uncachedAttachmentLinks = attachmentLinks.filter(
533
- (link) => !attachmentIdMap.has(link.attachmentId)
535
+ (link) => !attachmentIdMap.has(link.attachmentId),
534
536
  );
535
537
  const stagingMessageId =
536
538
  uncachedAttachmentLinks.length > 0 ? uuid() : null;
@@ -566,7 +568,7 @@ export function forkConversation(params: {
566
568
  const scopedAttachmentId = linkAttachmentToMessage(
567
569
  stagingMessageId ?? forkedMessageId,
568
570
  link.attachmentId,
569
- link.position
571
+ link.position,
570
572
  );
571
573
  attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
572
574
  }
@@ -583,6 +585,16 @@ export function forkConversation(params: {
583
585
  });
584
586
  }
585
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
+
586
598
  seedForkedConversationAttention({
587
599
  conversationId: fc.id,
588
600
  latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
@@ -601,7 +613,7 @@ export function forkConversation(params: {
601
613
  const persistedFork = getConversation(forkedConversation.id);
602
614
  if (!persistedFork) {
603
615
  throw new Error(
604
- `Failed to load forked conversation ${forkedConversation.id} after creation`
616
+ `Failed to load forked conversation ${forkedConversation.id} after creation`,
605
617
  );
606
618
  }
607
619
 
@@ -610,14 +622,13 @@ export function forkConversation(params: {
610
622
 
611
623
  /**
612
624
  * 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.
625
+ * artifacts (embeddings). Returns segment IDs so callers can clean up
626
+ * the corresponding Qdrant vector entries.
615
627
  */
616
628
  export function deleteConversation(id: string): DeletedMemoryIds {
617
629
  const db = getDb();
618
630
  const result: DeletedMemoryIds = {
619
631
  segmentIds: [],
620
- orphanedItemIds: [],
621
632
  deletedSummaryIds: [],
622
633
  };
623
634
 
@@ -662,8 +673,8 @@ export function deleteConversation(id: string): DeletedMemoryIds {
662
673
  .where(
663
674
  and(
664
675
  eq(memoryEmbeddings.targetType, "segment"),
665
- inArray(memoryEmbeddings.targetId, result.segmentIds)
666
- )
676
+ inArray(memoryEmbeddings.targetId, result.segmentIds),
677
+ ),
667
678
  )
668
679
  .run();
669
680
  }
@@ -691,8 +702,8 @@ export function deleteConversation(id: string): DeletedMemoryIds {
691
702
  .where(
692
703
  and(
693
704
  eq(memoryEmbeddings.targetType, "summary"),
694
- inArray(memoryEmbeddings.targetId, scopeSummaryIds)
695
- )
705
+ inArray(memoryEmbeddings.targetId, scopeSummaryIds),
706
+ ),
696
707
  )
697
708
  .run();
698
709
  tx.delete(memorySummaries)
@@ -723,14 +734,10 @@ export function deleteConversation(id: string): DeletedMemoryIds {
723
734
  *
724
735
  * Extends `deleteConversation` with:
725
736
  * - 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
737
  * - Deleting conversation-scoped memory summaries and their embeddings
729
- * - Enqueuing `embed_item` jobs for all restored items
730
738
  */
731
739
  export function wipeConversation(id: string): WipeConversationResult {
732
740
  const db = getDb();
733
- const unsupersededItemIds: string[] = [];
734
741
  const deletedSummaryIds: string[] = [];
735
742
 
736
743
  // Step A — Cancel pending memory jobs (before deleting messages, since
@@ -744,8 +751,8 @@ export function wipeConversation(id: string): WipeConversationResult {
744
751
  .where(
745
752
  and(
746
753
  eq(memorySummaries.scope, "conversation"),
747
- eq(memorySummaries.scopeKey, id)
748
- )
754
+ eq(memorySummaries.scopeKey, id),
755
+ ),
749
756
  )
750
757
  .all();
751
758
  const summaryIds = summaryRows.map((r) => r.id);
@@ -754,8 +761,8 @@ export function wipeConversation(id: string): WipeConversationResult {
754
761
  .where(
755
762
  and(
756
763
  eq(memoryEmbeddings.targetType, "summary"),
757
- inArray(memoryEmbeddings.targetId, summaryIds)
758
- )
764
+ inArray(memoryEmbeddings.targetId, summaryIds),
765
+ ),
759
766
  )
760
767
  .run();
761
768
  db.delete(memorySummaries)
@@ -772,7 +779,6 @@ export function wipeConversation(id: string): WipeConversationResult {
772
779
  // Step E — Return the combined result.
773
780
  return {
774
781
  ...deletedMemoryIds,
775
- unsupersededItemIds,
776
782
  deletedSummaryIds: [
777
783
  ...deletedSummaryIds,
778
784
  ...deletedMemoryIds.deletedSummaryIds,
@@ -802,20 +808,17 @@ export function purgePrivateConversations(): {
802
808
  count: 0,
803
809
  deletedMemory: {
804
810
  segmentIds: [],
805
- orphanedItemIds: [],
806
811
  deletedSummaryIds: [],
807
812
  },
808
813
  };
809
814
  }
810
815
 
811
816
  const allSegmentIds: string[] = [];
812
- const allOrphanedItemIds: string[] = [];
813
817
  const allDeletedSummaryIds: string[] = [];
814
818
 
815
819
  for (const conv of privateConvs) {
816
820
  const deleted = deleteConversation(conv.id);
817
821
  allSegmentIds.push(...deleted.segmentIds);
818
- allOrphanedItemIds.push(...deleted.orphanedItemIds);
819
822
  allDeletedSummaryIds.push(...deleted.deletedSummaryIds);
820
823
  }
821
824
 
@@ -823,7 +826,6 @@ export function purgePrivateConversations(): {
823
826
  count: privateConvs.length,
824
827
  deletedMemory: {
825
828
  segmentIds: allSegmentIds,
826
- orphanedItemIds: allOrphanedItemIds,
827
829
  deletedSummaryIds: allDeletedSummaryIds,
828
830
  },
829
831
  };
@@ -834,7 +836,7 @@ export async function addMessage(
834
836
  role: string,
835
837
  content: string,
836
838
  metadata?: Record<string, unknown>,
837
- opts?: { skipIndexing?: boolean }
839
+ opts?: { skipIndexing?: boolean },
838
840
  ) {
839
841
  const db = getDb();
840
842
  const messageId = uuid();
@@ -844,7 +846,7 @@ export async function addMessage(
844
846
  if (!result.success) {
845
847
  log.warn(
846
848
  { conversationId, messageId, issues: result.error.issues },
847
- "Invalid message metadata, storing as-is"
849
+ "Invalid message metadata, storing as-is",
848
850
  );
849
851
  }
850
852
  }
@@ -879,13 +881,13 @@ export async function addMessage(
879
881
  .where(
880
882
  and(
881
883
  eq(conversations.id, conversationId),
882
- isNull(conversations.originChannel)
883
- )
884
+ isNull(conversations.originChannel),
885
+ ),
884
886
  )
885
887
  .run();
886
888
  }
887
889
  tx.update(conversations)
888
- .set({ updatedAt: now })
890
+ .set({ updatedAt: now, lastMessageAt: now })
889
891
  .where(eq(conversations.id, conversationId))
890
892
  .run();
891
893
  });
@@ -899,7 +901,7 @@ export async function addMessage(
899
901
  ) {
900
902
  log.warn(
901
903
  { attempt, conversationId, code: errCode },
902
- "addMessage: transient SQLite error, retrying"
904
+ "addMessage: transient SQLite error, retrying",
903
905
  );
904
906
  await Bun.sleep(50 * (attempt + 1));
905
907
  continue;
@@ -938,12 +940,12 @@ export async function addMessage(
938
940
  provenanceTrustClass,
939
941
  automated,
940
942
  },
941
- config.memory
943
+ config.memory,
942
944
  );
943
945
  } catch (err) {
944
946
  log.warn(
945
947
  { err, conversationId, messageId: message.id },
946
- "Failed to index message for memory"
948
+ "Failed to index message for memory",
947
949
  );
948
950
  }
949
951
  }
@@ -958,7 +960,7 @@ export async function addMessage(
958
960
  } catch (err) {
959
961
  log.warn(
960
962
  { err, conversationId, messageId: message.id },
961
- "Failed to project assistant message for attention tracking"
963
+ "Failed to project assistant message for attention tracking",
962
964
  );
963
965
  }
964
966
  }
@@ -985,7 +987,7 @@ export interface PaginatedMessagesResult {
985
987
  export function getMessagesPaginated(
986
988
  conversationId: string,
987
989
  limit: number | undefined,
988
- beforeTimestamp?: number
990
+ beforeTimestamp?: number,
989
991
  ): PaginatedMessagesResult {
990
992
  const db = getDb();
991
993
 
@@ -1029,7 +1031,7 @@ export function getMessagesPaginated(
1029
1031
 
1030
1032
  export function getLastAssistantTimestampBefore(
1031
1033
  conversationId: string,
1032
- beforeTimestamp: number
1034
+ beforeTimestamp: number,
1033
1035
  ): number {
1034
1036
  const db = getDb();
1035
1037
  const row = db
@@ -1039,8 +1041,8 @@ export function getLastAssistantTimestampBefore(
1039
1041
  and(
1040
1042
  eq(messages.conversationId, conversationId),
1041
1043
  eq(messages.role, "assistant"),
1042
- lt(messages.createdAt, beforeTimestamp)
1043
- )
1044
+ lt(messages.createdAt, beforeTimestamp),
1045
+ ),
1044
1046
  )
1045
1047
  .orderBy(desc(messages.createdAt))
1046
1048
  .limit(1)
@@ -1051,7 +1053,7 @@ export function getLastAssistantTimestampBefore(
1051
1053
  /** Fetch a single message by ID, optionally scoped to a specific conversation. */
1052
1054
  export function getMessageById(
1053
1055
  messageId: string,
1054
- conversationId?: string
1056
+ conversationId?: string,
1055
1057
  ): MessageRow | null {
1056
1058
  const db = getDb();
1057
1059
  const conditions = [eq(messages.id, messageId)];
@@ -1069,7 +1071,7 @@ export function getMessageById(
1069
1071
  export function updateConversationTitle(
1070
1072
  id: string,
1071
1073
  title: string,
1072
- isAutoTitle?: number
1074
+ isAutoTitle?: number,
1073
1075
  ): void {
1074
1076
  const db = getDb();
1075
1077
  const set: Record<string, unknown> = { title, updatedAt: Date.now() };
@@ -1087,7 +1089,7 @@ export function updateConversationUsage(
1087
1089
  id: string,
1088
1090
  totalInputTokens: number,
1089
1091
  totalOutputTokens: number,
1090
- totalEstimatedCost: number
1092
+ totalEstimatedCost: number,
1091
1093
  ): void {
1092
1094
  const db = getDb();
1093
1095
  db.update(conversations)
@@ -1104,7 +1106,7 @@ export function updateConversationUsage(
1104
1106
  export function updateConversationContextWindow(
1105
1107
  id: string,
1106
1108
  contextSummary: string,
1107
- contextCompactedMessageCount: number
1109
+ contextCompactedMessageCount: number,
1108
1110
  ): void {
1109
1111
  const db = getDb();
1110
1112
  db.update(conversations)
@@ -1154,7 +1156,7 @@ export function clearAll(): { conversations: number; messages: number } {
1154
1156
  } catch (err) {
1155
1157
  log.warn(
1156
1158
  { err },
1157
- "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed"
1159
+ "clearAll: failed to clear messages_fts — dropping triggers so base-table cleanup can proceed",
1158
1160
  );
1159
1161
  rawExec("DROP TRIGGER IF EXISTS messages_fts_ai");
1160
1162
  rawExec("DROP TRIGGER IF EXISTS messages_fts_ad");
@@ -1170,7 +1172,7 @@ export function clearAll(): { conversations: number; messages: number } {
1170
1172
  `INSERT INTO lifecycle_events (id, event_name, created_at) VALUES (?, ?, ?)`,
1171
1173
  uuid(),
1172
1174
  "conversations_clear_all",
1173
- Date.now()
1175
+ Date.now(),
1174
1176
  );
1175
1177
 
1176
1178
  // Rebuild corrupted FTS tables and restore triggers after all base-table
@@ -1180,16 +1182,16 @@ export function clearAll(): { conversations: number; messages: number } {
1180
1182
  if (messagesFtsCorrupted) {
1181
1183
  rawExec("DROP TABLE IF EXISTS messages_fts");
1182
1184
  rawExec(
1183
- `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`
1185
+ `CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(message_id UNINDEXED, content)`,
1184
1186
  );
1185
1187
  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`
1188
+ `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
1189
  );
1188
1190
  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`
1191
+ `CREATE TRIGGER IF NOT EXISTS messages_fts_ad AFTER DELETE ON messages BEGIN DELETE FROM messages_fts WHERE message_id = old.id; END`,
1190
1192
  );
1191
1193
  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`
1194
+ `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
1195
  );
1194
1196
  }
1195
1197
 
@@ -1214,8 +1216,8 @@ export function deleteLastExchange(conversationId: string): number {
1214
1216
  .where(
1215
1217
  and(
1216
1218
  eq(messages.conversationId, conversationId),
1217
- eq(messages.role, "user")
1218
- )
1219
+ eq(messages.role, "user"),
1220
+ ),
1219
1221
  )
1220
1222
  .orderBy(sql`rowid DESC`)
1221
1223
  .limit(1)
@@ -1229,7 +1231,7 @@ export function deleteLastExchange(conversationId: string): number {
1229
1231
  const rowidSubquery = sql`(SELECT rowid FROM messages WHERE id = ${lastUserMsg.id})`;
1230
1232
  const condition = and(
1231
1233
  eq(messages.conversationId, conversationId),
1232
- sql`rowid >= ${rowidSubquery}`
1234
+ sql`rowid >= ${rowidSubquery}`,
1233
1235
  );
1234
1236
 
1235
1237
  const [{ deleted }] = db
@@ -1260,8 +1262,16 @@ export function deleteLastExchange(conversationId: string): number {
1260
1262
 
1261
1263
  db.transaction((tx) => {
1262
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();
1263
1270
  tx.update(conversations)
1264
- .set({ updatedAt: Date.now() })
1271
+ .set({
1272
+ updatedAt: Date.now(),
1273
+ lastMessageAt: maxResult?.maxCreatedAt ?? null,
1274
+ })
1265
1275
  .where(eq(conversations.id, conversationId))
1266
1276
  .run();
1267
1277
  });
@@ -1278,12 +1288,10 @@ export function deleteLastExchange(conversationId: string): number {
1278
1288
  */
1279
1289
  export interface DeletedMemoryIds {
1280
1290
  segmentIds: string[];
1281
- orphanedItemIds: string[];
1282
1291
  deletedSummaryIds: string[];
1283
1292
  }
1284
1293
 
1285
1294
  export interface WipeConversationResult extends DeletedMemoryIds {
1286
- unsupersededItemIds: string[];
1287
1295
  cancelledJobCount: number;
1288
1296
  }
1289
1297
 
@@ -1293,7 +1301,7 @@ export interface WipeConversationResult extends DeletedMemoryIds {
1293
1301
  */
1294
1302
  export function updateMessageContent(
1295
1303
  messageId: string,
1296
- newContent: string
1304
+ newContent: string,
1297
1305
  ): void {
1298
1306
  const db = getDb();
1299
1307
  db.update(messages)
@@ -1330,7 +1338,7 @@ export function updateMessageMetadata(
1330
1338
  */
1331
1339
  export function relinkAttachments(
1332
1340
  fromMessageIds: string[],
1333
- toMessageId: string
1341
+ toMessageId: string,
1334
1342
  ): number {
1335
1343
  if (fromMessageIds.length === 0) return 0;
1336
1344
  const db = getDb();
@@ -1365,7 +1373,6 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1365
1373
  const db = getDb();
1366
1374
  const result: DeletedMemoryIds = {
1367
1375
  segmentIds: [],
1368
- orphanedItemIds: [],
1369
1376
  deletedSummaryIds: [],
1370
1377
  };
1371
1378
 
@@ -1379,6 +1386,13 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1379
1386
  .map((r) => r.attachmentId)
1380
1387
  .filter((id): id is string => id !== undefined);
1381
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
+
1382
1396
  db.transaction((tx) => {
1383
1397
  // Collect memory segment IDs linked to this message before cascade.
1384
1398
  const linkedSegments = tx
@@ -1398,14 +1412,29 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1398
1412
  // and message_attachments.
1399
1413
  tx.delete(messages).where(eq(messages.id, messageId)).run();
1400
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
+
1401
1430
  // Clean up segment embeddings from SQLite (Qdrant cleanup is the caller's job).
1402
1431
  if (result.segmentIds.length > 0) {
1403
1432
  tx.delete(memoryEmbeddings)
1404
1433
  .where(
1405
1434
  and(
1406
1435
  eq(memoryEmbeddings.targetType, "segment"),
1407
- inArray(memoryEmbeddings.targetId, result.segmentIds)
1408
- )
1436
+ inArray(memoryEmbeddings.targetId, result.segmentIds),
1437
+ ),
1409
1438
  )
1410
1439
  .run();
1411
1440
  }
@@ -1418,7 +1447,7 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1418
1447
 
1419
1448
  export function setConversationOriginChannelIfUnset(
1420
1449
  conversationId: string,
1421
- channel: ChannelId
1450
+ channel: ChannelId,
1422
1451
  ): void {
1423
1452
  const db = getDb();
1424
1453
  db.update(conversations)
@@ -1426,14 +1455,14 @@ export function setConversationOriginChannelIfUnset(
1426
1455
  .where(
1427
1456
  and(
1428
1457
  eq(conversations.id, conversationId),
1429
- isNull(conversations.originChannel)
1430
- )
1458
+ isNull(conversations.originChannel),
1459
+ ),
1431
1460
  )
1432
1461
  .run();
1433
1462
  }
1434
1463
 
1435
1464
  export function getConversationOriginChannel(
1436
- conversationId: string
1465
+ conversationId: string,
1437
1466
  ): ChannelId | null {
1438
1467
  const db = getDb();
1439
1468
  const row = db
@@ -1446,7 +1475,7 @@ export function getConversationOriginChannel(
1446
1475
 
1447
1476
  export function setConversationOriginInterfaceIfUnset(
1448
1477
  conversationId: string,
1449
- interfaceId: InterfaceId
1478
+ interfaceId: InterfaceId,
1450
1479
  ): void {
1451
1480
  const db = getDb();
1452
1481
  db.update(conversations)
@@ -1454,14 +1483,14 @@ export function setConversationOriginInterfaceIfUnset(
1454
1483
  .where(
1455
1484
  and(
1456
1485
  eq(conversations.id, conversationId),
1457
- isNull(conversations.originInterface)
1458
- )
1486
+ isNull(conversations.originInterface),
1487
+ ),
1459
1488
  )
1460
1489
  .run();
1461
1490
  }
1462
1491
 
1463
1492
  export function getConversationOriginInterface(
1464
- conversationId: string
1493
+ conversationId: string,
1465
1494
  ): InterfaceId | null {
1466
1495
  const db = getDb();
1467
1496
  const row = db
@@ -1481,13 +1510,13 @@ export function getConversationOriginInterface(
1481
1510
  * conversation itself isn't a desktop-origin private conversation).
1482
1511
  */
1483
1512
  export function getConversationRecentProvenanceTrustClass(
1484
- conversationId: string
1513
+ conversationId: string,
1485
1514
  ): "guardian" | "trusted_contact" | "unknown" | undefined {
1486
1515
  const row = rawGet<{ metadata: string | null }>(
1487
1516
  `SELECT metadata FROM messages
1488
1517
  WHERE conversation_id = ? AND role = 'user' AND metadata IS NOT NULL
1489
1518
  ORDER BY created_at DESC LIMIT 1`,
1490
- conversationId
1519
+ conversationId,
1491
1520
  );
1492
1521
  if (!row?.metadata) return undefined;
1493
1522
  try {
@@ -1508,7 +1537,7 @@ export function batchSetDisplayOrders(
1508
1537
  displayOrder: number | null;
1509
1538
  isPinned: boolean;
1510
1539
  groupId?: string | null;
1511
- }>
1540
+ }>,
1512
1541
  ): void {
1513
1542
  ensureDisplayOrderMigration();
1514
1543
  ensureGroupMigration();
@@ -1525,7 +1554,7 @@ export function batchSetDisplayOrders(
1525
1554
  safeGroupId !== null &&
1526
1555
  !rawGet<{ id: string }>(
1527
1556
  "SELECT id FROM conversation_groups WHERE id = ?",
1528
- safeGroupId
1557
+ safeGroupId,
1529
1558
  )
1530
1559
  ) {
1531
1560
  safeGroupId = null;
@@ -1535,7 +1564,7 @@ export function batchSetDisplayOrders(
1535
1564
  update.displayOrder,
1536
1565
  safeGroupId === "system:pinned" ? 1 : 0,
1537
1566
  safeGroupId,
1538
- update.id
1567
+ update.id,
1539
1568
  );
1540
1569
  } else {
1541
1570
  // Old client: no groupId in payload
@@ -1546,7 +1575,7 @@ export function batchSetDisplayOrders(
1546
1575
  rawRun(
1547
1576
  "UPDATE conversations SET display_order = ?, is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
1548
1577
  update.displayOrder,
1549
- update.id
1578
+ update.id,
1550
1579
  );
1551
1580
  } else {
1552
1581
  // Restore system group from source/conversationType when old clients
@@ -1563,7 +1592,7 @@ export function batchSetDisplayOrders(
1563
1592
  ELSE group_id END
1564
1593
  WHERE id = ?`,
1565
1594
  update.displayOrder,
1566
- update.id
1595
+ update.id,
1567
1596
  );
1568
1597
  }
1569
1598
  }
@@ -1576,7 +1605,7 @@ export function batchSetDisplayOrders(
1576
1605
  }
1577
1606
 
1578
1607
  export function getDisplayMetaForConversations(
1579
- conversationIds: string[]
1608
+ conversationIds: string[],
1580
1609
  ): Map<
1581
1610
  string,
1582
1611
  { displayOrder: number | null; isPinned: boolean; groupId: string | null }
@@ -1595,7 +1624,7 @@ export function getDisplayMetaForConversations(
1595
1624
  group_id: string | null;
1596
1625
  }>(
1597
1626
  "SELECT display_order, is_pinned, group_id FROM conversations WHERE id = ?",
1598
- id
1627
+ id,
1599
1628
  );
1600
1629
  result.set(id, {
1601
1630
  displayOrder: row?.display_order ?? null,
@@ -1624,7 +1653,7 @@ function isToolResultMessage(role: string, content: string): boolean {
1624
1653
  (block: unknown) =>
1625
1654
  block != null &&
1626
1655
  typeof block === "object" &&
1627
- (block as Record<string, unknown>).type === "tool_result"
1656
+ (block as Record<string, unknown>).type === "tool_result",
1628
1657
  );
1629
1658
  } catch {
1630
1659
  return false;
@@ -1644,7 +1673,7 @@ function isToolResultMessage(role: string, content: string): boolean {
1644
1673
  */
1645
1674
  export function getTurnTimeBounds(
1646
1675
  conversationId: string,
1647
- messageCreatedAt: number
1676
+ messageCreatedAt: number,
1648
1677
  ): { startTime: number; endTime: number } | null {
1649
1678
  const db = getDb();
1650
1679
 
@@ -1666,8 +1695,8 @@ export function getTurnTimeBounds(
1666
1695
  .where(
1667
1696
  and(
1668
1697
  eq(messages.conversationId, conversationId),
1669
- sql`rowid <= ${rowidSubquery}`
1670
- )
1698
+ sql`rowid <= ${rowidSubquery}`,
1699
+ ),
1671
1700
  )
1672
1701
  .orderBy(sql`rowid DESC`)
1673
1702
  .limit(50)
@@ -1698,8 +1727,8 @@ export function getTurnTimeBounds(
1698
1727
  .where(
1699
1728
  and(
1700
1729
  eq(messages.conversationId, conversationId),
1701
- sql`rowid > ${forwardRowidSubquery}`
1702
- )
1730
+ sql`rowid > ${forwardRowidSubquery}`,
1731
+ ),
1703
1732
  )
1704
1733
  .orderBy(sql`rowid ASC`)
1705
1734
  .limit(50)
@@ -1740,8 +1769,8 @@ export function getTurnTimeBounds(
1740
1769
  and(
1741
1770
  eq(llmRequestLogs.conversationId, conversationId),
1742
1771
  gte(llmRequestLogs.createdAt, startTime),
1743
- lte(llmRequestLogs.createdAt, hardCeiling)
1744
- )
1772
+ lte(llmRequestLogs.createdAt, hardCeiling),
1773
+ ),
1745
1774
  )
1746
1775
  .orderBy(desc(llmRequestLogs.createdAt))
1747
1776
  .limit(1)
@@ -1789,8 +1818,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1789
1818
  .where(
1790
1819
  and(
1791
1820
  eq(messages.conversationId, target.conversationId),
1792
- lte(messages.createdAt, target.createdAt)
1793
- )
1821
+ lte(messages.createdAt, target.createdAt),
1822
+ ),
1794
1823
  )
1795
1824
  .orderBy(desc(messages.createdAt))
1796
1825
  .limit(50)
@@ -1827,8 +1856,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1827
1856
  .where(
1828
1857
  and(
1829
1858
  eq(messages.conversationId, target.conversationId),
1830
- gt(messages.createdAt, target.createdAt)
1831
- )
1859
+ gt(messages.createdAt, target.createdAt),
1860
+ ),
1832
1861
  )
1833
1862
  .orderBy(asc(messages.createdAt))
1834
1863
  .limit(50)
@@ -1864,8 +1893,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1864
1893
  and(
1865
1894
  eq(messages.conversationId, target.conversationId),
1866
1895
  gt(messages.createdAt, boundaryCreatedAt),
1867
- lte(messages.createdAt, target.createdAt)
1868
- )
1896
+ lte(messages.createdAt, target.createdAt),
1897
+ ),
1869
1898
  )
1870
1899
  .orderBy(asc(messages.createdAt))
1871
1900
  .all();
@@ -1888,8 +1917,8 @@ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1888
1917
  .where(
1889
1918
  and(
1890
1919
  eq(messages.conversationId, target.conversationId),
1891
- inArray(messages.id, [...idSet])
1892
- )
1920
+ inArray(messages.id, [...idSet]),
1921
+ ),
1893
1922
  )
1894
1923
  .orderBy(asc(messages.createdAt))
1895
1924
  .all();