@vellumai/assistant 0.9.1-staging.1 → 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 (433) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  3. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  4. package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
  5. package/openapi.yaml +852 -15
  6. package/package.json +1 -1
  7. package/src/__tests__/access-request-card-view.test.ts +98 -0
  8. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  9. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
  10. package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  12. package/src/__tests__/app-compiler.test.ts +15 -1
  13. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  14. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  15. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  16. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  18. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  19. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  20. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +174 -30
  22. package/src/__tests__/config-schema.test.ts +35 -0
  23. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  24. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  25. package/src/__tests__/contacts-tools.test.ts +29 -0
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  27. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  28. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  29. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  30. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  31. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  33. package/src/__tests__/conversation-title-service.test.ts +62 -0
  34. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  35. package/src/__tests__/credential-prompt-route.test.ts +1 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  37. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  38. package/src/__tests__/disk-usage.test.ts +65 -0
  39. package/src/__tests__/dynamic-page-surface.test.ts +51 -0
  40. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  41. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  42. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  43. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  44. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  45. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  46. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  47. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  48. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  49. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  50. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  51. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  52. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  53. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  54. package/src/__tests__/llm-catalog-parity.test.ts +30 -1
  55. package/src/__tests__/llm-resolver.test.ts +21 -0
  56. package/src/__tests__/llm-schema.test.ts +1 -0
  57. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  58. package/src/__tests__/mcp-health-check.test.ts +6 -7
  59. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  60. package/src/__tests__/path-policy.test.ts +34 -0
  61. package/src/__tests__/persona-resolver.test.ts +38 -0
  62. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  63. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  64. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  65. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  66. package/src/__tests__/reaction-persistence.test.ts +150 -29
  67. package/src/__tests__/relay-server.test.ts +285 -0
  68. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  69. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  70. package/src/__tests__/skill-execute-input.test.ts +5 -0
  71. package/src/__tests__/skills.test.ts +51 -0
  72. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  73. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  74. package/src/__tests__/subagent-tools.test.ts +150 -0
  75. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  76. package/src/__tests__/title-generate-hook.test.ts +100 -3
  77. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  78. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  79. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  80. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  81. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  82. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  83. package/src/__tests__/twilio-routes.test.ts +81 -1
  84. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  85. package/src/__tests__/weak-open-model.test.ts +30 -0
  86. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  87. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  88. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  89. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  90. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  91. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  92. package/src/agent/loop.ts +33 -33
  93. package/src/api/events/tool-result.ts +6 -0
  94. package/src/api/events/workflow-completed.ts +53 -0
  95. package/src/api/events/workflow-leaf-finished.ts +38 -0
  96. package/src/api/events/workflow-leaf-started.ts +35 -0
  97. package/src/api/events/workflow-progress.ts +32 -0
  98. package/src/api/events/workflow-started.ts +31 -0
  99. package/src/api/index.ts +40 -0
  100. package/src/api/responses/conversation-message.ts +26 -0
  101. package/src/api/responses/home.ts +26 -0
  102. package/src/api/responses/workflow-journal.ts +53 -0
  103. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  104. package/src/approvals/guardian-decision-primitive.ts +26 -3
  105. package/src/approvals/guardian-request-resolvers.ts +181 -78
  106. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  107. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  108. package/src/calls/call-pointer-messages.ts +10 -4
  109. package/src/calls/channel-admission-reader.ts +104 -0
  110. package/src/calls/guardian-dispatch.ts +17 -45
  111. package/src/calls/media-stream-server.ts +84 -2
  112. package/src/calls/relay-server.ts +66 -0
  113. package/src/calls/relay-setup-router.ts +82 -1
  114. package/src/calls/twilio-routes.ts +17 -8
  115. package/src/cli/commands/clients.ts +3 -0
  116. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  117. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  118. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  119. package/src/cli/commands/memory/index.ts +30 -0
  120. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  121. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  122. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  123. package/src/cli/commands/oauth/status.test.ts +36 -0
  124. package/src/cli/commands/oauth/status.ts +23 -3
  125. package/src/cli/commands/plugins.ts +57 -5
  126. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  127. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
  128. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  129. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
  130. package/src/cli/lib/inspect-plugin.ts +12 -1
  131. package/src/cli/lib/merge-plugin-tree.ts +149 -49
  132. package/src/cli/lib/plugin-surfaces.ts +104 -0
  133. package/src/cli/lib/upgrade-plugin.ts +64 -36
  134. package/src/cli/program.ts +2 -4
  135. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  136. package/src/config/assistant-feature-flags.ts +22 -7
  137. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  138. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  139. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  140. package/src/config/bundled-skills/workflows/SKILL.md +14 -7
  141. package/src/config/call-site-defaults.ts +3 -0
  142. package/src/config/feature-flag-registry.json +49 -18
  143. package/src/config/llm-resolver.ts +3 -0
  144. package/src/config/memory-v3-gate.ts +11 -0
  145. package/src/config/schema.ts +8 -6
  146. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  147. package/src/config/schemas/call-site-catalog.ts +7 -0
  148. package/src/config/schemas/channels.ts +11 -0
  149. package/src/config/schemas/llm.ts +31 -0
  150. package/src/config/schemas/memory-lifecycle.ts +3 -7
  151. package/src/config/schemas/memory-v3.ts +6 -0
  152. package/src/config/schemas/services.ts +18 -0
  153. package/src/config/seed-inference-profiles.ts +94 -34
  154. package/src/config/skills.ts +21 -0
  155. package/src/config/sync-gated-profiles.ts +220 -0
  156. package/src/contacts/contact-store.ts +2 -10
  157. package/src/contacts/contacts-write.ts +1 -2
  158. package/src/contacts/types.ts +0 -1
  159. package/src/context/compactor.ts +86 -52
  160. package/src/context/strip-injections.ts +58 -10
  161. package/src/context/token-estimator.ts +1 -1
  162. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  163. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  164. package/src/daemon/conversation-agent-loop.ts +100 -19
  165. package/src/daemon/conversation-history.ts +1 -1
  166. package/src/daemon/conversation-lifecycle.ts +3 -5
  167. package/src/daemon/conversation-process.ts +13 -5
  168. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  169. package/src/daemon/conversation-surfaces.ts +26 -0
  170. package/src/daemon/conversation-tool-setup.ts +16 -11
  171. package/src/daemon/conversation.ts +64 -14
  172. package/src/daemon/disk-pressure-policy.ts +5 -3
  173. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  174. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  175. package/src/daemon/handlers/config-a2a.ts +0 -2
  176. package/src/daemon/handlers/config-channels.ts +5 -10
  177. package/src/daemon/handlers/config-slack-channel.ts +20 -0
  178. package/src/daemon/handlers/conversations.ts +107 -0
  179. package/src/daemon/host-browser-proxy.ts +41 -0
  180. package/src/daemon/lifecycle.ts +55 -20
  181. package/src/daemon/message-provenance.ts +2 -0
  182. package/src/daemon/message-types/contacts.ts +0 -1
  183. package/src/daemon/message-types/web-activity.ts +7 -1
  184. package/src/daemon/message-types/workflows.ts +83 -1
  185. package/src/daemon/tool-setup-types.ts +4 -0
  186. package/src/daemon/trust-context.ts +1 -1
  187. package/src/events/tool-audit-listener.ts +2 -2
  188. package/src/home/feed-source-enrichment.test.ts +151 -0
  189. package/src/home/feed-source-enrichment.ts +176 -0
  190. package/src/instrument.ts +18 -6
  191. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  192. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  193. package/src/ipc/assistant-server.ts +37 -4
  194. package/src/ipc/gateway-flag-listener.ts +18 -2
  195. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  196. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  197. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  198. package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
  199. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  200. package/src/memory/auth-fallback-events-store.ts +2 -2
  201. package/src/memory/auto-analysis-enqueue.ts +3 -5
  202. package/src/memory/canonical-guardian-store.ts +39 -1
  203. package/src/memory/conversation-crud.ts +9 -4
  204. package/src/memory/conversation-key-store.ts +17 -2
  205. package/src/memory/conversation-title-service.ts +64 -7
  206. package/src/memory/db-init.ts +10 -0
  207. package/src/memory/embedding-backend.ts +15 -1
  208. package/src/memory/jobs-worker.ts +2 -1
  209. package/src/memory/lifecycle-events-store.ts +2 -2
  210. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  211. package/src/memory/memory-retrospective-job.ts +9 -0
  212. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  213. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  214. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  215. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  216. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  217. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  218. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  219. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  220. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  221. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  222. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  223. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  224. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  225. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  226. package/src/memory/migrations/index.ts +5 -0
  227. package/src/memory/onboarding-events-store.ts +3 -3
  228. package/src/memory/schema/contacts.ts +0 -1
  229. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  230. package/src/memory/skill-loaded-events-store.ts +2 -2
  231. package/src/memory/tool-executed-events-store.test.ts +7 -7
  232. package/src/memory/turn-trace-store.test.ts +736 -0
  233. package/src/memory/turn-trace-store.ts +364 -0
  234. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  235. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  236. package/src/memory/v2/consolidation-job.ts +2 -2
  237. package/src/memory/v2/skill-content.ts +25 -7
  238. package/src/memory/v2/skill-store.ts +7 -1
  239. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  240. package/src/memory/v3-eval/eval-packets.ts +546 -0
  241. package/src/messaging/providers/slack/api.ts +31 -0
  242. package/src/messaging/providers/slack/send.test.ts +114 -2
  243. package/src/messaging/providers/slack/send.ts +30 -7
  244. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  245. package/src/messaging/providers/slack/withdraw.ts +161 -0
  246. package/src/notifications/AGENTS.md +2 -0
  247. package/src/notifications/access-request-copy.ts +72 -59
  248. package/src/notifications/adapters/slack.ts +55 -73
  249. package/src/notifications/approval-card-data.ts +333 -0
  250. package/src/notifications/broadcaster.ts +6 -2
  251. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  252. package/src/notifications/copy-composer.ts +3 -3
  253. package/src/notifications/decision-engine.ts +4 -2
  254. package/src/notifications/destination-resolver.ts +4 -6
  255. package/src/notifications/guardian-question-mode.ts +10 -0
  256. package/src/notifications/home-feed-side-effect.ts +3 -13
  257. package/src/notifications/notification-utils.ts +2 -1
  258. package/src/notifications/signal.ts +79 -43
  259. package/src/notifications/types.ts +98 -128
  260. package/src/permissions/checker.test.ts +51 -0
  261. package/src/permissions/checker.ts +185 -26
  262. package/src/permissions/ipc-risk-types.ts +24 -0
  263. package/src/permissions/question-prompter.test.ts +27 -0
  264. package/src/permissions/question-prompter.ts +4 -0
  265. package/src/platform/client.test.ts +119 -0
  266. package/src/platform/client.ts +66 -0
  267. package/src/platform/consent-cache.test.ts +267 -0
  268. package/src/platform/consent-cache.ts +174 -0
  269. package/src/plugin-api/index.ts +27 -0
  270. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  271. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  272. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  273. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  274. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  275. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  276. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  277. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  278. package/src/plugins/defaults/advisor/config.ts +21 -0
  279. package/src/plugins/defaults/advisor/consult.ts +93 -0
  280. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  281. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  282. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  283. package/src/plugins/defaults/advisor/package.json +14 -0
  284. package/src/plugins/defaults/advisor/steering.ts +67 -0
  285. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  286. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  287. package/src/plugins/defaults/index.ts +35 -0
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  290. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  291. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  298. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  299. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
  300. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  301. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  302. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  303. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
  304. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  305. package/src/prompts/persona-resolver.ts +12 -2
  306. package/src/prompts/templates/system-sections.ts +7 -2
  307. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  308. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  309. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  310. package/src/providers/atlascloud/client.ts +85 -0
  311. package/src/providers/fetch-provider-catalog.ts +85 -0
  312. package/src/providers/inference/adapter-factory.ts +3 -0
  313. package/src/providers/model-catalog.ts +58 -0
  314. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  315. package/src/providers/openai/chat-completions-provider.ts +7 -0
  316. package/src/providers/openai/responses-provider.ts +10 -0
  317. package/src/providers/provider-send-message.ts +11 -3
  318. package/src/providers/retry.ts +53 -12
  319. package/src/providers/search-provider-catalog.ts +10 -0
  320. package/src/providers/weak-open-model.ts +22 -0
  321. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  322. package/src/runtime/__tests__/client-health.test.ts +44 -0
  323. package/src/runtime/access-request-helper.ts +21 -53
  324. package/src/runtime/actor-trust-resolver.ts +49 -21
  325. package/src/runtime/agent-wake.ts +52 -0
  326. package/src/runtime/assistant-event-hub.ts +18 -4
  327. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  328. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  329. package/src/runtime/capabilities.test.ts +120 -0
  330. package/src/runtime/capabilities.ts +197 -0
  331. package/src/runtime/channel-approval-types.ts +5 -1
  332. package/src/runtime/channel-retry-sweep.ts +1 -0
  333. package/src/runtime/channel-verification-service.ts +1 -2
  334. package/src/runtime/client-health.ts +26 -0
  335. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  336. package/src/runtime/effective-capabilities.test.ts +128 -0
  337. package/src/runtime/effective-capabilities.ts +84 -0
  338. package/src/runtime/guardian-reply-router.ts +106 -21
  339. package/src/runtime/invite-redemption-service.ts +6 -22
  340. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  341. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  342. package/src/runtime/pending-interactions.ts +15 -0
  343. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  344. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  345. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
  346. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  347. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  348. package/src/runtime/routes/client-routes.ts +10 -0
  349. package/src/runtime/routes/contact-routes.ts +31 -8
  350. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  351. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  352. package/src/runtime/routes/conversation-routes.ts +37 -12
  353. package/src/runtime/routes/events-routes.ts +1 -3
  354. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  355. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  356. package/src/runtime/routes/home-feed-routes.ts +8 -3
  357. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  358. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
  359. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  360. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  361. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  362. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  363. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  364. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  365. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  366. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  367. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  368. package/src/runtime/routes/index.ts +2 -0
  369. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  370. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  371. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  372. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  373. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  374. package/src/runtime/routes/notification-routes.ts +122 -133
  375. package/src/runtime/routes/platform-routes.ts +2 -2
  376. package/src/runtime/routes/plugins-routes.ts +40 -7
  377. package/src/runtime/routes/secret-routes.ts +10 -0
  378. package/src/runtime/routes/surface-action-routes.ts +2 -1
  379. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  380. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  381. package/src/runtime/routes/workflow-routes.test.ts +225 -1
  382. package/src/runtime/routes/workflow-routes.ts +131 -1
  383. package/src/runtime/tool-grant-request-helper.ts +18 -16
  384. package/src/runtime/trust-context-resolver.ts +8 -5
  385. package/src/schedule/schedule-store.ts +1 -1
  386. package/src/schedule/scheduler-types.ts +5 -1
  387. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  388. package/src/security/secret-patterns.ts +3 -0
  389. package/src/subagent/manager.ts +11 -4
  390. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  391. package/src/telemetry/trace-collection-policy.ts +30 -0
  392. package/src/telemetry/types.ts +89 -0
  393. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  394. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  395. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  396. package/src/tools/browser/browser-execution.ts +29 -18
  397. package/src/tools/document/document-tool.ts +2 -3
  398. package/src/tools/executor.ts +5 -3
  399. package/src/tools/host-terminal/host-shell.ts +5 -4
  400. package/src/tools/memory/register.ts +2 -2
  401. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  402. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  403. package/src/tools/network/web-fetch.ts +372 -1
  404. package/src/tools/network/web-search.ts +213 -10
  405. package/src/tools/permission-checker.ts +3 -2
  406. package/src/tools/registry.ts +20 -0
  407. package/src/tools/schedule/create.ts +4 -3
  408. package/src/tools/schedule/update.ts +2 -1
  409. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  410. package/src/tools/skills/execute.ts +1 -2
  411. package/src/tools/subagent/spawn.ts +37 -13
  412. package/src/tools/terminal/shell.ts +10 -4
  413. package/src/tools/tool-approval-handler.ts +17 -10
  414. package/src/tools/types.ts +9 -0
  415. package/src/tools/ui-surface/definitions.ts +25 -2
  416. package/src/tools/verification-control-plane-policy.ts +3 -1
  417. package/src/tools/workflows/run-workflow.ts +1 -0
  418. package/src/util/disk-usage.ts +78 -23
  419. package/src/util/platform.ts +8 -1
  420. package/src/watcher/telemetry.ts +2 -2
  421. package/src/workflows/engine.test.ts +175 -1
  422. package/src/workflows/engine.ts +82 -0
  423. package/src/workflows/journal-store.test.ts +70 -0
  424. package/src/workflows/journal-store.ts +18 -3
  425. package/src/workflows/run-manager.test.ts +171 -3
  426. package/src/workflows/run-manager.ts +64 -0
  427. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  428. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  429. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  430. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  431. package/src/workspace/migrations/registry.ts +8 -0
  432. package/src/notifications/tool-approval-copy.ts +0 -142
  433. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
