@vellumai/assistant 0.9.0 → 0.10.0-staging.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 (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -1,12 +1,14 @@
1
1
  import { and, asc, desc, eq, isNotNull, like, sql } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
+ import type { ChannelId } from "../channels/types.js";
4
5
  import { getDb } from "../memory/db-connection.js";
5
6
  import {
6
7
  assistantContactMetadata,
7
8
  contactChannels,
8
9
  contacts,
9
10
  } from "../memory/schema.js";
11
+ import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
10
12
  import { emitContactChange } from "./contact-events.js";
11
13
  import type {
12
14
  AssistantContactMetadata,
@@ -27,6 +29,27 @@ function escapeLike(value: string): string {
27
29
  return value.replace(/%/g, "").replace(/_/g, "");
28
30
  }
29
31
 
32
+ /**
33
+ * Find the first contact_channels row whose (type, address) matches.
34
+ * Uses COLLATE NOCASE to find legacy lowercased rows (pre-migration 290).
35
+ */
36
+ function findConflictingChannel(
37
+ db: ReturnType<typeof getDb>,
38
+ type: string,
39
+ address: string,
40
+ ) {
41
+ return db
42
+ .select()
43
+ .from(contactChannels)
44
+ .where(
45
+ and(
46
+ eq(contactChannels.type, type),
47
+ sql`${contactChannels.address} = ${address} COLLATE NOCASE`,
48
+ ),
49
+ )
50
+ .get();
51
+ }
52
+
30
53
  /**
31
54
  * Pure slug transform applied to a display name. No DB lookup, no collision
32
55
  * handling — callers that need a collision-free filename should use
@@ -93,7 +116,6 @@ function parseChannel(
93
116
  type: row.type,
94
117
  address: row.address,
95
118
  isPrimary: row.isPrimary,
96
- externalUserId: row.externalUserId,
97
119
  externalChatId: row.externalChatId,
98
120
  status: row.status as ContactChannel["status"],
99
121
  policy: row.policy as ContactChannel["policy"],
@@ -139,7 +161,6 @@ interface SyncChannelData {
139
161
  type: string;
140
162
  address: string;
141
163
  isPrimary?: boolean;
142
- externalUserId?: string | null;
143
164
  externalChatId?: string | null;
144
165
  status?: ChannelStatus;
145
166
  policy?: ChannelPolicy;
@@ -152,15 +173,7 @@ interface SyncChannelData {
152
173
 
153
174
  // ── CRUD ─────────────────────────────────────────────────────────────
154
175
 
155
- /** Retrieve a contact by ID.
156
- * Used by functions that have already resolved identity through channel lookups. */
157
- export function getContactInternal(id: string): ContactWithChannels | null {
158
- const db = getDb();
159
- const row = db.select().from(contacts).where(eq(contacts.id, id)).get();
160
- if (!row) return null;
161
- return withChannels(parseContact(row));
162
- }
163
-
176
+ /** Retrieve a contact by ID. */
164
177
  export function getContact(id: string): ContactWithChannels | null {
165
178
  const db = getDb();
166
179
  const row = db.select().from(contacts).where(eq(contacts.id, id)).get();
@@ -168,6 +181,9 @@ export function getContact(id: string): ContactWithChannels | null {
168
181
  return withChannels(parseContact(row));
169
182
  }
170
183
 
184
+ /** @deprecated Use {@link getContact} directly. */
185
+ export const getContactInternal = getContact;
186
+
171
187
  /**
172
188
  * Look up a single contact channel by its primary key.
173
189
  * Returns the parsed channel row, or null if it does not exist.
@@ -199,6 +215,15 @@ export function upsertContact(params: {
199
215
  const db = getDb();
200
216
  const now = Date.now();
201
217
 
218
+ // Canonicalize all channel addresses up front so every downstream path
219
+ // (lookups, inserts, conflict checks) uses the canonical form.
220
+ const canonicalChannels = params.channels?.map((ch) => ({
221
+ ...ch,
222
+ address:
223
+ canonicalizeInboundIdentity(ch.type as ChannelId, ch.address) ??
224
+ ch.address,
225
+ }));
226
+
202
227
  let contactId = params.id;
203
228
 
204
229
  // If an ID is provided, check if the contact exists for update
@@ -226,10 +251,10 @@ export function upsertContact(params: {
226
251
  .where(eq(contacts.id, contactId))
227
252
  .run();
228
253
 
229
- if (params.channels) {
254
+ if (canonicalChannels) {
230
255
  syncChannels(
231
256
  contactId,
232
- params.channels,
257
+ canonicalChannels,
233
258
  now,
234
259
  params.reassignConflictingChannels,
235
260
  );
@@ -240,20 +265,10 @@ export function upsertContact(params: {
240
265
  }
241
266
  }
242
267
 
243
- // Try to find by channel address to avoid duplicates
244
- if (!contactId && params.channels && params.channels.length > 0) {
245
- for (const ch of params.channels) {
246
- // Primary lookup: match by (type, address)
247
- const existingChannel = db
248
- .select()
249
- .from(contactChannels)
250
- .where(
251
- and(
252
- eq(contactChannels.type, ch.type),
253
- eq(contactChannels.address, ch.address.toLowerCase()),
254
- ),
255
- )
256
- .get();
268
+ // Try to find by channel canonical identity to avoid duplicates
269
+ if (!contactId && canonicalChannels && canonicalChannels.length > 0) {
270
+ for (const ch of canonicalChannels) {
271
+ const existingChannel = findConflictingChannel(db, ch.type, ch.address);
257
272
 
258
273
  if (existingChannel) {
259
274
  contactId = existingChannel.contactId;
@@ -274,7 +289,7 @@ export function upsertContact(params: {
274
289
  .where(eq(contacts.id, contactId))
275
290
  .run();
276
291
 
277
- syncChannels(contactId, params.channels, now);
292
+ syncChannels(contactId, canonicalChannels, now);
278
293
  emitContactChange();
279
294
  return { ...getContactInternal(contactId)!, created: false };
280
295
  }
@@ -318,10 +333,10 @@ export function upsertContact(params: {
318
333
  })
319
334
  .run();
320
335
 
321
- if (params.channels) {
336
+ if (canonicalChannels) {
322
337
  syncChannels(
323
338
  contactId,
324
- params.channels,
339
+ canonicalChannels,
325
340
  now,
326
341
  params.reassignConflictingChannels,
327
342
  );
@@ -345,9 +360,8 @@ function syncChannels(
345
360
  const db = getDb();
346
361
 
347
362
  for (const ch of channels) {
348
- const normalizedAddress = ch.address.toLowerCase();
349
-
350
- // Check if this channel already exists for this contact
363
+ // Match by (type, address) — the canonical identity for all channel types.
364
+ // COLLATE NOCASE catches legacy rows that were lowercased by old write paths.
351
365
  const existing = db
352
366
  .select()
353
367
  .from(contactChannels)
@@ -355,7 +369,7 @@ function syncChannels(
355
369
  and(
356
370
  eq(contactChannels.contactId, contactId),
357
371
  eq(contactChannels.type, ch.type),
358
- eq(contactChannels.address, normalizedAddress),
372
+ sql`${contactChannels.address} = ${ch.address} COLLATE NOCASE`,
359
373
  ),
360
374
  )
361
375
  .get();
@@ -367,9 +381,9 @@ function syncChannels(
367
381
  const isBlocked = existing.status === "blocked";
368
382
 
369
383
  const updateSet: Record<string, unknown> = {};
384
+ // Self-heal legacy lowercased addresses to canonical form.
385
+ if (existing.address !== ch.address) updateSet.address = ch.address;
370
386
  if (ch.isPrimary !== undefined) updateSet.isPrimary = ch.isPrimary;
371
- if (ch.externalUserId !== undefined)
372
- updateSet.externalUserId = ch.externalUserId;
373
387
  if (ch.externalChatId !== undefined)
374
388
  updateSet.externalChatId = ch.externalChatId;
375
389
  if (!isBlocked) {
@@ -394,17 +408,8 @@ function syncChannels(
394
408
  continue;
395
409
  }
396
410
 
397
- // Check if this channel exists for a different contact (unique constraint)
398
- const conflicting = db
399
- .select()
400
- .from(contactChannels)
401
- .where(
402
- and(
403
- eq(contactChannels.type, ch.type),
404
- eq(contactChannels.address, normalizedAddress),
405
- ),
406
- )
407
- .get();
411
+ // Check if this channel's canonical identity conflicts with another contact.
412
+ const conflicting = findConflictingChannel(db, ch.type, ch.address);
408
413
 
409
414
  if (conflicting) {
410
415
  if (reassignConflicting) {
@@ -419,8 +424,6 @@ function syncChannels(
419
424
  contactId,
420
425
  updatedAt: now,
421
426
  };
422
- if (ch.externalUserId !== undefined)
423
- reassignSet.externalUserId = ch.externalUserId;
424
427
  if (ch.externalChatId !== undefined)
425
428
  reassignSet.externalChatId = ch.externalChatId;
426
429
  if (!isBlocked) {
@@ -451,9 +454,8 @@ function syncChannels(
451
454
  id: uuid(),
452
455
  contactId,
453
456
  type: ch.type,
454
- address: normalizedAddress,
457
+ address: ch.address,
455
458
  isPrimary: ch.isPrimary ?? false,
456
- externalUserId: ch.externalUserId ?? null,
457
459
  externalChatId: ch.externalChatId ?? null,
458
460
  status: ch.status ?? "unverified",
459
461
  policy: ch.policy ?? "allow",
@@ -480,8 +482,8 @@ export function searchContacts(params: {
480
482
 
481
483
  // Search by channel address first (exact or partial match)
482
484
  if (params.channelAddress) {
483
- const normalizedAddress = escapeLike(params.channelAddress.toLowerCase());
484
- if (!normalizedAddress) return [];
485
+ const escapedAddress = escapeLike(params.channelAddress);
486
+ if (!escapedAddress) return [];
485
487
  const channelRows = db
486
488
  .select({ contactId: contactChannels.contactId })
487
489
  .from(contactChannels)
@@ -490,9 +492,9 @@ export function searchContacts(params: {
490
492
  params.channelType
491
493
  ? and(
492
494
  eq(contactChannels.type, params.channelType),
493
- like(contactChannels.address, `%${normalizedAddress}%`),
495
+ like(contactChannels.address, `%${escapedAddress}%`),
494
496
  )
495
- : and(like(contactChannels.address, `%${normalizedAddress}%`)),
497
+ : and(like(contactChannels.address, `%${escapedAddress}%`)),
496
498
  )
497
499
  .all();
498
500
 
@@ -675,6 +677,8 @@ export function mergeContacts(
675
677
  .all();
676
678
 
677
679
  for (const ch of donorChannels) {
680
+ // COLLATE NOCASE catches legacy lowercased rows so we don't try to
681
+ // move a donor channel that collides with an existing survivor channel.
678
682
  const exists = tx
679
683
  .select()
680
684
  .from(contactChannels)
@@ -682,7 +686,7 @@ export function mergeContacts(
682
686
  and(
683
687
  eq(contactChannels.contactId, keepId),
684
688
  eq(contactChannels.type, ch.type),
685
- eq(contactChannels.address, ch.address),
689
+ sql`${contactChannels.address} = ${ch.address} COLLATE NOCASE`,
686
690
  ),
687
691
  )
688
692
  .get();
@@ -705,11 +709,15 @@ export function mergeContacts(
705
709
 
706
710
  /**
707
711
  * Find a contact by a specific channel address. Returns null if not found.
712
+ * Canonicalizes the address before querying. Uses COLLATE NOCASE to match
713
+ * legacy lowercased rows that migration 290 couldn't restore.
708
714
  */
709
715
  export function findContactByAddress(
710
716
  type: string,
711
717
  address: string,
712
718
  ): ContactWithChannels | null {
719
+ const canonical =
720
+ canonicalizeInboundIdentity(type as ChannelId, address) ?? address;
713
721
  const db = getDb();
714
722
  const channel = db
715
723
  .select()
@@ -717,31 +725,7 @@ export function findContactByAddress(
717
725
  .where(
718
726
  and(
719
727
  eq(contactChannels.type, type),
720
- eq(contactChannels.address, address.toLowerCase()),
721
- ),
722
- )
723
- .get();
724
-
725
- if (!channel) return null;
726
- return getContactInternal(channel.contactId);
727
- }
728
-
729
- /**
730
- * Find a contact by channel external user ID. This is the key lookup for trust
731
- * resolution — maps a channel-native sender identity to its parent Contact.
732
- */
733
- export function findContactByChannelExternalId(
734
- channelType: string,
735
- externalUserId: string,
736
- ): ContactWithChannels | null {
737
- const db = getDb();
738
- const channel = db
739
- .select()
740
- .from(contactChannels)
741
- .where(
742
- and(
743
- eq(contactChannels.type, channelType),
744
- eq(contactChannels.externalUserId, externalUserId),
728
+ sql`${contactChannels.address} = ${canonical} COLLATE NOCASE`,
745
729
  ),
746
730
  )
747
731
  .get();
@@ -751,8 +735,9 @@ export function findContactByChannelExternalId(
751
735
  }
752
736
 
753
737
  /**
754
- * Find a contact by channel external chat ID. This is the fallback lookup path
755
- * when externalUserId is not available — matches by (type, externalChatId).
738
+ * Find a contact by channel external chat ID. Fallback for callers that only
739
+ * have a chat ID (no user-level address) — matches by (type, externalChatId).
740
+ * No unique constraint exists on externalChatId, so ORDER BY is needed.
756
741
  */
757
742
  function findContactByChannelExternalChatId(
758
743
  channelType: string,
@@ -768,30 +753,40 @@ function findContactByChannelExternalChatId(
768
753
  eq(contactChannels.externalChatId, externalChatId),
769
754
  ),
770
755
  )
756
+ .orderBy(
757
+ sql`CASE ${contactChannels.status}
758
+ WHEN 'active' THEN 0
759
+ WHEN 'unverified' THEN 1
760
+ ELSE 2
761
+ END`,
762
+ desc(contactChannels.updatedAt),
763
+ )
771
764
  .get();
772
765
  if (!channel) return null;
773
766
  return getContactInternal(channel.contactId);
774
767
  }
775
768
 
776
769
  /**
777
- * Find a contact and matching channel by trying externalUserId first, then
770
+ * Find a contact and matching channel by trying address first, then
778
771
  * falling back to externalChatId. Mirrors the findMember lookup strategy.
779
772
  */
780
773
  export function findContactChannel(params: {
781
774
  channelType: string;
782
- externalUserId?: string;
775
+ address?: string;
783
776
  externalChatId?: string;
784
777
  }): { contact: ContactWithChannels; channel: ContactChannel } | null {
785
- if (params.externalUserId) {
786
- const contact = findContactByChannelExternalId(
787
- params.channelType,
788
- params.externalUserId,
789
- );
778
+ if (params.address) {
779
+ const canonical =
780
+ canonicalizeInboundIdentity(
781
+ params.channelType as ChannelId,
782
+ params.address,
783
+ ) ?? params.address;
784
+ const contact = findContactByAddress(params.channelType, canonical);
790
785
  if (contact) {
791
786
  const ch = contact.channels.find(
792
787
  (c) =>
793
788
  c.type === params.channelType &&
794
- c.externalUserId === params.externalUserId,
789
+ c.address.toLowerCase() === canonical.toLowerCase(),
795
790
  );
796
791
  if (ch) return { contact, channel: ch };
797
792
  }
@@ -952,8 +947,6 @@ export function updateContactPrincipalAndChannel(
952
947
  ): boolean {
953
948
  const db = getDb();
954
949
  const now = Date.now();
955
- const normalizedAddress = newPrincipalId.toLowerCase();
956
-
957
950
  // Look up the channel we're about to update so we know its type.
958
951
  const channel = db
959
952
  .select()
@@ -962,17 +955,8 @@ export function updateContactPrincipalAndChannel(
962
955
  .get();
963
956
  if (!channel) return false;
964
957
 
965
- // Guard: check if another channel row already holds this (type, address).
966
- const conflicting = db
967
- .select()
968
- .from(contactChannels)
969
- .where(
970
- and(
971
- eq(contactChannels.type, channel.type),
972
- eq(contactChannels.address, normalizedAddress),
973
- ),
974
- )
975
- .get();
958
+ // Guard: check if another channel row holds this canonical identity.
959
+ const conflicting = findConflictingChannel(db, channel.type, newPrincipalId);
976
960
 
977
961
  if (conflicting && conflicting.id !== channelId) {
978
962
  return false;
@@ -986,8 +970,7 @@ export function updateContactPrincipalAndChannel(
986
970
 
987
971
  db.update(contactChannels)
988
972
  .set({
989
- externalUserId: newPrincipalId,
990
- address: normalizedAddress,
973
+ address: newPrincipalId,
991
974
  updatedAt: now,
992
975
  })
993
976
  .where(eq(contactChannels.id, channelId))
@@ -6,8 +6,6 @@
6
6
  * identity and access-control state.
7
7
  */
8
8
 
9
- import type { ChannelId } from "../channels/types.js";
10
- import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
11
9
  import { emitContactChange } from "./contact-events.js";
12
10
  import {
13
11
  findContactChannel,
@@ -25,7 +23,6 @@ import type {
25
23
  ContactWriteResult,
26
24
  } from "./types.js";
27
25
 
28
-
29
26
  // ── Guardian operations ──────────────────────────────────────────────
30
27
 
31
28
  /**
@@ -68,12 +65,7 @@ export function upsertContactChannel(params: {
68
65
  let address: string;
69
66
 
70
67
  if (params.externalUserId) {
71
- const canonical =
72
- canonicalizeInboundIdentity(
73
- params.sourceChannel as ChannelId,
74
- params.externalUserId,
75
- ) ?? params.externalUserId;
76
- address = canonical;
68
+ address = params.externalUserId;
77
69
  } else if (params.externalChatId) {
78
70
  address = params.externalChatId;
79
71
  } else {
@@ -81,11 +73,11 @@ export function upsertContactChannel(params: {
81
73
  return null;
82
74
  }
83
75
 
84
- let displayName = params.displayName ?? params.externalUserId ?? "Unknown";
76
+ let displayName = params.displayName ?? address;
85
77
 
86
78
  // When binding a channel to a specific contact (invite redemption), preserve
87
- // the target contact's curated displayName (e.g. "Mom") instead of overwriting
88
- // it with the redeemer's externalUserId.
79
+ // the target contact's curated displayName instead of overwriting it
80
+ // with the raw platform identity.
89
81
  if (params.contactId) {
90
82
  const targetContact = getContact(params.contactId);
91
83
  if (targetContact?.displayName?.trim().length) {
@@ -93,13 +85,6 @@ export function upsertContactChannel(params: {
93
85
  }
94
86
  }
95
87
 
96
- const canonicalId = params.externalUserId
97
- ? (canonicalizeInboundIdentity(
98
- params.sourceChannel as ChannelId,
99
- params.externalUserId,
100
- ) ?? params.externalUserId)
101
- : null;
102
-
103
88
  upsertContact({
104
89
  id: params.contactId,
105
90
  displayName,
@@ -108,7 +93,6 @@ export function upsertContactChannel(params: {
108
93
  {
109
94
  type: params.sourceChannel,
110
95
  address,
111
- externalUserId: canonicalId,
112
96
  externalChatId: params.externalChatId ?? null,
113
97
  status: (params.status as ChannelStatus) ?? undefined,
114
98
  policy: (params.policy as ChannelPolicy) ?? undefined,
@@ -133,7 +117,7 @@ export function upsertContactChannel(params: {
133
117
 
134
118
  const contactResult = findContactChannel({
135
119
  channelType: params.sourceChannel,
136
- externalUserId: canonicalId ?? undefined,
120
+ address,
137
121
  externalChatId: params.externalChatId,
138
122
  });
139
123
 
@@ -174,4 +158,3 @@ export function revokeMember(
174
158
  emitContactChange();
175
159
  return { contact, channel: updatedChannel };
176
160
  }
177
-
@@ -62,7 +62,6 @@ export interface ContactChannel {
62
62
  type: string; // 'email' | 'slack' | 'whatsapp' | 'phone' | etc.
63
63
  address: string;
64
64
  isPrimary: boolean;
65
- externalUserId: string | null;
66
65
  externalChatId: string | null;
67
66
  status: ChannelStatus;
68
67
  policy: ChannelPolicy;
@@ -37,10 +37,8 @@ import type {
37
37
  ProviderResponse,
38
38
  ToolDefinition,
39
39
  } from "../providers/types.js";
40
- import {
41
- isUntrustedTrustClass,
42
- type TrustClass,
43
- } from "../runtime/actor-trust-resolver.js";
40
+ import { type TrustClass } from "../runtime/actor-trust-resolver.js";
41
+ import { resolveCapabilities } from "../runtime/capabilities.js";
44
42
  import { getLogger } from "../util/logger.js";
45
43
  import { stripInjectionsForCompaction } from "./strip-injections.js";
46
44
  import {
@@ -387,7 +385,7 @@ export function collectImageManifest(
387
385
  actorTrustClass?: TrustClass,
388
386
  ): ManifestEntry[] {
389
387
  const allRows = getMessages(conversationId);
390
- const rows = isUntrustedTrustClass(actorTrustClass)
388
+ const rows = !resolveCapabilities(actorTrustClass).canAccessMemory
391
389
  ? filterMessagesForUntrustedActor(allRows)
392
390
  : allRows;
393
391
  const entries: ManifestEntry[] = [];
@@ -847,6 +845,69 @@ function buildCompactionRequest(
847
845
  return [...stripHistoricalWebSearchResults(history).messages, instruction];
848
846
  }
849
847
 
848
+ // Token headroom a compaction summary call reserves on top of its history: room
849
+ // for the instruction message and the summary the model emits, so a request
850
+ // front-truncated to `compactionPrefixBudget` still fits the context window.
851
+ const COMPACTION_INSTRUCTION_TOKEN_RESERVE = 800;
852
+ const COMPACTION_OUTPUT_BUDGET_RATIO = 0.15;
853
+
854
+ // Largest history (in estimated tokens) a compaction summary call may carry
855
+ // while leaving room for the instruction and the emitted summary within
856
+ // `maxInputTokens`.
857
+ function compactionPrefixBudget(maxInputTokens: number): number {
858
+ return (
859
+ maxInputTokens -
860
+ COMPACTION_INSTRUCTION_TOKEN_RESERVE -
861
+ Math.floor(maxInputTokens * COMPACTION_OUTPUT_BUDGET_RATIO)
862
+ );
863
+ }
864
+
865
+ // Front-truncate the history handed to a compaction summary call so the call
866
+ // itself fits the context window. Drops messages from the front until the
867
+ // estimated prompt fits `budgetTokens`, then prepends a marker noting how many
868
+ // were dropped so the model knows the summary covers only the visible portion.
869
+ // Returns the input untouched when it already fits or holds a single message —
870
+ // so a below-budget call keeps its prefix byte-aligned with the agent's warm
871
+ // cache and pays no extra cache write.
872
+ function truncateHistoryToBudget(args: {
873
+ messages: Message[];
874
+ systemPrompt: string;
875
+ budgetTokens: number;
876
+ providerName: string;
877
+ }): Message[] {
878
+ const { messages, systemPrompt, budgetTokens, providerName } = args;
879
+ let estimate = estimatePromptTokens(messages, systemPrompt, { providerName });
880
+ if (estimate <= budgetTokens || messages.length <= 1) {
881
+ return messages;
882
+ }
883
+ let dropCount = 0;
884
+ while (estimate > budgetTokens && dropCount < messages.length - 1) {
885
+ dropCount++;
886
+ estimate = estimatePromptTokens(messages.slice(dropCount), systemPrompt, {
887
+ providerName,
888
+ });
889
+ }
890
+ if (dropCount === 0) {
891
+ return messages;
892
+ }
893
+ log.info(
894
+ { dropCount, budgetTokens, totalMessages: messages.length },
895
+ "Compaction summary input exceeds context window — truncating from front",
896
+ );
897
+ return [
898
+ {
899
+ role: "user" as const,
900
+ content: [
901
+ {
902
+ type: "text" as const,
903
+ text: `[${dropCount} earlier messages truncated — summary covers only the visible portion]`,
904
+ },
905
+ ],
906
+ },
907
+ ...messages.slice(dropCount),
908
+ ];
909
+ }
910
+
850
911
  export async function runAssistantDrivenCompaction(
851
912
  args: CompactionRunArgs,
852
913
  ): Promise<CompactionRunResult> {
@@ -881,7 +942,19 @@ export async function runAssistantDrivenCompaction(
881
942
  args.targetTokens,
882
943
  );
883
944
 
884
- const requestMessages = buildCompactionRequest(args.messages, instruction);
945
+ // Bound the summary call's own input to the context window. With no tool
946
+ // pair to anchor an emergency split, an overflow recovery routes the full
947
+ // history straight here, so the summary call must front-truncate itself or
948
+ // it overflows in turn. `args.messages` stays intact for tail resolution
949
+ // below — only the outbound request is truncated. A below-budget history is
950
+ // returned untouched, keeping the prefix aligned with the agent's warm cache.
951
+ const summaryHistory = truncateHistoryToBudget({
952
+ messages: args.messages,
953
+ systemPrompt: args.systemPrompt,
954
+ budgetTokens: compactionPrefixBudget(args.maxInputTokens),
955
+ providerName: args.provider.tokenEstimationProvider ?? args.provider.name,
956
+ });
957
+ const requestMessages = buildCompactionRequest(summaryHistory, instruction);
885
958
 
886
959
  let response: ProviderResponse;
887
960
  try {
@@ -1132,8 +1205,8 @@ export async function runAssistantDrivenCompaction(
1132
1205
  // from the moment of capture — keeping them would (a) waste tokens on
1133
1206
  // outdated content, (b) duplicate against the freshly re-injected blocks
1134
1207
  // the next turn produces, and (c) leak `<system_reminder>` text the model
1135
- // is not supposed to see in history. `<turn_context>` and `<workspace>`
1136
- // are intentionally preserved by `RUNTIME_INJECTION_PREFIXES`.
1208
+ // is not supposed to see in history. `<turn_context>` is intentionally
1209
+ // preserved by `RUNTIME_INJECTION_PREFIXES`.
1137
1210
  const tailMessages = stripInjectionsForCompaction(
1138
1211
  args.messages.slice(tailIndex),
1139
1212
  );
@@ -1296,55 +1369,16 @@ export async function runEmergencyCompaction(
1296
1369
  const keptTail = stripInjectionsForCompaction(
1297
1370
  args.messages.slice(splitIndex),
1298
1371
  );
1299
- let prefix = args.messages.slice(0, splitIndex);
1300
-
1301
- // If the prefix itself exceeds the context window, truncate messages
1302
- // from the front so the model can at least see the recent portion.
1303
- // Reserve budget for the instruction message + output.
1304
- const instructionBudget = 800; // ~tokens for the emergency prompt
1305
- const outputBudget = Math.floor(args.maxInputTokens * 0.15);
1306
- const prefixBudget = args.maxInputTokens - instructionBudget - outputBudget;
1307
-
1308
- let prefixEstimate = estimatePromptTokens(prefix, args.systemPrompt, {
1372
+ // Bound the prefix to the context window so the summary call fits, reserving
1373
+ // budget for the instruction message and the emitted summary. Truncates from
1374
+ // the front, keeping the recent portion the summary most needs.
1375
+ const prefix = truncateHistoryToBudget({
1376
+ messages: args.messages.slice(0, splitIndex),
1377
+ systemPrompt: args.systemPrompt,
1378
+ budgetTokens: compactionPrefixBudget(args.maxInputTokens),
1309
1379
  providerName: args.provider.tokenEstimationProvider ?? args.provider.name,
1310
1380
  });
1311
1381
 
1312
- if (prefixEstimate > prefixBudget && prefix.length > 1) {
1313
- log.info(
1314
- {
1315
- prefixEstimate,
1316
- prefixBudget,
1317
- prefixMessages: prefix.length,
1318
- },
1319
- "Emergency compaction: prefix exceeds context window — truncating from front",
1320
- );
1321
- // Drop messages from the front until we fit. Keep at least the first
1322
- // message (may be an existing summary) and try to preserve recent context.
1323
- let dropCount = 0;
1324
- while (prefixEstimate > prefixBudget && dropCount < prefix.length - 1) {
1325
- dropCount++;
1326
- const truncated = prefix.slice(dropCount);
1327
- prefixEstimate = estimatePromptTokens(truncated, args.systemPrompt, {
1328
- providerName:
1329
- args.provider.tokenEstimationProvider ?? args.provider.name,
1330
- });
1331
- }
1332
- if (dropCount > 0) {
1333
- prefix = [
1334
- {
1335
- role: "user" as const,
1336
- content: [
1337
- {
1338
- type: "text" as const,
1339
- text: `[${dropCount} earlier messages truncated — summary covers only the visible portion]`,
1340
- },
1341
- ],
1342
- },
1343
- ...prefix.slice(dropCount),
1344
- ];
1345
- }
1346
- }
1347
-
1348
1382
  const instruction: Message = {
1349
1383
  role: "user",
1350
1384
  content: [{ type: "text", text: EMERGENCY_COMPACTION_PROMPT }],