@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
@@ -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 () => {
@@ -459,6 +459,20 @@ export interface WakeResult {
459
459
  producedToolCalls: boolean;
460
460
  /** Present only when `invoked: false`; identifies why the wake was skipped. */
461
461
  reason?: WakeSkipReason;
462
+ /**
463
+ * How the agent loop terminated, from its `agent_loop_exit` event. Set on
464
+ * `invoked: true` results whose loop reported a terminal exit, and absent
465
+ * when it reported none (a checkpoint handoff, whose run resumes
466
+ * separately).
467
+ *
468
+ * `"no_tool_calls"` is the model-driven stop: the run finished because the
469
+ * assistant answered without asking for another tool. Every other reason
470
+ * means something ended the run for it (cancellation, an output-token
471
+ * ceiling, an unhandled error), so a caller reading the run's output as a
472
+ * complete conclusion has to tell them apart: the memory retrospective
473
+ * accepts a reviewed-nothing-to-save pass only on a model-driven stop.
474
+ */
475
+ exitReason?: AgentLoopExitReason;
462
476
  }
463
477
 
464
478
  /**
@@ -1051,6 +1065,10 @@ export async function wakeAgentForOpportunity(
1051
1065
  // catch turns provider rejections and unhandled throws into a graceful
1052
1066
  // no-output stop instead of rethrowing).
1053
1067
  let terminalExitReason: AgentLoopExitReason | null = null;
1068
+ // The `exitReason` field for an `invoked: true` result: present exactly
1069
+ // when the loop reported a terminal exit.
1070
+ const exitReasonField = (): { exitReason?: AgentLoopExitReason } =>
1071
+ terminalExitReason !== null ? { exitReason: terminalExitReason } : {};
1054
1072
  const persistLog = (record: PendingLog): void => {
1055
1073
  try {
1056
1074
  recordRequestLog(
@@ -1125,10 +1143,23 @@ export async function wakeAgentForOpportunity(
1125
1143
  // the conversation-id seed resolves the same mix arm the dispatch
1126
1144
  // path chose. Without these, attribution credits the call-site
1127
1145
  // profile/arm instead of the one that ran.
1146
+ //
1147
+ // A rerouted call (`RetryProvider`'s fallback-profile escalation)
1148
+ // outranks both: the wake resolved `overrideProfile` before the
1149
+ // run and cannot know the request was escalated, while
1150
+ // `providerName` and `model` on this same row already follow the
1151
+ // backup off the event. Attributing the profile from the wake's
1152
+ // own resolution would write a row that contradicts itself. One
1153
+ // row is written per `usage` event, each from that event alone,
1154
+ // so this needs no cross-call state: a later non-rerouted call
1155
+ // writes its own row under the wake's resolution.
1128
1156
  {
1129
1157
  callSite,
1130
- overrideProfile: overrideProfile ?? null,
1131
- forceOverrideProfile,
1158
+ overrideProfile:
1159
+ event.actualInferenceProfile ?? overrideProfile ?? null,
1160
+ forceOverrideProfile:
1161
+ event.actualInferenceProfile !== undefined ||
1162
+ forceOverrideProfile,
1132
1163
  selectionSeed: conversationId,
1133
1164
  },
1134
1165
  opts.cronRunId ?? null,
@@ -1545,7 +1576,11 @@ export async function wakeAgentForOpportunity(
1545
1576
  reason: "run_error" as const,
1546
1577
  };
1547
1578
  }
1548
- return { invoked: true, producedToolCalls: false };
1579
+ return {
1580
+ invoked: true,
1581
+ producedToolCalls: false,
1582
+ ...exitReasonField(),
1583
+ };
1549
1584
  } finally {
1550
1585
  // Restore the pre-wake values so a queued user turn or background read
1551
1586
  // never observes the wake's stamps. (`runAgentLoopImpl` re-stamps both
@@ -1642,7 +1677,11 @@ export async function wakeAgentForOpportunity(
1642
1677
  // since checkpoints only fire after tool turns and there were
1643
1678
  // none.) The finally still kicks the queue drain so a racy queued
1644
1679
  // message isn't stranded.
1645
- return { invoked: true, producedToolCalls: false };
1680
+ return {
1681
+ invoked: true,
1682
+ producedToolCalls: false,
1683
+ ...exitReasonField(),
1684
+ };
1646
1685
  }
1647
1686
 
1648
1687
  tailMessageCount = tailMessages.length;
@@ -1677,7 +1716,7 @@ export async function wakeAgentForOpportunity(
1677
1716
  });
1678
1717
  drainedInTry = true;
1679
1718
 
1680
- return { invoked: true, producedToolCalls };
1719
+ return { invoked: true, producedToolCalls, ...exitReasonField() };
1681
1720
  } finally {
1682
1721
  // Put the conversation's resting trust back on every exit path.
1683
1722
  restorePersistentWakeTrust();
@@ -18,15 +18,18 @@
18
18
  * `suppressFailureNotifications`.
19
19
  */
20
20
 
21
+ import { resolveSingleRouteProfileKey } from "../config/llm-resolver.js";
22
+ import { getConfig } from "../config/loader.js";
21
23
  import type { LLMCallSite } from "../config/schemas/llm.js";
22
24
  import { processMessage } from "../daemon/process-message.js";
23
25
  import type { SubagentToolGateMode } from "../daemon/tool-setup-types.js";
24
26
  import type { TrustContext } from "../daemon/trust-context-types.js";
25
- import { emitNotificationSignal } from "../notifications/emit-signal.js";
26
- import type { AttentionHints } from "../notifications/signal.js";
27
+ import { emitBackgroundFailureSignal } from "../notifications/background-failure-signal.js";
27
28
  import { bootstrapConversation } from "../persistence/conversation-bootstrap.js";
28
29
  import { addMessage } from "../persistence/conversation-crud.js";
29
30
  import type { TitleOrigin } from "../persistence/conversation-title-service.js";
31
+ import { dispatchProviderResolvable } from "../providers/provider-resolvability.js";
32
+ import type { TurnFailure } from "../telemetry/turn-outcome.js";
30
33
  import { getLogger } from "../util/logger.js";
31
34
  import { hasReceivedUserMessage } from "./pre-first-message-gate.js";
32
35
 
@@ -53,12 +56,14 @@ class BackgroundJobTimeoutError extends Error {
53
56
  */
54
57
  class BackgroundJobTurnFailureError extends Error {
55
58
  override name = "BackgroundJobTurnFailureError";
56
- readonly failureCode: string | undefined;
57
- constructor(failureCode: string | undefined) {
59
+ readonly turnFailure: TurnFailure;
60
+ constructor(turnFailure: TurnFailure) {
58
61
  super(
59
- failureCode ? `Agent turn failed (${failureCode})` : "Agent turn failed",
62
+ turnFailure.failureCode
63
+ ? `Agent turn failed (${turnFailure.failureCode})`
64
+ : "Agent turn failed",
60
65
  );
61
- this.failureCode = failureCode;
66
+ this.turnFailure = turnFailure;
62
67
  }
63
68
  }
64
69
 
@@ -99,9 +104,9 @@ export interface RunBackgroundJobOptions {
99
104
  timeoutMs: number;
100
105
  /**
101
106
  * When true, failures do NOT emit an `activity.failed` notification.
102
- * Use for jobs that own their own failure UX (e.g. heartbeat's alerter)
103
- * or for "quiet" scheduled jobs that the user has explicitly asked to
104
- * suppress notifications for.
107
+ * Use for jobs whose failure alerting is owned elsewhere: heartbeat's
108
+ * alerter banner, and the scheduler's retry policy (which alerts once,
109
+ * when retries are exhausted, instead of once per attempt).
105
110
  */
106
111
  suppressFailureNotifications?: boolean;
107
112
  /** Conversation grouping id. Defaults to `"system:background"`. */
@@ -199,13 +204,14 @@ export interface RunBackgroundJobResult {
199
204
  error?: Error;
200
205
  errorKind?: BackgroundJobErrorKind;
201
206
  /**
202
- * Stable classified error code (`ConversationErrorCode`, e.g.
203
- * `"PROVIDER_BILLING"`) when the turn failed without throwing. Absent for
204
- * timeouts and thrown exceptions. Lets callers branch on the failure class
205
- * (e.g. billing vs transient) without depending on error identity or
206
- * message text.
207
+ * The failing turn's full carried classification (stable failureCode,
208
+ * authored user message, category, connection/profile attribution), when
209
+ * the failure was a non-throwing turn failure. Absent for timeouts and
210
+ * thrown exceptions. The single classification field on the result:
211
+ * callers branch on `turnFailure?.failureCode` and forward the object
212
+ * wholesale when reporting the failure onward.
207
213
  */
208
- failureCode?: string;
214
+ turnFailure?: TurnFailure;
209
215
  /**
210
216
  * Set when the runner declined to execute. Callers can distinguish a
211
217
  * skipped job from a successful one even though both report `ok: true`.
@@ -377,18 +383,17 @@ export async function runBackgroundJob(
377
383
  // Rethrow so it flows through the shared failure path below (logging +
378
384
  // `activity.failed` emission) rather than returning `ok: true`.
379
385
  if (runResult.turnFailure) {
380
- throw new BackgroundJobTurnFailureError(
381
- runResult.turnFailure.failureCode,
382
- );
386
+ throw new BackgroundJobTurnFailureError(runResult.turnFailure);
383
387
  }
384
388
  return { conversationId: conversation.id, ok: true };
385
389
  } catch (err) {
386
390
  const errorKind = classifyError(err);
387
391
  const error = err instanceof Error ? err : new Error(String(err));
388
- const failureCode =
392
+ const turnFailure =
389
393
  err instanceof BackgroundJobTurnFailureError
390
- ? err.failureCode
394
+ ? err.turnFailure
391
395
  : undefined;
396
+ const failureCode = turnFailure?.failureCode;
392
397
  // Bootstrap can fail before `conversation` is assigned; fall back to ""
393
398
  // so the structured failure result still flows to the caller.
394
399
  const conversationId = conversation?.id ?? "";
@@ -404,38 +409,51 @@ export async function runBackgroundJob(
404
409
  );
405
410
 
406
411
  if (!opts.suppressFailureNotifications) {
407
- const hints: AttentionHints = {
408
- requiresAction: false,
409
- urgency: "medium",
410
- isAsyncBackground: true,
411
- visibleInSourceNow: false,
412
- };
413
- // Dedupe by jobName + UTC date so repeated failures of the same
414
- // background job (e.g. a watcher whose credentials are revoked)
415
- // collapse into a single home-feed entry per day rather than
416
- // spamming on every tick.
417
- const day = new Date().toISOString().slice(0, 10);
418
- const dedupeKey = `activity-failed:${opts.jobName}:${day}`;
419
- emitNotificationSignal({
420
- sourceChannel: "assistant_tool",
421
- sourceContextId: conversationId,
422
- sourceEventName: "activity.failed",
423
- dedupeKey,
424
- contextPayload: {
425
- jobName: opts.jobName,
426
- errorMessage: error.message,
427
- errorKind,
428
- },
429
- attentionHints: hints,
430
- }).catch((emitErr) => {
431
- log.warn(
412
+ // Fallback scope for failures that carry no attribution: the profile
413
+ // key the resolver would use for this call site, computed with the
414
+ // same provider-resolvability predicate dispatch uses so the
415
+ // recomputation cannot accept a profile dispatch rejected (e.g. a pin
416
+ // whose connection was force-deleted). A mix winner, no named winner,
417
+ // or a config read failing on this path all yield no scope, and the
418
+ // key falls back to per-job. A carried `turnFailure.profileName` is
419
+ // dispatch's own attribution and takes precedence over all of this.
420
+ let fallbackProviderScope: string | undefined;
421
+ try {
422
+ fallbackProviderScope = resolveSingleRouteProfileKey(
423
+ opts.callSite,
424
+ getConfig().llm,
432
425
  {
433
- err: emitErr instanceof Error ? emitErr.message : String(emitErr),
434
- jobName: opts.jobName,
435
- conversationId,
426
+ ...(opts.overrideProfile
427
+ ? { overrideProfile: opts.overrideProfile }
428
+ : {}),
429
+ isResolvableProvider: dispatchProviderResolvable,
436
430
  },
437
- "Failed to emit activity.failed notification for background job",
438
431
  );
432
+ } catch {
433
+ fallbackProviderScope = undefined;
434
+ }
435
+ emitBackgroundFailureSignal({
436
+ jobName: opts.jobName,
437
+ sourceChannel: "assistant_tool",
438
+ sourceContextId: conversationId,
439
+ errorKind,
440
+ errorMessage: error.message,
441
+ ...(failureCode !== undefined ? { failureCode } : {}),
442
+ ...(turnFailure?.userMessage !== undefined
443
+ ? { failureSummary: turnFailure.userMessage }
444
+ : {}),
445
+ ...(turnFailure?.errorCategory !== undefined
446
+ ? { errorCategory: turnFailure.errorCategory }
447
+ : {}),
448
+ ...(turnFailure?.connectionName !== undefined
449
+ ? { connectionName: turnFailure.connectionName }
450
+ : {}),
451
+ ...(turnFailure?.profileName !== undefined
452
+ ? { profileName: turnFailure.profileName }
453
+ : {}),
454
+ ...(fallbackProviderScope !== undefined
455
+ ? { fallbackProviderScope }
456
+ : {}),
439
457
  });
440
458
  }
441
459
 
@@ -444,7 +462,7 @@ export async function runBackgroundJob(
444
462
  ok: false,
445
463
  error,
446
464
  errorKind,
447
- ...(failureCode !== undefined ? { failureCode } : {}),
465
+ ...(turnFailure !== undefined ? { turnFailure } : {}),
448
466
  };
449
467
  } finally {
450
468
  if (timer) {
@@ -548,44 +548,6 @@ const slackProbe: ChannelProbe = {
548
548
  },
549
549
  };
550
550
 
551
- /**
552
- * Whether Discord admits anything in the guilds it has joined.
553
- *
554
- * A configuration check rather than an operational one, and the distinction
555
- * carries the meaning. An empty allow-list is a setup step nobody finished,
556
- * not a channel that broke: direct messages reach the assistant either way,
557
- * because a message with no guild never meets this gate. Reporting it as
558
- * failing health would send an operator to look for an outage that is not
559
- * there, and reporting it as ready would claim delivery while every guild
560
- * message is dropped.
561
- *
562
- * An unreachable gateway is indeterminate, matching the socket check beside
563
- * it: nobody looked, so nothing is claimed either way.
564
- */
565
- async function checkDiscordGuildAdmission(): Promise<ReadinessCheckResult> {
566
- const { readDiscordAdmission } =
567
- await import("../channels/gateway-discord-admission.js");
568
-
569
- let admitted;
570
- try {
571
- admitted = (await readDiscordAdmission()).admittedChannelCount;
572
- } catch {
573
- return {
574
- name: "guild_admission",
575
- passed: true,
576
- message: "Could not reach the gateway to read Discord's allow-list",
577
- indeterminate: true,
578
- };
579
- }
580
-
581
- return check(
582
- "guild_admission",
583
- admitted > 0,
584
- `Discord may act in ${admitted} channel${admitted === 1 ? "" : "s"}`,
585
- "Discord is in no channel's allow-list, so every guild message is dropped. Add channel ids to `discord.allowedChannelIds`",
586
- );
587
- }
588
-
589
551
  // ── Discord Probe ───────────────────────────────────────────────────────────
590
552
 
591
553
  /**
@@ -614,7 +576,6 @@ const discordProbe: ChannelProbe = {
614
576
  "Discord bot token",
615
577
  ),
616
578
  await checkSocketInboundDelivery("discord"),
617
- await checkDiscordGuildAdmission(),
618
579
  ];
619
580
  },
620
581
  };
@@ -42,6 +42,7 @@ import {
42
42
  startPluginScheduleReconcileSweep,
43
43
  stopPluginScheduleReconcileSweep,
44
44
  } from "../schedule/plugin-schedule-reconciler.js";
45
+ import { sttCatalogKeyForRole } from "../stt/roles.js";
45
46
  import {
46
47
  activeSttStreamSessions,
47
48
  SttStreamSession,
@@ -265,16 +266,22 @@ export class RuntimeHttpServer {
265
266
  const sttData = data;
266
267
 
267
268
  // The runtime is config-authoritative: always resolve the
268
- // provider from `services.stt.provider` regardless of what
269
- // the client/gateway requested.
269
+ // provider from config regardless of what the client/gateway
270
+ // requested. Dictation's own role selects it, matching the dial
271
+ // below, so the session's label, its failure copy and the
272
+ // mismatch telemetry all name the provider this socket actually
273
+ // reaches.
270
274
  //
271
275
  // getConfig() can throw (e.g. after invalidateConfigCache()
272
276
  // when config.json is temporarily invalid). Wrap in try/catch
273
- // so the session still starts normally resolveStreamingTranscriber
277
+ // so the session still starts normally: resolveStreamingTranscriber
274
278
  // reads config inside SttStreamSession.start()'s own guarded path.
275
279
  let configuredProvider: string | undefined;
276
280
  try {
277
- configuredProvider = getConfig().services.stt.provider;
281
+ configuredProvider = sttCatalogKeyForRole(
282
+ getConfig().services.stt,
283
+ "dictation",
284
+ );
278
285
 
279
286
  // Mismatch telemetry: when the optional requested provider
280
287
  // disagrees with the configured provider, log a warning so
@@ -327,6 +334,7 @@ export class RuntimeHttpServer {
327
334
  // transcriber and sends a `ready` event on success.
328
335
  void session.start(() =>
329
336
  resolveStreamingTranscriber({
337
+ role: "dictation",
330
338
  sampleRate: sttData.sampleRate,
331
339
  }),
332
340
  );
@@ -464,7 +464,7 @@ describe("GET /avatar/get (manifest-driven precedence)", () => {
464
464
  base64?: string;
465
465
  }
466
466
 
467
- test("returns the PNG for an image manifest", () => {
467
+ test("returns the PNG for an image manifest", async () => {
468
468
  writeFileSync(path(IMAGE_FILENAME), Buffer.from("png bytes"));
469
469
  const state: AvatarState = {
470
470
  kind: "image",
@@ -474,12 +474,12 @@ describe("GET /avatar/get (manifest-driven precedence)", () => {
474
474
  };
475
475
  writeManifest(state, avatarDir);
476
476
 
477
- const result = getHandler("avatar_get")({}) as GetResult;
477
+ const result = (await getHandler("avatar_get")({})) as GetResult;
478
478
  expect(result.exists).toBe(true);
479
479
  expect(result.path).toBe(path(IMAGE_FILENAME));
480
480
  });
481
481
 
482
- test("returns base64 for an image manifest when format=base64", () => {
482
+ test("returns base64 for an image manifest when format=base64", async () => {
483
483
  writeFileSync(path(IMAGE_FILENAME), Buffer.from("png bytes"));
484
484
  const state: AvatarState = {
485
485
  kind: "image",
@@ -489,15 +489,15 @@ describe("GET /avatar/get (manifest-driven precedence)", () => {
489
489
  };
490
490
  writeManifest(state, avatarDir);
491
491
 
492
- const result = getHandler("avatar_get")({
492
+ const result = (await getHandler("avatar_get")({
493
493
  queryParams: { format: "base64" },
494
- }) as GetResult;
494
+ })) as GetResult;
495
495
  expect(result.exists).toBe(true);
496
496
  expect(result.base64).toBe(Buffer.from("png bytes").toString("base64"));
497
497
  expect(result.path).toBeUndefined();
498
498
  });
499
499
 
500
- test("returns the existing raster for a character manifest", () => {
500
+ test("returns the existing raster for a character manifest", async () => {
501
501
  // The builder writes the rendered PNG; assert the accessor returns it
502
502
  // without needing to re-render (the manifest says character).
503
503
  writeFileSync(path(IMAGE_FILENAME), Buffer.from("rendered character png"));
@@ -509,12 +509,12 @@ describe("GET /avatar/get (manifest-driven precedence)", () => {
509
509
  };
510
510
  writeManifest(state, avatarDir);
511
511
 
512
- const result = getHandler("avatar_get")({}) as GetResult;
512
+ const result = (await getHandler("avatar_get")({})) as GetResult;
513
513
  expect(result.exists).toBe(true);
514
514
  expect(result.path).toBe(path(IMAGE_FILENAME));
515
515
  });
516
516
 
517
- test("manifest precedence wins over file order: a character manifest is honored even when a PNG is present", () => {
517
+ test("manifest precedence wins over file order: a character manifest is honored even when a PNG is present", async () => {
518
518
  // Both a PNG and traits are on disk, but the manifest declares character.
519
519
  // The pre-manifest accessor was image-first; precedence is now the manifest.
520
520
  writeFileSync(path(IMAGE_FILENAME), Buffer.from("rendered character png"));
@@ -527,27 +527,27 @@ describe("GET /avatar/get (manifest-driven precedence)", () => {
527
527
  };
528
528
  writeManifest(state, avatarDir);
529
529
 
530
- const result = getHandler("avatar_get")({}) as GetResult;
530
+ const result = (await getHandler("avatar_get")({})) as GetResult;
531
531
  expect(result.exists).toBe(true);
532
532
  expect(result.path).toBe(path(IMAGE_FILENAME));
533
533
  });
534
534
 
535
- test("returns exists:false for a none manifest", () => {
535
+ test("returns exists:false for a none manifest", async () => {
536
536
  writeManifest(
537
537
  { kind: "none", traits: null, source: null, image: null },
538
538
  avatarDir,
539
539
  );
540
540
 
541
- const result = getHandler("avatar_get")({}) as GetResult;
541
+ const result = (await getHandler("avatar_get")({})) as GetResult;
542
542
  expect(result.exists).toBe(false);
543
543
  expect(result.path).toBeUndefined();
544
544
  });
545
545
 
546
- test("self-heals from legacy files when no manifest exists (image file) and persists the manifest", () => {
546
+ test("self-heals from legacy files when no manifest exists (image file) and persists the manifest", async () => {
547
547
  writeFileSync(path(IMAGE_FILENAME), Buffer.from("png bytes"));
548
548
  expect(existsSync(path(MANIFEST_FILENAME))).toBe(false);
549
549
 
550
- const result = getHandler("avatar_get")({}) as GetResult;
550
+ const result = (await getHandler("avatar_get")({})) as GetResult;
551
551
  expect(result.exists).toBe(true);
552
552
  expect(result.path).toBe(path(IMAGE_FILENAME));
553
553
 
@@ -558,14 +558,14 @@ describe("GET /avatar/get (manifest-driven precedence)", () => {
558
558
  expect(persisted.kind).toBe("image");
559
559
  });
560
560
 
561
- test("returns exists:false for an empty workspace (no manifest, no files)", () => {
562
- const result = getHandler("avatar_get")({}) as GetResult;
561
+ test("returns exists:false for an empty workspace (no manifest, no files)", async () => {
562
+ const result = (await getHandler("avatar_get")({})) as GetResult;
563
563
  expect(result.exists).toBe(false);
564
564
  });
565
565
 
566
- test("rejects an invalid format", () => {
567
- expect(() =>
566
+ test("rejects an invalid format", async () => {
567
+ await expect(
568
568
  getHandler("avatar_get")({ queryParams: { format: "bmp" } }),
569
- ).toThrow(/Invalid format/);
569
+ ).rejects.toThrow(/Invalid format/);
570
570
  });
571
571
  });
@@ -270,7 +270,7 @@ afterEach(() => {
270
270
  // ---------------------------------------------------------------------------
271
271
 
272
272
  describe("handleBackupList", () => {
273
- test("empty workspace: returns empty local array and one unreachable iCloud default", async () => {
273
+ test("empty workspace uses the platform offsite default", async () => {
274
274
  const origHome = process.env.HOME;
275
275
  process.env.HOME = ROOT;
276
276
  try {
@@ -287,10 +287,14 @@ describe("handleBackupList", () => {
287
287
 
288
288
  const result = await handleBackupList();
289
289
  expect(result.local).toEqual([]);
290
- expect(result.offsite).toHaveLength(1);
291
- expect(result.offsite[0].destination.encrypt).toBe(true);
292
- expect(result.offsite[0].snapshots).toEqual([]);
293
- expect(result.offsite[0].reachable).toBe(false);
290
+ expect(result.offsite).toHaveLength(
291
+ process.platform === "darwin" ? 1 : 0,
292
+ );
293
+ if (process.platform === "darwin") {
294
+ expect(result.offsite[0].destination.encrypt).toBe(true);
295
+ expect(result.offsite[0].snapshots).toEqual([]);
296
+ expect(result.offsite[0].reachable).toBe(false);
297
+ }
294
298
  expect(result.nextRunAt).toBeNull();
295
299
  } finally {
296
300
  if (origHome === undefined) {