@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
@@ -0,0 +1,161 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { activityFailedDedupeKey } from "../activity-failed-dedupe.js";
4
+
5
+ describe("activityFailedDedupeKey", () => {
6
+ test("a provider-scoped code collapses across jobs within one scope", () => {
7
+ const heartbeat = activityFailedDedupeKey({
8
+ jobName: "heartbeat",
9
+ errorKind: "model_provider",
10
+ failureCode: "PROVIDER_BILLING",
11
+ providerScope: "balanced",
12
+ });
13
+ const schedule = activityFailedDedupeKey({
14
+ jobName: "schedule:job-123",
15
+ errorKind: "model_provider",
16
+ failureCode: "PROVIDER_BILLING",
17
+ providerScope: "balanced",
18
+ });
19
+ expect(heartbeat).toBe(schedule);
20
+ expect(heartbeat).toMatch(
21
+ /^activity-failed:cause:PROVIDER_BILLING:balanced:\d{4}-\d{2}-\d{2}$/,
22
+ );
23
+ });
24
+
25
+ test("a provider-scoped code without a scope keys per job", () => {
26
+ // A caller that cannot name the provider route its failure hit must not
27
+ // collapse with other routes' failures; per-job is the safe fallback.
28
+ const key = activityFailedDedupeKey({
29
+ jobName: "job-a",
30
+ errorKind: "model_provider",
31
+ failureCode: "PROVIDER_BILLING",
32
+ });
33
+ expect(key).toMatch(/^activity-failed:job-a:\d{4}-\d{2}-\d{2}$/);
34
+ });
35
+
36
+ test("the same code in different provider scopes stays distinct", () => {
37
+ // Two provider connections can fail for the same class of reason with
38
+ // different remedies; the first notification must not hide the second.
39
+ const profileA = activityFailedDedupeKey({
40
+ jobName: "schedule:job-a",
41
+ errorKind: "model_provider",
42
+ failureCode: "PROVIDER_INVALID_KEY",
43
+ providerScope: "profile-a",
44
+ });
45
+ const profileB = activityFailedDedupeKey({
46
+ jobName: "schedule:job-b",
47
+ errorKind: "model_provider",
48
+ failureCode: "PROVIDER_INVALID_KEY",
49
+ providerScope: "profile-b",
50
+ });
51
+ expect(profileA).not.toBe(profileB);
52
+ expect(profileA).toMatch(
53
+ /^activity-failed:cause:PROVIDER_INVALID_KEY:profile-a:\d{4}-\d{2}-\d{2}$/,
54
+ );
55
+ });
56
+
57
+ test("a workspace-wide code collapses regardless of provider scope", () => {
58
+ const a = activityFailedDedupeKey({
59
+ jobName: "job-a",
60
+ errorKind: "model_provider",
61
+ failureCode: "MANAGED_KEY_INVALID",
62
+ providerScope: "profile-a",
63
+ });
64
+ const b = activityFailedDedupeKey({
65
+ jobName: "job-b",
66
+ errorKind: "model_provider",
67
+ failureCode: "MANAGED_KEY_INVALID",
68
+ providerScope: "profile-b",
69
+ });
70
+ expect(a).toBe(b);
71
+ expect(a).toMatch(
72
+ /^activity-failed:cause:MANAGED_KEY_INVALID:\d{4}-\d{2}-\d{2}$/,
73
+ );
74
+ });
75
+
76
+ test("job-specific failure codes key per job, not on the cause", () => {
77
+ const a = activityFailedDedupeKey({
78
+ jobName: "job-a",
79
+ errorKind: "model_provider",
80
+ failureCode: "CONTEXT_TOO_LARGE",
81
+ });
82
+ const b = activityFailedDedupeKey({
83
+ jobName: "job-b",
84
+ errorKind: "model_provider",
85
+ failureCode: "CONTEXT_TOO_LARGE",
86
+ });
87
+ expect(a).toMatch(/^activity-failed:job-a:\d{4}-\d{2}-\d{2}$/);
88
+ expect(a).not.toBe(b);
89
+ });
90
+
91
+ test("unknown failure codes default to the per-job key", () => {
92
+ const key = activityFailedDedupeKey({
93
+ jobName: "job-a",
94
+ errorKind: "model_provider",
95
+ failureCode: "SOME_FUTURE_CODE",
96
+ });
97
+ expect(key).toMatch(/^activity-failed:job-a:\d{4}-\d{2}-\d{2}$/);
98
+ });
99
+
100
+ test("distinct failure codes stay distinct", () => {
101
+ const billing = activityFailedDedupeKey({
102
+ jobName: "job",
103
+ errorKind: "model_provider",
104
+ failureCode: "PROVIDER_BILLING",
105
+ providerScope: "balanced",
106
+ });
107
+ const key = activityFailedDedupeKey({
108
+ jobName: "job",
109
+ errorKind: "model_provider",
110
+ failureCode: "MANAGED_KEY_INVALID",
111
+ providerScope: "balanced",
112
+ });
113
+ expect(billing).not.toBe(key);
114
+ });
115
+
116
+ test("code-less provider failures share the cause key within one scope", () => {
117
+ const a = activityFailedDedupeKey({
118
+ jobName: "watcher-a",
119
+ errorKind: "model_provider",
120
+ providerScope: "balanced",
121
+ });
122
+ const b = activityFailedDedupeKey({
123
+ jobName: "watcher-b",
124
+ errorKind: "model_provider",
125
+ providerScope: "balanced",
126
+ });
127
+ expect(a).toBe(b);
128
+ expect(a).toMatch(
129
+ /^activity-failed:cause:model_provider:balanced:\d{4}-\d{2}-\d{2}$/,
130
+ );
131
+ const otherScope = activityFailedDedupeKey({
132
+ jobName: "watcher-a",
133
+ errorKind: "model_provider",
134
+ providerScope: "profile-a",
135
+ });
136
+ expect(otherScope).not.toBe(a);
137
+ const scopeless = activityFailedDedupeKey({
138
+ jobName: "watcher-a",
139
+ errorKind: "model_provider",
140
+ });
141
+ expect(scopeless).toMatch(/^activity-failed:watcher-a:\d{4}-\d{2}-\d{2}$/);
142
+ });
143
+
144
+ test("job-specific failures key per job", () => {
145
+ const timeoutA = activityFailedDedupeKey({
146
+ jobName: "job-a",
147
+ errorKind: "timeout",
148
+ });
149
+ const timeoutB = activityFailedDedupeKey({
150
+ jobName: "job-b",
151
+ errorKind: "timeout",
152
+ });
153
+ const exceptionA = activityFailedDedupeKey({
154
+ jobName: "job-a",
155
+ errorKind: "exception",
156
+ });
157
+ expect(timeoutA).toMatch(/^activity-failed:job-a:\d{4}-\d{2}-\d{2}$/);
158
+ expect(timeoutA).not.toBe(timeoutB);
159
+ expect(exceptionA).toMatch(/^activity-failed:job-a:\d{4}-\d{2}-\d{2}$/);
160
+ });
161
+ });
@@ -0,0 +1,107 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ const emitCalls: Array<Record<string, unknown>> = [];
4
+ mock.module("../emit-signal.js", () => ({
5
+ emitNotificationSignal: (params: Record<string, unknown>) => {
6
+ emitCalls.push(params);
7
+ return Promise.resolve({
8
+ signalId: "sig-1",
9
+ deduplicated: false,
10
+ dispatched: true,
11
+ reason: "ok",
12
+ deliveryResults: [],
13
+ pipelineFailed: false,
14
+ });
15
+ },
16
+ }));
17
+
18
+ const { emitBackgroundFailureSignal } =
19
+ await import("../background-failure-signal.js");
20
+
21
+ describe("emitBackgroundFailureSignal", () => {
22
+ beforeEach(() => {
23
+ emitCalls.length = 0;
24
+ });
25
+
26
+ test("carried connection leads the dedupe scope and rides the payload", () => {
27
+ emitBackgroundFailureSignal({
28
+ jobName: "schedule:job-1",
29
+ displayName: "schedule:PR scan",
30
+ sourceChannel: "scheduler",
31
+ sourceContextId: "job-1",
32
+ errorKind: "model_provider",
33
+ errorMessage: "Agent turn failed (PROVIDER_BILLING)",
34
+ failureCode: "PROVIDER_BILLING",
35
+ failureSummary: "You're out of credits.",
36
+ errorCategory: "credits_exhausted",
37
+ connectionName: "conn-a",
38
+ profileName: "balanced",
39
+ fallbackProviderScope: "cost-optimized",
40
+ });
41
+
42
+ expect(emitCalls).toHaveLength(1);
43
+ const emitted = emitCalls[0];
44
+ expect(emitted.sourceEventName).toBe("activity.failed");
45
+ expect(emitted.dedupeKey as string).toMatch(
46
+ /^activity-failed:cause:PROVIDER_BILLING:conn-a:\d{4}-\d{2}-\d{2}$/,
47
+ );
48
+ expect(emitted.contextPayload).toMatchObject({
49
+ jobName: "schedule:PR scan",
50
+ errorKind: "model_provider",
51
+ failureCode: "PROVIDER_BILLING",
52
+ failureSummary: "You're out of credits.",
53
+ errorCategory: "credits_exhausted",
54
+ connectionName: "conn-a",
55
+ });
56
+ });
57
+
58
+ test("carried profile scopes when no connection is carried", () => {
59
+ emitBackgroundFailureSignal({
60
+ jobName: "heartbeat",
61
+ sourceChannel: "assistant_tool",
62
+ sourceContextId: "conv-1",
63
+ errorKind: "model_provider",
64
+ errorMessage: "rate limited",
65
+ failureCode: "PROVIDER_RATE_LIMIT",
66
+ profileName: "balanced",
67
+ fallbackProviderScope: "cost-optimized",
68
+ });
69
+
70
+ expect(emitCalls[0].dedupeKey as string).toMatch(
71
+ /^activity-failed:cause:PROVIDER_RATE_LIMIT:balanced:\d{4}-\d{2}-\d{2}$/,
72
+ );
73
+ });
74
+
75
+ test("recomputed fallback scopes when nothing is carried", () => {
76
+ emitBackgroundFailureSignal({
77
+ jobName: "watcher-a",
78
+ sourceChannel: "assistant_tool",
79
+ sourceContextId: "conv-2",
80
+ errorKind: "model_provider",
81
+ errorMessage: "provider returned 401",
82
+ fallbackProviderScope: "cost-optimized",
83
+ });
84
+
85
+ expect(emitCalls[0].dedupeKey as string).toMatch(
86
+ /^activity-failed:cause:model_provider:cost-optimized:\d{4}-\d{2}-\d{2}$/,
87
+ );
88
+ });
89
+
90
+ test("no scope at all keys per job", () => {
91
+ emitBackgroundFailureSignal({
92
+ jobName: "memory.v2.sweep",
93
+ sourceChannel: "scheduler",
94
+ sourceContextId: "job-9",
95
+ errorKind: "exception",
96
+ errorMessage: "boom",
97
+ });
98
+
99
+ expect(emitCalls[0].dedupeKey as string).toMatch(
100
+ /^activity-failed:memory\.v2\.sweep:\d{4}-\d{2}-\d{2}$/,
101
+ );
102
+ expect(emitCalls[0].contextPayload).toMatchObject({
103
+ jobName: "memory.v2.sweep",
104
+ errorKind: "exception",
105
+ });
106
+ });
107
+ });
@@ -78,6 +78,18 @@ describe("getConnectedChannels gateway connectivity", () => {
78
78
  expect(await getConnectedChannels()).toContain("slack");
79
79
  });
