@vellumai/assistant 0.11.6 → 0.11.7-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 (478) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -35,6 +35,8 @@ export interface ThresholdReaderMockState {
35
35
  executionContext?: string;
36
36
  cellQuery?: Record<string, unknown>;
37
37
  }>;
38
+ /** Resolved by `getContactAutoApproveThreshold`. */
39
+ contactThreshold: string | null;
38
40
  /** How many times the cell was looked up. */
39
41
  cellLookups: number;
40
42
  }
@@ -45,6 +47,7 @@ export const thresholdReaderMock: ThresholdReaderMockState = {
45
47
  cell: { ok: true, resolved: null },
46
48
  roomDefault: "low",
47
49
  thresholdReads: [],
50
+ contactThreshold: null,
48
51
  cellLookups: 0,
49
52
  };
50
53
 
@@ -58,6 +61,7 @@ export function resetThresholdReaderMock(): void {
58
61
  thresholdReaderMock.cell = { ok: true, resolved: null };
59
62
  thresholdReaderMock.roomDefault = "low";
60
63
  thresholdReaderMock.thresholdReads.length = 0;
64
+ thresholdReaderMock.contactThreshold = null;
61
65
  thresholdReaderMock.cellLookups = 0;
62
66
  }
63
67
 
@@ -81,6 +85,8 @@ export function installThresholdReaderMock(): void {
81
85
  return thresholdReaderMock.threshold;
82
86
  },
83
87
  refreshAutoApproveThreshold: async () => thresholdReaderMock.refreshed,
