@vellumai/assistant 0.6.4 → 0.6.5

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 (717) hide show
  1. package/.prettierignore +5 -0
  2. package/ARCHITECTURE.md +32 -36
  3. package/Dockerfile +12 -0
  4. package/README.md +3 -4
  5. package/bun.lock +8 -3
  6. package/docs/architecture/integrations.md +1 -20
  7. package/docs/architecture/security.md +16 -16
  8. package/docs/error-handling.md +111 -0
  9. package/docs/skills.md +10 -10
  10. package/docs/stt-provider-onboarding.md +2 -1
  11. package/knip.json +9 -2
  12. package/node_modules/@vellumai/ces-contracts/package.json +2 -1
  13. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  14. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  15. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  16. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  17. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  18. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  19. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  20. package/openapi.yaml +123 -11
  21. package/package.json +6 -3
  22. package/scripts/generate-openapi.ts +50 -11
  23. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  24. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  25. package/src/__tests__/agent-loop.test.ts +112 -1
  26. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  27. package/src/__tests__/anthropic-provider.test.ts +171 -2
  28. package/src/__tests__/approval-cascade.test.ts +31 -10
  29. package/src/__tests__/approval-routes-http.test.ts +134 -10
  30. package/src/__tests__/assistant-attachments.test.ts +44 -0
  31. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  32. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  33. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  34. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  35. package/src/__tests__/browser-skill-endstate.test.ts +51 -182
  36. package/src/__tests__/btw-routes.test.ts +47 -1
  37. package/src/__tests__/call-controller.test.ts +1 -2
  38. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  39. package/src/__tests__/catalog-cache.test.ts +27 -4
  40. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  41. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  42. package/src/__tests__/checker.test.ts +428 -501
  43. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  44. package/src/__tests__/compaction-circuit-breaker.test.ts +336 -0
  45. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  46. package/src/__tests__/config-analysis.test.ts +11 -28
  47. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  48. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  49. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  50. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  51. package/src/__tests__/config-schema.test.ts +427 -114
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/contact-store-user-file.test.ts +72 -73
  54. package/src/__tests__/contacts-write.test.ts +4 -4
  55. package/src/__tests__/context-token-estimator.test.ts +191 -1
  56. package/src/__tests__/context-window-manager.test.ts +530 -2
  57. package/src/__tests__/conversation-abort-tool-results.test.ts +30 -16
  58. package/src/__tests__/conversation-agent-loop-overflow.test.ts +61 -17
  59. package/src/__tests__/conversation-agent-loop.test.ts +412 -82
  60. package/src/__tests__/conversation-attachments.test.ts +1 -1
  61. package/src/__tests__/conversation-confirmation-signals.test.ts +30 -9
  62. package/src/__tests__/conversation-error.test.ts +37 -6
  63. package/src/__tests__/conversation-history-web-search.test.ts +6 -0
  64. package/src/__tests__/conversation-init.benchmark.test.ts +36 -0
  65. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  66. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  67. package/src/__tests__/conversation-pre-run-repair.test.ts +30 -16
  68. package/src/__tests__/conversation-process-callsite.test.ts +306 -0
  69. package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -16
  70. package/src/__tests__/conversation-queue.test.ts +41 -26
  71. package/src/__tests__/conversation-routes-disk-view.test.ts +29 -1
  72. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +2735 -55
  74. package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
  75. package/src/__tests__/conversation-skill-tools.test.ts +12 -146
  76. package/src/__tests__/conversation-slash-queue.test.ts +34 -19
  77. package/src/__tests__/conversation-slash-unknown.test.ts +30 -16
  78. package/src/__tests__/conversation-speed-override.test.ts +30 -11
  79. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  80. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  81. package/src/__tests__/conversation-title-service.test.ts +2 -2
  82. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
  83. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  84. package/src/__tests__/conversation-usage.test.ts +3 -1
  85. package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
  86. package/src/__tests__/conversation-workspace-injection.test.ts +43 -15
  87. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +44 -16
  88. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  89. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  90. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  91. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  92. package/src/__tests__/credential-vault-unit.test.ts +135 -19
  93. package/src/__tests__/credentials-cli.test.ts +1 -9
  94. package/src/__tests__/cross-provider-web-search.test.ts +84 -0
  95. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  96. package/src/__tests__/delete-propagation.test.ts +437 -0
  97. package/src/__tests__/dm-backfill.test.ts +417 -0
  98. package/src/__tests__/dm-persistence.test.ts +227 -0
  99. package/src/__tests__/edit-propagation.test.ts +280 -0
  100. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  101. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  102. package/src/__tests__/estimator-calibration.test.ts +213 -0
  103. package/src/__tests__/extension-id-sync-guard.test.ts +26 -7
  104. package/src/__tests__/file-write-tool.test.ts +151 -1
  105. package/src/__tests__/filing-service.test.ts +255 -0
  106. package/src/__tests__/gemini-provider.test.ts +0 -3
  107. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  108. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  109. package/src/__tests__/heartbeat-service.test.ts +96 -15
  110. package/src/__tests__/host-shell-tool.test.ts +124 -18
  111. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  112. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  113. package/src/__tests__/intent-routing.test.ts +1 -40
  114. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  115. package/src/__tests__/llm-context-normalization.test.ts +121 -0
  116. package/src/__tests__/llm-resolver.test.ts +214 -0
  117. package/src/__tests__/llm-schema.test.ts +223 -0
  118. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  119. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  120. package/src/__tests__/migration-import-from-url.test.ts +684 -0
  121. package/src/__tests__/model-intents.test.ts +9 -83
  122. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  123. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  124. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  125. package/src/__tests__/oauth-store.test.ts +10 -7
  126. package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
  127. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  128. package/src/__tests__/openai-provider.test.ts +7 -0
  129. package/src/__tests__/openai-responses-provider.test.ts +396 -0
  130. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  131. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  132. package/src/__tests__/permission-checker-host-gate.test.ts +1 -1
  133. package/src/__tests__/permission-mode.test.ts +16 -0
  134. package/src/__tests__/permission-types.test.ts +0 -1
  135. package/src/__tests__/persona-resolver.test.ts +13 -13
  136. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  137. package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
  138. package/src/__tests__/pricing.test.ts +50 -3
  139. package/src/__tests__/profiler-routes.test.ts +1 -1
  140. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  141. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  143. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  144. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  145. package/src/__tests__/proxy-approval-callback.test.ts +0 -1
  146. package/src/__tests__/reaction-persistence.test.ts +560 -0
  147. package/src/__tests__/relay-server.test.ts +1 -1
  148. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  149. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  150. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  151. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  152. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  153. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  154. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  155. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  156. package/src/__tests__/secret-scanner-executor.test.ts +1 -1
  157. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  158. package/src/__tests__/server-history-render.test.ts +31 -0
  159. package/src/__tests__/shell-parser-property.test.ts +13 -13
  160. package/src/__tests__/skill-cache-store.test.ts +182 -0
  161. package/src/__tests__/skills.test.ts +19 -33
  162. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  163. package/src/__tests__/slack-skill.test.ts +3 -8
  164. package/src/__tests__/starter-bundle.test.ts +35 -0
  165. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  166. package/src/__tests__/suggestion-routes.test.ts +160 -3
  167. package/src/__tests__/system-prompt.test.ts +22 -35
  168. package/src/__tests__/task-runner.test.ts +3 -1
  169. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  170. package/src/__tests__/terminal-tools.test.ts +8 -0
  171. package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
  172. package/src/__tests__/thread-backfill.test.ts +941 -0
  173. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
  174. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  175. package/src/__tests__/tool-executor.test.ts +60 -94
  176. package/src/__tests__/trust-store.test.ts +442 -109
  177. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  178. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  179. package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
  180. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  181. package/src/__tests__/volume-security-guard.test.ts +3 -2
  182. package/src/__tests__/web-search-history.test.ts +337 -0
  183. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  184. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  185. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  186. package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
  187. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  188. package/src/__tests__/workspace-policy.test.ts +1 -13
  189. package/src/acp/client-handler.ts +1 -2
  190. package/src/agent/loop.ts +209 -17
  191. package/src/avatar/resvg-lazy.test.ts +136 -0
  192. package/src/avatar/resvg-lazy.ts +82 -9
  193. package/src/avatar/traits-png-sync.ts +21 -1
  194. package/src/browser/__tests__/operations.test.ts +163 -0
  195. package/src/browser/identifiers.ts +51 -0
  196. package/src/browser/operations.ts +660 -0
  197. package/src/browser/types.ts +81 -0
  198. package/src/calls/guardian-question-copy.ts +2 -2
  199. package/src/calls/telephony-stt-routing.ts +1 -1
  200. package/src/calls/voice-session-bridge.ts +1 -0
  201. package/src/cli/AGENTS.md +1 -1
  202. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  203. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  204. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  205. package/src/cli/commands/__tests__/email-list.test.ts +6 -0
  206. package/src/cli/commands/__tests__/email-send.test.ts +93 -1
  207. package/src/cli/commands/__tests__/image-generation.test.ts +666 -0
  208. package/src/cli/commands/__tests__/inference-send.test.ts +451 -0
  209. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  210. package/src/cli/commands/__tests__/task.test.ts +913 -0
  211. package/src/cli/commands/__tests__/tts-synthesize.test.ts +594 -0
  212. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  213. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  214. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  215. package/src/cli/commands/attachment.ts +182 -0
  216. package/src/cli/commands/browser.ts +350 -0
  217. package/src/cli/commands/cache.ts +341 -0
  218. package/src/cli/commands/completions.ts +0 -3
  219. package/src/cli/commands/config.ts +6 -6
  220. package/src/cli/commands/conversations-import.ts +347 -0
  221. package/src/cli/commands/conversations.ts +14 -1
  222. package/src/cli/commands/email.ts +234 -194
  223. package/src/cli/commands/image-generation.ts +300 -0
  224. package/src/cli/commands/inference.ts +200 -0
  225. package/src/cli/commands/memory.ts +127 -17
  226. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  227. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
  228. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
  229. package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
  230. package/src/cli/commands/stt.ts +339 -0
  231. package/src/cli/commands/task.ts +795 -0
  232. package/src/cli/commands/trust.ts +50 -19
  233. package/src/cli/commands/tts.ts +273 -0
  234. package/src/cli/commands/ui.ts +670 -0
  235. package/src/cli/commands/watchers.ts +509 -0
  236. package/src/cli/lib/daemon-credential-client.ts +0 -19
  237. package/src/cli/program.ts +23 -4
  238. package/src/cli.ts +0 -37
  239. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
  240. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  241. package/src/config/bundled-skills/messaging/SKILL.md +2 -2
  242. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  243. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +8 -1
  244. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  245. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  246. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +11 -12
  247. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
  248. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  249. package/src/config/bundled-tool-registry.ts +0 -175
  250. package/src/config/env.ts +7 -2
  251. package/src/config/feature-flag-registry.json +25 -9
  252. package/src/config/llm-resolver.ts +128 -0
  253. package/src/config/loader.ts +194 -10
  254. package/src/config/raw-config-utils.ts +30 -2
  255. package/src/config/sanitize-for-transfer.ts +35 -0
  256. package/src/config/schema.ts +30 -41
  257. package/src/config/schemas/analysis.ts +3 -22
  258. package/src/config/schemas/calls.ts +0 -4
  259. package/src/config/schemas/filing.ts +2 -7
  260. package/src/config/schemas/heartbeat.ts +0 -5
  261. package/src/config/schemas/inference.ts +3 -23
  262. package/src/config/schemas/llm.ts +318 -0
  263. package/src/config/schemas/memory-processing.ts +1 -9
  264. package/src/config/schemas/notifications.ts +4 -11
  265. package/src/config/schemas/platform.ts +3 -9
  266. package/src/config/schemas/security.ts +33 -0
  267. package/src/config/schemas/services.ts +9 -4
  268. package/src/config/schemas/stt.ts +1 -0
  269. package/src/config/schemas/tts.ts +53 -0
  270. package/src/config/schemas/updates.ts +1 -1
  271. package/src/config/schemas/workspace-git.ts +3 -40
  272. package/src/config/skills.ts +2 -2
  273. package/src/context/__tests__/compact-prompt.test.ts +45 -0
  274. package/src/context/__tests__/microcompact.test.ts +805 -0
  275. package/src/context/estimator-calibration.ts +136 -0
  276. package/src/context/microcompact.ts +443 -0
  277. package/src/context/prompts/compact.md +12 -0
  278. package/src/context/token-estimator.ts +61 -3
  279. package/src/context/window-manager.ts +229 -25
  280. package/src/credential-execution/approval-bridge.ts +0 -1
  281. package/src/credential-execution/executable-discovery.ts +19 -8
  282. package/src/credential-execution/process-manager.test.ts +109 -0
  283. package/src/credential-execution/process-manager.ts +65 -2
  284. package/src/daemon/approval-generators.ts +29 -4
  285. package/src/daemon/assistant-attachments.ts +24 -13
  286. package/src/daemon/classifier.ts +2 -2
  287. package/src/daemon/config-watcher.ts +0 -1
  288. package/src/daemon/context-overflow-reducer.ts +4 -1
  289. package/src/daemon/conversation-agent-loop-handlers.ts +79 -12
  290. package/src/daemon/conversation-agent-loop.ts +462 -80
  291. package/src/daemon/conversation-attachments.ts +2 -6
  292. package/src/daemon/conversation-error.ts +36 -1
  293. package/src/daemon/conversation-lifecycle.ts +30 -6
  294. package/src/daemon/conversation-messaging.ts +73 -4
  295. package/src/daemon/conversation-process.ts +10 -4
  296. package/src/daemon/conversation-queue-manager.ts +3 -0
  297. package/src/daemon/conversation-runtime-assembly.ts +760 -29
  298. package/src/daemon/conversation-slash.ts +2 -2
  299. package/src/daemon/conversation-surfaces.ts +389 -1
  300. package/src/daemon/conversation-tool-setup.ts +10 -5
  301. package/src/daemon/conversation-usage.ts +1 -1
  302. package/src/daemon/conversation.ts +118 -30
  303. package/src/daemon/external-skills-bootstrap.ts +41 -0
  304. package/src/daemon/guardian-action-generators.ts +34 -14
  305. package/src/daemon/handlers/config-model.test.ts +86 -0
  306. package/src/daemon/handlers/config-model.ts +54 -12
  307. package/src/daemon/handlers/conversations.ts +9 -2
  308. package/src/daemon/handlers/shared.ts +39 -11
  309. package/src/daemon/handlers/skills.ts +2 -2
  310. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  311. package/src/daemon/lifecycle.ts +76 -14
  312. package/src/daemon/message-types/conversations.ts +14 -0
  313. package/src/daemon/message-types/messages.ts +9 -1
  314. package/src/daemon/message-types/trust.ts +0 -2
  315. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  316. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  317. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  318. package/src/daemon/pkb-context-tracker.ts +125 -0
  319. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  320. package/src/daemon/pkb-reminder-builder.ts +31 -0
  321. package/src/daemon/providers-setup.ts +6 -0
  322. package/src/daemon/server.ts +117 -9
  323. package/src/daemon/tool-side-effects.ts +0 -9
  324. package/src/daemon/watch-handler.ts +4 -4
  325. package/src/daemon/web-search-history.ts +126 -0
  326. package/src/events/domain-events.ts +0 -1
  327. package/src/filing/filing-service.ts +9 -10
  328. package/src/heartbeat/heartbeat-service.ts +76 -28
  329. package/src/home/__tests__/feed-scheduler.test.ts +39 -11
  330. package/src/home/__tests__/rollup-producer.test.ts +44 -0
  331. package/src/home/assistant-feed-authoring.ts +4 -0
  332. package/src/home/emit-feed-event.ts +4 -0
  333. package/src/home/feed-scheduler.ts +20 -4
  334. package/src/home/feed-types.ts +56 -2
  335. package/src/home/relationship-state-writer.ts +2 -2
  336. package/src/home/rollup-producer.ts +34 -5
  337. package/src/home/suggested-prompts.ts +101 -0
  338. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  339. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  340. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  341. package/src/ipc/__tests__/socket-path.test.ts +73 -0
  342. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  343. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  344. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  345. package/src/ipc/cli-client.ts +2 -1
  346. package/src/ipc/cli-server.ts +26 -8
  347. package/src/ipc/gateway-client.ts +4 -4
  348. package/src/ipc/routes/attachment.ts +114 -0
  349. package/src/ipc/routes/browser-context.ts +61 -0
  350. package/src/ipc/routes/browser.ts +96 -0
  351. package/src/ipc/routes/cache.ts +96 -0
  352. package/src/ipc/routes/index.ts +17 -1
  353. package/src/ipc/routes/task-queue.ts +226 -0
  354. package/src/ipc/routes/task.ts +173 -0
  355. package/src/ipc/routes/ui-request.ts +50 -0
  356. package/src/ipc/routes/watcher.ts +203 -0
  357. package/src/ipc/socket-path.ts +100 -0
  358. package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
  359. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  360. package/src/memory/admin.ts +18 -0
  361. package/src/memory/conversation-analyze-job.ts +14 -13
  362. package/src/memory/conversation-attention-store.ts +13 -6
  363. package/src/memory/conversation-crud.ts +103 -3
  364. package/src/memory/conversation-group-migration.ts +38 -6
  365. package/src/memory/conversation-title-service.ts +7 -4
  366. package/src/memory/db-init.ts +2 -0
  367. package/src/memory/embedding-backend.ts +1 -1
  368. package/src/memory/graph/compaction.ts +299 -0
  369. package/src/memory/graph/consolidation.ts +4 -4
  370. package/src/memory/graph/conversation-graph-memory.ts +89 -29
  371. package/src/memory/graph/extraction.test.ts +272 -2
  372. package/src/memory/graph/extraction.ts +173 -51
  373. package/src/memory/graph/graph-search.test.ts +92 -0
  374. package/src/memory/graph/graph-search.ts +4 -1
  375. package/src/memory/graph/narrative.ts +2 -2
  376. package/src/memory/graph/pattern-scan.ts +2 -2
  377. package/src/memory/graph/retriever.test.ts +459 -0
  378. package/src/memory/graph/retriever.ts +230 -48
  379. package/src/memory/graph/store.ts +41 -0
  380. package/src/memory/graph/tool-handlers.ts +27 -0
  381. package/src/memory/graph/tools.ts +6 -1
  382. package/src/memory/indexer.ts +5 -5
  383. package/src/memory/job-handlers/conversation-starters.ts +23 -20
  384. package/src/memory/job-handlers/summarization.ts +2 -2
  385. package/src/memory/job-utils.ts +7 -1
  386. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  387. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  388. package/src/memory/jobs-store.ts +44 -3
  389. package/src/memory/jobs-worker.ts +4 -0
  390. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  391. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
  392. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  393. package/src/memory/migrations/index.ts +1 -0
  394. package/src/memory/pkb/pkb-index.test.ts +368 -0
  395. package/src/memory/pkb/pkb-index.ts +255 -0
  396. package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
  397. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  398. package/src/memory/pkb/pkb-search.test.ts +438 -0
  399. package/src/memory/pkb/pkb-search.ts +137 -0
  400. package/src/memory/pkb/types.ts +53 -0
  401. package/src/memory/qdrant-client.ts +122 -1
  402. package/src/memory/slack-thread-store.ts +37 -0
  403. package/src/messaging/providers/gmail/adapter.ts +6 -16
  404. package/src/messaging/providers/gmail/client.ts +22 -0
  405. package/src/messaging/providers/gmail/types.ts +7 -0
  406. package/src/messaging/providers/slack/adapter.ts +14 -2
  407. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  408. package/src/messaging/providers/slack/backfill.ts +101 -0
  409. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  410. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  411. package/src/messaging/providers/slack/render-transcript.test.ts +1373 -0
  412. package/src/messaging/providers/slack/render-transcript.ts +443 -0
  413. package/src/messaging/style-analyzer.ts +5 -2
  414. package/src/notifications/README.md +9 -5
  415. package/src/notifications/decision-engine.ts +3 -9
  416. package/src/notifications/preference-extractor.ts +2 -6
  417. package/src/oauth/oauth-store.ts +1 -0
  418. package/src/oauth/platform-connection.test.ts +47 -0
  419. package/src/oauth/platform-connection.ts +15 -5
  420. package/src/oauth/seed-providers.ts +4 -2
  421. package/src/permissions/approval-policy.test.ts +948 -0
  422. package/src/permissions/approval-policy.ts +257 -0
  423. package/src/permissions/bash-risk-classifier.test.ts +1208 -0
  424. package/src/permissions/bash-risk-classifier.ts +707 -0
  425. package/src/permissions/checker.ts +217 -708
  426. package/src/permissions/command-registry.test.ts +535 -0
  427. package/src/permissions/command-registry.ts +825 -0
  428. package/src/permissions/defaults.ts +26 -78
  429. package/src/permissions/file-risk-classifier.test.ts +535 -0
  430. package/src/permissions/file-risk-classifier.ts +274 -0
  431. package/src/permissions/risk-types.ts +205 -0
  432. package/src/permissions/secret-prompter.ts +53 -2
  433. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  434. package/src/permissions/skill-risk-classifier.ts +214 -0
  435. package/src/permissions/trust-client.ts +52 -25
  436. package/src/permissions/trust-store-interface.ts +1 -6
  437. package/src/permissions/trust-store.ts +161 -62
  438. package/src/permissions/types.ts +23 -14
  439. package/src/permissions/web-risk-classifier.test.ts +170 -0
  440. package/src/permissions/web-risk-classifier.ts +89 -0
  441. package/src/permissions/workspace-policy.ts +1 -16
  442. package/src/platform/client.ts +19 -1
  443. package/src/prompts/persona-resolver.ts +3 -3
  444. package/src/prompts/system-prompt.ts +19 -20
  445. package/src/prompts/templates/SOUL.md +2 -2
  446. package/src/prompts/update-bulletin-job.ts +190 -0
  447. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  448. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  449. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  450. package/src/providers/anthropic/client.ts +183 -14
  451. package/src/providers/call-site-routing.ts +71 -0
  452. package/src/providers/gemini/client.ts +65 -2
  453. package/src/providers/managed-proxy/constants.ts +2 -1
  454. package/src/providers/model-catalog.ts +501 -33
  455. package/src/providers/model-intents.ts +4 -4
  456. package/src/providers/openai/chat-completions-provider.ts +57 -1
  457. package/src/providers/openai/responses-provider.ts +86 -9
  458. package/src/providers/openrouter/client.ts +76 -9
  459. package/src/providers/provider-env-vars.ts +56 -0
  460. package/src/providers/provider-send-message.ts +22 -5
  461. package/src/providers/ratelimit.ts +4 -0
  462. package/src/providers/registry.ts +19 -8
  463. package/src/providers/retry.ts +174 -39
  464. package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
  465. package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
  466. package/src/providers/speech-to-text/provider-catalog.ts +17 -0
  467. package/src/providers/speech-to-text/resolve.ts +7 -0
  468. package/src/providers/speech-to-text/xai-realtime.test.ts +578 -0
  469. package/src/providers/speech-to-text/xai-realtime.ts +796 -0
  470. package/src/providers/speech-to-text/xai.test.ts +155 -0
  471. package/src/providers/speech-to-text/xai.ts +97 -0
  472. package/src/providers/types.ts +93 -3
  473. package/src/runtime/AGENTS.md +2 -2
  474. package/src/runtime/__tests__/agent-wake.test.ts +43 -2
  475. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  476. package/src/runtime/agent-wake.ts +63 -22
  477. package/src/runtime/auth/route-policy.ts +4 -0
  478. package/src/runtime/btw-sidechain.ts +13 -3
  479. package/src/runtime/channel-reply-delivery.ts +106 -2
  480. package/src/runtime/decision-token.ts +116 -0
  481. package/src/runtime/gateway-client.ts +2 -2
  482. package/src/runtime/http-router.ts +32 -0
  483. package/src/runtime/http-server.ts +52 -1
  484. package/src/runtime/http-types.ts +23 -1
  485. package/src/runtime/interactive-ui.ts +362 -0
  486. package/src/runtime/invite-instruction-generator.ts +2 -2
  487. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  488. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  489. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  490. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  491. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  492. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  493. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  494. package/src/runtime/migrations/vbundle-importer.ts +154 -9
  495. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  496. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  497. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  498. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  499. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  500. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
  501. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
  502. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  503. package/src/runtime/routes/approval-prompt-ts-tracker.ts +58 -0
  504. package/src/runtime/routes/approval-routes.ts +12 -17
  505. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  506. package/src/runtime/routes/avatar-routes.ts +20 -4
  507. package/src/runtime/routes/btw-routes.ts +1 -4
  508. package/src/runtime/routes/conversation-management-routes.ts +20 -2
  509. package/src/runtime/routes/conversation-routes.ts +133 -27
  510. package/src/runtime/routes/debug-routes.ts +1 -1
  511. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  512. package/src/runtime/routes/events-routes.ts +16 -0
  513. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  514. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  515. package/src/runtime/routes/home-feed-routes.ts +120 -2
  516. package/src/runtime/routes/inbound-message-handler.ts +912 -2
  517. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  518. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  519. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  520. package/src/runtime/routes/integrations/slack/channel.ts +25 -3
  521. package/src/runtime/routes/llm-context-normalization.ts +23 -1
  522. package/src/runtime/routes/migration-routes.ts +720 -124
  523. package/src/runtime/routes/settings-routes.ts +4 -2
  524. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  525. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  526. package/src/runtime/routes/work-items-routes.ts +3 -2
  527. package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
  528. package/src/runtime/services/analyze-conversation.ts +12 -16
  529. package/src/runtime/skill-route-registry.ts +28 -6
  530. package/src/schedule/scheduler.ts +8 -0
  531. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  532. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  533. package/src/security/oauth2.ts +98 -35
  534. package/src/security/secure-keys.ts +7 -8
  535. package/src/security/token-manager.ts +27 -13
  536. package/src/security/untrusted-content.ts +102 -0
  537. package/src/skills/catalog-cache.ts +26 -7
  538. package/src/skills/catalog-install.ts +31 -3
  539. package/src/skills/skill-cache-store.ts +97 -0
  540. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
  541. package/src/stt/daemon-batch-transcriber.ts +33 -0
  542. package/src/stt/stt-stream-session.ts +8 -1
  543. package/src/stt/types.ts +5 -1
  544. package/src/subagent/manager.ts +41 -13
  545. package/src/tasks/ephemeral-permissions.ts +9 -4
  546. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  547. package/src/tools/browser/__tests__/browser-status.test.ts +45 -2
  548. package/src/tools/browser/browser-execution.ts +65 -38
  549. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
  550. package/src/tools/credentials/tool-policy.ts +39 -5
  551. package/src/tools/credentials/vault.ts +9 -4
  552. package/src/tools/executor.ts +4 -0
  553. package/src/tools/filesystem/write.ts +52 -0
  554. package/src/tools/host-terminal/host-shell.ts +45 -5
  555. package/src/tools/memory/register.test.ts +185 -0
  556. package/src/tools/memory/register.ts +3 -1
  557. package/src/tools/network/web-fetch.ts +20 -10
  558. package/src/tools/network/web-search.ts +19 -4
  559. package/src/tools/permission-checker.ts +36 -15
  560. package/src/tools/policy-context.ts +25 -8
  561. package/src/tools/registry.ts +55 -3
  562. package/src/tools/side-effects.ts +0 -11
  563. package/src/tools/skills/execute.ts +2 -2
  564. package/src/tools/skills/sandbox-runner.ts +5 -2
  565. package/src/tools/terminal/backends/native.ts +51 -2
  566. package/src/tools/terminal/safe-env.ts +3 -2
  567. package/src/tools/terminal/shell.ts +1 -0
  568. package/src/tools/tool-manifest.ts +6 -21
  569. package/src/tools/types.ts +12 -3
  570. package/src/tools/verification-control-plane-policy.ts +1 -1
  571. package/src/tts/__tests__/provider-adapters.test.ts +240 -13
  572. package/src/tts/provider-catalog.ts +18 -0
  573. package/src/tts/providers/index.ts +2 -0
  574. package/src/tts/providers/xai-provider.ts +224 -0
  575. package/src/tts/types.ts +46 -0
  576. package/src/types/tar-stream.d.ts +66 -0
  577. package/src/util/json.ts +17 -0
  578. package/src/util/platform.ts +2 -2
  579. package/src/util/pricing.ts +15 -5
  580. package/src/watcher/engine.ts +1 -1
  581. package/src/watcher/providers/google-calendar.ts +134 -8
  582. package/src/watcher/providers/outlook-calendar.ts +42 -2
  583. package/src/workspace/git-service.ts +23 -4
  584. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  585. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  586. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  587. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +57 -0
  588. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  589. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  590. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  591. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  592. package/src/workspace/migrations/AGENTS.md +1 -1
  593. package/src/workspace/migrations/registry.ts +16 -0
  594. package/src/workspace/provider-commit-message-generator.ts +19 -38
  595. package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
  596. package/src/__tests__/gmail-archive-gate.test.ts +0 -246
  597. package/src/__tests__/gmail-preferences.test.ts +0 -117
  598. package/src/__tests__/outlook-attachments.test.ts +0 -301
  599. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  600. package/src/__tests__/outlook-categories.test.ts +0 -212
  601. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  602. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  603. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  604. package/src/__tests__/outlook-trash.test.ts +0 -77
  605. package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
  606. package/src/__tests__/update-bulletin-format.test.ts +0 -181
  607. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  608. package/src/__tests__/update-bulletin.test.ts +0 -478
  609. package/src/__tests__/update-template-contract.test.ts +0 -29
  610. package/src/cli/commands/doctor.ts +0 -341
  611. package/src/config/bundled-skills/browser/SKILL.md +0 -88
  612. package/src/config/bundled-skills/browser/TOOLS.json +0 -516
  613. package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
  614. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  615. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  616. package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
  617. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  618. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  619. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  620. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  621. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  622. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  623. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  624. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  625. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  626. package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
  627. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  628. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
  629. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  630. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  631. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  632. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  633. package/src/config/bundled-skills/gmail/SKILL.md +0 -221
  634. package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
  635. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
  636. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  637. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  638. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  639. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  640. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  641. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  642. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
  643. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
  644. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
  645. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  646. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
  647. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  648. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  649. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  650. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  651. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  652. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  653. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  654. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  655. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  656. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  657. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  658. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  659. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  660. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  661. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  662. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  663. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  664. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  665. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  666. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  667. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  668. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  669. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  670. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  671. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  672. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  673. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  674. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  675. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  676. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  677. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  678. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  679. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  680. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  681. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  682. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  683. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  684. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  685. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  686. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  687. package/src/config/bundled-skills/slack/SKILL.md +0 -108
  688. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  689. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  690. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  691. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  692. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  693. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  694. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  695. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  696. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  697. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  698. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  699. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  700. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  701. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  702. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  703. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  704. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  705. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  706. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  707. package/src/prompts/templates/UPDATES.md +0 -50
  708. package/src/prompts/update-bulletin-format.ts +0 -85
  709. package/src/prompts/update-bulletin-state.ts +0 -58
  710. package/src/prompts/update-bulletin-template-path.ts +0 -13
  711. package/src/prompts/update-bulletin.ts +0 -139
  712. package/src/shared/provider-env-vars.ts +0 -19
  713. package/src/tools/watcher/create.ts +0 -86
  714. package/src/tools/watcher/delete.ts +0 -36
  715. package/src/tools/watcher/digest.ts +0 -54
  716. package/src/tools/watcher/list.ts +0 -83
  717. package/src/tools/watcher/update.ts +0 -71
