@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
@@ -16,11 +16,11 @@
16
16
  * re-install at the current pin — the underlying
17
17
  * {@link ./install-from-github.installPlugin} performs that atomically, and the
18
18
  * previously installed copy (including any local edits) is replaced wholesale.
19
- * `ours`/`theirs` instead three-way merge the on-disk tree and the pin against
20
- * the re-materialized install commit so non-conflicting local edits survive,
21
- * resolving conflicting hunks toward the local edit or the pin respectively.
22
- * `assistant` is reserved for assistant-driven conflict resolution and is not
23
- * yet implemented.
19
+ * `ours`/`theirs`/`assistant` instead three-way merge the on-disk tree and the
20
+ * pin against the re-materialized install commit so non-conflicting local edits
21
+ * survive; conflicting hunks resolve toward the local edit (`ours`) or the pin
22
+ * (`theirs`), while `assistant` writes git conflict markers into the file and
23
+ * reports the conflicted paths for the assistant to resolve.
24
24
  *
25
25
  * Designed for direct programmatic use with injected dependencies, mirroring
26
26
  * the sibling plugin libraries. The CLI command `assistant plugins upgrade
@@ -55,7 +55,7 @@ import {
55
55
  readInstallMeta,
56
56
  sanitizePluginName,
57
57
  } from "./install-from-github.js";
58
- import { mergePluginTree } from "./merge-plugin-tree.js";
58
+ import { type ConflictLabels, mergePluginTree } from "./merge-plugin-tree.js";
59
59
  import { computeFingerprint, fingerprintsEqual } from "./plugin-fingerprint.js";
60
60
  import { PluginNotInstalledError } from "./uninstall-plugin.js";
61
61
 
@@ -67,8 +67,8 @@ import { PluginNotInstalledError } from "./uninstall-plugin.js";
67
67
  * wholesale. Matches the historical upgrade behavior.
68
68
  * - `ours` — three-way merge; conflicting hunks resolve toward the local edit.
69
69
  * - `theirs` — three-way merge; conflicting hunks resolve toward the pin.
70
- * - `assistant` — hand conflicts to the assistant to resolve (not yet
71
- * implemented).
70
+ * - `assistant` — three-way merge that writes git conflict markers into
71
+ * conflicting files and reports them for the assistant to resolve.
72
72
  */
73
73
  export type PluginUpgradeStrategy =
74
74
  | "ours"
@@ -151,6 +151,19 @@ export interface PluginUpgradeResult {
151
151
  readonly dryRun: boolean;
152
152
  /** Conflict-resolution strategy the upgrade applied. */
153
153
  readonly strategy: PluginUpgradeStrategy;
154
+ /**
155
+ * Paths (relative to {@link PluginUpgradeResult.target}) left for the
156
+ * assistant to resolve under the `assistant` strategy: text files carry git
157
+ * conflict markers, modify/delete divergences keep the surviving content.
158
+ * Always empty for other strategies.
159
+ */
160
+ readonly conflicts: readonly string[];
161
+ /**
162
+ * Paths of binary files that conflicted under the `assistant` strategy; the
163
+ * local copy was kept since markers cannot be written into binary content.
164
+ * Always empty for other strategies.
165
+ */
166
+ readonly binaryConflicts: readonly string[];
154
167
  /**
155
168
  * Whether the installed copy lacked resolvable provenance before the
156
169
  * upgrade. Such installs are re-pinned to the current SHA, which also
@@ -170,16 +183,6 @@ export class PluginNotUpgradableError extends Error {
170
183
  }
171
184
  }
172
185
 
173
- /** A requested merge strategy is recognized but not yet implemented. */
174
- export class PluginUpgradeStrategyUnsupportedError extends Error {
175
- constructor(readonly strategy: PluginUpgradeStrategy) {
176
- super(
177
- `Upgrade strategy "${strategy}" is not yet supported. Use one of: ours, theirs, overwrite.`,
178
- );
179
- this.name = "PluginUpgradeStrategyUnsupportedError";
180
- }
181
- }
182
-
183
186
  /**
184
187
  * A merge strategy (`ours`/`theirs`) was requested but the install-time
185
188
  * baseline needed for a three-way merge cannot be reconstructed.
@@ -205,16 +208,16 @@ function pluginTarget(name: string, deps: UpgradePluginDeps): string {
205
208
  * Move an installed plugin to the marketplace's current pin.
206
209
  *
207
210
  * The `strategy` controls how local edits are reconciled with the pin:
208
- * `overwrite` (default) re-installs the pin wholesale; `ours`/`theirs` do a
209
- * three-way merge that carries non-conflicting local edits forward, resolving
210
- * conflicting hunks toward the local edit or the pin respectively; `assistant`
211
- * is not yet implemented.
211
+ * `overwrite` (default) re-installs the pin wholesale; `ours`/`theirs`/
212
+ * `assistant` do a three-way merge that carries non-conflicting local edits
213
+ * forward, resolving conflicting hunks toward the local edit (`ours`) or the
214
+ * pin (`theirs`), or leaving git conflict markers in the file for the assistant
215
+ * to resolve (`assistant`).
212
216
  *
213
217
  * Throws {@link PluginNotInstalledError} when no copy is installed,
214
218
  * {@link PluginNotUpgradableError} when the install has no marketplace entry to
215
- * advance to, {@link PluginUpgradeStrategyUnsupportedError} for the `assistant`
216
- * strategy, {@link PluginMergeBaselineError} when a merge strategy is requested
217
- * but the install-time baseline cannot be reconstructed,
219
+ * advance to, {@link PluginMergeBaselineError} when a merge strategy is
220
+ * requested but the install-time baseline cannot be reconstructed,
218
221
  * {@link PluginSourceUnavailableError} when the marketplace catalog is
219
222
  * temporarily unreachable (a retryable outage, distinct from the permanent
220
223
  * no-entry case), and propagates {@link installPlugin}'s errors (e.g. source
@@ -228,10 +231,6 @@ export async function upgradePlugin(
228
231
  const dryRun = opts.dryRun ?? false;
229
232
  const strategy = opts.strategy ?? DEFAULT_PLUGIN_UPGRADE_STRATEGY;
230
233
 
231
- if (strategy === "assistant") {
232
- throw new PluginUpgradeStrategyUnsupportedError(strategy);
233
- }
234
-
235
234
  let inspection: PluginInspection;
236
235
  try {
237
236
  inspection = await inspectPlugin(
@@ -292,6 +291,8 @@ export async function upgradePlugin(
292
291
  fileCount: null,
293
292
  dryRun,
294
293
  strategy,
294
+ conflicts: [],
295
+ binaryConflicts: [],
295
296
  provenanceWasUnknown: false,
296
297
  };
297
298
  }
@@ -308,13 +309,20 @@ export async function upgradePlugin(
308
309
  fileCount: null,
309
310
  dryRun: true,
310
311
  strategy,
312
+ conflicts: [],
313
+ binaryConflicts: [],
311
314
  provenanceWasUnknown,
312
315
  };
313
316
  }
314
317
 
315
- // `ours`/`theirs` carry local edits forward via a three-way merge; the
316
- // default `overwrite` discards them and re-installs the pin wholesale.
317
- if (strategy === "ours" || strategy === "theirs") {
318
+ // `ours`/`theirs`/`assistant` carry local edits forward via a three-way
319
+ // merge; the default `overwrite` discards them and re-installs the pin
320
+ // wholesale.
321
+ if (
322
+ strategy === "ours" ||
323
+ strategy === "theirs" ||
324
+ strategy === "assistant"
325
+ ) {
318
326
  return mergeUpgrade(
319
327
  {
320
328
  name,
@@ -323,6 +331,8 @@ export async function upgradePlugin(
323
331
  remote,
324
332
  fromCommit,
325
333
  fromTimestamp,
334
+ toCommit,
335
+ toTimestamp,
326
336
  provenanceWasUnknown,
327
337
  },
328
338
  deps,
@@ -350,6 +360,8 @@ export async function upgradePlugin(
350
360
  fileCount: result.fileCount,
351
361
  dryRun: false,
352
362
  strategy,
363
+ conflicts: [],
364
+ binaryConflicts: [],
353
365
  provenanceWasUnknown,
354
366
  };
355
367
  }
@@ -359,22 +371,35 @@ export async function upgradePlugin(
359
371
  * and the marketplace pin (`theirs`) against the re-materialized install commit
360
372
  * (`base`), then atomically swapping the merged tree into place pinned at the
361
373
  * new commit. Conflicting hunks resolve toward `ours` or `theirs` per the
362
- * strategy; non-conflicting edits from both sides survive.
374
+ * strategy, or are left as git conflict markers under `assistant`;
375
+ * non-conflicting edits from both sides survive.
363
376
  */
364
377
  async function mergeUpgrade(
365
378
  ctx: {
366
379
  readonly name: string;
367
- readonly strategy: "ours" | "theirs";
380
+ readonly strategy: "ours" | "theirs" | "assistant";
368
381
  readonly local: PluginLocalInfo;
369
382
  readonly remote: PluginRemoteInfo;
370
383
  readonly fromCommit: string | null;
371
384
  readonly fromTimestamp: string | null;
385
+ readonly toCommit: string;
386
+ readonly toTimestamp: string | null;
372
387
  readonly provenanceWasUnknown: boolean;
373
388
  },
374
389
  deps: UpgradePluginDeps,
375
390
  ): Promise<PluginUpgradeResult> {
376
391
  const { name, strategy, local, remote } = ctx;
377
392
 
393
+ // Marker labels name each side by its commit so the assistant can tell the
394
+ // local edit from the incoming pin when resolving.
395
+ const shortSha = (sha: string | null): string =>
396
+ sha ? sha.slice(0, 7) : "unknown";
397
+ const conflictLabels: ConflictLabels = {
398
+ ours: `local edits (was ${shortSha(ctx.fromCommit)})`,
399
+ base: `install baseline ${shortSha(ctx.fromCommit)}`,
400
+ theirs: `upgrade pin ${shortSha(ctx.toCommit)}`,
401
+ };
402
+
378
403
  const meta = readInstallMeta(local.target);
379
404
  if (!meta || !meta.commit || !meta.fingerprint) {
380
405
  throw new PluginMergeBaselineError(
@@ -461,12 +486,13 @@ async function mergeUpgrade(
461
486
  );
462
487
  }
463
488
 
464
- const fileCount = await mergePluginTree({
489
+ const merge = await mergePluginTree({
465
490
  baseDir,
466
491
  oursDir: local.target,
467
492
  theirsDir,
468
493
  destDir: stagingDir,
469
494
  strategy,
495
+ conflictLabels,
470
496
  });
471
497
 
472
498
  const toCommit = theirs.commit ?? remote.commit;
@@ -488,9 +514,11 @@ async function mergeUpgrade(
488
514
  toCommit,
489
515
  toTimestamp,
490
516
  target: join(pluginsDir, name),
491
- fileCount,
517
+ fileCount: merge.fileCount,
492
518
  dryRun: false,
493
519
  strategy,
520
+ conflicts: merge.conflicts,
521
+ binaryConflicts: merge.binaryConflicts,
494
522
  provenanceWasUnknown: ctx.provenanceWasUnknown,
495
523
  };
496
524
  } catch (err) {
@@ -34,8 +34,7 @@ import { registerImageGenerationCommand } from "./commands/image-generation.js";
34
34
  import { registerInferenceCommand } from "./commands/inference.js";
35
35
  import { registerKeysCommand } from "./commands/keys.js";
36
36
  import { registerMcpCommand } from "./commands/mcp.js";
37
- import { registerMemoryV2Command } from "./commands/memory-v2.js";
38
- import { registerMemoryV3Command } from "./commands/memory-v3.js";
37
+ import { registerMemoryCommand } from "./commands/memory/index.js";
39
38
  import { registerNotificationsCommand } from "./commands/notifications.js";
40
39
  import { registerOAuthCommand } from "./commands/oauth/index.js";
41
40
  import { registerPendingCommand } from "./commands/pending.js";
@@ -130,8 +129,7 @@ Examples:
130
129
  registerInferenceCommand(program);
131
130
  registerKeysCommand(program);
132
131
  registerMcpCommand(program);
133
- registerMemoryV2Command(program);
134
- registerMemoryV3Command(program);
132
+ registerMemoryCommand(program);
135
133
  registerNotificationsCommand(program);
136
134
  registerOAuthCommand(program);
137
135
  registerPendingCommand(program);
@@ -0,0 +1,368 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { join } from "node:path";
9
+ import {
10
+ afterAll,
11
+ afterEach,
12
+ beforeEach,
13
+ describe,
14
+ expect,
15
+ mock,
16
+ test,
17
+ } from "bun:test";
18
+
19
+ const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
20
+ const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
21
+
22
+ function ensureTestDir(): void {
23
+ const dirs = [
24
+ WORKSPACE_DIR,
25
+ join(WORKSPACE_DIR, "data"),
26
+ join(WORKSPACE_DIR, "data", "memory"),
27
+ join(WORKSPACE_DIR, "data", "logs"),
28
+ ];
29
+ for (const dir of dirs) {
30
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+
34
+ function makeLoggerStub(): Record<string, unknown> {
35
+ const stub: Record<string, unknown> = {};
36
+ for (const m of [
37
+ "info",
38
+ "warn",
39
+ "error",
40
+ "debug",
41
+ "trace",
42
+ "fatal",
43
+ "silent",
44
+ "child",
45
+ ]) {
46
+ stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
47
+ }
48
+ return stub;
49
+ }
50
+
51
+ mock.module("../../util/logger.js", () => ({
52
+ getLogger: () => makeLoggerStub(),
53
+ }));
54
+
55
+ afterAll(() => {
56
+ mock.restore();
57
+ });
58
+
59
+ import { setOverridesForTesting } from "../../__tests__/feature-flag-test-helpers.js";
60
+ import { invalidateConfigCache } from "../loader.js";
61
+ import { LLMSchema } from "../schemas/llm.js";
62
+ import { seedInferenceProfiles } from "../seed-inference-profiles.js";
63
+ import { reconcileFlagGatedProfiles } from "../sync-gated-profiles.js";
64
+
65
+ type RawConfig = {
66
+ llm: {
67
+ profiles: Record<string, Record<string, unknown>>;
68
+ profileOrder: string[];
69
+ activeProfile?: string;
70
+ advisorProfile?: string;
71
+ };
72
+ };
73
+
74
+ function writeConfig(obj: unknown): void {
75
+ writeFileSync(CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n");
76
+ }
77
+
78
+ function readConfig(): RawConfig {
79
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
80
+ }
81
+
82
+ function seedBalancedConfig(): void {
83
+ writeConfig({ llm: { default: { provider: "anthropic" } } });
84
+ invalidateConfigCache();
85
+ seedInferenceProfiles({});
86
+ }
87
+
88
+ describe("reconcileFlagGatedProfiles", () => {
89
+ beforeEach(() => {
90
+ ensureTestDir();
91
+ if (existsSync(CONFIG_PATH)) rmSync(CONFIG_PATH, { force: true });
92
+ delete process.env.IS_PLATFORM;
93
+ setOverridesForTesting({});
94
+ invalidateConfigCache();
95
+ });
96
+
97
+ afterEach(() => {
98
+ delete process.env.IS_PLATFORM;
99
+ setOverridesForTesting({});
100
+ invalidateConfigCache();
101
+ });
102
+
103
+ test("flag on materializes the managed os-beta profile after balanced", () => {
104
+ process.env.IS_PLATFORM = "true";
105
+ seedBalancedConfig();
106
+ setOverridesForTesting({ "os-beta": true });
107
+
108
+ expect(reconcileFlagGatedProfiles()).toBe(true);
109
+
110
+ const raw = readConfig();
111
+ const osBeta = raw.llm.profiles["os-beta"]!;
112
+ expect(osBeta.model).toBe("accounts/fireworks/models/glm-5p2");
113
+ expect(osBeta.provider_connection).toBe("fireworks-managed");
114
+ expect(osBeta.source).toBe("managed");
115
+ expect(osBeta.label).toBe("OS Beta");
116
+
117
+ const order = raw.llm.profileOrder;
118
+ expect(order.indexOf("os-beta")).toBe(order.indexOf("balanced") + 1);
119
+ });
120
+
121
+ test("flag on in BYOK mode seeds the os-beta profile disabled", () => {
122
+ seedBalancedConfig();
123
+ setOverridesForTesting({ "os-beta": true });
124
+
125
+ expect(reconcileFlagGatedProfiles()).toBe(true);
126
+
127
+ const osBeta = readConfig().llm.profiles["os-beta"]!;
128
+ expect(osBeta.status).toBe("disabled");
129
+ expect(osBeta.label).toBe("OS Beta (Managed)");
130
+ expect(osBeta.source).toBe("managed");
131
+ });
132
+
133
+ test("flag on is idempotent across repeated runs", () => {
134
+ process.env.IS_PLATFORM = "true";
135
+ seedBalancedConfig();
136
+ setOverridesForTesting({ "os-beta": true });
137
+
138
+ expect(reconcileFlagGatedProfiles()).toBe(true);
139
+ invalidateConfigCache();
140
+ expect(reconcileFlagGatedProfiles()).toBe(false);
141
+ });
142
+
143
+ test("flag on preserves user overrides on the managed profile", () => {
144
+ process.env.IS_PLATFORM = "true";
145
+ seedBalancedConfig();
146
+ setOverridesForTesting({ "os-beta": true });
147
+ reconcileFlagGatedProfiles();
148
+
149
+ const raw = readConfig();
150
+ raw.llm.profiles["os-beta"]!.label = "My OS Beta";
151
+ raw.llm.profiles["os-beta"]!.status = "disabled";
152
+ raw.llm.profiles["os-beta"]!.advisorEnabled = true;
153
+ writeConfig(raw);
154
+ invalidateConfigCache();
155
+
156
+ reconcileFlagGatedProfiles();
157
+
158
+ const after = readConfig().llm.profiles["os-beta"]!;
159
+ expect(after.label).toBe("My OS Beta");
160
+ expect(after.status).toBe("disabled");
161
+ expect(after.advisorEnabled).toBe(true);
162
+ expect(after.model).toBe("accounts/fireworks/models/glm-5p2");
163
+ });
164
+
165
+ test("flag off removes a managed os-beta and applies fallbacks", () => {
166
+ process.env.IS_PLATFORM = "true";
167
+ seedBalancedConfig();
168
+ setOverridesForTesting({ "os-beta": true });
169
+ reconcileFlagGatedProfiles();
170
+
171
+ const raw = readConfig();
172
+ raw.llm.activeProfile = "os-beta";
173
+ raw.llm.advisorProfile = "os-beta";
174
+ writeConfig(raw);
175
+ invalidateConfigCache();
176
+
177
+ setOverridesForTesting({ "os-beta": false });
178
+ expect(reconcileFlagGatedProfiles()).toBe(true);
179
+
180
+ const after = readConfig();
181
+ expect(after.llm.profiles["os-beta"]).toBeUndefined();
182
+ expect(after.llm.profileOrder.includes("os-beta")).toBe(false);
183
+ expect(after.llm.activeProfile).toBe("balanced");
184
+ expect(after.llm.advisorProfile).toBe("quality-optimized");
185
+ });
186
+
187
+ test("flag off with no os-beta present is a no-op", () => {
188
+ process.env.IS_PLATFORM = "true";
189
+ seedBalancedConfig();
190
+ setOverridesForTesting({ "os-beta": false });
191
+
192
+ expect(reconcileFlagGatedProfiles()).toBe(false);
193
+ });
194
+
195
+ test("flag off scrubs os-beta from user mix arms, keeping >= 2-arm mixes", () => {
196
+ process.env.IS_PLATFORM = "true";
197
+ seedBalancedConfig();
198
+ setOverridesForTesting({ "os-beta": true });
199
+ reconcileFlagGatedProfiles();
200
+
201
+ const raw = readConfig();
202
+ raw.llm.profiles["my-mix"] = {
203
+ source: "user",
204
+ label: "My Mix",
205
+ mix: [
206
+ { profile: "balanced", weight: 70 },
207
+ { profile: "quality-optimized", weight: 20 },
208
+ { profile: "os-beta", weight: 30 },
209
+ ],
210
+ };
211
+ raw.llm.profileOrder.push("my-mix");
212
+ writeConfig(raw);
213
+ invalidateConfigCache();
214
+
215
+ setOverridesForTesting({ "os-beta": false });
216
+ expect(reconcileFlagGatedProfiles()).toBe(true);
217
+
218
+ const after = readConfig();
219
+ expect(after.llm.profiles["os-beta"]).toBeUndefined();
220
+ const mix = after.llm.profiles["my-mix"]!;
221
+ expect(mix.mix).toEqual([
222
+ { profile: "balanced", weight: 70 },
223
+ { profile: "quality-optimized", weight: 20 },
224
+ ]);
225
+ expect(mix.source).toBe("user");
226
+ expect(mix.label).toBe("My Mix");
227
+
228
+ invalidateConfigCache();
229
+ expect(reconcileFlagGatedProfiles()).toBe(false);
230
+ });
231
+
232
+ test("flag off removes a two-arm mix that loses os-beta and repoints refs", () => {
233
+ process.env.IS_PLATFORM = "true";
234
+ seedBalancedConfig();
235
+ setOverridesForTesting({ "os-beta": true });
236
+ reconcileFlagGatedProfiles();
237
+
238
+ const raw = readConfig();
239
+ raw.llm.profiles["experiment"] = {
240
+ source: "user",
241
+ label: "Experiment",
242
+ mix: [
243
+ { profile: "balanced", weight: 50 },
244
+ { profile: "os-beta", weight: 50 },
245
+ ],
246
+ };
247
+ raw.llm.profileOrder.push("experiment");
248
+ raw.llm.activeProfile = "experiment";
249
+ raw.llm.advisorProfile = "os-beta";
250
+ (raw.llm as Record<string, unknown>).callSites = {
251
+ mainAgent: { profile: "experiment" },
252
+ };
253
+ writeConfig(raw);
254
+ invalidateConfigCache();
255
+
256
+ setOverridesForTesting({ "os-beta": false });
257
+ expect(reconcileFlagGatedProfiles()).toBe(true);
258
+
259
+ const after = readConfig();
260
+ expect(after.llm.profiles["os-beta"]).toBeUndefined();
261
+ expect(after.llm.profiles["experiment"]).toBeUndefined();
262
+ expect(after.llm.profileOrder.includes("os-beta")).toBe(false);
263
+ expect(after.llm.profileOrder.includes("experiment")).toBe(false);
264
+ expect(after.llm.activeProfile).toBe("balanced");
265
+ expect(after.llm.advisorProfile).toBe("quality-optimized");
266
+ expect(
267
+ (
268
+ after.llm as unknown as Record<
269
+ string,
270
+ Record<string, Record<string, unknown>>
271
+ >
272
+ ).callSites.mainAgent.profile,
273
+ ).toBeUndefined();
274
+
275
+ expect(LLMSchema.safeParse(after.llm).success).toBe(true);
276
+ });
277
+
278
+ test("flag off shortens a three-arm mix to two surviving arms", () => {
279
+ process.env.IS_PLATFORM = "true";
280
+ seedBalancedConfig();
281
+ setOverridesForTesting({ "os-beta": true });
282
+ reconcileFlagGatedProfiles();
283
+
284
+ const raw = readConfig();
285
+ raw.llm.profiles["blend"] = {
286
+ source: "user",
287
+ label: "Blend",
288
+ mix: [
289
+ { profile: "balanced", weight: 40 },
290
+ { profile: "os-beta", weight: 30 },
291
+ { profile: "quality-optimized", weight: 30 },
292
+ ],
293
+ };
294
+ raw.llm.profileOrder.push("blend");
295
+ writeConfig(raw);
296
+ invalidateConfigCache();
297
+
298
+ setOverridesForTesting({ "os-beta": false });
299
+ expect(reconcileFlagGatedProfiles()).toBe(true);
300
+
301
+ const after = readConfig();
302
+ expect(after.llm.profiles["os-beta"]).toBeUndefined();
303
+ expect(after.llm.profiles["blend"]!.mix).toEqual([
304
+ { profile: "balanced", weight: 40 },
305
+ { profile: "quality-optimized", weight: 30 },
306
+ ]);
307
+
308
+ expect(LLMSchema.safeParse(after.llm).success).toBe(true);
309
+ });
310
+
311
+ test("flag off clears a call-site profile reference to os-beta", () => {
312
+ process.env.IS_PLATFORM = "true";
313
+ seedBalancedConfig();
314
+ setOverridesForTesting({ "os-beta": true });
315
+ reconcileFlagGatedProfiles();
316
+
317
+ const raw = readConfig();
318
+ (raw.llm as Record<string, unknown>).callSites = {
319
+ advisor: { profile: "os-beta", temperature: 0.3 },
320
+ };
321
+ writeConfig(raw);
322
+ invalidateConfigCache();
323
+
324
+ setOverridesForTesting({ "os-beta": false });
325
+ expect(reconcileFlagGatedProfiles()).toBe(true);
326
+
327
+ const after = readConfig();
328
+ const advisor = (
329
+ after.llm as unknown as Record<
330
+ string,
331
+ Record<string, Record<string, unknown>>
332
+ >
333
+ ).callSites.advisor;
334
+ expect(advisor.profile).toBeUndefined();
335
+ expect(advisor.temperature).toBe(0.3);
336
+
337
+ expect(LLMSchema.safeParse(after.llm).success).toBe(true);
338
+ });
339
+
340
+ test("a user-owned os-beta profile is never touched", () => {
341
+ process.env.IS_PLATFORM = "true";
342
+ writeConfig({
343
+ llm: {
344
+ default: { provider: "anthropic" },
345
+ profiles: {
346
+ "os-beta": {
347
+ source: "user",
348
+ label: "Mine",
349
+ provider: "anthropic",
350
+ model: "claude-x",
351
+ provider_connection: "anthropic-personal",
352
+ },
353
+ },
354
+ profileOrder: ["os-beta"],
355
+ },
356
+ });
357
+ invalidateConfigCache();
358
+ const before = readFileSync(CONFIG_PATH, "utf-8");
359
+
360
+ setOverridesForTesting({ "os-beta": true });
361
+ expect(reconcileFlagGatedProfiles()).toBe(false);
362
+ expect(readFileSync(CONFIG_PATH, "utf-8")).toBe(before);
363
+
364
+ setOverridesForTesting({ "os-beta": false });
365
+ expect(reconcileFlagGatedProfiles()).toBe(false);
366
+ expect(readFileSync(CONFIG_PATH, "utf-8")).toBe(before);
367
+ });
368
+ });
@@ -103,7 +103,11 @@ function parseRegistryToDefaults(parsed: unknown): FeatureFlagDefaultsRegistry {
103
103
  const entry = flag as Record<string, unknown>;
104
104
  if (entry.scope !== "assistant" && entry.scope !== "both") continue;
105
105
  if (typeof entry.key !== "string") continue;
106
- if (typeof entry.defaultEnabled !== "boolean" && typeof entry.defaultEnabled !== "string") continue;
106
+ if (
107
+ typeof entry.defaultEnabled !== "boolean" &&
108
+ typeof entry.defaultEnabled !== "string"
109
+ )
110
+ continue;
107
111
 
108
112
  result[entry.key as string] = {
109
113
  defaultEnabled: entry.defaultEnabled,
@@ -178,6 +182,12 @@ const DEFAULT_INIT_RETRY_BACKOFFS_MS: readonly number[] = [
178
182
  * tests preseed flag state via `setOverridesForTesting()` (in
179
183
  * `__tests__/feature-flag-test-helpers.ts`) without the gateway IPC call
180
184
  * clobbering their setup.
185
+ *
186
+ * Resolves `true` when the override cache is populated from the gateway and
187
+ * `false` when the cache is left unset (exhausted retries / unreachable
188
+ * gateway). Callers that mutate persisted state from flag values gate that
189
+ * work on a `true` result so a failed fetch never resolves a flag to its
190
+ * registry default and drops user state.
181
191
  */
182
192
  export async function initFeatureFlagOverrides(options?: {
183
193
  retryBackoffsMs?: readonly number[];
@@ -188,8 +198,8 @@ export async function initFeatureFlagOverrides(options?: {
188
198
  * instead of blocking startup.
189
199
  */
190
200
  callTimeoutMs?: number;
191
- }): Promise<void> {
192
- if (isCachedFromGateway()) return;
201
+ }): Promise<boolean> {
202
+ if (isCachedFromGateway()) return true;
193
203
 
194
204
  const backoffs = options?.retryBackoffsMs ?? DEFAULT_INIT_RETRY_BACKOFFS_MS;
195
205
  const callTimeoutMs = options?.callTimeoutMs;
@@ -205,7 +215,7 @@ export async function initFeatureFlagOverrides(options?: {
205
215
  // Re-check after the wait: a concurrent caller (e.g. a test using
206
216
  // `setOverridesForTesting`) may have populated the cache while we
207
217
  // were sleeping. Bail out so we don't clobber their setup.
208
- if (isCachedFromGateway()) return;
218
+ if (isCachedFromGateway()) return true;
209
219
  }
210
220
 
211
221
  const gatewayOverrides = await fetchOverridesFromGateway(callTimeoutMs);
@@ -217,7 +227,7 @@ export async function initFeatureFlagOverrides(options?: {
217
227
  "Feature flag overrides loaded from gateway after retry",
218
228
  );
219
229
  }
220
- return;
230
+ return true;
221
231
  }
222
232
  }
223
233
 
@@ -230,6 +240,7 @@ export async function initFeatureFlagOverrides(options?: {
230
240
  "Feature flag overrides empty after all retries; falling back to registry defaults and fail-closed undeclared flags",
231
241
  );
232
242
  }
243
+ return false;
233
244
  }
234
245
 
235
246
  /**
@@ -262,10 +273,14 @@ export function clearFeatureFlagOverridesCache(): void {
262
273
  * retries (the gateway is known to be up because it just pushed an event).
263
274
  * Called by the gateway flag listener when a `feature_flags_changed` event
264
275
  * arrives.
276
+ *
277
+ * Resolves `true` when the cache was repopulated from the gateway, `false`
278
+ * when the fetch came back empty/failed — callers gate persisted-state
279
+ * reconciliation on a `true` result.
265
280
  */
266
- export async function refreshOverridesFromGateway(): Promise<void> {
281
+ export async function refreshOverridesFromGateway(): Promise<boolean> {
267
282
  clearFeatureFlagOverridesCache();
268
- await initFeatureFlagOverrides({ retryBackoffsMs: [] });
283
+ return initFeatureFlagOverrides({ retryBackoffsMs: [] });
269
284
  }
270
285
 
271
286
  // ---------------------------------------------------------------------------