@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
@@ -17,66 +17,26 @@ import {
17
17
  } from "node:fs";
18
18
  import { join } from "node:path";
19
19
 
20
- import { getLogger } from "../util/logger.js";
21
20
  import {
22
21
  AVATAR_IMAGE_FILENAME,
23
22
  AVATAR_MANIFEST_FILENAME,
24
- getAvatarDir,
25
- } from "../util/platform.js";
26
- import { type CharacterTraits, TRAITS_FILENAME } from "./traits-png-sync.js";
27
-
28
- const log = getLogger("avatar-manifest");
29
-
30
- export type AvatarKind = "character" | "image" | "none";
31
- export type AvatarSource = "builder" | "upload" | "ai";
32
-
33
- export interface AvatarImageMeta {
34
- updatedAt: string;
35
- etag: string;
36
- }
23
+ AVATAR_TRAITS_FILENAME,
24
+ type AvatarImageMeta,
25
+ type AvatarKind,
26
+ type AvatarSource,
27
+ type AvatarState,
28
+ type CharacterTraits,
29
+ deriveAvatarFromLegacyFiles,
30
+ parseAvatarManifest,
31
+ } from "@vellumai/avatar-manifest";
37
32
 
38
- export interface AvatarState {
39
- kind: AvatarKind;
40
- traits: CharacterTraits | null;
41
- source: AvatarSource | null;
42
- image: AvatarImageMeta | null;
43
- }
33
+ import { getLogger } from "../util/logger.js";
34
+ import { getAvatarDir } from "../util/platform.js";
44
35
 
45
- const AVATAR_KINDS: ReadonlySet<string> = new Set<AvatarKind>([
46
- "character",
47
- "image",
48
- "none",
49
- ]);
36
+ const log = getLogger("avatar-manifest");
50
37
 
51
- /** Narrows an unknown value to valid CharacterTraits (presence check only). */
52
- function isValidTraits(value: unknown): value is CharacterTraits {
53
- if (!value || typeof value !== "object") {
54
- return false;
55
- }
56
- const t = value as Record<string, unknown>;
57
- return (
58
- typeof t.bodyShape === "string" &&
59
- !!t.bodyShape &&
60
- typeof t.eyeStyle === "string" &&
61
- !!t.eyeStyle &&
62
- typeof t.color === "string" &&
63
- !!t.color
64
- );
65
- }
66
-
67
- /** Narrows an unknown value to valid AvatarImageMeta (presence check only). */
68
- function isValidImageMeta(value: unknown): value is AvatarImageMeta {
69
- if (!value || typeof value !== "object") {
70
- return false;
71
- }
72
- const m = value as Record<string, unknown>;
73
- return (
74
- typeof m.updatedAt === "string" &&
75
- !!m.updatedAt &&
76
- typeof m.etag === "string" &&
77
- !!m.etag
78
- );
79
- }
38
+ export type { AvatarImageMeta, AvatarKind, AvatarSource, AvatarState };
39
+ export type { CharacterTraits };
80
40
 
81
41
  /**
82
42
  * Reads and validates the avatar manifest. Returns `null` when the manifest is
@@ -93,32 +53,9 @@ export function readManifest(
93
53
  }
94
54
 
95
55
  try {
96
- const parsed = JSON.parse(readFileSync(manifestPath, "utf-8")) as unknown;
97
- if (!parsed || typeof parsed !== "object") {
98
- return null;
99
- }
100
- const obj = parsed as Record<string, unknown>;
101
- if (typeof obj.kind !== "string" || !AVATAR_KINDS.has(obj.kind)) {
102
- return null;
103
- }
104
- const kind = obj.kind as AvatarKind;
105
-
106
- // Reject partial manifests: a valid `kind` with a missing/malformed payload
107
- // would otherwise short-circuit the legacy fallback and surface an avatar
108
- // with null traits/image.
109
- if (kind === "character" && !isValidTraits(obj.traits)) {
110
- return null;
111
- }
112
- if (kind === "image" && !isValidImageMeta(obj.image)) {
113
- return null;
114
- }
115
-
116
- return {
117
- kind,
118
- traits: (obj.traits as CharacterTraits | null) ?? null,
119
- source: (obj.source as AvatarSource | null) ?? null,
120
- image: (obj.image as AvatarImageMeta | null) ?? null,
121
- };
56
+ return parseAvatarManifest(
57
+ JSON.parse(readFileSync(manifestPath, "utf-8")) as unknown,
58
+ );
122
59
  } catch (err) {
123
60
  log.warn({ err }, "Failed to read avatar manifest — treating as absent");
124
61
  return null;
@@ -167,24 +104,16 @@ export function computeImageMeta(imagePath: string): AvatarImageMeta {
167
104
  * Used by the read handlers to self-heal once on a manifest-miss: the derived
168
105
  * state is persisted via `writeManifest` so subsequent reads are manifest-only.
169
106
  *
170
- * Inference is **traits-first**: whenever a valid `character-traits.json`
171
- * exists, the result is `character` regardless of whether a PNG is also
172
- * present. We deliberately do NOT compare mtimes to break the both-present
173
- * tie, because the builder writes the rendered PNG *after* the traits file
174
- * (see traits-png-sync.ts), so the PNG is always newer even when the
175
- * character is the source of truth. Comparing mtimes would misclassify a
176
- * builder-generated character as an uploaded image.
107
+ * Inference is traits-first (see `deriveAvatarFromLegacyFiles`).
177
108
  */