@@ -27,7 +27,10 @@ type ManagedProfileTemplate = Omit<
27
27
  ProfileEntry,
28
28
  "provider" | "model" | "provider_connection"
29
29
  > & {
30
- intent: ModelIntent;
30
+ // Exactly one of `intent` or `model` must be set. `intent` resolves the
31
+ // model from the catalog at seed time; `model` pins an explicit model id.
32
+ intent?: ModelIntent;
33
+ model?: string;
31
34
  provider: NonNullable<ProfileEntry["provider"]>;
32
35
  connectionName: string;
33
36
  };
@@ -38,17 +41,20 @@ type ManagedProfileTemplate = Omit<
38
41
  * (`preserveProfileNames`) take precedence when present.
39
42
  */
40
43
  const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
44
+ // Served by MiniMax M3 on Fireworks via managed platform inference: a strong
45
+ // open model at a lower price point than the managed Anthropic route.
41
46
  balanced: {
42
47
  intent: "balanced",
43
- provider: "anthropic",
44
- connectionName: "anthropic-managed",
48
+ provider: "fireworks",
49
+ connectionName: "fireworks-managed",
45
50
  source: "managed",
46
51
  label: "Balanced",
47
52
  description: "Good balance of quality, cost, and speed",
48
- maxTokens: 16000,
53
+ maxTokens: 32000,
49
54
  effort: "high",
50
55
  thinking: { enabled: true, streamThinking: true },
51
56
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
57
+ topP: 0.95,
52
58
  },
