@vellumai/assistant 0.11.6 → 0.11.7-staging.1

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 (447) 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/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Managed Flux STT through the gateway's speech relay
3
+ * (gateway -> velay -> Deepgram Flux; velay contact is gateway-only).
4
+ *
5
+ * Composes {@link DeepgramFluxRealtimeTranscriber} against the relay's
6
+ * managed STT v2 endpoint with `contract=flux`, so Flux's own turn events
7
+ * arrive verbatim. Without that opt-in velay translates them into the
8
+ * released v1 Deepgram dialect, which drops `EagerEndOfTurn` and
9
+ * `TurnResumed` entirely and leaves live voice with no provider turn end to
10
+ * act on.
11
+ *
12
+ * This wrapper owns what is relay-specific, mirroring
13
+ * `VellumManagedRealtimeTranscriber` on the v1 path:
14
+ *
15
+ * - Dialing the gateway (`/v2/speech/stt/stream`, `?key=` carries a
16
+ * daemon-minted service token, no `model` param since velay derives and
17
+ * prices the Flux model from `language`).
18
+ * - Surfacing `velay_error` control frames as categorized SttErrors instead
19
+ * of generic socket failures.
20
+ * - Transparent re-dial on velay's 30-minute session cap, so one live-voice
21
+ * session can outlive it.
22
+ *
23
+ * There is deliberately no `finalizeUtterance`: Flux commits turns itself,
24
+ * and the interface makes the method optional so live voice feature-detects
25
+ * its absence and never asks for a flush.
26
+ */
27
+
28
+ import {
29
+ type StreamingTranscriber,
30
+ SttError,
31
+ type SttStreamServerEvent,
32
+ } from "../../stt/types.js";
33
+ import { getLogger } from "../../util/logger.js";
34
+ import { DeepgramFluxRealtimeTranscriber } from "./deepgram-flux-realtime.js";
35
+ import {
36
+ mapVelayError,
37
+ probeVelayRejection,
38
+ type SpeechRelayConnection,
39
+ type VelayErrorInfo,
40
+ } from "./vellum-speech-relay-connection.js";
41
+
42
+ const log = getLogger("vellum-managed-flux-realtime");
43
+
44
+ /** Managed STT v2: velay resolves this to Deepgram's Flux `/v2/listen`. */
45
+ const STT_STREAM_PATH = "/v2/speech/stt/stream";
46
+
47
+ export interface VellumManagedFluxRealtimeOptions {
48
+ /** Audio sample rate in Hz (default: 16000). */
49
+ sampleRate?: number;
50
+ /**
51
+ * BCP-47 language code (or `"multi"`), forwarded to the relay. This is
52
+ * how velay picks between `flux-general-en` and `flux-general-multi`, so
53
+ * it is the only lever over the Flux model from this side.
54
+ */
55
+ language?: string;
56
+ }
57
+
58
+ export class VellumManagedFluxRealtimeTranscriber implements StreamingTranscriber {
59
+ readonly providerId = "vellum-flux" as const;
60
+ readonly boundaryId = "daemon-streaming" as const;
61
+
62
+ private readonly connection: SpeechRelayConnection;
63
+ private readonly options: VellumManagedFluxRealtimeOptions;
64
+
65
+ private onEvent: ((event: SttStreamServerEvent) => void) | null = null;
66
+ /** The current relay session; null while re-dialing after a session cap. */
67
+ private inner: DeepgramFluxRealtimeTranscriber | null = null;
68
+ /**
69
+ * The most recent `velay_error` frame from the current session. Set just
70
+ * before velay closes the socket; consumed when the inner adapter turns
71
+ * that close into an error event.
72
+ */
73
+ private pendingRelayError: VelayErrorInfo | null = null;
74
+ /**
75
+ * Set when the current session hit velay's duration cap: the swap to a
76
+ * fresh session is deferred to the capped session's `closed` event so its
77
+ * close cleanup drains through first.
78
+ */
79
+ private redialOnClose = false;
80
+ private stopping = false;
81
+ private closedEmitted = false;
82
+
83
+ constructor(
84
+ connection: SpeechRelayConnection,
85
+ options: VellumManagedFluxRealtimeOptions = {},
86
+ ) {
87
+ this.connection = connection;
88
+ this.options = options;
89
+ }
90
+
91
+ async start(onEvent: (event: SttStreamServerEvent) => void): Promise<void> {
92
+ if (this.onEvent) {
93
+ throw new Error(
94
+ "VellumManagedFluxRealtimeTranscriber: start() called twice",
95
+ );
96
+ }
97
+ this.onEvent = onEvent;
98
+ try {
99
+ const inner = await this.dial();
100
+ if (this.stopping) {
101
+ // stop() raced the initial dial: it saw no inner to tear down and
102
+ // already emitted closed, so the freshly opened relay session must
103
+ // not be kept (a leaked session keeps velay metering).
104
+ inner.stop();
105
+ return;
106
+ }
107
+ this.inner = inner;
108
+ } catch (err) {
109
+ // A failed WebSocket upgrade exposes no HTTP details; replay the
110
+ // request as a plain GET to recover the relay's {code, detail}
111
+ // rejection.
112
+ const rejection = await probeVelayRejection(this.probeUrl());
113
+ if (rejection) {
114
+ const mapped = mapVelayError(rejection);
115
+ throw new SttError(mapped.category, mapped.message, {
116
+ userFacing: true,
117
+ });
118
+ }
119
+ throw err;
120
+ }
121
+ }
122
+
123
+ sendAudio(audio: Buffer, mimeType: string): void {
124
+ // Audio arriving during a session-cap re-dial (sub-second) is dropped,
125
+ // mirroring the inner adapter's backpressure-drop semantics.
126
+ this.inner?.sendAudio(audio, mimeType);
127
+ }
128
+
129
+ stop(): void {
130
+ if (this.stopping) {
131
+ return;
132
+ }
133
+ this.stopping = true;
134
+ if (this.inner) {
135
+ this.inner.stop();
136
+ return;
137
+ }
138
+ // Mid-redial stop: there is no session to drain.
139
+ this.emitClosedOnce();
140
+ }
141
+
142
+ // -- Relay session management ------------------------------------------
143
+
144
+ private async dial(): Promise<DeepgramFluxRealtimeTranscriber> {
145
+ this.pendingRelayError = null;
146
+ // Fresh per dial: a session-cap re-dial happens ~30 minutes in, long
147
+ // after any previously minted token expired.
148
+ const inner = new DeepgramFluxRealtimeTranscriber(
149
+ this.connection.mintServiceToken(),
150
+ {
151
+ baseUrl: this.connection.wsBaseUrl,
152
+ path: STT_STREAM_PATH,
153
+ queryAuth: true,
154
+ omitModelParam: true,
155
+ nativeContract: true,
156
+ ...(this.options.sampleRate !== undefined
157
+ ? { sampleRate: this.options.sampleRate }
158
+ : {}),
159
+ ...(this.options.language ? { language: this.options.language } : {}),
160
+ onUnhandledFrame: (frame) => this.handleRelayFrame(frame),
161
+ },
162
+ );
163
+ await inner.start((event) => this.handleInnerEvent(inner, event));
164
+ return inner;
165
+ }
166
+
167
+ private probeUrl(): string {
168
+ const key = encodeURIComponent(this.connection.mintServiceToken());
169
+ return `${this.connection.httpBaseUrl}${STT_STREAM_PATH}?key=${key}`;
170
+ }
171
+
172
+ private handleRelayFrame(frame: Record<string, unknown>): void {
173
+ if (frame.type !== "velay_error" || typeof frame.code !== "string") {
174
+ return;
175
+ }
176
+ this.pendingRelayError = {
177
+ code: frame.code,
178
+ ...(typeof frame.detail === "string" ? { detail: frame.detail } : {}),
179
+ };
180
+ log.info(
181
+ { code: frame.code, detail: frame.detail },
182
+ "Received velay_error frame on managed Flux relay",
183
+ );
184
+ }
185
+
186
+ private handleInnerEvent(
187
+ source: DeepgramFluxRealtimeTranscriber,
188
+ event: SttStreamServerEvent,
189
+ ): void {
190
+ // Events from a superseded session (the one replaced by a re-dial) must
191
+ // not leak through: its close/error was already accounted for.
192
+ if (source !== this.inner) {
193
+ return;
194
+ }
195
+
196
+ if (event.type === "error") {
197
+ const relayError = this.pendingRelayError;
198
+ if (relayError?.code === "session_duration_exceeded") {
199
+ // Velay's 30-minute cap, not a failure. Swallow the error but keep
200
+ // the capped session current so its close cleanup drains first; the
201
+ // re-dial happens on its `closed` event.
202
+ this.redialOnClose = !this.stopping;
203
+ return;
204
+ }
205
+ if (relayError) {
206
+ const mapped = mapVelayError(relayError);
207
+ this.emit({
208
+ type: "error",
209
+ category: mapped.category,
210
+ message: mapped.message,
211
+ });
212
+ return;
213
+ }
214
+ this.emit(event);
215
+ return;
216
+ }
217
+
218
+ if (event.type === "closed") {
219
+ if (this.redialOnClose && !this.stopping) {
220
+ this.redialOnClose = false;
221
+ this.beginRedial();
222
+ return;
223
+ }
224
+ this.emitClosedOnce();
225
+ return;
226
+ }
227
+
228
+ this.emit(event);
229
+ }
230
+
231
+ /**
232
+ * Replace the capped session with a fresh one. Detached: audio sent during
233
+ * the swap is dropped (see {@link sendAudio}); a dial failure is a real
234
+ * error and ends the stream.
235
+ */
236
+ private beginRedial(): void {
237
+ log.info("Managed Flux relay session hit velay's duration cap, re-dialing");
238
+ this.inner = null;
239
+ void (async () => {
240
+ try {
241
+ const inner = await this.dial();
242
+ if (this.stopping) {
243
+ inner.stop();
244
+ return;
245
+ }
246
+ this.inner = inner;
247
+ log.info("Managed Flux relay re-dial complete; session continues");
248
+ } catch (err) {
249
+ if (this.stopping) {
250
+ return;
251
+ }
252
+ const rejection = await probeVelayRejection(this.probeUrl());
253
+ if (rejection) {
254
+ const mapped = mapVelayError(rejection);
255
+ this.emit({
256
+ type: "error",
257
+ category: mapped.category,
258
+ message: mapped.message,
259
+ });
260
+ } else {
261
+ this.emit({
262
+ type: "error",
263
+ category: "provider-error",
264
+ message: `Managed Flux relay re-dial failed after the session cap: ${
265
+ err instanceof Error ? err.message : String(err)
266
+ }`,
267
+ });
268
+ }
269
+ this.emitClosedOnce();
270
+ }
271
+ })();
272
+ }
273
+
274
+ // -- Event emission -----------------------------------------------------
275
+
276
+ private emit(event: SttStreamServerEvent): void {
277
+ if (!this.onEvent || this.closedEmitted) {
278
+ return;
279
+ }
280
+ if (event.type === "closed") {
281
+ this.closedEmitted = true;
282
+ }
283
+ try {
284
+ this.onEvent(event);
285
+ } catch (err) {
286
+ log.warn(
287
+ { error: err },
288
+ "Listener error in vellum managed Flux realtime adapter",
289
+ );
290
+ }
291
+ }
292
+
293
+ private emitClosedOnce(): void {
294
+ this.emit({ type: "closed" });
295
+ }
296
+ }
@@ -1,6 +1,16 @@
1
1
  import type { ToolDefinition } from "../tools/tool-types.js";