88
+ getContactAutoApproveThreshold: async () =>
89
+ thresholdReaderMock.contactThreshold,
84
90
  resolveChannelPermissionCell: async () => {
85
91
  thresholdReaderMock.cellLookups += 1;
86
92
  return thresholdReaderMock.cell;
@@ -14,6 +14,11 @@ mock.module("../messaging/providers/slack/withdraw.js", () => ({
14
14
  withdrawSlackApprovalCard,
15
15
  }));
16
16
 
17
+ const withdrawDiscordApprovalCard = mock(async (_params: unknown) => undefined);
18
+ mock.module("../messaging/providers/discord/withdraw.js", () => ({
19
+ withdrawDiscordApprovalCard,
20
+ }));
21
+
17
22
  const withdrawTelegramApprovalCard = mock(
18
23
  async (_params: Record<string, unknown>) => {},
19
24
  );
@@ -68,6 +73,7 @@ describe("withdrawGuardianRequestCards", () => {
68
73
  markSurfaceCompleted.mockClear();
69
74
  withdrawSlackApprovalCard.mockClear();
70
75
  withdrawTelegramApprovalCard.mockClear();
76
+ withdrawDiscordApprovalCard.mockClear();
71
77
  });
72
78
 
73
79
  test("withdraws + broadcasts the in-app card when the decision came from another surface", async () => {
@@ -232,6 +238,30 @@ describe("withdrawGuardianRequestCards", () => {
232
238
  });
233
239
  });
234
240
 
241
+ test("withdraws the Discord card by rewriting it to the outcome", async () => {
242
+ const req = makeRequest({ sourceChannel: "discord" });
243
+ bridgeState.seedDelivery({
244
+ requestId: req.id,
245
+ destinationChannel: "discord",
246
+ destinationChatId: "111222333444555666",
247
+ destinationMessageId: "2001",
248
+ });
249
+
250
+ await withdrawGuardianRequestCards({
251
+ request: req,
252
+ status: "approved",
253
+ originChannel: "vellum",
254
+ });
255
+
256
+ expect(withdrawTelegramApprovalCard).not.toHaveBeenCalled();
257
+ expect(withdrawDiscordApprovalCard).toHaveBeenCalledTimes(1);
258
+ expect(withdrawDiscordApprovalCard).toHaveBeenCalledWith({
259
+ guardianUserId: "111222333444555666",
260
+ messageId: "2001",
261
+ status: "approved",
262
+ });
263
+ });
264
+
235
265
  test("suppresses the Telegram status reply only when the origin flow replies to the guardian", async () => {
236
266
  const req = makeRequest({ sourceChannel: "telegram" });
237
267
  bridgeState.seedDelivery({
@@ -101,8 +101,6 @@ describe("handleConfirmationResponse guardian status sync", () => {
101
101
  "req-confirm-allow",
102
102
  "allow",
103
103
  {
104
- selectedPattern: undefined,
105
- selectedScope: undefined,
106
104
  emissionContext: { source: "button" },
107
105
  },
108
106
  ]);
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Tests for the shared sips-backed image converter.
2
+ * Tests for the shared cross-platform image converter.
3
3
  *
4
4
  * Pure logic (ftyp sniffing, filename rewriting, passthrough behavior) runs
5
- * everywhere; actual conversion requires macOS `sips`, so those cases are
6
- * gated on darwin.
5
+ * everywhere; macOS-specific HEIC fixture generation is gated on Darwin.
7
6
  */
8
7
 
9
8
  import { createHash } from "node:crypto";
@@ -151,6 +150,41 @@ describe("isCompleteJpeg", () => {
151
150
  });
152
151
  });
153
152
 
153
+ describe("convertImageToJpeg", () => {
154
+ test("converts PNG input with the cross-platform encoder", async () => {
155
+ const converted = await convertImageToJpeg(PNG_1PX_BYTES, {
156
+ resizeToPx: { width: 2, height: 2 },
157
+ quality: 82,
158
+ });
159
+
160
+ expect(converted).not.toBeNull();
161
+ expect(isCompleteJpeg(converted!)).toBe(true);
162
+ expect(hasValidJpegStructure(converted!)).toBe(true);
163
+ });
164
+
165
+ test("applies EXIF orientation before encoding", async () => {
166
+ const { default: sharp } = await import("sharp");
167
+ const source = await sharp({
168
+ create: {
169
+ width: 2,
170
+ height: 1,
171
+ channels: 3,
172
+ background: { r: 255, g: 0, b: 0 },
173
+ },
174
+ })
175
+ .jpeg()
176
+ .withMetadata({ orientation: 6 })
177
+ .toBuffer();
178
+
179
+ const converted = await convertImageToJpeg(source, { quality: 83 });
180
+ expect(converted).not.toBeNull();
181
+ const metadata = await sharp(converted!).metadata();
182
+ expect(metadata.width).toBe(1);
183
+ expect(metadata.height).toBe(2);
184
+ expect(metadata.orientation).toBeUndefined();
185
+ });
186
+ });
187
+
154
188
  // Mirrors the converter's cache-key derivation so the test can plant a
155
189
  // poisoned entry at the exact path a conversion will consult.
156
190
  function cacheKeyFor(bytes: Uint8Array, quality: number): string {
@@ -256,7 +290,7 @@ describe("normalizeImageBytes passthrough", () => {
256
290
  });
257
291
 
258
292
  test("HEIF header with undecodable payload passes through", async () => {
259
- // sips fails (or is absent off-macOS) the original bytes are kept.
293
+ // The malformed container cannot be decoded, so the bytes are kept.
260
294
  const fake = fakeHeifHeaderBytes();
261
295
  const result = await normalizeImageBytes("image/heic", fake);
262
296
  expect(result.converted).toBe(false);
@@ -13,6 +13,7 @@ const mockCatalogSkills = mock(
13
13
  description: string;
14
14
  source: string;
15
15
  directoryPath?: string;
16
+ platforms?: Array<"macos" | "windows" | "linux">;
16
17
  }> => [],
17
18
  );
18
19
  const mockClawhubInstall = mock(
@@ -110,6 +111,7 @@ mock.module("../providers/provider-send-message.js", () => ({
110
111
  userMessage: () => ({}),
111
112
  }));
112
113
  mock.module("../skills/catalog-cache.js", () => ({
114
+ getCachedCatalogSync: () => [],
113
115
  getCatalog: mockGetCatalog,
114
116
  }));
115
117
  mock.module("../skills/catalog-install.js", () => ({
@@ -382,6 +384,29 @@ Body.
382
384
  expect(mockClawhubInstall).not.toHaveBeenCalled();
383
385
  });
384
386
 
387
+ test("rejects bundled skills that do not support the host platform", async () => {
388
+ const incompatiblePlatform =
389
+ process.platform === "darwin" ? "windows" : "macos";
390
+ mockCatalogSkills.mockReturnValue([
391
+ {
392
+ id: "platform-skill",
393
+ displayName: "Platform Skill",
394
+ description: "A platform-specific skill",
395
+ source: "bundled",
396
+ directoryPath: "/tmp/test-bundled-skills/platform-skill",
397
+ platforms: [incompatiblePlatform],
398
+ },
399
+ ]);
400
+
401
+ const result = await installSkill({ slug: "platform-skill" });
402
+
403
+ expect(result.success).toBe(false);
404
+ if (!result.success) {
405
+ expect(result.error).toContain("unavailable on this operating system");
406
+ }
407
+ expect(mockEnsureSkillEntry).not.toHaveBeenCalled();
408
+ });
409
+
385
410
  test("skills.sh install failure propagates error", async () => {
386
411
  mockInstallExternalSkill.mockRejectedValue(
387
412
  new Error("Skill not found in repo"),
@@ -67,6 +67,7 @@ interface ClientCatalogModel {
67
67
  cacheWritePer1mTokens?: number;
68
68
  }>;
69
69
  };
70
+ featureFlag?: string;
70
71
  }
71
72
 
72
73
  interface ClientCatalogEntry {
@@ -79,6 +80,7 @@ interface ClientCatalogEntry {
79
80
  apiKeyPlaceholder?: string;
80
81
  credentialsGuide?: ClientCatalogCredentialsGuide;
81
82
  supportsPlatformAuth?: boolean;
83
+ featureFlag?: string;
82
84
  defaultModel: string;
83
85
  models: ClientCatalogModel[];
84
86
  }
@@ -133,6 +135,7 @@ describe("LLM catalog parity: daemon vs client", () => {
133
135
  expect(clientEntry.supportsPlatformAuth).toBe(
134
136
  daemonEntry.supportsPlatformAuth,
135
137
  );
138
+ expect(clientEntry.featureFlag).toBe(daemonEntry.featureFlag);
136
139
  expect(clientEntry.credentialsGuide).toEqual(
137
140
  daemonEntry.credentialsGuide,
138
141
  );
@@ -199,6 +202,7 @@ describe("LLM catalog parity: daemon vs client", () => {
199
202
  expect(clientModel.supportsVision).toBe(daemonModel.supportsVision);
200
203
  expect(clientModel.supportsToolUse).toBe(daemonModel.supportsToolUse);
201
204
  expect(clientModel.pricing).toEqual(daemonModel.pricing);
205
+ expect(clientModel.featureFlag).toBe(daemonModel.featureFlag);
202
206
  }
203
207
  }
204
208
  });
@@ -9,6 +9,7 @@ import {
9
9
  resolveCallSiteConfigWithProfile,
10
10
  resolveDefaultProfileKey,
11
11
  resolveEffectiveProfileKey,
12
+ resolveSingleRouteProfileKey,
12
13
  } from "../config/llm-resolver.js";
13
14
  import { type LLMCallSite, LLMSchema } from "../config/schemas/llm.js";
14
15
  import { resolveModelIntent } from "../providers/model-intents.js";
@@ -303,6 +304,26 @@ describe("resolveCallSiteConfig", () => {
303
304
  expect(resolved.effort).toBe("high");
304
305
  });
305
306
 
307
+ test("a winning profile's fallbackProfile metadata never leaks into the resolved config", () => {
308
+ const llm = LLMSchema.parse({
309
+ defaultProvider: { provider: "vellum" },
310
+ profiles: {
311
+ "quality-optimized": {
312
+ source: "managed",
313
+ fallbackProfile: "quality-optimized-backup",
314
+ },
315
+ },
316
+ callSites: {
317
+ memoryExtraction: { profile: "quality-optimized" },
318
+ },
319
+ });
320
+ const resolved = resolveCallSiteConfig("memoryExtraction", llm);
321
+ expect(resolved.model).toBe("gpt-5.6-sol");
322
+ // `fallbackProfile` is ProfileEntry-only metadata, absent from
323
+ // `LLMConfigBase`; the resolved config must not carry the key at all.
324
+ expect(Object.hasOwn(resolved, "fallbackProfile")).toBe(false);
325
+ });
326
+
306
327
  test("topP defaults to null when no profile or override sets it", () => {
307
328
  const llm = LLMSchema.parse({});
308
329
  const resolved = resolveCallSiteConfig("mainAgent", llm);
@@ -869,6 +890,59 @@ describe("mix profiles", () => {
869
890
  );
870
891
  });
871
892
 
893
+ test("resolveSingleRouteProfileKey names standard winners and refuses mixes", () => {
894
+ // The mix's arm is picked per conversation and can differ from any
895
+ // post-hoc recomputation, so the outer mix key must never be used as a
896
+ // single-route attribution scope.
897
+ expect(resolveSingleRouteProfileKey("mainAgent", mixLlm)).toBeUndefined();
898
+ expect(
899
+ resolveSingleRouteProfileKey("mainAgent", mixLlm, {
900
+ overrideProfile: "a",
901
+ }),
902
+ ).toBe("a");
903
+ // An unusable override falls through to the active mix, which still
904
+ // refuses to name a route.
905
+ expect(
906
+ resolveSingleRouteProfileKey("mainAgent", mixLlm, {
907
+ overrideProfile: "no-such-profile",
908
+ }),
909
+ ).toBeUndefined();
910
+ });
911
+
912
+ test("resolveSingleRouteProfileKey honors the dispatch resolvability predicate", () => {
913
+ // A pin whose provider dispatch cannot resolve (e.g. a force-deleted
914
+ // connection) must not be claimed as a scope: selection falls through to
915
+ // the active mix exactly as dispatch does, which then refuses to name a
916
+ // route. Without the predicate the stale pin still wins the chain, which
917
+ // is why scope callers must pass dispatch's predicate.
918
+ const staleLlm = LLMSchema.parse({
919
+ profiles: {
920
+ a: { provider: "anthropic", model: "model-a" },
921
+ b: { provider: "anthropic", model: "model-b" },
922
+ stale: { provider: "openai", model: "model-s" },
923
+ ab: {
924
+ mix: [
925
+ { profile: "a", weight: 1 },
926
+ { profile: "b", weight: 1 },
927
+ ],
928
+ },
929
+ },
930
+ activeProfile: "ab",
931
+ });
932
+ const openaiForceDeleted = (provider: string) => provider !== "openai";
933
+ expect(
934
+ resolveSingleRouteProfileKey("mainAgent", staleLlm, {
935
+ overrideProfile: "stale",
936
+ isResolvableProvider: openaiForceDeleted,
937
+ }),
938
+ ).toBeUndefined();
939
+ expect(
940
+ resolveSingleRouteProfileKey("mainAgent", staleLlm, {
941
+ overrideProfile: "stale",
942
+ }),
943
+ ).toBe("stale");
944
+ });
945
+
872
946
  test("all dereference spots in a turn agree for the same seed", () => {
873
947
  // mainAgent (mix as activeProfile) and a non-main call site resolving the
874
948
  // same mix as its call-site profile must pick the same arm when given the
@@ -0,0 +1,359 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ BACKUP_PROFILE_KEYS,
7
+ DEFAULT_PROFILE_KEYS,
8
+ FALLBACK_PROFILE_BY_KEY,
9
+ } from "../config/default-profile-names.js";
10
+ import { LLMSchema } from "../config/schemas/llm.js";
11
+
12
+ describe("LLMSchema fallbackProfile", () => {
13
+ test.each([...DEFAULT_PROFILE_KEYS])(
14
+ "accepts the code-owned managed pointer for %s",
15
+ (profileName) => {
16
+ const result = LLMSchema.safeParse({
17
+ defaultProvider: { provider: "vellum" },
18
+ profiles: {
19
+ [profileName]: {
20
+ source: "managed",
21
+ fallbackProfile: FALLBACK_PROFILE_BY_KEY[profileName],
22
+ },
23
+ },
24
+ });
25
+ expect(result.success).toBe(true);
26
+ },
27
+ );
28
+
29
+ test("rejects a user-authored fallbackProfile even when its target exists", () => {
30
+ const result = LLMSchema.safeParse({
31
+ profiles: {
32
+ primary: { fallbackProfile: "backup" },
33
+ backup: { speed: "fast" },
34
+ },
35
+ });
36
+ expect(result.success).toBe(false);
37
+ if (!result.success) {
38
+ const issue = result.error.issues.find((i) =>
39
+ i.message.includes("Automatic fallbacks are code-owned"),
40
+ );
41
+ expect(issue?.path).toEqual(["profiles", "primary", "fallbackProfile"]);
42
+ }
43
+ });
44
+
45
+ test("rejects a fallbackProfile on a user-owned default-profile shadow", () => {
46
+ const profileName = DEFAULT_PROFILE_KEYS[0];
47
+ const result = LLMSchema.safeParse({
48
+ profiles: {
49
+ [profileName]: {
50
+ source: "user",
51
+ fallbackProfile: FALLBACK_PROFILE_BY_KEY[profileName],
52
+ },
53
+ },
54
+ });
55
+ expect(result.success).toBe(false);
56
+ });
57
+
58
+ test("rejects a managed default pointing anywhere except its code-owned backup", () => {
59
+ const profileName = DEFAULT_PROFILE_KEYS[0];
60
+ const result = LLMSchema.safeParse({
61
+ profiles: {
62
+ [profileName]: {
63
+ source: "managed",
64
+ fallbackProfile: "custom-backup",
65
+ },
66
+ "custom-backup": { speed: "fast" },
67
+ },
68
+ });
69
+ expect(result.success).toBe(false);
70
+ });
71
+
72
+ test("empty-string fallbackProfile is rejected at field level", () => {
73
+ const result = LLMSchema.safeParse({
74
+ profiles: {
75
+ primary: { fallbackProfile: "" },
76
+ },
77
+ });
78
+ expect(result.success).toBe(false);
79
+ });
80
+
81
+ test("profiles without fallbackProfile still parse (back-compat)", () => {
82
+ const result = LLMSchema.safeParse({
83
+ profiles: {
84
+ fast: { speed: "fast", effort: "low" },
85
+ thorough: { effort: "high", maxTokens: 128000 },
86
+ },
87
+ callSites: {
88
+ mainAgent: { profile: "thorough" },
89
+ },
90
+ });
91
+ expect(result.success).toBe(true);
92
+ if (result.success) {
93
+ expect(result.data.profiles["fast"]?.fallbackProfile).toBeUndefined();
94
+ }
95
+ });
96
+
97
+ test("a code-owned fallback pointer is rejected on a non-managed column", () => {
98
+ const profileName = DEFAULT_PROFILE_KEYS[0];
99
+ for (const provider of ["anthropic", "chatgpt"] as const) {
100
+ const result = LLMSchema.safeParse({
101
+ defaultProvider: { provider },
102
+ profiles: {
103
+ [profileName]: {
104
+ source: "managed",
105
+ fallbackProfile: FALLBACK_PROFILE_BY_KEY[profileName],
106
+ },
107
+ },
108
+ });
109
+ expect(result.success).toBe(false);
110
+ if (!result.success) {
111
+ const issue = result.error.issues.find((i) =>
112
+ i.path.join(".").endsWith("fallbackProfile"),
113
+ );
114
+ expect(issue?.message).toContain("managed backup profile");
115
+ expect(issue?.path).toEqual([
116
+ "profiles",
117
+ profileName,
118
+ "fallbackProfile",
119
+ ]);
120
+ }
121
+ }
122
+ });
123
+
124
+ test("z.toJSONSchema still generates for LLMSchema (config docs/routes)", () => {
125
+ const json = z.toJSONSchema(LLMSchema, {
126
+ unrepresentable: "any",
127
+ io: "input",
128
+ });
129
+ expect(json).toMatchObject({
130
+ properties: {
131
+ profiles: {
132
+ additionalProperties: {
133
+ properties: { fallbackProfile: { readOnly: true } },
134
+ },
135
+ },
136
+ },
137
+ });
138
+ });
139
+ });
140
+
141
+ describe("LLMSchema backup profile references vs llm.defaultProvider", () => {
142
+ const backupKey = BACKUP_PROFILE_KEYS[0];
143
+ const managed = { provider: "vellum" } as const;
144
+ const nonManaged = [{ provider: "anthropic" }, { provider: "chatgpt" }];
145
+
146
+ test("activeProfile naming a backup parses on the managed column", () => {
147
+ const result = LLMSchema.safeParse({
148
+ defaultProvider: managed,
149
+ activeProfile: backupKey,
150
+ });
151
+ expect(result.success).toBe(true);
152
+ });
153
+
154
+ test("activeProfile naming a backup is rejected on a non-managed column", () => {
155
+ for (const defaultProvider of nonManaged) {
156
+ const result = LLMSchema.safeParse({
157
+ defaultProvider,
158
+ activeProfile: backupKey,
159
+ });
160
+ expect(result.success).toBe(false);
161
+ if (!result.success) {
162
+ const issue = result.error.issues.find(
163
+ (i) => i.path.join(".") === "activeProfile",
164
+ );
165
+ expect(issue?.message).toContain("managed backup profile");
166
+ }
167
+ }
168
+ });
169
+
170
+ test("advisorProfile naming a backup follows the same split", () => {
171
+ expect(
172
+ LLMSchema.safeParse({
173
+ defaultProvider: managed,
174
+ advisorProfile: backupKey,
175
+ }).success,
176
+ ).toBe(true);
177
+ for (const defaultProvider of nonManaged) {
178
+ const result = LLMSchema.safeParse({
179
+ defaultProvider,
180
+ advisorProfile: backupKey,
181
+ });
182
+ expect(result.success).toBe(false);
183
+ if (!result.success) {
184
+ expect(
185
+ result.error.issues.some(
186
+ (i) => i.path.join(".") === "advisorProfile",
187
+ ),
188
+ ).toBe(true);
189
+ }
190
+ }
191
+ });
192
+
193
+ test("a call-site pin on a backup follows the same split", () => {
194
+ expect(
195
+ LLMSchema.safeParse({
196
+ defaultProvider: managed,
197
+ callSites: { recall: { profile: backupKey } },
198
+ }).success,
199
+ ).toBe(true);
200
+ for (const defaultProvider of nonManaged) {
201
+ const result = LLMSchema.safeParse({
202
+ defaultProvider,
203
+ callSites: { recall: { profile: backupKey } },
204
+ });
205
+ expect(result.success).toBe(false);
206
+ if (!result.success) {
207
+ const issue = result.error.issues.find(
208
+ (i) => i.path.join(".") === "callSites.recall.profile",
209
+ );
210
+ expect(issue?.message).toContain("managed backup profile");
211
+ }
212
+ }
213
+ });
214
+
215
+ test("a mix arm naming a backup follows the same split", () => {
216
+ const mixConfig = (defaultProvider: { provider: string }) => ({
217
+ defaultProvider,
218
+ profiles: {
219
+ armA: { speed: "fast" },
220
+ blend: {
221
+ mix: [
222
+ { profile: backupKey, weight: 1 },
223
+ { profile: "armA", weight: 1 },
224
+ ],
225
+ },
226
+ },
227
+ });
228
+ expect(LLMSchema.safeParse(mixConfig(managed)).success).toBe(true);
229
+ for (const defaultProvider of nonManaged) {
230
+ expect(LLMSchema.safeParse(mixConfig(defaultProvider)).success).toBe(
231
+ false,
232
+ );
233
+ }
234
+ });
235
+
236
+ test("an absent or malformed defaultProvider reads as the managed column", () => {
237
+ // `DefaultProviderField` catches an invalid value to `undefined`, and an
238
+ // install predating the field is managed by definition, so both keep
239
+ // their backups referenceable rather than losing a valid selection.
240
+ for (const defaultProvider of [undefined, { provider: 42 }, "vellum"]) {
241
+ const result = LLMSchema.safeParse({
242
+ defaultProvider,
243
+ activeProfile: backupKey,
244
+ });
245
+ expect(result.success).toBe(true);
246
+ }
247
+ });
248
+
249
+ test("the default profile keys stay valid on every column", () => {
250
+ for (const defaultProvider of [managed, ...nonManaged]) {
251
+ for (const key of DEFAULT_PROFILE_KEYS) {
252
+ expect(
253
+ LLMSchema.safeParse({ defaultProvider, activeProfile: key }).success,
254
+ ).toBe(true);
255
+ }
256
+ }
257
+ });
258
+
259
+ // A managed install can persist a thin `{ source: "managed" }` stub for a
260
+ // backup key: `normalizeManagedProfileWrites` reduces a `config get` ->
261
+ // `config set` echo of the effective profile list to exactly that. The stub
262
+ // holds no body of its own, so it must not carry a reference past the
263
+ // provider gate once the install moves off the managed column.
264
+ const stub = { source: "managed" } as const;
265
+
266
+ test("a managed stub does not keep a backup reference alive off the managed column", () => {
267
+ for (const defaultProvider of nonManaged) {
268
+ const result = LLMSchema.safeParse({
269
+ defaultProvider,
270
+ profiles: { [backupKey]: stub, primary: { effort: "high" } },
271
+ activeProfile: backupKey,
272
+ });
273
+ expect(result.success).toBe(false);
274
+ if (!result.success) {
275
+ const issue = result.error.issues.find(
276
+ (i) => i.path.join(".") === "activeProfile",
277
+ );
278
+ expect(issue?.message).toContain("managed backup profile");
279
+ }
280
+ }
281
+ });
282
+
283
+ test("a managed stub does not keep a call-site pin or mix arm alive either", () => {
284
+ for (const defaultProvider of nonManaged) {
285
+ const callSitePin = LLMSchema.safeParse({
286
+ defaultProvider,
287
+ profiles: { [backupKey]: stub },
288
+ callSites: { recall: { profile: backupKey } },
289
+ });
290
+ expect(callSitePin.success).toBe(false);
291
+
292
+ const mixArm = LLMSchema.safeParse({
293
+ defaultProvider,
294
+ profiles: {
295
+ [backupKey]: stub,
296
+ armA: { speed: "fast" },
297
+ blend: {
298
+ mix: [
299
+ { profile: backupKey, weight: 1 },
300
+ { profile: "armA", weight: 1 },
301
+ ],
302
+ },
303
+ },
304
+ });
305
+ expect(mixArm.success).toBe(false);
306
+ }
307
+ });
308
+
309
+ test("a managed stub does not keep a fallbackProfile pointer alive either", () => {
310
+ for (const defaultProvider of nonManaged) {
311
+ const result = LLMSchema.safeParse({
312
+ defaultProvider,
313
+ profiles: {
314
+ [backupKey]: stub,
315
+ primary: { fallbackProfile: backupKey },
316
+ },
317
+ });
318
+ expect(result.success).toBe(false);
319
+ if (!result.success) {
320
+ const issue = result.error.issues.find(
321
+ (i) => i.path.join(".") === "profiles.primary.fallbackProfile",
322
+ );
323
+ expect(issue?.message).toContain("Automatic fallbacks are code-owned");
324
+ }
325
+ }
326
+ });
327
+
328
+ test("a managed stub keeps its references valid on the managed column", () => {
329
+ // The stub stands for a body that does resolve here, so nothing about it
330
+ // makes the reference worse than the unmaterialized case.
331
+ const result = LLMSchema.safeParse({
332
+ defaultProvider: managed,
333
+ profiles: {
334
+ [backupKey]: stub,
335
+ },
336
+ activeProfile: backupKey,
337
+ callSites: { recall: { profile: backupKey } },
338
+ });
339
+ expect(result.success).toBe(true);
340
+ });
341
+
342
+ test("a user-owned entry under a backup name cannot enable custom fallback", () => {
343
+ for (const defaultProvider of [managed, ...nonManaged]) {
344
+ const result = LLMSchema.safeParse({
345
+ defaultProvider,
346
+ profiles: {
347
+ [backupKey]: {
348
+ source: "user",
349
+ provider: "anthropic",
350
+ model: "claude-opus-4-7",
351
+ },
352
+ primary: { fallbackProfile: backupKey },
353
+ },
354
+ activeProfile: backupKey,
355
+ });
356
+ expect(result.success).toBe(false);
357
+ }
358
+ });
359
+ });