@vellumai/assistant 0.11.6 → 0.11.7-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. package/ARCHITECTURE.md +31 -19
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +3 -3
  4. package/docs/flux-turn-detection-spike.md +8 -7
  5. package/docs/runbook-trusted-contacts.md +14 -0
  6. package/docs/stt-provider-onboarding.md +8 -4
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
  9. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
  10. package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
  11. package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
  12. package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
  13. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
  14. package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
  15. package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  19. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  20. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  21. package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
  22. package/node_modules/@vellumai/ces-client/src/index.ts +1 -0
  23. package/node_modules/@vellumai/environments/package.json +2 -1
  24. package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
  25. package/node_modules/@vellumai/environments/src/shell.ts +65 -0
  26. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  27. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  28. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  29. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  30. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  31. package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
  32. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
  33. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
  34. package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -0
  35. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
  36. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -11
  37. package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
  38. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
  39. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
  40. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
  41. package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
  42. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
  43. package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
  44. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
  45. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
  46. package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
  47. package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
  48. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
  49. package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
  50. package/openapi.yaml +419 -55
  51. package/package.json +4 -1
  52. package/scripts/sync-llm-catalog.ts +6 -0
  53. package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
  54. package/src/__tests__/approval-card-edit.test.ts +153 -0
  55. package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
  56. package/src/__tests__/available-skills.test.ts +54 -0
  57. package/src/__tests__/avatar-identity-sync.test.ts +5 -1
  58. package/src/__tests__/catalog-install-normalize.test.ts +9 -1
  59. package/src/__tests__/channel-policy.test.ts +1 -3
  60. package/src/__tests__/channel-readiness-discord.test.ts +9 -70
  61. package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
  62. package/src/__tests__/config-loader-backfill.test.ts +69 -0
  63. package/src/__tests__/config-schema.test.ts +2 -3
  64. package/src/__tests__/conversation-agent-loop.test.ts +238 -0
  65. package/src/__tests__/conversation-attachments.test.ts +0 -1
  66. package/src/__tests__/conversation-error.test.ts +44 -0
  67. package/src/__tests__/conversation-evictor.test.ts +19 -0
  68. package/src/__tests__/conversation-fork-referential.test.ts +73 -63
  69. package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
  70. package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
  71. package/src/__tests__/conversation-slash.test.ts +9 -0
  72. package/src/__tests__/conversation-title-service.test.ts +97 -0
  73. package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
  74. package/src/__tests__/credential-broker-server-use.test.ts +0 -218
  75. package/src/__tests__/credential-broker.test.ts +43 -420
  76. package/src/__tests__/credential-record-write-through.test.ts +78 -0
  77. package/src/__tests__/credential-routes.test.ts +17 -1
  78. package/src/__tests__/credential-security-invariants.test.ts +8 -9
  79. package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
  80. package/src/__tests__/delete-propagation.test.ts +92 -2
  81. package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
  82. package/src/__tests__/discord-channel-config.test.ts +237 -0
  83. package/src/__tests__/edit-propagation.test.ts +42 -2
  84. package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
  85. package/src/__tests__/fallback-breaker.test.ts +1335 -0
  86. package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
  87. package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
  88. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
  89. package/src/__tests__/image-conversion.test.ts +38 -4
  90. package/src/__tests__/install-skill-routing.test.ts +25 -0
  91. package/src/__tests__/llm-catalog-parity.test.ts +4 -0
  92. package/src/__tests__/llm-resolver.test.ts +74 -0
  93. package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
  94. package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
  95. package/src/__tests__/managed-profile-guard.test.ts +28 -0
  96. package/src/__tests__/managed-speech-defaults.test.ts +196 -1
  97. package/src/__tests__/mcp-cli.test.ts +23 -1
  98. package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
  99. package/src/__tests__/media-stream-stt-session.test.ts +2 -0
  100. package/src/__tests__/notification-decision-fallback.test.ts +106 -0
  101. package/src/__tests__/notification-discord-adapter.test.ts +162 -0
  102. package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -0
  103. package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
  104. package/src/__tests__/persist-unsendable-image.test.ts +3 -3
  105. package/src/__tests__/platform.test.ts +19 -1
  106. package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
  107. package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
  108. package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
  109. package/src/__tests__/provider-usage-tracking.test.ts +56 -0
  110. package/src/__tests__/require-fresh-approval.test.ts +5 -2
  111. package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
  112. package/src/__tests__/schedule-retry.test.ts +183 -4
  113. package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
  115. package/src/__tests__/skill-load-tool.test.ts +34 -0
  116. package/src/__tests__/skill-tool-factory.test.ts +6 -1
  117. package/src/__tests__/skill-tool-manifest.test.ts +20 -0
  118. package/src/__tests__/skills.test.ts +25 -0
  119. package/src/__tests__/subagent-manager-notify.test.ts +38 -0
  120. package/src/__tests__/terminal-tools.test.ts +2 -0
  121. package/src/__tests__/tool-approval-handler.test.ts +146 -2
  122. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
  123. package/src/__tests__/tool-audit.test.ts +111 -40
  124. package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
  125. package/src/__tests__/ui-shape-teaching.test.ts +1 -1
  126. package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
  127. package/src/__tests__/voice-config-update.test.ts +43 -0
  128. package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
  129. package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
  130. package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
  131. package/src/agent/loop.ts +31 -2
  132. package/src/approvals/guardian-card-withdrawal.ts +22 -0
  133. package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
  134. package/src/avatar/avatar-manifest.ts +48 -100
  135. package/src/avatar/avatar-store.ts +7 -7
  136. package/src/avatar/ensure-raster.ts +146 -0
  137. package/src/avatar/resvg-lazy.test.ts +0 -1
  138. package/src/avatar/traits-png-sync.ts +9 -10
  139. package/src/background-wake/wake-intent-hooks.test.ts +0 -1
  140. package/src/backup/__tests__/paths.test.ts +6 -2
  141. package/src/backup/paths.ts +6 -4
  142. package/src/bundler/app-compiler.ts +2 -2
  143. package/src/bundler/compiler-tools.test.ts +36 -0
  144. package/src/bundler/compiler-tools.ts +54 -18
  145. package/src/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
  146. package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
  147. package/src/calls/media-stream-stt-session.ts +2 -1
  148. package/src/calls/telephony-synthesis-language.ts +13 -5
  149. package/src/calls/voice-session-bridge.ts +19 -4
  150. package/src/channels/config.ts +23 -12
  151. package/src/cli/AGENTS.md +3 -1
  152. package/src/cli/commands/backup.help.ts +10 -13
  153. package/src/cli/commands/contacts.help.ts +16 -4
  154. package/src/cli/commands/contacts.ts +5 -1
  155. package/src/cli/commands/credentials.help.ts +1 -1
  156. package/src/cli/commands/inference-providers.ts +18 -53
  157. package/src/cli/commands/mcp.help.ts +6 -5
  158. package/src/cli/commands/mcp.ts +1 -1
  159. package/src/cli/commands/memory/memory-retrospective.ts +1 -0
  160. package/src/cli/commands/stt.help.ts +2 -2
  161. package/src/config/__tests__/backup-schema.test.ts +1 -1
  162. package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
  163. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
  164. package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
  165. package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
  166. package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
  167. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
  168. package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
  169. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  170. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
  171. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -1
  172. package/src/config/bundled-skills/settings/TOOLS.json +11 -5
  173. package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
  174. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
  175. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
  176. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
  177. package/src/config/default-profile-catalog.ts +195 -10
  178. package/src/config/default-profile-names.ts +41 -0
  179. package/src/config/feature-flag-registry.json +34 -2
  180. package/src/config/inference-profile-validation.ts +11 -2
  181. package/src/config/llm-resolver.ts +25 -0
  182. package/src/config/loader.ts +3 -0
  183. package/src/config/managed-speech-defaults.ts +160 -10
  184. package/src/config/schema.ts +2 -0
  185. package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
  186. package/src/config/schemas/__tests__/stt.test.ts +94 -0
  187. package/src/config/schemas/backup.ts +1 -1
  188. package/src/config/schemas/channels.ts +32 -0
  189. package/src/config/schemas/live-voice.ts +13 -4
  190. package/src/config/schemas/llm.ts +263 -15
  191. package/src/config/schemas/mcp.ts +10 -2
  192. package/src/config/schemas/memory-retrospective.ts +1 -19
  193. package/src/config/schemas/services.ts +24 -0
  194. package/src/config/schemas/stt.ts +128 -5
  195. package/src/config/seed-inference-profiles.ts +32 -3
  196. package/src/config/skill-state.ts +5 -0
  197. package/src/config/skills.ts +21 -0
  198. package/src/contacts/notify-contacts-changed.ts +5 -2
  199. package/src/context/compactor.ts +74 -1
  200. package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
  201. package/src/daemon/config-watcher.ts +2 -1
  202. package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
  203. package/src/daemon/conversation-agent-loop.ts +86 -10
  204. package/src/daemon/conversation-error.ts +25 -0
  205. package/src/daemon/conversation-evictor.ts +2 -4
  206. package/src/daemon/conversation-runtime-assembly.ts +19 -7
  207. package/src/daemon/conversation-skill-tools.ts +5 -1
  208. package/src/daemon/conversation-slash.ts +2 -2
  209. package/src/daemon/conversation-store.ts +8 -1
  210. package/src/daemon/conversation-tool-setup.ts +37 -0
  211. package/src/daemon/conversation.ts +0 -4
  212. package/src/daemon/handlers/channel-config-result.ts +30 -0
  213. package/src/daemon/handlers/config-discord-channel.ts +301 -0
  214. package/src/daemon/handlers/config-ingress.ts +9 -0
  215. package/src/daemon/handlers/config-slack-channel.ts +13 -15
  216. package/src/daemon/handlers/config-telegram.ts +2 -5
  217. package/src/daemon/handlers/conversations.ts +0 -2
  218. package/src/daemon/handlers/skills.ts +25 -7
  219. package/src/daemon/lifecycle.ts +3 -1
  220. package/src/daemon/message-types/messages.ts +0 -2
  221. package/src/daemon/tool-setup-types.ts +5 -3
  222. package/src/daemon/trust-context-types.test.ts +33 -0
  223. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  224. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
  225. package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
  226. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
  227. package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
  228. package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
  229. package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
  230. package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
  231. package/src/live-voice/__tests__/protocol.test.ts +224 -0
  232. package/src/live-voice/live-voice-credential-preflight.ts +5 -1
  233. package/src/live-voice/live-voice-session-manager.ts +39 -2
  234. package/src/live-voice/live-voice-session.ts +224 -9
  235. package/src/live-voice/protocol.ts +187 -1
  236. package/src/messaging/providers/discord/api.ts +2 -1
  237. package/src/messaging/providers/discord/withdraw.ts +62 -0
  238. package/src/messaging/read-provider-metadata.ts +32 -0
  239. package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
  240. package/src/monitoring/__tests__/process-memory.test.ts +34 -1
  241. package/src/monitoring/db-integrity-sample.ts +1 -0
  242. package/src/monitoring/file-descriptors.ts +39 -6
  243. package/src/monitoring/process-memory.ts +37 -5
  244. package/src/monitoring/resource-sampler.ts +25 -8
  245. package/src/notifications/__tests__/activity-failed-dedupe.test.ts +161 -0
  246. package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
  247. package/src/notifications/__tests__/connected-channels.test.ts +12 -0
  248. package/src/notifications/__tests__/copy-composer.test.ts +83 -0
  249. package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
  250. package/src/notifications/activity-failed-dedupe.ts +95 -0
  251. package/src/notifications/adapters/discord.ts +132 -0
  252. package/src/notifications/adapters/shared.ts +16 -0
  253. package/src/notifications/adapters/slack.ts +6 -1
  254. package/src/notifications/adapters/telegram.ts +5 -8
  255. package/src/notifications/approval-card-data.ts +94 -1
  256. package/src/notifications/background-failure-signal.ts +116 -0
  257. package/src/notifications/broadcaster.ts +3 -9
  258. package/src/notifications/copy-composer.ts +41 -8
  259. package/src/notifications/decision-engine.ts +55 -0
  260. package/src/notifications/destination-resolver.ts +45 -5
  261. package/src/notifications/emit-signal.ts +19 -5
  262. package/src/notifications/guardian-question-mode.ts +57 -0
  263. package/src/notifications/types.ts +2 -8
  264. package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
  265. package/src/oauth/connect-orchestrator.ts +16 -0
  266. package/src/oauth/seed-providers.ts +262 -0
  267. package/src/permissions/checker.test.ts +21 -0
  268. package/src/permissions/checker.ts +6 -0
  269. package/src/permissions/gateway-threshold-reader.test.ts +184 -0
  270. package/src/permissions/gateway-threshold-reader.ts +144 -10
  271. package/src/permissions/prompter.ts +0 -6
  272. package/src/permissions/types.ts +5 -4
  273. package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
  274. package/src/persistence/bulk-write-gate.ts +9 -10
  275. package/src/persistence/conversation-crud.ts +88 -134
  276. package/src/persistence/conversation-title-service.ts +43 -29
  277. package/src/persistence/embeddings/embedding-local.ts +14 -85
  278. package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
  279. package/src/persistence/lifecycle-events-store.test.ts +50 -0
  280. package/src/persistence/lifecycle-events-store.ts +75 -5
  281. package/src/platform/platform-patch-queue.test.ts +368 -0
  282. package/src/platform/platform-patch-queue.ts +190 -0
  283. package/src/platform/sync-avatar.test.ts +527 -0
  284. package/src/platform/sync-avatar.ts +209 -0
  285. package/src/platform/sync-identity.test.ts +75 -0
  286. package/src/platform/sync-identity.ts +17 -110
  287. package/src/plugin-api/model-profiles.test.ts +2 -2
  288. package/src/plugin-api/model-profiles.ts +7 -7
  289. package/src/plugin-api/transcription-session.ts +1 -1
  290. package/src/plugins/defaults/compaction/window-manager.ts +4 -0
  291. package/src/plugins/defaults/image-recovery/recover.ts +2 -2
  292. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +133 -34
  293. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
  294. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
  295. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
  296. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
  297. package/src/plugins/defaults/memory/memory-retrospective-fork-boundary.ts +27 -13
  298. package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
  299. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
  300. package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
  301. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +1 -1
  302. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
  303. package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
  304. package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
  305. package/src/plugins/defaults/memory/worker.ts +7 -0
  306. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  307. package/src/providers/__tests__/retry-callsite.test.ts +55 -0
  308. package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
  309. package/src/providers/connection-resolution.ts +2 -17
  310. package/src/providers/fallback-breaker.ts +389 -0
  311. package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
  312. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
  313. package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
  314. package/src/providers/inference/adapter-factory.ts +246 -3
  315. package/src/providers/inference/auth.ts +15 -14
  316. package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
  317. package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
  318. package/src/providers/inference/resolve-auth.ts +1 -1
  319. package/src/providers/media-resolve.ts +2 -3
  320. package/src/providers/model-catalog.ts +100 -0
  321. package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
  322. package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
  323. package/src/providers/openai/api-error-normalization.ts +13 -0
  324. package/src/providers/openai/chat-completions-provider.ts +84 -0
  325. package/src/providers/opencode/client.test.ts +150 -0
  326. package/src/providers/opencode/client.ts +61 -0
  327. package/src/providers/platform-proxy/constants.ts +5 -0
  328. package/src/providers/provider-resolvability.ts +20 -0
  329. package/src/providers/provider-secret-catalog.ts +3 -2
  330. package/src/providers/retry.ts +956 -68
  331. package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
  332. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
  333. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
  334. package/src/providers/speech-to-text/__tests__/resolve.test.ts +316 -12
  335. package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
  336. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
  337. package/src/providers/speech-to-text/provider-catalog.ts +191 -3
  338. package/src/providers/speech-to-text/resolve.ts +132 -24
  339. package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
  340. package/src/providers/types.ts +39 -0
  341. package/src/providers/usage-tracking.ts +12 -1
  342. package/src/providers/vellum/client.ts +29 -0
  343. package/src/providers/vellum-model-routing.test.ts +2 -0
  344. package/src/providers/vellum-model-routing.ts +3 -3
  345. package/src/runtime/AGENTS.md +1 -2
  346. package/src/runtime/__tests__/agent-wake.test.ts +105 -3
  347. package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
  348. package/src/runtime/agent-wake.ts +44 -5
  349. package/src/runtime/background-job-runner.ts +68 -50
  350. package/src/runtime/channel-readiness-service.ts +0 -39
  351. package/src/runtime/http-server.ts +12 -4
  352. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
  353. package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
  354. package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
  355. package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
  356. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
  357. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
  358. package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
  359. package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
  360. package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
  361. package/src/runtime/routes/approval-routes.ts +0 -12
  362. package/src/runtime/routes/avatar-routes.ts +17 -30
  363. package/src/runtime/routes/backup-routes.ts +2 -2
  364. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
  365. package/src/runtime/routes/contact-prompt-routes.ts +65 -6
  366. package/src/runtime/routes/contact-routes.ts +20 -15
  367. package/src/runtime/routes/conversation-query-routes.ts +98 -10
  368. package/src/runtime/routes/conversation-routes.ts +2 -2
  369. package/src/runtime/routes/credential-routes.ts +2 -1
  370. package/src/runtime/routes/guardian-approval-interception.ts +12 -9
  371. package/src/runtime/routes/inbound-message-handler.ts +47 -48
  372. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
  373. package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
  374. package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
  375. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
  376. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
  377. package/src/runtime/routes/index.ts +2 -0
  378. package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
  379. package/src/runtime/routes/inference-profiles-routes.ts +12 -7
  380. package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
  381. package/src/runtime/routes/inference-send-routes.ts +2 -2
  382. package/src/runtime/routes/ingress-status-routes.ts +2 -2
  383. package/src/runtime/routes/integrations/discord.ts +119 -0
  384. package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
  385. package/src/runtime/routes/mcp-auth-routes.ts +12 -6
  386. package/src/runtime/routes/migration-routes.ts +3 -1
  387. package/src/runtime/routes/oauth-pending-flows.ts +136 -0
  388. package/src/runtime/routes/secret-routes.ts +7 -0
  389. package/src/runtime/routes/stt-routes.ts +38 -6
  390. package/src/runtime/routes/wake-conversation-routes.ts +8 -1
  391. package/src/runtime/routes/watch-routes.ts +9 -5
  392. package/src/runtime/sync/resource-sync-events.ts +2 -0
  393. package/src/runtime/trust-verdict-consumer.ts +3 -3
  394. package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
  395. package/src/schedule/retry-policy.ts +8 -8
  396. package/src/schedule/run-script.ts +14 -23
  397. package/src/schedule/schedule-recovery.ts +0 -1
  398. package/src/schedule/scheduler.ts +89 -51
  399. package/src/schedule/worker.ts +57 -0
  400. package/src/security/__tests__/openai-device-auth.test.ts +355 -0
  401. package/src/security/ces-rpc-record-backend.ts +123 -0
  402. package/src/security/oauth2.ts +7 -0
  403. package/src/security/openai-device-auth.ts +364 -0
  404. package/src/security/secure-keys.ts +15 -0
  405. package/src/skills/available-skills.ts +10 -1
  406. package/src/skills/catalog-install.ts +19 -1
  407. package/src/skills/inline-command-runner.ts +6 -2
  408. package/src/skills/platform-compatibility.test.ts +49 -0
  409. package/src/skills/platform-compatibility.ts +65 -0
  410. package/src/skills/tool-manifest.ts +20 -0
  411. package/src/stt/__tests__/roles.test.ts +152 -0
  412. package/src/stt/daemon-batch-transcriber.ts +4 -1
  413. package/src/stt/roles.ts +180 -0
  414. package/src/stt/stt-stream-session.ts +3 -2
  415. package/src/stt/types.ts +33 -3
  416. package/src/subagent/manager.ts +12 -0
  417. package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
  418. package/src/telemetry/live-voice-funnel.ts +12 -2
  419. package/src/telemetry/telemetry-wire.generated.ts +6 -0
  420. package/src/telemetry/tool-audit.ts +83 -30
  421. package/src/telemetry/tool-executed-events-store.test.ts +0 -1
  422. package/src/telemetry/turn-outcome.ts +75 -4
  423. package/src/tools/client-os.ts +115 -0
  424. package/src/tools/computer-use/definitions.ts +11 -7
  425. package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
  426. package/src/tools/credentials/broker-types.ts +0 -71
  427. package/src/tools/credentials/broker.ts +6 -201
  428. package/src/tools/credentials/metadata-store.ts +65 -16
  429. package/src/tools/credentials/store.ts +2 -0
  430. package/src/tools/executor.ts +2 -8
  431. package/src/tools/host-shell.test.ts +75 -0
  432. package/src/tools/host-shell.ts +24 -0
  433. package/src/tools/host-terminal/host-shell.ts +15 -44
  434. package/src/tools/mcp/mcp-tool-factory.ts +44 -9
  435. package/src/tools/permission-checker.ts +73 -21
  436. package/src/tools/policy-context.ts +1 -0
  437. package/src/tools/shared/filesystem/image-read.ts +1 -1
  438. package/src/tools/skills/find-similar-skills.ts +10 -2
  439. package/src/tools/skills/load.ts +41 -6
  440. package/src/tools/skills/sandbox-runner.ts +8 -16
  441. package/src/tools/skills/skill-tool-factory.ts +1 -0
  442. package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
  443. package/src/tools/terminal/safe-env.ts +39 -9
  444. package/src/tools/terminal/shell.ts +12 -31
  445. package/src/tools/tool-approval-handler.ts +64 -10
  446. package/src/tools/tool-types.ts +10 -0
  447. package/src/tools/types.ts +19 -3
  448. package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
  449. package/src/tools/workflows/manage-workflows.ts +2 -2
  450. package/src/tools/workflows/run-workflow.test.ts +2 -1
  451. package/src/util/__tests__/file-use.test.ts +61 -0
  452. package/src/util/__tests__/process-table.test.ts +66 -0
  453. package/src/util/__tests__/process-tree.test.ts +46 -0
  454. package/src/util/errors.ts +1 -0
  455. package/src/util/file-use.ts +91 -0
  456. package/src/util/host-process.test.ts +17 -0
  457. package/src/util/host-process.ts +75 -0
  458. package/src/util/image-conversion.ts +39 -7
  459. package/src/util/platform.ts +32 -13
  460. package/src/util/process-table.ts +249 -0
  461. package/src/util/process-tree.ts +85 -129
  462. package/src/util/provider-error-patterns.ts +20 -0
  463. package/src/util/spawn.ts +9 -2
  464. package/src/watch/__tests__/watch-retro.test.ts +48 -1
  465. package/src/watch/watch-retro.ts +46 -4
  466. package/src/workspace/git-service.ts +4 -14
  467. package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
  468. package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
  469. package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
  470. package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
  471. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
  472. package/src/workspace/migrations/registry.ts +8 -0
  473. package/src/__tests__/permission-types.test.ts +0 -17
  474. package/src/channels/gateway-discord-admission.ts +0 -32
  475. package/src/persistence/fork-message-copy.ts +0 -239
  476. package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
  477. package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
  478. package/src/security/oauth2-device-code.ts +0 -312
