@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
@@ -373,7 +373,7 @@ export async function buildMemoryRecall(
373
373
  // those messages are no longer in the conversation history and memory is
374
374
  // the only way they can influence the response.
375
375
  if (conversationId) {
376
- const inContextMessageIds = getInContextMessageIds(conversationId);
376
+ const inContextMessageIds = getEffectiveInContextMessageIds(conversationId);
377
377
  if (inContextMessageIds) {
378
378
  for (const [key, c] of candidateMap) {
379
379
  if (c.type === "segment") {
@@ -392,6 +392,51 @@ export async function buildMemoryRecall(
392
392
  }
393
393
  }
394
394
  }
395
+
396
+ // ── Item filtering: exclude items whose ALL sources are in-context ──
397
+ // Items distilled from messages the model can already see are redundant.
398
+ // However, items with ANY source outside the in-context set carry
399
+ // cross-conversation information and must be preserved.
400
+ const itemCandidateIds = [...candidateMap.values()]
401
+ .filter((c) => c.type === "item")
402
+ .map((c) => c.id);
403
+
404
+ if (itemCandidateIds.length > 0) {
405
+ try {
406
+ const db = getDb();
407
+ const allSources = db
408
+ .select({
409
+ memoryItemId: memoryItemSources.memoryItemId,
410
+ messageId: memoryItemSources.messageId,
411
+ })
412
+ .from(memoryItemSources)
413
+ .where(inArray(memoryItemSources.memoryItemId, itemCandidateIds))
414
+ .all();
415
+
416
+ // Build item ID → source message IDs map
417
+ const itemSourceMap = new Map<string, string[]>();
418
+ for (const s of allSources) {
419
+ const existing = itemSourceMap.get(s.memoryItemId);
420
+ if (existing) existing.push(s.messageId);
421
+ else itemSourceMap.set(s.memoryItemId, [s.messageId]);
422
+ }
423
+
424
+ // Filter items whose ALL sources are in-context
425
+ for (const [key, c] of candidateMap) {
426
+ if (c.type !== "item") continue;
427
+ const sourceMessageIds = itemSourceMap.get(c.id);
428
+ if (!sourceMessageIds || sourceMessageIds.length === 0) continue;
429
+ if (sourceMessageIds.every((mid) => inContextMessageIds.has(mid))) {
430
+ candidateMap.delete(key);
431
+ }
432
+ }
433
+ } catch (err) {
434
+ log.warn(
435
+ { err },
436
+ "Failed to fetch item sources for in-context filtering; skipping",
437
+ );
438
+ }
439
+ }
395
440
  }
396
441
  }
397
442
 
@@ -574,14 +619,22 @@ export async function buildMemoryRecall(
574
619
  }
575
620
 
576
621
  /**
577
- * Get the set of message IDs that are still in the conversation's context
578
- * window (i.e., not compacted away). Uses `contextCompactedMessageCount` to
579
- * determine the offset: messages ordered by createdAt after that count are
580
- * still visible to the model.
622
+ * Get the set of message IDs that are effectively in the conversation's
623
+ * context window. This includes:
624
+ * 1. Messages still visible (not compacted) in the conversation history.
625
+ * 2. Fork-source message IDs when a conversation is forked, messages are
626
+ * copied with new IDs but their metadata stores the original parent
627
+ * message ID as `forkSourceMessageId`. Segments sourced from those parent
628
+ * messages are redundant because the fork already contains their content.
629
+ *
630
+ * Uses `contextCompactedMessageCount` to determine the compaction offset:
631
+ * messages ordered by createdAt after that count are still visible to the model.
581
632
  *
582
633
  * Returns `null` if the conversation is not found (deleted, or no DB row).
583
634
  */
584
- function getInContextMessageIds(conversationId: string): Set<string> | null {
635
+ function getEffectiveInContextMessageIds(
636
+ conversationId: string,
637
+ ): Set<string> | null {
585
638
  try {
586
639
  const db = getDb();
587
640
 
@@ -599,9 +652,9 @@ function getInContextMessageIds(conversationId: string): Set<string> | null {
599
652
 
600
653
  const offset = conv.contextCompactedMessageCount;
601
654
 
602
- // Fetch message IDs ordered by creation time, skipping compacted ones
655
+ // Fetch message IDs and metadata ordered by creation time
603
656
  const rows = db
604
- .select({ id: messages.id })
657
+ .select({ id: messages.id, metadata: messages.metadata })
605
658
  .from(messages)
606
659
  .where(eq(messages.conversationId, conversationId))
607
660
  .orderBy(asc(messages.createdAt))
@@ -609,7 +662,30 @@ function getInContextMessageIds(conversationId: string): Set<string> | null {
609
662
 
610
663
  // Messages up to `offset` have been compacted out of context
611
664
  const inContextRows = rows.slice(offset);
612
- return new Set(inContextRows.map((r) => r.id));
665
+ const idSet = new Set(inContextRows.map((r) => r.id));
666
+
667
+ // Also include fork-source message IDs from in-context messages.
668
+ // When a conversation is forked, each copied message's metadata contains
669
+ // `forkSourceMessageId` pointing to the original (parent or grandparent)
670
+ // message ID. Segments sourced from those original messages are redundant.
671
+ for (const row of inContextRows) {
672
+ if (!row.metadata) continue;
673
+ try {
674
+ const parsed = JSON.parse(row.metadata);
675
+ if (
676
+ parsed &&
677
+ typeof parsed === "object" &&
678
+ !Array.isArray(parsed) &&
679
+ typeof parsed.forkSourceMessageId === "string"
680
+ ) {
681
+ idSet.add(parsed.forkSourceMessageId);
682
+ }
683
+ } catch {
684
+ // Invalid metadata JSON — skip, don't break filtering.
685
+ }
686
+ }
687
+
688
+ return idSet;
613
689
  } catch (err) {
614
690
  log.warn(
615
691
  { err },
@@ -26,12 +26,17 @@ export const conversations = sqliteTable(
26
26
  memoryScopeId: text("memory_scope_id").notNull().default("default"),
27
27
  originChannel: text("origin_channel"),
28
28
  originInterface: text("origin_interface"),
29
+ forkParentConversationId: text("fork_parent_conversation_id"),
30
+ forkParentMessageId: text("fork_parent_message_id"),
29
31
  isAutoTitle: integer("is_auto_title").notNull().default(1),
30
32
  scheduleJobId: text("schedule_job_id"),
31
33
  },
32
34
  (table) => [
33
35
  index("idx_conversations_updated_at").on(table.updatedAt),
34
36
  index("idx_conversations_conversation_type").on(table.conversationType),
37
+ index("idx_conversations_fork_parent_conversation_id").on(
38
+ table.forkParentConversationId,
39
+ ),
35
40
  ],
36
41
  );
37
42
 
@@ -88,6 +93,7 @@ export const attachments = sqliteTable("attachments", {
88
93
  dataBase64: text("data_base64").notNull(),
89
94
  contentHash: text("content_hash"),
90
95
  thumbnailBase64: text("thumbnail_base64"),
96
+ filePath: text("file_path"),
91
97
  createdAt: integer("created_at").notNull(),
92
98
  });
93
99
 
@@ -106,13 +106,19 @@ export const watcherEvents = sqliteTable("watcher_events", {
106
106
  createdAt: integer("created_at").notNull(),
107
107
  });
108
108
 
109
- export const llmRequestLogs = sqliteTable("llm_request_logs", {
110
- id: text("id").primaryKey(),
111
- conversationId: text("conversation_id").notNull(),
112
- requestPayload: text("request_payload").notNull(),
113
- responsePayload: text("response_payload").notNull(),
114
- createdAt: integer("created_at").notNull(),
115
- });
109
+ export const llmRequestLogs = sqliteTable(
110
+ "llm_request_logs",
111
+ {
112
+ id: text("id").primaryKey(),
113
+ conversationId: text("conversation_id").notNull(),
114
+ messageId: text("message_id"),
115
+ provider: text("provider"),
116
+ requestPayload: text("request_payload").notNull(),
117
+ responsePayload: text("response_payload").notNull(),
118
+ createdAt: integer("created_at").notNull(),
119
+ },
120
+ (table) => [index("idx_llm_request_logs_message_id").on(table.messageId)],
121
+ );
116
122
 
117
123
  export const llmUsageEvents = sqliteTable(
118
124
  "llm_usage_events",
@@ -18,6 +18,12 @@ export const oauthProviders = sqliteTable("oauth_providers", {
18
18
  extraParams: text("extra_params"),
19
19
  callbackTransport: text("callback_transport"),
20
20
  pingUrl: text("ping_url"),
21
+ managedServiceConfigKey: text("managed_service_config_key"),
22
+ displayName: text("display_name"),
23
+ description: text("description"),
24
+ dashboardUrl: text("dashboard_url"),
25
+ clientIdPlaceholder: text("client_id_placeholder"),
26
+ requiresClientSecret: integer("requires_client_secret").notNull().default(1),
21
27
  createdAt: integer("created_at").notNull(),
22
28
  updatedAt: integer("updated_at").notNull(),
23
29
  });
@@ -45,7 +45,9 @@ export function buildMultipartMime(options: MimeMessageOptions): string {
45
45
  const sanitizedSubject = sanitizeHeaderValue(subject);
46
46
  const sanitizedCc = cc ? sanitizeHeaderValue(cc) : undefined;
47
47
  const sanitizedBcc = bcc ? sanitizeHeaderValue(bcc) : undefined;
48
- const sanitizedInReplyTo = inReplyTo ? sanitizeHeaderValue(inReplyTo) : undefined;
48
+ const sanitizedInReplyTo = inReplyTo
49
+ ? sanitizeHeaderValue(inReplyTo)
50
+ : undefined;
49
51
 
50
52
  const headers = [
51
53
  `To: ${sanitizedTo}`,
@@ -196,6 +196,14 @@ export function hasInviteFlowDirective(text: string | undefined): boolean {
196
196
  * Build the deterministic access-request contract text from payload fields.
197
197
  * This is the canonical baseline that enforcement can append when generated
198
198
  * copy is missing required elements.
199
+ *
200
+ * Channel-agnostic by design: this function reads from the generic
201
+ * `contextPayload` and works identically regardless of which channel
202
+ * (Slack, Telegram, desktop, etc.) the notification is delivered to.
203
+ * When `guardianResolutionSource` is present and not `"source-channel-contact"`,
204
+ * the guardian was resolved via fallback (e.g. vellum anchor) rather than
205
+ * a verified same-channel contact — downstream copy or routing can use
206
+ * this to append verification CTAs like "Was this you?".
199
207
  */
200
208
  export function buildAccessRequestContractText(
201
209
  payload: Record<string, unknown>,
@@ -208,6 +216,15 @@ export function buildAccessRequestContractText(
208
216
  ? payload.previousMemberStatus
209
217
  : undefined;
210
218
 
219
+ const guardianResolutionSource =
220
+ typeof payload.guardianResolutionSource === "string"
221
+ ? payload.guardianResolutionSource
222
+ : undefined;
223
+ const sourceChannel =
224
+ typeof payload.sourceChannel === "string"
225
+ ? payload.sourceChannel
226
+ : undefined;
227
+
211
228
  const lines: string[] = [];
212
229
  lines.push(buildAccessRequestIdentityLine(payload));
213
230
  if (previousMemberStatus === "revoked") {
@@ -220,6 +237,15 @@ export function buildAccessRequestContractText(
220
237
  );
221
238
  }
222
239
  lines.push(buildAccessRequestInviteDirective());
240
+ if (
241
+ (guardianResolutionSource === "vellum-anchor" ||
242
+ guardianResolutionSource === "none") &&
243
+ sourceChannel
244
+ ) {
245
+ lines.push(
246
+ `Note: You haven't verified your identity on ${sourceChannel} yet. If this was you trying to message your assistant, say "help me verify as guardian on ${sourceChannel}" to set up direct access.`,
247
+ );
248
+ }
223
249
  return lines.join("\n");
224
250
  }
225
251
 
@@ -22,6 +22,7 @@ import {
22
22
  } from "../providers/provider-send-message.js";
23
23
  import type { ModelIntent, Provider } from "../providers/types.js";
24
24
  import { getLogger } from "../util/logger.js";
25
+ import { truncate } from "../util/truncate.js";
25
26
  import {
26
27
  buildConversationCandidates,
27
28
  type ConversationCandidateSet,
@@ -55,6 +56,15 @@ const log = getLogger("notification-decision-engine");
55
56
  const DECISION_TIMEOUT_MS = 15_000;
56
57
  const PROMPT_VERSION = "v4";
57
58
 
59
+ /**
60
+ * Maximum character budget for identity context injected into the notification
61
+ * decision prompt. We truncate to prevent oversized prompts when SOUL.md /
62
+ * IDENTITY.md / USER.md are large — exceeding the provider context window
63
+ * would cause the LLM call to fail and silently degrade to deterministic
64
+ * fallback for all notifications.
65
+ */
66
+ const MAX_IDENTITY_CONTEXT_CHARS = 2000;
67
+
58
68
  // ── System prompt ──────────────────────────────────────────────────────
59
69
 
60
70
  function buildSystemPrompt(
@@ -790,7 +800,10 @@ async function classifyWithLLM(
790
800
  const candidateContext = candidateSet
791
801
  ? (serializeCandidatesForPrompt(candidateSet) ?? undefined)
792
802
  : undefined;
793
- const identityContext = buildCoreIdentityContext() ?? undefined;
803
+ const rawIdentityContext = buildCoreIdentityContext();
804
+ const identityContext = rawIdentityContext
805
+ ? truncate(rawIdentityContext, MAX_IDENTITY_CONTEXT_CHARS, "\n…[truncated]")
806
+ : undefined;
794
807
  const systemPrompt = buildSystemPrompt(
795
808
  availableChannels,
796
809
  preferenceContext,
@@ -220,7 +220,7 @@ export async function emitNotificationSignal<TEventName extends string>(
220
220
  sourceChannel: params.sourceChannel,
221
221
  sourceContextId: params.sourceContextId,
222
222
  attentionHints: params.attentionHints,
223
- payload: params.contextPayload ?? {},
223
+ payload: (params.contextPayload ?? {}) as Record<string, unknown>,
224
224
  dedupeKey: params.dedupeKey,
225
225
  });
226
226
 
@@ -118,8 +118,44 @@ export interface AttentionHints {
118
118
 
119
119
  export type RoutingIntent = "single_channel" | "multi_channel" | "all_channels";
120
120
 
121
+ // ── Typed context payloads ──────────────────────────────────────────────
122
+
123
+ /**
124
+ * How the guardian was resolved for an access request.
125
+ *
126
+ * - `"source-channel-contact"` — Guardian was found via the originating channel's
127
+ * contact store and their principalId matches the assistant's anchor.
128
+ * - `"vellum-anchor"` — No same-channel guardian matched; fell back to the
129
+ * assistant's vellum guardian principal.
130
+ * - `"none"` — No guardian binding could be resolved at all.
131
+ *
132
+ * Downstream consumers (notification copy, routing) use this to decide whether
133
+ * to append a "Was this you?" CTA or route notifications beyond the source channel.
134
+ * This is channel-agnostic by design — any channel's access request that
135
+ * resolves to a non-source-channel guardian gets the same treatment.
136
+ */
137
+ export type GuardianResolutionSource =
138
+ | "source-channel-contact"
139
+ | "vellum-anchor"
140
+ | "none";
141
+
142
+ export interface AccessRequestContextPayload {
143
+ requestId: string;
144
+ requestCode: string;
145
+ sourceChannel: string;
146
+ conversationExternalId: string;
147
+ actorExternalId: string;
148
+ actorDisplayName: string | null;
149
+ actorUsername: string | null;
150
+ senderIdentifier: string;
151
+ guardianBindingChannel: string | null;
152
+ guardianResolutionSource: GuardianResolutionSource;
153
+ previousMemberStatus: string | null;
154
+ }
155
+
121
156
  export interface NotificationEventContextPayloadMap {
122
157
  "guardian.question": GuardianQuestionPayload;
158
+ "ingress.access_request": AccessRequestContextPayload;
123
159
  }
124
160
 
125
161
  export type NotificationContextPayload<TEventName extends string = string> =
@@ -236,8 +236,6 @@ function createConnection(service = "integration:google"): BYOOAuthConnection {
236
236
  providerKey: service,
237
237
  baseUrl: "https://gmail.googleapis.com/gmail/v1/users/me",
238
238
  accountInfo: null,
239
- grantedScopes: ["read", "write"],
240
- credentialService: service,
241
239
  });
242
240
  }
243
241
 
@@ -500,12 +498,11 @@ describe("resolveOAuthConnection", () => {
500
498
 
501
499
  expect(conn).toBeInstanceOf(BYOOAuthConnection);
502
500
  expect(conn.providerKey).toBe("integration:google");
503
- expect(conn.grantedScopes).toEqual(["read", "write"]);
504
501
  });
505
502
 
506
503
  test("throws when no credential metadata exists", async () => {
507
504
  await expect(resolveOAuthConnection("integration:unknown")).rejects.toThrow(
508
- /No credential found for "integration:unknown"/,
505
+ /No active OAuth connection found for "integration:unknown"/,
509
506
  );
510
507
  });
511
508
 
@@ -517,45 +514,4 @@ describe("resolveOAuthConnection", () => {
517
514
  /No base URL configured for "integration:custom-service"/,
518
515
  );
519
516
  });
520
-
521
- test("resolves base URL via app's canonical providerKey for custom credential_service", async () => {
522
- // Set up a well-known provider with a baseUrl
523
- mockProviders.set("github", {
524
- key: "github",
525
- tokenUrl: "https://github.com/login/oauth/access_token",
526
- baseUrl: "https://api.github.com",
527
- });
528
- // The custom credential service has no provider entry of its own
529
- // (getProvider("integration:github-work") returns undefined)
530
-
531
- // App points to the canonical "github" provider
532
- const appId = "app-github-work";
533
- mockApps.set(appId, {
534
- id: appId,
535
- providerKey: "github",
536
- clientId: "test-client-id",
537
- clientSecretCredentialPath: `oauth_app/${appId}/client_secret`,
538
- });
539
-
540
- // Connection uses the custom credential service as its providerKey
541
- const connId = "conn-github-work";
542
- mockConnections.set("integration:github-work", {
543
- id: connId,
544
- providerKey: "integration:github-work",
545
- oauthAppId: appId,
546
- expiresAt: Date.now() + 3600 * 1000,
547
- grantedScopes: JSON.stringify(["repo"]),
548
- accountInfo: null,
549
- });
550
- await setSecureKeyAsync(
551
- `oauth_connection/${connId}/access_token`,
552
- "ghp-test-token",
553
- );
554
-
555
- const conn = await resolveOAuthConnection("integration:github-work");
556
-
557
- expect(conn).toBeInstanceOf(BYOOAuthConnection);
558
- expect(conn.providerKey).toBe("integration:github-work");
559
- expect(conn.grantedScopes).toEqual(["repo"]);
560
- });
561
517
  });
@@ -25,31 +25,25 @@ export interface BYOOAuthConnectionOptions {
25
25
  providerKey: string;
26
26
  baseUrl: string;
27
27
  accountInfo: string | null;
28
- grantedScopes: string[];
29
- credentialService: string;
30
28
  }
31
29
 
32
30
  export class BYOOAuthConnection implements OAuthConnection {
33
31
  readonly id: string;
34
32
  readonly providerKey: string;
35
33
  readonly accountInfo: string | null;
36
- readonly grantedScopes: string[];
37
34
 
38
35
  private readonly baseUrl: string;
39
- private readonly credentialService: string;
40
36
 
41
37
  constructor(opts: BYOOAuthConnectionOptions) {
42
38
  this.id = opts.id;
43
39
  this.providerKey = opts.providerKey;
44
40
  this.baseUrl = opts.baseUrl;
45
41
  this.accountInfo = opts.accountInfo;
46
- this.grantedScopes = opts.grantedScopes;
47
- this.credentialService = opts.credentialService;
48
42
  }
49
43
 
50
44
  async request(req: OAuthConnectionRequest): Promise<OAuthConnectionResponse> {
51
45
  return withValidToken(
52
- this.credentialService,
46
+ this.providerKey,
53
47
  async (token) => {
54
48
  const effectiveBaseUrl = req.baseUrl ?? this.baseUrl;
55
49
  let fullUrl = `${effectiveBaseUrl}${req.path}`;
@@ -106,7 +100,7 @@ export class BYOOAuthConnection implements OAuthConnection {
106
100
  }
107
101
 
108
102
  async withToken<T>(fn: (token: string) => Promise<T>): Promise<T> {
109
- return withValidToken(this.credentialService, fn, {
103
+ return withValidToken(this.providerKey, fn, {
110
104
  connectionId: this.id,
111
105
  });
112
106
  }
@@ -252,12 +252,13 @@ export async function orchestrateOAuthConnect(
252
252
  prepared.completion
253
253
  .then(async (result) => {
254
254
  try {
255
- let accountInfo: string | undefined;
255
+ let parsedAccountIdentifier: string | undefined;
256
256
 
257
- // Run identity verifier if available (code-side behavior)
257
+ // Parse account identifier from the provider's identity endpoint.
258
+ // Best-effort — format varies by provider and may fail.
258
259
  if (behavior.identityVerifier) {
259
260
  try {
260
- accountInfo = await behavior.identityVerifier(
261
+ parsedAccountIdentifier = await behavior.identityVerifier(
261
262
  result.tokens.accessToken,
262
263
  );
263
264
  } catch {
@@ -270,19 +271,19 @@ export async function orchestrateOAuthConnect(
270
271
  tokens: result.tokens,
271
272
  grantedScopes: result.grantedScopes,
272
273
  rawTokenResponse: result.rawTokenResponse,
273
- identityAccountInfo: accountInfo,
274
+ parsedAccountIdentifier,
274
275
  });
275
276
  log.info(
276
277
  {
277
278
  service: resolvedService,
278
- accountInfo: stored.accountInfo ?? accountInfo,
279
+ accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
279
280
  },
280
281
  "Deferred OAuth2 flow completed — tokens stored",
281
282
  );
282
283
  options.onDeferredComplete?.({
283
284
  success: true,
284
285
  service: resolvedService,
285
- accountInfo: stored.accountInfo ?? accountInfo,
286
+ accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
286
287
  });
287
288
  } catch (err) {
288
289
  log.error(
@@ -353,11 +354,14 @@ export async function orchestrateOAuthConnect(
353
354
  : undefined,
354
355
  );
355
356
 
356
- // Run identity verifier if available (code-side behavior)
357
- let verifiedIdentity: string | undefined;
357
+ // Parse account identifier from the provider's identity endpoint.
358
+ // Best-effort format varies by provider and may fail.
359
+ let parsedAccountIdentifier: string | undefined;
358
360
  if (behavior.identityVerifier) {
359
361
  try {
360
- verifiedIdentity = await behavior.identityVerifier(tokens.accessToken);
362
+ parsedAccountIdentifier = await behavior.identityVerifier(
363
+ tokens.accessToken,
364
+ );
361
365
  } catch {
362
366
  // Non-fatal
363
367
  }
@@ -368,14 +372,14 @@ export async function orchestrateOAuthConnect(
368
372
  tokens,
369
373
  grantedScopes,
370
374
  rawTokenResponse,
371
- identityAccountInfo: verifiedIdentity,
375
+ parsedAccountIdentifier,
372
376
  });
373
377
 
374
378
  return {
375
379
  success: true,
376
380
  deferred: false,
377
381
  grantedScopes,
378
- accountInfo: accountInfo ?? verifiedIdentity,
382
+ accountInfo: accountInfo ?? parsedAccountIdentifier,
379
383
  };
380
384
  } catch (err: unknown) {
381
385
  const message =