53
59
  "quality-optimized": {
54
60
  intent: "quality-optimized",
@@ -61,6 +67,9 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
61
67
  effort: "high",
62
68
  thinking: { enabled: true, streamThinking: true },
63
69
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
70
+ // This is the advisor's own (strongest) profile: when it's also the chat
71
+ // profile there's nothing stronger to consult, so the advisor defaults off.
72
+ advisorEnabled: false,
64
73
  },
65
74
  "cost-optimized": {
66
75
  intent: "latency-optimized",
@@ -74,20 +83,6 @@ const MANAGED_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
74
83
  thinking: { enabled: false, streamThinking: false },
75
84
  contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
76
85
  },
77
- // Open-weight economy option: MiniMax M3 served by Fireworks via managed
78
- // platform inference.
79
- "balanced-economy": {
80
- intent: "balanced",
81
- provider: "fireworks",
82
- connectionName: "fireworks-managed",
83
- source: "managed",
84
- label: "Balanced Economy",
85
- description: "Strong open model (MiniMax M3) at a lower price point",
86
- maxTokens: 32000,
87
- effort: "high",
88
- thinking: { enabled: true, streamThinking: true },
89
- contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
90
- },
91
86
  };
92
87
 
93
88
  /**
@@ -144,8 +139,37 @@ const USER_PROFILE_TEMPLATES: Record<string, ManagedProfileTemplate> = {
144
139
  */