@@ -0,0 +1,144 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { Database } from "bun:sqlite";
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import { repointBackupProfileSelectionsMigration } from "../workspace/migrations/149-repoint-backup-profile-selections.js";
14
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
15
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
16
+
17
+ let workspaceDir: string;
18
+
19
+ function dbPath(): string {
20
+ return join(workspaceDir, "data", "db", "assistant.db");
21
+ }
22
+
23
+ function writeConfig(data: Record<string, unknown>): void {
24
+ writeFileSync(
25
+ join(workspaceDir, "config.json"),
26
+ JSON.stringify(data, null, 2) + "\n",
27
+ );
28
+ }
29
+
30
+ function readConfig(): Record<string, any> {
31
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
32
+ }
33
+
34
+ function seedProfilePinTables(): void {
35
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
36
+ const db = new Database(dbPath());
37
+ db.run(
38
+ "CREATE TABLE conversations (id TEXT PRIMARY KEY, inference_profile TEXT)",
39
+ );
40
+ db.run(
41
+ "CREATE TABLE cron_jobs (id TEXT PRIMARY KEY, inference_profile TEXT)",
42
+ );
43
+ db.query(
44
+ "INSERT INTO conversations (id, inference_profile) VALUES (?, ?)",
45
+ ).run("conv-1", "quality-optimized-backup");
46
+ db.query("INSERT INTO cron_jobs (id, inference_profile) VALUES (?, ?)").run(
47
+ "job-1",
48
+ "balanced-backup",
49
+ );
50
+ db.close();
51
+ }
52
+
53
+ function readPins(table: "conversations" | "cron_jobs"): string[] {
54
+ const db = new Database(dbPath());
55
+ try {
56
+ return (
57
+ db
58
+ .query(`SELECT inference_profile AS profile FROM ${table} ORDER BY id`)
59
+ .all() as Array<{ profile: string }>
60
+ ).map((row) => row.profile);
61
+ } finally {
62
+ db.close();
63
+ }
64
+ }
65
+
66
+ beforeEach(() => {
67
+ workspaceDir = join(
68
+ tmpdir(),
69
+ `vellum-migration-149-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
70
+ );
71
+ mkdirSync(workspaceDir, { recursive: true });
72
+ });
73
+
74
+ afterEach(() => {
75
+ if (existsSync(workspaceDir)) {
76
+ assertNotLiveDb(workspaceDir);
77
+ rmSync(workspaceDir, { recursive: true, force: true });
78
+ }
79
+ });
80
+
81
+ describe("149-repoint-backup-profile-selections migration", () => {
82
+ test("is registered", () => {
83
+ expect(repointBackupProfileSelectionsMigration.id).toBe(
84
+ "149-repoint-backup-profile-selections",
85
+ );
86
+ // The "newest migration" assertion lives with whichever migration is
87
+ // actually last; this one only has to stay in the ordered list.
88
+ expect(WORKSPACE_MIGRATIONS.map((m) => m.id)).toContain(
89
+ "149-repoint-backup-profile-selections",
90
+ );
91
+ });
92
+
93
+ test("repoints direct config selections and preserves fallback metadata", async () => {
94
+ writeConfig({
95
+ llm: {
96
+ activeProfile: "balanced-backup",
97
+ advisorProfile: "quality-optimized-backup",
98
+ profileOrder: ["balanced", "balanced-backup"],
99
+ callSites: {
100
+ mainAgent: { profile: "cost-optimized-backup" },
101
+ },
102
+ profiles: {
103
+ custom: {
104
+ mix: [{ profile: "latency-optimized-backup", weight: 1 }],
105
+ },
106
+ balanced: {
107
+ source: "managed",
108
+ fallbackProfile: "balanced-backup",
109
+ },
110
+ },
111
+ },
112
+ });
113
+
114
+ await repointBackupProfileSelectionsMigration.run(workspaceDir);
115
+
116
+ const llm = readConfig().llm;
117
+ expect(llm.activeProfile).toBe("balanced");
118
+ expect(llm.advisorProfile).toBe("quality-optimized");
119
+ expect(llm.callSites.mainAgent.profile).toBe("cost-optimized");
120
+ expect(llm.profiles.custom.mix[0].profile).toBe("latency-optimized");
121
+ expect(llm.profiles.balanced.fallbackProfile).toBe("balanced-backup");
122
+ expect(llm.profileOrder).toEqual(["balanced", "balanced-backup"]);
123
+ });
124
+
125
+ test("repoints conversation and schedule pins", async () => {
126
+ seedProfilePinTables();
127
+
128
+ await repointBackupProfileSelectionsMigration.run(workspaceDir);
129
+
130
+ expect(readPins("conversations")).toEqual(["quality-optimized"]);
131
+ expect(readPins("cron_jobs")).toEqual(["balanced"]);
132
+ });
133
+
134
+ test("is idempotent and tolerates a missing config or database", async () => {
135
+ await expect(
136
+ repointBackupProfileSelectionsMigration.run(workspaceDir),
137
+ ).resolves.toBeUndefined();
138
+
139
+ writeConfig({ llm: { activeProfile: "balanced" } });
140
+ const before = readConfig();
141
+ await repointBackupProfileSelectionsMigration.run(workspaceDir);
142
+ expect(readConfig()).toEqual(before);
143
+ });
144
+ });
package/src/agent/loop.ts CHANGED
@@ -43,7 +43,10 @@ import type {
43
43
  ToolDefinition,
44
44
  ToolResultContent,
45
45
  } from "../providers/types.js";
46
- import { isContextOverflowError } from "../providers/types.js";
46
+ import {
47
+ isContextOverflowError,
48
+ NATIVE_WEB_SEARCH_TOOL_NAME,
49
+ } from "../providers/types.js";
47
50
  import { getTool } from "../tools/registry.js";
48
51
  import type { SensitiveOutputBinding } from "../tools/sensitive-output-placeholders.js";
49
52
  import {
@@ -133,7 +136,7 @@ export interface AgentLoopConfig {
133
136
  * `input_schema` is informational — the provider supplies the real schema.
134
137
  */
135
138
  const NATIVE_WEB_SEARCH_TOOL: ToolDefinition = {
136
- name: "web_search",
139
+ name: NATIVE_WEB_SEARCH_TOOL_NAME,
137
140
  description:
138
141
  "Search the web for current information to ground your response.",
139
142
  input_schema: {
@@ -369,6 +372,16 @@ export type AgentEvent =
369
372
  cacheReadInputTokens?: number;
370
373
  model: string;
371
374
  actualProvider?: string;
375
+ /**
376
+ * Inference profile that actually governed the call, set only when a
377
+ * wrapper re-routed the request away from the caller's own resolution
378
+ * (`RetryProvider`'s fallback-profile escalation). Travels alongside
379
+ * `actualProvider` so the daemon's usage ledger attributes a degraded
380
+ * serve to the backup profile that answered rather than to the primary
381
+ * that failed. Absent on the normal (non-rerouted) path, where the
382
+ * caller's own resolution is already correct.
383
+ */
384
+ actualInferenceProfile?: string;
372
385
  providerDurationMs: number;
373
386
  rawRequest?: unknown;
374
387
  rawResponse?: unknown;
@@ -1468,6 +1481,14 @@ export class AgentLoop {
1468
1481
  providerConfig.tool_choice = { type: "auto" };
1469
1482
  }
1470
1483
 
1484
+ // Mark the sentinel so a route change downstream can tell it from an
1485
+ // app-executed `web_search` of the same name (see
1486
+ // `SendMessageConfig.nativeWebSearchSentinel`). Only set when true so
1487
+ // the wire config stays byte-identical otherwise.
1488
+ if (attachNativeWebSearch) {
1489
+ providerConfig.nativeWebSearchSentinel = true;
1490
+ }
1491
+
1471
1492
  if (this.config.cacheTtl) {
1472
1493
  providerConfig.cacheTtl = this.config.cacheTtl;
1473
1494
  }
@@ -1809,6 +1830,14 @@ export class AgentLoop {
1809
1830
  cacheReadInputTokens: response.usage.cacheReadInputTokens,
1810
1831
  model: response.model,
1811
1832
  actualProvider: response.actualProvider ?? this.provider.name,
1833
+ // Only present when a wrapper rerouted this call, so the normal
1834
+ // path's event shape stays byte-identical. There is no caller-side
1835
+ // fallback value to fill in: the loop's own profile resolution is
1836
+ // exactly what a reroute invalidates, and the daemon already reads
1837
+ // its own resolution when this is absent.
1838
+ ...(response.actualInferenceProfile !== undefined
1839
+ ? { actualInferenceProfile: response.actualInferenceProfile }
1840
+ : {}),
1812
1841
  providerDurationMs,
1813
1842
  rawRequest: response.rawRequest,
1814
1843
  rawResponse: response.rawResponse,
@@ -31,6 +31,7 @@ import {
31
31
  completeSurfaceAndNotify,
32
32
  markSurfaceCompleted,
33
33
  } from "../daemon/conversation-surfaces.js";
34
+ import { withdrawDiscordApprovalCard } from "../messaging/providers/discord/withdraw.js";
34
35
  import { withdrawSlackApprovalCard } from "../messaging/providers/slack/withdraw.js";
35
36
  import { withdrawTelegramApprovalCard } from "../messaging/providers/telegram-bot/withdraw.js";
36
37
  import { approvalCardSurfaceId } from "../notifications/approval-card-data.js";
@@ -131,6 +132,8 @@ export async function withdrawGuardianRequestCards(
131
132
  decidedAction,
132
133
  hasOriginGuardianReply ?? false,
133
134
  );
135
+ } else if (delivery.destinationChannel === "discord") {
136
+ await withdrawDiscordCard(delivery, status, decidedAction);
134
137
  }
135
138
  // WhatsApp direct delivery can't edit a message in place (it would
136
139
  // post a new one), so its stale clicks are left to the existing
@@ -231,6 +234,25 @@ async function withdrawSlackCard(
231
234
  * outcome. No-ops when the channel-native message id was not captured at
232
235
  * delivery time.
233
236
  */
237
+ async function withdrawDiscordCard(
238
+ delivery: GuardianRequestDeliveryWire,
239
+ status: GuardianRequestStatus,
240
+ decidedAction: ApprovalAction | undefined,
241
+ ): Promise<void> {
242
+ if (!delivery.destinationChatId || !delivery.destinationMessageId) {
243
+ return;
244
+ }
245
+ // The Discord edit rewrites the card's actionable tail to the outcome, so
246
+ // it serves both halves of withdrawal in one call and needs no separate
247
+ // status reply regardless of where the decision originated.
248
+ await withdrawDiscordApprovalCard({
249
+ guardianUserId: delivery.destinationChatId,
250
+ messageId: delivery.destinationMessageId,
251
+ status,
252
+ ...(decidedAction ? { decidedAction } : {}),
253
+ });
254
+ }
255
+
234
256
  async function withdrawTelegramCard(
235
257
  delivery: GuardianRequestDeliveryWire,
236
258
  status: GuardianRequestStatus,
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Tests for ensureAvatarRaster and ensureAvatarRasterPath.
3
+ *
4
+ * Character re-render routes through the native @resvg/resvg-js binding. As in
5
+ * avatar-store.test.ts, the re-render case branches on `isResvgAvailable()` so
6
+ * the suite is deterministic whether or not the binding is installed.
7
+ */
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ mkdtempSync,
12
+ rmSync,
13
+ symlinkSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
19
+
20
+ import {
21
+ ensureAvatarRaster,
22
+ ensureAvatarRasterPath,
23
+ } from "../ensure-raster.js";
24
+ import {
25
+ __resetResvgCacheForTests,
26
+ __setResvgCacheForTests,
27
+ isResvgAvailable,
28
+ } from "../resvg-lazy.js";
29
+
30
+ const VALID_TRAITS = { bodyShape: "blob", eyeStyle: "curious", color: "green" };
31
+
32
+ const IMAGE_FILENAME = "avatar-image.png";
33
+ const MANIFEST_FILENAME = "avatar.json";
34
+ const NATIVE_RENDER_TEST_TIMEOUT_MS = 15_000;
35
+
36
+ const FAKE_PNG = Buffer.from("not-really-a-png");
37
+
38
+ describe("ensureAvatarRaster", () => {
39
+ let workspaceDir: string;
40
+ let avatarDir: string;
41
+ let prevWorkspaceDir: string | undefined;
42
+
43
+ beforeEach(() => {
44
+ workspaceDir = mkdtempSync(join(tmpdir(), "ensure-raster-test-"));
45
+ avatarDir = join(workspaceDir, "data", "avatar");
46
+ mkdirSync(avatarDir, { recursive: true });
47
+ prevWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
48
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
49
+ });
50
+
51
+ afterEach(() => {
52
+ __resetResvgCacheForTests();
53
+ if (prevWorkspaceDir === undefined) {
54
+ delete process.env.VELLUM_WORKSPACE_DIR;
55
+ } else {
56
+ process.env.VELLUM_WORKSPACE_DIR = prevWorkspaceDir;
57
+ }
58
+ rmSync(workspaceDir, { recursive: true, force: true });
59
+ });
60
+
61
+ const writeManifestFile = (manifest: Record<string, unknown>) => {
62
+ writeFileSync(join(avatarDir, MANIFEST_FILENAME), JSON.stringify(manifest));
63
+ };
64
+
65
+ test("returns null for kind none", async () => {
66
+ writeManifestFile({
67
+ kind: "none",
68
+ traits: null,
69
+ source: null,
70
+ image: null,
71
+ });
72
+ expect(await ensureAvatarRaster()).toBeNull();
73
+ });
74
+
75
+ test("returns null with no manifest and no legacy files", async () => {
76
+ expect(await ensureAvatarRaster()).toBeNull();
77
+ });
78
+
79
+ test("returns the existing PNG bytes for an image avatar", async () => {
80
+ writeFileSync(join(avatarDir, IMAGE_FILENAME), FAKE_PNG);
81
+ writeManifestFile({
82
+ kind: "image",
83
+ traits: null,
84
+ source: "upload",
85
+ image: { updatedAt: new Date().toISOString(), etag: "0123456789abcdef" },
86
+ });
87
+ const raster = await ensureAvatarRaster();
88
+ expect(raster).not.toBeNull();
89
+ expect(raster!.equals(FAKE_PNG)).toBe(true);
90
+ });
91
+
92
+ test("returns the existing PNG for a character without re-rendering", async () => {
93
+ writeFileSync(join(avatarDir, IMAGE_FILENAME), FAKE_PNG);
94
+ writeManifestFile({
95
+ kind: "character",
96
+ traits: VALID_TRAITS,
97
+ source: "builder",
98
+ image: null,
99
+ });
100
+ // Force the unavailable path: a re-render attempt would return null.
101
+ __setResvgCacheForTests({ available: false, error: new Error("nope") });
102
+ const raster = await ensureAvatarRaster();
103
+ expect(raster).not.toBeNull();
104
+ expect(raster!.equals(FAKE_PNG)).toBe(true);
105
+ });
106
+
107
+ test("returns null for a symlinked PNG but still reports its path", async () => {
108
+ const outside = join(workspaceDir, "outside.png");
109
+ writeFileSync(outside, FAKE_PNG);
110
+ symlinkSync(outside, join(avatarDir, IMAGE_FILENAME));
111
+ writeManifestFile({
112
+ kind: "image",
113
+ traits: null,
114
+ source: "upload",
115
+ image: { updatedAt: new Date().toISOString(), etag: "0123456789abcdef" },
116
+ });
117
+ expect(await ensureAvatarRasterPath()).toBe(
118
+ join(avatarDir, IMAGE_FILENAME),
119
+ );
120
+ expect(await ensureAvatarRaster()).toBeNull();
121
+ });
122
+
123
+ test("returns null when the avatar dir is a symlink to another workspace", async () => {
124
+ const foreign = mkdtempSync(join(tmpdir(), "foreign-avatar-"));
125
+ writeFileSync(join(foreign, IMAGE_FILENAME), FAKE_PNG);
126
+ rmSync(avatarDir, { recursive: true, force: true });
127
+ symlinkSync(foreign, avatarDir);
128
+ writeManifestFile({
129
+ kind: "image",
130
+ traits: null,
131
+ source: "upload",
132
+ image: { updatedAt: new Date().toISOString(), etag: "0123456789abcdef" },
133
+ });
134
+ expect(await ensureAvatarRaster()).toBeNull();
135
+ rmSync(foreign, { recursive: true, force: true });
136
+ });
137
+
138
+ test("returns null for an image avatar whose PNG is missing", async () => {
139
+ writeManifestFile({
140
+ kind: "image",
141
+ traits: null,
142
+ source: "upload",
143
+ image: { updatedAt: new Date().toISOString(), etag: "0123456789abcdef" },
144
+ });
145
+ expect(await ensureAvatarRaster()).toBeNull();
146
+ expect(existsSync(join(avatarDir, IMAGE_FILENAME))).toBe(false);
147
+ });
148
+
149
+ test("returns null for a character when resvg is unavailable", async () => {
150
+ writeManifestFile({
151
+ kind: "character",
152
+ traits: VALID_TRAITS,
153
+ source: "builder",
154
+ image: null,
155
+ });
156
+ __setResvgCacheForTests({ available: false, error: new Error("nope") });
157
+ expect(await ensureAvatarRaster()).toBeNull();
158
+ expect(existsSync(join(avatarDir, IMAGE_FILENAME))).toBe(false);
159
+ });
160
+
161
+ test(
162
+ "re-renders a character whose PNG is missing",
163
+ async () => {
164
+ writeManifestFile({
165
+ kind: "character",
166
+ traits: VALID_TRAITS,
167
+ source: "builder",
168
+ image: null,
169
+ });
170
+ const raster = await ensureAvatarRaster();
171
+ if (!isResvgAvailable()) {
172
+ expect(raster).toBeNull();
173
+ return;
174
+ }
175
+ expect(raster).not.toBeNull();
176
+ expect(raster!.length).toBeGreaterThan(0);
177
+ // PNG magic bytes
178
+ expect(
179
+ raster!.subarray(0, 4).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47])),
180
+ ).toBe(true);
181
+ expect(existsSync(join(avatarDir, IMAGE_FILENAME))).toBe(true);
182
+ },
183
+ NATIVE_RENDER_TEST_TIMEOUT_MS,
184
+ );
185
+
186
+ test("accepts an explicit state instead of reading the manifest", async () => {
187
+ writeFileSync(join(avatarDir, IMAGE_FILENAME), FAKE_PNG);
188
+ // No manifest on disk; explicit `none` wins over the legacy PNG.
189
+ expect(
190
+ await ensureAvatarRaster({
191
+ kind: "none",
192
+ traits: null,
193
+ source: null,
194
+ image: null,
195
+ }),
196
+ ).toBeNull();
197
+ });
198
+
199
+ describe("ensureAvatarRasterPath", () => {
200
+ test("returns null for kind none", async () => {
201
+ writeFileSync(join(avatarDir, IMAGE_FILENAME), FAKE_PNG);
202
+ writeManifestFile({
203
+ kind: "none",
204
+ traits: null,
205
+ source: null,
206
+ image: null,
207
+ });
208
+ expect(await ensureAvatarRasterPath()).toBeNull();
209
+ });
210
+
211
+ test("returns the path of an existing PNG without reading it", async () => {
212
+ writeFileSync(join(avatarDir, IMAGE_FILENAME), FAKE_PNG);
213
+ writeManifestFile({
214
+ kind: "image",
215
+ traits: null,
216
+ source: "upload",
217
+ image: {
218
+ updatedAt: new Date().toISOString(),
219
+ etag: "0123456789abcdef",
220
+ },
221
+ });
222
+ expect(await ensureAvatarRasterPath()).toBe(
223
+ join(avatarDir, IMAGE_FILENAME),
224
+ );
225
+ });
226
+
227
+ test("returns null for a symlinked PNG but still reports its path", async () => {
228
+ const outside = join(workspaceDir, "outside.png");
229
+ writeFileSync(outside, FAKE_PNG);
230
+ symlinkSync(outside, join(avatarDir, IMAGE_FILENAME));
231
+ writeManifestFile({
232
+ kind: "image",
233
+ traits: null,
234
+ source: "upload",
235
+ image: {
236
+ updatedAt: new Date().toISOString(),
237
+ etag: "0123456789abcdef",
238
+ },
239
+ });
240
+ expect(await ensureAvatarRasterPath()).toBe(
241
+ join(avatarDir, IMAGE_FILENAME),
242
+ );
243
+ expect(await ensureAvatarRaster()).toBeNull();
244
+ });
245
+
246
+ test("returns null for an image avatar whose PNG is missing", async () => {
247
+ writeManifestFile({
248
+ kind: "image",
249
+ traits: null,
250
+ source: "upload",
251
+ image: {
252
+ updatedAt: new Date().toISOString(),
253
+ etag: "0123456789abcdef",
254
+ },
255
+ });
256
+ expect(await ensureAvatarRasterPath()).toBeNull();
257
+ });
258
+
259
+ test("returns null for a character when resvg is unavailable", async () => {
260
+ writeManifestFile({
261
+ kind: "character",
262
+ traits: VALID_TRAITS,
263
+ source: "builder",
264
+ image: null,
265
+ });
266
+ __setResvgCacheForTests({ available: false, error: new Error("nope") });
267
+ expect(await ensureAvatarRasterPath()).toBeNull();
268
+ expect(existsSync(join(avatarDir, IMAGE_FILENAME))).toBe(false);
269
+ });
270
+
271
+ test(
272
+ "re-renders a character whose PNG is missing and returns its path",
273
+ async () => {
274
+ writeManifestFile({
275
+ kind: "character",
276
+ traits: VALID_TRAITS,
277
+ source: "builder",
278
+ image: null,
279
+ });
280
+ const path = await ensureAvatarRasterPath();
281
+ if (!isResvgAvailable()) {
282
+ expect(path).toBeNull();
283
+ return;
284
+ }
285
+ expect(path).toBe(join(avatarDir, IMAGE_FILENAME));
286
+ expect(existsSync(path!)).toBe(true);
287
+ },
288
+ NATIVE_RENDER_TEST_TIMEOUT_MS,
289
+ );
290
+ });
291
+ });