80
80
 
81
+ test("marks discord connected from a verified guardian binding", async () => {
82
+ deliverableChannels = ["discord"];
83
+ gatewayGuardians = [gatewayBinding("discord", "999888777666555444")];
84
+ expect(await getConnectedChannels()).toContain("discord");
85
+ });
86
+
87
+ test("marks discord disconnected when the gateway has no binding", async () => {
88
+ deliverableChannels = ["discord"];
89
+ gatewayGuardians = [];
90
+ expect(await getConnectedChannels()).not.toContain("discord");
91
+ });
92
+
81
93
  test("always reports vellum and platform connected", async () => {
82
94
  deliverableChannels = ["vellum", "platform"];
83
95
  gatewayGuardians = null;
@@ -35,6 +35,89 @@ function makeSignal(
35
35
 
36
36
  const CHANNELS: NotificationChannel[] = ["vellum" as NotificationChannel];
37
37
 
38
+ // ── activity.failed rendering ─────────────────────────────────────────
39
+
40
+ describe("activity.failed copy", () => {
41
+ function failedSignal(
42
+ contextPayload: Record<string, unknown>,
43
+ ): NotificationSignal {
44
+ return makeSignal({
45
+ sourceEventName: "activity.failed",
46
+ contextPayload,
47
+ });
48
+ }
49
+
50
+ test("renders the carried classification message verbatim", () => {
51
+ const copy = composeFallbackCopy(
52
+ failedSignal({
53
+ jobName: "schedule:PR scan",
54
+ errorKind: "model_provider",
55
+ errorMessage: "Agent turn failed (PROVIDER_BILLING)",
56
+ failureSummary:
57
+ "You're out of credits. Add credits in Settings to continue.",
58
+ }),
59
+ CHANNELS,
60
+ );
61
+ expect(copy.vellum?.title).toBe("Background job failed: schedule:PR scan");
62
+ expect(copy.vellum?.body).toBe(
63
+ "You're out of credits. Add credits in Settings to continue.",
64
+ );
65
+ });
66
+
67
+ test("an oversized carried summary is bounded", () => {
68
+ const copy = composeFallbackCopy(
69
+ failedSignal({
70
+ jobName: "job",
71
+ errorKind: "model_provider",
72
+ errorMessage: "irrelevant",
73
+ failureSummary: "x".repeat(400),
74
+ }),
75
+ CHANNELS,
76
+ );
77
+ expect((copy.vellum?.body ?? "").length).toBeLessThanOrEqual(303);
78
+ expect(copy.vellum?.body).toEndWith("...");
79
+ });
80
+
81
+ test("constant-shaped raw detail never reaches the body", () => {
82
+ const copy = composeFallbackCopy(
83
+ failedSignal({
84
+ jobName: "heartbeat",
85
+ errorKind: "model_provider",
86
+ errorMessage: "Agent turn failed (PROVIDER_BILLING)",
87
+ }),
88
+ CHANNELS,
89
+ );
90
+ expect(copy.vellum?.body).toBe("The model provider did not respond.");
91
+ expect(copy.vellum?.body).not.toContain("PROVIDER_BILLING");
92
+ });
93
+
94
+ test("readable raw detail is appended to the kind prose", () => {
95
+ const copy = composeFallbackCopy(
96
+ failedSignal({
97
+ jobName: "watcher",
98
+ errorKind: "exception",
99
+ errorMessage: "The feed endpoint returned an empty document.",
100
+ }),
101
+ CHANNELS,
102
+ );
103
+ expect(copy.vellum?.body).toBe(
104
+ "It stopped with an error. The feed endpoint returned an empty document.",
105
+ );
106
+ });
107
+
108
+ test("timeout renders its own prose", () => {
109
+ const copy = composeFallbackCopy(
110
+ failedSignal({
111
+ jobName: "filing",
112
+ errorKind: "timeout",
113
+ errorMessage: "Background job 'filing' timed out after 1800000ms",
114
+ }),
115
+ CHANNELS,
116
+ );
117
+ expect(copy.vellum?.body).toContain("It ran out of time before finishing.");
118
+ });
119
+ });
120
+
38
121
  // ── composeFallbackCopy with requestedMessage ─────────────────────────
39
122
 
40
123
  describe("composeFallbackCopy honors requestedMessage / requestedTitle", () => {
@@ -79,6 +79,44 @@ describe("resolveDestinations — gateway guardian list", () => {
79
79
  expect(result.has("telegram")).toBe(false);
80
80
  });
81
81
 
82
+ test("discord addresses the guardian's user snowflake, never a room id", () => {
83
+ const list = [
84
+ guardian({
85
+ channelType: "discord",
86
+ address: "111222333444555666",
87
+ externalChatId: "999888777666555444",
88
+ }),
89
+ ];
90
+ const result = resolveDestinations(["discord"], list);
91
+ expect(result.get("discord")).toEqual({
92
+ channel: "discord",
93
+ endpoint: "111222333444555666",
94
+ metadata: { externalUserId: "111222333444555666" },
95
+ bindingContext: {
96
+ sourceChannel: "discord",
97
+ externalChatId: "999888777666555444",
98
+ externalUserId: "111222333444555666",
99
+ },
100
+ });
101
+ });
102
+
103
+ test("discord without a binding conversation still resolves the person", () => {
104
+ const list = [
105
+ guardian({ channelType: "discord", address: "111222333444555666" }),
106
+ ];
107
+ const result = resolveDestinations(["discord"], list);
108
+ expect(result.get("discord")).toEqual({
109
+ channel: "discord",
110
+ endpoint: "111222333444555666",
111
+ metadata: { externalUserId: "111222333444555666" },
112
+ });
113
+ });
114
+
115
+ test("discord with no guardian binding is omitted", () => {
116
+ const result = resolveDestinations(["discord"], []);
117
+ expect(result.has("discord")).toBe(false);
118
+ });
119
+
82
120
  test("slack resolves DM endpoint and binding context", () => {
83
121
  const list = [
84
122
  guardian({
@@ -0,0 +1,95 @@
1
+ import type { ConversationErrorCode } from "../api/events/conversation-error.js";
2
+ import type { BackgroundJobErrorKind } from "../runtime/background-job-runner.js";
3
+
4
+ /**
5
+ * Failure codes whose cause is the whole workspace: the managed route and the
6
+ * local machine exist once, so every job they hit shares one root cause and
7
+ * one remedy, whatever provider or profile the job used. These collapse into
8
+ * a single notification per cause per UTC day.
9
+ *
10
+ * Typed against {@link ConversationErrorCode} so a renamed code fails the
11
+ * build here instead of silently un-collapsing.
12
+ */
13
+ const WORKSPACE_WIDE_FAILURE_CODES = new Set<string>([
14
+ "MANAGED_KEY_INVALID",
15
+ "MANAGED_USAGE_LIMIT",
16
+ "DISK_SPACE_CRITICAL",
17
+ ] satisfies ConversationErrorCode[]);
18
+
19
+ /**
20
+ * Failure codes whose cause is one provider account or connection: a billing
21
+ * lapse, a rejected key, a rate limit, an outage. One workspace can hold
22
+ * several provider connections with independent remedies, so these collapse
23
+ * only within a provider scope (see {@link activityFailedDedupeKey}), never
24
+ * across scopes where the first failure would hide a second one that needs a
25
+ * different fix.
26
+ *
27
+ * Codes in NEITHER set key per job on purpose. A code like
28
+ * `CONTEXT_TOO_LARGE` or `MAX_TOKENS_REACHED` names a defect in one job's own
29
+ * prompt or output, and collapsing it would hide a second job's unrelated
30
+ * failure behind the first's notification. Unknown or future codes default to
31
+ * per-job for the same reason: an extra notification is recoverable, a hidden
32
+ * one is not.
33
+ */
34
+ const PROVIDER_SCOPED_FAILURE_CODES = new Set<string>([
35
+ "PROVIDER_BILLING",
36
+ "PROVIDER_NOT_CONFIGURED",
37
+ "PROVIDER_INVALID_KEY",
38
+ "PROVIDER_RATE_LIMIT",
39
+ "PROVIDER_OVERLOADED",
40
+ "PROVIDER_NETWORK",
41
+ "PROVIDER_API",
42
+ ] satisfies ConversationErrorCode[]);
43
+
44
+ /**
45
+ * Dedupe key for a background-job failure notification.
46
+ *
47
+ * Workspace-wide failures collapse into one notification per cause per UTC
48
+ * day, however many jobs fail. Provider-scoped failures collapse within one
49
+ * provider scope per day, so two provider connections failing for the same
50
+ * class of reason still surface separately (their remedies differ).
51
+ * Job-specific failures (timeouts, generic exceptions, and unlisted codes)
52
+ * keep the per-job key so one job's failure cannot swallow the visibility of
53
+ * another's.
54
+ *
55
+ * `providerScope` is the inference-profile key the failing call actually
56
+ * resolved through, and only when that key names a single provider route
57
+ * (both producers derive it via `resolveSingleRouteProfileKey`; a mix
58
+ * winner has no single route). Callers that cannot name a trustworthy scope
59
+ * omit it, and provider-scoped failures then key per job:
60
+ * an extra notification is recoverable, while a false collapse across two
61
+ * provider routes hides a failure whose remedy differs. The profile is a
62
+ * transitional stand-in for the provider connection the failure actually hit:
63
+ * one profile resolves to one connection at a time, so scoping by profile can
64
+ * split one incident across profiles that share a connection but cannot merge
65
+ * two connections' incidents. Once the turn boundary carries the classified
66
+ * `connectionName` (today it keeps only the code), this scope should become
67
+ * the connection itself.
68
+ *
69
+ * Shared by every `activity.failed` producer that reports a classified
70
+ * background failure (the background-job runner, and the scheduler's
71
+ * retries-exhausted alert), so a schedule exhausting on the same cause as an
72
+ * already-reported job failure does not produce a second notification.
73
+ */
74
+ export function activityFailedDedupeKey(args: {
75
+ jobName: string;
76
+ errorKind: BackgroundJobErrorKind;
77
+ failureCode?: string;
78
+ providerScope?: string;
79
+ }): string {
80
+ const day = new Date().toISOString().slice(0, 10);
81
+ const scope = args.providerScope;
82
+ if (args.failureCode) {
83
+ if (WORKSPACE_WIDE_FAILURE_CODES.has(args.failureCode)) {
84
+ return `activity-failed:cause:${args.failureCode}:${day}`;
85
+ }
86
+ if (PROVIDER_SCOPED_FAILURE_CODES.has(args.failureCode) && scope) {
87
+ return `activity-failed:cause:${args.failureCode}:${scope}:${day}`;
88
+ }
89
+ return `activity-failed:${args.jobName}:${day}`;
90
+ }
91
+ if (args.errorKind === "model_provider" && scope) {
92
+ return `activity-failed:cause:model_provider:${scope}:${day}`;
93
+ }
94
+ return `activity-failed:${args.jobName}:${day}`;
95
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Discord channel adapter: delivers notifications to the guardian's DM by
3
+ * calling the Discord REST API directly.
4
+ *
5
+ * The destination endpoint is the guardian's *user* snowflake, never a stored
6
+ * channel id: user and channel snowflakes are indistinguishable bare numbers,
7
+ * so the adapter resolves the DM channel from the person at send time
8
+ * (cached per user) and a room id on the binding can never become a delivery
9
+ * target. That is Slack's D-prefix DM gate, achieved by construction.
10
+ *
11
+ * Approval notifications carry the typed-command instructions instead of
12
+ * component buttons: nothing ingests INTERACTION_CREATE yet, and a button
13
+ * whose press goes nowhere is a dead control. When interaction ingest lands,
14
+ * buttons arrive here as a rendering upgrade, not a new decision path.
15
+ */
16
+
17
+ import { openDiscordDmChannel } from "../../messaging/providers/discord/api.js";
18
+ import {
19
+ editDiscordMessage,
20
+ sendDiscordReply,
21
+ } from "../../messaging/providers/discord/send.js";
22
+ import { ConfigError } from "../../util/errors.js";
23
+ import { getLogger } from "../../util/logger.js";
24
+ import type {
25
+ ChannelAdapter,
26
+ ChannelDeliveryPayload,
27
+ ChannelDestination,
28
+ ChannelUpdateContext,
29
+ ChannelUpdatePayload,
30
+ DeliveryResult,
31
+ NotificationChannel,
32
+ } from "../types.js";
33
+ import { appendPlainTextFallback, resolveMessageText } from "./shared.js";
34
+
35
+ const log = getLogger("notif-adapter-discord");
36
+
37
+ export class DiscordAdapter implements ChannelAdapter {
38
+ readonly channel: NotificationChannel = "discord";
39
+
40
+ async send(
41
+ payload: ChannelDeliveryPayload,
42
+ destination: ChannelDestination,
43
+ ): Promise<DeliveryResult> {
44
+ const guardianUserId = destination.endpoint;
45
+ if (!guardianUserId) {
46
+ log.warn(
47
+ { sourceEventName: payload.sourceEventName },
48
+ "Discord destination has no guardian user id; skipping",
49
+ );
50
+ return {
51
+ success: false,
52
+ error: "No guardian user id configured for Discord destination",
53
+ };
54
+ }
55
+
56
+ const text = appendPlainTextFallback(
57
+ resolveMessageText(payload),
58
+ payload.approvalContext,
59
+ );
60
+
61
+ try {
62
+ const channelId = await openDiscordDmChannel(guardianUserId);
63
+ const sent = await sendDiscordReply({ channelId }, text);
64
+
65
+ log.info(
66
+ { sourceEventName: payload.sourceEventName, guardianUserId },
67
+ "Discord notification delivered",
68
+ );
69
+
70
+ // The message id lets the delivery row address the card later
71
+ // (in-place withdrawal when the request resolves elsewhere).
72
+ return { success: true, messageId: sent.lastMessageId };
73
+ } catch (err) {
74
+ const message = err instanceof Error ? err.message : String(err);
75
+ // A missing bot token means the operator simply hasn't configured
76
+ // Discord; it is not a code fault, so log it at warn to keep it out
77
+ // of Sentry. Genuine and transient failures stay at error so they
78
+ // remain visible.
79
+ const logFn = err instanceof ConfigError ? log.warn : log.error;
80
+ logFn(
81
+ { err, sourceEventName: payload.sourceEventName, guardianUserId },
82
+ "Failed to deliver Discord notification",
83
+ );
84
+ return { success: false, error: message };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Replace a delivered notification in place, so a card that has been
90
+ * answered or withdrawn stops reading as still waiting.
91
+ */
92
+ async update(
93
+ delivery: ChannelUpdateContext,
94
+ patch: ChannelUpdatePayload,
95
+ ): Promise<DeliveryResult> {
96
+ if (!delivery.messageId) {
97
+ return {
98
+ success: false,
99
+ error:
100
+ "missing_message_id: this delivery has no captured Discord message id",
101
+ };
102
+ }
103
+ const text = patch.body?.trim() || patch.title?.trim();
104
+ if (!text) {
105
+ return { success: false, error: "no body or title supplied for update" };
106
+ }
107
+ try {
108
+ // The delivery row's destination is the guardian's user snowflake, so
109
+ // the edit resolves the same cached DM channel the send used.
110
+ const channelId = await openDiscordDmChannel(delivery.destination);
111
+ await editDiscordMessage({ channelId }, delivery.messageId, text);
112
+ log.info(
113
+ { guardianUserId: delivery.destination, messageId: delivery.messageId },
114
+ "Discord notification updated",
115
+ );
116
+ // An edit keeps the message it addressed, so the delivery row's id
117
+ // still identifies the card.
118
+ return { success: true, messageId: delivery.messageId };
119
+ } catch (err) {
120
+ const message = err instanceof Error ? err.message : String(err);
121
+ log.error(
122
+ {
123
+ err,
124
+ guardianUserId: delivery.destination,
125
+ messageId: delivery.messageId,
126
+ },
127
+ "Failed to update Discord notification",
128
+ );
129
+ return { success: false, error: message };
130
+ }
131
+ }
132
+ }
@@ -33,3 +33,19 @@ export function resolveMessageText(payload: ChannelDeliveryPayload): string {
33
33
 
34
34
  return payload.sourceEventName.replace(/[._]/g, " ");
35
35
  }
36
+
37
+ /**
38
+ * Append an approval's typed-command instructions to the message text, when
39
+ * the copy does not already carry them. Used by adapters delivering an
40
+ * approval without live buttons (Discord always; Telegram when its rich
41
+ * delivery fails), so the guardian still knows how to decide.
42
+ */
43
+ export function appendPlainTextFallback(
44
+ text: string,
45
+ approval: ChannelDeliveryPayload["approvalContext"],
46
+ ): string {
47
+ return approval?.plainTextFallback &&
48
+ !text.includes(approval.plainTextFallback)
49
+ ? `${text}\n\n${approval.plainTextFallback}`
50
+ : text;
51
+ }
@@ -355,7 +355,12 @@ function buildToolApprovalCardBlocks(
355
355
  type: "card",
356
356
  title: {
357
357
  type: "mrkdwn",
358
- text: details ? "Tool Approval" : "Approval Request",
358
+ text:
359
+ approval.intent === "question"
360
+ ? "Question"
361
+ : details
362
+ ? "Tool Approval"
363
+ : "Approval Request",
359
364
  },
360
365
  body: { type: "mrkdwn", text: head },
361
366
  actions: buildCardActions(approval),