@vellumai/assistant 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -37,6 +37,7 @@ import {
37
37
  migrateAssistantContactMetadata,
38
38
  migrateBackfillContactInteractionStats,
39
39
  migrateBackfillGuardianPrincipalId,
40
+ migrateBackfillInlineAttachmentsToDisk,
40
41
  migrateBackfillUsageCacheAccounting,
41
42
  migrateCallSessionInviteMetadata,
42
43
  migrateCallSessionMode,
@@ -50,6 +51,7 @@ import {
50
51
  migrateContactsAssistantId,
51
52
  migrateContactsNotesColumn,
52
53
  migrateContactsRolePrincipal,
54
+ migrateConversationForkLineage,
53
55
  migrateConversationsThreadTypeIndex,
54
56
  migrateCreateThreadStartersTable,
55
57
  migrateCreateTraceEventsTable,
@@ -78,11 +80,15 @@ import {
78
80
  migrateGuardianVerificationSessions,
79
81
  migrateInviteCodeHashColumn,
80
82
  migrateInviteContactId,
83
+ migrateLlmRequestLogMessageId,
84
+ migrateLlmRequestLogProvider,
81
85
  migrateMemoryItemSupersession,
82
86
  migrateMessagesFtsBackfill,
83
87
  migrateNormalizePhoneIdentities,
84
88
  migrateNotificationDeliveryThreadDecision,
85
89
  migrateOAuthAppsClientSecretPath,
90
+ migrateOAuthProvidersDisplayMetadata,
91
+ migrateOAuthProvidersManagedServiceConfigKey,
86
92
  migrateOAuthProvidersPingUrl,
87
93
  migrateReminderRoutingIntent,
88
94
  migrateRemindersToSchedules,
@@ -96,6 +102,7 @@ import {
96
102
  migrateRenameSequenceEnrollmentsThreadIdColumn,
97
103
  migrateRenameSequenceStepsReplyKey,
98
104
  migrateRenameSourceSessionIdColumn,
105
+ migrateRenameThreadStartersCheckpoints,
99
106
  migrateRenameThreadStartersTable,
100
107
  migrateRenameVerificationSessionIdColumn,
101
108
  migrateRenameVerificationTable,
@@ -447,6 +454,9 @@ export function initializeDb(): void {
447
454
  // 77. Rename thread_starters → conversation_starters table and indexes
448
455
  migrateRenameThreadStartersTable(database);
449
456
 
457
+ // 77b. Rename checkpoint keys from thread_starters: → conversation_starters: prefix
458
+ migrateRenameThreadStartersCheckpoints(database);
459
+
450
460
  // 78. Lifecycle events table for app_open / hatch telemetry
451
461
  createLifecycleEventsTable(database);
452
462
 
@@ -456,6 +466,24 @@ export function initializeDb(): void {
456
466
  // 80. Trace events table for persistent trace/activity storage across sessions
457
467
  migrateCreateTraceEventsTable(database);
458
468
 
469
+ // 81. Add managed_service_config_key column to oauth_providers
470
+ migrateOAuthProvidersManagedServiceConfigKey(database);
471
+
472
+ // 81b. Add display metadata columns to oauth_providers (display_name, description, dashboard_url, etc.)
473
+ migrateOAuthProvidersDisplayMetadata(database);
474
+
475
+ // 82. Add message_id column to llm_request_logs for per-message LLM context lookup
476
+ migrateLlmRequestLogMessageId(database);
477
+
478
+ // 82b. Add provider column to llm_request_logs for runtime provider lookup
479
+ migrateLlmRequestLogProvider(database);
480
+
481
+ // 83. Backfill existing inline (base64-in-DB) attachments to on-disk storage
482
+ migrateBackfillInlineAttachmentsToDisk(database);
483
+
484
+ // 84. Add nullable conversation fork lineage columns and parent lookup index
485
+ migrateConversationForkLineage(database);
486
+
459
487
  validateMigrationState(database);
460
488
 
461
489
  if (process.env.BUN_TEST === "1") {
@@ -203,7 +203,14 @@ function getCachedOrCreate<T extends EmbeddingBackend>(
203
203
  return instance;
204
204
  }
205
205
 
206
- /** Check if a backend instance already exists in the cache. */
206
+ /**
207
+ * Look up a previously cached backend instance. Returns undefined when no
208
+ * cached entry exists. Used as a fallback when a provider key lookup
209
+ * returns undefined — a transient credential-store outage should not
210
+ * disable a provider whose backend is already warmed in memory. Explicit
211
+ * key deletion triggers `clearEmbeddingBackendCache()` which empties the
212
+ * cache, so a stale backend is never returned after intentional removal.
213
+ */
207
214
  function getCached(
208
215
  provider: string,
209
216
  model: string,
@@ -259,9 +266,6 @@ export async function selectEmbeddingBackend(
259
266
  };
260
267
  }
261
268
  if (requested === "ollama") {
262
- // Check cache first to avoid unnecessary async key fetch on cache hits
263
- const cached = getCached("ollama", config.memory.embeddings.ollamaModel);
264
- if (cached) return { backend: cached, reason: null };
265
269
  const ollamaKey = (await getProviderKeyAsync("ollama")) ?? undefined;
266
270
  return {
267
271
  backend: getCachedOrCreate(
@@ -298,14 +302,17 @@ export async function selectEmbeddingBackend(
298
302
  reason: null,
299
303
  };
300
304
  case "openai": {
301
- // Check cache first to avoid unnecessary async key fetch on cache hits
302
- const cachedOpenai = getCached(
303
- "openai",
304
- config.memory.embeddings.openaiModel,
305
- );
306
- if (cachedOpenai) return { backend: cachedOpenai, reason: null };
307
305
  const openaiKey = await getProviderKeyAsync("openai");
308
- if (!openaiKey) continue;
306
+ if (!openaiKey) {
307
+ // Preserve cached backend on transient credential-store failures.
308
+ // Explicit key deletion clears the cache via clearEmbeddingBackendCache().
309
+ const cached = getCached(
310
+ "openai",
311
+ config.memory.embeddings.openaiModel,
312
+ );
313
+ if (cached) return { backend: cached, reason: null };
314
+ continue;
315
+ }
309
316
  return {
310
317
  backend: getCachedOrCreate(
311
318
  "openai",
@@ -320,15 +327,16 @@ export async function selectEmbeddingBackend(
320
327
  };
321
328
  }
322
329
  case "gemini": {
323
- // Check cache first to avoid unnecessary async key fetch on cache hits
324
- const cachedGemini = getCached(
325
- "gemini",
326
- config.memory.embeddings.geminiModel,
327
- geminiCacheExtras(config),
328
- );
329
- if (cachedGemini) return { backend: cachedGemini, reason: null };
330
330
  const geminiKey = await getProviderKeyAsync("gemini");
331
- if (!geminiKey) continue;
331
+ if (!geminiKey) {
332
+ const cached = getCached(
333
+ "gemini",
334
+ config.memory.embeddings.geminiModel,
335
+ geminiCacheExtras(config),
336
+ );
337
+ if (cached) return { backend: cached, reason: null };
338
+ continue;
339
+ }
332
340
  return {
333
341
  backend: getCachedOrCreate(
334
342
  "gemini",
@@ -348,12 +356,6 @@ export async function selectEmbeddingBackend(
348
356
  };
349
357
  }
350
358
  case "ollama": {
351
- // Check cache first to avoid unnecessary async key fetch on cache hits
352
- const cachedOllama = getCached(
353
- "ollama",
354
- config.memory.embeddings.ollamaModel,
355
- );
356
- if (cachedOllama) return { backend: cachedOllama, reason: null };
357
359
  if (!(await isOllamaConfigured(config))) continue;
358
360
  const ollamaKey = (await getProviderKeyAsync("ollama")) ?? undefined;
359
361
  return {
@@ -570,15 +572,6 @@ async function selectFallbackBackends(
570
572
  if (provider === exclude) continue;
571
573
  switch (provider) {
572
574
  case "openai": {
573
- // Check cache first to avoid unnecessary async key fetch on cache hits
574
- const cachedOpenai = getCached(
575
- "openai",
576
- config.memory.embeddings.openaiModel,
577
- );
578
- if (cachedOpenai) {
579
- backends.push(cachedOpenai);
580
- break;
581
- }
582
575
  const openaiKey = await getProviderKeyAsync("openai");
583
576
  if (openaiKey) {
584
577
  backends.push(
@@ -592,20 +585,17 @@ async function selectFallbackBackends(
592
585
  ),
593
586
  ),
594
587
  );
588
+ } else {
589
+ // Preserve cached backend on transient credential-store failures.
590
+ const cached = getCached(
591
+ "openai",
592
+ config.memory.embeddings.openaiModel,
593
+ );
594
+ if (cached) backends.push(cached);
595
595
  }
596
596
  break;
597
597
  }
598
598
  case "gemini": {
599
- // Check cache first to avoid unnecessary async key fetch on cache hits
600
- const cachedGemini = getCached(
601
- "gemini",
602
- config.memory.embeddings.geminiModel,
603
- geminiCacheExtras(config),
604
- );
605
- if (cachedGemini) {
606
- backends.push(cachedGemini);
607
- break;
608
- }
609
599
  const geminiKey = await getProviderKeyAsync("gemini");
610
600
  if (geminiKey) {
611
601
  backends.push(
@@ -624,19 +614,18 @@ async function selectFallbackBackends(
624
614
  geminiCacheExtras(config),
625
615
  ),
626
616
  );
617
+ } else {
618
+ // Preserve cached backend on transient credential-store failures.
619
+ const cached = getCached(
620
+ "gemini",
621
+ config.memory.embeddings.geminiModel,
622
+ geminiCacheExtras(config),
623
+ );
624
+ if (cached) backends.push(cached);
627
625
  }
628
626
  break;
629
627
  }
630
628
  case "ollama": {
631
- // Check cache first to avoid unnecessary async key fetch on cache hits
632
- const cachedOllama = getCached(
633
- "ollama",
634
- config.memory.embeddings.ollamaModel,
635
- );
636
- if (cachedOllama) {
637
- backends.push(cachedOllama);
638
- break;
639
- }
640
629
  if (await isOllamaConfigured(config)) {
641
630
  const ollamaKey = (await getProviderKeyAsync("ollama")) ?? undefined;
642
631
  backends.push(
@@ -14,7 +14,9 @@ describe("GeminiEmbeddingBackend", () => {
14
14
  let mockFetch: ReturnType<typeof mock>;
15
15
 
16
16
  beforeEach(() => {
17
- mockFetch = mock(() => Promise.resolve(makeSuccessResponse([0.1, 0.2, 0.3])));
17
+ mockFetch = mock(() =>
18
+ Promise.resolve(makeSuccessResponse([0.1, 0.2, 0.3])),
19
+ );
18
20
  globalThis.fetch = mockFetch as unknown as typeof fetch;
19
21
  });
20
22
 
@@ -186,9 +188,7 @@ describe("GeminiEmbeddingBackend", () => {
186
188
  describe("error handling", () => {
187
189
  test("throws on non-OK response", async () => {
188
190
  mockFetch = mock(() =>
189
- Promise.resolve(
190
- new Response("Internal Server Error", { status: 500 }),
191
- ),
191
+ Promise.resolve(new Response("Internal Server Error", { status: 500 })),
192
192
  );
193
193
  globalThis.fetch = mockFetch as unknown as typeof fetch;
194
194
 
@@ -66,9 +66,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
66
66
  const texts = inputs.map((i) => {
67
67
  const n = normalizeEmbeddingInput(i);
68
68
  if (n.type !== "text") {
69
- throw new Error(
70
- "Local embedding backend only supports text inputs",
71
- );
69
+ throw new Error("Local embedding backend only supports text inputs");
72
70
  }
73
71
  return n.text;
74
72
  });
@@ -34,9 +34,7 @@ export class OllamaEmbeddingBackend implements EmbeddingBackend {
34
34
  const texts = inputs.map((i) => {
35
35
  const n = normalizeEmbeddingInput(i);
36
36
  if (n.type !== "text") {
37
- throw new Error(
38
- "Ollama embedding backend only supports text inputs",
39
- );
37
+ throw new Error("Ollama embedding backend only supports text inputs");
40
38
  }
41
39
  return n.text;
42
40
  });
@@ -26,9 +26,7 @@ export class OpenAIEmbeddingBackend implements EmbeddingBackend {
26
26
  const texts = inputs.map((i) => {
27
27
  const n = normalizeEmbeddingInput(i);
28
28
  if (n.type !== "text") {
29
- throw new Error(
30
- "OpenAI embedding backend only supports text inputs",
31
- );
29
+ throw new Error("OpenAI embedding backend only supports text inputs");
32
30
  }
33
31
  return n.text;
34
32
  });
@@ -9,7 +9,7 @@ import { getDb } from "./db.js";
9
9
  import { selectedBackendSupportsMultimodal } from "./embedding-backend.js";
10
10
  import { enqueueMemoryJob } from "./jobs-store.js";
11
11
  import {
12
- extractMediaBlocks,
12
+ extractMediaBlockMeta,
13
13
  extractTextFromStoredMessageContent,
14
14
  } from "./message-content.js";
15
15
  import { memorySegments } from "./schema.js";
@@ -71,16 +71,18 @@ export async function indexMessageNow(
71
71
  input.role === "user" ||
72
72
  (input.role === "assistant" && config.extraction.extractFromAssistant);
73
73
  // Check if the message has any image blocks before probing the backend.
74
- // extractMediaBlocks is synchronous and cheap, while
75
- // selectedBackendSupportsMultimodal requires async key resolution that
76
- // would add unnecessary latency for text-only messages.
77
- const candidateMediaBlocks = extractMediaBlocks(input.content).filter(
74
+ // extractMediaBlockMeta is synchronous and lightweight — it detects image
75
+ // blocks without decoding base64 data into Buffers, avoiding CPU/memory
76
+ // overhead for messages on non-multimodal backends.
77
+ // selectedBackendSupportsMultimodal requires async key resolution, so we
78
+ // skip it entirely for text-only messages.
79
+ const candidateMediaMeta = extractMediaBlockMeta(input.content).filter(
78
80
  (b) => b.type === "image",
79
81
  );
80
82
  const mediaBlocks =
81
- candidateMediaBlocks.length > 0 &&
83
+ candidateMediaMeta.length > 0 &&
82
84
  (await selectedBackendSupportsMultimodal(getConfig()))
83
- ? candidateMediaBlocks
85
+ ? candidateMediaMeta
84
86
  : [];
85
87
 
86
88
  // Wrap all segment inserts and job enqueues in a single transaction so they
@@ -135,6 +135,13 @@ function hasSemanticDensity(text: string): boolean {
135
135
 
136
136
  // ── LLM-powered extraction ────────────────────────────────────────────
137
137
 
138
+ // Budget for the extraction system prompt (in characters). This is a
139
+ // conservative estimate that fits comfortably within even small model
140
+ // context windows (latency-optimized models like Haiku). The remaining
141
+ // context budget is consumed by the user message, tool schema, and response
142
+ // tokens. ~6000 tokens ≈ 24 000 chars is a safe ceiling.
143
+ const EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET = 24_000;
144
+
138
145
  function buildExtractionSystemPrompt(
139
146
  existingItems: Array<{
140
147
  id: string;
@@ -144,16 +151,9 @@ function buildExtractionSystemPrompt(
144
151
  }>,
145
152
  messageRole: string,
146
153
  ): string {
147
- // Inject identity context so extracted memories use real names instead of
148
- // generic "User ..." labels.
149
- const identityContext = buildCoreIdentityContext();
150
-
151
- let prompt = "";
152
- if (identityContext) {
153
- prompt += `# Identity Context\n\n${identityContext}\n\n---\n\n`;
154
- }
155
-
156
- prompt += `You are a memory extraction system. Given a message from a conversation, extract structured memory items that would be valuable to remember for future interactions.
154
+ // Build the fixed instruction body first so we can measure it and allocate
155
+ // the remaining budget to identity context.
156
+ let instructions = `You are a memory extraction system. Given a message from a conversation, extract structured memory items that would be valuable to remember for future interactions.
157
157
 
158
158
  Extract items in these categories:
159
159
  - identity: Personal info (name, role, location, timezone, background), notable facts, relationships between people/teams/systems
@@ -183,22 +183,51 @@ Rules:
183
183
  - Only extract genuinely memorable information. Skip pleasantries, filler, and transient discussion.
184
184
  - Do NOT extract information about what tools the assistant used or what files it read — only extract substantive facts about the user, their projects, and their preferences.
185
185
  - Do NOT extract claims about actions the assistant performed, outcomes it achieved, or progress it reported (e.g., "I booked an appointment", "I sent the email"). Only extract facts stated by the user or from external sources — the assistant's self-reports are not reliable memory material.
186
+ - Do NOT extract raw code snippets, JSON fragments, YAML, configuration values, log output, or data structures. Only extract the human-readable meaning or intent behind such content, not the literal syntax.
186
187
  - Prefer fewer high-quality items over many low-quality ones.
187
188
  - If the message contains no memorable information, return an empty array.`;
188
189
 
189
190
  if (messageRole === "assistant") {
190
- prompt += `
191
+ instructions += `
191
192
 
192
193
  IMPORTANT: The message below is from the ASSISTANT, not the user. Do NOT attribute the assistant's own statements, feelings, self-descriptions, or introspection to the user. Only extract facts about the user, the world, or the project that the assistant is referencing or relaying — NOT the assistant's own identity, uncertainty, or behavior. If the assistant is simply talking about itself (e.g., introducing itself, expressing uncertainty about its own purpose), extract nothing.`;
193
194
  }
194
195
 
195
196
  if (existingItems.length > 0) {
196
- prompt += `\n\nExisting memory items (use these to identify supersession targets — set \`supersedes\` to the item ID if the new information replaces one of these):\n`;
197
+ instructions += `\n\nExisting memory items (use these to identify supersession targets — set \`supersedes\` to the item ID if the new information replaces one of these):\n`;
197
198
  for (const item of existingItems) {
198
- prompt += `- [${item.id}] (${item.kind}) ${item.subject}: ${item.statement}\n`;
199
+ instructions += `- [${item.id}] (${item.kind}) ${item.subject}: ${item.statement}\n`;
200
+ }
201
+ }
202
+
203
+ // Inject identity context so extracted memories use real names instead of
204
+ // generic "User ..." labels. Budget is dynamically computed: whatever
205
+ // remains after the fixed instructions fits within the system prompt
206
+ // ceiling, preventing oversized prompts from exceeding the provider input
207
+ // window (which would cause sendMessage to error and fall back to
208
+ // lower-quality pattern-based extraction).
209
+ const rawIdentityContext = buildCoreIdentityContext();
210
+
211
+ let prompt = "";
212
+ if (rawIdentityContext) {
213
+ // Reserve space for the wrapping text: "# Identity Context\n\n" + "\n\n---\n\n"
214
+ const wrapperOverhead = "# Identity Context\n\n\n\n---\n\n".length;
215
+ const identityBudget =
216
+ EXTRACTION_SYSTEM_PROMPT_CHAR_BUDGET -
217
+ instructions.length -
218
+ wrapperOverhead;
219
+
220
+ if (identityBudget > 0) {
221
+ const identityContext = truncate(
222
+ rawIdentityContext,
223
+ identityBudget,
224
+ "\n...[identity context truncated]",
225
+ );
226
+ prompt += `# Identity Context\n\n${identityContext}\n\n---\n\n`;
199
227
  }
200
228
  }
201
229
 
230
+ prompt += instructions;
202
231
  return prompt;
203
232
  }
204
233
 
@@ -169,7 +169,12 @@ async function generateStarters(scopeId: string): Promise<GeneratedStarter[]> {
169
169
  const now = new Date();
170
170
  const timeContext = `Current time: ${now.toLocaleString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", hour: "numeric", minute: "2-digit", hour12: true })}`;
171
171
 
172
- const identityContext = buildCoreIdentityContext();
172
+ // Truncate identity context to prevent oversized prompts when SOUL.md /
173
+ // IDENTITY.md / USER.md are large.
174
+ const rawIdentityContext = buildCoreIdentityContext();
175
+ const identityContext = rawIdentityContext
176
+ ? truncate(rawIdentityContext, 2000, "\n…[truncated]")
177
+ : null;
173
178
 
174
179
  const systemPrompt = `You are generating 4 conversation starters for a personal assistant app. These appear as clickable chips on the empty conversation page — the first thing the user sees when they open the app.
175
180
 
@@ -142,10 +142,7 @@ describe("embedMediaJob", () => {
142
142
  })
143
143
  .run();
144
144
 
145
- await embedMediaJob(
146
- makeJob({ assetId: "asset-registered" }),
147
- TEST_CONFIG,
148
- );
145
+ await embedMediaJob(makeJob({ assetId: "asset-registered" }), TEST_CONFIG);
149
146
  expect(embedAndUpsertCalls).toHaveLength(0);
150
147
  });
151
148
 
@@ -1,6 +1,11 @@
1
- import { and, eq, gte, lte } from "drizzle-orm";
1
+ import { and, eq, gte, inArray, isNull, lte } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
+ import {
5
+ getAssistantMessageIdsInTurn,
6
+ getMessageById,
7
+ messageMetadataSchema,
8
+ } from "./conversation-crud.js";
4
9
  import { getDb } from "./db.js";
5
10
  import { llmRequestLogs } from "./schema.js";
6
11
 
@@ -8,12 +13,16 @@ export function recordRequestLog(
8
13
  conversationId: string,
9
14
  requestPayload: string,
10
15
  responsePayload: string,
16
+ messageId?: string,
17
+ provider?: string,
11
18
  ): void {
12
19
  const db = getDb();
13
20
  db.insert(llmRequestLogs)
14
21
  .values({
15
22
  id: uuid(),
16
23
  conversationId,
24
+ messageId: messageId ?? null,
25
+ provider: provider ?? null,
17
26
  requestPayload,
18
27
  responsePayload,
19
28
  createdAt: Date.now(),
@@ -46,3 +55,93 @@ export function queryRequestLogs(
46
55
  .orderBy(llmRequestLogs.createdAt)
47
56
  .all();
48
57
  }
58
+
59
+ export function backfillMessageIdOnLogs(
60
+ conversationId: string,
61
+ messageId: string,
62
+ ): void {
63
+ const db = getDb();
64
+ db.update(llmRequestLogs)
65
+ .set({ messageId })
66
+ .where(
67
+ and(
68
+ eq(llmRequestLogs.conversationId, conversationId),
69
+ isNull(llmRequestLogs.messageId),
70
+ ),
71
+ )
72
+ .run();
73
+ }
74
+
75
+ /**
76
+ * Internal helper: query `llm_request_logs` for rows matching any of the
77
+ * given message IDs, ordered by `createdAt ASC`. Uses the existing
78
+ * `idx_llm_request_logs_message_id` index via `inArray`.
79
+ */
80
+ function selectLogsByMessageIds(messageIds: string[]): Array<{
81
+ id: string;
82
+ conversationId: string;
83
+ messageId: string | null;
84
+ provider: string | null;
85
+ requestPayload: string;
86
+ responsePayload: string;
87
+ createdAt: number;
88
+ }> {
89
+ if (messageIds.length === 0) return [];
90
+ const db = getDb();
91
+ return db
92
+ .select({
93
+ id: llmRequestLogs.id,
94
+ conversationId: llmRequestLogs.conversationId,
95
+ messageId: llmRequestLogs.messageId,
96
+ provider: llmRequestLogs.provider,
97
+ requestPayload: llmRequestLogs.requestPayload,
98
+ responsePayload: llmRequestLogs.responsePayload,
99
+ createdAt: llmRequestLogs.createdAt,
100
+ })
101
+ .from(llmRequestLogs)
102
+ .where(inArray(llmRequestLogs.messageId, messageIds))
103
+ .orderBy(llmRequestLogs.createdAt)
104
+ .all();
105
+ }
106
+
107
+ export function getRequestLogsByMessageId(messageId: string): Array<{
108
+ id: string;
109
+ conversationId: string;
110
+ messageId: string | null;
111
+ provider: string | null;
112
+ requestPayload: string;
113
+ responsePayload: string;
114
+ createdAt: number;
115
+ }> {
116
+ // Resolve all assistant message IDs in the same turn so the inspector
117
+ // shows every LLM call from the entire agent turn, not just the queried message.
118
+ const turnMessageIds = getAssistantMessageIdsInTurn(messageId);
119
+ const turnLogs = selectLogsByMessageIds(turnMessageIds);
120
+ if (turnLogs.length > 0) {
121
+ return turnLogs;
122
+ }
123
+
124
+ // Fork-source fallback: if no logs found for the turn, check whether
125
+ // the queried message was forked from a source and resolve that source's turn.
126
+ const message = getMessageById(messageId);
127
+ if (!message?.metadata) {
128
+ return [];
129
+ }
130
+
131
+ try {
132
+ const parsed = messageMetadataSchema.safeParse(
133
+ JSON.parse(message.metadata),
134
+ );
135
+ const sourceMessageId =
136
+ parsed.success && typeof parsed.data.forkSourceMessageId === "string"
137
+ ? parsed.data.forkSourceMessageId
138
+ : null;
139
+ if (!sourceMessageId || sourceMessageId === messageId) {
140
+ return [];
141
+ }
142
+ const sourceTurnIds = getAssistantMessageIdsInTurn(sourceMessageId);
143
+ return selectLogsByMessageIds(sourceTurnIds);
144
+ } catch {
145
+ return [];
146
+ }
147
+ }
@@ -261,6 +261,11 @@ export function addCoreColumns(database: DrizzleDb): void {
261
261
  } catch {
262
262
  /* already exists */
263
263
  }
264
+ try {
265
+ database.run(/*sql*/ `ALTER TABLE attachments ADD COLUMN source_path TEXT`);
266
+ } catch {
267
+ /* already exists */
268
+ }
264
269
 
265
270
  // cron_jobs
266
271
  try {
@@ -15,9 +15,9 @@ export function migrateScheduleOneShotRouting(database: DrizzleDb): void {
15
15
  const raw = getSqliteFrom(database);
16
16
 
17
17
  // Check if migration is already done by inspecting whether the status column exists
18
- const tableInfo = raw
19
- .query("PRAGMA table_info(cron_jobs)")
20
- .all() as Array<{ name: string }>;
18
+ const tableInfo = raw.query("PRAGMA table_info(cron_jobs)").all() as Array<{
19
+ name: string;
20
+ }>;
21
21
  const hasStatusColumn = tableInfo.some((col) => col.name === "status");
22
22
  if (hasStatusColumn) {
23
23
  // Ensure all indexes exist even if the column migration already ran