@vellumai/assistant 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -1,6 +1,11 @@
1
1
  import { v4 as uuid } from "uuid";
2
2
 
3
- import { getApp, getAppPreview, updateApp } from "../memory/app-store.js";
3
+ import {
4
+ getApp,
5
+ getAppPreview,
6
+ resolveAppDir,
7
+ updateApp,
8
+ } from "../memory/app-store.js";
4
9
  import type { ToolExecutionResult } from "../tools/types.js";
5
10
  import { getLogger } from "../util/logger.js";
6
11
  import { isPlainObject } from "../util/object.js";
@@ -187,6 +192,7 @@ export interface SurfaceConversationContext {
187
192
  }
188
193
  >;
189
194
  surfaceUndoStacks: Map<string, string[]>;
195
+ accumulatedSurfaceState: Map<string, Record<string, unknown>>;
190
196
  /** Request IDs that originated from surface action button clicks (not regular user messages). */
191
197
  surfaceActionRequestIds: Set<string>;
192
198
  currentTurnSurfaces: Array<{
@@ -346,6 +352,39 @@ function handleDocumentContentChanged(
346
352
  }
347
353
  }
348
354
 
355
+ /**
356
+ * Handle state_update action from a dynamic page.
357
+ * Accumulates state via shallow merge without triggering an LLM turn.
358
+ */
359
+ function handleStateUpdate(
360
+ ctx: SurfaceConversationContext,
361
+ surfaceId: string,
362
+ data?: Record<string, unknown>,
363
+ ): void {
364
+ if (!data) {
365
+ log.debug({ surfaceId }, "state_update action called with no data");
366
+ return;
367
+ }
368
+
369
+ const surfaceState = ctx.surfaceState.get(surfaceId);
370
+ if (!surfaceState || surfaceState.surfaceType !== "dynamic_page") {
371
+ log.warn(
372
+ { surfaceId, surfaceType: surfaceState?.surfaceType },
373
+ "state_update action received for non-dynamic_page surface",
374
+ );
375
+ return;
376
+ }
377
+
378
+ const existing = ctx.accumulatedSurfaceState.get(surfaceId) ?? {};
379
+ const merged = { ...existing, ...data };
380
+ ctx.accumulatedSurfaceState.set(surfaceId, merged);
381
+
382
+ log.debug(
383
+ { surfaceId, accumulatedState: merged },
384
+ "Accumulated surface state updated",
385
+ );
386
+ }
387
+
349
388
  export function pushUndoState(
350
389
  surfaceUndoStacks: Map<string, string[]>,
351
390
  surfaceId: string,
@@ -543,17 +582,60 @@ export function handleSurfaceAction(
543
582
  const pending = ctx.pendingSurfaceActions.get(surfaceId);
544
583
 
545
584
  // When surfaces are restored from history (e.g. onboarding cards), there is
546
- // no in-memory pendingSurfaceActions entry. For relay_prompt / agent_prompt
547
- // actions the client already sends the full payload (including { prompt }),
548
- // so we can handle them without stored state.
585
+ // no in-memory pendingSurfaceActions entry. Handle non-terminal actions
586
+ // directly, and forward custom/relay actions to the LLM.
549
587
  if (!pending) {
588
+ // Non-terminal actions don't need stored state — handle directly.
589
+ if (actionId === "selection_changed") {
590
+ log.debug(
591
+ { surfaceId, data },
592
+ "Selection changed (history-restored, not forwarding)",
593
+ );
594
+ return;
595
+ }
596
+ if (actionId === "content_changed") {
597
+ log.debug(
598
+ { surfaceId },
599
+ "Content changed (history-restored, no surface state — skipping)",
600
+ );
601
+ return;
602
+ }
603
+ if (actionId === "state_update") {
604
+ if (data) {
605
+ const existing = ctx.accumulatedSurfaceState.get(surfaceId) ?? {};
606
+ ctx.accumulatedSurfaceState.set(surfaceId, { ...existing, ...data });
607
+ }
608
+ log.debug(
609
+ { surfaceId, data },
610
+ "Silent state accumulated (history-restored)",
611
+ );
612
+ return;
613
+ }
614
+
615
+ // Determine message content from the action.
550
616
  const isRelay = actionId === "relay_prompt" || actionId === "agent_prompt";
551
617
  const prompt =
552
618
  isRelay && typeof data?.prompt === "string" ? data.prompt.trim() : "";
553
619
 
554
- if (!prompt) {
555
- log.warn({ surfaceId, actionId }, "No pending surface action found");
556
- return;
620
+ let content: string;
621
+ let displayContent: string | undefined;
622
+ if (prompt) {
623
+ content = prompt;
624
+ } else {
625
+ // Custom action from an app (e.g. sendAction('answer_selected', {...}))
626
+ const summary = actionId
627
+ .replace(/_/g, " ")
628
+ .replace(/\b\w/g, (c) => c.toUpperCase());
629
+ content = `[User action on app: ${summary}]`;
630
+ if (data && Object.keys(data).length > 0) {
631
+ content += `\n\nAction data: ${JSON.stringify(data)}`;
632
+ }
633
+ const accState = ctx.accumulatedSurfaceState.get(surfaceId);
634
+ if (accState && Object.keys(accState).length > 0) {
635
+ content += `\n\nAccumulated surface state: ${JSON.stringify(accState)}`;
636
+ ctx.accumulatedSurfaceState.delete(surfaceId);
637
+ }
638
+ displayContent = summary;
557
639
  }
558
640
 
559
641
  const requestId = uuid();
@@ -567,11 +649,15 @@ export function handleSurfaceAction(
567
649
  });
568
650
 
569
651
  const result = ctx.enqueueMessage(
570
- prompt,
652
+ content,
571
653
  [],
572
654
  onEvent,
573
655
  requestId,
574
656
  surfaceId,
657
+ undefined,
658
+ undefined,
659
+ undefined,
660
+ displayContent,
575
661
  );
576
662
 
577
663
  if (result.rejected) {
@@ -581,16 +667,18 @@ export function handleSurfaceAction(
581
667
 
582
668
  // Echo the prompt to the client so it appears in the chat UI.
583
669
  // Deferred until after rejection check to avoid ghost messages.
584
- ctx.sendToClient({
585
- type: "user_message_echo",
586
- text: prompt,
587
- conversationId: ctx.conversationId,
588
- });
670
+ if (prompt) {
671
+ ctx.sendToClient({
672
+ type: "user_message_echo",
673
+ text: prompt,
674
+ conversationId: ctx.conversationId,
675
+ });
676
+ }
589
677
 
590
678
  if (result.queued) {
591
679
  log.info(
592
680
  { surfaceId, actionId, requestId },
593
- "Relay prompt queued (conversation busy, history-restored)",
681
+ "Surface action queued (conversation busy, history-restored)",
594
682
  );
595
683
  return;
596
684
  }
@@ -598,22 +686,31 @@ export function handleSurfaceAction(
598
686
  // Conversation is idle — process the message immediately.
599
687
  log.info(
600
688
  { surfaceId, actionId, requestId },
601
- "Processing relay prompt immediately (history-restored)",
689
+ "Processing surface action immediately (history-restored)",
602
690
  );
603
691
  ctx
604
- .processMessage(prompt, [], onEvent, requestId, surfaceId)
692
+ .processMessage(
693
+ content,
694
+ [],
695
+ onEvent,
696
+ requestId,
697
+ surfaceId,
698
+ undefined,
699
+ undefined,
700
+ displayContent,
701
+ )
605
702
  .catch((err) => {
606
703
  const message = err instanceof Error ? err.message : String(err);
607
704
  log.error(
608
705
  { err, surfaceId, actionId },
609
- "Failed to process history-restored relay prompt",
706
+ "Failed to process history-restored surface action",
610
707
  );
611
708
  onEvent(
612
709
  buildConversationErrorMessage(ctx.conversationId, {
613
710
  code: "CONVERSATION_PROCESSING_FAILED",
614
711
  userMessage: `Something went wrong: ${message}`,
615
712
  retryable: false,
616
- debugDetails: `History-restored relay prompt processing failed: ${message}`,
713
+ debugDetails: `History-restored surface action processing failed: ${message}`,
617
714
  errorCategory: "processing_failed",
618
715
  }),
619
716
  );
@@ -637,6 +734,14 @@ export function handleSurfaceAction(
637
734
  handleDocumentContentChanged(ctx, surfaceId, data);
638
735
  return;
639
736
  }
737
+
738
+ // state_update is a silent accumulation action — merge data into accumulated
739
+ // state without triggering an LLM turn.
740
+ if (actionId === "state_update") {
741
+ handleStateUpdate(ctx, surfaceId, data);
742
+ return;
743
+ }
744
+
640
745
  // Merge stored action-level data (from ui_show definition) with client-sent
641
746
  // data. This is critical for relay_prompt buttons: the client only sends the
642
747
  // actionId, but the prompt payload lives in the action definition's data.
@@ -694,6 +799,10 @@ export function handleSurfaceAction(
694
799
  selectedIds,
695
800
  );
696
801
  }
802
+ const accumulatedState = ctx.accumulatedSurfaceState.get(surfaceId);
803
+ if (accumulatedState && Object.keys(accumulatedState).length > 0) {
804
+ fallbackContent += `\n\nAccumulated surface state: ${JSON.stringify(accumulatedState)}`;
805
+ }
697
806
  // When a relay_prompt button also carries selection data (e.g. list/table
698
807
  // surface with a canned prompt + user-selected rows), append the selection
699
808
  // context so the LLM sees both the prompt and the user's selections.
@@ -707,6 +816,11 @@ export function handleSurfaceAction(
707
816
  );
708
817
  }
709
818
  }
819
+ // When prompt is truthy, fallbackContent (which includes accumulated state)
820
+ // is discarded. Re-append accumulated state so the LLM sees it.
821
+ if (prompt && accumulatedState && Object.keys(accumulatedState).length > 0) {
822
+ content += `\n\nAccumulated surface state: ${JSON.stringify(accumulatedState)}`;
823
+ }
710
824
  // Show the user plain-text instead of raw JSON action data.
711
825
  const displayContent = prompt
712
826
  ? undefined
@@ -743,6 +857,12 @@ export function handleSurfaceAction(
743
857
  return;
744
858
  }
745
859
 
860
+ // One-shot: clear accumulated state now that the message has been accepted.
861
+ // Deferred until after rejection check so state is preserved for retry on rejection.
862
+ if (accumulatedState && Object.keys(accumulatedState).length > 0) {
863
+ ctx.accumulatedSurfaceState.delete(surfaceId);
864
+ }
865
+
746
866
  // Echo the user's prompt to the client so it appears in the chat UI.
747
867
  // Deferred until after rejection check to avoid ghost messages.
748
868
  if (shouldRelayPrompt && prompt) {
@@ -811,7 +931,7 @@ export function handleSurfaceAction(
811
931
  }
812
932
 
813
933
  /**
814
- * After an app_update, refresh any active surface that displays the updated app.
934
+ * After an app_refresh, refresh any active surface that displays the updated app.
815
935
  */
816
936
  export function refreshSurfacesForApp(
817
937
  ctx: SurfaceConversationContext,
@@ -860,7 +980,7 @@ export function refreshSurfacesForApp(
860
980
  refreshed = true;
861
981
  log.info(
862
982
  { conversationId: ctx.conversationId, surfaceId, appId },
863
- "Auto-refreshed surface after app_update",
983
+ "Auto-refreshed surface after app_refresh",
864
984
  );
865
985
  }
866
986
  return refreshed;
@@ -1201,6 +1321,7 @@ export async function surfaceProxyResolver(
1201
1321
  ctx.surfaceState.delete(surfaceId);
1202
1322
  ctx.surfaceUndoStacks.delete(surfaceId);
1203
1323
  ctx.lastSurfaceAction.delete(surfaceId);
1324
+ ctx.accumulatedSurfaceState.delete(surfaceId);
1204
1325
  return {
1205
1326
  content: lastAction ? "Surface completed" : "Surface dismissed",
1206
1327
  isError: false,
@@ -1219,9 +1340,11 @@ export async function surfaceProxyResolver(
1219
1340
  const defaultPreview = { title: app.name, subtitle: app.description };
1220
1341
 
1221
1342
  const storedPreview = getAppPreview(app.id);
1343
+ const { dirName } = resolveAppDir(app.id);
1222
1344
  const surfaceData: DynamicPageSurfaceData = {
1223
1345
  html: app.htmlDefinition,
1224
1346
  appId: app.id,
1347
+ dirName,
1225
1348
  preview: {
1226
1349
  ...defaultPreview,
1227
1350
  ...preview,
@@ -560,8 +560,6 @@ export interface SkillProjectionContext {
560
560
  };
561
561
  /** True when no client is connected (HTTP-only). */
562
562
  readonly hasNoClient?: boolean;
563
- /** True when the conversation has user-uploaded attachments. */
564
- hasAttachments?: boolean;
565
563
  }
566
564
 
567
565
  // ── Conditional tool sets ────────────────────────────────────────────
@@ -573,7 +571,6 @@ const HOST_TOOL_NAMES = new Set([
573
571
  "host_file_edit",
574
572
  "host_bash",
575
573
  ]);
576
- const ASSET_TOOL_NAMES = new Set(["asset_search", "asset_materialize"]);
577
574
  const CLIENT_CAPABILITY_TOOL_NAMES = new Set(["app_open"]);
578
575
  const PLATFORM_TOOL_NAMES = new Set(["request_system_permission"]);
579
576
 
@@ -597,9 +594,6 @@ export function isToolActiveForContext(
597
594
  // unchecked host command execution on the daemon host.
598
595
  return !ctx.hasNoClient;
599
596
  }
600
- if (ASSET_TOOL_NAMES.has(name)) {
601
- return ctx.hasAttachments ?? false;
602
- }
603
597
  if (CLIENT_CAPABILITY_TOOL_NAMES.has(name)) {
604
598
  return !ctx.hasNoClient;
605
599
  }
@@ -1,3 +1,7 @@
1
+ import { join } from "node:path";
2
+
3
+ import { getConversation } from "../memory/conversation-crud.js";
4
+ import { resolveConversationDirectoryPaths } from "../memory/conversation-directories.js";
1
5
  import { renderWorkspaceTopLevelContext } from "../workspace/top-level-renderer.js";
2
6
  import { scanTopLevelDirectories } from "../workspace/top-level-scanner.js";
3
7
 
@@ -5,6 +9,7 @@ import { scanTopLevelDirectories } from "../workspace/top-level-scanner.js";
5
9
  * Subset of Conversation state that workspace context helpers need.
6
10
  */
7
11
  export interface WorkspaceConversationContext {
12
+ conversationId: string;
8
13
  workingDir: string;
9
14
  workspaceTopLevelContext: string | null;
10
15
  workspaceTopLevelDirty: boolean;
@@ -17,6 +22,21 @@ export function refreshWorkspaceTopLevelContextIfNeeded(
17
22
  if (!ctx.workspaceTopLevelDirty && ctx.workspaceTopLevelContext != null)
18
23
  return;
19
24
  const snapshot = scanTopLevelDirectories(ctx.workingDir);
20
- ctx.workspaceTopLevelContext = renderWorkspaceTopLevelContext(snapshot);
25
+ const conversation = getConversation(ctx.conversationId);
26
+ let currentConversationPath: string | null = null;
27
+ if (conversation && typeof conversation.createdAt === "number") {
28
+ const { resolvedDirName } = resolveConversationDirectoryPaths(
29
+ conversation.id,
30
+ conversation.createdAt,
31
+ join(ctx.workingDir, "conversations"),
32
+ );
33
+ currentConversationPath = `conversations/${resolvedDirName}/`;
34
+ }
35
+ ctx.workspaceTopLevelContext = renderWorkspaceTopLevelContext(snapshot, {
36
+ currentConversationPath,
37
+ currentConversationAttachmentsPath: currentConversationPath
38
+ ? `${currentConversationPath}attachments/`
39
+ : null,
40
+ });
21
41
  ctx.workspaceTopLevelDirty = false;
22
42
  }
@@ -38,6 +38,7 @@ import {
38
38
  import { registerToolTraceListener } from "../events/tool-trace-listener.js";
39
39
  import { getHookManager } from "../hooks/manager.js";
40
40
  import { resolveCanonicalGuardianRequest } from "../memory/canonical-guardian-store.js";
41
+ import { getMessages } from "../memory/conversation-crud.js";
41
42
  import { PermissionPrompter } from "../permissions/prompter.js";
42
43
  import { SecretPrompter } from "../permissions/secret-prompter.js";
43
44
  import { patternMatchesCandidate } from "../permissions/trust-store.js";
@@ -81,7 +82,6 @@ import type {
81
82
  ChannelCapabilities,
82
83
  TrustContext,
83
84
  } from "./conversation-runtime-assembly.js";
84
- import { messagesContainAttachments } from "./conversation-runtime-assembly.js";
85
85
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
86
86
  import {
87
87
  createSurfaceMutex,
@@ -164,7 +164,6 @@ export class Conversation {
164
164
  /** @internal */ contextCompactedAt: number | null = null;
165
165
  /** @internal */ currentRequestId?: string;
166
166
  /** @internal */ hasNoClient = false;
167
- /** @internal */ hasAttachments = false;
168
167
  /** @internal */ headlessLock = false;
169
168
  /** @internal */ taskRunId?: string;
170
169
  /** @internal */ callSessionId?: string;
@@ -200,6 +199,10 @@ export class Conversation {
200
199
  { surfaceType: SurfaceType; data: SurfaceData; title?: string }
201
200
  >();
202
201
  /** @internal */ surfaceUndoStacks = new Map<string, string[]>();
202
+ /** @internal */ accumulatedSurfaceState = new Map<
203
+ string,
204
+ Record<string, unknown>
205
+ >();
203
206
  /** @internal */ withSurface = createSurfaceMutex();
204
207
  /** @internal */ currentTurnSurfaces: Array<{
205
208
  surfaceId: string;
@@ -379,13 +382,35 @@ export class Conversation {
379
382
 
380
383
  async loadFromDb(): Promise<void> {
381
384
  await loadFromDbImpl(this);
382
- // Scan loaded history for attachment content blocks so that asset
383
- // tools are available when resuming a conversation that already had
384
- // attachments. One-way: once true it stays true for the conversation.
385
- // Also picks up the hasAttachments flag set by loadFromDbImpl which
386
- // scans compacted (sliced-off) messages that aren't in this.messages.
387
- if (!this.hasAttachments && messagesContainAttachments(this.messages)) {
388
- this.hasAttachments = true;
385
+ this.restoreSurfaceStateFromHistory();
386
+ }
387
+
388
+ /**
389
+ * Scan ALL persisted messages (including compacted ones) for ui_surface
390
+ * content blocks and populate surfaceState so findConversationBySurfaceId
391
+ * works for surfaces restored from history (e.g. after daemon restart).
392
+ */
393
+ private restoreSurfaceStateFromHistory(): void {
394
+ const dbMessages = getMessages(this.conversationId);
395
+ for (const row of dbMessages) {
396
+ try {
397
+ const content = JSON.parse(row.content);
398
+ if (!Array.isArray(content)) continue;
399
+ for (const block of content) {
400
+ if (
401
+ block.type === "ui_surface" &&
402
+ typeof block.surfaceId === "string"
403
+ ) {
404
+ this.surfaceState.set(block.surfaceId, {
405
+ surfaceType: (block.surfaceType ?? "dynamic_page") as SurfaceType,
406
+ data: (block.data ?? {}) as SurfaceData,
407
+ title: block.title as string | undefined,
408
+ });
409
+ }
410
+ }
411
+ } catch {
412
+ // Content isn't valid JSON — skip
413
+ }
389
414
  }
390
415
  }
391
416
 
@@ -606,6 +631,18 @@ export class Conversation {
606
631
  "Resuming after approval",
607
632
  );
608
633
 
634
+ // Sync the canonical guardian request status so stale "pending" DB
635
+ // records don't get matched by later guardian reply routing. Best-effort:
636
+ // CAS may harmlessly fail if the canonical decision primitive already
637
+ // resolved the request (e.g. channel approval path).
638
+ try {
639
+ resolveCanonicalGuardianRequest(requestId, "pending", {
640
+ status: resolvedState,
641
+ });
642
+ } catch {
643
+ // Canonical request tracking should not break the primary approval flow.
644
+ }
645
+
609
646
  // Cascade to other pending confirmations that match this decision
610
647
  this.cascadePendingApprovals(requestId, decision, selectedPattern);
611
648
  }
@@ -648,10 +685,11 @@ export class Conversation {
648
685
  // Consume from pending-interactions tracker
649
686
  pendingInteractions.resolve(candidateId);
650
687
 
651
- // Resolve via handleConfirmationResponse which emits events.
652
- // Use simple "allow"/"deny" so the permission-checker won't save
653
- // duplicate rules or re-activate temporary modes. Recursion
654
- // terminates because allow/deny exit cascadePendingApprovals early.
688
+ // Resolve via handleConfirmationResponse which emits events and
689
+ // syncs canonical status. Use simple "allow"/"deny" so the
690
+ // permission-checker won't save duplicate rules or re-activate
691
+ // temporary modes. Recursion terminates because allow/deny exit
692
+ // cascadePendingApprovals early.
655
693
  this.handleConfirmationResponse(
656
694
  candidateId,
657
695
  cascadeResult.allow ? "allow" : "deny",
@@ -663,17 +701,6 @@ export class Conversation {
663
701
  causedByRequestId: primaryRequestId,
664
702
  },
665
703
  );
666
-
667
- // Sync the canonical guardian request status for the cascaded request.
668
- // Best-effort: canonical request tracking should not break the cascade flow.
669
- try {
670
- const targetStatus = cascadeResult.allow ? "approved" : "denied";
671
- resolveCanonicalGuardianRequest(candidateId, "pending", {
672
- status: targetStatus,
673
- });
674
- } catch {
675
- // Ignore — canonical request tracking is best-effort
676
- }
677
704
  }
678
705
  }
679
706
 
@@ -880,11 +907,6 @@ export class Conversation {
880
907
  if (!this.processing) {
881
908
  await this.ensureActorScopedHistory();
882
909
  }
883
- // One-way flag: once an attachment arrives, asset tools stay available
884
- // for the remainder of the conversation.
885
- if (!this.hasAttachments && attachments.length > 0) {
886
- this.hasAttachments = true;
887
- }
888
910
  return persistUserMessageImpl(
889
911
  this,
890
912
  content,
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+
3
+ import { getWorkspacePromptPath } from "../util/platform.js";
4
+
5
+ /**
6
+ * The canned assistant response for the wake-up greeting on a fresh workspace.
7
+ * Warm, non-presumptuous greeting that communicates "I'm new," "I improve over
8
+ * time," "I'm ready to be useful," and "you're in control."
9
+ */
10
+ export const CANNED_FIRST_GREETING =
11
+ "Hey. I'm brand new, no name, no memories, nothing yet. The more we work together, the more context and memory I build, and the better I get. But let's not wait around. Throw a question at me, give me a task, or ask what I can do.";
12
+
13
+ /**
14
+ * Returns `true` when all of the following are true:
15
+ * - `conversationMessageCount === 0` (no prior messages in this conversation)
16
+ * - BOOTSTRAP.md exists at the workspace prompt path
17
+ * - The trimmed content matches the macOS wake-up greeting (case-insensitive)
18
+ */
19
+ export function isWakeUpGreeting(
20
+ content: string,
21
+ conversationMessageCount: number,
22
+ ): boolean {
23
+ if (conversationMessageCount !== 0) return false;
24
+ if (!existsSync(getWorkspacePromptPath("BOOTSTRAP.md"))) return false;
25
+ return content.trim().toLowerCase() === "wake up, my friend.";
26
+ }
27
+
28
+ /**
29
+ * Returns the canned first-greeting string. Simple getter that exists to keep
30
+ * the call site consistent and allow future flexibility (e.g., locale-aware
31
+ * greetings) without changing the API.
32
+ */
33
+ export function getCannedFirstGreeting(): string {
34
+ return CANNED_FIRST_GREETING;
35
+ }
@@ -0,0 +1,148 @@
1
+ import {
2
+ getConfig,
3
+ loadRawConfig,
4
+ saveRawConfig,
5
+ } from "../../config/loader.js";
6
+ import { setMemoryEmbeddingField } from "../../config/raw-config-utils.js";
7
+ import { VALID_MEMORY_EMBEDDING_PROVIDERS } from "../../config/schemas/memory-storage.js";
8
+ import {
9
+ clearEmbeddingBackendCache,
10
+ getMemoryBackendStatus,
11
+ } from "../../memory/embedding-backend.js";
12
+ import type { ModelSetContext } from "./config-model.js";
13
+ import { CONFIG_RELOAD_DEBOUNCE_MS, log } from "./shared.js";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Embedding provider catalog
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const EMBEDDING_PROVIDER_CATALOG = [
20
+ {
21
+ id: "auto",
22
+ displayName: "Auto (Best Available)",
23
+ defaultModel: "",
24
+ requiresKey: false,
25
+ },
26
+ {
27
+ id: "local",
28
+ displayName: "Local (In-Process)",
29
+ defaultModel: "Xenova/bge-small-en-v1.5",
30
+ requiresKey: false,
31
+ },
32
+ {
33
+ id: "openai",
34
+ displayName: "OpenAI",
35
+ defaultModel: "text-embedding-3-small",
36
+ requiresKey: true,
37
+ },
38
+ {
39
+ id: "gemini",
40
+ displayName: "Gemini",
41
+ defaultModel: "gemini-embedding-2-preview",
42
+ requiresKey: true,
43
+ },
44
+ {
45
+ id: "ollama",
46
+ displayName: "Ollama",
47
+ defaultModel: "nomic-embed-text",
48
+ requiresKey: false,
49
+ },
50
+ ];
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Provider-specific model field names
54
+ // ---------------------------------------------------------------------------
55
+
56
+ const PROVIDER_MODEL_FIELD: Record<string, string> = {
57
+ local: "localModel",
58
+ openai: "openaiModel",
59
+ gemini: "geminiModel",
60
+ ollama: "ollamaModel",
61
+ };
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // GET — return current embedding config + resolved status
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export async function getEmbeddingConfigInfo(): Promise<{
68
+ provider: string;
69
+ model: string | null;
70
+ activeProvider: string | null;
71
+ activeModel: string | null;
72
+ availableProviders: typeof EMBEDDING_PROVIDER_CATALOG;
73
+ status: { enabled: boolean; degraded: boolean; reason: string | null };
74
+ }> {
75
+ const config = getConfig();
76
+ const embeddingConfig = config.memory.embeddings;
77
+ const backendStatus = await getMemoryBackendStatus(config);
78
+
79
+ // Derive the provider-specific model from config
80
+ const fieldName = PROVIDER_MODEL_FIELD[embeddingConfig.provider];
81
+ const model = fieldName
82
+ ? (embeddingConfig as Record<string, unknown>)[fieldName]
83
+ : null;
84
+
85
+ return {
86
+ provider: embeddingConfig.provider,
87
+ model: typeof model === "string" ? model : null,
88
+ activeProvider: backendStatus.provider,
89
+ activeModel: backendStatus.model,
90
+ availableProviders: EMBEDDING_PROVIDER_CATALOG,
91
+ status: {
92
+ enabled: backendStatus.enabled,
93
+ degraded: backendStatus.degraded,
94
+ reason: backendStatus.reason,
95
+ },
96
+ };
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // PUT — persist embedding provider/model to config
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export async function setEmbeddingConfig(
104
+ provider: string,
105
+ model: string | undefined,
106
+ ctx: ModelSetContext,
107
+ ): Promise<ReturnType<typeof getEmbeddingConfigInfo>> {
108
+ const validProviders = new Set<string>(VALID_MEMORY_EMBEDDING_PROVIDERS);
109
+ if (!validProviders.has(provider)) {
110
+ throw new Error(
111
+ `Invalid embedding provider "${provider}". Valid providers: ${[...validProviders].join(", ")}`,
112
+ );
113
+ }
114
+
115
+ const raw = loadRawConfig();
116
+ setMemoryEmbeddingField(raw, "provider", provider);
117
+
118
+ if (model !== undefined) {
119
+ const fieldName = PROVIDER_MODEL_FIELD[provider];
120
+ if (fieldName) {
121
+ setMemoryEmbeddingField(raw, fieldName, model);
122
+ }
123
+ }
124
+
125
+ // Suppress the file watcher callback — we handle the reload ourselves.
126
+ const wasSuppressed = ctx.suppressConfigReload;
127
+ ctx.setSuppressConfigReload(true);
128
+ try {
129
+ saveRawConfig(raw);
130
+ } catch (err) {
131
+ ctx.setSuppressConfigReload(wasSuppressed);
132
+ throw err;
133
+ }
134
+ ctx.debounceTimers.schedule(
135
+ "__suppress_reset__",
136
+ () => {
137
+ ctx.setSuppressConfigReload(false);
138
+ },
139
+ CONFIG_RELOAD_DEBOUNCE_MS,
140
+ );
141
+
142
+ clearEmbeddingBackendCache();
143
+ ctx.updateConfigFingerprint();
144
+
145
+ log.info({ provider, model }, "Embedding config updated");
146
+
147
+ return getEmbeddingConfigInfo();
148
+ }