145
140
  export const AUTO_PROFILE_KEY = "auto";
146
141
 
142
+ export const OS_BETA_PROFILE_KEY = "os-beta";
143
+ export const OS_BETA_FEATURE_FLAG_KEY = "os-beta";
144
+
145
+ /**
146
+ * Flag-gated managed profile. NOT in MANAGED_PROFILE_TEMPLATES, so the
147
+ * unconditional boot seed never creates it. Reconciled in/out by
148
+ * the flag-gated profile reconcile based on the `os-beta` feature flag.
149
+ * Balanced-parity defaults; GLM 5.2 pinned explicitly via `model`.
150
+ */
151
+ export const OS_BETA_PROFILE_TEMPLATE: ManagedProfileTemplate = {
152
+ model: "accounts/fireworks/models/glm-5p2",
153
+ provider: "fireworks",
154
+ connectionName: "fireworks-managed",
155
+ source: "managed",
156
+ label: "OS Beta",
157
+ description: "Open-source frontier model (GLM 5.2), in beta",
158
+ maxTokens: 32000,
159
+ effort: "high",
160
+ thinking: { enabled: true, streamThinking: true },
161
+ contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
162
+ };
163
+
164
+ // Membership here marks a name as managed. The route layer applies managed
165
+ // restrictions (blocking model/provider edits and deletion) only to entries
166
+ // whose on-disk `source` is `managed`, so a user-owned profile sharing one of
167
+ // these names is not locked. `OS_BETA_PROFILE_KEY` is flag-gated: it is
168
+ // materialized by the flag-gated profile reconcile, which refuses to touch a
169
+ // same-named user profile.
147
170
  export const MANAGED_PROFILE_NAMES = new Set([
148
171
  ...Object.keys(MANAGED_PROFILE_TEMPLATES),
172
+ OS_BETA_PROFILE_KEY,
149
173
  AUTO_PROFILE_KEY,
150
174
  ]);
151
175
 