@@ -212,19 +212,7 @@ describe("isWorkspaceScopedInvocation", () => {
212
212
  // ── Network tools ──────────────────────────────────────────────────
213
213
 
214
214
  describe("network tools", () => {
215
- const networkTools = [
216
- "web_search",
217
- "web_fetch",
218
- "browser_navigate",
219
- "browser_click",
220
- "browser_type",
221
- "browser_scroll",
222
- "browser_screenshot",
223
- "browser_close",
224
- "browser_attach",
225
- "browser_detach",
226
- "network_request",
227
- ];
215
+ const networkTools = ["web_search", "web_fetch", "network_request"];
228
216
 
229
217
  for (const tool of networkTools) {
230
218
  test(`${tool} is NOT workspace-scoped`, () => {
@@ -412,12 +412,11 @@ function mapDecisionToOptionId(
412
412
  decision === "allow_10m" ||
413
413
  decision === "allow_conversation" ||
414
414
  decision === "always_allow" ||
415
- decision === "always_allow_high_risk" ||
416
415
  decision === "temporary_override";
417
416
 
418
417
  if (isAllow) {
419
418
  // Prefer allow_always for persistent decisions, fallback to allow_once
420
- if (decision === "always_allow" || decision === "always_allow_high_risk") {
419
+ if (decision === "always_allow") {
421
420
  const alwaysOpt = options.find((o) => o.kind === "allow_always");
422
421
  if (alwaysOpt) return alwaysOpt.optionId;
423
422
  }
package/src/agent/loop.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import * as Sentry from "@sentry/node";
2
2
 
3
- import { estimateToolsTokens } from "../context/token-estimator.js";
3
+ import type { LLMCallSite } from "../config/schemas/llm.js";
4
+ import {
5
+ estimatePromptTokensRaw,
6
+ estimateToolsTokens,
7
+ getCalibrationProviderKey,
8
+ } from "../context/token-estimator.js";
4
9
  import { truncateOversizedToolResults } from "../context/tool-result-truncation.js";
5
10
  import { getHookManager } from "../hooks/manager.js";
6
11
  import type {
@@ -15,7 +20,9 @@ import {
15
20
  applyStreamingSubstitution,
16
21
  applySubstitutions,
17
22
  } from "../tools/sensitive-output-placeholders.js";
23
+ import { ProviderError } from "../util/errors.js";
18
24
  import { getLogger } from "../util/logger.js";
25
+ import { isRetryableNetworkError } from "../util/retry.js";
19
26
 
20
27
  const log = getLogger("agent-loop");
21
28
 
@@ -23,7 +30,7 @@ export interface AgentLoopConfig {
23
30
  maxTokens: number;
24
31
  maxInputTokens?: number; // context window size for tool result truncation
25
32
  thinking?: { enabled: boolean };
26
- effort: "low" | "medium" | "high" | "max";
33
+ effort: "low" | "medium" | "high" | "xhigh" | "max";
27
34
  speed?: "standard" | "fast";
28
35
  toolChoice?:
29
36
  | { type: "auto" }
@@ -100,6 +107,13 @@ export type AgentEvent =
100
107
  providerDurationMs: number;
101
108
  rawRequest?: unknown;
102
109
  rawResponse?: unknown;
110
+ /**
111
+ * Pre-send token estimate for the same call. Used by the estimator
112
+ * calibrator to learn how off the heuristic is versus provider
113
+ * ground truth. Omitted only when estimation genuinely was not run
114
+ * for this call (e.g. legacy/stubbed code paths).
115
+ */
116
+ estimatedInputTokens?: number;
103
117
  };
104
118
 
105
119
  const DEFAULT_CONFIG: AgentLoopConfig = {
@@ -111,6 +125,42 @@ const DEFAULT_CONFIG: AgentLoopConfig = {
111
125
  const MAX_CONSECUTIVE_ERROR_NUDGES = 3;
112
126
  const MAX_EMPTY_RESPONSE_RETRIES = 1;
113
127
 
128
+ /**
129
+ * User-config HTTP status codes that should never page the on-call: billing
130
+ * exhaustion (402), invalid credentials (401), and forbidden/plan-gated (403).
131
+ * The user-facing error path already surfaces an actionable message (e.g.
132
+ * credits_exhausted); a Sentry issue adds noise without engineering signal.
133
+ */
134
+ const USER_CONFIG_STATUS_CODES = new Set([401, 402, 403]);
135
+
136
+ /**
137
+ * Whether an agent-loop error should be reported to Sentry. Suppresses:
138
+ *
139
+ * - `ProviderError` carrying a user-config status code (401/402/403) — these
140
+ * are bad API keys, exhausted billing, or plan gates, not engineering bugs.
141
+ * - Retry-exhausted transient network errors (`retriesExhausted === true` +
142
+ * still categorized as retryable network) — the retry loop already tried
143
+ * its best; the user's network was flaky, not our code.
144
+ *
145
+ * Everything else (5xx with no retry-exhaustion tag, surprise errors, tool
146
+ * failures, etc.) still pages.
147
+ */
148
+ export function shouldCaptureAgentLoopError(err: Error): boolean {
149
+ if (
150
+ err instanceof ProviderError &&
151
+ err.statusCode !== undefined &&
152
+ USER_CONFIG_STATUS_CODES.has(err.statusCode)
153
+ ) {
154
+ return false;
155
+ }
156
+ const exhausted = (err as Error & { retriesExhausted?: boolean })
157
+ .retriesExhausted;
158
+ if (exhausted === true && isRetryableNetworkError(err)) {
159
+ return false;
160
+ }
161
+ return true;
162
+ }
163
+
114
164
  export interface ResolvedSystemPrompt {
115
165
  systemPrompt: string;
116
166
  maxTokens?: number;
@@ -204,8 +254,10 @@ export class AgentLoop {
204
254
  signal?: AbortSignal,
205
255
  requestId?: string,
206
256
  onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
257
+ callSite?: LLMCallSite,
207
258
  ): Promise<Message[]> {
208
259
  const history = [...messages];
260
+ const initialHistoryLength = messages.length;
209
261
  let toolUseTurns = 0;
210
262
  let consecutiveErrorTurns = 0;
211
263
  let emptyResponseRetries = 0;
@@ -221,6 +273,11 @@ export class AgentLoop {
221
273
  while (true) {
222
274
  if (signal?.aborted) break;
223
275
 
276
+ rlog.info(
277
+ { turn: toolUseTurns, messageCount: history.length },
278
+ "Agent loop iteration start",
279
+ );
280
+
224
281
  let toolUseBlocks: Extract<ContentBlock, { type: "tool_use" }>[] = [];
225
282
 
226
283
  try {
@@ -235,25 +292,47 @@ export class AgentLoop {
235
292
  ? this.resolveSystemPrompt(history)
236
293
  : null;
237
294
  const turnSystemPrompt = resolved?.systemPrompt ?? this.systemPrompt;
238
- const turnMaxTokens = resolved?.maxTokens ?? this.config.maxTokens;
239
295
  const turnModel = resolved?.model;
240
296
 
241
- const providerConfig: Record<string, unknown> = {
242
- max_tokens: turnMaxTokens,
243
- };
244
- if (turnModel) {
245
- providerConfig.model = turnModel;
246
- }
247
- if (this.config.thinking?.enabled) {
248
- providerConfig.thinking = { type: "adaptive" };
297
+ // Field precedence (highest wins):
298
+ // 1. Per-turn explicit (`resolved.maxTokens` / `resolved.model`)
299
+ // 2. Call-site resolved values (filled by
300
+ // `RetryProvider.normalizeSendMessageOptions` from
301
+ // `resolveCallSiteConfig(callSite, llm)`)
302
+ // 3. Conversation defaults (`this.config.*`, sourced from
303
+ // `llm.default`)
304
+ //
305
+ // When `callSite` is present we deliberately leave
306
+ // `max_tokens`/`thinking`/`effort`/`speed` *unset* in `providerConfig`
307
+ // so the normalizer can fill them from the call-site resolution. The
308
+ // normalizer only writes these fields when they're undefined; if we
309
+ // pre-set them from `this.config` here, every per-call-site override
310
+ // for these knobs is silently ignored.
311
+ //
312
+ // `toolChoice` and `cacheTtl` are not part of the call-site schema, so
313
+ // they always come from `this.config` regardless of `callSite`.
314
+ const providerConfig: Record<string, unknown> = {};
315
+
316
+ if (resolved?.maxTokens !== undefined) {
317
+ providerConfig.max_tokens = resolved.maxTokens;
318
+ } else if (!callSite) {
319
+ providerConfig.max_tokens = this.config.maxTokens;
249
320
  }
250
321
 
251
- if (this.config.effort) {
252
- providerConfig.effort = this.config.effort;
322
+ if (turnModel) {
323
+ providerConfig.model = turnModel;
253
324
  }
254
325
 
255
- if (this.config.speed && this.config.speed !== "standard") {
256
- providerConfig.speed = this.config.speed;
326
+ if (!callSite) {
327
+ if (this.config.thinking?.enabled) {
328
+ providerConfig.thinking = { type: "adaptive" };
329
+ }
330
+ if (this.config.effort) {
331
+ providerConfig.effort = this.config.effort;
332
+ }
333
+ if (this.config.speed && this.config.speed !== "standard") {
334
+ providerConfig.speed = this.config.speed;
335
+ }
257
336
  }
258
337
 
259
338
  if (this.config.toolChoice) {
@@ -264,6 +343,17 @@ export class AgentLoop {
264
343
  providerConfig.cacheTtl = this.config.cacheTtl;
265
344
  }
266
345
 
346
+ // Per-call LLM call-site identifier. Surfaces on the per-call
347
+ // `config.callSite` so `RetryProvider.normalizeSendMessageOptions`
348
+ // can route through `resolveCallSiteConfig` against
349
+ // `llm.callSites.<id>` (falling back to `llm.default` when absent).
350
+ // User-initiated conversation turns default to `mainAgent` in the
351
+ // agent loop's caller; other invocation contexts (heartbeat, filing,
352
+ // analyze, etc.) pass their own `callSite`.
353
+ if (callSite) {
354
+ providerConfig.callSite = callSite;
355
+ }
356
+
267
357
  const preLlmResult = await getHookManager().trigger("pre-llm-call", {
268
358
  systemPrompt: turnSystemPrompt,
269
359
  messages: history,
@@ -292,6 +382,25 @@ export class AgentLoop {
292
382
  const providerStart = Date.now();
293
383
  lastLlmCallTime = providerStart;
294
384
 
385
+ // Compute the pre-send estimate against the full in-memory
386
+ // history — matching what upstream callers of
387
+ // `estimatePromptTokens` (preflight, mid-loop checkpoints, the
388
+ // window manager) see. We use the RAW estimate (before applying
389
+ // the existing correction) so the calibrator learns the true
390
+ // bias against provider ground truth instead of ratcheting a
391
+ // feedback loop against its own corrected output.
392
+ const toolTokenBudget =
393
+ currentTools.length > 0 ? estimateToolsTokens(currentTools) : 0;
394
+ const preSendEstimatedTokens = estimatePromptTokensRaw(
395
+ history,
396
+ turnSystemPrompt,
397
+ {
398
+ providerName: getCalibrationProviderKey(this.provider),
399
+ toolTokenBudget,
400
+ },
401
+ );
402
+ rlog.info({ turn: toolUseTurns }, "LLM call start");
403
+
295
404
  // Strip image contentBlocks from older tool results to prevent
296
405
  // screenshots from accumulating in the context window. The LLM
297
406
  // already saw each image on the turn it was captured; keeping
@@ -372,6 +481,7 @@ export class AgentLoop {
372
481
  providerDurationMs,
373
482
  rawRequest: response.rawRequest,
374
483
  rawResponse: response.rawResponse,
484
+ estimatedInputTokens: preSendEstimatedTokens,
375
485
  });
376
486
 
377
487
  void getHookManager().trigger("post-llm-call", {
@@ -407,17 +517,65 @@ export class AgentLoop {
407
517
  block.type === "tool_use",
408
518
  );
409
519
 
520
+ rlog.info(
521
+ {
522
+ turn: toolUseTurns,
523
+ stopReason: response.stopReason,
524
+ contentBlocks: response.content.length,
525
+ toolUseCount: toolUseBlocks.length,
526
+ durationMs: providerDurationMs,
527
+ },
528
+ "LLM call complete",
529
+ );
530
+
410
531
  // Detect empty responses: no user-visible text and no tool calls.
411
532
  // This can happen when the model fails to produce output after
412
533
  // receiving a large tool result. Retry once with a nudge before
413
534
  // the message is persisted.
535
+ //
536
+ // Only nudge when the model hasn't already delivered text to the user
537
+ // earlier in this tool-use chain. If a prior assistant turn in history
538
+ // contained visible text (e.g. the model said its piece before calling
539
+ // a side-effect tool like `remember`), an empty follow-up is the model
540
+ // correctly ending its turn — nudging would mislead it into thinking
541
+ // its earlier text didn't land and cause a verbatim re-send.
542
+ //
543
+ // Note: we check ANY prior assistant turn from this run()
544
+ // invocation, not just the most recent one. In multi-step tool-use
545
+ // chains (say-something → call-tool → call-another-tool → end),
546
+ // the "say-something" text lives on an earlier assistant turn while
547
+ // the most recent assistant turn is a pure tool_use with no text.
548
+ // Restricting the check to the most recent assistant turn would
549
+ // falsely nudge in that case and trigger a duplicate re-send of
550
+ // text the user already saw.
551
+ //
552
+ // Scope the scan to messages appended during this run() call only.
553
+ // Assistant text from prior conversation turns (earlier run()
554
+ // invocations passed in via `messages`) must NOT suppress the
555
+ // nudge — those turns completed long ago and have no bearing on
556
+ // whether the current tool-use chain has delivered text yet.
414
557
  const hasVisibleText = response.content.some(
415
558
  (block) => block.type === "text" && block.text.trim().length > 0,
416
559
  );
560
+ const priorAssistantHadVisibleText = (() => {
561
+ for (let i = history.length - 1; i >= initialHistoryLength; i--) {
562
+ const msg = history[i];
563
+ if (msg.role !== "assistant") continue;
564
+ const hasText = msg.content.some(
565
+ (block) =>
566
+ block.type === "text" &&
567
+ typeof (block as { text?: unknown }).text === "string" &&
568
+ (block as { text: string }).text.trim().length > 0,
569
+ );
570
+ if (hasText) return true;
571
+ }
572
+ return false;
573
+ })();
417
574
  if (
418
575
  !hasVisibleText &&
419
576
  toolUseBlocks.length === 0 &&
420
577
  toolUseTurns > 0 &&
578
+ !priorAssistantHadVisibleText &&
421
579
  emptyResponseRetries < MAX_EMPTY_RESPONSE_RETRIES
422
580
  ) {
423
581
  emptyResponseRetries++;
@@ -437,7 +595,12 @@ export class AgentLoop {
437
595
  continue;
438
596
  }
439
597
 
440
- if (!hasVisibleText && toolUseBlocks.length === 0 && toolUseTurns > 0) {
598
+ if (
599
+ !hasVisibleText &&
600
+ toolUseBlocks.length === 0 &&
601
+ toolUseTurns > 0 &&
602
+ !priorAssistantHadVisibleText
603
+ ) {
441
604
  rlog.error(
442
605
  { turn: toolUseTurns, retries: emptyResponseRetries },
443
606
  "Model returned empty response after tool results — retries exhausted",
@@ -479,6 +642,15 @@ export class AgentLoop {
479
642
  // Execute all tools concurrently for reduced latency.
480
643
  // Race against the abort signal so cancellation isn't blocked by
481
644
  // stuck tools (e.g. a hung browser navigation).
645
+ const toolExecStart = Date.now();
646
+ rlog.info(
647
+ {
648
+ turn: toolUseTurns,
649
+ toolNames: toolUseBlocks.map((t) => t.name),
650
+ },
651
+ "Tool execution start",
652
+ );
653
+
482
654
  const toolExecutionPromise = Promise.all(
483
655
  toolUseBlocks.map(async (toolUse) => {
484
656
  const result = await this.toolExecutor!(
@@ -522,6 +694,15 @@ export class AgentLoop {
522
694
  toolResults = await toolExecutionPromise;
523
695
  }
524
696
 
697
+ rlog.info(
698
+ {
699
+ turn: toolUseTurns,
700
+ toolCount: toolResults.length,
701
+ durationMs: Date.now() - toolExecStart,
702
+ },
703
+ "Tool execution complete",
704
+ );
705
+
525
706
  // Merge sensitive output bindings from tool results into the
526
707
  // per-run substitution map. Bindings carry placeholder->value pairs
527
708
  // that are resolved in streamed text deltas and final message text.
@@ -653,12 +834,23 @@ export class AgentLoop {
653
834
  { err, turn: toolUseTurns, messageCount: history.length },
654
835
  "Agent loop error during turn processing",
655
836
  );
656
- Sentry.captureException(err);
837
+ if (shouldCaptureAgentLoopError(err)) {
838
+ Sentry.captureException(err);
839
+ }
657
840
  onEvent({ type: "error", error: err });
658
841
  break;
659
842
  }
660
843
  }
661
844
 
845
+ rlog.info(
846
+ {
847
+ turns: toolUseTurns,
848
+ finalMessageCount: history.length,
849
+ aborted: signal?.aborted ?? false,
850
+ },
851
+ "Agent loop exited",
852
+ );
853
+
662
854
  return history;
663
855
  }
664
856
  }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Tests for resvg-lazy and the writeTraitsAndRenderAvatar fallback it feeds.
3
+ *
4
+ * Covers the graceful-degradation path when the native @resvg/resvg-js binding
5
+ * is missing (e.g. bun install skipped the platform-specific optional
6
+ * dependency). The lazy loader must warn once and report unavailability, and
7
+ * writeTraitsAndRenderAvatar must return `native_unavailable` so the HTTP
8
+ * layer can respond 503 instead of 500.
9
+ *
10
+ * Bun's `mock.module` evaluates factories eagerly on re-registration, which
11
+ * makes it awkward to flip a module between "throws" and "returns fake
12
+ * export" across tests in the same file. We instead install a single
13
+ * throwing mock at module scope for the require-path test, and use the
14
+ * `__setResvgCacheForTests` / `__resetResvgCacheForTests` hooks to drive the
15
+ * rest of the cases deterministically.
16
+ */
17
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
18
+
19
+ type LogCall = { bindings: unknown; msg: string };
20
+ const warnCalls: LogCall[] = [];
21
+
22
+ mock.module("../util/logger.js", () => ({
23
+ getLogger: () => ({
24
+ warn: (bindings: unknown, msg: string) => {
25
+ warnCalls.push({ bindings, msg });
26
+ },
27
+ info: () => {},
28
+ error: () => {},
29
+ debug: () => {},
30
+ trace: () => {},
31
+ fatal: () => {},
32
+ }),
33
+ }));
34
+
35
+ mock.module("../util/platform.js", () => ({
36
+ AVATAR_IMAGE_FILENAME: "avatar-image.png",
37
+ getAvatarDir: () => "/tmp/vellum-test-avatar-never-written",
38
+ }));
39
+
40
+ // Install a throwing @resvg/resvg-js mock at module scope. Bun only calls
41
+ // this factory the first time the module is required within the test worker;
42
+ // later `require` calls return whatever was produced by that first call (or
43
+ // throw, if the factory threw). That's exactly what we want for the
44
+ // "require fails" test below.
45
+ mock.module("@resvg/resvg-js", () => {
46
+ throw new Error("Cannot require module @resvg/resvg-js-darwin-x64");
47
+ });
48
+
49
+ describe("resvg-lazy — require failure path", () => {
50
+ beforeEach(async () => {
51
+ warnCalls.length = 0;
52
+ const { __resetResvgCacheForTests } = await import("./resvg-lazy.js");
53
+ __resetResvgCacheForTests();
54
+ });
55
+
56
+ test("real require failure triggers warn log with platform context", async () => {
57
+ const { isResvgAvailable } = await import("./resvg-lazy.js");
58
+
59
+ // Call twice — caching must prevent a duplicate warn.
60
+ expect(isResvgAvailable()).toBe(false);
61
+ expect(isResvgAvailable()).toBe(false);
62
+
63
+ expect(warnCalls.length).toBe(1);
64
+ const call = warnCalls[0]!;
65
+ const bindings = call.bindings as Record<string, unknown>;
66
+ expect(bindings.platform).toBe(process.platform);
67
+ expect(bindings.arch).toBe(process.arch);
68
+ expect(String(bindings.module)).toContain("@resvg/resvg-js-");
69
+ expect(bindings.err).toBeInstanceOf(Error);
70
+ expect(call.msg).toContain("@resvg/resvg-js");
71
+ });
72
+ });
73
+
74
+ describe("resvg-lazy — API shape when unavailable", () => {
75
+ beforeEach(async () => {
76
+ warnCalls.length = 0;
77
+ const { __setResvgCacheForTests } = await import("./resvg-lazy.js");
78
+ __setResvgCacheForTests({
79
+ available: false,
80
+ error: new Error("Cannot require module @resvg/resvg-js-darwin-x64"),
81
+ });
82
+ });
83
+
84
+ test("isResvgAvailable returns false", async () => {
85
+ const { isResvgAvailable } = await import("./resvg-lazy.js");
86
+ expect(isResvgAvailable()).toBe(false);
87
+ });
88
+
89
+ test("getResvg throws the underlying error", async () => {
90
+ const { getResvg } = await import("./resvg-lazy.js");
91
+ expect(() => getResvg()).toThrow(
92
+ /Cannot require module @resvg\/resvg-js-darwin-x64/,
93
+ );
94
+ });
95
+ });
96
+
97
+ describe("writeTraitsAndRenderAvatar — native module missing", () => {
98
+ beforeEach(async () => {
99
+ warnCalls.length = 0;
100
+ const { __setResvgCacheForTests } = await import("./resvg-lazy.js");
101
+ __setResvgCacheForTests({
102
+ available: false,
103
+ error: new Error("Cannot require module @resvg/resvg-js-darwin-x64"),
104
+ });
105
+ });
106
+
107
+ test("returns native_unavailable without attempting disk writes", async () => {
108
+ const { writeTraitsAndRenderAvatar } = await import("./traits-png-sync.js");
109
+
110
+ const result = writeTraitsAndRenderAvatar({
111
+ bodyShape: "blob",
112
+ eyeStyle: "curious",
113
+ color: "green",
114
+ });
115
+
116
+ expect(result.ok).toBe(false);
117
+ if (result.ok) return; // narrow for TypeScript
118
+ expect(result.reason).toBe("native_unavailable");
119
+ expect(result.message).toContain("@resvg/resvg-js");
120
+ });
121
+
122
+ test("returns invalid_traits (not native_unavailable) when traits are bad", async () => {
123
+ const { writeTraitsAndRenderAvatar } = await import("./traits-png-sync.js");
124
+
125
+ // Empty traits object fails the shape check before we ever consult resvg.
126
+ const result = writeTraitsAndRenderAvatar({
127
+ bodyShape: "",
128
+ eyeStyle: "",
129
+ color: "",
130
+ });
131
+
132
+ expect(result.ok).toBe(false);
133
+ if (result.ok) return;
134
+ expect(result.reason).toBe("invalid_traits");
135
+ });
136
+ });
@@ -1,21 +1,94 @@
1
1
  import type { Resvg as ResvgType } from "@resvg/resvg-js";
2
2
 
3
- let ResvgClass: typeof ResvgType | undefined;
3
+ import { getLogger } from "../util/logger.js";
4
+
5
+ const log = getLogger("resvg-lazy");
6
+
7
+ type ResvgLoadResult =
8
+ | { available: true; Resvg: typeof ResvgType }
9
+ | { available: false; error: Error };
10
+
11
+ let cached: ResvgLoadResult | undefined;
12
+
13
+ function loadResvg(): ResvgLoadResult {
14
+ try {
15
+ // Inline require is necessary here: @resvg/resvg-js loads a platform-specific
16
+ // native .node addon at import time. A top-level import would crash the daemon
17
+ // on startup inside bun --compile binaries where native addons are unavailable.
18
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
19
+ const mod = require("@resvg/resvg-js") as typeof import("@resvg/resvg-js");
20
+ return { available: true, Resvg: mod.Resvg };
21
+ } catch (err) {
22
+ const error = err instanceof Error ? err : new Error(String(err));
23
+ // Log once at warn level (not error) — the daemon should keep running and
24
+ // callers should fall back to a non-native path (ASCII only, or a 503 at
25
+ // the HTTP layer). Emitting at error level would page Sentry on every
26
+ // install that skipped the platform-specific optional dependency.
27
+ log.warn(
28
+ {
29
+ err: error,
30
+ platform: process.platform,
31
+ arch: process.arch,
32
+ module: `@resvg/resvg-js-${process.platform}-${process.arch}`,
33
+ },
34
+ "Failed to load @resvg/resvg-js native module — avatar PNG rendering will be unavailable. " +
35
+ "The platform-specific optional dependency is likely missing.",
36
+ );
37
+ return { available: false, error };
38
+ }
39
+ }
40
+
41
+ function getLoadResult(): ResvgLoadResult {
42
+ if (!cached) {
43
+ cached = loadResvg();
44
+ }
45
+ return cached;
46
+ }
47
+
48
+ /**
49
+ * Returns `true` if the native @resvg/resvg-js binding loaded successfully on
50
+ * first access. Callers should check this before calling `getResvg()` and fall
51
+ * back to a non-native path (e.g. ASCII-only rendering, or a 503 at the HTTP
52
+ * layer) when it is `false`.
53
+ *
54
+ * Loading is attempted lazily on first access and the result is cached, so
55
+ * calling this multiple times is cheap and does not re-emit warnings.
56
+ */
57
+ export function isResvgAvailable(): boolean {
58
+ return getLoadResult().available;
59
+ }
4
60
 
5
61
  /**
6
62
  * Returns the Resvg constructor, loading the native module on first call.
7
63
  * Defers the native-addon require so the daemon can start even when the
8
64
  * platform-specific binary is unavailable (e.g. inside a bun --compile
9
65
  * single-file executable).
66
+ *
67
+ * Throws if the native module could not be loaded. Callers that need to
68
+ * degrade gracefully should check `isResvgAvailable()` first.
10
69
  */
11
70
  export function getResvg(): typeof ResvgType {
12
- if (!ResvgClass) {
13
- // Inline require is necessary here: @resvg/resvg-js loads a platform-specific
14
- // native .node addon at import time. A top-level import would crash the daemon
15
- // on startup inside bun --compile binaries where native addons are unavailable.
16
- // eslint-disable-next-line @typescript-eslint/no-require-imports
17
- const mod = require("@resvg/resvg-js") as typeof import("@resvg/resvg-js");
18
- ResvgClass = mod.Resvg;
71
+ const result = getLoadResult();
72
+ if (!result.available) {
73
+ throw result.error;
19
74
  }
20
- return ResvgClass;
75
+ return result.Resvg;
76
+ }
77
+
78
+ /**
79
+ * Test-only hook to reset the cached load result between test cases. Do not
80
+ * call from production code.
81
+ */
82
+ export function __resetResvgCacheForTests(): void {
83
+ cached = undefined;
84
+ }
85
+
86
+ /**
87
+ * Test-only hook to force the cached load result to a specific state without
88
+ * exercising the real `require`. Useful for asserting the unavailable path
89
+ * without depending on Bun's module-mock behavior (which re-imports the real
90
+ * module after the factory throws once).
91
+ */
92
+ export function __setResvgCacheForTests(result: ResvgLoadResult): void {
93
+ cached = result;
21
94
  }
@@ -7,6 +7,7 @@ import { AVATAR_IMAGE_FILENAME, getAvatarDir } from "../util/platform.js";
7
7
  import { renderCharacterAscii } from "./ascii-renderer.js";
8
8
  import { getCharacterComponents } from "./character-components.js";
9
9
  import { renderCharacterPng } from "./png-renderer.js";
10
+ import { isResvgAvailable } from "./resvg-lazy.js";
10
11
 
11
12
  const log = getLogger("traits-png-sync");
12
13
 
@@ -20,7 +21,7 @@ export type TraitsSyncResult =
20
21
  | { ok: true; asciiWritten: boolean }
21
22
  | {
22
23
  ok: false;
23
- reason: "invalid_traits" | "render_error";
24
+ reason: "invalid_traits" | "render_error" | "native_unavailable";
24
25
  message: string;
25
26
  };
26
27
 
@@ -144,6 +145,25 @@ export function writeTraitsAndRenderAvatar(
144
145
  };
145
146
  }
146
147
 
148
+ // Short-circuit before touching disk when the native rasterizer is missing.
149
+ // Both PNG and ASCII rendering route through @resvg/resvg-js, so without it
150
+ // we cannot produce either artifact. Callers translate this into a 503 so
151
+ // the HTTP route returns an actionable status rather than a 500.
152
+ if (!isResvgAvailable()) {
153
+ log.warn(
154
+ { traits },
155
+ "Skipping avatar render — native @resvg/resvg-js binding is unavailable",
156
+ );
157
+ return {
158
+ ok: false,
159
+ reason: "native_unavailable",
160
+ message:
161
+ "Avatar PNG rendering is unavailable on this platform because the " +
162
+ "@resvg/resvg-js native binding failed to load. Reinstall dependencies " +
163
+ "to pull the platform-specific optional package.",
164
+ };
165
+ }
166
+
147
167
  const avatarDir = getAvatarDir();
148
168
  const traitsPath = join(avatarDir, "character-traits.json");
149
169