@vellumai/assistant 0.11.6 → 0.11.7-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/environments/package.json +2 -1
  21. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  22. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  30. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  32. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  33. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  34. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  35. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  36. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  37. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  38. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  39. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  40. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  41. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  42. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  43. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  44. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  45. package/openapi.yaml +419 -55
  46. package/package.json +4 -1
  47. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  48. package/src/__tests__/approval-card-edit.test.ts +153 -0
  49. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  50. package/src/__tests__/available-skills.test.ts +54 -0
  51. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  52. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  53. package/src/__tests__/channel-policy.test.ts +1 -3
  54. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  55. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  56. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  57. package/src/__tests__/config-schema.test.ts +2 -3
  58. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  59. package/src/__tests__/conversation-attachments.test.ts +0 -1
  60. package/src/__tests__/conversation-error.test.ts +44 -0
  61. package/src/__tests__/conversation-evictor.test.ts +19 -0
  62. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  63. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  65. package/src/__tests__/conversation-slash.test.ts +9 -0
  66. package/src/__tests__/conversation-title-service.test.ts +97 -0
  67. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  68. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  69. package/src/__tests__/credential-broker.test.ts +43 -420
  70. package/src/__tests__/credential-routes.test.ts +17 -1
  71. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  72. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  73. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  74. package/src/__tests__/discord-channel-config.test.ts +237 -0
  75. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  76. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  77. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  78. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  79. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  80. package/src/__tests__/image-conversion.test.ts +38 -4
  81. package/src/__tests__/install-skill-routing.test.ts +25 -0
  82. package/src/__tests__/llm-resolver.test.ts +74 -0
  83. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  84. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  85. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  86. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  87. package/src/__tests__/mcp-cli.test.ts +23 -1
  88. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  89. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  90. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  91. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  92. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  93. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  94. package/src/__tests__/platform.test.ts +19 -1
  95. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  96. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  97. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  98. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  99. package/src/__tests__/schedule-retry.test.ts +183 -4
  100. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  101. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  102. package/src/__tests__/skill-load-tool.test.ts +34 -0
  103. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  104. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  105. package/src/__tests__/skills.test.ts +25 -0
  106. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  107. package/src/__tests__/terminal-tools.test.ts +2 -0
  108. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  109. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  110. package/src/__tests__/tool-audit.test.ts +111 -40
  111. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  112. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  113. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  114. package/src/__tests__/voice-config-update.test.ts +43 -0
  115. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  116. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  117. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  118. package/src/agent/loop.ts +31 -2
  119. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  120. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  121. package/src/avatar/avatar-manifest.ts +48 -100
  122. package/src/avatar/avatar-store.ts +7 -7
  123. package/src/avatar/ensure-raster.ts +146 -0
  124. package/src/avatar/resvg-lazy.test.ts +0 -1
  125. package/src/avatar/traits-png-sync.ts +9 -10
  126. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  127. package/src/backup/__tests__/paths.test.ts +6 -2
  128. package/src/backup/paths.ts +6 -4
  129. package/src/bundler/app-compiler.ts +2 -2
  130. package/src/bundler/compiler-tools.test.ts +36 -0
  131. package/src/bundler/compiler-tools.ts +54 -18
  132. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +20 -0
  134. package/src/calls/media-stream-stt-session.ts +2 -1
  135. package/src/calls/telephony-synthesis-language.ts +13 -5
  136. package/src/calls/voice-session-bridge.ts +6 -4
  137. package/src/channels/config.ts +23 -12
  138. package/src/cli/AGENTS.md +3 -1
  139. package/src/cli/commands/backup.help.ts +10 -13
  140. package/src/cli/commands/contacts.help.ts +16 -4
  141. package/src/cli/commands/contacts.ts +5 -1
  142. package/src/cli/commands/credentials.help.ts +1 -1
  143. package/src/cli/commands/inference-providers.ts +18 -53
  144. package/src/cli/commands/mcp.help.ts +6 -5
  145. package/src/cli/commands/mcp.ts +1 -1
  146. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  147. package/src/cli/commands/stt.help.ts +2 -2
  148. package/src/config/__tests__/backup-schema.test.ts +1 -1
  149. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  150. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  151. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  152. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  153. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  154. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  155. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  156. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  157. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  158. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  159. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  160. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  161. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  162. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  163. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  164. package/src/config/default-profile-catalog.ts +195 -10
  165. package/src/config/default-profile-names.ts +41 -0
  166. package/src/config/feature-flag-registry.json +18 -2
  167. package/src/config/inference-profile-validation.ts +11 -2
  168. package/src/config/llm-resolver.ts +25 -0
  169. package/src/config/loader.ts +3 -0
  170. package/src/config/managed-speech-defaults.ts +160 -10
  171. package/src/config/schema.ts +2 -0
  172. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  173. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  174. package/src/config/schemas/backup.ts +1 -1
  175. package/src/config/schemas/channels.ts +32 -0
  176. package/src/config/schemas/live-voice.ts +13 -4
  177. package/src/config/schemas/llm.ts +259 -11
  178. package/src/config/schemas/mcp.ts +10 -2
  179. package/src/config/schemas/memory-retrospective.ts +1 -19
  180. package/src/config/schemas/services.ts +24 -0
  181. package/src/config/schemas/stt.ts +128 -5
  182. package/src/config/seed-inference-profiles.ts +32 -3
  183. package/src/config/skill-state.ts +5 -0
  184. package/src/config/skills.ts +21 -0
  185. package/src/contacts/notify-contacts-changed.ts +5 -2
  186. package/src/context/compactor.ts +74 -1
  187. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  188. package/src/daemon/config-watcher.ts +2 -1
  189. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  190. package/src/daemon/conversation-agent-loop.ts +86 -10
  191. package/src/daemon/conversation-error.ts +25 -0
  192. package/src/daemon/conversation-evictor.ts +2 -4
  193. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  194. package/src/daemon/conversation-skill-tools.ts +5 -1
  195. package/src/daemon/conversation-slash.ts +2 -2
  196. package/src/daemon/conversation-store.ts +8 -1
  197. package/src/daemon/conversation-tool-setup.ts +37 -0
  198. package/src/daemon/conversation.ts +0 -4
  199. package/src/daemon/handlers/channel-config-result.ts +30 -0
  200. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  201. package/src/daemon/handlers/config-ingress.ts +9 -0
  202. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  203. package/src/daemon/handlers/config-telegram.ts +2 -5
  204. package/src/daemon/handlers/conversations.ts +0 -2
  205. package/src/daemon/handlers/skills.ts +25 -7
  206. package/src/daemon/lifecycle.ts +3 -1
  207. package/src/daemon/message-types/messages.ts +0 -2
  208. package/src/daemon/tool-setup-types.ts +5 -3
  209. package/src/daemon/trust-context-types.test.ts +33 -0
  210. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  211. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  212. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  213. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  214. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  215. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  216. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  217. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  218. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  219. package/src/live-voice/live-voice-session-manager.ts +39 -2
  220. package/src/live-voice/live-voice-session.ts +213 -8
  221. package/src/live-voice/protocol.ts +187 -1
  222. package/src/messaging/providers/discord/api.ts +2 -1
  223. package/src/messaging/providers/discord/withdraw.ts +62 -0
  224. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  225. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  226. package/src/monitoring/db-integrity-sample.ts +1 -0
  227. package/src/monitoring/file-descriptors.ts +39 -6
  228. package/src/monitoring/process-memory.ts +37 -5
  229. package/src/monitoring/resource-sampler.ts +25 -8
  230. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  231. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  232. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  233. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  234. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  235. package/src/notifications/activity-failed-dedupe.ts +95 -0
  236. package/src/notifications/adapters/discord.ts +132 -0
  237. package/src/notifications/adapters/shared.ts +16 -0
  238. package/src/notifications/adapters/slack.ts +6 -1
  239. package/src/notifications/adapters/telegram.ts +5 -8
  240. package/src/notifications/approval-card-data.ts +94 -1
  241. package/src/notifications/background-failure-signal.ts +116 -0
  242. package/src/notifications/broadcaster.ts +3 -9
  243. package/src/notifications/copy-composer.ts +41 -8
  244. package/src/notifications/decision-engine.ts +55 -0
  245. package/src/notifications/destination-resolver.ts +45 -5
  246. package/src/notifications/emit-signal.ts +19 -5
  247. package/src/notifications/guardian-question-mode.ts +57 -0
  248. package/src/notifications/types.ts +2 -8
  249. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  250. package/src/oauth/seed-providers.ts +261 -0
  251. package/src/permissions/checker.test.ts +21 -0
  252. package/src/permissions/checker.ts +6 -0
  253. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  254. package/src/permissions/gateway-threshold-reader.ts +144 -10
  255. package/src/permissions/prompter.ts +0 -6
  256. package/src/permissions/types.ts +5 -4
  257. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  258. package/src/persistence/bulk-write-gate.ts +9 -10
  259. package/src/persistence/conversation-crud.ts +85 -133
  260. package/src/persistence/conversation-title-service.ts +43 -29
  261. package/src/persistence/embeddings/embedding-local.ts +14 -85
  262. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  263. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  264. package/src/persistence/lifecycle-events-store.ts +75 -5
  265. package/src/platform/platform-patch-queue.test.ts +368 -0
  266. package/src/platform/platform-patch-queue.ts +190 -0
  267. package/src/platform/sync-avatar.test.ts +527 -0
  268. package/src/platform/sync-avatar.ts +209 -0
  269. package/src/platform/sync-identity.test.ts +75 -0
  270. package/src/platform/sync-identity.ts +17 -110
  271. package/src/plugin-api/model-profiles.test.ts +2 -2
  272. package/src/plugin-api/model-profiles.ts +7 -7
  273. package/src/plugin-api/transcription-session.ts +1 -1
  274. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  275. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  276. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  277. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  278. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  279. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  280. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  281. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  282. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  283. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  284. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  285. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  286. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  287. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  288. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  289. package/src/plugins/defaults/memory/worker.ts +7 -0
  290. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  291. package/src/providers/connection-resolution.ts +2 -17
  292. package/src/providers/fallback-breaker.ts +389 -0
  293. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  294. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  295. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  296. package/src/providers/inference/adapter-factory.ts +231 -3
  297. package/src/providers/inference/auth.ts +1 -0
  298. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  299. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  300. package/src/providers/inference/resolve-auth.ts +1 -1
  301. package/src/providers/media-resolve.ts +2 -3
  302. package/src/providers/model-catalog.ts +75 -0
  303. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  304. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  305. package/src/providers/openai/api-error-normalization.ts +13 -0
  306. package/src/providers/openai/chat-completions-provider.ts +84 -0
  307. package/src/providers/opencode/client.test.ts +150 -0
  308. package/src/providers/opencode/client.ts +61 -0
  309. package/src/providers/provider-resolvability.ts +20 -0
  310. package/src/providers/retry.ts +956 -68
  311. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  312. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  313. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  314. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  315. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  316. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  317. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  318. package/src/providers/speech-to-text/resolve.ts +132 -24
  319. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  320. package/src/providers/types.ts +39 -0
  321. package/src/providers/usage-tracking.ts +12 -1
  322. package/src/runtime/AGENTS.md +1 -2
  323. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  324. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  325. package/src/runtime/agent-wake.ts +44 -5
  326. package/src/runtime/background-job-runner.ts +68 -50
  327. package/src/runtime/channel-readiness-service.ts +0 -39
  328. package/src/runtime/http-server.ts +12 -4
  329. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  330. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  331. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  332. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  333. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  334. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  335. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  336. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  337. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  338. package/src/runtime/routes/approval-routes.ts +0 -12
  339. package/src/runtime/routes/avatar-routes.ts +17 -30
  340. package/src/runtime/routes/backup-routes.ts +2 -2
  341. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  342. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  343. package/src/runtime/routes/contact-routes.ts +20 -15
  344. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  345. package/src/runtime/routes/conversation-routes.ts +2 -2
  346. package/src/runtime/routes/credential-routes.ts +2 -1
  347. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  348. package/src/runtime/routes/inbound-message-handler.ts +21 -9
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  350. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  351. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  352. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  353. package/src/runtime/routes/index.ts +2 -0
  354. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  355. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  356. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  357. package/src/runtime/routes/inference-send-routes.ts +2 -2
  358. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  359. package/src/runtime/routes/integrations/discord.ts +119 -0
  360. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  361. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  362. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  363. package/src/runtime/routes/secret-routes.ts +7 -0
  364. package/src/runtime/routes/stt-routes.ts +38 -6
  365. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  366. package/src/runtime/routes/watch-routes.ts +9 -5
  367. package/src/runtime/sync/resource-sync-events.ts +2 -0
  368. package/src/runtime/trust-verdict-consumer.ts +3 -3
  369. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  370. package/src/schedule/retry-policy.ts +8 -8
  371. package/src/schedule/run-script.ts +14 -23
  372. package/src/schedule/schedule-recovery.ts +0 -1
  373. package/src/schedule/scheduler.ts +89 -51
  374. package/src/schedule/worker.ts +57 -0
  375. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  376. package/src/security/oauth2.ts +7 -0
  377. package/src/security/openai-device-auth.ts +364 -0
  378. package/src/skills/available-skills.ts +10 -1
  379. package/src/skills/catalog-install.ts +19 -1
  380. package/src/skills/inline-command-runner.ts +6 -2
  381. package/src/skills/platform-compatibility.test.ts +49 -0
  382. package/src/skills/platform-compatibility.ts +65 -0
  383. package/src/skills/tool-manifest.ts +20 -0
  384. package/src/stt/__tests__/roles.test.ts +152 -0
  385. package/src/stt/daemon-batch-transcriber.ts +4 -1
  386. package/src/stt/roles.ts +180 -0
  387. package/src/stt/stt-stream-session.ts +3 -2
  388. package/src/stt/types.ts +33 -3
  389. package/src/subagent/manager.ts +12 -0
  390. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  391. package/src/telemetry/live-voice-funnel.ts +12 -2
  392. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  393. package/src/telemetry/tool-audit.ts +83 -30
  394. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  395. package/src/telemetry/turn-outcome.ts +75 -4
  396. package/src/tools/client-os.ts +115 -0
  397. package/src/tools/computer-use/definitions.ts +11 -7
  398. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  399. package/src/tools/credentials/broker-types.ts +0 -71
  400. package/src/tools/credentials/broker.ts +6 -201
  401. package/src/tools/executor.ts +2 -8
  402. package/src/tools/host-shell.test.ts +75 -0
  403. package/src/tools/host-shell.ts +24 -0
  404. package/src/tools/host-terminal/host-shell.ts +15 -44
  405. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  406. package/src/tools/permission-checker.ts +73 -21
  407. package/src/tools/policy-context.ts +1 -0
  408. package/src/tools/shared/filesystem/image-read.ts +1 -1
  409. package/src/tools/skills/find-similar-skills.ts +10 -2
  410. package/src/tools/skills/load.ts +41 -6
  411. package/src/tools/skills/sandbox-runner.ts +8 -16
  412. package/src/tools/skills/skill-tool-factory.ts +1 -0
  413. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  414. package/src/tools/terminal/safe-env.ts +39 -9
  415. package/src/tools/terminal/shell.ts +12 -31
  416. package/src/tools/tool-approval-handler.ts +64 -10
  417. package/src/tools/tool-types.ts +10 -0
  418. package/src/tools/types.ts +19 -3
  419. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  420. package/src/tools/workflows/manage-workflows.ts +2 -2
  421. package/src/tools/workflows/run-workflow.test.ts +2 -1
  422. package/src/util/__tests__/file-use.test.ts +61 -0
  423. package/src/util/__tests__/process-table.test.ts +66 -0
  424. package/src/util/__tests__/process-tree.test.ts +46 -0
  425. package/src/util/errors.ts +1 -0
  426. package/src/util/file-use.ts +91 -0
  427. package/src/util/host-process.test.ts +17 -0
  428. package/src/util/host-process.ts +75 -0
  429. package/src/util/image-conversion.ts +39 -7
  430. package/src/util/platform.ts +32 -13
  431. package/src/util/process-table.ts +249 -0
  432. package/src/util/process-tree.ts +85 -129
  433. package/src/util/provider-error-patterns.ts +20 -0
  434. package/src/util/spawn.ts +9 -2
  435. package/src/workspace/git-service.ts +4 -14
  436. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  437. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  438. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  439. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  440. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  441. package/src/workspace/migrations/registry.ts +8 -0
  442. package/src/__tests__/permission-types.test.ts +0 -17
  443. package/src/channels/gateway-discord-admission.ts +0 -32
  444. package/src/persistence/fork-message-copy.ts +0 -239
  445. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  446. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  447. package/src/security/oauth2-device-code.ts +0 -312