@@ -167,13 +191,17 @@ export type SeedInferenceProfilesOptions = {
167
191
  *
168
192
  * Runs on every daemon startup. Two responsibilities:
169
193
  *
170
- * 1. **Managed profiles** (`balanced`, `quality-optimized`, `cost-optimized`,
171
- * `balanced-economy`): reconciled from the code templates on every boot —
194
+ * 1. **Managed profiles** (`balanced`, `quality-optimized`,
195
+ * `cost-optimized`): reconciled from the code templates on every boot —
172
196
  * on-platform and off-platform alike — so Vellum can push model/config
173
197
  * updates to customers in a release without a workspace migration. The
174
- * templates own all profile content; only `label` and `status` are
175
- * user-overridable and survive reseeds. Platform overlays
176
- * (`preserveProfileNames`) take precedence for the boot they are supplied.
198
+ * templates own all profile content; `label`, `status`, `advisorEnabled`,
199
+ * and `topP` are user overrides that survive reseeds. Of those, only
200
+ * `label`, `status`, and `topP` are editable through the managed PUT route
201
+ * allowlist; `advisorEnabled` is edited via the generic config path but is
202
+ * still preserved here so it survives reboots.
203
+ * Platform overlays (`preserveProfileNames`) take precedence for the boot
204
+ * they are supplied.
177
205
  *
178
206
  * 2. **User profiles** (`custom-balanced`, `custom-quality-optimized`,
179
207
  * `custom-cost-optimized`): materialized once at hatch time for
@@ -225,13 +253,17 @@ export function seedInferenceProfiles(
225
253
  // its first merge), so on subsequent boots the templates reconcile content
226
254
  // as usual.
227
255
  //
228
- // Two user-editable fields survive the reconcile: `label` (display
229
- // rename) and `status` (active/disabled toggle) — the only fields a user
230
- // may override. The PUT route `/v1/config/llm/profiles/:name` lets users
231
- // patch these on managed profiles without duplicating; we honor those
232
- // edits across reseeds or they'd silently revert on every boot. Carry by
233
- // key-presence rather than truthiness so an explicit `null` (user
234
- // cleared the label) survives too.
256
+ // A whitelist of user-editable fields survives the reconcile: `label`
257
+ // (display rename), `status` (active/disabled toggle), `advisorEnabled`
258
+ // (per-profile advisor toggle), and `topP` (sampling override) — the only
259
+ // fields a user may override. The managed PUT route
260
+ // `/v1/config/llm/profiles/:name` lets users patch `label`, `status`, and
261
+ // `topP` on managed profiles without duplicating (its editable allowlist,
262
+ // `MANAGED_PROFILE_EDITABLE_KEYS`, deliberately excludes `advisorEnabled`,
263
+ // which is set through the generic config path). We honor every one of
264
+ // these overrides across reseeds or they'd silently revert on every boot.
265
+ // Carry by key-presence rather than truthiness so an explicit `null` (user
266
+ // cleared the field) survives too.
235
267
  //
236
268
  // BYOK seed defaults (off-platform only):
237
269
  // • label: " (Managed)" suffix disambiguates managed profile labels
@@ -286,6 +318,18 @@ export function seedInferenceProfiles(
286
318
  : previous.label;
287
319
  }
288
320
  if ("status" in previous) next.status = previous.status;
321
+ // The per-profile advisor toggle is a user override — preserve it across
322
+ // reseeds so a user's choice survives reboots (the template value only
323
+ // seeds the initial default, e.g. off for quality-optimized).
324
+ if ("advisorEnabled" in previous) {
325
+ next.advisorEnabled = previous.advisorEnabled;
326
+ }
327
+ // `topP` is user-editable on managed profiles (see the managed-profile
328
+ // editable allowlist on the PUT route) — preserve a user override across
329
+ // reseeds, including an explicit `null` clear, or it would silently revert
330
+ // to the template value on every boot. Carry by key-presence (not
331
+ // truthiness) so `null` survives too.
332
+ if ("topP" in previous) next.topP = previous.topP;
289
333
  }
290
334
  profiles[name] = next as ProfileEntry;
291
335
  }
@@ -369,6 +413,17 @@ export function seedInferenceProfiles(
369
413
  }
370
414
  }
371
415
 
416
+ // Advisor profile: default to the strongest managed profile when unset, so
417
+ // the advisor consults `quality-optimized` out of the box. Guarded on
418
+ // existence so it never names a missing profile (superRefine rejects that);
419
+ // off-platform/BYOK installs can repoint it at one of their own profiles.
420
+ if (
421
+ readString(llm.advisorProfile) === undefined &&
422
+ readObject(profiles["quality-optimized"]) !== null
423
+ ) {
424
+ llm.advisorProfile = "quality-optimized";
425
+ }
426
+
372
427
  // Profile ordering — ensure all seeded profiles appear in the order array.
373
428
  // "auto" is prepended so it appears first in the picker.
374
429
  const profileOrder = Array.isArray(llm.profileOrder)
@@ -410,21 +465,26 @@ export function seedInferenceProfiles(
410
465
  saveRawConfig(config);
411
466
  }
412
467
 
