@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,375 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ afterAll,
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ mock,
11
+ test,
12
+ } from "bun:test";
13
+
14
+ const testDir = mkdtempSync(join(tmpdir(), "memory-observation-archive-test-"));
15
+ const dbPath = join(testDir, "test.db");
16
+
17
+ mock.module("../util/platform.js", () => ({
18
+ getDataDir: () => testDir,
19
+ isMacOS: () => process.platform === "darwin",
20
+ isLinux: () => process.platform === "linux",
21
+ isWindows: () => process.platform === "win32",
22
+ getPidPath: () => join(testDir, "test.pid"),
23
+ getDbPath: () => dbPath,
24
+ getLogPath: () => join(testDir, "test.log"),
25
+ ensureDataDir: () => {},
26
+ }));
27
+
28
+ mock.module("../util/logger.js", () => ({
29
+ getLogger: () =>
30
+ new Proxy({} as Record<string, unknown>, {
31
+ get: () => () => {},
32
+ }),
33
+ }));
34
+
35
+ import { eq } from "drizzle-orm";
36
+
37
+ import {
38
+ computeObservationContentHash,
39
+ getChunkByObservationId,
40
+ getObservation,
41
+ insertObservation,
42
+ type InsertObservationParams,
43
+ insertObservations,
44
+ } from "../memory/archive-store.js";
45
+ import { getDb, initializeDb, rawAll, resetDb } from "../memory/db.js";
46
+ import { claimMemoryJobs } from "../memory/jobs-store.js";
47
+ import { conversations, memoryChunks, messages } from "../memory/schema.js";
48
+
49
+ function removeTestDbFiles(): void {
50
+ rmSync(dbPath, { force: true });
51
+ rmSync(`${dbPath}-shm`, { force: true });
52
+ rmSync(`${dbPath}-wal`, { force: true });
53
+ }
54
+
55
+ // ── Helpers ─────────────────────────────────────────────────────────
56
+
57
+ function createConversation(id: string): void {
58
+ const db = getDb();
59
+ const now = Date.now();
60
+ db.insert(conversations)
61
+ .values({
62
+ id,
63
+ createdAt: now,
64
+ updatedAt: now,
65
+ })
66
+ .run();
67
+ }
68
+
69
+ function createMessage(id: string, conversationId: string): void {
70
+ const db = getDb();
71
+ db.insert(messages)
72
+ .values({
73
+ id,
74
+ conversationId,
75
+ role: "user",
76
+ content: "test message",
77
+ createdAt: Date.now(),
78
+ })
79
+ .run();
80
+ }
81
+
82
+ function getJobsByType(type: string) {
83
+ return rawAll<{
84
+ id: string;
85
+ type: string;
86
+ payload: string;
87
+ status: string;
88
+ }>(`SELECT id, type, payload, status FROM memory_jobs WHERE type = ?`, type);
89
+ }
90
+
91
+ // ── Setup ───────────────────────────────────────────────────────────
92
+
93
+ describe("memory observation archive store", () => {
94
+ beforeEach(() => {
95
+ resetDb();
96
+ removeTestDbFiles();
97
+ initializeDb();
98
+ });
99
+
100
+ afterEach(() => {
101
+ resetDb();
102
+ removeTestDbFiles();
103
+ });
104
+
105
+ afterAll(() => {
106
+ resetDb();
107
+ rmSync(testDir, { recursive: true, force: true });
108
+ });
109
+
110
+ // ── Row insertion ───────────────────────────────────────────────
111
+
112
+ describe("insertObservation", () => {
113
+ test("inserts observation row into memory_observations table", () => {
114
+ createConversation("conv-1");
115
+
116
+ const result = insertObservation({
117
+ conversationId: "conv-1",
118
+ role: "user",
119
+ content: "The user prefers dark mode",
120
+ });
121
+
122
+ expect(result.observationId).toBeTruthy();
123
+ expect(result.contentHash).toBeTruthy();
124
+
125
+ const obs = getObservation(result.observationId);
126
+ expect(obs).toBeDefined();
127
+ expect(obs!.conversationId).toBe("conv-1");
128
+ expect(obs!.role).toBe("user");
129
+ expect(obs!.content).toBe("The user prefers dark mode");
130
+ expect(obs!.modality).toBe("text");
131
+ expect(obs!.scopeId).toBe("default");
132
+ });
133
+
134
+ test("inserts associated chunk with correct content hash", () => {
135
+ createConversation("conv-1");
136
+
137
+ const result = insertObservation({
138
+ conversationId: "conv-1",
139
+ role: "user",
140
+ content: "The user lives in NYC",
141
+ });
142
+
143
+ expect(result.chunkId).toBeTruthy();
144
+
145
+ const chunk = getChunkByObservationId(result.observationId);
146
+ expect(chunk).toBeDefined();
147
+ expect(chunk!.content).toBe("The user lives in NYC");
148
+ expect(chunk!.contentHash).toBe(result.contentHash);
149
+ expect(chunk!.tokenEstimate).toBeGreaterThan(0);
150
+ expect(chunk!.scopeId).toBe("default");
151
+ });
152
+
153
+ test("respects optional params: scopeId, modality, source, messageId", () => {
154
+ createConversation("conv-1");
155
+ createMessage("msg-1", "conv-1");
156
+
157
+ const result = insertObservation({
158
+ conversationId: "conv-1",
159
+ messageId: "msg-1",
160
+ role: "assistant",
161
+ content: "Voice observation about weather",
162
+ scopeId: "custom-scope",
163
+ modality: "voice",
164
+ source: "phone",
165
+ });
166
+
167
+ const obs = getObservation(result.observationId);
168
+ expect(obs).toBeDefined();
169
+ expect(obs!.messageId).toBe("msg-1");
170
+ expect(obs!.scopeId).toBe("custom-scope");
171
+ expect(obs!.modality).toBe("voice");
172
+ expect(obs!.source).toBe("phone");
173
+ });
174
+
175
+ test("does not touch legacy memory tables", () => {
176
+ createConversation("conv-1");
177
+
178
+ insertObservation({
179
+ conversationId: "conv-1",
180
+ role: "user",
181
+ content: "A fact about the user",
182
+ });
183
+
184
+ // Verify no rows in legacy memory_segments or memory_items
185
+ const segments = rawAll<{ id: string }>(`SELECT id FROM memory_segments`);
186
+ const items = rawAll<{ id: string }>(`SELECT id FROM memory_items`);
187
+ expect(segments).toHaveLength(0);
188
+ expect(items).toHaveLength(0);
189
+ });
190
+ });
191
+
192
+ // ── Content hash idempotency ──────────────────────────────────
193
+
194
+ describe("content hash idempotency", () => {
195
+ test("duplicate content in same scope does not create a second chunk", () => {
196
+ createConversation("conv-1");
197
+
198
+ const result1 = insertObservation({
199
+ conversationId: "conv-1",
200
+ role: "user",
201
+ content: "The user likes cats",
202
+ });
203
+
204
+ const result2 = insertObservation({
205
+ conversationId: "conv-1",
206
+ role: "user",
207
+ content: "The user likes cats",
208
+ });
209
+
210
+ // Both observations should exist
211
+ expect(getObservation(result1.observationId)).toBeDefined();
212
+ expect(getObservation(result2.observationId)).toBeDefined();
213
+
214
+ // First creates a chunk, second is deduplicated
215
+ expect(result1.chunkId).toBeTruthy();
216
+ expect(result2.chunkId).toBeNull();
217
+
218
+ // Only one chunk row should exist
219
+ const db = getDb();
220
+ const chunks = db
221
+ .select()
222
+ .from(memoryChunks)
223
+ .where(eq(memoryChunks.scopeId, "default"))
224
+ .all();
225
+ expect(chunks).toHaveLength(1);
226
+ });
227
+
228
+ test("same content in different scopes creates separate chunks", () => {
229
+ createConversation("conv-1");
230
+
231
+ const result1 = insertObservation({
232
+ conversationId: "conv-1",
233
+ role: "user",
234
+ content: "The user likes dogs",
235
+ scopeId: "scope-a",
236
+ });
237
+
238
+ const result2 = insertObservation({
239
+ conversationId: "conv-1",
240
+ role: "user",
241
+ content: "The user likes dogs",
242
+ scopeId: "scope-b",
243
+ });
244
+
245
+ expect(result1.chunkId).toBeTruthy();
246
+ expect(result2.chunkId).toBeTruthy();
247
+ expect(result1.chunkId).not.toBe(result2.chunkId);
248
+ });
249
+
250
+ test("content hashes are deterministic", () => {
251
+ const hash1 = computeObservationContentHash("default", "Hello world");
252
+ const hash2 = computeObservationContentHash("default", "Hello world");
253
+ expect(hash1).toBe(hash2);
254
+
255
+ // Different scope produces different hash
256
+ const hash3 = computeObservationContentHash("other", "Hello world");
257
+ expect(hash1).not.toBe(hash3);
258
+ });
259
+ });
260
+
261
+ // ── Embedding job dispatch ────────────────────────────────────
262
+
263
+ describe("embedding job dispatch", () => {
264
+ test("enqueues embed_observation job when new chunk is created", () => {
265
+ createConversation("conv-1");
266
+
267
+ const result = insertObservation({
268
+ conversationId: "conv-1",
269
+ role: "user",
270
+ content: "User prefers TypeScript",
271
+ });
272
+
273
+ expect(result.embeddingJobId).toBeTruthy();
274
+
275
+ const jobs = getJobsByType("embed_observation");
276
+ expect(jobs).toHaveLength(1);
277
+ expect(jobs[0].status).toBe("pending");
278
+
279
+ const payload = JSON.parse(jobs[0].payload);
280
+ expect(payload.observationId).toBe(result.observationId);
281
+ expect(payload.chunkId).toBe(result.chunkId);
282
+ });
283
+
284
+ test("does not enqueue embed job when chunk is deduplicated", () => {
285
+ createConversation("conv-1");
286
+
287
+ // First insert creates a chunk and job
288
+ insertObservation({
289
+ conversationId: "conv-1",
290
+ role: "user",
291
+ content: "User prefers Python",
292
+ });
293
+
294
+ // Second insert with same content should not create another job
295
+ const result2 = insertObservation({
296
+ conversationId: "conv-1",
297
+ role: "user",
298
+ content: "User prefers Python",
299
+ });
300
+
301
+ expect(result2.embeddingJobId).toBeNull();
302
+
303
+ const jobs = getJobsByType("embed_observation");
304
+ expect(jobs).toHaveLength(1); // Only from the first insert
305
+ });
306
+
307
+ test("embed_observation jobs are claimable by the worker", () => {
308
+ createConversation("conv-1");
309
+
310
+ insertObservation({
311
+ conversationId: "conv-1",
312
+ role: "user",
313
+ content: "Claimable observation",
314
+ });
315
+
316
+ const claimed = claimMemoryJobs(10);
317
+ const embedJobs = claimed.filter((j) => j.type === "embed_observation");
318
+ expect(embedJobs).toHaveLength(1);
319
+ expect(embedJobs[0].status).toBe("running");
320
+ expect(embedJobs[0].payload.observationId).toBeTruthy();
321
+ expect(embedJobs[0].payload.chunkId).toBeTruthy();
322
+ });
323
+ });
324
+
325
+ // ── Batch insertion ───────────────────────────────────────────
326
+
327
+ describe("insertObservations (batch)", () => {
328
+ test("inserts multiple observations atomically", () => {
329
+ createConversation("conv-1");
330
+
331
+ const params: InsertObservationParams[] = [
332
+ { conversationId: "conv-1", role: "user", content: "Fact A" },
333
+ { conversationId: "conv-1", role: "user", content: "Fact B" },
334
+ { conversationId: "conv-1", role: "assistant", content: "Fact C" },
335
+ ];
336
+
337
+ const results = insertObservations(params);
338
+ expect(results).toHaveLength(3);
339
+
340
+ // All observations should exist
341
+ for (const result of results) {
342
+ expect(getObservation(result.observationId)).toBeDefined();
343
+ }
344
+
345
+ // All should have chunks (different content)
346
+ for (const result of results) {
347
+ expect(result.chunkId).toBeTruthy();
348
+ }
349
+
350
+ // All should have embedding jobs
351
+ const jobs = getJobsByType("embed_observation");
352
+ expect(jobs).toHaveLength(3);
353
+ });
354
+
355
+ test("batch handles content hash dedup within the batch", () => {
356
+ createConversation("conv-1");
357
+
358
+ const params: InsertObservationParams[] = [
359
+ { conversationId: "conv-1", role: "user", content: "Same content" },
360
+ { conversationId: "conv-1", role: "user", content: "Same content" },
361
+ ];
362
+
363
+ const results = insertObservations(params);
364
+ expect(results).toHaveLength(2);
365
+
366
+ // First creates a chunk, second is deduplicated
367
+ expect(results[0].chunkId).toBeTruthy();
368
+ expect(results[1].chunkId).toBeNull();
369
+
370
+ // Only one embedding job
371
+ const jobs = getJobsByType("embed_observation");
372
+ expect(jobs).toHaveLength(1);
373
+ });
374
+ });
375
+ });
@@ -0,0 +1,318 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ afterAll,
6
+ afterEach,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ mock,
11
+ test,
12
+ } from "bun:test";
13
+
14
+ const testDir = mkdtempSync(
15
+ join(tmpdir(), "memory-observation-dual-write-test-"),
16
+ );
17
+ const dbPath = join(testDir, "test.db");
18
+
19
+ mock.module("../util/platform.js", () => ({
20
+ getDataDir: () => testDir,
21
+ getRootDir: () => join(testDir, ".vellum"),
22
+ getWorkspaceDir: () => join(testDir, ".vellum", "workspace"),
23
+ getConversationsDir: () =>
24
+ join(testDir, ".vellum", "workspace", "conversations"),
25
+ isMacOS: () => process.platform === "darwin",
26
+ isLinux: () => process.platform === "linux",
27
+ isWindows: () => process.platform === "win32",
28
+ getPidPath: () => join(testDir, "test.pid"),
29
+ getDbPath: () => dbPath,
30
+ getLogPath: () => join(testDir, "test.log"),
31
+ ensureDataDir: () => {},
32
+ }));
33
+
34
+ mock.module("../util/logger.js", () => ({
35
+ getLogger: () =>
36
+ new Proxy({} as Record<string, unknown>, {
37
+ get: () => () => {},
38
+ }),
39
+ }));
40
+
41
+ // Stub the local embedding backend so the real ONNX model never loads.
42
+ mock.module("../memory/embedding-local.js", () => ({
43
+ LocalEmbeddingBackend: class {
44
+ readonly provider = "local" as const;
45
+ readonly model: string;
46
+ constructor(model: string) {
47
+ this.model = model;
48
+ }
49
+ async embed(texts: string[]): Promise<number[][]> {
50
+ return texts.map(() => new Array(384).fill(0));
51
+ }
52
+ },
53
+ }));
54
+
55
+ // Mock Qdrant client so semantic search returns empty results.
56
+ mock.module("../memory/qdrant-client.js", () => ({
57
+ getQdrantClient: () => ({
58
+ searchWithFilter: async () => [],
59
+ hybridSearch: async () => [],
60
+ upsertPoints: async () => {},
61
+ deletePoints: async () => {},
62
+ }),
63
+ initQdrantClient: () => {},
64
+ }));
65
+
66
+ import { DEFAULT_CONFIG } from "../config/defaults.js";
67
+
68
+ // Enable memory but disable LLM extraction and summarization.
69
+ const TEST_CONFIG = {
70
+ ...DEFAULT_CONFIG,
71
+ memory: {
72
+ ...DEFAULT_CONFIG.memory,
73
+ enabled: true,
74
+ extraction: {
75
+ ...DEFAULT_CONFIG.memory.extraction,
76
+ useLLM: false,
77
+ },
78
+ summarization: {
79
+ ...DEFAULT_CONFIG.memory.summarization,
80
+ useLLM: false,
81
+ },
82
+ },
83
+ };
84
+
85
+ mock.module("../config/loader.js", () => ({
86
+ loadConfig: () => TEST_CONFIG,
87
+ getConfig: () => TEST_CONFIG,
88
+ invalidateConfigCache: () => {},
89
+ }));
90
+
91
+ import { eq } from "drizzle-orm";
92
+
93
+ import { getChunkByObservationId } from "../memory/archive-store.js";
94
+ import { addMessage, createConversation } from "../memory/conversation-crud.js";
95
+ import { getDb, initializeDb, rawAll, resetDb } from "../memory/db.js";
96
+ import { memoryObservations, memorySegments } from "../memory/schema.js";
97
+
98
+ function removeTestDbFiles(): void {
99
+ rmSync(dbPath, { force: true });
100
+ rmSync(`${dbPath}-shm`, { force: true });
101
+ rmSync(`${dbPath}-wal`, { force: true });
102
+ }
103
+
104
+ function getObservationsByConversation(conversationId: string) {
105
+ const db = getDb();
106
+ return db
107
+ .select()
108
+ .from(memoryObservations)
109
+ .where(eq(memoryObservations.conversationId, conversationId))
110
+ .all();
111
+ }
112
+
113
+ function getJobsByType(type: string) {
114
+ return rawAll<{
115
+ id: string;
116
+ type: string;
117
+ payload: string;
118
+ status: string;
119
+ }>(`SELECT id, type, payload, status FROM memory_jobs WHERE type = ?`, type);
120
+ }
121
+
122
+ // ── Setup ───────────────────────────────────────────────────────────
123
+
124
+ describe("memory observation dual-write from addMessage", () => {
125
+ beforeEach(() => {
126
+ resetDb();
127
+ removeTestDbFiles();
128
+ initializeDb();
129
+ });
130
+
131
+ afterEach(() => {
132
+ resetDb();
133
+ removeTestDbFiles();
134
+ });
135
+
136
+ afterAll(() => {
137
+ resetDb();
138
+ rmSync(testDir, { recursive: true, force: true });
139
+ });
140
+
141
+ // ── Text-only messages ──────────────────────────────────────────
142
+
143
+ describe("text-only messages", () => {
144
+ test("creates an observation for a plain text user message", async () => {
145
+ const conv = createConversation("test-conv");
146
+ await addMessage(conv.id, "user", "I prefer dark mode for all editors");
147
+
148
+ const observations = getObservationsByConversation(conv.id);
149
+ expect(observations).toHaveLength(1);
150
+ expect(observations[0].role).toBe("user");
151
+ expect(observations[0].content).toBe(
152
+ "I prefer dark mode for all editors",
153
+ );
154
+ expect(observations[0].modality).toBe("text");
155
+ expect(observations[0].scopeId).toBe("default");
156
+ });
157
+
158
+ test("creates an observation for an assistant message", async () => {
159
+ const conv = createConversation("test-conv");
160
+ await addMessage(
161
+ conv.id,
162
+ "assistant",
163
+ "Sure, I will use dark mode from now on.",
164
+ );
165
+
166
+ const observations = getObservationsByConversation(conv.id);
167
+ expect(observations).toHaveLength(1);
168
+ expect(observations[0].role).toBe("assistant");
169
+ expect(observations[0].content).toBe(
170
+ "Sure, I will use dark mode from now on.",
171
+ );
172
+ });
173
+
174
+ test("creates a chunk with embed_chunk job for text message", async () => {
175
+ const conv = createConversation("test-conv");
176
+ await addMessage(conv.id, "user", "My favorite language is TypeScript");
177
+
178
+ const observations = getObservationsByConversation(conv.id);
179
+ expect(observations).toHaveLength(1);
180
+
181
+ const chunk = getChunkByObservationId(observations[0].id);
182
+ expect(chunk).toBeDefined();
183
+ expect(chunk!.content).toBe("My favorite language is TypeScript");
184
+
185
+ const embedJobs = getJobsByType("embed_chunk");
186
+ expect(embedJobs.length).toBeGreaterThanOrEqual(1);
187
+ const matchingJob = embedJobs.find((j) => {
188
+ const payload = JSON.parse(j.payload);
189
+ return payload.chunkId === chunk!.id;
190
+ });
191
+ expect(matchingJob).toBeDefined();
192
+ });
193
+
194
+ test("links observation to the correct messageId", async () => {
195
+ const conv = createConversation("test-conv");
196
+ const msg = await addMessage(conv.id, "user", "Testing message link");
197
+
198
+ const observations = getObservationsByConversation(conv.id);
199
+ expect(observations).toHaveLength(1);
200
+ expect(observations[0].messageId).toBe(msg.id);
201
+ });
202
+
203
+ test("uses conversation memory scope for observation", async () => {
204
+ const conv = createConversation({
205
+ conversationType: "private",
206
+ });
207
+ await addMessage(conv.id, "user", "Private observation");
208
+
209
+ const observations = getObservationsByConversation(conv.id);
210
+ expect(observations).toHaveLength(1);
211
+ expect(observations[0].scopeId).toBe(`private:${conv.id}`);
212
+ });
213
+ });
214
+
215
+ // ── Multimodal messages ─────────────────────────────────────────
216
+
217
+ describe("multimodal messages", () => {
218
+ test("creates observation for message with text + image blocks", async () => {
219
+ const conv = createConversation("test-conv");
220
+ const content = JSON.stringify([
221
+ { type: "text", text: "Here is my screenshot" },
222
+ {
223
+ type: "image",
224
+ source: {
225
+ type: "base64",
226
+ media_type: "image/png",
227
+ data: "iVBORw0KGgo=",
228
+ },
229
+ },
230
+ ]);
231
+ await addMessage(conv.id, "user", content);
232
+
233
+ const observations = getObservationsByConversation(conv.id);
234
+ expect(observations).toHaveLength(1);
235
+ // Text extraction produces the text portion
236
+ expect(observations[0].content).toContain("Here is my screenshot");
237
+ // Text+image = multimodal since media blocks are present
238
+ expect(observations[0].modality).toBe("multimodal");
239
+ });
240
+
241
+ test("creates observation with multimodal modality for image-only message", async () => {
242
+ const conv = createConversation("test-conv");
243
+ const content = JSON.stringify([
244
+ {
245
+ type: "image",
246
+ source: {
247
+ type: "base64",
248
+ media_type: "image/png",
249
+ data: "iVBORw0KGgo=",
250
+ },
251
+ },
252
+ ]);
253
+ await addMessage(conv.id, "user", content);
254
+
255
+ const observations = getObservationsByConversation(conv.id);
256
+ expect(observations).toHaveLength(1);
257
+ expect(observations[0].modality).toBe("multimodal");
258
+ });
259
+ });
260
+
261
+ // ── Legacy indexing unchanged ───────────────────────────────────
262
+
263
+ describe("legacy indexing continues alongside dual-write", () => {
264
+ test("legacy memory_segments are still created for text messages", async () => {
265
+ const conv = createConversation("test-conv");
266
+ await addMessage(conv.id, "user", "A fact worth remembering for memory");
267
+
268
+ // Legacy segments should exist
269
+ const db = getDb();
270
+ const segments = db
271
+ .select()
272
+ .from(memorySegments)
273
+ .where(eq(memorySegments.conversationId, conv.id))
274
+ .all();
275
+ expect(segments.length).toBeGreaterThanOrEqual(1);
276
+
277
+ // Observation should also exist
278
+ const observations = getObservationsByConversation(conv.id);
279
+ expect(observations).toHaveLength(1);
280
+ });
281
+
282
+ test("legacy extract_items jobs are still enqueued for user messages", async () => {
283
+ const conv = createConversation("test-conv");
284
+ await addMessage(conv.id, "user", "The user lives in San Francisco");
285
+
286
+ const extractJobs = getJobsByType("extract_items");
287
+ expect(extractJobs.length).toBeGreaterThanOrEqual(1);
288
+ });
289
+
290
+ test("skipping indexing skips both legacy and observation writes", async () => {
291
+ const conv = createConversation("test-conv");
292
+ await addMessage(conv.id, "user", "No indexing please", undefined, {
293
+ skipIndexing: true,
294
+ });
295
+
296
+ // No legacy segments
297
+ const db = getDb();
298
+ const segments = db
299
+ .select()
300
+ .from(memorySegments)
301
+ .where(eq(memorySegments.conversationId, conv.id))
302
+ .all();
303
+ expect(segments).toHaveLength(0);
304
+
305
+ // No observations
306
+ const observations = getObservationsByConversation(conv.id);
307
+ expect(observations).toHaveLength(0);
308
+ });
309
+
310
+ test("does not create observation for empty content messages", async () => {
311
+ const conv = createConversation("test-conv");
312
+ await addMessage(conv.id, "user", "");
313
+
314
+ const observations = getObservationsByConversation(conv.id);
315
+ expect(observations).toHaveLength(0);
316
+ });
317
+ });
318
+ });