@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
@@ -0,0 +1,125 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getConversationsDir } from "../util/platform.js";
5
+
6
+ function getConversationDirTimestamp(createdAtMs: number): string {
7
+ return new Date(createdAtMs).toISOString().replace(/:/g, "-");
8
+ }
9
+
10
+ export function getLegacyConversationDirName(
11
+ id: string,
12
+ createdAtMs: number,
13
+ ): string {
14
+ return `${id}_${getConversationDirTimestamp(createdAtMs)}`;
15
+ }
16
+
17
+ /**
18
+ * Build a filesystem-safe directory name for a conversation.
19
+ * Format: `{isoDate}_{id}` where colons in the ISO date are replaced with
20
+ * hyphens so the name is valid on all platforms (Windows forbids colons).
21
+ */
22
+ export function getConversationDirName(
23
+ id: string,
24
+ createdAtMs: number,
25
+ ): string {
26
+ return `${getConversationDirTimestamp(createdAtMs)}_${id}`;
27
+ }
28
+
29
+ /**
30
+ * Return the absolute path to a conversation's timestamp-first disk-view
31
+ * directory.
32
+ */
33
+ export function getConversationDirPath(
34
+ id: string,
35
+ createdAtMs: number,
36
+ ): string {
37
+ return join(getConversationsDir(), getConversationDirName(id, createdAtMs));
38
+ }
39
+
40
+ export function getLegacyConversationDirPath(
41
+ id: string,
42
+ createdAtMs: number,
43
+ ): string {
44
+ return join(
45
+ getConversationsDir(),
46
+ getLegacyConversationDirName(id, createdAtMs),
47
+ );
48
+ }
49
+
50
+ export interface ResolvedConversationDirectoryPaths {
51
+ canonicalDirPath: string;
52
+ canonicalDirName: string;
53
+ legacyDirPath: string;
54
+ legacyDirName: string;
55
+ resolvedDirPath: string;
56
+ resolvedDirName: string;
57
+ hasCanonicalDir: boolean;
58
+ hasLegacyDir: boolean;
59
+ }
60
+
61
+ export function resolveConversationDirectoryPaths(
62
+ id: string,
63
+ createdAtMs: number,
64
+ conversationsDir: string = getConversationsDir(),
65
+ ): ResolvedConversationDirectoryPaths {
66
+ const canonicalDirName = getConversationDirName(id, createdAtMs);
67
+ const canonicalDirPath = join(conversationsDir, canonicalDirName);
68
+ const hasCanonicalDir = existsSync(canonicalDirPath);
69
+
70
+ const legacyDirName = getLegacyConversationDirName(id, createdAtMs);
71
+ const legacyDirPath = join(conversationsDir, legacyDirName);
72
+ const hasLegacyDir = existsSync(legacyDirPath);
73
+
74
+ const resolvedDirPath = hasCanonicalDir
75
+ ? canonicalDirPath
76
+ : hasLegacyDir
77
+ ? legacyDirPath
78
+ : canonicalDirPath;
79
+ const resolvedDirName = hasCanonicalDir
80
+ ? canonicalDirName
81
+ : hasLegacyDir
82
+ ? legacyDirName
83
+ : canonicalDirName;
84
+
85
+ return {
86
+ canonicalDirPath,
87
+ canonicalDirName,
88
+ legacyDirPath,
89
+ legacyDirName,
90
+ resolvedDirPath,
91
+ resolvedDirName,
92
+ hasCanonicalDir,
93
+ hasLegacyDir,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Resolve the active conversation directory path:
99
+ * 1) prefer timestamp-first when it exists;
100
+ * 2) otherwise reuse legacy sibling when present;
101
+ * 3) otherwise fall back to timestamp-first as the creation target.
102
+ */
103
+ export function getResolvedConversationDirPath(
104
+ id: string,
105
+ createdAtMs: number,
106
+ ): string {
107
+ return resolveConversationDirectoryPaths(id, createdAtMs).resolvedDirPath;
108
+ }
109
+
110
+ export function getResolvedConversationDirName(
111
+ id: string,
112
+ createdAtMs: number,
113
+ ): string {
114
+ return resolveConversationDirectoryPaths(id, createdAtMs).resolvedDirName;
115
+ }
116
+
117
+ export function getConversationAttachmentsDirPath(
118
+ conversationId: string,
119
+ createdAtMs: number,
120
+ ): string {
121
+ return join(
122
+ getResolvedConversationDirPath(conversationId, createdAtMs),
123
+ "attachments",
124
+ );
125
+ }
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Write-through disk view for conversations.
3
+ *
4
+ * Projects conversation metadata, messages, and attachments to a browsable
5
+ * filesystem layout under ~/.vellum/workspace/conversations/. This enables
6
+ * the assistant to search/read/manipulate conversation data using standard
7
+ * file tools.
8
+ *
9
+ * All disk writes are best-effort — failures are logged but never thrown,
10
+ * so the disk view cannot break DB operations.
11
+ */
12
+
13
+ import {
14
+ appendFileSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ rmSync,
18
+ writeFileSync,
19
+ } from "node:fs";
20
+ import { basename, dirname, extname, join } from "node:path";
21
+
22
+ import { getLogger } from "../util/logger.js";
23
+ import {
24
+ getAttachmentContent,
25
+ getAttachmentMetadataForMessage,
26
+ getFilePathForAttachment,
27
+ } from "./attachments-store.js";
28
+ import {
29
+ getConversation,
30
+ getMessageById,
31
+ getMessages,
32
+ } from "./conversation-crud.js";
33
+ import {
34
+ getConversationDirName,
35
+ getConversationDirPath,
36
+ getLegacyConversationDirPath,
37
+ getResolvedConversationDirPath,
38
+ } from "./conversation-directories.js";
39
+
40
+ const log = getLogger("conversation-disk-view");
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Directory helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export {
47
+ getConversationDirName,
48
+ getConversationDirPath,
49
+ getResolvedConversationDirPath,
50
+ };
51
+
52
+ function ensureConversationDirPath(id: string, createdAtMs: number): string {
53
+ const dirPath = getResolvedConversationDirPath(id, createdAtMs);
54
+ mkdirSync(dirPath, { recursive: true });
55
+ return dirPath;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Write operations
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * Create the conversation directory and write the initial meta.json.
64
+ */
65
+ export function initConversationDir(conv: {
66
+ id: string;
67
+ title: string | null;
68
+ createdAt: number;
69
+ conversationType: string;
70
+ originChannel: string | null;
71
+ }): void {
72
+ try {
73
+ const dirPath = ensureConversationDirPath(conv.id, conv.createdAt);
74
+
75
+ const meta = {
76
+ id: conv.id,
77
+ title: conv.title,
78
+ type: conv.conversationType,
79
+ channel: conv.originChannel,
80
+ createdAt: new Date(conv.createdAt).toISOString(),
81
+ updatedAt: new Date(conv.createdAt).toISOString(),
82
+ };
83
+
84
+ writeFileSync(
85
+ join(dirPath, "meta.json"),
86
+ JSON.stringify(meta, null, 2) + "\n",
87
+ );
88
+ } catch (err) {
89
+ log.warn(
90
+ { err, conversationId: conv.id },
91
+ "Failed to init conversation dir",
92
+ );
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Rewrite meta.json with updated fields.
98
+ */
99
+ export function updateMetaFile(conv: {
100
+ id: string;
101
+ title: string | null;
102
+ createdAt: number;
103
+ updatedAt: number;
104
+ conversationType: string;
105
+ originChannel: string | null;
106
+ }): void {
107
+ try {
108
+ const dirPath = ensureConversationDirPath(conv.id, conv.createdAt);
109
+
110
+ const meta = {
111
+ id: conv.id,
112
+ title: conv.title,
113
+ type: conv.conversationType,
114
+ channel: conv.originChannel,
115
+ createdAt: new Date(conv.createdAt).toISOString(),
116
+ updatedAt: new Date(conv.updatedAt).toISOString(),
117
+ };
118
+
119
+ writeFileSync(
120
+ join(dirPath, "meta.json"),
121
+ JSON.stringify(meta, null, 2) + "\n",
122
+ );
123
+ } catch (err) {
124
+ log.warn({ err, conversationId: conv.id }, "Failed to update meta file");
125
+ }
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Content flattening
130
+ // ---------------------------------------------------------------------------
131
+
132
+ interface ContentBlock {
133
+ type: string;
134
+ text?: string;
135
+ name?: string;
136
+ input?: unknown;
137
+ content?: unknown;
138
+ }
139
+
140
+ interface FlattenedContent {
141
+ content: string;
142
+ toolCalls: Array<{ name: string; input: unknown }>;
143
+ toolResults: Array<{ content: unknown }>;
144
+ }
145
+
146
+ /**
147
+ * Parse the message `content` JSON string (ContentBlock[]) and extract
148
+ * text, tool_use, and tool_result blocks into flat fields.
149
+ */
150
+ export function flattenContentBlocks(rawContent: string): FlattenedContent {
151
+ const result: FlattenedContent = {
152
+ content: "",
153
+ toolCalls: [],
154
+ toolResults: [],
155
+ };
156
+
157
+ let blocks: ContentBlock[];
158
+ try {
159
+ const parsed = JSON.parse(rawContent);
160
+ if (!Array.isArray(parsed)) {
161
+ // Plain text content (not block array)
162
+ return { ...result, content: rawContent };
163
+ }
164
+ blocks = parsed;
165
+ } catch {
166
+ // Not valid JSON — treat as plain text
167
+ return { ...result, content: rawContent };
168
+ }
169
+
170
+ const textParts: string[] = [];
171
+
172
+ for (const block of blocks) {
173
+ switch (block.type) {
174
+ case "text":
175
+ if (typeof block.text === "string") {
176
+ textParts.push(block.text);
177
+ }
178
+ break;
179
+ case "tool_use":
180
+ if (typeof block.name === "string") {
181
+ result.toolCalls.push({ name: block.name, input: block.input });
182
+ }
183
+ break;
184
+ case "tool_result":
185
+ result.toolResults.push({ content: block.content });
186
+ break;
187
+ // Skip "image" and "file" blocks — represented via attachments
188
+ }
189
+ }
190
+
191
+ result.content = textParts.join("\n");
192
+ return result;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Attachment projection
197
+ // ---------------------------------------------------------------------------
198
+
199
+ /**
200
+ * Resolve a unique filename within a directory, handling collisions by
201
+ * appending a suffix (e.g., `photo-2.png`, `photo-3.png`).
202
+ */
203
+ export function resolveUniqueFilename(dir: string, filename: string): string {
204
+ const sanitized = basename(filename);
205
+ if (!existsSync(join(dir, sanitized))) return sanitized;
206
+
207
+ const ext = extname(sanitized);
208
+ const base = basename(sanitized, ext);
209
+ let counter = 2;
210
+ let candidate: string;
211
+ do {
212
+ candidate = `${base}-${counter}${ext}`;
213
+ counter++;
214
+ } while (existsSync(join(dir, candidate)));
215
+
216
+ return candidate;
217
+ }
218
+
219
+ /**
220
+ * Ensure an attachment is present in the conversation's attachments/
221
+ * subdirectory and return the filename recorded in the disk view.
222
+ */
223
+ function writeAttachmentFile(
224
+ conversationDirPath: string,
225
+ attachmentId: string,
226
+ originalFilename: string,
227
+ ): string | null {
228
+ try {
229
+ const attachDir = join(conversationDirPath, "attachments");
230
+ mkdirSync(attachDir, { recursive: true });
231
+
232
+ const existingPath = getFilePathForAttachment(attachmentId);
233
+ if (
234
+ existingPath &&
235
+ existsSync(existingPath) &&
236
+ dirname(existingPath) === attachDir
237
+ ) {
238
+ return basename(existingPath);
239
+ }
240
+
241
+ const content = getAttachmentContent(attachmentId);
242
+ if (!content) return null;
243
+
244
+ const resolvedName = resolveUniqueFilename(attachDir, originalFilename);
245
+ writeFileSync(join(attachDir, resolvedName), content);
246
+ return resolvedName;
247
+ } catch (err) {
248
+ log.warn(
249
+ { err, attachmentId, originalFilename },
250
+ "Failed to write attachment file to disk",
251
+ );
252
+ return null;
253
+ }
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Message sync
258
+ // ---------------------------------------------------------------------------
259
+
260
+ /**
261
+ * Read a message and its attachments from DB, flatten content, and append
262
+ * a JSONL line to `messages.jsonl` in the conversation's disk-view directory.
263
+ * Attachment filenames are recorded from the conversation's attachments/
264
+ * subdirectory, materializing legacy rows there only when needed.
265
+ *
266
+ * Requires `createdAtMs` of the conversation to resolve the directory path.
267
+ */
268
+ export function syncMessageToDisk(
269
+ conversationId: string,
270
+ messageId: string,
271
+ createdAtMs: number,
272
+ ): void {
273
+ try {
274
+ const message = getMessageById(messageId, conversationId);
275
+ if (!message) {
276
+ log.warn(
277
+ { conversationId, messageId },
278
+ "syncMessageToDisk: message not found",
279
+ );
280
+ return;
281
+ }
282
+
283
+ const dirPath = ensureConversationDirPath(conversationId, createdAtMs);
284
+ const { content, toolCalls, toolResults } = flattenContentBlocks(
285
+ message.content,
286
+ );
287
+
288
+ // Project attachments to disk
289
+ const attachmentMeta = getAttachmentMetadataForMessage(messageId);
290
+ const attachmentFilenames: string[] = [];
291
+ for (const att of attachmentMeta) {
292
+ const resolved = writeAttachmentFile(
293
+ dirPath,
294
+ att.id,
295
+ att.originalFilename,
296
+ );
297
+ if (resolved) {
298
+ attachmentFilenames.push(resolved);
299
+ }
300
+ }
301
+
302
+ // Build JSONL record
303
+ const record: Record<string, unknown> = {
304
+ role: message.role,
305
+ ts: new Date(message.createdAt).toISOString(),
306
+ };
307
+
308
+ if (content) record.content = content;
309
+ if (toolCalls.length > 0) record.toolCalls = toolCalls;
310
+ if (toolResults.length > 0) record.toolResults = toolResults;
311
+ if (attachmentFilenames.length > 0)
312
+ record.attachments = attachmentFilenames;
313
+
314
+ appendFileSync(
315
+ join(dirPath, "messages.jsonl"),
316
+ JSON.stringify(record) + "\n",
317
+ );
318
+ } catch (err) {
319
+ log.warn(
320
+ { err, conversationId, messageId },
321
+ "Failed to sync message to disk",
322
+ );
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Rebuild a single conversation's disk view from current DB state.
328
+ *
329
+ * This rewrites append-only `messages.jsonl` and replays all persisted messages
330
+ * in DB order so disk data matches post-mutation state (e.g., after assistant-
331
+ * message consolidation). Existing attachment files are preserved to avoid
332
+ * losing file-backed rows where base64 payloads were already compacted out.
333
+ */
334
+ export function rebuildConversationDiskViewFromDbState(
335
+ conversationId: string,
336
+ ): void {
337
+ try {
338
+ const conv = getConversation(conversationId);
339
+ if (!conv) {
340
+ log.warn(
341
+ { conversationId },
342
+ "rebuildConversationDiskViewFromDbState: conversation not found",
343
+ );
344
+ return;
345
+ }
346
+
347
+ const dirPath = ensureConversationDirPath(conversationId, conv.createdAt);
348
+ const messagesPath = join(dirPath, "messages.jsonl");
349
+
350
+ rmSync(messagesPath, { force: true });
351
+ writeFileSync(messagesPath, "");
352
+ // Preserve attachment files: many attachment rows are file-backed with
353
+ // data_base64 cleared, so deleting attachments/ can make content
354
+ // unrecoverable for replay.
355
+ mkdirSync(join(dirPath, "attachments"), { recursive: true });
356
+
357
+ const convMessages = getMessages(conversationId);
358
+ for (const msg of convMessages) {
359
+ syncMessageToDisk(conversationId, msg.id, conv.createdAt);
360
+ }
361
+
362
+ updateMetaFile(conv);
363
+ } catch (err) {
364
+ log.warn(
365
+ { err, conversationId },
366
+ "Failed to rebuild conversation disk view from DB state",
367
+ );
368
+ }
369
+ }
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // Removal
373
+ // ---------------------------------------------------------------------------
374
+
375
+ /**
376
+ * Remove a conversation's disk-view directory entirely.
377
+ */
378
+ export function removeConversationDir(id: string, createdAtMs: number): void {
379
+ try {
380
+ const dirPaths = new Set([
381
+ getConversationDirPath(id, createdAtMs),
382
+ getLegacyConversationDirPath(id, createdAtMs),
383
+ ]);
384
+ for (const dirPath of dirPaths) {
385
+ rmSync(dirPath, { recursive: true, force: true });
386
+ }
387
+ } catch (err) {
388
+ log.warn({ err, conversationId: id }, "Failed to remove conversation dir");
389
+ }
390
+ }
@@ -10,6 +10,7 @@
10
10
  import { eq } from "drizzle-orm";
11
11
  import { v4 as uuid } from "uuid";
12
12
 
13
+ import { initConversationDir } from "./conversation-disk-view.js";
13
14
  import { GENERATING_TITLE } from "./conversation-title-service.js";
14
15
  import { getDb } from "./db.js";
15
16
  import { conversationKeys, conversations } from "./schema.js";
@@ -138,7 +139,7 @@ export function getOrCreateConversation(
138
139
  const db = getDb();
139
140
  const conversationType = opts?.conversationType ?? "standard";
140
141
 
141
- return db.transaction((tx) => {
142
+ const result = db.transaction((tx) => {
142
143
  const existing = tx
143
144
  .select()
144
145
  .from(conversationKeys)
@@ -146,7 +147,7 @@ export function getOrCreateConversation(
146
147
  .get();
147
148
 
148
149
  if (existing) {
149
- return { conversationId: existing.conversationId, created: false };
150
+ return { conversationId: existing.conversationId, created: false as const };
150
151
  }
151
152
 
152
153
  // Check if the conversationKey itself is an existing conversation ID.
@@ -167,18 +168,19 @@ export function getOrCreateConversation(
167
168
  createdAt: Date.now(),
168
169
  })
169
170
  .run();
170
- return { conversationId: existingConversation.id, created: false };
171
+ return { conversationId: existingConversation.id, created: false as const };
171
172
  }
172
173
 
173
174
  const now = Date.now();
174
175
  const conversationId = uuid();
176
+ const title = GENERATING_TITLE;
175
177
  const memoryScopeId =
176
178
  conversationType === "private" ? `private:${conversationId}` : "default";
177
179
 
178
180
  tx.insert(conversations)
179
181
  .values({
180
182
  id: conversationId,
181
- title: GENERATING_TITLE,
183
+ title,
182
184
  createdAt: now,
183
185
  updatedAt: now,
184
186
  totalInputTokens: 0,
@@ -201,6 +203,16 @@ export function getOrCreateConversation(
201
203
  })
202
204
  .run();
203
205
 
204
- return { conversationId, created: true };
206
+ return {
207
+ conversationId,
208
+ created: true as const,
209
+ conversation: { id: conversationId, title, createdAt: now, conversationType },
210
+ };
205
211
  });
212
+
213
+ if (result.created) {
214
+ initConversationDir({ ...result.conversation, originChannel: null });
215
+ }
216
+
217
+ return { conversationId: result.conversationId, created: result.created };
206
218
  }
@@ -101,7 +101,11 @@ export function isLastUserMessageToolResult(conversationId: string): boolean {
101
101
  parsed.every(
102
102
  (block: Record<string, unknown>) =>
103
103
  block.type === "tool_result" ||
104
- block.type === "web_search_tool_result",
104
+ block.type === "web_search_tool_result" ||
105
+ (block.type === "text" &&
106
+ typeof block.text === "string" &&
107
+ block.text.startsWith("<system_notice>") &&
108
+ block.text.endsWith("</system_notice>")),
105
109
  )
106
110
  ) {
107
111
  return true;
@@ -10,6 +10,7 @@
10
10
  import { getConfig } from "../config/loader.js";
11
11
  import { getConfiguredProvider } from "../providers/provider-send-message.js";
12
12
  import type { Provider } from "../providers/types.js";
13
+ import { runBtwSidechain } from "../runtime/btw-sidechain.js";
13
14
  import { getLogger } from "../util/logger.js";
14
15
  import { truncate } from "../util/truncate.js";
15
16
  import {
@@ -129,30 +130,16 @@ export async function generateAndPersistConversationTitle(
129
130
 
130
131
  const config = getConfig();
131
132
  const prompt = buildTitlePrompt(context, userMessage, assistantResponse);
132
- const timeoutSignal = AbortSignal.timeout(10_000);
133
- const combinedSignal = signal
134
- ? AbortSignal.any([signal, timeoutSignal])
135
- : timeoutSignal;
136
-
137
- const response = await provider.sendMessage(
138
- [{ role: "user", content: [{ type: "text", text: prompt }] }],
139
- [],
140
- undefined,
141
- {
142
- config: {
143
- max_tokens: config.daemon.titleGenerationMaxTokens,
144
- modelIntent: "latency-optimized",
145
- },
146
- signal: combinedSignal,
147
- },
148
- );
149
- const textBlock = response.content.find((b) => b.type === "text");
150
- if (textBlock && textBlock.type === "text") {
151
- let title = normalizeTitle(textBlock.text);
152
- if (!title) {
153
- title = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
154
- }
155
-
133
+ const result = await runBtwSidechain({
134
+ content: prompt,
135
+ provider,
136
+ maxTokens: config.daemon.titleGenerationMaxTokens,
137
+ modelIntent: "latency-optimized",
138
+ signal,
139
+ timeoutMs: 10_000,
140
+ });
141
+ const title = normalizeTitle(result.text);
142
+ if (title) {
156
143
  // Re-check replaceability before persisting (race guard)
157
144
  const current = getConversation(conversationId);
158
145
  if (current && !isReplaceableTitle(current.title)) {
@@ -246,31 +233,16 @@ export async function regenerateConversationTitle(
246
233
 
247
234
  const prompt = buildRegenerationPrompt(recentMessages);
248
235
  const config = getConfig();
249
- const timeoutSignal = AbortSignal.timeout(10_000);
250
- const combinedSignal = signal
251
- ? AbortSignal.any([signal, timeoutSignal])
252
- : timeoutSignal;
253
-
254
- const response = await provider.sendMessage(
255
- [{ role: "user", content: [{ type: "text", text: prompt }] }],
256
- [],
257
- undefined,
258
- {
259
- config: {
260
- max_tokens: config.daemon.titleGenerationMaxTokens,
261
- modelIntent: "latency-optimized",
262
- },
263
- signal: combinedSignal,
264
- },
265
- );
266
-
267
- const textBlock = response.content.find((b) => b.type === "text");
268
- if (textBlock && textBlock.type === "text") {
269
- const title = normalizeTitle(textBlock.text);
270
- if (!title) {
271
- return { title: conversation.title ?? UNTITLED_FALLBACK, updated: false };
272
- }
273
-
236
+ const result = await runBtwSidechain({
237
+ content: prompt,
238
+ provider,
239
+ maxTokens: config.daemon.titleGenerationMaxTokens,
240
+ modelIntent: "latency-optimized",
241
+ signal,
242
+ timeoutMs: 10_000,
243
+ });
244
+ const title = normalizeTitle(result.text);
245
+ if (title) {
274
246
  // Re-check isAutoTitle before persisting (race guard against manual rename)
275
247
  const current = getConversation(conversationId);
276
248
  if (!current || !current.isAutoTitle) {