@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
@@ -23,7 +23,7 @@ import type {
23
23
  DeliveryResult,
24
24
  NotificationChannel,
25
25
  } from "../types.js";
26
- import { resolveMessageText } from "./shared.js";
26
+ import { appendPlainTextFallback, resolveMessageText } from "./shared.js";
27
27
 
28
28
  const log = getLogger("notif-adapter-telegram");
29
29
 
@@ -74,13 +74,10 @@ export class TelegramAdapter implements ChannelAdapter {
74
74
 
75
75
  // When falling back from rich delivery, append the plain-text
76
76
  // instructions so the guardian still knows how to approve/reject.
77
- const fallbackText =
78
- approval?.plainTextFallback &&
79
- !messageText.includes(approval.plainTextFallback)
80
- ? `${messageText}\n\n${approval.plainTextFallback}`
81
- : messageText;
82
-
83
- const sent = await sendTelegramReply(chatId, fallbackText);
77
+ const sent = await sendTelegramReply(
78
+ chatId,
79
+ appendPlainTextFallback(messageText, approval),
80
+ );
84
81
 
85
82
  log.info(
86
83
  { sourceEventName: payload.sourceEventName, chatId },
@@ -29,11 +29,13 @@ import {
29
29
  } from "./approval-card-builder.js";
30
30
  import {
31
31
  buildGuardianRequestCodeInstruction,
32
+ buildQuestionDeliveryText,
32
33
  buildToolApprovalSourceView,
33
34
  type GuardianQuestionPayload,
34
35
  type LenientToolApprovalPayload,
35
36
  LenientToolApprovalPayloadSchema,
36
37
  parseGuardianQuestionPayload,
38
+ type PendingQuestionGuardianPayload,
37
39
  resolveGuardianInstructionModeFromFields,
38
40
  resolveGuardianInstructionModeFromPayload,
39
41
  } from "./guardian-question-mode.js";
@@ -122,12 +124,21 @@ export interface ToolApprovalCardData {
122
124
  card: ApprovalCardParams;
123
125
  }
124
126
 
127
+ /** Resolved card data for an `ask_question` prompt awaiting an answer. */
128
+ export interface QuestionCardData {
129
+ kind: "question";
130
+ card: ApprovalCardParams;
131
+ }
132
+
125
133
  /**
126
134
  * Channel-agnostic approval card content, resolved once from `contextPayload`.
127
135
  * The discriminant `kind` lets consumers branch on the approval type without
128
136
  * re-parsing the payload.
129
137
  */
130
- export type ApprovalCardData = AccessRequestCardData | ToolApprovalCardData;
138
+ export type ApprovalCardData =
139
+ | AccessRequestCardData
140
+ | ToolApprovalCardData
141
+ | QuestionCardData;
131
142
 
132
143
  // ── Shared source rendering ──────────────────────────────────────────────────
133
144
 
@@ -335,6 +346,65 @@ function extractToolApprovalCard(
335
346
  };
336
347
  }
337
348
 
349
+ /**
350
+ * Shape a parked `ask_question` prompt into card params.
351
+ *
352
+ * The question itself is the subject, so it leads; the options become the
353
+ * card's actions under the index-based token scheme the resolver maps back to
354
+ * the pending interaction, plus an explicit skip. Channels without buttons get
355
+ * the same options enumerated in `fallbackText`, so the answer is reachable by
356
+ * a request-code reply on a channel that can only carry text.
357
+ */
358
+ function extractQuestionCard(
359
+ p: PendingQuestionGuardianPayload,
360
+ ): ApprovalCardParams {
361
+ const metadata: Array<{ label: string; value: string }> = [];
362
+ const sourceView = buildToolApprovalSourceView(p);
363
+ const sourceRow = sourceMetadataRow(
364
+ nonEmpty(p.sourceChannel),
365
+ sourceView?.chatId,
366
+ sourceView?.isSlackDm ?? false,
367
+ );
368
+ if (sourceRow) {
369
+ metadata.push(sourceRow);
370
+ }
371
+
372
+ const options = p.options ?? [];
373
+ const bodyParts: string[] = [];
374
+ if (options.length > 0) {
375
+ bodyParts.push(
376
+ options
377
+ .map((option, index) => `${index + 1}. ${option.label}`)
378
+ .join("\n"),
379
+ );
380
+ }
381
+ if (sourceView?.permalink) {
382
+ bodyParts.push(viewMessageLine(sourceView.permalink));
383
+ }
384
+
385
+ return {
386
+ // Deliberately the tool-approval prefix rather than one of its own: the
387
+ // withdrawal path recomputes a card's surface id from the request kind,
388
+ // and `pending_question` already maps here. A new prefix would orphan
389
+ // every question card written before it.
390
+ surfaceIdPrefix: TOOL_APPROVAL_SURFACE_PREFIX,
391
+ cardTitle: "Question",
392
+ primaryLine: p.questionText,
393
+ subtitle: "Waiting on your answer",
394
+ body: bodyParts.join("\n\n"),
395
+ metadata,
396
+ requestId: nonEmpty(p.requestId),
397
+ // Empty rather than absent, and empty rather than the answer options.
398
+ // Absent means the generic Approve/Reject pair, which is the wrong verb
399
+ // for a question. The options themselves are not offered here because the
400
+ // in-app surface route resolves only approval actions, so an `answer_`
401
+ // button on this card would be inert; channel buttons come from the
402
+ // broadcaster, whose taps the guardian reply router does resolve.
403
+ actions: [],
404
+ fallbackText: buildQuestionDeliveryText(p),
405
+ };
406
+ }
407
+
338
408
  /**
339
409
  * Resolve a guardian.question payload into tool-approval card params, or
340
410
  * `null` when it does not represent a tool approval.
@@ -343,6 +413,21 @@ function extractToolApprovalCard(
343
413
  * to lenient field extraction so cards still render when optional fields are
344
414
  * absent.
345
415
  */
416
+ function resolveQuestionCard(
417
+ payload: Record<string, unknown>,
418
+ ): ApprovalCardParams | null {
419
+ const parsed = parseGuardianQuestionPayload(payload);
420
+ if (!parsed || parsed.requestKind !== "pending_question") {
421
+ return null;
422
+ }
423
+ // A `pending_question` carrying a tool name is a tool approval wearing the
424
+ // question kind, and the mode table says so; leave it to the approval card.
425
+ if (resolveGuardianInstructionModeFromPayload(parsed).mode !== "answer") {
426
+ return null;
427
+ }
428
+ return extractQuestionCard(parsed);
429
+ }
430
+
346
431
  function resolveToolApprovalCard(
347
432
  payload: Record<string, unknown>,
348
433
  ): ApprovalCardParams | null {
@@ -388,6 +473,14 @@ export function resolveApprovalCardData(
388
473
  }
389
474
 
390
475
  if (sourceEventName === "guardian.question") {
476
+ // Both kinds arrive under this one event, split by instruction mode: a
477
+ // question is asked, an approval is granted, and the tool-approval
478
+ // resolver accepts only `approval`. Ask about the question first, since a
479
+ // `pending_question` carrying a tool name is an approval either way.
480
+ const question = resolveQuestionCard(contextPayload);
481
+ if (question) {
482
+ return { kind: "question", card: question };
483
+ }
391
484
  const card = resolveToolApprovalCard(contextPayload);
392
485
  return card ? { kind: "tool_approval", card } : null;
393
486
  }
@@ -0,0 +1,116 @@
1
+ import type { BackgroundJobErrorKind } from "../runtime/background-job-runner.js";
2
+ import { getLogger } from "../util/logger.js";
3
+ import { activityFailedDedupeKey } from "./activity-failed-dedupe.js";
4
+ import { emitNotificationSignal } from "./emit-signal.js";
5
+ import type { NotificationSourceChannel } from "./signal.js";
6
+
7
+ const log = getLogger("background-failure-signal");
8
+
9
+ /**
10
+ * One background failure, as a producer reports it.
11
+ *
12
+ * The classification fields are the authored attribution carried out of the
13
+ * failing turn (`TurnFailure`): `failureSummary` is the classifier's
14
+ * user-facing message, and `profileName`/`connectionName` name the route the
15
+ * call actually resolved. Producers whose failures never ran a turn (a
16
+ * timeout, a bootstrap crash, a data job's own exception) leave them unset
17
+ * and the signal degrades to the classified kind.
18
+ */
19
+ export interface BackgroundFailureReport {
20
+ /** Stable job identifier for logs and per-job dedupe, e.g. `heartbeat`. */
21
+ jobName: string;
22
+ /** Human-facing job label for notification copy. Defaults to `jobName`. */
23
+ displayName?: string;
24
+ sourceChannel: NotificationSourceChannel;
25
+ /** Conversation or job id the notification links back to. */
26
+ sourceContextId: string;
27
+ errorKind: BackgroundJobErrorKind;
28
+ /** Raw error text, carried for debugging; copy prefers `failureSummary`. */
29
+ errorMessage: string;
30
+ /** Stable classified code (`ConversationErrorCode`), when classified. */
31
+ failureCode?: string;
32
+ /** The classification's authored user-facing message, when carried. */
33
+ failureSummary?: string;
34
+ /** The classification's machine-readable category, when carried. */
35
+ errorCategory?: string;
36
+ /**
37
+ * Connection row the failing call resolved, when carried. The most precise
38
+ * route identity, so it leads the dedupe-scope hierarchy.
39
+ */
40
+ connectionName?: string;
41
+ /** Profile key the failing call resolved, when carried. */
42
+ profileName?: string;
43
+ /**
44
+ * Recomputed single-route profile key (see `resolveSingleRouteProfileKey`).
45
+ * Used only when the failure carries no attribution of its own.
46
+ */
47
+ fallbackProviderScope?: string;
48
+ }
49
+
50
+ /**
51
+ * The single emission seam for background-job failure notifications.
52
+ *
53
+ * Owns the payload shape, the dedupe-key derivation, and the attention hints
54
+ * so every producer (the background-job runner, the scheduler's
55
+ * retries-exhausted alert, standalone data jobs) reports the same way and a
56
+ * new producer cannot invent a divergent one. Fire-and-forget: emission
57
+ * errors are logged, never thrown, because a notification failure must not
58
+ * break the job path that reports it.
59
+ *
60
+ * The dedupe scope is the most precise route identity the failure carries:
61
+ * the resolved connection, else the resolved profile, else the recomputed
62
+ * fallback. Failures carrying less precision can split an incident into an
63
+ * extra notification (never merge two incidents into one), and the split
64
+ * shrinks as more paths carry attribution.
65
+ */
66
+ export function emitBackgroundFailureSignal(
67
+ report: BackgroundFailureReport,
68
+ ): void {
69
+ const providerScope =
70
+ report.connectionName ?? report.profileName ?? report.fallbackProviderScope;
71
+ emitNotificationSignal({
72
+ sourceChannel: report.sourceChannel,
73
+ sourceContextId: report.sourceContextId,
74
+ sourceEventName: "activity.failed",
75
+ dedupeKey: activityFailedDedupeKey({
76
+ jobName: report.jobName,
77
+ errorKind: report.errorKind,
78
+ ...(report.failureCode !== undefined
79
+ ? { failureCode: report.failureCode }
80
+ : {}),
81
+ ...(providerScope !== undefined ? { providerScope } : {}),
82
+ }),
83
+ contextPayload: {
84
+ jobName: report.displayName ?? report.jobName,
85
+ errorMessage: report.errorMessage,
86
+ errorKind: report.errorKind,
87
+ ...(report.failureCode !== undefined
88
+ ? { failureCode: report.failureCode }
89
+ : {}),
90
+ ...(report.failureSummary !== undefined
91
+ ? { failureSummary: report.failureSummary }
92
+ : {}),
93
+ ...(report.errorCategory !== undefined
94
+ ? { errorCategory: report.errorCategory }
95
+ : {}),
96
+ ...(report.connectionName !== undefined
97
+ ? { connectionName: report.connectionName }
98
+ : {}),
99
+ },
100
+ attentionHints: {
101
+ requiresAction: false,
102
+ urgency: "medium",
103
+ isAsyncBackground: true,
104
+ visibleInSourceNow: false,
105
+ },
106
+ }).catch((emitErr) => {
107
+ log.warn(
108
+ {
109
+ err: emitErr instanceof Error ? emitErr.message : String(emitErr),
110
+ jobName: report.jobName,
111
+ sourceContextId: report.sourceContextId,
112
+ },
113
+ "Failed to emit background-failure notification",
114
+ );
115
+ });
116
+ }
@@ -33,11 +33,10 @@ import {
33
33
  } from "./deliveries-store.js";
34
34
  import { resolveDestinations } from "./destination-resolver.js";
35
35
  import {
36
- buildQuestionOptionActionId,
36
+ buildQuestionAnswerActions,
37
37
  buildToolApprovalSourceView,
38
38
  parseGuardianQuestionPayload,
39
39
  parseInteractiveApprovalPayload,
40
- QUESTION_SKIP_ACTION_ID,
41
40
  resolveGuardianInstructionModeFromPayload,
42
41
  type ToolApprovalSourceView,
43
42
  } from "./guardian-question-mode.js";
@@ -188,14 +187,9 @@ function resolveQuestionOptionsContext(
188
187
  return {
189
188
  approval: {
190
189
  requestId,
191
- actions: [
192
- ...options.map((option, index) => ({
193
- id: buildQuestionOptionActionId(index),
194
- label: option.label,
195
- })),
196
- { id: QUESTION_SKIP_ACTION_ID, label: "Skip" },
197
- ],
190
+ actions: buildQuestionAnswerActions(options),
198
191
  plainTextFallback: `Reply "${code} <your answer>".`,
192
+ intent: "question",
199
193
  },
200
194
  };
201
195
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { normalizeTitle } from "../util/short-title.js";
13
+ import { truncate } from "../util/truncate.js";
13
14
  import {
14
15
  accessRequestCardTitle,
15
16
  buildAccessRequestContractText,
@@ -41,6 +42,33 @@ import type { NotificationChannel, RenderedChannelCopy } from "./types.js";
41
42
 
42
43
  type CopyTemplate = (payload: Record<string, unknown>) => RenderedChannelCopy;
43
44
 
45
+ /**
46
+ * Failure prose for an `activity.failed` payload with no authored
47
+ * classification message. The classified kind supplies the readable part,
48
+ * and the raw error text is appended only when it reads as prose: a
49
+ * stack-shaped or constant-shaped message (PROVIDER_BILLING and friends)
50
+ * says nothing to the person reading the notification and never lands in
51
+ * the body.
52
+ */
53
+ function describeUnclassifiedFailure(payload: Record<string, unknown>): string {
54
+ const kind = str(payload.errorKind, "exception");
55
+ const opening =
56
+ kind === "timeout"
57
+ ? "It ran out of time before finishing."
58
+ : kind === "model_provider"
59
+ ? "The model provider did not respond."
60
+ : "It stopped with an error.";
61
+ const raw =
62
+ typeof payload.errorMessage === "string"
63
+ ? payload.errorMessage.replace(/\s+/g, " ").trim()
64
+ : "";
65
+ const readable =
66
+ raw.length > 0 &&
67
+ raw.length <= 200 &&
68
+ !/[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+/.test(raw);
69
+ return readable ? `${opening} ${raw}` : opening;
70
+ }
71
+
44
72
  function str(value: unknown, fallback: string): string {
45
73
  if (typeof value === "string" && value.length > 0) {
46
74
  return value;
@@ -335,16 +363,21 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
335
363
 
336
364
  "activity.failed": (payload) => {
337
365
  const jobName = str(payload.jobName, "background job");
338
- const errorKind = str(payload.errorKind, "exception");
339
- const rawMessage =
340
- typeof payload.errorMessage === "string"
341
- ? payload.errorMessage
342
- : "no message";
343
- const truncated =
344
- rawMessage.length > 200 ? rawMessage.slice(0, 200) + "…" : rawMessage;
366
+ // The classifier's authored user-facing message rides the payload as
367
+ // `failureSummary` when the failing turn carried its classification;
368
+ // it already says what happened and what to do about it.
369
+ // Authored messages are short by construction, but the classifier's
370
+ // fallback branches can embed provider error text, so bound the body
371
+ // like any other untrusted payload string.
372
+ const summary = truncate(
373
+ typeof payload.failureSummary === "string"
374
+ ? payload.failureSummary.trim()
375
+ : "",
376
+ 300,
377
+ );
345
378
  return {
346
379
  title: `Background job failed: ${jobName}`,
347
- body: `${errorKind}: ${truncated}`,
380
+ body: summary !== "" ? summary : describeUnclassifiedFailure(payload),
348
381
  };
349
382
  },
350
383
 
@@ -48,8 +48,11 @@ import { composeFallbackCopy, resolveTitle } from "./copy-composer.js";
48
48
  import { createDecision } from "./decisions-store.js";
49
49
  import {
50
50
  buildGuardianRequestCodeInstruction,
51
+ buildQuestionDeliveryText,
51
52
  hasGuardianRequestCodeInstruction,
53
+ parseGuardianQuestionPayload,
52
54
  parseInteractiveApprovalPayload,
55
+ resolveGuardianInstructionModeFromPayload,
53
56
  resolveGuardianQuestionInstructionMode,
54
57
  stripConflictingGuardianRequestInstructions,
55
58
  stripGuardianRequestCodeInstructions,
@@ -744,6 +747,55 @@ function ensureSeedContentBlocks(
744
747
  };
745
748
  }
746
749
 
750
+ /**
751
+ * Pin a question's channel text to the question itself.
752
+ *
753
+ * Every other notification's copy is composed here, and for most of them a
754
+ * paraphrase is an improvement. A question is the exception: the guardian is
755
+ * being asked to choose between these words, and the composed copy replaces
756
+ * `title` / `body` / `deliveryText` wholesale, so a summary of the question
757
+ * arrives where the question should be and the options never appear at all.
758
+ * Runs on every decision path for the same reason the seed-block guard does.
759
+ *
760
+ * Scoped to answer mode. A `pending_question` carrying a tool name is a voice
761
+ * tool approval, where composed copy is the right thing.
762
+ */
763
+ function pinQuestionDeliveryCopy(
764
+ decision: NotificationDecision,
765
+ signal: NotificationSignal,
766
+ ): NotificationDecision {
767
+ if (signal.sourceEventName !== "guardian.question") {
768
+ return decision;
769
+ }
770
+ const parsed = parseGuardianQuestionPayload(signal.contextPayload);
771
+ if (
772
+ !parsed ||
773
+ parsed.requestKind !== "pending_question" ||
774
+ resolveGuardianInstructionModeFromPayload(parsed).mode !== "answer"
775
+ ) {
776
+ return decision;
777
+ }
778
+
779
+ const text = buildQuestionDeliveryText(parsed);
780
+ const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
781
+ ...decision.renderedCopy,
782
+ };
783
+ for (const channel of Object.keys(nextCopy) as NotificationChannel[]) {
784
+ const copy = nextCopy[channel];
785
+ if (!copy) {
786
+ continue;
787
+ }
788
+ nextCopy[channel] = {
789
+ ...copy,
790
+ body: text,
791
+ deliveryText: text,
792
+ conversationSeedMessage: text,
793
+ };
794
+ }
795
+
796
+ return { ...decision, renderedCopy: nextCopy };
797
+ }
798
+
747
799
  /**
748
800
  * Tool-approval and tool-grant notifications need a Surface card with
749
801
  * Approve/Reject buttons on all decision paths.
@@ -943,6 +995,7 @@ function buildPassThroughDecision(params: {
943
995
  };
944
996
  decision = enforceGuardianRequestCode(decision, signal);
945
997
  decision = enforceToolApprovalSeedBlocks(decision, signal);
998
+ decision = pinQuestionDeliveryCopy(decision, signal);
946
999
  decision = enforceAccessRequestInstructions(decision, signal);
947
1000
  decision = enforceGuardianRequestConversationAffinity(decision, signal);
948
1001
  decision = enforceConversationAffinity(
@@ -1068,6 +1121,7 @@ export async function evaluateSignal(
1068
1121
  let decision = buildFallbackDecision(signal, availableChannels);
1069
1122
  decision = enforceGuardianRequestCode(decision, signal);
1070
1123
  decision = enforceToolApprovalSeedBlocks(decision, signal);
1124
+ decision = pinQuestionDeliveryCopy(decision, signal);
1071
1125
  decision = enforceAccessRequestInstructions(decision, signal);
1072
1126
  decision = enforceGuardianRequestConversationAffinity(decision, signal);
1073
1127
  decision = enforceConversationAffinity(
@@ -1098,6 +1152,7 @@ export async function evaluateSignal(
1098
1152
 
1099
1153
  decision = enforceGuardianRequestCode(decision, signal);
1100
1154
  decision = enforceToolApprovalSeedBlocks(decision, signal);
1155
+ decision = pinQuestionDeliveryCopy(decision, signal);
1101
1156
  decision = enforceAccessRequestInstructions(decision, signal);
1102
1157
  decision = enforceGuardianRequestConversationAffinity(decision, signal);
1103
1158
  decision = enforceConversationAffinity(
@@ -66,10 +66,9 @@ export function resolveDestinations(
66
66
  continue;
67
67
  }
68
68
 
69
- // After the deliverability check, `channel` is guaranteed to be a
70
- // NotificationChannel TypeScript cannot infer this from the runtime
71
- // guard, so we narrow with a switch over known deliverable values.
72
- switch (channel as NotificationChannel) {
69
+ // `isNotificationDeliverable` is a type predicate, so past this point
70
+ // `channel` is a NotificationChannel and the switch below is exhaustive.
71
+ switch (channel) {
73
72
  case "vellum": {
74
73
  // Vellum delivery is local — no external endpoint required.
75
74
  // Include the guardianPrincipalId so the adapter can annotate
@@ -155,6 +154,44 @@ export function resolveDestinations(
155
154
  );
156
155
  break;
157
156
  }
157
+ case "discord": {
158
+ const guardian = resolveGuardian(guardians, "discord");
159
+ // The endpoint is the guardian's user snowflake: the person, never a
160
+ // stored room id. User and channel snowflakes are indistinguishable
161
+ // bare numbers, so the adapter resolves the DM channel from the
162
+ // person at send time and a guild room can never become a delivery
163
+ // target. The binding context reuses the conversation the guardian
164
+ // verified in, which the verification intercept scopes to the DM
165
+ // lane; delivery stays DM-safe regardless, since it never reads
166
+ // this id.
167
+ if (guardian?.address) {
168
+ result.set("discord", {
169
+ channel: "discord",
170
+ endpoint: guardian.address,
171
+ metadata: {
172
+ externalUserId: guardian.address,
173
+ },
174
+ ...(guardian.externalChatId
175
+ ? {
176
+ bindingContext: {
177
+ sourceChannel: "discord" as const,
178
+ externalChatId: guardian.externalChatId,
179
+ externalUserId: guardian.address,
180
+ },
181
+ }
182
+ : {}),
183
+ });
184
+ }
185
+ log.debug(
186
+ {
187
+ channel: "discord",
188
+ source: "guardian-delivery",
189
+ hasEndpoint: !!guardian?.address,
190
+ },
191
+ "destination resolved",
192
+ );
193
+ break;
194
+ }
158
195
  case "platform": {
159
196
  // Platform delivery goes through the daemon's VellumPlatformClient —
160
197
  // no external binding needed. Include guardianPrincipalId so the
@@ -180,7 +217,10 @@ export function resolveDestinations(
180
217
  break;
181
218
  }
182
219
  default: {
183
- // Future deliverable channels without a resolver skip silently.
220
+ // Exhaustive over NotificationChannel: a channel whose policy turns
221
+ // deliveryEnabled on must answer addressing here at compile time,
222
+ // because a silent skip is how a half-wired channel disappears.
223
+ channel satisfies never;
184
224
  break;
185
225
  }
186
226
  }
@@ -22,6 +22,7 @@ import { isPlatformClientConfigured } from "../platform/client.js";
22
22
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
23
23
  import { getLogger } from "../util/logger.js";
24
24
  import { withSqliteRetry } from "../util/sqlite-retry.js";
25
+ import { DiscordAdapter } from "./adapters/discord.js";
25
26
  import { VellumAdapter } from "./adapters/macos.js";
26
27
  import { PlatformPushAdapter } from "./adapters/platform.js";
27
28
  import { SlackAdapter } from "./adapters/slack.js";
@@ -64,6 +65,7 @@ export function getBroadcaster(): NotificationBroadcaster {
64
65
  new VellumAdapter(broadcastMessage),
65
66
  new TelegramAdapter(),
66
67
  new SlackAdapter(),
68
+ new DiscordAdapter(),
67
69
  new PlatformPushAdapter(),
68
70
  ]);
69
71
 
@@ -117,9 +119,6 @@ export async function getConnectedChannels(): Promise<NotificationChannel[]> {
117
119
  // unreachable, so binding-based connectivity falls back to the local read.
118
120
  const guardians = await getGuardianDelivery();
119
121
 
120
- // getDeliverableChannels() returns ChannelId[] but every returned channel
121
- // has deliveryEnabled: true, making it a valid NotificationChannel at
122
- // runtime. We iterate over the broad type and narrow via the switch.
123
122
  for (const channel of getDeliverableChannels()) {
124
123
  switch (channel) {
125
124
  case "vellum":
@@ -153,9 +152,24 @@ export async function getConnectedChannels(): Promise<NotificationChannel[]> {
153
152
  }
154
153
  break;
155
154
  }
155
+ case "discord": {
156
+ // Connected when a verified guardian binding names a user to DM,
157
+ // mirroring destination-resolver: the person is the endpoint, and
158
+ // the adapter resolves the DM channel from them at send time.
159
+ const guardian = guardians
160
+ ? guardianForChannel(guardians, channel)
161
+ : undefined;
162
+ if (guardian?.address) {
163
+ channels.push(channel);
164
+ }
165
+ break;
166
+ }
156
167
  default:
157
- // Future deliverable channels skip until a connectivity check
158
- // is implemented for them.
168
+ // Exhaustive over NotificationChannel: a channel whose policy turns
169
+ // deliveryEnabled on must answer connectivity here at compile time.
170
+ // A silent skip is how a half-wired channel disappears without a
171
+ // trace, so the compiler holds the door instead.
172
+ channel satisfies never;
159
173
  break;
160
174
  }
161
175
  }
@@ -250,6 +250,63 @@ export function buildQuestionOptionActionId(index: number): string {
250
250
  return `answer_${index}`;
251
251
  }
252
252
 
253
+ /**
254
+ * The full answer action set for a question's options: one action per option
255
+ * in the interaction's own order, then an explicit skip.
256
+ *
257
+ * Shared because a question is rendered twice from the same options, once as
258
+ * an approval-metadata action set for a channel transport and once as card
259
+ * actions on the in-app surface, and the two must agree: the resolver maps
260
+ * the index back to the pending interaction, so a divergence answers the
261
+ * wrong option rather than failing.
262
+ */
263
+ /**
264
+ * The message text for a question that must be answered: the question, its
265
+ * options numbered as they are ordered, and how to reply by reference code.
266
+ *
267
+ * Deterministic on purpose. A notification's channel copy is otherwise
268
+ * composed by the decision engine, and a question is the one payload that
269
+ * cannot survive being paraphrased: the guardian is being asked to choose
270
+ * between these words, so the words have to arrive.
271
+ */
272
+ export function buildQuestionDeliveryText(p: {
273
+ questionText: string;
274
+ requestCode?: string;
275
+ options?: readonly { label: string }[];
276
+ }): string {
277
+ const parts = [p.questionText];
278
+ const options = p.options ?? [];
279
+ if (options.length > 0) {
280
+ parts.push(
281
+ options
282
+ .map((option, index) => `${index + 1}. ${option.label}`)
283
+ .join("\n"),
284
+ );
285
+ }
286
+ const requestCode = p.requestCode?.trim();
287
+ if (requestCode) {
288
+ parts.push(
289
+ buildGuardianRequestCodeInstruction(requestCode.toUpperCase(), "answer"),
290
+ );
291
+ }
292
+ return parts.join("\n\n");
293
+ }
294
+
295
+ export function buildQuestionAnswerActions(
296
+ options: readonly { label: string }[],
297
+ ): Array<{ id: string; label: string }> {
298
+ if (options.length === 0) {
299
+ return [];
300
+ }
301
+ return [
302
+ ...options.map((option, index) => ({
303
+ id: buildQuestionOptionActionId(index),
304
+ label: option.label,
305
+ })),
306
+ { id: QUESTION_SKIP_ACTION_ID, label: "Skip" },
307
+ ];
308
+ }
309
+
253
310
  export type QuestionAnswerSelection =
254
311
  | { kind: "option"; index: number }
255
312
  | { kind: "skip" };