178
109
  export function deriveStateFromLegacyFiles(
179
110
  avatarDir: string = getAvatarDir(),
180
111
  ): AvatarState {
181
- const traitsPath = join(avatarDir, TRAITS_FILENAME);
112
+ const traitsPath = join(avatarDir, AVATAR_TRAITS_FILENAME);
113
+ let traitsJson: unknown;
182
114
  if (existsSync(traitsPath)) {
183
115
  try {
184
- const traits = JSON.parse(readFileSync(traitsPath, "utf-8")) as unknown;
185
- if (isValidTraits(traits)) {
186
- return { kind: "character", traits, source: null, image: null };
187
- }
116
+ traitsJson = JSON.parse(readFileSync(traitsPath, "utf-8")) as unknown;
188
117
  } catch (err) {
189
118
  log.warn(
190
119
  { err },
@@ -194,14 +123,33 @@ export function deriveStateFromLegacyFiles(
194
123
  }
195
124
 
196
125
  const imagePath = join(avatarDir, AVATAR_IMAGE_FILENAME);
197
- if (existsSync(imagePath)) {
198
- return {
199
- kind: "image",
200
- traits: null,
201
- source: null,
202
- image: computeImageMeta(imagePath),
203
- };
126
+ const derived = deriveAvatarFromLegacyFiles({
127
+ traitsJson,
128
+ hasImage: existsSync(imagePath),
129
+ });
130
+ switch (derived.kind) {
131
+ case "character":
132
+ return {
133
+ kind: "character",
134
+ traits: derived.traits,
135
+ source: null,
136
+ image: null,
137
+ };
138
+ case "image":
139
+ return {
140
+ kind: "image",
141
+ traits: null,
142
+ source: null,
143
+ image: computeImageMeta(imagePath),
144
+ };
145
+ case "none":
146
+ return { kind: "none", traits: null, source: null, image: null };
204
147
  }
148
+ }
205
149
 
206
- return { kind: "none", traits: null, source: null, image: null };
150
+ /** Manifest first, legacy sidecars as fallback. Never persists. */
151
+ export function readAvatarState(
152
+ avatarDir: string = getAvatarDir(),
153
+ ): AvatarState {
154
+ return readManifest(avatarDir) ?? deriveStateFromLegacyFiles(avatarDir);
207
155
  }
@@ -15,13 +15,14 @@ import { randomUUID } from "node:crypto";
15
15
  import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
16
16
  import { join } from "node:path";
17
17
 
18
- import { getLogger } from "../util/logger.js";
19
18
  import {
20
19
  AVATAR_IMAGE_FILENAME,
21
20
  AVATAR_MANIFEST_FILENAME,
22
- getAvatarDir,
23
- getAvatarImagePath,
24
- } from "../util/platform.js";
21
+ AVATAR_TRAITS_FILENAME,
22
+ } from "@vellumai/avatar-manifest";
23
+
24
+ import { getLogger } from "../util/logger.js";
25
+ import { getAvatarDir, getAvatarImagePath } from "../util/platform.js";
25
26
  import {
26
27
  type AvatarSource,
27
28
  computeImageMeta,
@@ -30,7 +31,6 @@ import {
30
31
  import {
31
32
  ASCII_FILENAME,
32
33
  type CharacterTraits,
33
- TRAITS_FILENAME,
34
34
  type TraitsSyncResult,
35
35
  writeTraitsAndRenderAvatar,
36
36
  } from "./traits-png-sync.js";
@@ -77,7 +77,7 @@ export function setImage(pngBuffer: Buffer, source: AvatarSource): void {
77
77
  writeFileSync(pngTmp, pngBuffer);
78
78
  renameSync(pngTmp, pngPath);
79
79
 
80
- rmSync(join(avatarDir, TRAITS_FILENAME), { force: true });
80
+ rmSync(join(avatarDir, AVATAR_TRAITS_FILENAME), { force: true });
81
81
  rmSync(join(avatarDir, ASCII_FILENAME), { force: true });
82
82
 
83
83
  writeManifest({
@@ -105,7 +105,7 @@ export function clearAvatar(): void {
105
105
  mkdirSync(avatarDir, { recursive: true });
106
106
 
107
107
  rmSync(join(avatarDir, AVATAR_IMAGE_FILENAME), { force: true });
108
- rmSync(join(avatarDir, TRAITS_FILENAME), { force: true });
108
+ rmSync(join(avatarDir, AVATAR_TRAITS_FILENAME), { force: true });
109
109
  rmSync(join(avatarDir, ASCII_FILENAME), { force: true });
110
110
  rmSync(join(avatarDir, AVATAR_MANIFEST_FILENAME), { force: true });
111
111
 
@@ -0,0 +1,146 @@
1
+ import {
2
+ closeSync,
3
+ constants,
4
+ existsSync,
5
+ fstatSync,
6
+ lstatSync,
7
+ openSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ } from "node:fs";
11
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
12
+
13
+ import { getLogger } from "../util/logger.js";
14
+ import { getAvatarImagePath, getWorkspaceDir } from "../util/platform.js";
15
+ import { type AvatarState, readAvatarState } from "./avatar-manifest.js";
16
+ import { writeTraitsAndRenderAvatar } from "./traits-png-sync.js";
17
+
18
+ const log = getLogger("ensure-raster");
19
+
20
+ /** Largest raster any reader serves; a bigger file is treated as unreadable. */
21
+ export const AVATAR_RASTER_MAX_BYTES = 5 * 1024 * 1024;
22
+
23
+ function isWithin(dir: string, filePath: string): boolean {
24
+ const rel = relative(dir, filePath);
25
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
26
+ }
27
+
28
+ /**
29
+ * Reads the raster only when it is a regular file that truly lives under the
30
+ * avatar dir: a symlinked PNG (or a symlinked ancestor) would let the
31
+ * workspace hand a reader an arbitrary host file. Validation and the read
32
+ * share one descriptor so a concurrent swap of the path cannot slip a
33
+ * different file past the checks, and the descriptor's location is
34
+ * revalidated after the open. Returns null when the file is missing,
35
+ * symlinked, outside the avatar dir, not a regular file, or over the cap.
36
+ */
37
+ export function readContainedAvatarRaster(rasterPath: string): Buffer | null {
38
+ let fd: number | undefined;
39
+ try {
40
+ // Anchored on the real workspace root: a data/avatar symlink pointing at
41
+ // another workspace resolves outside it and is rejected.
42
+ // The root is resolved once and reused after the open so a workspace
43
+ // pathname swapped for a symlink mid-read cannot re-anchor the check.
44
+ const realRoot = realpathSync(getWorkspaceDir());
45
+ const realDir = realpathSync(dirname(rasterPath));
46
+ if (!isWithin(realRoot, realDir)) {
47
+ return null;
48
+ }
49
+ const realPath = join(realDir, basename(rasterPath));
50
+ const linkStats = lstatSync(realPath);
51
+ if (!linkStats.isFile()) {
52
+ return null;
53
+ }
54
+ fd = openSync(realPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
55
+ const stats = fstatSync(fd);
56
+ if (
57
+ !stats.isFile() ||
58
+ stats.dev !== linkStats.dev ||
59
+ stats.ino !== linkStats.ino ||
60
+ stats.size > AVATAR_RASTER_MAX_BYTES
61
+ ) {
62
+ return null;
63
+ }
64
+ // O_NOFOLLOW guards only the last component; an ancestor swapped for an
65
+ // outside symlink between the check and the open either stays swapped
66
+ // (fresh realpath resolves outside) or is swapped back (the inside file
67
+ // is a different inode than the opened fd). Node has no openat.
68
+ const reopenedPath = realpathSync(rasterPath);
69
+ if (!isWithin(realRoot, reopenedPath)) {
70
+ return null;
71
+ }
72
+ const reopenedStats = lstatSync(reopenedPath);
73
+ if (
74
+ !reopenedStats.isFile() ||
75
+ reopenedStats.dev !== stats.dev ||
76
+ reopenedStats.ino !== stats.ino
77
+ ) {
78
+ return null;
79
+ }
80
+ const bytes = readFileSync(fd);
81
+ return bytes.length > AVATAR_RASTER_MAX_BYTES ? null : bytes;
82
+ } catch {
83
+ return null;
84
+ } finally {
85
+ if (fd !== undefined) {
86
+ closeSync(fd);
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Returns the on-disk path of the current avatar PNG, or null when there is
93
+ * none. Never reads the raster bytes.
94
+ *
95
+ * Side-effect-free when the PNG already exists. A character whose PNG is
96
+ * missing is re-rendered from its persisted traits (best-effort; null when
97
+ * the native rasterizer is unavailable or the render fails). An image-kind
98
+ * avatar with a missing PNG cannot be recovered and yields null.
99
+ *
100
+ * Callers that already hold the avatar state can pass it to skip the re-read.
101
+ */
102
+ export async function ensureAvatarRasterPath(
103
+ state: AvatarState = readAvatarState(),
104
+ ): Promise<string | null> {
105
+ if (state.kind === "none") {
106
+ return null;
107
+ }
108
+
109
+ const avatarPath = getAvatarImagePath();
110
+ if (existsSync(avatarPath)) {
111
+ return avatarPath;
112
+ }
113
+ if (state.kind !== "character" || !state.traits) {
114
+ return null;
115
+ }
116
+
117
+ try {
118
+ const result = writeTraitsAndRenderAvatar(state.traits);
119
+ if (result.ok) {
120
+ return avatarPath;
121
+ }
122
+ log.warn({ reason: result.reason }, "Avatar raster re-render failed");
123
+ } catch (err) {
124
+ log.warn({ err }, "Avatar raster re-render threw");
125
+ }
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Returns the PNG raster for the current avatar, or null when there is none
131
+ * or it cannot be read (see `readContainedAvatarRaster`). Same regeneration
132
+ * semantics as `ensureAvatarRasterPath`.
133
+ */
134
+ export async function ensureAvatarRaster(
135
+ state: AvatarState = readAvatarState(),
136
+ ): Promise<Buffer | null> {
137
+ const avatarPath = await ensureAvatarRasterPath(state);
138
+ if (!avatarPath) {
139
+ return null;
140
+ }
141
+ const bytes = readContainedAvatarRaster(avatarPath);
142
+ if (!bytes) {
143
+ log.warn({ path: avatarPath }, "Avatar raster unreadable or not contained");
144
+ }
145
+ return bytes;
146
+ }
@@ -33,7 +33,6 @@ mock.module("../util/logger.js", () => ({
33
33
  }));
34
34
 
35
35
  mock.module("../util/platform.js", () => ({
36
- AVATAR_IMAGE_FILENAME: "avatar-image.png",
37
36
  getAvatarDir: () => "/tmp/vellum-test-avatar-never-written",
38
37
  }));
39
38
 
@@ -2,8 +2,14 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdirSync, renameSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
+ import {
6
+ AVATAR_IMAGE_FILENAME,
7
+ AVATAR_TRAITS_FILENAME,
8
+ type CharacterTraits,
9
+ } from "@vellumai/avatar-manifest";
10
+
5
11
  import { getLogger } from "../util/logger.js";
6
- import { AVATAR_IMAGE_FILENAME, getAvatarDir } from "../util/platform.js";
12
+ import { getAvatarDir } from "../util/platform.js";
7
13
  import { renderCharacterAscii } from "./ascii-renderer.js";
8
14
  import { getCharacterComponents } from "./character-components.js";
9
15
  import { renderCharacterPng } from "./png-renderer.js";
@@ -11,17 +17,10 @@ import { isResvgAvailable } from "./resvg-lazy.js";
11
17
 
12
18
  const log = getLogger("traits-png-sync");
13
19
 
14
- /** Sidecar filename for the persisted character traits JSON. */
15
- export const TRAITS_FILENAME = "character-traits.json";
16
-
17
20
  /** Sidecar filename for the rendered ASCII art. */
18
21
  export const ASCII_FILENAME = "character-ascii.txt";
19
22
 
20
- export interface CharacterTraits {
21
- bodyShape: string;
22
- eyeStyle: string;
23
- color: string;
24
- }
23
+ export type { CharacterTraits } from "@vellumai/avatar-manifest";
25
24
 
26
25
  export type TraitsSyncResult =
27
26
  | { ok: true; asciiWritten: boolean }
@@ -171,7 +170,7 @@ export function writeTraitsAndRenderAvatar(
171
170
  }
172
171
 
173
172
  const avatarDir = getAvatarDir();
174
- const traitsPath = join(avatarDir, TRAITS_FILENAME);
173
+ const traitsPath = join(avatarDir, AVATAR_TRAITS_FILENAME);
175
174
 
176
175
  try {
177
176
  mkdirSync(avatarDir, { recursive: true });
@@ -82,7 +82,6 @@ mock.module("../util/platform.js", () => ({
82
82
  getSandboxWorkingDir: () => testWorkspaceDir,
83
83
  getSoundsDir: () => workspacePath("data", "sounds"),
84
84
  getAvatarDir: () => workspacePath("data", "avatar"),
85
- AVATAR_IMAGE_FILENAME: "avatar-image.png",
86
85
  getAvatarImagePath: () => workspacePath("data", "avatar", "avatar-image.png"),
87
86
  getXdgVellumConfigDirName: () => ".vellum",
88
87
  getProfilerRootDir: () => workspacePath("data", "profiler"),
@@ -134,7 +134,7 @@ describe("getDefaultOffsiteBackupsDir", () => {
134
134
 
135
135
  describe("resolveOffsiteDestinations", () => {
136
136
  test("returns iCloud default with encrypt=true when override is null", () => {
137
- const result = resolveOffsiteDestinations(null);
137
+ const result = resolveOffsiteDestinations(null, "darwin");
138
138
  expect(result).toHaveLength(1);
139
139
  expect(result[0].encrypt).toBe(true);
140
140
  expect(result[0].path).toContain(
@@ -143,11 +143,15 @@ describe("resolveOffsiteDestinations", () => {
143
143
  });
144
144
 
145
145
  test("returns iCloud default when override is undefined", () => {
146
- const result = resolveOffsiteDestinations(undefined);
146
+ const result = resolveOffsiteDestinations(undefined, "darwin");
147
147
  expect(result).toHaveLength(1);
148
148
  expect(result[0].encrypt).toBe(true);
149
149
  });
150
150
 
151
+ test("returns no implicit destination on Windows", () => {
152
+ expect(resolveOffsiteDestinations(null, "win32")).toEqual([]);
153
+ });
154
+
151
155
  test("returns an empty array unchanged", () => {
152
156
  expect(resolveOffsiteDestinations([])).toEqual([]);
153
157
  });
@@ -115,15 +115,17 @@ export function deriveSafeAncestor(destinationPath: string): string {
115
115
 
116
116
  /**
117
117
  * Resolves the list of offsite backup destinations from an optional config
118
- * override. When `override` is `null` (the "not configured" sentinel), returns
119
- * a single-element array pointing at the iCloud default with encryption
120
- * enabled. When `override` is an array (including the empty array), returns it
121
- * unchanged so callers never need to null-check.
118
+ * override. A null override uses encrypted iCloud Drive on macOS and no
119
+ * implicit destination on other platforms. Explicit arrays are unchanged.
122
120
  */
123
121
  export function resolveOffsiteDestinations(
124
122
  override?: BackupDestination[] | null,
123
+ platform: NodeJS.Platform = process.platform,
125
124
  ): BackupDestination[] {
126
125
  if (override == null) {
126
+ if (platform !== "darwin") {
127
+ return [];
128
+ }
127
129
  return [{ path: getDefaultOffsiteBackupsDir(), encrypt: true }];
128
130
  }
129
131
  return override;
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { existsSync, rmSync } from "node:fs";
13
13
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
14
- import { dirname, join, resolve } from "node:path";
14
+ import { delimiter, dirname, join, resolve } from "node:path";
15
15
 
16
16
  import { getLogger } from "../util/logger.js";
17
17
  import { ensureCompilerTools } from "./compiler-tools.js";
@@ -469,7 +469,7 @@ export async function runCompile(
469
469
  const cacheNodeModules = join(getCacheDir(), "node_modules");
470
470
  const nodePath = [preactParent, cacheNodeModules]
471
471
  .filter((p) => existsSync(p))
472
- .join(":");
472
+ .join(delimiter);
473
473
 
474
474
  // Shell out to esbuild CLI
475
475
  const args = [
@@ -0,0 +1,36 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { resolveEsbuildPlatform } from "./compiler-tools.js";
4
+
5
+ describe("resolveEsbuildPlatform", () => {
6
+ test("selects Windows executables", () => {
7
+ expect(resolveEsbuildPlatform("win32", "x64")).toEqual({
8
+ packageName: "win32-x64",
9
+ binaryPathParts: ["esbuild.exe"],
10
+ });
11
+ expect(resolveEsbuildPlatform("win32", "arm64")).toEqual({
12
+ packageName: "win32-arm64",
13
+ binaryPathParts: ["esbuild.exe"],
14
+ });
15
+ });
16
+
17
+ test("keeps POSIX binaries under bin", () => {
18
+ expect(resolveEsbuildPlatform("darwin", "arm64")).toEqual({
19
+ packageName: "darwin-arm64",
20
+ binaryPathParts: ["bin", "esbuild"],
21
+ });
22
+ expect(resolveEsbuildPlatform("linux", "x64")).toEqual({
23
+ packageName: "linux-x64",
24
+ binaryPathParts: ["bin", "esbuild"],
25
+ });
26
+ });
27
+
28
+ test("rejects unsupported targets", () => {
29
+ expect(() => resolveEsbuildPlatform("freebsd", "x64")).toThrow(
30
+ "Unsupported esbuild platform: freebsd",
31
+ );
32
+ expect(() => resolveEsbuildPlatform("win32", "ia32")).toThrow(
33
+ "Unsupported esbuild architecture: ia32",
34
+ );
35
+ });
36
+ });
@@ -16,7 +16,7 @@ import {
16
16
  writeFileSync,
17
17
  } from "node:fs";
18
18
  import { arch, platform } from "node:os";
19
- import { join } from "node:path";
19
+ import { basename, join } from "node:path";
20
20
 
21
21
  import { getLogger } from "../util/logger.js";
22
22
  import { getWorkspaceDir } from "../util/platform.js";
@@ -44,6 +44,37 @@ interface VersionManifest {
44
44
  installedAt: string;
45
45
  }
46
46
 
47
+ interface EsbuildPlatform {
48
+ packageName: string;
49
+ binaryPathParts: string[];
50
+ }
51
+
52
+ export function resolveEsbuildPlatform(
53
+ os: NodeJS.Platform,
54
+ cpu: string,
55
+ ): EsbuildPlatform {
56
+ if (cpu !== "arm64" && cpu !== "x64") {
57
+ throw new Error(`Unsupported esbuild architecture: ${cpu}`);
58
+ }
59
+ if (os === "darwin" || os === "linux") {
60
+ return {
61
+ packageName: `${os}-${cpu}`,
62
+ binaryPathParts: ["bin", "esbuild"],
63
+ };
64
+ }
65
+ if (os === "win32") {
66
+ return {
67
+ packageName: `win32-${cpu}`,
68
+ binaryPathParts: ["esbuild.exe"],
69
+ };
70
+ }
71
+ throw new Error(`Unsupported esbuild platform: ${os}`);
72
+ }
73
+
74
+ function installedEsbuildName(os: NodeJS.Platform): string {
75
+ return os === "win32" ? "esbuild.exe" : "esbuild";
76
+ }
77
+
47
78
  function getToolsDir(): string {
48
79
  return join(getWorkspaceDir(), "compiler-tools");
49
80
  }
@@ -171,8 +202,11 @@ function isReady(baseDir: string): boolean {
171
202
  if (!manifest || manifest.toolsVersion !== TOOLS_VERSION) {
172
203
  return false;
173
204
  }
205
+ if (manifest.platform !== platform() || manifest.arch !== arch()) {
206
+ return false;
207
+ }
174
208
  return (
175
- existsSync(join(baseDir, "bin", "esbuild")) &&
209
+ existsSync(join(baseDir, "bin", installedEsbuildName(platform()))) &&
176
210
  existsSync(join(baseDir, "node_modules", "preact"))
177
211
  );
178
212
  }
@@ -193,7 +227,7 @@ export async function ensureCompilerTools(): Promise<CompilerTools> {
193
227
  }
194
228
 
195
229
  return {
196
- esbuildBin: join(baseDir, "bin", "esbuild"),
230
+ esbuildBin: join(baseDir, "bin", installedEsbuildName(platform())),
197
231
  preactDir: join(baseDir, "node_modules", "preact"),
198
232
  };
199
233
  }
@@ -247,22 +281,17 @@ async function install(baseDir: string): Promise<void> {
247
281
  mkdirSync(tmpDir, { recursive: true });
248
282
 
249
283
  try {
250
- // Determine esbuild platform package name
251
- const esbuildPlatform =
252
- os === "darwin"
253
- ? cpu === "arm64"
254
- ? "darwin-arm64"
255
- : "darwin-x64"
256
- : cpu === "arm64"
257
- ? "linux-arm64"
258
- : "linux-x64";
284
+ const esbuildPlatform = resolveEsbuildPlatform(os, cpu);
259
285
 
260
286
  // Download esbuild binary + preact in parallel
261
287
  await Promise.all([
262
288
  downloadAndExtract(
263
- `@esbuild/${esbuildPlatform}`,
289
+ `@esbuild/${esbuildPlatform.packageName}`,
264
290
  ESBUILD_VERSION,
265
- npmTarballUrl(`@esbuild/${esbuildPlatform}`, ESBUILD_VERSION),
291
+ npmTarballUrl(
292
+ `@esbuild/${esbuildPlatform.packageName}`,
293
+ ESBUILD_VERSION,
294
+ ),
266
295
  join(tmpDir, "esbuild-pkg"),
267
296
  ),
268
297
  downloadAndExtract(
@@ -274,12 +303,19 @@ async function install(baseDir: string): Promise<void> {
274
303
  ]);
275
304
 
276
305
  // Move esbuild binary to bin/
277
- const esbuildBinSrc = join(tmpDir, "esbuild-pkg", "bin", "esbuild");
306
+ const esbuildBinSrc = join(
307
+ tmpDir,
308
+ "esbuild-pkg",
309
+ ...esbuildPlatform.binaryPathParts,
310
+ );
278
311
  const binDir = join(tmpDir, "bin");
279
312
  mkdirSync(binDir, { recursive: true });
280
313
  const { renameSync } = await import("node:fs");
281
- renameSync(esbuildBinSrc, join(binDir, "esbuild"));
282
- chmodSync(join(binDir, "esbuild"), 0o755);
314
+ const esbuildBin = join(binDir, installedEsbuildName(os));
315
+ renameSync(esbuildBinSrc, esbuildBin);
316
+ if (os !== "win32") {
317
+ chmodSync(esbuildBin, 0o755);
318
+ }
283
319
  rmSync(join(tmpDir, "esbuild-pkg"), { recursive: true, force: true });
284
320
 
285
321
  // Write version manifest
@@ -299,7 +335,7 @@ async function install(baseDir: string): Promise<void> {
299
335
  // Atomic swap: clear old install, move new files in
300
336
  const { readdirSync } = await import("node:fs");
301
337
  for (const entry of readdirSync(baseDir)) {
302
- if (entry.startsWith(".") || entry === tmpDir.split("/").pop()) {
338
+ if (entry.startsWith(".") || entry === basename(tmpDir)) {
303
339
  continue;
304
340
  }
305
341
  rmSync(join(baseDir, entry), { recursive: true, force: true });