413
- function materializeProfile(
468
+ export function materializeProfile(
414
469
  template: ManagedProfileTemplate,
415
470
  provider: NonNullable<ProfileEntry["provider"]>,
416
471
  connectionName: string,
417
472
  ): ProfileEntry {
418
- const { intent, provider: _p, connectionName: _c, ...rest } = template;
473
+ const { intent, model, provider: _p, connectionName: _c, ...rest } = template;
474
+ const resolvedModel =
475
+ model ?? (intent ? resolveModelIntent(provider, intent) : undefined);
476
+ if (!resolvedModel) {
477
+ throw new Error("ManagedProfileTemplate requires `intent` or `model`");
478
+ }
419
479
  return {
420
480
  ...rest,
421
481
  provider,
422
482
  provider_connection: connectionName,
423
- model: resolveModelIntent(provider, intent),
483
+ model: resolvedModel,
424
484
  };
425
485
  }
426
486
 
427
- function readObject(value: unknown): Record<string, unknown> | null {
487
+ export function readObject(value: unknown): Record<string, unknown> | null {
428
488
  return value !== null && typeof value === "object" && !Array.isArray(value)
429
489
  ? (value as Record<string, unknown>)
430
490
  : null;
@@ -47,6 +47,7 @@ const VellumMetadataSchema = z
47
47
  "activation-hints": z.array(z.string()).optional(),
48
48
  "avoid-when": z.array(z.string()).optional(),
49
49
  category: z.string().optional(),
50
+ "always-candidate": z.boolean().optional(),
50
51
  })
51
52
  .passthrough();
52
53
 
@@ -109,6 +110,13 @@ export interface SkillSummary {
109
110
  avoidWhen?: string[];
110
111
  /** Category slug declared in frontmatter, used as a fallback when the skill is not in the Vellum catalog. */
111
112
  category?: string;
113
+ /**
114
+ * When true, this skill is pinned into the memory-v3 selector's stable-prefix
115
+ * candidate pool every turn (so the selector can choose it even when no
116
+ * retrieval lane surfaces it). For cross-cutting capabilities whose relevance
117
+ * the model must judge, not embedding similarity.
118
+ */
119
+ alwaysCandidate?: boolean;
112
120
  /** Parsed inline command expansion descriptors (`!\`command\``) found in the skill body. */
113
121
  inlineCommandExpansions?: InlineCommandExpansion[];
114
122
  }
@@ -227,6 +235,7 @@ interface ParsedFrontmatter {
227
235
  activationHints?: string[];
228
236
  avoidWhen?: string[];
229
237
  category?: string;
238
+ alwaysCandidate?: boolean;
230
239
  inlineCommandExpansions?: InlineCommandExpansion[];
231
240
  }
232
241
 
@@ -342,6 +351,11 @@ function parseFrontmatter(
342
351
  ? vellum.category.trim()
343
352
  : undefined;
344
353
 
354
+ const alwaysCandidate =
355
+ typeof vellum?.["always-candidate"] === "boolean"
356
+ ? vellum["always-candidate"]
357
+ : undefined;
358
+
345
359
  const strippedBody = stripCommentLines(body);
346
360
 
347
361
  // Parse inline command expansions from the body (after frontmatter/comment stripping)
@@ -366,6 +380,7 @@ function parseFrontmatter(
366
380
  activationHints,
367
381
  avoidWhen,
368
382
  category,
383
+ alwaysCandidate,
369
384
  inlineCommandExpansions,
370
385
  };
371
386
  }
@@ -523,6 +538,7 @@ function readSkillFromDirectory(
523
538
  activationHints: parsed.activationHints,
524
539
  avoidWhen: parsed.avoidWhen,
525
540
  category: parsed.category,
541
+ alwaysCandidate: parsed.alwaysCandidate,
526
542
  inlineCommandExpansions: parsed.inlineCommandExpansions,
527
543
  };
528
544
  } catch (err) {
@@ -576,6 +592,7 @@ function readBundledSkillFromDirectory(
576
592
  activationHints: parsed.activationHints,
577
593
  avoidWhen: parsed.avoidWhen,
578
594
  category: parsed.category,
595
+ alwaysCandidate: parsed.alwaysCandidate,
579
596
  inlineCommandExpansions: parsed.inlineCommandExpansions,
580
597
  };
581
598
  } catch (err) {
@@ -637,6 +654,7 @@ function loadBundledSkills(): SkillSummary[] {
637
654
  activationHints: skill.activationHints,
638
655
  avoidWhen: skill.avoidWhen,
639
656
  category: skill.category,
657
+ alwaysCandidate: skill.alwaysCandidate,
640
658
  inlineCommandExpansions: skill.inlineCommandExpansions,
641
659
  });
642
660
  }
@@ -764,6 +782,7 @@ function skillSummaryFromDefinition(
764
782
  activationHints: skill.activationHints,
765
783
  avoidWhen: skill.avoidWhen,
766
784
  category: skill.category,
785
+ alwaysCandidate: skill.alwaysCandidate,
767
786
  inlineCommandExpansions: skill.inlineCommandExpansions,
768
787
  };
769
788
  }
@@ -818,6 +837,7 @@ export function loadSkillCatalog(
818
837
  activationHints: parsed.activationHints,
819
838
  avoidWhen: parsed.avoidWhen,
820
839
  category: parsed.category,
840
+ alwaysCandidate: parsed.alwaysCandidate,
821
841
  inlineCommandExpansions: parsed.inlineCommandExpansions,
822
842
  });
