@vellumai/assistant 0.5.1 → 0.5.3

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 (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -1,4 +1,17 @@
1
- import { and, asc, count, eq, inArray, isNull, sql } from "drizzle-orm";
1
+ import { mkdirSync, rmSync } from "node:fs";
2
+
3
+ import {
4
+ and,
5
+ asc,
6
+ count,
7
+ desc,
8
+ eq,
9
+ gt,
10
+ inArray,
11
+ isNull,
12
+ lte,
13
+ sql,
14
+ } from "drizzle-orm";
2
15
  import { v4 as uuid } from "uuid";
3
16
  import { z } from "zod";
4
17
 
@@ -7,10 +20,24 @@ import { parseChannelId, parseInterfaceId } from "../channels/types.js";
7
20
  import { CHANNEL_IDS, INTERFACE_IDS, isChannelId } from "../channels/types.js";
8
21
  import { getConfig } from "../config/loader.js";
9
22
  import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
23
+ import { UserError } from "../util/errors.js";
10
24
  import { getLogger } from "../util/logger.js";
25
+ import { getConversationsDir } from "../util/platform.js";
11
26
  import { createRowMapper } from "../util/row-mapper.js";
12
- import { deleteOrphanAttachments } from "./attachments-store.js";
13
- import { projectAssistantMessage } from "./conversation-attention-store.js";
27
+ import {
28
+ deleteOrphanAttachments,
29
+ linkAttachmentToMessage,
30
+ } from "./attachments-store.js";
31
+ import {
32
+ projectAssistantMessage,
33
+ seedForkedConversationAttention,
34
+ } from "./conversation-attention-store.js";
35
+ import {
36
+ initConversationDir,
37
+ removeConversationDir,
38
+ syncMessageToDisk,
39
+ updateMetaFile,
40
+ } from "./conversation-disk-view.js";
14
41
  import { ensureDisplayOrderMigration } from "./conversation-display-order-migration.js";
15
42
  import { getDb, rawAll, rawExec, rawGet, rawRun } from "./db.js";
16
43
  import { indexMessageNow } from "./indexer.js";
@@ -18,6 +45,7 @@ import { enqueueMemoryJob } from "./jobs-store.js";
18
45
  import {
19
46
  channelInboundEvents,
20
47
  conversations,
48
+ conversationStarters,
21
49
  llmRequestLogs,
22
50
  memoryEmbeddings,
23
51
  memoryItems,
@@ -47,6 +75,9 @@ const subagentNotificationSchema = z.object({
47
75
  conversationId: z.string().optional(),
48
76
  });
49
77
 
78
+ export const PRIVATE_CONVERSATION_FORK_ERROR =
79
+ "Private conversations cannot be forked";
80
+
50
81
  export const messageMetadataSchema = z
51
82
  .object({
52
83
  userMessageChannel: channelIdSchema.optional(),
@@ -67,11 +98,42 @@ export const messageMetadataSchema = z
67
98
  provenanceGuardianExternalUserId: z.string().optional(),
68
99
  provenanceRequesterIdentifier: z.string().optional(),
69
100
  automated: z.boolean().optional(),
101
+ forkSourceMessageId: z.string().optional(),
102
+ /** Image source paths from desktop attachments, keyed by filename. */
103
+ imageSourcePaths: z.record(z.string(), z.string()).optional(),
70
104
  })
71
105
  .passthrough();
72
106
 
73
107
  export type MessageMetadata = z.infer<typeof messageMetadataSchema>;
74
108
 
109
+ function cloneForkMessageMetadata(
110
+ metadata: string | null,
111
+ sourceMessageId: string,
112
+ ): string {
113
+ if (!metadata) {
114
+ return JSON.stringify({ forkSourceMessageId: sourceMessageId });
115
+ }
116
+
117
+ try {
118
+ const parsed = JSON.parse(metadata);
119
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
120
+ const sourceRecord = parsed as Record<string, unknown>;
121
+ const forkSourceMessageId =
122
+ typeof sourceRecord.forkSourceMessageId === "string"
123
+ ? sourceRecord.forkSourceMessageId
124
+ : sourceMessageId;
125
+ return JSON.stringify({
126
+ ...sourceRecord,
127
+ forkSourceMessageId,
128
+ });
129
+ }
130
+ } catch {
131
+ // Fall through to source-only metadata.
132
+ }
133
+
134
+ return JSON.stringify({ forkSourceMessageId: sourceMessageId });
135
+ }
136
+
75
137
  /**
76
138
  * Extract provenance metadata fields from a TrustContext.
77
139
  * When no guardian context is provided, defaults to 'unknown' because the
@@ -106,8 +168,13 @@ export interface ConversationRow {
106
168
  memoryScopeId: string;
107
169
  originChannel: string | null;
108
170
  originInterface: string | null;
171
+ forkParentConversationId: string | null;
172
+ forkParentMessageId: string | null;
109
173
  isAutoTitle: number;
110
174
  scheduleJobId: string | null;
175
+ memoryReducedThroughMessageId: string | null;
176
+ memoryDirtyTailSinceMessageId: string | null;
177
+ memoryLastReducedAt: number | null;
111
178
  }
112
179
 
113
180
  export const parseConversation = createRowMapper<
@@ -129,8 +196,13 @@ export const parseConversation = createRowMapper<
129
196
  memoryScopeId: "memoryScopeId",
130
197
  originChannel: "originChannel",
131
198
  originInterface: "originInterface",
199
+ forkParentConversationId: "forkParentConversationId",
200
+ forkParentMessageId: "forkParentMessageId",
132
201
  isAutoTitle: "isAutoTitle",
133
202
  scheduleJobId: "scheduleJobId",
203
+ memoryReducedThroughMessageId: "memoryReducedThroughMessageId",
204
+ memoryDirtyTailSinceMessageId: "memoryDirtyTailSinceMessageId",
205
+ memoryLastReducedAt: "memoryLastReducedAt",
134
206
  });
135
207
 
136
208
  export interface MessageRow {
@@ -232,6 +304,8 @@ export function createConversation(
232
304
  }
233
305
  }
234
306
 
307
+ initConversationDir({ ...conversation, originChannel: null });
308
+
235
309
  return conversation;
236
310
  }
237
311
 
@@ -258,6 +332,213 @@ export function getConversationMemoryScopeId(conversationId: string): string {
258
332
  return conv?.memoryScopeId ?? "default";
259
333
  }
260
334
 
335
+ export function forkConversation(params: {
336
+ conversationId: string;
337
+ throughMessageId?: string;
338
+ }): ConversationRow {
339
+ const { conversationId, throughMessageId } = params;
340
+ const db = getDb();
341
+ const sourceConversation = getConversation(conversationId);
342
+
343
+ if (!sourceConversation) {
344
+ throw new UserError(`Conversation ${conversationId} not found`);
345
+ }
346
+ if (sourceConversation.conversationType === "private") {
347
+ throw new UserError(PRIVATE_CONVERSATION_FORK_ERROR);
348
+ }
349
+
350
+ const sourceMessages = getMessages(conversationId);
351
+
352
+ if (sourceMessages.length === 0) {
353
+ throw new UserError(
354
+ `Conversation ${conversationId} has no persisted messages to fork`,
355
+ );
356
+ }
357
+
358
+ const copyBoundaryIndex =
359
+ throughMessageId == null
360
+ ? sourceMessages.length - 1
361
+ : sourceMessages.findIndex((message) => message.id === throughMessageId);
362
+
363
+ if (throughMessageId != null && copyBoundaryIndex === -1) {
364
+ throw new UserError(
365
+ `Message ${throughMessageId} does not belong to conversation ${conversationId}`,
366
+ );
367
+ }
368
+
369
+ const visibleWindowStartIndex = Math.max(
370
+ 0,
371
+ Math.min(
372
+ sourceConversation.contextCompactedMessageCount,
373
+ sourceMessages.length,
374
+ ),
375
+ );
376
+ const preserveSourceCompactionState =
377
+ copyBoundaryIndex >= visibleWindowStartIndex;
378
+
379
+ const messagesToCopy =
380
+ copyBoundaryIndex >= 0
381
+ ? sourceMessages.slice(0, copyBoundaryIndex + 1)
382
+ : ([] as MessageRow[]);
383
+ const forkParentMessageId = messagesToCopy.at(-1)?.id ?? null;
384
+ const forkTitle = `${sourceConversation.title ?? "Untitled"} (Fork)`;
385
+
386
+ // Collect disk-sync work to run after the transaction commits.
387
+ const diskSyncQueue: Array<{
388
+ conversationId: string;
389
+ messageId: string;
390
+ createdAt: number;
391
+ }> = [];
392
+
393
+ // Wrap all DB mutations in a single transaction so a mid-flight failure
394
+ // rolls back cleanly instead of leaving a partial fork. Helper functions
395
+ // (linkAttachmentToMessage, relinkAttachments, seedForkedConversationAttention)
396
+ // use the same underlying bun:sqlite connection, so their writes participate
397
+ // in this transaction automatically.
398
+ const forkedConversation = db.transaction(() => {
399
+ const fc = createConversation({
400
+ title: forkTitle,
401
+ conversationType: "standard",
402
+ });
403
+
404
+ db.update(conversations)
405
+ .set({
406
+ forkParentConversationId: sourceConversation.id,
407
+ forkParentMessageId,
408
+ contextSummary: preserveSourceCompactionState
409
+ ? sourceConversation.contextSummary
410
+ : null,
411
+ contextCompactedMessageCount: preserveSourceCompactionState
412
+ ? sourceConversation.contextCompactedMessageCount
413
+ : 0,
414
+ contextCompactedAt: preserveSourceCompactionState
415
+ ? sourceConversation.contextCompactedAt
416
+ : null,
417
+ })
418
+ .where(eq(conversations.id, fc.id))
419
+ .run();
420
+
421
+ const forkedMessageIds = new Map<string, string>();
422
+ let latestForkedAssistant: {
423
+ messageId: string;
424
+ messageAt: number;
425
+ } | null = null;
426
+
427
+ for (const message of messagesToCopy) {
428
+ const forkedMessageId = uuid();
429
+ db.insert(messages)
430
+ .values({
431
+ id: forkedMessageId,
432
+ conversationId: fc.id,
433
+ role: message.role,
434
+ content: message.content,
435
+ createdAt: message.createdAt,
436
+ metadata: cloneForkMessageMetadata(message.metadata, message.id),
437
+ })
438
+ .run();
439
+ forkedMessageIds.set(message.id, forkedMessageId);
440
+
441
+ if (message.role === "assistant") {
442
+ latestForkedAssistant = {
443
+ messageId: forkedMessageId,
444
+ messageAt: message.createdAt,
445
+ };
446
+ }
447
+ }
448
+
449
+ const attachmentIdMap = new Map<string, string>();
450
+ for (const message of messagesToCopy) {
451
+ const forkedMessageId = forkedMessageIds.get(message.id);
452
+ if (!forkedMessageId) continue;
453
+
454
+ const attachmentLinks = db
455
+ .select({
456
+ attachmentId: messageAttachments.attachmentId,
457
+ position: messageAttachments.position,
458
+ })
459
+ .from(messageAttachments)
460
+ .where(eq(messageAttachments.messageId, message.id))
461
+ .orderBy(messageAttachments.position)
462
+ .all();
463
+ const uncachedAttachmentLinks = attachmentLinks.filter(
464
+ (link) => !attachmentIdMap.has(link.attachmentId),
465
+ );
466
+ const stagingMessageId =
467
+ uncachedAttachmentLinks.length > 0 ? uuid() : null;
468
+
469
+ if (stagingMessageId) {
470
+ db.insert(messages)
471
+ .values({
472
+ id: stagingMessageId,
473
+ conversationId: fc.id,
474
+ role: message.role,
475
+ content: "",
476
+ createdAt: message.createdAt,
477
+ metadata: null,
478
+ })
479
+ .run();
480
+ }
481
+
482
+ for (const link of attachmentLinks) {
483
+ const cachedAttachmentId = attachmentIdMap.get(link.attachmentId);
484
+ if (cachedAttachmentId) {
485
+ db.insert(messageAttachments)
486
+ .values({
487
+ id: uuid(),
488
+ messageId: forkedMessageId,
489
+ attachmentId: cachedAttachmentId,
490
+ position: link.position,
491
+ createdAt: Date.now(),
492
+ })
493
+ .run();
494
+ continue;
495
+ }
496
+
497
+ const scopedAttachmentId = linkAttachmentToMessage(
498
+ stagingMessageId ?? forkedMessageId,
499
+ link.attachmentId,
500
+ link.position,
501
+ );
502
+ attachmentIdMap.set(link.attachmentId, scopedAttachmentId);
503
+ }
504
+
505
+ if (stagingMessageId) {
506
+ relinkAttachments([stagingMessageId], forkedMessageId);
507
+ db.delete(messages).where(eq(messages.id, stagingMessageId)).run();
508
+ }
509
+
510
+ diskSyncQueue.push({
511
+ conversationId: fc.id,
512
+ messageId: forkedMessageId,
513
+ createdAt: fc.createdAt,
514
+ });
515
+ }
516
+
517
+ seedForkedConversationAttention({
518
+ conversationId: fc.id,
519
+ latestAssistantMessageId: latestForkedAssistant?.messageId ?? null,
520
+ latestAssistantMessageAt: latestForkedAssistant?.messageAt ?? null,
521
+ });
522
+
523
+ return fc;
524
+ });
525
+
526
+ // Disk-view sync runs after commit — file I/O is idempotent and
527
+ // conversation deletion cleans up orphaned directories.
528
+ for (const entry of diskSyncQueue) {
529
+ syncMessageToDisk(entry.conversationId, entry.messageId, entry.createdAt);
530
+ }
531
+
532
+ const persistedFork = getConversation(forkedConversation.id);
533
+ if (!persistedFork) {
534
+ throw new Error(
535
+ `Failed to load forked conversation ${forkedConversation.id} after creation`,
536
+ );
537
+ }
538
+
539
+ return persistedFork;
540
+ }
541
+
261
542
  /**
262
543
  * Delete a conversation and all its messages, cleaning up orphaned memory
263
544
  * artifacts (items, embeddings). Returns segment and orphaned item IDs so
@@ -265,7 +546,18 @@ export function getConversationMemoryScopeId(conversationId: string): string {
265
546
  */
266
547
  export function deleteConversation(id: string): DeletedMemoryIds {
267
548
  const db = getDb();
268
- const result: DeletedMemoryIds = { segmentIds: [], orphanedItemIds: [] };
549
+ const result: DeletedMemoryIds = {
550
+ segmentIds: [],
551
+ orphanedItemIds: [],
552
+ deletedSummaryIds: [],
553
+ };
554
+
555
+ // Capture createdAt before the transaction deletes the row — needed to
556
+ // resolve the conversation's disk-view directory path after deletion.
557
+ const convBeforeDelete = getConversation(id);
558
+ const createdAtForDiskCleanup = convBeforeDelete?.createdAt;
559
+ const memoryScopeId = convBeforeDelete?.memoryScopeId;
560
+ const isPrivateScope = memoryScopeId?.startsWith("private:") ?? false;
269
561
 
270
562
  db.transaction((tx) => {
271
563
  // Collect all message IDs for this conversation.
@@ -354,9 +646,73 @@ export function deleteConversation(id: string): DeletedMemoryIds {
354
646
  .run();
355
647
  }
356
648
 
649
+ if (isPrivateScope && memoryScopeId) {
650
+ // Sweep remaining memory items with this private scopeId.
651
+ const scopeItems = tx
652
+ .select({ id: memoryItems.id })
653
+ .from(memoryItems)
654
+ .where(eq(memoryItems.scopeId, memoryScopeId))
655
+ .all();
656
+ const alreadyDeleted = new Set(result.orphanedItemIds);
657
+ const scopeItemIds = scopeItems
658
+ .map((r) => r.id)
659
+ .filter((id) => !alreadyDeleted.has(id));
660
+
661
+ if (scopeItemIds.length > 0) {
662
+ tx.delete(memoryEmbeddings)
663
+ .where(
664
+ and(
665
+ eq(memoryEmbeddings.targetType, "item"),
666
+ inArray(memoryEmbeddings.targetId, scopeItemIds),
667
+ ),
668
+ )
669
+ .run();
670
+ tx.delete(memoryItemSources)
671
+ .where(inArray(memoryItemSources.memoryItemId, scopeItemIds))
672
+ .run();
673
+ tx.delete(memoryItems)
674
+ .where(inArray(memoryItems.id, scopeItemIds))
675
+ .run();
676
+ result.orphanedItemIds.push(...scopeItemIds);
677
+ }
678
+
679
+ // Sweep memory summaries with this private scopeId.
680
+ const scopeSummaries = tx
681
+ .select({ id: memorySummaries.id })
682
+ .from(memorySummaries)
683
+ .where(eq(memorySummaries.scopeId, memoryScopeId))
684
+ .all();
685
+ const scopeSummaryIds = scopeSummaries.map((r) => r.id);
686
+
687
+ if (scopeSummaryIds.length > 0) {
688
+ tx.delete(memoryEmbeddings)
689
+ .where(
690
+ and(
691
+ eq(memoryEmbeddings.targetType, "summary"),
692
+ inArray(memoryEmbeddings.targetId, scopeSummaryIds),
693
+ ),
694
+ )
695
+ .run();
696
+ tx.delete(memorySummaries)
697
+ .where(inArray(memorySummaries.id, scopeSummaryIds))
698
+ .run();
699
+ result.deletedSummaryIds.push(...scopeSummaryIds);
700
+ }
701
+
702
+ // Sweep conversation starters with this private scopeId.
703
+ tx.delete(conversationStarters)
704
+ .where(eq(conversationStarters.scopeId, memoryScopeId))
705
+ .run();
706
+ }
707
+
357
708
  tx.delete(conversations).where(eq(conversations.id, id)).run();
358
709
  });
359
710
 
711
+ // Remove the conversation's disk-view directory after the DB transaction
712
+ if (createdAtForDiskCleanup != null) {
713
+ removeConversationDir(id, createdAtForDiskCleanup);
714
+ }
715
+
360
716
  return result;
361
717
  }
362
718
 
@@ -541,7 +897,10 @@ export function wipeConversation(id: string): WipeConversationResult {
541
897
  return {
542
898
  ...deletedMemoryIds,
543
899
  unsupersededItemIds,
544
- deletedSummaryIds,
900
+ deletedSummaryIds: [
901
+ ...deletedSummaryIds,
902
+ ...deletedMemoryIds.deletedSummaryIds,
903
+ ],
545
904
  cancelledJobCount,
546
905
  };
547
906
  }
@@ -563,16 +922,25 @@ export function purgePrivateConversations(): {
563
922
  .all();
564
923
 
565
924
  if (privateConvs.length === 0) {
566
- return { count: 0, deletedMemory: { segmentIds: [], orphanedItemIds: [] } };
925
+ return {
926
+ count: 0,
927
+ deletedMemory: {
928
+ segmentIds: [],
929
+ orphanedItemIds: [],
930
+ deletedSummaryIds: [],
931
+ },
932
+ };
567
933
  }
568
934
 
569
935
  const allSegmentIds: string[] = [];
570
936
  const allOrphanedItemIds: string[] = [];
937
+ const allDeletedSummaryIds: string[] = [];
571
938
 
572
939
  for (const conv of privateConvs) {
573
940
  const deleted = deleteConversation(conv.id);
574
941
  allSegmentIds.push(...deleted.segmentIds);
575
942
  allOrphanedItemIds.push(...deleted.orphanedItemIds);
943
+ allDeletedSummaryIds.push(...deleted.deletedSummaryIds);
576
944
  }
577
945
 
578
946
  return {
@@ -580,6 +948,7 @@ export function purgePrivateConversations(): {
580
948
  deletedMemory: {
581
949
  segmentIds: allSegmentIds,
582
950
  orphanedItemIds: allOrphanedItemIds,
951
+ deletedSummaryIds: allDeletedSummaryIds,
583
952
  },
584
953
  };
585
954
  }
@@ -662,6 +1031,13 @@ export async function addMessage(
662
1031
  throw err;
663
1032
  }
664
1033
  }
1034
+
1035
+ // Mark the conversation dirty for delayed memory reduction. This runs
1036
+ // after the insert transaction succeeds so the reducer knows which
1037
+ // conversations have unprocessed messages. The helper preserves the
1038
+ // earliest unreduced boundary (no-op when already dirty).
1039
+ markConversationMemoryDirty(conversationId, messageId);
1040
+
665
1041
  const message = {
666
1042
  id: messageId,
667
1043
  conversationId,
@@ -759,6 +1135,12 @@ export function updateConversationTitle(
759
1135
  const set: Record<string, unknown> = { title, updatedAt: Date.now() };
760
1136
  if (isAutoTitle !== undefined) set.isAutoTitle = isAutoTitle;
761
1137
  db.update(conversations).set(set).where(eq(conversations.id, id)).run();
1138
+
1139
+ // Update disk view meta.json with the new title
1140
+ const conv = getConversation(id);
1141
+ if (conv) {
1142
+ updateMetaFile(conv);
1143
+ }
762
1144
  }
763
1145
 
764
1146
  export function updateConversationUsage(
@@ -864,6 +1246,14 @@ export function clearAll(): { conversations: number; messages: number } {
864
1246
  );
865
1247
  }
866
1248
 
1249
+ // Clear the disk-view conversations directory and recreate it empty
1250
+ try {
1251
+ rmSync(getConversationsDir(), { recursive: true, force: true });
1252
+ mkdirSync(getConversationsDir(), { recursive: true });
1253
+ } catch (err) {
1254
+ log.warn({ err }, "clearAll: failed to reset conversations directory");
1255
+ }
1256
+
867
1257
  return { conversations: convCount, messages: msgCount };
868
1258
  }
869
1259
 
@@ -942,11 +1332,11 @@ export function deleteLastExchange(conversationId: string): number {
942
1332
  export interface DeletedMemoryIds {
943
1333
  segmentIds: string[];
944
1334
  orphanedItemIds: string[];
1335
+ deletedSummaryIds: string[];
945
1336
  }
946
1337
 
947
1338
  export interface WipeConversationResult extends DeletedMemoryIds {
948
1339
  unsupersededItemIds: string[];
949
- deletedSummaryIds: string[];
950
1340
  cancelledJobCount: number;
951
1341
  }
952
1342
 
@@ -1012,7 +1402,11 @@ export function relinkAttachments(
1012
1402
  */
1013
1403
  export function deleteMessageById(messageId: string): DeletedMemoryIds {
1014
1404
  const db = getDb();
1015
- const result: DeletedMemoryIds = { segmentIds: [], orphanedItemIds: [] };
1405
+ const result: DeletedMemoryIds = {
1406
+ segmentIds: [],
1407
+ orphanedItemIds: [],
1408
+ deletedSummaryIds: [],
1409
+ };
1016
1410
 
1017
1411
  // Collect attachment IDs linked to this message before cascade-delete
1018
1412
  // so we can scope orphan cleanup to only those candidates.
@@ -1100,6 +1494,28 @@ export function deleteMessageById(messageId: string): DeletedMemoryIds {
1100
1494
  return result;
1101
1495
  }
1102
1496
 
1497
+ /**
1498
+ * Mark a conversation as having unreduced messages starting from the given
1499
+ * message. Sets `memoryDirtyTailSinceMessageId` only when it is currently
1500
+ * null so the earliest unreduced boundary is preserved across multiple
1501
+ * messages — later messages must not clobber the original dirty marker.
1502
+ */
1503
+ export function markConversationMemoryDirty(
1504
+ conversationId: string,
1505
+ messageId: string,
1506
+ ): void {
1507
+ const db = getDb();
1508
+ db.update(conversations)
1509
+ .set({ memoryDirtyTailSinceMessageId: messageId })
1510
+ .where(
1511
+ and(
1512
+ eq(conversations.id, conversationId),
1513
+ isNull(conversations.memoryDirtyTailSinceMessageId),
1514
+ ),
1515
+ )
1516
+ .run();
1517
+ }
1518
+
1103
1519
  export function setConversationOriginChannelIfUnset(
1104
1520
  conversationId: string,
1105
1521
  channel: ChannelId,
@@ -1232,3 +1648,170 @@ export function getDisplayMetaForConversations(
1232
1648
  }
1233
1649
  return result;
1234
1650
  }
1651
+
1652
+ // ── Turn boundary resolution ─────────────────────────────────────────
1653
+
1654
+ /**
1655
+ * Returns `true` if a message is a tool-result user message — i.e. its
1656
+ * role is "user" and its content is a JSON array where every block has
1657
+ * `type === "tool_result"`. These synthetic user messages are injected
1658
+ * between assistant messages within a single agent turn and should NOT
1659
+ * be treated as turn boundaries.
1660
+ */
1661
+ function isToolResultMessage(role: string, content: string): boolean {
1662
+ if (role !== "user") return false;
1663
+ try {
1664
+ const parsed = JSON.parse(content);
1665
+ if (!Array.isArray(parsed) || parsed.length === 0) return false;
1666
+ return parsed.every(
1667
+ (block: unknown) =>
1668
+ block != null &&
1669
+ typeof block === "object" &&
1670
+ (block as Record<string, unknown>).type === "tool_result",
1671
+ );
1672
+ } catch {
1673
+ return false;
1674
+ }
1675
+ }
1676
+
1677
+ /**
1678
+ * Resolve all assistant message IDs that belong to the same agent turn
1679
+ * as the given `messageId`. A "turn" is bounded by:
1680
+ * - The start of the conversation, or
1681
+ * - A user message whose content is NOT a tool_result array.
1682
+ *
1683
+ * Within a multi-step agent loop, the pattern is:
1684
+ * user msg → assistant A1 → user (tool_result) → assistant A2 → ...
1685
+ * All assistant messages from A1 through the queried message (and beyond,
1686
+ * up to the next real user message) are part of the same turn.
1687
+ *
1688
+ * Returns `[messageId]` as a fallback if the message is not found,
1689
+ * preserving backward compatibility for callers.
1690
+ */
1691
+ export function getAssistantMessageIdsInTurn(messageId: string): string[] {
1692
+ const db = getDb();
1693
+
1694
+ // Look up the target message to get its conversationId and createdAt.
1695
+ const target = getMessageById(messageId);
1696
+ if (!target) return [messageId];
1697
+
1698
+ // Walk backward from the target message to find the turn boundary.
1699
+ // Limit to 50 rows — sufficient for even aggressive tool-use loops.
1700
+ const backwardRows = db
1701
+ .select({
1702
+ id: messages.id,
1703
+ role: messages.role,
1704
+ content: messages.content,
1705
+ createdAt: messages.createdAt,
1706
+ })
1707
+ .from(messages)
1708
+ .where(
1709
+ and(
1710
+ eq(messages.conversationId, target.conversationId),
1711
+ lte(messages.createdAt, target.createdAt),
1712
+ ),
1713
+ )
1714
+ .orderBy(desc(messages.createdAt))
1715
+ .limit(50)
1716
+ .all();
1717
+
1718
+ const assistantIds: string[] = [];
1719
+ let boundaryCreatedAt: number | null = null;
1720
+
1721
+ for (const row of backwardRows) {
1722
+ if (row.role === "assistant") {
1723
+ assistantIds.push(row.id);
1724
+ } else if (row.role === "user") {
1725
+ if (isToolResultMessage(row.role, row.content)) {
1726
+ // Tool-result user message — still within the same turn, continue.
1727
+ continue;
1728
+ }
1729
+ // Real user message — this is the turn boundary.
1730
+ boundaryCreatedAt = row.createdAt;
1731
+ break;
1732
+ }
1733
+ }
1734
+
1735
+ // Walk forward from the target to collect any later assistant messages
1736
+ // still within the same turn (e.g. when querying an intermediate
1737
+ // message like A1 in a multi-step turn A1 → tool_result → A2).
1738
+ const forwardRows = db
1739
+ .select({
1740
+ id: messages.id,
1741
+ role: messages.role,
1742
+ content: messages.content,
1743
+ createdAt: messages.createdAt,
1744
+ })
1745
+ .from(messages)
1746
+ .where(
1747
+ and(
1748
+ eq(messages.conversationId, target.conversationId),
1749
+ gt(messages.createdAt, target.createdAt),
1750
+ ),
1751
+ )
1752
+ .orderBy(asc(messages.createdAt))
1753
+ .limit(50)
1754
+ .all();
1755
+
1756
+ for (const row of forwardRows) {
1757
+ if (row.role === "assistant") {
1758
+ if (!assistantIds.includes(row.id)) {
1759
+ assistantIds.push(row.id);
1760
+ }
1761
+ } else if (row.role === "user") {
1762
+ if (isToolResultMessage(row.role, row.content)) {
1763
+ // Tool-result user message — still within the same turn.
1764
+ continue;
1765
+ }
1766
+ // Real user message — end of the turn.
1767
+ break;
1768
+ }
1769
+ }
1770
+
1771
+ // Also query forward from the backward-walk boundary to pick up any
1772
+ // assistant messages between the boundary and the target that may have
1773
+ // been missed (e.g. due to the 50-row limit in the backward walk).
1774
+ if (boundaryCreatedAt != null) {
1775
+ const gapRows = db
1776
+ .select({
1777
+ id: messages.id,
1778
+ role: messages.role,
1779
+ createdAt: messages.createdAt,
1780
+ })
1781
+ .from(messages)
1782
+ .where(
1783
+ and(
1784
+ eq(messages.conversationId, target.conversationId),
1785
+ gt(messages.createdAt, boundaryCreatedAt),
1786
+ lte(messages.createdAt, target.createdAt),
1787
+ ),
1788
+ )
1789
+ .orderBy(asc(messages.createdAt))
1790
+ .all();
1791
+
1792
+ for (const row of gapRows) {
1793
+ if (row.role === "assistant" && !assistantIds.includes(row.id)) {
1794
+ assistantIds.push(row.id);
1795
+ }
1796
+ }
1797
+ }
1798
+
1799
+ // Sort by createdAt to ensure stable ordering.
1800
+ // Re-fetch createdAt for all collected IDs so the sort is accurate.
1801
+ if (assistantIds.length <= 1) return assistantIds;
1802
+
1803
+ const idSet = new Set(assistantIds);
1804
+ const sorted = db
1805
+ .select({ id: messages.id, createdAt: messages.createdAt })
1806
+ .from(messages)
1807
+ .where(
1808
+ and(
1809
+ eq(messages.conversationId, target.conversationId),
1810
+ inArray(messages.id, [...idSet]),
1811
+ ),
1812
+ )
1813
+ .orderBy(asc(messages.createdAt))
1814
+ .all();
1815
+
1816
+ return sorted.map((r) => r.id);
1817
+ }