@@ -438,7 +438,7 @@ describe("assistant mcp add", () => {
438
438
  expect(stderr).toContain("--url is required");
439
439
  });
440
440
 
441
- test("defaults risk to high", async () => {
441
+ test("sends no risk when --risk is omitted, leaving the config default", async () => {
442
442
  const { exitCode } = await runMcpAdd("default-risk", [
443
443
  "-t",
444
444
  "sse",
@@ -447,6 +447,28 @@ describe("assistant mcp add", () => {
447
447
  ]);
448
448
  expect(exitCode).toBe(0);
449
449
 
450
+ const addCall = mockCliIpcCallFn.mock.calls.find(
451
+ (c) => c[0] === "internal_mcp_add",
452
+ );
453
+ expect(addCall).toBeDefined();
454
+ const body = (addCall![1] as Record<string, unknown>).body as Record<
455
+ string,
456
+ unknown
457
+ >;
458
+ expect(body.risk).toBeUndefined();
459
+ });
460
+
461
+ test("forwards an explicit --risk", async () => {
462
+ const { exitCode } = await runMcpAdd("explicit-risk", [
463
+ "-t",
464
+ "sse",
465
+ "-u",
466
+ "https://example.com/sse",
467
+ "-r",
468
+ "high",
469
+ ]);
470
+ expect(exitCode).toBe(0);
471
+
450
472
  const addCall = mockCliIpcCallFn.mock.calls.find(
451
473
  (c) => c[0] === "internal_mcp_add",
452
474
  );
@@ -40,60 +40,69 @@ function serverConfig(defaultRiskLevel: ServerRisk) {
40
40
  };
41
41
  }
42
42
 
43
- function toolWithAnnotations(readOnlyHint?: boolean) {
43
+ interface RiskAnnotations {
44
+ readOnlyHint?: boolean;
45
+ destructiveHint?: boolean;
46
+ }
47
+
48
+ function toolWithAnnotations(annotations?: RiskAnnotations) {
44
49
  return {
45
50
  name: "get-summary",
46
51
  description: "Read a summary",
47
52
  inputSchema: { type: "object", properties: {} },
48
- ...(readOnlyHint === undefined ? {} : { annotations: { readOnlyHint } }),
53
+ ...(annotations === undefined ? {} : { annotations }),
49
54
  };
50
55
  }
51
56
 
52
- describe("MCP tool risk from readOnlyHint annotations", () => {
57
+ describe("MCP tool risk from annotations", () => {
53
58
  const fakeManager = { callTool: jest.fn() } as never;
54
59
 
55
- test("readOnlyHint true on a medium-risk server lowers risk to Low", () => {
56
- const tool = createMcpTool(
57
- toolWithAnnotations(true),
58
- "fathom",
59
- serverConfig("medium"),
60
+ function riskFor(server: ServerRisk, annotations?: RiskAnnotations) {
61
+ return createMcpTool(
62
+ toolWithAnnotations(annotations),
63
+ "server",
64
+ serverConfig(server),
60
65
  fakeManager,
61
- );
66
+ ).defaultRiskLevel;
67
+ }
62
68
 
63
- expect(tool.defaultRiskLevel).toBe(RiskLevel.Low);
69
+ test("no annotations keeps the server level", () => {
70
+ expect(riskFor("low")).toBe(RiskLevel.Low);
71
+ expect(riskFor("medium")).toBe(RiskLevel.Medium);
72
+ expect(riskFor("high")).toBe(RiskLevel.High);
64
73
  });
65
74
 
66
- test("readOnlyHint true on a high-risk server leaves risk at High", () => {
67
- const tool = createMcpTool(
68
- toolWithAnnotations(true),
69
- "untrusted",
70
- serverConfig("high"),
71
- fakeManager,
72
- );
75
+ test("readOnlyHint steps down from the medium default", () => {
76
+ expect(riskFor("medium", { readOnlyHint: true })).toBe(RiskLevel.Low);
77
+ });
73
78
 
74
- expect(tool.defaultRiskLevel).toBe(RiskLevel.High);
79
+ test("readOnlyHint cannot lower a server the user pinned to high", () => {
80
+ expect(riskFor("high", { readOnlyHint: true })).toBe(RiskLevel.High);
75
81
  });
76
82
 
77
- test("missing annotations keeps the server default risk", () => {
78
- const tool = createMcpTool(
79
- toolWithAnnotations(undefined),
80
- "fathom",
81
- serverConfig("medium"),
82
- fakeManager,
83
- );
83
+ test("readOnlyHint cannot step below low", () => {
84
+ expect(riskFor("low", { readOnlyHint: true })).toBe(RiskLevel.Low);
85
+ });
84
86
 
85
- expect(tool.defaultRiskLevel).toBe(RiskLevel.Medium);
87
+ test("destructiveHint steps up one level", () => {
88
+ expect(riskFor("low", { destructiveHint: true })).toBe(RiskLevel.Medium);
89
+ expect(riskFor("medium", { destructiveHint: true })).toBe(RiskLevel.High);
86
90
  });
87
91
 
88
- test("readOnlyHint false on a low-risk server keeps risk at Low", () => {
89
- const tool = createMcpTool(
90
- toolWithAnnotations(false),
91
- "trusted",
92
- serverConfig("low"),
93
- fakeManager,
94
- );
92
+ test("destructiveHint cannot step above high", () => {
93
+ expect(riskFor("high", { destructiveHint: true })).toBe(RiskLevel.High);
94
+ });
95
+
96
+ test("destructiveHint wins when a server sends both", () => {
97
+ expect(
98
+ riskFor("medium", { readOnlyHint: true, destructiveHint: true }),
99
+ ).toBe(RiskLevel.High);
100
+ });
95
101
 
96
- expect(tool.defaultRiskLevel).toBe(RiskLevel.Low);
102
+ test("hints set to false leave the server level alone", () => {
103
+ expect(
104
+ riskFor("medium", { readOnlyHint: false, destructiveHint: false }),
105
+ ).toBe(RiskLevel.Medium);
97
106
  });
98
107
  });
99
108
 
@@ -774,6 +774,7 @@ describe("MediaStreamSttSession", () => {
774
774
 
775
775
  expect(resolveStreamingTranscriber).toHaveBeenCalledTimes(1);
776
776
  expect(resolveStreamingTranscriber).toHaveBeenCalledWith({
777
+ role: "telephony",
777
778
  sampleRate: 16_000,
778
779
  utteranceBoundaryFinals: true,
779
780
  utteranceEndMs: 1000,
@@ -787,6 +788,7 @@ describe("MediaStreamSttSession", () => {
787
788
  const { session } = await startStreamingSession();
788
789
 
789
790
  expect(resolveStreamingTranscriber).toHaveBeenCalledWith({
791
+ role: "telephony",
790
792
  sampleRate: 16_000,
791
793
  utteranceBoundaryFinals: true,
792
794
  utteranceEndMs: 2500,
@@ -629,3 +629,109 @@ describe("access-request instruction enforcement", () => {
629
629
  expect(decision.renderedCopy.vellum?.body).not.toContain("reject");
630
630
  });
631
631
  });
632
+
633
+ /**
634
+ * A question is the one payload whose copy cannot be composed: the guardian is
635
+ * choosing between the words it carries. The engine replaces title / body /
636
+ * deliveryText wholesale with the model's version, which is how a question
637
+ * reached a channel as a summary of itself with its options missing.
638
+ */
639
+ describe("ask_question copy is pinned to the question", () => {
640
+ const askQuestionSignal = (): NotificationSignal => ({
641
+ signalId: "sig-ask-question-1",
642
+ createdAt: Date.now(),
643
+ sourceChannel: "slack",
644
+ sourceContextId: "conv-1",
645
+ sourceEventName: "guardian.question",
646
+ contextPayload: {
647
+ requestId: "req-ask-1",
648
+ requestCode: "08B619",
649
+ requestKind: "pending_question",
650
+ questionText: "What should I dig into?",
651
+ options: [
652
+ { id: "opt-thread", label: "This Slack thread" },
653
+ { id: "opt-pr", label: "The pull request" },
654
+ ],
655
+ },
656
+ attentionHints: {
657
+ requiresAction: true,
658
+ urgency: "high",
659
+ isAsyncBackground: false,
660
+ visibleInSourceNow: false,
661
+ },
662
+ });
663
+
664
+ beforeEach(() => {
665
+ configuredProvider = { sendMessage: async () => ({ content: [] }) };
666
+ // The tool-use envelope `classifyWithLLM` reads, not the decision fields
667
+ // alone: it takes `toolBlock.input`, and the field names inside must be
668
+ // the ones `validateDecisionOutput` requires. Either mismatch drops the
669
+ // decision to the fallback path, where this suite's own copy is correct
670
+ // and the pin under test never runs.
671
+ extractedToolUse = {
672
+ name: "record_notification_decision",
673
+ input: {
674
+ shouldNotify: true,
675
+ selectedChannels: ["slack"],
676
+ reasoningSummary: "guardian needs to answer",
677
+ dedupeKey: "ask-question-req-ask-1",
678
+ confidence: 0.9,
679
+ renderedCopy: {
680
+ slack: {
681
+ title: "Guardian question",
682
+ body: "Guardian wants to know where to focus",
683
+ deliveryText: "Guardian wants to know where to focus",
684
+ },
685
+ },
686
+ },
687
+ };
688
+ });
689
+
690
+ test("the composed paraphrase does not replace the question", async () => {
691
+ const decision = await evaluateSignal(askQuestionSignal(), [
692
+ "slack",
693
+ ] as NotificationChannel[]);
694
+
695
+ // Guards the fixture itself: a shape the validator rejects would fall
696
+ // back, and the assertions below would then be testing the fallback copy
697
+ // rather than the pin that overrides a composed paraphrase.
698
+ expect(decision.fallbackUsed).toBe(false);
699
+
700
+ const copy = decision.renderedCopy.slack;
701
+ // `deliveryText` is what the Slack adapter sends, ahead of body and title.
702
+ expect(copy?.deliveryText).toContain("What should I dig into?");
703
+ expect(copy?.deliveryText).not.toContain("wants to know where to focus");
704
+ expect(copy?.body).toContain("What should I dig into?");
705
+ });
706
+
707
+ test("the options ride along, numbered as the resolver orders them", async () => {
708
+ const decision = await evaluateSignal(askQuestionSignal(), [
709
+ "slack",
710
+ ] as NotificationChannel[]);
711
+
712
+ expect(decision.fallbackUsed).toBe(false);
713
+ const delivered = decision.renderedCopy.slack?.deliveryText ?? "";
714
+ expect(delivered).toContain("1. This Slack thread");
715
+ expect(delivered).toContain("2. The pull request");
716
+ expect(delivered).toContain("08B619");
717
+ });
718
+
719
+ test("a voice tool approval keeps its composed copy", async () => {
720
+ const signal = askQuestionSignal();
721
+ // A `pending_question` carrying a tool name is an approval, not a
722
+ // question, so composing its copy is correct.
723
+ signal.contextPayload = {
724
+ ...signal.contextPayload,
725
+ toolName: "bash",
726
+ options: undefined,
727
+ };
728
+ const decision = await evaluateSignal(signal, [
729
+ "slack",
730
+ ] as NotificationChannel[]);
731
+
732
+ expect(decision.fallbackUsed).toBe(false);
733
+ expect(decision.renderedCopy.slack?.deliveryText).toBe(
734
+ "Guardian wants to know where to focus",
735
+ );
736
+ });
737
+ });
@@ -0,0 +1,162 @@
1
+ /**
2
+ * DiscordAdapter delivery behavior.
3
+ *
4
+ * The invariant under test: the adapter addresses the *person*. Every send
5
+ * and update resolves the DM channel from the guardian's user snowflake at
6
+ * call time, so a room id stored anywhere can never become a delivery
7
+ * target, and approval notifications carry the typed-command instructions
8
+ * because nothing ingests component presses yet.
9
+ */
10
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ const dmOpens: string[] = [];
13
+ const sendCalls: Array<{ channelId: string; text: string }> = [];
14
+ const editCalls: Array<{ channelId: string; messageId: string; text: string }> =
15
+ [];
16
+
17
+ mock.module("../messaging/providers/discord/api.js", () => ({
18
+ openDiscordDmChannel: async (userId: string) => {
19
+ dmOpens.push(userId);
20
+ return `dm-for-${userId}`;
21
+ },
22
+ }));
23
+
24
+ mock.module("../messaging/providers/discord/send.js", () => ({
25
+ sendDiscordReply: async (target: { channelId: string }, text: string) => {
26
+ sendCalls.push({ channelId: target.channelId, text });
27
+ return { lastMessageId: String(2000 + sendCalls.length) };
28
+ },
29
+ editDiscordMessage: async (
30
+ target: { channelId: string },
31
+ messageId: string,
32
+ text: string,
33
+ ) => {
34
+ editCalls.push({ channelId: target.channelId, messageId, text });
35
+ },
36
+ }));
37
+
38
+ import type {
39
+ ChannelDeliveryPayload,
40
+ ChannelDestination,
41
+ } from "../notifications/types.js";
42
+
43
+ const { DiscordAdapter } = await import("../notifications/adapters/discord.js");
44
+
45
+ const GUARDIAN_USER_ID = "111222333444555666";
46
+
47
+ function makePayload(
48
+ overrides?: Partial<ChannelDeliveryPayload>,
49
+ ): ChannelDeliveryPayload {
50
+ return {
51
+ sourceEventName: "schedule.notify",
52
+ copy: {
53
+ title: "Heads up",
54
+ body: "Your task finished.",
55
+ },
56
+ urgency: "medium",
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ function makeDestination(
62
+ overrides?: Partial<ChannelDestination>,
63
+ ): ChannelDestination {
64
+ return {
65
+ channel: "discord",
66
+ endpoint: GUARDIAN_USER_ID,
67
+ ...overrides,
68
+ };
69
+ }
70
+
71
+ beforeEach(() => {
72
+ dmOpens.length = 0;
73
+ sendCalls.length = 0;
74
+ editCalls.length = 0;
75
+ });
76
+
77
+ describe("DiscordAdapter.send", () => {
78
+ test("resolves the DM channel from the guardian's user snowflake", async () => {
79
+ const adapter = new DiscordAdapter();
80
+ const result = await adapter.send(makePayload(), makeDestination());
81
+
82
+ expect(result.success).toBe(true);
83
+ expect(result.messageId).toBe("2001");
84
+ expect(dmOpens).toEqual([GUARDIAN_USER_ID]);
85
+ expect(sendCalls).toHaveLength(1);
86
+ expect(sendCalls[0].channelId).toBe(`dm-for-${GUARDIAN_USER_ID}`);
87
+ expect(sendCalls[0].text).toContain("Your task finished.");
88
+ });
89
+
90
+ test("approval notifications carry the typed-command instructions", async () => {
91
+ const adapter = new DiscordAdapter();
92
+ const result = await adapter.send(
93
+ makePayload({
94
+ approvalContext: {
95
+ requestId: "req-1",
96
+ actions: [{ id: "approve_once", label: "Approve once" }],
97
+ plainTextFallback: 'Reply "approve" or "reject" to decide.',
98
+ },
99
+ }),
100
+ makeDestination(),
101
+ );
102
+
103
+ expect(result.success).toBe(true);
104
+ expect(sendCalls).toHaveLength(1);
105
+ expect(sendCalls[0].text).toContain(
106
+ 'Reply "approve" or "reject" to decide.',
107
+ );
108
+ });
109
+
110
+ test("a destination without a guardian user id fails without calling the API", async () => {
111
+ const adapter = new DiscordAdapter();
112
+ const result = await adapter.send(
113
+ makePayload(),
114
+ makeDestination({ endpoint: undefined }),
115
+ );
116
+
117
+ expect(result.success).toBe(false);
118
+ expect(dmOpens).toHaveLength(0);
119
+ expect(sendCalls).toHaveLength(0);
120
+ });
121
+ });
122
+
123
+ describe("DiscordAdapter.update", () => {
124
+ test("edits the card through the same person-resolved DM channel", async () => {
125
+ const adapter = new DiscordAdapter();
126
+ const result = await adapter.update(
127
+ {
128
+ deliveryId: "delivery-1",
129
+ destination: GUARDIAN_USER_ID,
130
+ messageId: "2001",
131
+ },
132
+ { body: "This approval request has been resolved." },
133
+ );
134
+
135
+ expect(result.success).toBe(true);
136
+ expect(result.messageId).toBe("2001");
137
+ expect(dmOpens).toEqual([GUARDIAN_USER_ID]);
138
+ expect(editCalls).toEqual([
139
+ {
140
+ channelId: `dm-for-${GUARDIAN_USER_ID}`,
141
+ messageId: "2001",
142
+ text: "This approval request has been resolved.",
143
+ },
144
+ ]);
145
+ });
146
+
147
+ test("a delivery row without a captured message id cannot be updated", async () => {
148
+ const adapter = new DiscordAdapter();
149
+ const result = await adapter.update(
150
+ {
151
+ deliveryId: "delivery-2",
152
+ destination: GUARDIAN_USER_ID,
153
+ messageId: null,
154
+ },
155
+ { body: "resolved" },
156
+ );
157
+
158
+ expect(result.success).toBe(false);
159
+ expect(result.error).toContain("missing_message_id");
160
+ expect(editCalls).toHaveLength(0);
161
+ });
162
+ });
@@ -2,18 +2,16 @@
2
2
  * Regression test for the durable-downscale path of the image-too-large
3
3
  * recovery (JARVIS-1041 review follow-up).
4
4
  *
5
- * When an oversized stored image *can* be shrunk on this host (the common macOS
6
- * path where `sips` is available), `persistUnsendableImageDowngrades` must write
7
- * the downscaled bytes back to the DB — not leave the original in place. The
5
+ * When an oversized stored image can be shrunk,
6
+ * `persistUnsendableImageDowngrades` must write the downscaled bytes back to
7
+ * the DB, not leave the original in place. The
8
8
  * latest tool-result media is intentionally kept in context, so leaving the
9
9
  * full-size block would rehydrate and re-reject on every later turn instead of
10
10
  * durably self-healing the conversation.
11
11
  *
12
- * `optimizeImageForTransport` needs `sips` and a decodable image to actually
13
- * downscale, which is not portable to CI, so it is mocked here to simulate a
14
- * successful shrink. The mock is process-global, so this case lives in its own
15
- * file (the test runner isolates each file in its own process) to avoid
16
- * disturbing the no-op-resize cases in persist-unsendable-image.test.ts.
12
+ * The encoder is mocked here to isolate the durable persistence behavior from
13
+ * image decoding. The mock is process-global, so this case lives in its own
14
+ * file to avoid disturbing the no-op resize cases.
17
15
  */
18
16
 
19
17
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -43,9 +43,9 @@ function resetTables(): void {
43
43
  /**
44
44
  * Build a minimal PNG whose IHDR declares the given dimensions. Only the
45
45
  * 8-byte signature and the width/height fields (read by `parseImageDimensions`)
46
- * need to be correct; the rest is padding. `optimizeImageForTransport` cannot
47
- * downscale this off macOS (no `sips`), so it stays a no-op — exactly the
48
- * host condition that produces an unsendable stored image.
46
+ * need to be correct; the rest is padding. The image is not decodable, so
47
+ * `optimizeImageForTransport` leaves it unchanged and produces the unsendable
48
+ * stored-image condition.
49
49
  */
50
50
  function makePngBase64(width: number, height: number, padBytes = 0): string {
51
51
  const header = Buffer.from(
@@ -1,10 +1,11 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
2
  import { homedir, tmpdir } from "node:os";
3
- import { join } from "node:path";
3
+ import { join, win32 } from "node:path";
4
4
  import { afterAll, afterEach, describe, expect, test } from "bun:test";
5
5
 
6
6
  import {
7
7
  ensureDataDir,
8
+ formatHomeRelativePath,
8
9
  getDataDir,
9
10
  getDbPath,
10
11
  getHistoryPath,
@@ -64,6 +65,23 @@ afterEach(() => {
64
65
  // Workspace helpers resolve under VELLUM_WORKSPACE_DIR when set,
65
66
  // otherwise under ~/.vellum/workspace.
66
67
  describe("path characterization", () => {
68
+ test("formats Windows workspace paths relative to the home directory", () => {
69
+ expect(
70
+ formatHomeRelativePath(
71
+ "C:\\Users\\Alice\\.vellum\\workspace",
72
+ "C:\\Users\\Alice",
73
+ win32,
74
+ ),
75
+ ).toBe("~\\.vellum\\workspace");
76
+ expect(
77
+ formatHomeRelativePath(
78
+ "D:\\Vellum\\workspace",
79
+ "C:\\Users\\Alice",
80
+ win32,
81
+ ),
82
+ ).toBe("D:\\Vellum\\workspace");
83
+ });
84
+
67
85
  test("all path helpers resolve to expected locations", () => {
68
86
  // Without VELLUM_WORKSPACE_DIR override, workspace is under ~/.vellum
69
87
  delete process.env.VELLUM_WORKSPACE_DIR;
@@ -128,7 +128,7 @@ const BASELINE: Record<string, readonly string[]> = {
128
128
  "../../../../daemon/embedding-reconcile.js",
129
129
  "../../../../daemon/trust-context.js",
130
130
  "../../../../daemon/turn-latency-sub-spans.js",
131
- "../../../../notifications/emit-signal.js",
131
+ "../../../../notifications/background-failure-signal.js",
132
132
  "../../../../persistence/checkpoints.js",
133
133
  "../../../../persistence/conversation-types.js",
134
134
  "../../../../persistence/db-connection.js",
@@ -173,6 +173,10 @@ const BASELINE: Record<string, readonly string[]> = {
173
173
  "../../../context/strip-injections.js",
174
174
  "../../../context/token-estimator.js",
175
175
  "../../../conversations/job-handlers/summarization.js",
176
+ // The standalone memory worker hosts real agent conversations, so its
177
+ // entry point starts the same eviction sweep the daemon starts at
178
+ // startup. No plugin-api equivalent.
179
+ "../../../daemon/conversation-evictor.js",
176
180
  "../../../daemon/date-context.js",
177
181
  "../../../daemon/disk-pressure-background-gate.js",
178
182
  "../../../daemon/embedding-reconcile.js",
@@ -164,6 +164,62 @@ describe("UsageTrackingProvider", () => {
164
164
  });
165
165
  });
166
166
 
167
+ test("attributes a fallback-served response to the stamped backup profile", async () => {
168
+ // RetryProvider's fallback escalation stamps `actualProvider` and
169
+ // `actualInferenceProfile` on the response it serves from the backup
170
+ // route. The usage event must follow those stamps, not the original
171
+ // request options, which still carry the failed primary's resolution.
172
+ setLlmConfig({
173
+ profiles: {
174
+ balanced: {
175
+ provider: "openai",
176
+ model: "gpt-5.4-mini",
177
+ },
178
+ "backup-profile": {
179
+ provider: "anthropic",
180
+ model: "claude-sonnet-4-5",
181
+ },
182
+ },
183
+ activeProfile: "balanced",
184
+ callSites: {
185
+ conversationTitle: { profile: "balanced" },
186
+ },
187
+ pricingOverrides: [],
188
+ });
189
+
190
+ const provider = new UsageTrackingProvider(
191
+ makeProvider({
192
+ content: [{ type: "text", text: "Title" }],
193
+ model: "claude-sonnet-4-5",
194
+ actualProvider: "anthropic",
195
+ actualInferenceProfile: "backup-profile",
196
+ usage: {
197
+ inputTokens: 1_000,
198
+ outputTokens: 2_000,
199
+ },
200
+ stopReason: "end_turn",
201
+ }),
202
+ );
203
+
204
+ await provider.sendMessage(
205
+ [{ role: "user", content: [{ type: "text", text: "Summarize" }] }],
206
+ {
207
+ config: {
208
+ callSite: "conversationTitle",
209
+ },
210
+ },
211
+ );
212
+
213
+ const events = listUsageEvents();
214
+ expect(events).toHaveLength(1);
215
+ expect(events[0]).toMatchObject({
216
+ provider: "anthropic",
217
+ model: "claude-sonnet-4-5",
218
+ callSite: "conversationTitle",
219
+ inferenceProfile: "backup-profile",
220
+ });
221
+ });
222
+
167
223
  test("does not record calls without a call site", async () => {
168
224
  const provider = new UsageTrackingProvider(
169
225
  makeProvider({
@@ -34,14 +34,17 @@ const auditCalls = {
34
34
  denied: [] as any[],
35
35
  error: [] as any[],
36
36
  executed: [] as any[],
37
- prompted: [] as string[],
37
+ prompted: [] as any[],
38
+ decided: [] as any[],
38
39
  };
39
40
 
40
41
  mock.module("../telemetry/tool-audit.js", () => ({
41
42
  recordToolDenied: (e: any) => auditCalls.denied.push(e),
42
43
  recordToolError: (e: any) => auditCalls.error.push(e),
43
44
  recordToolExecuted: (e: any) => auditCalls.executed.push(e),
44
- recordToolPermissionPrompted: (n: string) => auditCalls.prompted.push(n),
45
+ recordToolPermissionPrompted: (e: any) => auditCalls.prompted.push(e),
46
+ recordToolPermissionDecided: (e: any, outcome: string) =>
47
+ auditCalls.decided.push({ entry: e, outcome }),
45
48
  }));
46
49
 
47
50
  // ---------------------------------------------------------------------------