823
843
  } catch (err) {
@@ -954,6 +974,7 @@ export function loadSkillCatalog(
954
974
  activationHints: parsed.activationHints,
955
975
  avoidWhen: parsed.avoidWhen,
956
976
  category: parsed.category,
977
+ alwaysCandidate: parsed.alwaysCandidate,
957
978
  inlineCommandExpansions: parsed.inlineCommandExpansions,
958
979
  };
959
980
 
@@ -0,0 +1,220 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+
3
+ import { getLogger } from "../util/logger.js";
4
+ import { isAssistantFeatureFlagEnabled } from "./assistant-feature-flags.js";
5
+ import {
6
+ getConfigReadOnly,
7
+ invalidateConfigCache,
8
+ loadRawConfig,
9
+ saveRawConfig,
10
+ } from "./loader.js";
11
+ import type { ProfileEntry } from "./schemas/llm.js";
12
+ import {
13
+ materializeProfile,
14
+ OS_BETA_FEATURE_FLAG_KEY,
15
+ OS_BETA_PROFILE_KEY,
16
+ OS_BETA_PROFILE_TEMPLATE,
17
+ readObject,
18
+ } from "./seed-inference-profiles.js";
19
+
20
+ const log = getLogger("sync-gated-profiles");
21
+
22
+ /**
23
+ * Reconcile flag-gated managed profiles against the current feature-flag state.
24
+ *
25
+ * `seedInferenceProfiles()` runs synchronously at boot before feature flags are
26
+ * available, so the OS Beta profile (GLM 5.2 / fireworks-managed) is materialized
27
+ * here once flags have loaded. When the `os-beta` flag is on, the managed profile
28
+ * is created (ordered right after `balanced`); when it is off, a previously
29
+ * managed entry is removed with `profileOrder` / `activeProfile` / `advisorProfile`
30
+ * fallbacks. The reconcile is idempotent and never touches a user-owned profile of
31
+ * the same name.
32
+ *
33
+ * Returns whether the on-disk config changed.
34
+ */
35
+ export function reconcileFlagGatedProfiles(): boolean {
36
+ const config = loadRawConfig();
37
+
38
+ if (config.llm == null || typeof config.llm !== "object") {
39
+ config.llm = {};
40
+ }
41
+ const llm = config.llm as Record<string, unknown>;
42
+
43
+ if (llm.profiles == null || typeof llm.profiles !== "object") {
44
+ llm.profiles = {};
45
+ }
46
+ const profiles = llm.profiles as Record<string, Record<string, unknown>>;
47
+
48
+ const profileOrder = Array.isArray(llm.profileOrder)
49
+ ? (llm.profileOrder as string[])
50
+ : [];
51
+ llm.profileOrder = profileOrder;
52
+
53
+ // The resolver reads flag state from the gateway-populated override cache and
54
+ // ignores the config argument; pass the read-only config for signature parity
55
+ // without mutating disk before the reconcile decision is made.
56
+ const enabled = isAssistantFeatureFlagEnabled(
57
+ OS_BETA_FEATURE_FLAG_KEY,
58
+ getConfigReadOnly(),
59
+ );
60
+
61
+ const isPlatform =
62
+ process.env.IS_PLATFORM === "true" || process.env.IS_PLATFORM === "1";
63
+ const isByokMode = !isPlatform;
64
+
65
+ const previous = readObject(profiles[OS_BETA_PROFILE_KEY]);
66
+
67
+ // Never clobber a user-owned profile that happens to be named `os-beta`. The
68
+ // entry is ours to manage only when it is absent or already managed; a
69
+ // user-sourced entry of the same name is left untouched on every path.
70
+ const isOursToManage = previous == null || previous.source === "managed";
71
+ if (!isOursToManage) {
72
+ return false;
73
+ }
74
+
75
+ const changed = enabled
76
+ ? enableProfile(profiles, profileOrder, previous, isByokMode)
77
+ : disableProfile(llm, profiles, profileOrder, previous);
78
+
79
+ if (changed) {
80
+ saveRawConfig(config);
81
+ invalidateConfigCache();
82
+ log.info(
83
+ { profile: OS_BETA_PROFILE_KEY, enabled },
84
+ "Reconciled flag-gated profile",
85
+ );
86
+ }
87
+ return changed;
88
+ }
89
+
90
+ function enableProfile(
91
+ profiles: Record<string, Record<string, unknown>>,
92
+ profileOrder: string[],
93
+ previous: Record<string, unknown> | null,
94
+ isByokMode: boolean,
95
+ ): boolean {
96
+ const effectiveTemplate = isByokMode
97
+ ? {
98
+ ...OS_BETA_PROFILE_TEMPLATE,
99
+ label: `${OS_BETA_PROFILE_TEMPLATE.label} (Managed)`,
100
+ }
101
+ : OS_BETA_PROFILE_TEMPLATE;
102
+ const next = materializeProfile(
103
+ effectiveTemplate,
104
+ OS_BETA_PROFILE_TEMPLATE.provider,
105
+ OS_BETA_PROFILE_TEMPLATE.connectionName,
106
+ ) as Record<string, unknown>;
107
+
108
+ // BYOK installs seed managed profiles disabled: the platform-auth
109
+ // `fireworks-managed` connection backing this profile isn't usable until the
110
+ // user enables it, so a fresh OS Beta entry starts disabled to avoid offering
111
+ // an unusable route. A user's own status override (preserved below) wins on
112
+ // later reconciles.
113
+ if (isByokMode && !previous) {
114
+ next.status = "disabled";
115
+ }
116
+
117
+ if (previous) {
118
+ // The only fields a user may override on a managed profile. Carry `label`
119
+ // by key-presence so an explicit null (user cleared it) survives too.
120
+ if ("label" in previous) next.label = previous.label;
121
+ if ("status" in previous) next.status = previous.status;
122
+ if ("advisorEnabled" in previous) {
123
+ next.advisorEnabled = previous.advisorEnabled;
124
+ }
125
+ }
126
+
127
+ let changed = false;
128
+ if (!previous || !isDeepStrictEqual(previous, next)) {
129
+ profiles[OS_BETA_PROFILE_KEY] = next as ProfileEntry;
130
+ changed = true;
131
+ }
132
+
133
+ if (!profileOrder.includes(OS_BETA_PROFILE_KEY)) {
134
+ const balancedIndex = profileOrder.indexOf("balanced");
135
+ if (balancedIndex >= 0) {
136
+ profileOrder.splice(balancedIndex + 1, 0, OS_BETA_PROFILE_KEY);
137
+ } else {
138
+ profileOrder.push(OS_BETA_PROFILE_KEY);
139
+ }
140
+ changed = true;
141
+ }
142
+
143
+ return changed;
144
+ }
145
+
146
+ // `MixSchema = z.array(MixArmSchema).min(2)` in schemas/llm.ts: mixes require
147
+ // >= 2 arms. A mix that drops below this is invalid and cannot be kept.
148
+ const MIX_MIN_ARMS = 2;
149
+
150
+ function disableProfile(
151
+ llm: Record<string, unknown>,
152
+ profiles: Record<string, Record<string, unknown>>,
153
+ profileOrder: string[],
154
+ previous: Record<string, unknown> | null,
155
+ ): boolean {
156
+ if (!previous) return false;
157
+
158
+ delete profiles[OS_BETA_PROFILE_KEY];
159
+
160
+ // The removal closure: every name here is absent from `profiles` once the
161
+ // closure settles, so the written config can never reference one. A mix that
162
+ // loses arms below the >= 2 minimum is itself invalid, so it joins the set
163
+ // and the loop runs to a fixpoint to resolve any references that cascade.
164
+ const removed = new Set<string>([OS_BETA_PROFILE_KEY]);
165
+ let cascading = true;
166
+ while (cascading) {
167
+ cascading = false;
168
+ for (const [name, profile] of Object.entries(profiles)) {
169
+ if (removed.has(name)) continue;
170
+ if (!Array.isArray(profile.mix)) continue;
171
+ const arms = profile.mix as unknown[];
172
+ const kept = arms.filter((arm) => {
173
+ const armProfile = readObject(arm)?.profile;
174
+ return typeof armProfile !== "string" || !removed.has(armProfile);
175
+ });
176
+ if (kept.length === arms.length) continue;
177
+ if (kept.length >= MIX_MIN_ARMS) {
178
+ profile.mix = kept;
179
+ } else {
180
+ delete profiles[name];
181
+ removed.add(name);
182
+ }
183
+ cascading = true;
184
+ }
185
+ }
186
+
187
+ llm.profileOrder = profileOrder.filter((name) => !removed.has(name));
188
+
189
+ if (typeof llm.activeProfile === "string" && removed.has(llm.activeProfile)) {
190
+ llm.activeProfile = "balanced";
191
+ }
192
+ if (
193
+ typeof llm.advisorProfile === "string" &&
194
+ removed.has(llm.advisorProfile)
195
+ ) {
196
+ if (readObject(profiles["quality-optimized"]) !== null) {
197
+ llm.advisorProfile = "quality-optimized";
198
+ } else {
199
+ delete llm.advisorProfile;
200
+ }
201
+ }
202
+
203
+ // Clear any call-site `profile` reference to a removed profile; other override
204
+ // fields on the entry stay intact (an empty override object is valid).
205
+ const callSites = readObject(llm.callSites);
206
+ if (callSites) {
207
+ for (const entry of Object.values(callSites)) {
208
+ const site = readObject(entry);
209
+ if (
210
+ site &&
211
+ typeof site.profile === "string" &&
212
+ removed.has(site.profile)
213
+ ) {
214
+ delete site.profile;
215
+ }
216
+ }
217
+ }
218
+
219
+ return true;
220
+ }
@@ -116,7 +116,6 @@ function parseChannel(
116
116
  type: row.type,
117
117
  address: row.address,
118
118
  isPrimary: row.isPrimary,
119
- externalUserId: row.externalUserId,
120
119
  externalChatId: row.externalChatId,
121
120
  status: row.status as ContactChannel["status"],
122
121
  policy: row.policy as ContactChannel["policy"],
@@ -162,7 +161,6 @@ interface SyncChannelData {
162
161
  type: string;
163
162
  address: string;
164
163
  isPrimary?: boolean;
165
- externalUserId?: string | null;
166
164
  externalChatId?: string | null;
167
165
  status?: ChannelStatus;
168
166
  policy?: ChannelPolicy;
@@ -386,8 +384,6 @@ function syncChannels(
386
384
  // Self-heal legacy lowercased addresses to canonical form.
387
385
  if (existing.address !== ch.address) updateSet.address = ch.address;
388
386
  if (ch.isPrimary !== undefined) updateSet.isPrimary = ch.isPrimary;
389
- if (ch.externalUserId !== undefined)
390
- updateSet.externalUserId = ch.externalUserId;
391
387
  if (ch.externalChatId !== undefined)
392
388
  updateSet.externalChatId = ch.externalChatId;
393
389
  if (!isBlocked) {
@@ -428,8 +424,6 @@ function syncChannels(
428
424
  contactId,
429
425
  updatedAt: now,
430
426
  };
431
- if (ch.externalUserId !== undefined)
432
- reassignSet.externalUserId = ch.externalUserId;
433
427
  if (ch.externalChatId !== undefined)
434
428
  reassignSet.externalChatId = ch.externalChatId;
435
429
  if (!isBlocked) {
@@ -462,7 +456,6 @@ function syncChannels(
462
456
  type: ch.type,
463
457
  address: ch.address,
464
458
  isPrimary: ch.isPrimary ?? false,
465
- externalUserId: ch.externalUserId ?? null,
466
459
  externalChatId: ch.externalChatId ?? null,
467
460
  status: ch.status ?? "unverified",
468
461
  policy: ch.policy ?? "allow",
@@ -742,8 +735,8 @@ export function findContactByAddress(
742
735
  }
743
736
 
744
737
  /**
745
- * Find a contact by channel external chat ID. This is the fallback lookup path
746
- * 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).
747
740
  * No unique constraint exists on externalChatId, so ORDER BY is needed.
748
741
  */
749
742
  function findContactByChannelExternalChatId(
@@ -978,7 +971,6 @@ export function updateContactPrincipalAndChannel(
978
971
  db.update(contactChannels)
979
972
  .set({
980
973
  address: newPrincipalId,
981
- externalUserId: newPrincipalId,
982
974
  updatedAt: now,
983
975
  })
984
976
  .where(eq(contactChannels.id, channelId))
@@ -73,7 +73,7 @@ export function upsertContactChannel(params: {
73
73
  return null;
74
74
  }
75
75
 
76
- let displayName = params.displayName ?? params.externalUserId ?? "Unknown";
76
+ let displayName = params.displayName ?? address;
77
77
 
78
78
  // When binding a channel to a specific contact (invite redemption), preserve
79
79
  // the target contact's curated displayName instead of overwriting it
@@ -93,7 +93,6 @@ export function upsertContactChannel(params: {
93
93
  {
94
94
  type: params.sourceChannel,
95
95
  address,
96
- externalUserId: params.externalUserId ? address : null,
97
96
  externalChatId: params.externalChatId ?? null,
98
97
  status: (params.status as ChannelStatus) ?? undefined,
99
98
  policy: (params.policy as ChannelPolicy) ?? undefined,
@@ -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;