@vellumai/assistant 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -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,20 @@ 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"),
33
+ memoryReducedThroughMessageId: text("memory_reduced_through_message_id"),
34
+ memoryDirtyTailSinceMessageId: text("memory_dirty_tail_since_message_id"),
35
+ memoryLastReducedAt: integer("memory_last_reduced_at"),
31
36
  },
32
37
  (table) => [
33
38
  index("idx_conversations_updated_at").on(table.updatedAt),
34
39
  index("idx_conversations_conversation_type").on(table.conversationType),
40
+ index("idx_conversations_fork_parent_conversation_id").on(
41
+ table.forkParentConversationId,
42
+ ),
35
43
  ],
36
44
  );
37
45
 
@@ -88,6 +96,7 @@ export const attachments = sqliteTable("attachments", {
88
96
  dataBase64: text("data_base64").notNull(),
89
97
  contentHash: text("content_hash"),
90
98
  thumbnailBase64: text("thumbnail_base64"),
99
+ filePath: text("file_path"),
91
100
  createdAt: integer("created_at").notNull(),
92
101
  });
93
102
 
@@ -3,6 +3,8 @@ export * from "./contacts.js";
3
3
  export * from "./conversations.js";
4
4
  export * from "./guardian.js";
5
5
  export * from "./infrastructure.js";
6
+ export * from "./memory-archive.js";
7
+ export * from "./memory-brief.js";
6
8
  export * from "./memory-core.js";
7
9
  export * from "./notifications.js";
8
10
  export * from "./oauth.js";
@@ -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",
@@ -0,0 +1,121 @@
1
+ import {
2
+ index,
3
+ integer,
4
+ sqliteTable,
5
+ text,
6
+ uniqueIndex,
7
+ } from "drizzle-orm/sqlite-core";
8
+
9
+ import { conversations, messages } from "./conversations.js";
10
+
11
+ /**
12
+ * Raw observation records captured from conversation turns. Each observation
13
+ * is a single factual statement extracted from user or assistant messages,
14
+ * annotated with modality and source metadata for downstream recall.
15
+ */
16
+ export const memoryObservations = sqliteTable(
17
+ "memory_observations",
18
+ {
19
+ id: text("id").primaryKey(),
20
+ scopeId: text("scope_id").notNull().default("default"),
21
+ conversationId: text("conversation_id")
22
+ .notNull()
23
+ .references(() => conversations.id, { onDelete: "cascade" }),
24
+ messageId: text("message_id").references(() => messages.id, {
25
+ onDelete: "set null",
26
+ }),
27
+ /** The role that produced the observation (e.g. "user", "assistant"). */
28
+ role: text("role").notNull(),
29
+ /** Free-text statement capturing the observed fact. */
30
+ content: text("content").notNull(),
31
+ /**
32
+ * Modality of the source material: "text", "voice", "image", etc.
33
+ * Enables downstream filters for recall relevance.
34
+ */
35
+ modality: text("modality").notNull().default("text"),
36
+ /**
37
+ * Source channel or interface that produced the observation
38
+ * (e.g. "vellum", "telegram", "phone").
39
+ */
40
+ source: text("source"),
41
+ createdAt: integer("created_at").notNull(),
42
+ },
43
+ (table) => [
44
+ index("idx_memory_observations_scope_id").on(table.scopeId),
45
+ index("idx_memory_observations_conversation_id").on(table.conversationId),
46
+ index("idx_memory_observations_created_at").on(table.createdAt),
47
+ ],
48
+ );
49
+
50
+ /**
51
+ * Deduplicated content chunks derived from observations. Chunks are the unit
52
+ * of embedding and recall — each chunk carries a contentHash for idempotent
53
+ * dual-write safety so the same content is never stored twice.
54
+ */
55
+ export const memoryChunks = sqliteTable(
56
+ "memory_chunks",
57
+ {
58
+ id: text("id").primaryKey(),
59
+ scopeId: text("scope_id").notNull().default("default"),
60
+ observationId: text("observation_id")
61
+ .notNull()
62
+ .references(() => memoryObservations.id, { onDelete: "cascade" }),
63
+ /** The chunk text used for embedding and recall. */
64
+ content: text("content").notNull(),
65
+ /** Token count estimate for context-window budgeting. */
66
+ tokenEstimate: integer("token_estimate").notNull(),
67
+ /**
68
+ * SHA-256 hash of the normalized content, used to skip duplicate inserts
69
+ * during dual-write windows.
70
+ */
71
+ contentHash: text("content_hash").notNull(),
72
+ createdAt: integer("created_at").notNull(),
73
+ },
74
+ (table) => [
75
+ index("idx_memory_chunks_scope_id").on(table.scopeId),
76
+ index("idx_memory_chunks_observation_id").on(table.observationId),
77
+ uniqueIndex("idx_memory_chunks_content_hash").on(
78
+ table.scopeId,
79
+ table.contentHash,
80
+ ),
81
+ index("idx_memory_chunks_created_at").on(table.createdAt),
82
+ ],
83
+ );
84
+
85
+ /**
86
+ * Episode records that group related observations into coherent narrative
87
+ * units. An episode represents a meaningful interaction or topic span,
88
+ * with source-link metadata for provenance tracking.
89
+ */
90
+ export const memoryEpisodes = sqliteTable(
91
+ "memory_episodes",
92
+ {
93
+ id: text("id").primaryKey(),
94
+ scopeId: text("scope_id").notNull().default("default"),
95
+ conversationId: text("conversation_id")
96
+ .notNull()
97
+ .references(() => conversations.id, { onDelete: "cascade" }),
98
+ /** Human-readable title summarizing the episode. */
99
+ title: text("title").notNull(),
100
+ /** Longer narrative summary of the episode content. */
101
+ summary: text("summary").notNull(),
102
+ /** Token count estimate for the summary. */
103
+ tokenEstimate: integer("token_estimate").notNull(),
104
+ /**
105
+ * Source channel or interface that produced the episode
106
+ * (mirrors observation.source for episode-level filtering).
107
+ */
108
+ source: text("source"),
109
+ /** Epoch-ms timestamp of the earliest observation in the episode. */
110
+ startAt: integer("start_at").notNull(),
111
+ /** Epoch-ms timestamp of the latest observation in the episode. */
112
+ endAt: integer("end_at").notNull(),
113
+ createdAt: integer("created_at").notNull(),
114
+ updatedAt: integer("updated_at").notNull(),
115
+ },
116
+ (table) => [
117
+ index("idx_memory_episodes_scope_id").on(table.scopeId),
118
+ index("idx_memory_episodes_conversation_id").on(table.conversationId),
119
+ index("idx_memory_episodes_created_at").on(table.createdAt),
120
+ ],
121
+ );
@@ -0,0 +1,55 @@
1
+ import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+
3
+ /**
4
+ * Time contexts represent bounded temporal windows that are relevant to the
5
+ * assistant's current awareness — e.g. "user is traveling next week",
6
+ * "quarterly planning period ends Friday". Each row captures one window
7
+ * with an activation range and a human-readable summary the brief can surface.
8
+ */
9
+ export const timeContexts = sqliteTable(
10
+ "time_contexts",
11
+ {
12
+ id: text("id").primaryKey(),
13
+ scopeId: text("scope_id").notNull(),
14
+ summary: text("summary").notNull(),
15
+ source: text("source").notNull(), // e.g. 'conversation', 'schedule', 'manual'
16
+ activeFrom: integer("active_from").notNull(), // epoch ms — window start
17
+ activeUntil: integer("active_until").notNull(), // epoch ms — window end
18
+ createdAt: integer("created_at").notNull(),
19
+ updatedAt: integer("updated_at").notNull(),
20
+ },
21
+ (table) => [
22
+ index("idx_time_contexts_scope_active_until").on(
23
+ table.scopeId,
24
+ table.activeUntil,
25
+ ),
26
+ ],
27
+ );
28
+
29
+ /**
30
+ * Open loops track unresolved items the assistant should follow up on —
31
+ * e.g. "waiting for Bob's reply", "need to file taxes before April 15".
32
+ * Each row carries a status and an optional due date so the brief can
33
+ * prioritise which loops to surface.
34
+ */
35
+ export const openLoops = sqliteTable(
36
+ "open_loops",
37
+ {
38
+ id: text("id").primaryKey(),
39
+ scopeId: text("scope_id").notNull(),
40
+ summary: text("summary").notNull(),
41
+ status: text("status").notNull().default("open"), // 'open' | 'resolved' | 'expired'
42
+ source: text("source").notNull(), // e.g. 'conversation', 'followup', 'manual'
43
+ dueAt: integer("due_at"), // epoch ms — optional deadline
44
+ surfacedAt: integer("surfaced_at"), // epoch ms — last time shown in brief
45
+ createdAt: integer("created_at").notNull(),
46
+ updatedAt: integer("updated_at").notNull(),
47
+ },
48
+ (table) => [
49
+ index("idx_open_loops_scope_status_due").on(
50
+ table.scopeId,
51
+ table.status,
52
+ table.dueAt,
53
+ ),
54
+ ],
55
+ );
@@ -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
  });
