@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
@@ -48,6 +48,7 @@ import {
48
48
  import { AssistantConfigSchema } from "../../config/schema.js";
49
49
  import { getSchemaAtPath } from "../../config/schema-utils.js";
50
50
  import {
51
+ collectFallbackProfileIssues,
51
52
  DefaultProviderSchema,
52
53
  LLMConfigBase,
53
54
  LLMConfigFragment,
@@ -156,6 +157,7 @@ type LlmContextRouteResult = Omit<LlmContextNormalizationResult, "summary"> & {
156
157
  import {
157
158
  CODE_OWNED_PROFILE_NAMES,
158
159
  getEffectiveProfilesForProvider,
160
+ getUserSelectableProfilesForProvider,
159
161
  INVARIANT_PROFILE_NAMES,
160
162
  MANAGED_PROFILE_NAMES,
161
163
  resolveDefaultProfileForProvider,
@@ -241,6 +243,13 @@ function replaceInferenceProfileConfig(
241
243
  for (const key of INFERENCE_PROFILE_UI_KEYS) {
242
244
  delete nextProfile[key];
243
245
  }
246
+ // Converting a profile to a mix is an explicit replacement: a mix carries
247
+ // no model route of its own to fall back from, so an existing
248
+ // `fallbackProfile` pointer is dropped rather than preserved alongside
249
+ // `mix` (a combination `LLMSchema` rejects on the next full reload).
250
+ if (fragment.mix != null) {
251
+ delete nextProfile.fallbackProfile;
252
+ }
244
253
  const fragmentTopLevel = { ...fragment };
245
254
  delete fragmentTopLevel.contextWindow;
246
255
  profiles[name] = { ...nextProfile, ...fragmentTopLevel };
@@ -891,7 +900,7 @@ function overlayEffectiveProfilesForWire(config: unknown): void {
891
900
  if (!existingLlm) {
892
901
  root.llm = llm;
893
902
  }
894
- llm.profiles = getEffectiveProfilesForProvider(
903
+ llm.profiles = getUserSelectableProfilesForProvider(
895
904
  readPlainObject(llm.profiles) as Record<string, ProfileEntry> | undefined,
896
905
  getConfig().llm.defaultProvider ?? null,
897
906
  );
@@ -1399,6 +1408,28 @@ function assertRoutableIdentityEntries(
1399
1408
  }
1400
1409
  }
1401
1410
 
1411
+ /**
1412
+ * Reject writes that would persist unsupported `fallbackProfile` metadata.
1413
+ * Automatic fallbacks are code-owned for managed default profiles. The write
1414
+ * paths save raw config without a full-schema parse, so this check keeps a
1415
+ * custom pointer from reaching disk. It runs unconditionally so an existing
1416
+ * hand-edited pointer blocks unrelated rewrites until the user clears it with
1417
+ * `null`.
1418
+ */
1419
+ function assertCodeOwnedFallbackProfiles(raw: Record<string, unknown>): void {
1420
+ const llm = readPlainObject(raw.llm);
1421
+ const profiles = readPlainObject(llm?.profiles);
1422
+ // The sibling `llm.defaultProvider` decides whether the managed backups are
1423
+ // valid fallback targets: they resolve on the managed column alone.
1424
+ const issues = collectFallbackProfileIssues(
1425
+ profiles ?? undefined,
1426
+ llm?.defaultProvider,
1427
+ );
1428
+ if (issues.length > 0) {
1429
+ throw new BadRequestError(issues.map((issue) => issue.message).join(" "));
1430
+ }
1431
+ }
1432
+
1402
1433
  export async function commitConfigWrite(
1403
1434
  raw: Record<string, unknown>,
1404
1435
  opLabel: string,
@@ -1411,6 +1442,7 @@ export async function commitConfigWrite(
1411
1442
  completeChangedCustomProfiles(preWrite, raw);
1412
1443
  assertInvariantProfilesPreserved(preWrite, raw);
1413
1444
  assertRoutableIdentityEntries(preWrite, raw);
1445
+ assertCodeOwnedFallbackProfiles(raw);
1414
1446
 
1415
1447
  // Suppress the file-watcher callback for the duration of the debounce
1416
1448
  // window. Without this, the ConfigWatcher detects the config.json write
@@ -1714,6 +1746,18 @@ async function handleReplaceInferenceProfile({
1714
1746
  const isManaged =
1715
1747
  MANAGED_PROFILE_NAMES.has(name) &&
1716
1748
  (existingProfile == null || existingProfile.source === "managed");
1749
+ if (CODE_OWNED_PROFILE_NAMES.has(name)) {
1750
+ // A code-owned profile resolves from the catalog whatever the workspace
1751
+ // holds, so even a status re-enable would persist a stub that never
1752
+ // governs anything. Reject the write rather than accept a silent no-op.
1753
+ // Checked ahead of the availability gate below so the code-owned backups,
1754
+ // which are always available yet are not `DEFAULT_PROFILE_KEYS` members,
1755
+ // report why the write is refused rather than claiming they do not exist.
1756
+ throw new BadRequestError(
1757
+ `Profile "${name}" is code-owned and cannot be edited. ` +
1758
+ `Duplicate it to a custom profile to customize.`,
1759
+ );
1760
+ }
1717
1761
  // A flag-gated managed name (`os-beta`) with no materialized entry cannot
1718
1762
  // be patched: it only resolves while the flag reconcile has created its
1719
1763
  // stub, so writing status here would persist an entry that fights the
@@ -1728,15 +1772,6 @@ async function handleReplaceInferenceProfile({
1728
1772
  `Profile "${name}" is not currently available and cannot be edited.`,
1729
1773
  );
1730
1774
  }
1731
- if (CODE_OWNED_PROFILE_NAMES.has(name)) {
1732
- // A code-owned profile resolves from the catalog whatever the workspace
1733
- // holds, so even a status re-enable would persist a stub that never
1734
- // governs anything. Reject the write rather than accept a silent no-op.
1735
- throw new BadRequestError(
1736
- `Profile "${name}" is code-owned and cannot be edited. ` +
1737
- `Duplicate it to a custom profile to customize.`,
1738
- );
1739
- }
1740
1775
  if (isManaged) {
1741
1776
  // Managed profiles are daemon-seeded and read-only — the commit guard
1742
1777
  // (`assertInvariantProfilesPreserved`) rejects every write to them
@@ -1808,6 +1843,59 @@ async function handleReplaceInferenceProfile({
1808
1843
  );
1809
1844
  }
1810
1845
  });
1846
+ // A fallback target must stay a standard profile: converting one to a
1847
+ // mix would leave another profile's fallbackProfile pointing at a mix,
1848
+ // which `LLMSchema.superRefine` rejects on the next full reparse.
1849
+ for (const [otherName, other] of Object.entries(existingProfiles)) {
1850
+ if (otherName !== name && other?.fallbackProfile === name) {
1851
+ throw new BadRequestError(
1852
+ `Profile "${otherName}" declares profile "${name}" as its fallbackProfile; a fallback must be a standard profile, so "${name}" cannot become a mix.`,
1853
+ );
1854
+ }
1855
+ }
1856
+ }
1857
+
1858
+ // `fallbackProfile` references another profile by name. As with `mix`, the
1859
+ // cross-profile integrity rules `LLMSchema.superRefine` enforces on
1860
+ // full-config load (target exists, no self-reference, target is not a mix,
1861
+ // single hop) must be checked here against the live profile set; otherwise
1862
+ // a dangling or chained pointer would persist and break the next full
1863
+ // config reparse.
1864
+ if (parsed.data.fallbackProfile != null) {
1865
+ const fallback = parsed.data.fallbackProfile;
1866
+ if (fallback === name) {
1867
+ throw new BadRequestError(
1868
+ `Profile "${name}" cannot declare itself as its fallbackProfile.`,
1869
+ );
1870
+ }
1871
+ const { llm: parsedLlm } = getConfig();
1872
+ const existingProfiles = getEffectiveProfilesForProvider(
1873
+ parsedLlm.profiles,
1874
+ parsedLlm.defaultProvider ?? null,
1875
+ );
1876
+ const target = existingProfiles[fallback];
1877
+ if (target == null) {
1878
+ throw new BadRequestError(
1879
+ `Profile "${name}" declares fallbackProfile "${fallback}" which is not defined.`,
1880
+ );
1881
+ }
1882
+ if (target.mix != null) {
1883
+ throw new BadRequestError(
1884
+ `Profile "${name}" declares fallbackProfile "${fallback}" which is a mix profile; a fallback must be a standard profile.`,
1885
+ );
1886
+ }
1887
+ if (target.fallbackProfile != null) {
1888
+ throw new BadRequestError(
1889
+ `Profile "${name}" declares fallbackProfile "${fallback}" which sets its own fallbackProfile; fallback is a single hop, chains are not allowed.`,
1890
+ );
1891
+ }
1892
+ for (const [otherName, other] of Object.entries(existingProfiles)) {
1893
+ if (otherName !== name && other?.fallbackProfile === name) {
1894
+ throw new BadRequestError(
1895
+ `Profile "${otherName}" already declares profile "${name}" as its fallbackProfile; fallback is a single hop, chains are not allowed.`,
1896
+ );
1897
+ }
1898
+ }
1811
1899
  }
1812
1900
 
1813
1901
  // When the UI sends provider but no provider_connection, derive the connection
@@ -39,7 +39,7 @@ import {
39
39
  supportsHostProxy,
40
40
  } from "../../channels/types.js";
41
41
  import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
42
- import { getEffectiveProfilesForProvider } from "../../config/default-profile-catalog.js";
42
+ import { getUserSelectableProfilesForProvider } from "../../config/default-profile-catalog.js";
43
43
  import { isHttpAuthDisabled } from "../../config/env.js";
44
44
  import { getConfig } from "../../config/loader.js";
45
45
  import {
@@ -1559,7 +1559,7 @@ export async function handleSendMessage(
1559
1559
  }
1560
1560
  if (requestedInferenceProfile !== undefined) {
1561
1561
  const { llm } = getConfig();
1562
- const profiles = getEffectiveProfilesForProvider(
1562
+ const profiles = getUserSelectableProfilesForProvider(
1563
1563
  llm.profiles,
1564
1564
  llm.defaultProvider ?? null,
1565
1565
  );
@@ -72,7 +72,8 @@ function scrubSecret(secret: string | undefined): string {
72
72
  if (secret.length <= 4) {
73
73
  return "****";
74
74
  }
75
- return "****" + secret.slice(-4);
75
+ // Keep the leading 4 characters so vendor prefixes stay visible.
76
+ return secret.slice(0, 4) + "****";
76
77
  }
77
78
 
78
79
  function safeGetConnectionByProvider(
@@ -45,7 +45,8 @@ export interface ApprovalInterceptionParams {
45
45
  assistantId: string;
46
46
  approvalCopyGenerator?: ApprovalCopyGenerator;
47
47
  approvalConversationGenerator?: ApprovalConversationGenerator;
48
- /** Original approval message timestamp (Slack ts) for editing after resolution. */
48
+ /** Id of the channel message holding the approval buttons (Slack ts,
49
+ * Telegram message id), for editing the card after resolution. */
49
50
  approvalMessageId?: string;
50
51
  }
51
52
 
@@ -242,7 +243,7 @@ export async function handleApprovalInterception(
242
243
  "Callback request ID does not match any pending interaction, ignoring stale button press",
243
244
  );
244
245
 
245
- // Edit the original Slack approval message to remove stale buttons
246
+ // Edit the original approval message to remove stale buttons
246
247
  if (approvalMessageId) {
247
248
  editStaleApprovalMessage({
248
249
  replyCallbackUrl,
@@ -260,8 +261,10 @@ export async function handleApprovalInterception(
260
261
  const result = await handleChannelDecision(conversationId, cbDecision);
261
262
 
262
263
  if (result.applied) {
263
- // Edit the original Slack approval message to show the decision
264
- // and remove stale action buttons.
264
+ // Edit the original approval message to show the decision and drop
265
+ // its action buttons. Routed by the callback URL through the channel
266
+ // edit capability; a transport without edit declines, so this is
267
+ // safe to attempt on every channel.
265
268
  if (approvalMessageId) {
266
269
  const decisionOutcome: "approved" | "denied" =
267
270
  cbDecision.action === "reject" ? "denied" : "approved";
@@ -276,7 +279,7 @@ export async function handleApprovalInterception(
276
279
  }).catch((err) => {
277
280
  log.error(
278
281
  { err, conversationId, messageTs: approvalMessageId },
279
- "Failed to edit Slack approval message after decision",
282
+ "Failed to edit approval message after decision",
280
283
  );
281
284
  });
282
285
  }
@@ -287,8 +290,8 @@ export async function handleApprovalInterception(
287
290
  }
288
291
 
289
292
  // Race condition: request was already resolved between the stale check
290
- // above and the decision attempt.
291
- // Edit the original Slack approval message to remove stale buttons
293
+ // above and the decision attempt. Edit the original approval message to
294
+ // remove stale buttons
292
295
  if (approvalMessageId) {
293
296
  editStaleApprovalMessage({
294
297
  replyCallbackUrl,
@@ -368,7 +371,7 @@ export async function handleApprovalInterception(
368
371
  }
369
372
 
370
373
  // ---------------------------------------------------------------------------
371
- // Slack approval message edit helper
374
+ // Approval message edit helper
372
375
  // ---------------------------------------------------------------------------
373
376
 
374
377
  /**
@@ -399,7 +402,7 @@ function editStaleApprovalMessage(params: {
399
402
  conversationId: params.conversationId,
400
403
  messageId: params.messageId,
401
404
  },
402
- "Failed to edit stale Slack approval message",
405
+ "Failed to edit stale approval message",
403
406
  );
404
407
  });
405
408
  }
@@ -6,6 +6,7 @@
6
6
  * messages reach this handler.
7
7
  */
8
8
  import type { SourceMetadata } from "@vellumai/gateway-client";
9
+ import { resolveInboundEventKind } from "@vellumai/gateway-client";
9
10
  import {
10
11
  ADMISSION_POLICY_DEFAULT,
11
12
  type AdmissionPolicy,
@@ -63,6 +64,7 @@ import {
63
64
  type SlackMessageMetadata,
64
65
  writeSlackMetadata,
65
66
  } from "../../messaging/providers/slack/message-metadata.js";
67
+ import { mergeProviderMessageMetadata } from "../../messaging/read-provider-metadata.js";
66
68
  import { MESSAGE_PREVIEW_MAX_LENGTH } from "../../notifications/notification-utils.js";
67
69
  import {
68
70
  attachInlineAttachmentToMessage,
@@ -93,6 +95,7 @@ import { upsertBinding } from "../../persistence/external-conversation-store.js"
93
95
  import type { ContentBlock } from "../../providers/types.js";
94
96
  import { checkIngressForSecrets } from "../../security/secret-ingress.js";
95
97
  import { canonicalizeInboundIdentity } from "../../util/canonicalize-identity.js";
98
+ import { safeParseRecord } from "../../util/json.js";
96
99
  import { getLogger } from "../../util/logger.js";
97
100
  import { truncate } from "../../util/truncate.js";
98
101
  import {
@@ -281,6 +284,7 @@ export async function handleChannelInbound({
281
284
  conversationExternalId?: string;
282
285
  externalMessageId?: string;
283
286
  content?: string;
287
+ eventKind?: string;
284
288
  isEdit?: boolean;
285
289
  actorDisplayName?: string;
286
290
  attachmentIds?: string[];
@@ -297,11 +301,16 @@ export async function handleChannelInbound({
297
301
  conversationExternalId,
298
302
  externalMessageId,
299
303
  content,
300
- isEdit,
301
304
  attachmentIds,
302
305
  sourceMetadata,
303
306
  } = body;
304
307
 
308
+ // The named event family. Stamped by every gateway producer; replayed
309
+ // retry payloads arrive unstamped and classify by their flag and sentinel
310
+ // fields instead.
311
+ const eventKind = resolveInboundEventKind(body);
312
+ const isEdit = eventKind === "edit";
313
+
305
314
  if (!body.sourceChannel || typeof body.sourceChannel !== "string") {
306
315
  throw new BadRequestError("sourceChannel is required");
307
316
  }
@@ -500,7 +509,10 @@ export async function handleChannelInbound({
500
509
  // untouched for audit purposes — rendering elides based on the deletedAt
501
510
  // marker. Gated behind ingress ACL so non-members cannot drive deletes
502
511
  // (matches the edit-intercept policy).
503
- if (sourceChannel === "slack" && body.callbackData === "message_deleted") {
512
+ // The Slack gate mirrors the reaction intercept's: deletes are ingested
513
+ // from Slack alone today, and each further channel arrives as its own
514
+ // decision with its own wire shape.
515
+ if (sourceChannel === "slack" && eventKind === "delete") {
504
516
  const deletedMessageTs =
505
517
  typeof sourceMetadata?.messageId === "string"
506
518
  ? sourceMetadata.messageId
@@ -557,43 +569,15 @@ export async function handleChannelInbound({
557
569
  return { accepted: true, deleted: false };
558
570
  }
559
571
 
560
- // Merge deletedAt into the existing slackMeta sub-key. If the row has
561
- // no slackMeta (legacy pre-upgrade row), skip the renderer's flat
562
- // fallback ignores deletedAt for those rows anyway, and synthesizing
563
- // a partial slackMeta here would produce metadata that fails
564
- // readSlackMetadata validation.
572
+ // Merge deletedAt into the existing slackMeta sub-key when the row has
573
+ // one, so the Slack transcript renderer keeps seeing its own envelope.
574
+ // A row without it (a legacy pre-enrichment row) stamps the neutral
575
+ // shape instead: readProviderMetadata serves either to every
576
+ // channel-agnostic reader, so no delete goes unmarked over a metadata
577
+ // accident.
565
578
  const row = getMessageById(original.messageId);
566
- if (!row?.metadata) {
567
- log.debug(
568
- {
569
- conversationExternalId,
570
- deletedMessageTs,
571
- messageId: original.messageId,
572
- },
573
- "Stored Slack message has no metadata; skipping delete marker",
574
- );
575
- return { accepted: true, deleted: false };
576
- }
577
-
578
- let parentMetadata: Record<string, unknown>;
579
- try {
580
- const parsed = JSON.parse(row.metadata) as unknown;
581
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
582
- parentMetadata = parsed as Record<string, unknown>;
583
- } else {
584
- parentMetadata = {};
585
- }
586
- } catch {
587
- log.debug(
588
- {
589
- conversationExternalId,
590
- deletedMessageTs,
591
- messageId: original.messageId,
592
- },
593
- "Failed to parse stored metadata; skipping delete marker",
594
- );
595
- return { accepted: true, deleted: false };
596
- }
579
+ const parentMetadata: Record<string, unknown> =
580
+ row?.metadata != null ? safeParseRecord(row.metadata) : {};
597
581
 
598
582
  const existingSlackMeta =
599
583
  typeof parentMetadata.slackMeta === "string"
@@ -601,15 +585,28 @@ export async function handleChannelInbound({
601
585
  : null;
602
586
 
603
587
  if (!existingSlackMeta) {
604
- log.debug(
588
+ const providerMeta = mergeProviderMessageMetadata(
589
+ row?.metadata ?? null,
590
+ {
591
+ source: sourceChannel,
592
+ conversationExternalId,
593
+ messageId: deletedMessageTs,
594
+ ...(sourceMetadata?.threadId
595
+ ? { threadId: sourceMetadata.threadId }
596
+ : {}),
597
+ },
598
+ { deletedAt: Date.now() },
599
+ );
600
+ updateMessageMetadata(original.messageId, { providerMeta });
601
+ log.info(
605
602
  {
606
603
  conversationExternalId,
607
604
  deletedMessageTs,
608
605
  messageId: original.messageId,
609
606
  },
610
- "Stored Slack message has no slackMeta; skipping delete marker",
607
+ "Marked message deleted via neutral metadata",
611
608
  );
612
- return { accepted: true, deleted: false };
609
+ return { accepted: true, deleted: true, messageId: original.messageId };
613
610
  }
614
611
 
615
612
  const updatedSlackMeta = mergeSlackMetadata(existingSlackMeta, {
@@ -764,9 +761,10 @@ export async function handleChannelInbound({
764
761
  // re-verification challenge — §8.2). The gateway kill switch already
765
762
  // dropped `no_one` upstream, but the stage handles it defensively.
766
763
  //
767
- // Internal channels (`vellum`, `platform`, `a2a`) short-circuit admit
768
- // inside `enforceAdmissionPolicy` defense in depth alongside the
769
- // gateway's exempt-channel skip and the PUT-handler's 403.
764
+ // Exempt channels (`platform`, `a2a`) short-circuit admit inside
765
+ // `enforceAdmissionPolicy`: defense in depth alongside the gateway's
766
+ // exempt-channel skip and the PUT-handler's 403. `vellum` is hidden, not
767
+ // exempt: its floor is evaluated like any enforced channel's.
770
768
  //
771
769
  // Bootstrap deep-link: when ACL resolved a validated pending_bootstrap
772
770
  // session, skip the floor entirely. The bootstrap intercept stage below
@@ -1091,10 +1089,11 @@ export async function handleChannelInbound({
1091
1089
  !result.duplicate &&
1092
1090
  !guardianReplyResult.skipApprovalInterception
1093
1091
  ) {
1094
- // Extract the original approval message timestamp for Slack button
1095
- // cleanup. When a Slack block_actions payload is forwarded, the gateway
1096
- // sets sourceMetadata.messageId to the ts of the message containing
1097
- // the button. This lets us edit the message after resolution.
1092
+ // The id of the message holding the approval buttons, for editing the
1093
+ // card after resolution. The gateway sets sourceMetadata.messageId on
1094
+ // every button-press forward: Slack's block_actions carry the ts of the
1095
+ // message containing the button, Telegram's callback_query the id of the
1096
+ // message the keyboard is attached to.
1098
1097
  const approvalMessageId =
1099
1098
  typeof sourceMetadata?.messageId === "string"
1100
1099
  ? sourceMetadata.messageId
@@ -6,10 +6,11 @@
6
6
  * Invite code/token redemption is intercepted at gateway ingress; redeemed
7
7
  * messages never reach this stage.
8
8
  */
9
- import type {
10
- AdmissionPolicy,
11
- SourceMetadata,
12
- TrustVerdict,
9
+ import {
10
+ ACCESS_DENIED_NOT_APPROVED_REPLY,
11
+ type AdmissionPolicy,
12
+ type SourceMetadata,
13
+ type TrustVerdict,
13
14
  } from "@vellumai/gateway-client";
14
15
 
15
16
  import type { VerificationSessionWire } from "../../../channels/gateway-verification-sessions.js";
@@ -78,7 +79,7 @@ export function composeAccessDenialReply(params: {
78
79
  if (params.guardianNotified) {
79
80
  return `Hmm looks like you don't have access to talk to me. I'll let ${resolveGuardianLabel(params.verdict)} know you tried talking to me and get back to you.`;
80
81
  }
81
- return "Sorry, you haven't been approved to message this assistant.";
82
+ return ACCESS_DENIED_NOT_APPROVED_REPLY;
82
83
  }
83
84
 
84
85
  // ---------------------------------------------------------------------------
@@ -98,15 +99,20 @@ export interface AclEnforcementParams {
98
99
  replyCallbackUrl: string | undefined;
99
100
  assistantId: string;
100
101
  /**
101
- * Effective admission policy for this request (gateway floor resolved with
102
- * any per-conversation override). When set, ACL skips its hard-deny paths
103
- * when the policy is permissive enough:
102
+ * Effective admission policy for this request (the gateway's per-channel
103
+ * floor). When set, ACL skips its hard-deny paths when the policy makes
104
+ * the floor stage the right place to answer:
104
105
  * - `strangers`: non-members and inactive (non-blocked) members are passed
105
106
  * through so the admission floor can emit the final verdict.
106
- * - `any_contact`: inactive `pending` members are passed through.
107
+ * - `guardian_only`: non-members and inactive `pending`/`unverified`
108
+ * members are passed through; the floor denies everyone below guardian.
109
+ * - `any_contact`: inactive `pending`/`unverified` members are passed
110
+ * through.
107
111
  *
108
- * Passing this in avoids having ACL fire guardian notifications and canned
109
- * replies for senders who will be admitted by the floor stage anyway.
112
+ * Passing this in keeps ACL from firing guardian notifications and canned
113
+ * replies for senders whose final answer belongs to the floor stage:
114
+ * admission under the permissive floors, or a `guardian_only` denial
115
+ * delivered without a misleading verification challenge.
110
116
  */
111
117
  effectiveAdmissionPolicy?: AdmissionPolicy;
112
118
  /**
@@ -880,8 +886,7 @@ export async function enforceIngressAcl(
880
886
  { sourceChannel, channelId: resolvedMember.channelId },
881
887
  "Ingress ACL: member policy deny",
882
888
  );
883
- const denyReplyText =
884
- "Sorry, you haven't been approved to message this assistant.";
889
+ const denyReplyText = ACCESS_DENIED_NOT_APPROVED_REPLY;
885
890
  let denyReplyDelivered = false;
886
891
  if (replyCallbackUrl) {
887
892
  const denyPayload: Parameters<typeof deliverChannelReply>[1] = {
@@ -7,7 +7,7 @@
7
7
  * floor (`sourceMetadata.admissionPolicy`); this stage compares the floor
8
8
  * to the resolved trust class's rank and either admits or denies.
9
9
  *
10
- * Deny semantics see `wave-b-plan.md` §8.2:
10
+ * Deny semantics (the locked §8.2 decisions):
11
11
  *
12
12
  * - `shouldChallenge: true` when the policy is one that re-verification
13
13
  * could lift past (`any_contact`, `strangers`). The caller fires the
@@ -66,7 +66,7 @@ export type AdmissionPolicyResult =
66
66
  * resolved trust class could clear the floor after verification.
67
67
  */
68
68
  shouldChallenge: boolean;
69
- /** Effective policy that produced the deny (after override resolution). */
69
+ /** Policy that produced the deny. */
70
70
  effectivePolicy: AdmissionPolicy;
71
71
  };
72
72
 
@@ -20,10 +20,10 @@ import {
20
20
  mergeSlackMetadata,
21
21
  readSlackMetadata,
22
22
  } from "../../../messaging/providers/slack/message-metadata.js";
23
+ import { mergeProviderMessageMetadata } from "../../../messaging/read-provider-metadata.js";
23
24
  import type { MessageRow } from "../../../persistence/conversation-crud.js";
24
25
  import {
25
26
  getMessageById,
26
- updateMessageContent,
27
27
  updateMessageContentAndMetadata,
28
28
  } from "../../../persistence/conversation-crud.js";
29
29
  import {
@@ -31,6 +31,7 @@ import {
31
31
  findMessageBySourceId,
32
32
  recordInbound,
33
33
  } from "../../../persistence/delivery-crud.js";
34
+ import { markProcessed } from "../../../persistence/delivery-status.js";
34
35
  import { enqueueLexicalIndexForMessage } from "../../../persistence/job-handlers/message-lexical.js";
35
36
  import { stringifyMessageContent } from "../../../persistence/message-content.js";
36
37
  import { safeParseRecord } from "../../../util/json.js";
@@ -182,6 +183,7 @@ export async function handleEditIntercept(
182
183
  },
183
184
  "Edit text unchanged; skipping update",
184
185
  );
186
+ markProcessed(editResult.eventId);
185
187
  return {
186
188
  accepted: true,
187
189
  duplicate: false,
@@ -203,13 +205,29 @@ export async function handleEditIntercept(
203
205
  newContent,
204
206
  });
205
207
  } else {
206
- updateMessageContent(original.messageId, newContent);
208
+ // Every channel marks its edits. Slack's envelope carries the extra
209
+ // fields its own renderer needs; the rest stamp the neutral shape that
210
+ // readProviderMetadata serves to every channel-agnostic reader.
211
+ const providerMeta = mergeProviderMessageMetadata(
212
+ existingRow?.metadata ?? null,
213
+ {
214
+ source: sourceChannel,
215
+ conversationExternalId,
216
+ messageId: sourceMessageId,
217
+ ...(sourceThreadId ? { threadId: sourceThreadId } : {}),
218
+ },
219
+ { editedAt: Date.now() },
220
+ );
221
+ updateMessageContentAndMetadata(original.messageId, newContent, {
222
+ providerMeta,
223
+ });
207
224
  }
208
225
  // The edit changed searchable text (the no-op guard above already returned
209
226
  // for identical content) and this path bypasses the `addMessage` persist
210
227
  // path, so reindex the message into the lexical index — the idempotent
211
228
  // upsert replaces the stale Qdrant point with the edited content.
212
229
  enqueueLexicalIndexForMessage(original.messageId);
230
+ markProcessed(editResult.eventId);
213
231
  log.info(
214
232
  { assistantId, sourceMessageId, messageId: original.messageId },
215
233
  "Updated message content from edited_message",
@@ -20,6 +20,7 @@
20
20
  * `unknown` (drop). Reactions never drive an agent turn.
21
21
  */
22
22
  import type { SourceMetadata } from "@vellumai/gateway-client";
23
+ import { resolveInboundEventKind } from "@vellumai/gateway-client";
23
24
 
24
25
  import type { ChannelId, InterfaceId } from "../../../channels/types.js";
25
26
  import { getDiskPressureStatus } from "../../../daemon/disk-pressure-guard.js";
@@ -58,16 +59,16 @@ const log = getLogger("runtime-http");
58
59
  */
59
60
  export function isSlackReactionEvent(body: {
60
61
  sourceChannel?: string;
62
+ eventKind?: string;
61
63
  callbackData?: string;
62
64
  }): boolean {
63
- if (body.sourceChannel !== "slack") {
64
- return false;
65
- }
66
- const cb = body.callbackData;
67
- if (typeof cb !== "string") {
68
- return false;
69
- }
70
- return cb.startsWith("reaction:") || cb.startsWith("reaction_removed:");
65
+ // The Slack gate is deliberate, not residue: Slack is the only channel
66
+ // whose reactions are ingested today, and widening this is a per-channel
67
+ // decision made with that channel's reaction wire shape, not a fallthrough.
68
+ return (
69
+ body.sourceChannel === "slack" &&
70
+ resolveInboundEventKind(body) === "reaction"
71
+ );
71
72
  }
72
73
 
73
74
  /**
@@ -53,7 +53,7 @@ export async function tryTranscribeAudioAttachments(
53
53
  }
54
54
 
55
55
  // Resolve STT provider via daemon batch transcriber facade
56
- const transcriber = await resolveBatchTranscriber();
56
+ const transcriber = await resolveBatchTranscriber({ role: "batch" });
57
57
  if (!transcriber) {
58
58
  return {
59
59
  status: "no_provider",
@@ -93,6 +93,7 @@ import { ROUTES as INFERENCE_PROVIDER_CONNECTION_ROUTES } from "./inference-prov
93
93
  import { ROUTES as INFERENCE_SEND_ROUTES } from "./inference-send-routes.js";
94
94
  import { ROUTES as INGRESS_STATUS_ROUTES } from "./ingress-status-routes.js";
95
95
  import { ROUTES as A2A_ROUTES } from "./integrations/a2a.js";
96
+ import { ROUTES as DISCORD_CONFIG_ROUTES } from "./integrations/discord.js";
96
97
  import { ROUTES as SLACK_CHANNEL_CONFIG_ROUTES } from "./integrations/slack/channel.js";
97
98
  import { ROUTES as SLACK_CHANNELS_ROUTES } from "./integrations/slack/channels.js";
98
99
  import { ROUTES as SLACK_SHARE_ROUTES } from "./integrations/slack/share.js";
@@ -287,6 +288,7 @@ export const ROUTES: RouteDefinition[] = [
287
288
  ...SOUNDS_CONFIG_ROUTES,
288
289
  ...SKILL_ROUTES,
289
290
  ...A2A_ROUTES,
291
+ ...DISCORD_CONFIG_ROUTES,
290
292
  ...SLACK_CHANNEL_CONFIG_ROUTES,
291
293
  ...SLACK_CHANNEL_RESOLVE_ROUTES,
292
294
  ...SLACK_CHANNELS_ROUTES,
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { randomUUID } from "node:crypto";
16
16
 
17
- import { getEffectiveProfilesForProvider } from "../../config/default-profile-catalog.js";
17
+ import { getUserSelectableProfilesForProvider } from "../../config/default-profile-catalog.js";
18
18
  import { loadConfig } from "../../config/loader.js";
19
19
  import { findConversation } from "../../daemon/conversation-registry.js";
20
20
  import {
@@ -153,7 +153,7 @@ export async function setInferenceProfileSession({
153
153
 
154
154
  // --- Validate profile ---
155
155
  const { llm } = loadConfig();
156
- const profiles = getEffectiveProfilesForProvider(
156
+ const profiles = getUserSelectableProfilesForProvider(
157
157
  llm?.profiles,
158
158
  llm?.defaultProvider ?? null,
159
159
  );