2
2
  export type { ToolDefinition };
3
3
 
4
+ /**
5
+ * The tool name that activates provider-native web search. Callers append a
6
+ * tool under this name only to instances reporting
7
+ * {@link Provider.supportsNativeWebSearch}; anywhere a request can change
8
+ * routes after that decision (see `RetryProvider`'s backup-profile
9
+ * escalation), the tool has to be dropped again when the new route cannot
10
+ * serve it, or the model answers with a tool call nothing can execute.
11
+ */
12
+ export const NATIVE_WEB_SEARCH_TOOL_NAME = "web_search";
13
+
4
14
  import type { LLMCallSite } from "../config/schemas/llm.js";
5
15
  import {
6
16
  ProviderError,
@@ -191,6 +201,15 @@ export interface ProviderResponse {
191
201
  model: string;
192
202
  /** Provider that actually produced this response, which may differ from a wrapper provider name. */
193
203
  actualProvider?: string;
204
+ /**
205
+ * Inference profile key that actually governed this response when a
206
+ * wrapper re-routed the request away from the caller's own resolution
207
+ * (`RetryProvider`'s fallback-route escalation). `UsageTrackingProvider`
208
+ * prefers this over re-resolving from the original request options, so a
209
+ * successful fallback serve is attributed to the backup profile rather
210
+ * than the failed primary's. Absent on the normal (non-rerouted) path.
211
+ */
212
+ actualInferenceProfile?: string;
194
213
  /**
195
214
  * Base URL the provider's HTTP client actually resolved to for this request,
196
215
  * read from the live SDK client instance rather than re-derived from config.
@@ -281,6 +300,18 @@ export interface SendMessageConfig {
281
300
  * request.
282
301
  */
283
302
  forceOverrideProfile?: boolean;
303
+ /**
304
+ * True when the caller appended {@link NATIVE_WEB_SEARCH_TOOL_NAME} purely
305
+ * to activate the route's provider-native web search, rather than passing an
306
+ * app-executed search tool of the same name (which is what runs when a
307
+ * search backend like Brave or the platform search proxy is configured).
308
+ * Only the caller can tell those apart, and a route change after that
309
+ * decision has to: `RetryProvider` drops the tool on a backup that runs no
310
+ * native search, and must not touch it when the daemon executes it itself.
311
+ * A resolution/routing-time concern only; stripped before any provider wire
312
+ * request.
313
+ */
314
+ nativeWebSearchSentinel?: boolean;
284
315
  /**
285
316
  * Per-conversation seed for deterministic `mix`-profile expansion. The agent
286
317
  * loop sets this to the conversation id so every resolver call this send
@@ -316,6 +347,14 @@ export interface SendMessageConfig {
316
347
  * JSON request bodies.
317
348
  */
318
349
  usageAttributionHeaders?: Record<string, string>;
350
+ /**
351
+ * Per-request HTTP headers merged onto the transport. `RetryProvider`
352
+ * stamps these for providers that need support-lookup headers (OpenCode
353
+ * `x-opencode-session` / `x-opencode-request`). Provider clients pass
354
+ * them through SDK request options only and must never include this
355
+ * object in provider JSON request bodies.
356
+ */
357
+ requestHeaders?: Record<string, string>;
319
358
  /**
320
359
  * Controls local usage-ledger writes for attributed provider calls.
321
360
  * Defaults to `auto`; conversation paths that aggregate usage separately
@@ -66,9 +66,20 @@ export class UsageTrackingProvider implements Provider {
66
66
  }
67
67
 
68
68
  try {
69
+ // A fallback-serving wrapper (`RetryProvider`'s escalation) stamps the
70
+ // profile that actually governed the response; prefer it over the
71
+ // original request's resolution so a degraded serve bills under the
72
+ // backup profile, not the failed primary's. `forceOverrideProfile`
73
+ // mirrors how the fallback dispatch floated that profile above the
74
+ // call-site layers.
75
+ const appliedOverride =
76
+ response.actualInferenceProfile ?? config.overrideProfile;
69
77
  const attribution = resolveUsageAttribution({
70
78
  callSite: config.callSite,
71
- overrideProfile: config.overrideProfile,
79
+ overrideProfile: appliedOverride,
80
+ ...(response.actualInferenceProfile !== undefined
81
+ ? { forceOverrideProfile: true }
82
+ : {}),
72
83
  });
73
84
  const providerName = response.actualProvider ?? this.inner.name;
74
85
  const pricingUsage = buildPricingUsageFromResponse(
@@ -41,9 +41,8 @@ Approvals are **orthogonal to message sending**. The assistant asks for approval
41
41
 
42
42
  - **Discovery**: Clients discover pending approvals via SSE events (`confirmation_request`, `secret_request`) which include a `requestId`.
43
43
  - **Resolution**: Clients respond via standalone endpoints keyed by `requestId`:
44
- - `POST /v1/confirm` — `{ requestId, decision, selectedPattern?, selectedScope? }`. Valid decisions: `"allow"`, `"allow_10m"`, `"allow_conversation"`, `"deny"`, `"always_allow"`, `"always_deny"`. For persistent decisions (`always_allow`, `always_deny`), `selectedPattern` and `selectedScope` are validated against the server-provided allowlist/scope options from the original confirmation request before trust rules are persisted.
44
+ - `POST /v1/confirm` — `{ requestId, decision }`. Valid decisions: `"allow"`, `"deny"`. The confirm route resolves the pending interaction and nothing else: durable Always-Allow rules are minted through the gateway trust-rules API (`POST /v1/trust-rules`, also served assistant-scoped), which carries the rule's tool, pattern, and risk. The daemon's own `trust-rules` surface is read-only (`GET /v1/trust-rules` list, `POST /v1/trust-rules/suggest`).
45
45
  - `POST /v1/secret` — `{ requestId, value, delivery }`
46
- - `POST /v1/trust-rules` — `{ requestId, pattern, scope, decision }`. Validates pattern/scope against server-provided options. Does not resolve the confirmation itself.
47
46
  - **Tracking**: The `pending-interactions` tracker (`assistant/src/runtime/pending-interactions.ts`) maps `requestId → conversation`. Use `register()` to track, `resolve()` to consume, `getByConversation()` to query.
48
47
 
49
48
  Do NOT couple approval handling to message sending. Do NOT add run/status tracking to the send path.
@@ -266,6 +266,7 @@ const recordRequestLogCalls: Array<{
266
266
  }> = [];
267
267
  const recordUsageCalls: Array<{
268
268
  conversationId: string;
269
+ providerName: string | undefined;
269
270
  inputTokens: number;
270
271
  outputTokens: number;
271
272
  model: string;
@@ -279,7 +280,7 @@ const recordUsageCalls: Array<{
279
280
  }> = [];
280
281
  mock.module("../../daemon/conversation-usage.js", () => ({
281
282
  recordUsage: (
282
- ctx: { conversationId: string },
283
+ ctx: { conversationId: string; providerName?: string },
283
284
  inputTokens: number,
284
285
  outputTokens: number,
285
286
  model: string,
@@ -300,6 +301,7 @@ mock.module("../../daemon/conversation-usage.js", () => ({
300
301
  ) => {
301
302
  recordUsageCalls.push({
302
303
  conversationId: ctx.conversationId,
304
+ providerName: ctx.providerName,
303
305
  inputTokens,
304
306
  outputTokens,
305
307
  model,
@@ -1748,7 +1750,13 @@ describe("wakeAgentForOpportunity", () => {
1748
1750
  { resolveTarget: async () => conversation },
1749
1751
  );
1750
1752
 
1751
- expect(result).toEqual({ invoked: true, producedToolCalls: false });
1753
+ // The result carries how the run ended, so a caller that treats output as
1754
+ // a finished answer can see that something else ended this one.
1755
+ expect(result).toEqual({
1756
+ invoked: true,
1757
+ producedToolCalls: false,
1758
+ exitReason: "error",
1759
+ });
1752
1760
  // The checkpoint's flush pushed + persisted the partial tail.
1753
1761
  expect(conversation.pushedMessages.length).toBeGreaterThan(0);
1754
1762
  expect(conversation.persistedTailCalls.length).toBeGreaterThan(0);
@@ -1770,7 +1778,11 @@ describe("wakeAgentForOpportunity", () => {
1770
1778
  { resolveTarget: async () => conversation },
1771
1779
  );
1772
1780
 
1773
- expect(result).toEqual({ invoked: true, producedToolCalls: false });
1781
+ expect(result).toEqual({
1782
+ invoked: true,
1783
+ producedToolCalls: false,
1784
+ exitReason: "no_tool_calls",
1785
+ });
1774
1786
  });
1775
1787
 
1776
1788
  test("reports no_output on a clean empty reply when the caller requires usable output", async () => {
@@ -2986,6 +2998,96 @@ describe("wakeAgentForOpportunity", () => {
2986
2998
  });
2987
2999
  });
2988
3000
 
3001
+ test("rerouted wake records usage under the profile that actually served", async () => {
3002
+ // The wake resolved its own profile before the run; the request then hit
3003
+ // an outage-shaped failure and `RetryProvider` escalated to a backup
3004
+ // profile on a different provider, stamping both `actualProvider` and
3005
+ // `actualInferenceProfile` on the response the loop reports.
3006
+ const usageEvent: AgentEvent = {
3007
+ type: "usage",
3008
+ inputTokens: 100,
3009
+ outputTokens: 5,
3010
+ model: "claude-sonnet-5",
3011
+ actualProvider: "anthropic",
3012
+ actualInferenceProfile: "backupProfile",
3013
+ providerDurationMs: 10,
3014
+ rawRequest: { request: "rerouted wake" },
3015
+ rawResponse: { response: "real reply" },
3016
+ };
3017
+ const conversation = makeWakeConversation({
3018
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
3019
+ scriptedEvents: [usageEvent],
3020
+ scriptedAssistant: {
3021
+ role: "assistant",
3022
+ content: [{ type: "text", text: "real reply" }],
3023
+ },
3024
+ });
3025
+
3026
+ await wakeAgentForOpportunity(
3027
+ {
3028
+ conversationId: conversation.conversationId,
3029
+ hint: "do reply",
3030
+ source: "unit-test",
3031
+ callSite: "memoryRetrospective",
3032
+ },
3033
+ { resolveTarget: async () => conversation },
3034
+ );
3035
+
3036
+ // All three attribution facets of the row must describe the same call.
3037
+ // Provider and model already followed the backup off the event; the
3038
+ // profile has to follow it too or the row contradicts itself.
3039
+ expect(recordUsageCalls).toHaveLength(1);
3040
+ expect(recordUsageCalls[0]).toMatchObject({
3041
+ providerName: "anthropic",
3042
+ model: "claude-sonnet-5",
3043
+ overrideProfile: "backupProfile",
3044
+ forceOverrideProfile: true,
3045
+ selectionSeed: conversation.conversationId,
3046
+ });
3047
+ });
3048
+
3049
+ test("non-rerouted wake keeps its own profile resolution", async () => {
3050
+ // No reroute: the event carries no `actualInferenceProfile`, so the wake's
3051
+ // own pre-run resolution stands and nothing is floated above the call site.
3052
+ const usageEvent: AgentEvent = {
3053
+ type: "usage",
3054
+ inputTokens: 100,
3055
+ outputTokens: 5,
3056
+ model: "test-model",
3057
+ actualProvider: "test-provider",
3058
+ providerDurationMs: 10,
3059
+ rawRequest: { request: "normal wake" },
3060
+ rawResponse: { response: "real reply" },
3061
+ };
3062
+ const conversation = makeWakeConversation({
3063
+ baseline: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
3064
+ scriptedEvents: [usageEvent],
3065
+ scriptedAssistant: {
3066
+ role: "assistant",
3067
+ content: [{ type: "text", text: "real reply" }],
3068
+ },
3069
+ });
3070
+
3071
+ await wakeAgentForOpportunity(
3072
+ {
3073
+ conversationId: conversation.conversationId,
3074
+ hint: "do reply",
3075
+ source: "unit-test",
3076
+ callSite: "memoryRetrospective",
3077
+ },
3078
+ { resolveTarget: async () => conversation },
3079
+ );
3080
+
3081
+ expect(recordUsageCalls).toHaveLength(1);
3082
+ expect(recordUsageCalls[0]).toMatchObject({
3083
+ providerName: "test-provider",
3084
+ model: "test-model",
3085
+ overrideProfile: null,
3086
+ forceOverrideProfile: false,
3087
+ selectionSeed: conversation.conversationId,
3088
+ });
3089
+ });
3090
+
2989
3091
  test("silent no-op wake still records usage even though its request log is dropped", async () => {
2990
3092
  const usageEvent: AgentEvent = {
2991
3093
  type: "usage",
@@ -267,24 +267,106 @@ describe("runBackgroundJob", () => {
267
267
  // `turnFailure`. The runner must surface this as a failure, not ok=true.
268
268
  processMessageImpl = async () => ({
269
269
  messageId: "msg-failed-turn",
270
- turnFailure: { failureCode: "provider_error" },
270
+ turnFailure: { failureCode: "PROVIDER_BILLING" },
271
271
  });
272
272
 
273
273
  const result = await runBackgroundJob(baseOpts());
274
274
 
275
275
  expect(result.ok).toBe(false);
276
276
  expect(result.errorKind).toBe("model_provider");
277
- expect(result.error?.message).toContain("provider_error");
277
+ expect(result.error?.message).toContain("PROVIDER_BILLING");
278
278
  expect(result.conversationId).toBe(STUB_CONVERSATION_ID);
279
279
  // The stable classified code rides the result so callers can branch on
280
280
  // the failure class without parsing the error message.
281
- expect(result.failureCode).toBe("provider_error");
281
+ expect(result.turnFailure?.failureCode).toBe("PROVIDER_BILLING");
282
282
 
283
283
  expect(emitCalls).toHaveLength(1);
284
284
  expect(emitCalls[0].sourceEventName).toBe("activity.failed");
285
285
  expect(
286
286
  (emitCalls[0].contextPayload as { errorKind: string }).errorKind,
287
287
  ).toBe("model_provider");
288
+ // A provider-scoped failure code keys the notification on the cause and
289
+ // the resolved provider scope, not the job. `heartbeatAgent` resolves the
290
+ // cost-optimized default profile, so every job failing on this code
291
+ // through that route collapses into one notification per UTC day.
292
+ expect(emitCalls[0].dedupeKey as string).toMatch(
293
+ /^activity-failed:cause:PROVIDER_BILLING:cost-optimized:\d{4}-\d{2}-\d{2}$/,
294
+ );
295
+ });
296
+
297
+ test("a usable overrideProfile scopes the cause-keyed dedupe", async () => {
298
+ processMessageImpl = async () => ({
299
+ messageId: "msg-failed-turn",
300
+ turnFailure: { failureCode: "PROVIDER_BILLING" },
301
+ });
302
+
303
+ await runBackgroundJob(baseOpts({ overrideProfile: "balanced" }));
304
+
305
+ expect(emitCalls).toHaveLength(1);
306
+ expect(emitCalls[0].dedupeKey as string).toMatch(
307
+ /^activity-failed:cause:PROVIDER_BILLING:balanced:\d{4}-\d{2}-\d{2}$/,
308
+ );
309
+ });
310
+
311
+ test("carried turn attribution scopes the dedupe and rides the payload", async () => {
312
+ processMessageImpl = async () => ({
313
+ messageId: "msg-failed-turn",
314
+ turnFailure: {
315
+ failureCode: "PROVIDER_BILLING",
316
+ userMessage: "You're out of credits.",
317
+ errorCategory: "credits_exhausted",
318
+ connectionName: "conn-a",
319
+ profileName: "balanced",
320
+ },
321
+ });
322
+
323
+ const result = await runBackgroundJob(baseOpts());
324
+
325
+ expect(result.ok).toBe(false);
326
+ expect(result.turnFailure?.userMessage).toBe("You're out of credits.");
327
+ expect(emitCalls).toHaveLength(1);
328
+ // The carried connection outranks the call site's recomputed profile.
329
+ expect(emitCalls[0].dedupeKey as string).toMatch(
330
+ /^activity-failed:cause:PROVIDER_BILLING:conn-a:\d{4}-\d{2}-\d{2}$/,
331
+ );
332
+ expect(emitCalls[0].contextPayload).toMatchObject({
333
+ failureSummary: "You're out of credits.",
334
+ errorCategory: "credits_exhausted",
335
+ connectionName: "conn-a",
336
+ });
337
+ });
338
+
339
+ test("job-specific failure code keeps the per-job dedupe key", async () => {
340
+ // CONTEXT_TOO_LARGE names a defect in this job's own prompt; collapsing
341
+ // it across jobs would hide a second job's unrelated failure.
342
+ processMessageImpl = async () => ({
343
+ messageId: "msg-failed-turn",
344
+ turnFailure: { failureCode: "CONTEXT_TOO_LARGE" },
345
+ });
346
+
347
+ const result = await runBackgroundJob(baseOpts());
348
+
349
+ expect(result.ok).toBe(false);
350
+ expect(result.turnFailure?.failureCode).toBe("CONTEXT_TOO_LARGE");
351
+ expect(emitCalls).toHaveLength(1);
352
+ expect(emitCalls[0].dedupeKey as string).toMatch(
353
+ /^activity-failed:test-job:\d{4}-\d{2}-\d{2}$/,
354
+ );
355
+ });
356
+
357
+ test("code-less provider failure dedupes on the shared model_provider cause", async () => {
358
+ processMessageImpl = async () => {
359
+ throw new Error("provider returned 401");
360
+ };
361
+
362
+ const result = await runBackgroundJob(baseOpts());
363
+
364
+ expect(result.ok).toBe(false);
365
+ expect(result.errorKind).toBe("model_provider");
366
+ expect(emitCalls).toHaveLength(1);
367
+ expect(emitCalls[0].dedupeKey as string).toMatch(
368
+ /^activity-failed:cause:model_provider:cost-optimized:\d{4}-\d{2}-\d{2}$/,
369
+ );
288
370
  });
289
371
 
290
372
  test("timeout: returns ok=false with errorKind=timeout and emits activity.failed", async () => {
@@ -301,6 +383,10 @@ describe("runBackgroundJob", () => {
301
383
  expect(
302
384
  (emitCalls[0].contextPayload as { errorKind: string }).errorKind,
303
385
  ).toBe("timeout");
386
+ // A timeout is job-specific, so it keeps the per-job dedupe key.
387
+ expect(emitCalls[0].dedupeKey as string).toMatch(
388
+ /^activity-failed:test-job:\d{4}-\d{2}-\d{2}$/,
389
+ );
304
390
  });
305
391
 
306
392
  test("suppressFailureNotifications: failure returns ok=false but emits nothing", async () => {