@@ -61,6 +61,7 @@ export async function semanticSearch(
61
61
  fetchLimit,
62
62
  ["item", "summary", "segment", "media"],
63
63
  excludedMessageIds,
64
+ scopeIds,
64
65
  ),
65
66
  );
66
67
  }
@@ -277,13 +278,13 @@ export async function semanticSearch(
277
278
  * Build a Qdrant filter for hybrid search. Mirrors the logic in
278
279
  * `searchWithFilter` but as a standalone object for the query API.
279
280
  *
280
- * Scope filtering: items and media store `memory_scope_id` on the Qdrant
281
- * point payload, so we can filter at the Qdrant level. Segments and
282
- * summaries rely on post-query DB filtering (same as dense-only search).
281
+ * Scope filtering: points with a `memory_scope_id` payload field are
282
+ * filtered at the Qdrant level. Legacy points without the field pass
283
+ * through and are caught by post-query DB filtering.
283
284
  */
284
285
  function buildHybridFilter(
285
286
  excludeMessageIds: string[],
286
- _scopeIds?: string[],
287
+ scopeIds?: string[],
287
288
  ): Record<string, unknown> {
288
289
  const mustConditions: Array<Record<string, unknown>> = [
289
290
  {
@@ -310,6 +311,18 @@ function buildHybridFilter(
310
311
  });
311
312
  }
312
313
 
314
+ // Scope filtering: accept points whose memory_scope_id matches one of the
315
+ // allowed scopes, OR points that lack the field entirely (legacy data).
316
+ // Post-query DB filtering remains as defense-in-depth for legacy points.
317
+ if (scopeIds && scopeIds.length > 0) {
318
+ mustConditions.push({
319
+ should: [
320
+ { key: "memory_scope_id", match: { any: scopeIds } },
321
+ { is_empty: { key: "memory_scope_id" } },
322
+ ],
323
+ });
324
+ }
325
+
313
326
  const mustNotConditions: Array<Record<string, unknown>> = [
314
327
  { key: "_meta", match: { value: true } },
315
328
  ];
@@ -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
  });