@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
@@ -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,18 @@ import {
78
80
  migrateGuardianVerificationSessions,
79
81
  migrateInviteCodeHashColumn,
80
82
  migrateInviteContactId,
83
+ migrateLlmRequestLogMessageId,
84
+ migrateLlmRequestLogProvider,
85
+ migrateMemoryArchiveTables,
86
+ migrateMemoryBriefState,
81
87
  migrateMemoryItemSupersession,
88
+ migrateMemoryReducerCheckpoints,
82
89
  migrateMessagesFtsBackfill,
83
90
  migrateNormalizePhoneIdentities,
84
91
  migrateNotificationDeliveryThreadDecision,
85
92
  migrateOAuthAppsClientSecretPath,
93
+ migrateOAuthProvidersDisplayMetadata,
94
+ migrateOAuthProvidersManagedServiceConfigKey,
86
95
  migrateOAuthProvidersPingUrl,
87
96
  migrateReminderRoutingIntent,
88
97
  migrateRemindersToSchedules,
@@ -96,6 +105,7 @@ import {
96
105
  migrateRenameSequenceEnrollmentsThreadIdColumn,
97
106
  migrateRenameSequenceStepsReplyKey,
98
107
  migrateRenameSourceSessionIdColumn,
108
+ migrateRenameThreadStartersCheckpoints,
99
109
  migrateRenameThreadStartersTable,
100
110
  migrateRenameVerificationSessionIdColumn,
101
111
  migrateRenameVerificationTable,
@@ -447,6 +457,9 @@ export function initializeDb(): void {
447
457
  // 77. Rename thread_starters → conversation_starters table and indexes
448
458
  migrateRenameThreadStartersTable(database);
449
459
 
460
+ // 77b. Rename checkpoint keys from thread_starters: → conversation_starters: prefix
461
+ migrateRenameThreadStartersCheckpoints(database);
462
+
450
463
  // 78. Lifecycle events table for app_open / hatch telemetry
451
464
  createLifecycleEventsTable(database);
452
465
 
@@ -456,6 +469,33 @@ export function initializeDb(): void {
456
469
  // 80. Trace events table for persistent trace/activity storage across sessions
457
470
  migrateCreateTraceEventsTable(database);
458
471
 
472
+ // 81. Add managed_service_config_key column to oauth_providers
473
+ migrateOAuthProvidersManagedServiceConfigKey(database);
474
+
475
+ // 81b. Add display metadata columns to oauth_providers (display_name, description, dashboard_url, etc.)
476
+ migrateOAuthProvidersDisplayMetadata(database);
477
+
478
+ // 82. Add message_id column to llm_request_logs for per-message LLM context lookup
479
+ migrateLlmRequestLogMessageId(database);
480
+
481
+ // 82b. Add provider column to llm_request_logs for runtime provider lookup
482
+ migrateLlmRequestLogProvider(database);
483
+
484
+ // 83. Backfill existing inline (base64-in-DB) attachments to on-disk storage
485
+ migrateBackfillInlineAttachmentsToDisk(database);
486
+
487
+ // 84. Add nullable conversation fork lineage columns and parent lookup index
488
+ migrateConversationForkLineage(database);
489
+
490
+ // 85. Memory brief state tables (time_contexts, open_loops) for simplified memory system
491
+ migrateMemoryBriefState(database);
492
+
493
+ // 86. Memory archive tables (observations, chunks, episodes) for simplified memory v1
494
+ migrateMemoryArchiveTables(database);
495
+
496
+ // 87. Add memory reducer checkpoint columns to conversations
497
+ migrateMemoryReducerCheckpoints(database);
498
+
459
499
  validateMigrationState(database);
460
500
 
461
501
  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
  });
@@ -5,14 +5,15 @@ import { getConfig } from "../config/loader.js";
5
5
  import type { MemoryConfig } from "../config/types.js";
6
6
  import type { TrustClass } from "../runtime/actor-trust-resolver.js";
7
7
  import { getLogger } from "../util/logger.js";
8
+ import { computeChunkContentHash } from "./archive-store.js";
8
9
  import { getDb } from "./db.js";
9
10
  import { selectedBackendSupportsMultimodal } from "./embedding-backend.js";
10
11
  import { enqueueMemoryJob } from "./jobs-store.js";
11
12
  import {
12
- extractMediaBlocks,
13
+ extractMediaBlockMeta,
13
14
  extractTextFromStoredMessageContent,
14
15
  } from "./message-content.js";
15
- import { memorySegments } from "./schema.js";
16
+ import { memoryChunks, memoryObservations, memorySegments } from "./schema.js";
16
17
  import { segmentText } from "./segmenter.js";
17
18
 
18
19
  const log = getLogger("memory-indexer");
@@ -53,7 +54,12 @@ export async function indexMessageNow(
53
54
  input.provenanceTrustClass === undefined;
54
55
 
55
56
  const text = extractTextFromStoredMessageContent(input.content);
56
- if (text.length === 0) {
57
+ const hasText = text.length > 0;
58
+ const candidateMediaMeta = extractMediaBlockMeta(input.content).filter(
59
+ (b) => b.type === "image",
60
+ );
61
+ const hasMedia = candidateMediaMeta.length > 0;
62
+ if (!hasText && !hasMedia) {
57
63
  enqueueMemoryJob("build_conversation_summary", {
58
64
  conversationId: input.conversationId,
59
65
  });
@@ -62,31 +68,35 @@ export async function indexMessageNow(
62
68
 
63
69
  const db = getDb();
64
70
  const now = Date.now();
65
- const segments = segmentText(
66
- text,
67
- config.segmentation.targetTokens,
68
- config.segmentation.overlapTokens,
69
- );
71
+ const segments = hasText
72
+ ? segmentText(
73
+ text,
74
+ config.segmentation.targetTokens,
75
+ config.segmentation.overlapTokens,
76
+ )
77
+ : [];
70
78
  const shouldExtract =
71
79
  input.role === "user" ||
72
80
  (input.role === "assistant" && config.extraction.extractFromAssistant);
73
81
  // 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(
78
- (b) => b.type === "image",
79
- );
82
+ // extractMediaBlockMeta is synchronous and lightweight — it detects image
83
+ // blocks without decoding base64 data into Buffers, avoiding CPU/memory
84
+ // overhead for messages on non-multimodal backends.
85
+ // selectedBackendSupportsMultimodal requires async key resolution, so we
86
+ // skip it entirely for text-only messages.
80
87
  const mediaBlocks =
81
- candidateMediaBlocks.length > 0 &&
88
+ candidateMediaMeta.length > 0 &&
82
89
  (await selectedBackendSupportsMultimodal(getConfig()))
83
- ? candidateMediaBlocks
90
+ ? candidateMediaMeta
84
91
  : [];
85
92
 
86
93
  // Wrap all segment inserts and job enqueues in a single transaction so they
87
94
  // either all succeed or all roll back, preventing partial/orphaned state.
88
95
  let skippedEmbedJobs = 0;
96
+ let skippedChunkEmbedJobs = 0;
97
+ const scopeId = input.scopeId ?? "default";
89
98
  db.transaction((tx) => {
99
+ // ── Legacy segment path (kept intact for parallel validation) ───
90
100
  for (const segment of segments) {
91
101
  const segmentId = buildSegmentId(input.messageId, segment.segmentIndex);
92
102
  const hash = createHash("sha256").update(segment.text).digest("hex");
@@ -107,7 +117,7 @@ export async function indexMessageNow(
107
117
  segmentIndex: segment.segmentIndex,
108
118
  text: segment.text,
109
119
  tokenEstimate: segment.tokenEstimate,
110
- scopeId: input.scopeId ?? "default",
120
+ scopeId,
111
121
  contentHash: hash,
112
122
  createdAt: input.createdAt,
113
123
  updatedAt: now,
@@ -117,7 +127,7 @@ export async function indexMessageNow(
117
127
  set: {
118
128
  text: segment.text,
119
129
  tokenEstimate: segment.tokenEstimate,
120
- scopeId: input.scopeId ?? "default",
130
+ scopeId,
121
131
  contentHash: hash,
122
132
  updatedAt: now,
123
133
  },
@@ -131,6 +141,65 @@ export async function indexMessageNow(
131
141
  }
132
142
  }
133
143
 
144
+ // ── Archive chunk dual-write (mirrors segment boundaries) ──────
145
+ // Create a single observation per message, then create one chunk per
146
+ // segment using the same segmentation boundaries. Chunks are
147
+ // deduplicated by (scopeId, contentHash) via onConflictDoNothing so
148
+ // unchanged content does not enqueue duplicate embed_chunk jobs.
149
+ const observationId = buildObservationId(input.messageId);
150
+ tx.insert(memoryObservations)
151
+ .values({
152
+ id: observationId,
153
+ scopeId,
154
+ conversationId: input.conversationId,
155
+ messageId: input.messageId,
156
+ role: input.role,
157
+ content: hasText ? text : input.content,
158
+ modality: hasMedia ? "multimodal" : "text",
159
+ source: null,
160
+ createdAt: input.createdAt,
161
+ })
162
+ .onConflictDoNothing({ target: memoryObservations.id })
163
+ .run();
164
+
165
+ for (const segment of segments) {
166
+ const chunkId = buildChunkId(input.messageId, segment.segmentIndex);
167
+ const chunkHash = computeChunkContentHash(scopeId, segment.text);
168
+
169
+ // Check if this chunk already exists with the same content hash
170
+ const existingChunk = tx
171
+ .select({ contentHash: memoryChunks.contentHash })
172
+ .from(memoryChunks)
173
+ .where(eq(memoryChunks.id, chunkId))
174
+ .get();
175
+
176
+ tx.insert(memoryChunks)
177
+ .values({
178
+ id: chunkId,
179
+ scopeId,
180
+ observationId,
181
+ content: segment.text,
182
+ tokenEstimate: segment.tokenEstimate,
183
+ contentHash: chunkHash,
184
+ createdAt: input.createdAt,
185
+ })
186
+ .onConflictDoUpdate({
187
+ target: memoryChunks.id,
188
+ set: {
189
+ content: segment.text,
190
+ tokenEstimate: segment.tokenEstimate,
191
+ contentHash: chunkHash,
192
+ },
193
+ })
194
+ .run();
195
+
196
+ if (existingChunk?.contentHash === chunkHash) {
197
+ skippedChunkEmbedJobs++;
198
+ } else {
199
+ enqueueMemoryJob("embed_chunk", { chunkId, scopeId }, Date.now(), tx);
200
+ }
201
+ }
202
+
134
203
  // Enqueue embed_attachment jobs for image content blocks when the
135
204
  // embedding provider supports multimodal (Gemini only).
136
205
  for (const block of mediaBlocks) {
@@ -145,7 +214,7 @@ export async function indexMessageNow(
145
214
  if (shouldExtract && isTrustedActor && !input.automated) {
146
215
  enqueueMemoryJob(
147
216
  "extract_items",
148
- { messageId: input.messageId, scopeId: input.scopeId ?? "default" },
217
+ { messageId: input.messageId, scopeId },
149
218
  Date.now(),
150
219
  tx,
151
220
  );
@@ -164,6 +233,12 @@ export async function indexMessageNow(
164
233
  );
165
234
  }
166
235
 
236
+ if (skippedChunkEmbedJobs > 0) {
237
+ log.debug(
238
+ `Skipped ${skippedChunkEmbedJobs}/${segments.length} embed_chunk jobs (content unchanged)`,
239
+ );
240
+ }
241
+
167
242
  if (!isTrustedActor && shouldExtract) {
168
243
  log.info(
169
244
  `Skipping extraction jobs for untrusted actor (trustClass=${input.provenanceTrustClass})`,
@@ -175,9 +250,11 @@ export async function indexMessageNow(
175
250
  }
176
251
 
177
252
  const extractionGated = !isTrustedActor || !!input.automated;
253
+ const segmentEmbedJobs = segments.length - skippedEmbedJobs;
254
+ const chunkEmbedJobs = segments.length - skippedChunkEmbedJobs;
178
255
  const enqueuedJobs =
179
- segments.length -
180
- skippedEmbedJobs +
256
+ segmentEmbedJobs +
257
+ chunkEmbedJobs +
181
258
  mediaBlocks.length +
182
259
  (shouldExtract && !extractionGated ? 2 : 1);
183
260
  return {
@@ -211,3 +288,19 @@ export function getRecentSegmentsForConversation(
211
288
  function buildSegmentId(messageId: string, segmentIndex: number): string {
212
289
  return `${messageId}:${segmentIndex}`;
213
290
  }
291
+
292
+ /**
293
+ * Deterministic observation ID derived from the messageId so repeated
294
+ * indexer runs for the same message converge on the same observation row.
295
+ */
296
+ function buildObservationId(messageId: string): string {
297
+ return `obs:${messageId}`;
298
+ }
299
+
300
+ /**
301
+ * Deterministic chunk ID derived from the messageId and segment index so
302
+ * the dual-write path mirrors the legacy segment identity scheme exactly.
303
+ */
304
+ function buildChunkId(messageId: string, segmentIndex: number): string {
305
+ return `chunk:${messageId}:${segmentIndex}`;
306
+ }
@@ -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
 
@@ -186,6 +183,7 @@ describe("embedMediaJob", () => {
186
183
  expect(call.extraPayload).toEqual({
187
184
  created_at: now,
188
185
  kind: "image",
186
+ memory_scope_id: "default",
189
187
  subject: "My Screenshot",
190
188
  });
191
189
  });