@vellumai/assistant 0.4.52 → 0.4.54

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 (380) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/bun.lock +62 -349
  3. package/docs/architecture/integrations.md +1 -1
  4. package/docs/architecture/keychain-broker.md +91 -40
  5. package/docs/architecture/memory.md +3 -3
  6. package/docs/architecture/security.md +2 -2
  7. package/knip.json +7 -29
  8. package/package.json +2 -9
  9. package/src/__tests__/agent-loop.test.ts +1 -1
  10. package/src/__tests__/app-git-history.test.ts +0 -2
  11. package/src/__tests__/app-git-service.test.ts +1 -6
  12. package/src/__tests__/approval-cascade.test.ts +3 -2
  13. package/src/__tests__/approval-routes-http.test.ts +0 -1
  14. package/src/__tests__/asset-materialize-tool.test.ts +0 -1
  15. package/src/__tests__/asset-search-tool.test.ts +0 -1
  16. package/src/__tests__/assistant-events-sse-hardening.test.ts +0 -1
  17. package/src/__tests__/attachments-store.test.ts +0 -1
  18. package/src/__tests__/avatar-e2e.test.ts +5 -1
  19. package/src/__tests__/browser-fill-credential.test.ts +4 -6
  20. package/src/__tests__/btw-routes.test.ts +39 -0
  21. package/src/__tests__/call-controller.test.ts +0 -1
  22. package/src/__tests__/call-domain.test.ts +1 -1
  23. package/src/__tests__/call-routes-http.test.ts +1 -3
  24. package/src/__tests__/canonical-guardian-store.test.ts +33 -2
  25. package/src/__tests__/channel-guardian.test.ts +4 -4
  26. package/src/__tests__/channel-readiness-routes.test.ts +0 -1
  27. package/src/__tests__/channel-readiness-service.test.ts +1 -1
  28. package/src/__tests__/checker.test.ts +13 -11
  29. package/src/__tests__/claude-code-skill-regression.test.ts +5 -2
  30. package/src/__tests__/claude-code-tool-profiles.test.ts +7 -3
  31. package/src/__tests__/config-loader-backfill.test.ts +1 -5
  32. package/src/__tests__/config-schema.test.ts +9 -46
  33. package/src/__tests__/config-watcher.test.ts +11 -3
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -1
  35. package/src/__tests__/credential-broker-browser-fill.test.ts +27 -24
  36. package/src/__tests__/credential-broker-server-use.test.ts +76 -40
  37. package/src/__tests__/credential-security-e2e.test.ts +1 -6
  38. package/src/__tests__/credential-security-invariants.test.ts +27 -8
  39. package/src/__tests__/credential-vault-unit.test.ts +32 -16
  40. package/src/__tests__/credential-vault.test.ts +40 -28
  41. package/src/__tests__/credentials-cli.test.ts +1 -21
  42. package/src/__tests__/email-invite-adapter.test.ts +0 -1
  43. package/src/__tests__/error-handler-friendly-messages.test.ts +4 -5
  44. package/src/__tests__/fixtures/credential-security-fixtures.ts +3 -3
  45. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -79
  46. package/src/__tests__/gateway-only-enforcement.test.ts +1 -23
  47. package/src/__tests__/guardian-action-conversation-turn.test.ts +8 -8
  48. package/src/__tests__/guardian-action-late-reply.test.ts +13 -14
  49. package/src/__tests__/guardian-action-store.test.ts +0 -57
  50. package/src/__tests__/guardian-outbound-http.test.ts +1 -1
  51. package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -3
  52. package/src/__tests__/hooks-blocking.test.ts +1 -1
  53. package/src/__tests__/hooks-config.test.ts +5 -29
  54. package/src/__tests__/hooks-discovery.test.ts +1 -1
  55. package/src/__tests__/hooks-integration.test.ts +1 -1
  56. package/src/__tests__/hooks-manager.test.ts +1 -1
  57. package/src/__tests__/hooks-runner.test.ts +1 -23
  58. package/src/__tests__/hooks-settings.test.ts +1 -1
  59. package/src/__tests__/hooks-templates.test.ts +1 -1
  60. package/src/__tests__/host-shell-tool.test.ts +0 -1
  61. package/src/__tests__/http-user-message-parity.test.ts +19 -0
  62. package/src/__tests__/integration-status.test.ts +0 -1
  63. package/src/__tests__/invite-routes-http.test.ts +0 -3
  64. package/src/__tests__/list-messages-attachments.test.ts +0 -1
  65. package/src/__tests__/llm-usage-store.test.ts +50 -0
  66. package/src/__tests__/log-export-workspace.test.ts +233 -0
  67. package/src/__tests__/managed-proxy-context.test.ts +41 -41
  68. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  69. package/src/__tests__/media-generate-image.test.ts +9 -4
  70. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -7
  71. package/src/__tests__/memory-regressions.experimental.test.ts +4 -4
  72. package/src/__tests__/memory-regressions.test.ts +27 -28
  73. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  74. package/src/__tests__/memory-upsert-concurrency.test.ts +4 -4
  75. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  76. package/src/__tests__/migration-export-http.test.ts +0 -1
  77. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  78. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  79. package/src/__tests__/migration-validate-http.test.ts +0 -1
  80. package/src/__tests__/notification-decision-fallback.test.ts +1 -1
  81. package/src/__tests__/notification-schedule-dedup.test.ts +237 -0
  82. package/src/__tests__/oauth-cli.test.ts +2 -14
  83. package/src/__tests__/oauth-store.test.ts +3 -7
  84. package/src/__tests__/oauth2-gateway-transport.test.ts +5 -4
  85. package/src/__tests__/onboarding-starter-tasks.test.ts +1 -1
  86. package/src/__tests__/onboarding-template-contract.test.ts +1 -2
  87. package/src/__tests__/openai-provider.test.ts +7 -7
  88. package/src/__tests__/platform.test.ts +14 -4
  89. package/src/__tests__/pricing.test.ts +0 -234
  90. package/src/__tests__/provider-commit-message-generator.test.ts +19 -15
  91. package/src/__tests__/provider-fail-open-selection.test.ts +67 -62
  92. package/src/__tests__/provider-managed-proxy-integration.test.ts +88 -85
  93. package/src/__tests__/provider-registry-ollama.test.ts +10 -4
  94. package/src/__tests__/public-ingress-urls.test.ts +1 -1
  95. package/src/__tests__/recording-handler.test.ts +0 -1
  96. package/src/__tests__/registry.test.ts +3 -103
  97. package/src/__tests__/relay-server.test.ts +0 -1
  98. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  99. package/src/__tests__/runtime-events-sse-parity.test.ts +0 -1
  100. package/src/__tests__/runtime-events-sse.test.ts +0 -1
  101. package/src/__tests__/script-proxy-injection-runtime.test.ts +2 -7
  102. package/src/__tests__/secret-onetime-send.test.ts +1 -6
  103. package/src/__tests__/secret-routes-managed-proxy.test.ts +6 -14
  104. package/src/__tests__/secret-scanner-executor.test.ts +0 -1
  105. package/src/__tests__/secure-keys.test.ts +241 -229
  106. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  107. package/src/__tests__/session-abort-tool-results.test.ts +3 -2
  108. package/src/__tests__/session-agent-loop-overflow.test.ts +1012 -838
  109. package/src/__tests__/session-agent-loop.test.ts +2 -2
  110. package/src/__tests__/session-confirmation-signals.test.ts +3 -2
  111. package/src/__tests__/session-error.test.ts +5 -4
  112. package/src/__tests__/session-history-web-search.test.ts +34 -9
  113. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -7
  114. package/src/__tests__/session-pre-run-repair.test.ts +3 -2
  115. package/src/__tests__/session-provider-retry-repair.test.ts +31 -27
  116. package/src/__tests__/session-queue.test.ts +5 -5
  117. package/src/__tests__/session-runtime-assembly.test.ts +118 -0
  118. package/src/__tests__/session-slash-known.test.ts +31 -14
  119. package/src/__tests__/session-slash-queue.test.ts +3 -2
  120. package/src/__tests__/session-slash-unknown.test.ts +3 -2
  121. package/src/__tests__/session-workspace-cache-state.test.ts +3 -1
  122. package/src/__tests__/session-workspace-injection.test.ts +3 -2
  123. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -2
  124. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  125. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  126. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  127. package/src/__tests__/skillssh-registry.test.ts +21 -0
  128. package/src/__tests__/slack-channel-config.test.ts +1 -7
  129. package/src/__tests__/slack-share-routes.test.ts +1 -1
  130. package/src/__tests__/swarm-recursion.test.ts +4 -1
  131. package/src/__tests__/swarm-session-integration.test.ts +24 -14
  132. package/src/__tests__/swarm-tool.test.ts +4 -2
  133. package/src/__tests__/task-compiler.test.ts +1 -1
  134. package/src/__tests__/telegram-bot-username-resolution.test.ts +2 -4
  135. package/src/__tests__/test-support/browser-skill-harness.ts +0 -18
  136. package/src/__tests__/test-support/computer-use-skill-harness.ts +0 -23
  137. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1521 -0
  138. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  139. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  140. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  141. package/src/__tests__/tool-executor.test.ts +1 -2
  142. package/src/__tests__/trust-store.test.ts +8 -83
  143. package/src/__tests__/twilio-config.test.ts +0 -1
  144. package/src/__tests__/twilio-provider.test.ts +0 -5
  145. package/src/__tests__/twilio-routes.test.ts +2 -3
  146. package/src/__tests__/usage-cache-backfill-migration.test.ts +10 -10
  147. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  148. package/src/__tests__/voice-quality.test.ts +2 -1
  149. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  150. package/src/__tests__/web-search.test.ts +1 -1
  151. package/src/agent/loop.ts +17 -1
  152. package/src/bundler/app-bundler.ts +40 -24
  153. package/src/calls/call-controller.ts +16 -0
  154. package/src/calls/guardian-question-copy.ts +1 -1
  155. package/src/calls/relay-server.ts +29 -13
  156. package/src/calls/voice-control-protocol.ts +1 -0
  157. package/src/calls/voice-quality.ts +1 -1
  158. package/src/calls/voice-session-bridge.ts +9 -3
  159. package/src/channels/types.ts +16 -0
  160. package/src/cli/commands/bash.ts +173 -0
  161. package/src/cli/commands/doctor.ts +15 -57
  162. package/src/cli/commands/memory.ts +3 -5
  163. package/src/cli/commands/oauth/connections.ts +4 -2
  164. package/src/cli/commands/oauth/providers.ts +1 -13
  165. package/src/cli/commands/sessions.ts +1 -1
  166. package/src/cli/commands/usage.ts +359 -0
  167. package/src/cli/http-client.ts +22 -12
  168. package/src/cli/program.ts +4 -0
  169. package/src/cli/reference.ts +2 -0
  170. package/src/cli.ts +251 -181
  171. package/src/config/assistant-feature-flags.ts +0 -7
  172. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  173. package/src/config/bundled-skills/claude-code/SKILL.md +1 -1
  174. package/src/config/bundled-skills/claude-code/TOOLS.json +1 -1
  175. package/src/config/bundled-skills/gmail/SKILL.md +0 -1
  176. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +4 -3
  177. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  178. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +3 -5
  179. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -3
  180. package/src/config/bundled-skills/messaging/SKILL.md +0 -1
  181. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +1 -1
  182. package/src/config/bundled-skills/sequences/SKILL.md +0 -1
  183. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +5 -6
  184. package/src/config/env.ts +13 -0
  185. package/src/config/feature-flag-registry.json +15 -39
  186. package/src/config/loader.ts +7 -135
  187. package/src/config/schema.ts +0 -6
  188. package/src/config/schemas/channels.ts +1 -0
  189. package/src/config/schemas/elevenlabs.ts +2 -2
  190. package/src/config/schemas/security.ts +1 -2
  191. package/src/config/skills.ts +1 -1
  192. package/src/contacts/contact-store.ts +21 -75
  193. package/src/contacts/contacts-write.ts +6 -6
  194. package/src/contacts/types.ts +2 -0
  195. package/src/context/token-estimator.ts +35 -2
  196. package/src/context/window-manager.ts +16 -2
  197. package/src/daemon/approved-devices-store.ts +0 -44
  198. package/src/daemon/classifier.ts +1 -1
  199. package/src/daemon/config-watcher.ts +35 -11
  200. package/src/daemon/context-overflow-reducer.ts +13 -2
  201. package/src/daemon/handlers/config-ingress.ts +25 -8
  202. package/src/daemon/handlers/config-model.ts +22 -16
  203. package/src/daemon/handlers/config-telegram.ts +18 -6
  204. package/src/daemon/handlers/dictation.ts +0 -429
  205. package/src/daemon/handlers/sessions.ts +4 -116
  206. package/src/daemon/handlers/skills.ts +2 -201
  207. package/src/daemon/lifecycle.ts +21 -20
  208. package/src/daemon/message-types/contacts.ts +2 -0
  209. package/src/daemon/message-types/integrations.ts +1 -0
  210. package/src/daemon/message-types/sessions.ts +2 -0
  211. package/src/daemon/parse-actual-tokens-from-error.test.ts +75 -0
  212. package/src/daemon/providers-setup.ts +1 -1
  213. package/src/daemon/server.ts +42 -5
  214. package/src/daemon/session-agent-loop-handlers.ts +1 -1
  215. package/src/daemon/session-agent-loop.ts +27 -79
  216. package/src/daemon/session-error.ts +5 -4
  217. package/src/daemon/session-process.ts +17 -10
  218. package/src/daemon/session-runtime-assembly.ts +50 -0
  219. package/src/daemon/session-slash.ts +34 -22
  220. package/src/daemon/session.ts +1 -0
  221. package/src/daemon/shutdown-handlers.ts +15 -0
  222. package/src/daemon/watch-handler.ts +2 -2
  223. package/src/email/guardrails.ts +1 -1
  224. package/src/email/service.ts +0 -5
  225. package/src/events/domain-events.ts +1 -0
  226. package/src/hooks/templates.ts +1 -1
  227. package/src/media/app-icon-generator.ts +4 -3
  228. package/src/media/avatar-router.ts +5 -4
  229. package/src/media/gemini-image-service.ts +5 -5
  230. package/src/memory/admin.ts +2 -2
  231. package/src/memory/app-git-service.ts +0 -7
  232. package/src/memory/canonical-guardian-store.ts +25 -3
  233. package/src/memory/conversation-crud.ts +1 -1
  234. package/src/memory/conversation-title-service.ts +2 -2
  235. package/src/memory/db-init.ts +12 -0
  236. package/src/memory/embedding-backend.ts +46 -33
  237. package/src/memory/external-conversation-store.ts +0 -30
  238. package/src/memory/guardian-action-store.ts +0 -31
  239. package/src/memory/guardian-approvals.ts +1 -56
  240. package/src/memory/indexer.ts +4 -3
  241. package/src/memory/items-extractor.ts +1 -1
  242. package/src/memory/job-handlers/backfill.ts +5 -2
  243. package/src/memory/job-handlers/index-maintenance.ts +2 -2
  244. package/src/memory/job-handlers/media-processing.ts +2 -2
  245. package/src/memory/job-handlers/summarization.ts +1 -1
  246. package/src/memory/job-utils.ts +1 -2
  247. package/src/memory/jobs-worker.ts +2 -2
  248. package/src/memory/llm-usage-store.ts +57 -11
  249. package/src/memory/media-store.ts +4 -535
  250. package/src/memory/migrations/032-guardian-delivery-conversation-index.ts +2 -2
  251. package/src/memory/migrations/110-channel-guardian.ts +0 -1
  252. package/src/memory/migrations/158-channel-interaction-columns.ts +18 -0
  253. package/src/memory/migrations/159-drop-contact-interaction-columns.ts +16 -0
  254. package/src/memory/migrations/160-drop-loopback-port-column.ts +13 -0
  255. package/src/memory/migrations/index.ts +3 -0
  256. package/src/memory/published-pages-store.ts +0 -83
  257. package/src/memory/qdrant-circuit-breaker.ts +0 -8
  258. package/src/memory/retriever.test.ts +19 -12
  259. package/src/memory/retriever.ts +1 -1
  260. package/src/memory/schema/contacts.ts +2 -2
  261. package/src/memory/schema/oauth.ts +0 -1
  262. package/src/memory/search/semantic.ts +1 -8
  263. package/src/memory/shared-app-links-store.ts +0 -15
  264. package/src/messaging/registry.ts +0 -5
  265. package/src/messaging/style-analyzer.ts +1 -1
  266. package/src/notifications/copy-composer.ts +5 -13
  267. package/src/notifications/decision-engine.ts +2 -2
  268. package/src/notifications/deliveries-store.ts +0 -39
  269. package/src/notifications/guardian-question-mode.ts +6 -10
  270. package/src/notifications/preference-extractor.ts +1 -1
  271. package/src/oauth/byo-connection.test.ts +29 -20
  272. package/src/oauth/connect-orchestrator.ts +5 -3
  273. package/src/oauth/connect-types.ts +9 -2
  274. package/src/oauth/manual-token-connection.ts +9 -7
  275. package/src/oauth/oauth-store.ts +2 -8
  276. package/src/oauth/provider-behaviors.ts +11 -1
  277. package/src/oauth/seed-providers.ts +13 -5
  278. package/src/permissions/checker.ts +21 -2
  279. package/src/permissions/shell-identity.ts +0 -5
  280. package/src/permissions/trust-store.ts +0 -37
  281. package/src/prompts/__tests__/build-cli-reference-section.test.ts +1 -1
  282. package/src/prompts/system-prompt.ts +5 -14
  283. package/src/prompts/templates/BOOTSTRAP.md +1 -3
  284. package/src/providers/anthropic/client.ts +16 -8
  285. package/src/providers/managed-proxy/constants.ts +9 -11
  286. package/src/providers/managed-proxy/context.ts +14 -9
  287. package/src/providers/provider-send-message.ts +4 -52
  288. package/src/providers/registry.ts +29 -57
  289. package/src/providers/types.ts +1 -1
  290. package/src/runtime/actor-token-store.ts +0 -23
  291. package/src/runtime/auth/route-policy.ts +4 -0
  292. package/src/runtime/channel-invite-transports/telegram.ts +12 -6
  293. package/src/runtime/channel-retry-sweep.ts +6 -0
  294. package/src/runtime/http-router.ts +5 -1
  295. package/src/runtime/http-server.ts +101 -4
  296. package/src/runtime/http-types.ts +1 -0
  297. package/src/runtime/invite-instruction-generator.ts +25 -51
  298. package/src/runtime/invite-service.ts +0 -20
  299. package/src/runtime/middleware/error-handler.ts +1 -2
  300. package/src/runtime/routes/app-management-routes.ts +1 -0
  301. package/src/runtime/routes/attachment-routes.ts +1 -1
  302. package/src/runtime/routes/brain-graph-routes.ts +1 -1
  303. package/src/runtime/routes/btw-routes.ts +20 -1
  304. package/src/runtime/routes/call-routes.ts +1 -1
  305. package/src/runtime/routes/conversation-routes.ts +64 -24
  306. package/src/runtime/routes/debug-routes.ts +1 -1
  307. package/src/runtime/routes/diagnostics-routes.ts +2 -2
  308. package/src/runtime/routes/documents-routes.ts +3 -3
  309. package/src/runtime/routes/global-search-routes.ts +1 -1
  310. package/src/runtime/routes/guardian-bootstrap-routes.ts +0 -20
  311. package/src/runtime/routes/guardian-refresh-routes.ts +0 -20
  312. package/src/runtime/routes/inbound-message-handler.ts +10 -2
  313. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  314. package/src/runtime/routes/inbound-stages/edit-intercept.ts +5 -5
  315. package/src/runtime/routes/integrations/slack/share.ts +5 -5
  316. package/src/runtime/routes/log-export-routes.ts +122 -10
  317. package/src/runtime/routes/secret-routes.ts +4 -4
  318. package/src/runtime/routes/session-query-routes.ts +3 -3
  319. package/src/runtime/routes/settings-routes.ts +53 -0
  320. package/src/runtime/routes/trust-rules-routes.ts +1 -1
  321. package/src/runtime/routes/workspace-routes.ts +3 -0
  322. package/src/runtime/verification-templates.ts +1 -1
  323. package/src/security/credential-backend.ts +148 -0
  324. package/src/security/oauth2.ts +5 -5
  325. package/src/security/secret-allowlist.ts +1 -1
  326. package/src/security/secure-keys.ts +98 -160
  327. package/src/security/token-manager.ts +0 -7
  328. package/src/sequence/guardrails.ts +0 -4
  329. package/src/sequence/store.ts +1 -20
  330. package/src/sequence/types.ts +1 -36
  331. package/src/signals/bash.ts +157 -0
  332. package/src/signals/cancel.ts +69 -0
  333. package/src/signals/conversation-undo.ts +127 -0
  334. package/src/signals/trust-rule.ts +174 -0
  335. package/src/skills/clawhub.ts +5 -5
  336. package/src/skills/managed-store.ts +4 -4
  337. package/src/skills/skillssh-registry.ts +6 -1
  338. package/src/swarm/backend-claude-code.ts +6 -6
  339. package/src/swarm/worker-backend.ts +1 -1
  340. package/src/swarm/worker-runner.ts +1 -1
  341. package/src/telegram/bot-username.ts +11 -0
  342. package/src/telemetry/usage-telemetry-reporter.test.ts +366 -0
  343. package/src/telemetry/usage-telemetry-reporter.ts +181 -0
  344. package/src/tools/claude-code/claude-code.ts +6 -6
  345. package/src/tools/credentials/broker.ts +7 -5
  346. package/src/tools/credentials/vault.ts +11 -6
  347. package/src/tools/memory/handlers.test.ts +24 -26
  348. package/src/tools/memory/handlers.ts +1 -13
  349. package/src/tools/network/__tests__/web-search.test.ts +18 -86
  350. package/src/tools/network/web-search.ts +9 -15
  351. package/src/tools/registry.ts +5 -100
  352. package/src/tools/terminal/parser.ts +34 -4
  353. package/src/tools/tool-manifest.ts +0 -10
  354. package/src/usage/actors.ts +0 -12
  355. package/src/util/canonicalize-identity.ts +0 -9
  356. package/src/util/errors.ts +0 -3
  357. package/src/util/platform.ts +31 -8
  358. package/src/util/pricing.ts +0 -39
  359. package/src/watcher/constants.ts +0 -7
  360. package/src/watcher/providers/linear.ts +1 -1
  361. package/src/work-items/work-item-store.ts +4 -4
  362. package/src/workspace/commit-message-provider.ts +1 -1
  363. package/src/workspace/git-service.ts +44 -1
  364. package/src/workspace/provider-commit-message-generator.ts +11 -7
  365. package/src/__tests__/fixtures/proxy-fixtures.ts +0 -147
  366. package/src/browser-extension-relay/client.ts +0 -155
  367. package/src/contacts/index.ts +0 -18
  368. package/src/daemon/tls-certs.ts +0 -270
  369. package/src/errors.ts +0 -41
  370. package/src/events/index.ts +0 -18
  371. package/src/followups/index.ts +0 -10
  372. package/src/playbooks/index.ts +0 -10
  373. package/src/runtime/auth/index.ts +0 -44
  374. package/src/tasks/candidate-store.ts +0 -95
  375. package/src/tools/browser/api-map.ts +0 -313
  376. package/src/tools/browser/auto-navigate.ts +0 -469
  377. package/src/tools/browser/headless-browser.ts +0 -590
  378. package/src/tools/browser/recording-store.ts +0 -75
  379. package/src/tools/computer-use/registry.ts +0 -21
  380. package/src/tools/tasks/index.ts +0 -27
@@ -133,7 +133,7 @@ const log = getLogger("session-agent-loop");
133
133
  *
134
134
  * Returns the actual token count or null if it cannot be parsed.
135
135
  */
136
- function parseActualTokensFromError(
136
+ export function parseActualTokensFromError(
137
137
  errorMessage: string | null,
138
138
  ): number | null {
139
139
  if (!errorMessage) return null;
@@ -710,10 +710,11 @@ export async function runAgentLoopImpl(
710
710
  const preflightBudget = Math.floor(providerMaxTokens * (1 - safetyMargin));
711
711
  let reducerState: ReducerState | undefined;
712
712
 
713
+ const toolTokenBudget = ctx.agentLoop.getToolTokenBudget(runMessages);
713
714
  const preflightTokens = estimatePromptTokens(
714
715
  runMessages,
715
716
  ctx.systemPrompt,
716
- { providerName: ctx.provider.name },
717
+ { providerName: ctx.provider.name, toolTokenBudget },
717
718
  );
718
719
 
719
720
  if (overflowRecovery.enabled && preflightTokens > preflightBudget) {
@@ -747,6 +748,7 @@ export async function runAgentLoopImpl(
747
748
  systemPrompt: ctx.systemPrompt,
748
749
  contextWindow: config.contextWindow,
749
750
  targetTokens: preflightBudget,
751
+ toolTokenBudget,
750
752
  },
751
753
  reducerState,
752
754
  (msgs, signal, opts) =>
@@ -863,7 +865,7 @@ export async function runAgentLoopImpl(
863
865
  const estimated = estimatePromptTokens(
864
866
  checkpoint.history,
865
867
  ctx.systemPrompt,
866
- { providerName: ctx.provider.name },
868
+ { providerName: ctx.provider.name, toolTokenBudget },
867
869
  );
868
870
  if (estimated > midLoopThreshold) {
869
871
  rlog.warn(
@@ -993,6 +995,23 @@ export async function runAgentLoopImpl(
993
995
  );
994
996
  }
995
997
 
998
+ // If mid-loop compaction exhausted all attempts but the agent loop
999
+ // still yielded (yieldedForBudget is true), the turn is incomplete.
1000
+ // Escalate to the convergence loop's more aggressive reducer tiers
1001
+ // (tool-result truncation, media stubbing, injection downgrade)
1002
+ // instead of silently treating an incomplete turn as done.
1003
+ if (yieldedForBudget && !abortController.signal.aborted) {
1004
+ rlog.warn(
1005
+ {
1006
+ phase: "mid-loop-compact",
1007
+ midLoopCompactAttempts,
1008
+ maxAttempts: overflowRecovery.maxAttempts,
1009
+ },
1010
+ "Mid-loop compaction exhausted all attempts — escalating to convergence loop",
1011
+ );
1012
+ state.contextTooLargeDetected = true;
1013
+ }
1014
+
996
1015
  // One-shot ordering error retry
997
1016
  if (
998
1017
  state.orderingErrorDetected &&
@@ -1045,6 +1064,7 @@ export async function runAgentLoopImpl(
1045
1064
  ),
1046
1065
  });
1047
1066
  preRepairMessages = updatedHistory;
1067
+ preRunHistoryLength = updatedHistory.length;
1048
1068
  }
1049
1069
  if (!reducerState) {
1050
1070
  reducerState = createInitialReducerState();
@@ -1061,7 +1081,7 @@ export async function runAgentLoopImpl(
1061
1081
  const estimatedTokensAtOverflow = estimatePromptTokens(
1062
1082
  ctx.messages,
1063
1083
  ctx.systemPrompt,
1064
- { providerName: ctx.provider.name },
1084
+ { providerName: ctx.provider.name, toolTokenBudget },
1065
1085
  );
1066
1086
  let correctedTarget = preflightBudget;
1067
1087
  if (actualTokens && estimatedTokensAtOverflow > 0) {
@@ -1113,6 +1133,7 @@ export async function runAgentLoopImpl(
1113
1133
  systemPrompt: ctx.systemPrompt,
1114
1134
  contextWindow: config.contextWindow,
1115
1135
  targetTokens: correctedTarget,
1136
+ toolTokenBudget,
1116
1137
  },
1117
1138
  reducerState,
1118
1139
  (msgs, signal, opts) =>
@@ -1124,12 +1145,6 @@ export async function runAgentLoopImpl(
1124
1145
  ctx.messages = step.messages;
1125
1146
  currentInjectionMode = step.state.injectionMode;
1126
1147
 
1127
- // If the reducer is now exhausted without compacting, break out
1128
- // so the overflow policy path can attempt emergency compaction.
1129
- if (reducerState.exhausted && !step.compactionResult?.compacted) {
1130
- break;
1131
- }
1132
-
1133
1148
  if (step.compactionResult?.compacted) {
1134
1149
  ctx.contextCompactedMessageCount +=
1135
1150
  step.compactionResult.compactedPersistedMessages;
@@ -1183,77 +1198,10 @@ export async function runAgentLoopImpl(
1183
1198
  );
1184
1199
  }
1185
1200
 
1186
- // When all reducer tiers are exhausted but the context is still too
1187
- // large, attempt one last emergency compaction before consulting the
1188
- // overflow policy. This covers the case where progress was made
1189
- // (messages grew) and the normal tiers couldn't compact enough.
1190
- if (state.contextTooLargeDetected && reducerState.exhausted) {
1191
- const emergencyCompact = await ctx.contextWindowManager.maybeCompact(
1192
- ctx.messages,
1193
- abortController.signal,
1194
- {
1195
- lastCompactedAt: ctx.contextCompactedAt ?? undefined,
1196
- force: true,
1197
- minKeepRecentUserTurns: 0,
1198
- targetInputTokensOverride: correctedTarget,
1199
- },
1200
- );
1201
- if (emergencyCompact.compacted) {
1202
- ctx.messages = emergencyCompact.messages;
1203
- ctx.contextCompactedMessageCount +=
1204
- emergencyCompact.compactedPersistedMessages;
1205
- ctx.contextCompactedAt = Date.now();
1206
- updateConversationContextWindow(
1207
- ctx.conversationId,
1208
- emergencyCompact.summaryText,
1209
- ctx.contextCompactedMessageCount,
1210
- );
1211
- onEvent({
1212
- type: "context_compacted",
1213
- previousEstimatedInputTokens:
1214
- emergencyCompact.previousEstimatedInputTokens,
1215
- estimatedInputTokens: emergencyCompact.estimatedInputTokens,
1216
- maxInputTokens: emergencyCompact.maxInputTokens,
1217
- thresholdTokens: emergencyCompact.thresholdTokens,
1218
- compactedMessages: emergencyCompact.compactedMessages,
1219
- summaryCalls: emergencyCompact.summaryCalls,
1220
- summaryInputTokens: emergencyCompact.summaryInputTokens,
1221
- summaryOutputTokens: emergencyCompact.summaryOutputTokens,
1222
- summaryModel: emergencyCompact.summaryModel,
1223
- });
1224
- emitUsage(
1225
- ctx,
1226
- emergencyCompact.summaryInputTokens,
1227
- emergencyCompact.summaryOutputTokens,
1228
- emergencyCompact.summaryModel,
1229
- onEvent,
1230
- "context_compactor",
1231
- reqId,
1232
- emergencyCompact.summaryCacheCreationInputTokens ?? 0,
1233
- emergencyCompact.summaryCacheReadInputTokens ?? 0,
1234
- collapseRawResponses(emergencyCompact.summaryRawResponses),
1235
- );
1236
-
1237
- runMessages = applyRuntimeInjections(ctx.messages, {
1238
- ...injectionOpts,
1239
- mode: currentInjectionMode,
1240
- });
1241
- preRepairMessages = runMessages;
1242
- preRunHistoryLength = runMessages.length;
1243
- state.contextTooLargeDetected = false;
1244
-
1245
- updatedHistory = await ctx.agentLoop.run(
1246
- runMessages,
1247
- eventHandler,
1248
- abortController.signal,
1249
- reqId,
1250
- onCheckpoint,
1251
- );
1252
- }
1253
- }
1254
-
1255
1201
  // All reducer tiers exhausted but provider still rejects —
1256
1202
  // consult the overflow policy for latest-turn compression.
1203
+ // Emergency compaction is deferred to the policy-gated paths below
1204
+ // so that `request_user_approval` sessions collect consent first.
1257
1205
  if (state.contextTooLargeDetected) {
1258
1206
  const action = resolveOverflowAction({
1259
1207
  overflowRecovery,
@@ -218,7 +218,7 @@ function classifyCore(
218
218
  return {
219
219
  code: "PROVIDER_WEB_SEARCH",
220
220
  userMessage:
221
- "An internal error occurred with web search. Retrying...",
221
+ "An internal error occurred with web search. Please try again.",
222
222
  retryable: true,
223
223
  errorCategory: "web_search_ordering",
224
224
  };
@@ -226,7 +226,7 @@ function classifyCore(
226
226
  if (isOrderingError(message)) {
227
227
  return {
228
228
  code: "PROVIDER_ORDERING",
229
- userMessage: "An internal error occurred. Retrying...",
229
+ userMessage: "An internal error occurred. Please try again.",
230
230
  retryable: true,
231
231
  errorCategory: "tool_ordering",
232
232
  };
@@ -308,7 +308,8 @@ function classifyByMessage(
308
308
  if (isWebSearchOrderingError(message)) {
309
309
  return {
310
310
  code: "PROVIDER_WEB_SEARCH",
311
- userMessage: "An internal error occurred with web search. Retrying...",
311
+ userMessage:
312
+ "An internal error occurred with web search. Please try again.",
312
313
  retryable: true,
313
314
  errorCategory: "web_search_ordering",
314
315
  };
@@ -318,7 +319,7 @@ function classifyByMessage(
318
319
  if (isOrderingError(message)) {
319
320
  return {
320
321
  code: "PROVIDER_ORDERING",
321
- userMessage: "An internal error occurred. Retrying...",
322
+ userMessage: "An internal error occurred. Please try again.",
322
323
  retryable: true,
323
324
  errorCategory: "tool_ordering",
324
325
  };
@@ -15,7 +15,7 @@ import type {
15
15
  TurnInterfaceContext,
16
16
  } from "../channels/types.js";
17
17
  import { parseChannelId, parseInterfaceId } from "../channels/types.js";
18
- import { getConfig } from "../config/loader.js";
18
+ import { API_KEY_PROVIDERS, getConfig } from "../config/loader.js";
19
19
  import { listPendingRequestsByConversationScope } from "../memory/canonical-guardian-store.js";
20
20
  import {
21
21
  addMessage,
@@ -27,6 +27,7 @@ import { extractPreferences } from "../notifications/preference-extractor.js";
27
27
  import { createPreference } from "../notifications/preferences-store.js";
28
28
  import type { Message } from "../providers/types.js";
29
29
  import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
30
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
30
31
  import { getLogger } from "../util/logger.js";
31
32
  import type {
32
33
  ServerMessage,
@@ -47,12 +48,15 @@ import { resolveVerificationSessionIntent } from "./verification-session-intent.
47
48
  const log = getLogger("session-process");
48
49
 
49
50
  /** Build a model_info event with fresh config data. */
50
- export function buildModelInfoEvent(): ServerMessage {
51
+ export async function buildModelInfoEvent(): Promise<ServerMessage> {
51
52
  const config = getConfig();
52
- const configured = Object.keys(config.apiKeys).filter(
53
- (k) => !!config.apiKeys[k],
54
- );
55
- if (!configured.includes("ollama")) configured.push("ollama");
53
+ const configured: string[] = ["ollama"];
54
+ for (const p of API_KEY_PROVIDERS) {
55
+ if (p === "ollama") continue;
56
+ if (await getSecureKeyAsync(p)) {
57
+ configured.push(p);
58
+ }
59
+ }
56
60
  return {
57
61
  type: "model_info",
58
62
  model: config.model,
@@ -296,7 +300,10 @@ export async function drainQueue(
296
300
  }
297
301
 
298
302
  // Resolve slash commands for queued messages
299
- const slashResult = resolveSlash(next.content, buildSlashContext(session));
303
+ const slashResult = await resolveSlash(
304
+ next.content,
305
+ buildSlashContext(session),
306
+ );
300
307
 
301
308
  // Unknown slash — persist the exchange and continue draining.
302
309
  // Persist each message before pushing to session.messages so that a
@@ -365,7 +372,7 @@ export async function drainQueue(
365
372
  isModelSlashCommand(next.content) ||
366
373
  isProviderShortcut(next.content)
367
374
  ) {
368
- next.onEvent(buildModelInfoEvent());
375
+ next.onEvent(await buildModelInfoEvent());
369
376
  }
370
377
  next.onEvent({ type: "assistant_text_delta", text: slashResult.message });
371
378
  session.traceEmitter.emit(
@@ -651,7 +658,7 @@ export async function processMessage(
651
658
  }
652
659
 
653
660
  // Resolve slash commands before persistence
654
- const slashResult = resolveSlash(content, buildSlashContext(session));
661
+ const slashResult = await resolveSlash(content, buildSlashContext(session));
655
662
 
656
663
  // Unknown slash command — persist the exchange (user + assistant) so the
657
664
  // messageId is real. Persist each message before pushing to session.messages
@@ -715,7 +722,7 @@ export async function processMessage(
715
722
  // Emit fresh model info before the text delta so the client has
716
723
  // up-to-date configuredProviders when rendering /model or /models UI.
717
724
  if (isModelSlashCommand(content) || isProviderShortcut(content)) {
718
- onEvent(buildModelInfoEvent());
725
+ onEvent(await buildModelInfoEvent());
719
726
  }
720
727
  onEvent({ type: "assistant_text_delta", text: slashResult.message });
721
728
  session.traceEmitter.emit(
@@ -37,6 +37,8 @@ export interface ChannelCapabilities {
37
37
  pttActivationKey?: string;
38
38
  /** Whether the client has been granted microphone permission by the OS. */
39
39
  microphonePermissionGranted?: boolean;
40
+ /** Chat type from the gateway (e.g. "private", "group", "supergroup", "channel", "im", "mpim"). */
41
+ chatType?: string;
40
42
  }
41
43
 
42
44
  /**
@@ -296,6 +298,7 @@ export function resolveChannelCapabilities(
296
298
  sourceChannel?: string | null,
297
299
  sourceInterface?: string | null,
298
300
  pttMetadata?: PttMetadata | null,
301
+ chatType?: string | null,
299
302
  ): ChannelCapabilities {
300
303
  // Normalise legacy pseudo-channel IDs to canonical ChannelId values.
301
304
  let channel: string;
@@ -330,6 +333,8 @@ export function resolveChannelCapabilities(
330
333
  }
331
334
  }
332
335
 
336
+ const resolvedChatType = chatType ?? undefined;
337
+
333
338
  switch (channel) {
334
339
  case "vellum": {
335
340
  const supportsDesktopUi = iface === "macos";
@@ -342,6 +347,7 @@ export function resolveChannelCapabilities(
342
347
  pttMetadata?.pttActivationKey,
343
348
  ),
344
349
  microphonePermissionGranted: pttMetadata?.microphonePermissionGranted,
350
+ chatType: resolvedChatType,
345
351
  };
346
352
  }
347
353
  case "telegram":
@@ -354,6 +360,7 @@ export function resolveChannelCapabilities(
354
360
  dashboardCapable: false,
355
361
  supportsDynamicUi: false,
356
362
  supportsVoiceInput: false,
363
+ chatType: resolvedChatType,
357
364
  };
358
365
  default:
359
366
  return {
@@ -361,10 +368,28 @@ export function resolveChannelCapabilities(
361
368
  dashboardCapable: false,
362
369
  supportsDynamicUi: false,
363
370
  supportsVoiceInput: false,
371
+ chatType: resolvedChatType,
364
372
  };
365
373
  }
366
374
  }
367
375
 
376
+ /**
377
+ * Returns true when the chat type indicates a group/multi-party conversation
378
+ * (Telegram group/supergroup, Slack channel/group/mpim, etc.).
379
+ */
380
+ export function isGroupChatType(chatType?: string): boolean {
381
+ if (!chatType) return false;
382
+ switch (chatType) {
383
+ case "group":
384
+ case "supergroup":
385
+ case "channel":
386
+ case "mpim":
387
+ return true;
388
+ default:
389
+ return false;
390
+ }
391
+ }
392
+
368
393
  /** Context about the active workspace surface, passed to applyRuntimeInjections. */
369
394
  export interface ActiveSurfaceContext {
370
395
  surfaceId: string;
@@ -632,6 +657,31 @@ export function injectChannelCapabilityContext(
632
657
  }
633
658
  }
634
659
 
660
+ // Inject group chat etiquette only when the chat type indicates a multi-party
661
+ // conversation, avoiding misconditioned "stay silent" guidance in 1:1 DMs.
662
+ if (isGroupChatType(caps.chatType)) {
663
+ lines.push(`chat_type: ${caps.chatType}`);
664
+ lines.push("");
665
+ lines.push("GROUP CHAT ETIQUETTE:");
666
+ lines.push(
667
+ "- You are a **participant**, not the user's proxy. Think before you speak.",
668
+ );
669
+ lines.push(
670
+ "- **Respond when:** directly mentioned, you can add genuine value, something witty fits naturally, or correcting important misinformation.",
671
+ );
672
+ lines.push(
673
+ '- **Stay silent when:** casual banter between humans, someone already answered, your response would just be "yeah" or "nice", or the conversation flows fine without you.',
674
+ );
675
+ lines.push(
676
+ "- **The human rule:** humans don't respond to every message in a group chat. Neither should you. Quality over quantity.",
677
+ );
678
+ if (caps.channel === "slack") {
679
+ lines.push(
680
+ "- Use emoji reactions naturally to acknowledge without cluttering.",
681
+ );
682
+ }
683
+ }
684
+
635
685
  lines.push("</channel_capabilities>");
636
686
 
637
687
  const block = lines.join("\n");
@@ -5,10 +5,16 @@ import { join } from "node:path";
5
5
  import QRCode from "qrcode";
6
6
 
7
7
  import { getGatewayPort, getIngressPublicBaseUrl } from "../config/env.js";
8
- import { getConfig, loadRawConfig, saveRawConfig } from "../config/loader.js";
8
+ import {
9
+ API_KEY_PROVIDERS,
10
+ getConfig,
11
+ loadRawConfig,
12
+ saveRawConfig,
13
+ } from "../config/loader.js";
9
14
  import { resolveSkillStates } from "../config/skill-state.js";
10
15
  import { loadSkillCatalog } from "../config/skills.js";
11
16
  import { initializeProviders } from "../providers/registry.js";
17
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
12
18
  import {
13
19
  buildInvocableSlashCatalog,
14
20
  resolveSlashSkillCommand,
@@ -53,14 +59,12 @@ export interface SlashContext {
53
59
 
54
60
  const AVAILABLE_MODELS = [
55
61
  "claude-opus-4-6",
56
- "claude-opus-4-6-fast",
57
62
  "claude-sonnet-4-6",
58
63
  "claude-haiku-4-5-20251001",
59
64
  ] as const;
60
65
 
61
66
  const MODEL_DISPLAY_NAMES: Record<string, string> = {
62
67
  "claude-opus-4-6": "Claude Opus 4.6",
63
- "claude-opus-4-6-fast": "Claude Opus 4.6 Fast",
64
68
  "claude-sonnet-4-6": "Claude Sonnet 4.6",
65
69
  "claude-haiku-4-5-20251001": "Claude Haiku 4.5",
66
70
  };
@@ -75,11 +79,6 @@ const PROVIDER_MODEL_SHORTCUTS: Record<
75
79
  model: "claude-opus-4-6",
76
80
  displayName: "Claude Opus 4.6",
77
81
  },
78
- "opus-fast": {
79
- provider: "anthropic",
80
- model: "claude-opus-4-6-fast",
81
- displayName: "Claude Opus 4.6 Fast",
82
- },
83
82
  sonnet: {
84
83
  provider: "anthropic",
85
84
  model: "claude-sonnet-4-6",
@@ -146,7 +145,9 @@ function matchModel(input: string): string | undefined {
146
145
  return AVAILABLE_MODELS.find((m) => m.includes(lower));
147
146
  }
148
147
 
149
- function resolveProviderModelCommand(content: string): SlashResolution | null {
148
+ async function resolveProviderModelCommand(
149
+ content: string,
150
+ ): Promise<SlashResolution | null> {
150
151
  const trimmed = content.trim();
151
152
  if (!trimmed.startsWith("/")) return null;
152
153
 
@@ -163,7 +164,7 @@ function resolveProviderModelCommand(content: string): SlashResolution | null {
163
164
  const name = getAssistantName();
164
165
 
165
166
  // Check if API key exists for this provider (Ollama doesn't require an API key)
166
- if (provider !== "ollama" && !config.apiKeys[provider]) {
167
+ if (provider !== "ollama" && !(await getSecureKeyAsync(provider))) {
167
168
  return {
168
169
  kind: "unknown",
169
170
  message: `Cannot switch to ${displayName}. No API key configured for ${provider}.\n\nSet it with: \`keys set ${provider} <your-key>\``,
@@ -189,7 +190,7 @@ function resolveProviderModelCommand(content: string): SlashResolution | null {
189
190
 
190
191
  // Re-initialize providers with new config
191
192
  const newConfig = getConfig();
192
- initializeProviders(newConfig);
193
+ await initializeProviders(newConfig);
193
194
 
194
195
  const switchedMsg = name
195
196
  ? `Switched ${name} to **${displayName}**. New conversations will use this model.`
@@ -201,14 +202,23 @@ function resolveProviderModelCommand(content: string): SlashResolution | null {
201
202
  };
202
203
  }
203
204
 
204
- function resolveModelList(): SlashResolution {
205
+ async function resolveModelList(): Promise<SlashResolution> {
205
206
  const config = getConfig();
207
+
208
+ // Build a set of providers that have a configured API key.
209
+ const configuredProviders = new Set<string>(["ollama"]);
210
+ for (const p of API_KEY_PROVIDERS) {
211
+ if (await getSecureKeyAsync(p)) {
212
+ configuredProviders.add(p);
213
+ }
214
+ }
215
+
206
216
  const lines = ["Available models:\n"];
207
217
 
208
218
  for (const [cmd, { provider, model, displayName }] of Object.entries(
209
219
  PROVIDER_MODEL_SHORTCUTS,
210
220
  )) {
211
- const hasKey = provider === "ollama" || !!config.apiKeys[provider];
221
+ const hasKey = configuredProviders.has(provider);
212
222
  const isCurrent = config.provider === provider && config.model === model;
213
223
  const status = hasKey ? "✓" : "✗";
214
224
  const current = isCurrent ? " **[current]**" : "";
@@ -224,11 +234,13 @@ function resolveModelList(): SlashResolution {
224
234
  };
225
235
  }
226
236
 
227
- function resolveModelCommand(content: string): SlashResolution | null {
237
+ async function resolveModelCommand(
238
+ content: string,
239
+ ): Promise<SlashResolution | null> {
228
240
  const trimmed = content.trim();
229
241
  // Match /models → route to list
230
242
  if (trimmed === "/models") {
231
- return resolveModelList();
243
+ return await resolveModelList();
232
244
  }
233
245
 
234
246
  if (!trimmed.startsWith("/model")) return null;
@@ -251,7 +263,7 @@ function resolveModelCommand(content: string): SlashResolution | null {
251
263
 
252
264
  // Handle /model list
253
265
  if (args === "list") {
254
- return resolveModelList();
266
+ return await resolveModelList();
255
267
  }
256
268
 
257
269
  // Try to match the model name
@@ -280,7 +292,7 @@ function resolveModelCommand(content: string): SlashResolution | null {
280
292
  }
281
293
 
282
294
  // Validate that Anthropic provider is available
283
- if (!currentConfig.apiKeys.anthropic) {
295
+ if (!(await getSecureKeyAsync("anthropic"))) {
284
296
  const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
285
297
  return {
286
298
  kind: "unknown",
@@ -294,7 +306,7 @@ function resolveModelCommand(content: string): SlashResolution | null {
294
306
  raw.model = matched;
295
307
  saveRawConfig(raw);
296
308
  const config = getConfig();
297
- initializeProviders(config);
309
+ await initializeProviders(config);
298
310
 
299
311
  const displayName = MODEL_DISPLAY_NAMES[matched] ?? matched;
300
312
  const switchedMsg = name
@@ -343,16 +355,16 @@ function resolveStatusCommand(context: SlashContext): SlashResolution {
343
355
  * Resolve slash commands against the current skill catalog.
344
356
  * Returns `unknown` with a deterministic message, or the (possibly rewritten) content.
345
357
  */
346
- export function resolveSlash(
358
+ export async function resolveSlash(
347
359
  content: string,
348
360
  context?: SlashContext,
349
- ): SlashResolution {
361
+ ): Promise<SlashResolution> {
350
362
  // Check provider shortcuts first (/gpt4, /opus, etc.)
351
- const providerResult = resolveProviderModelCommand(content);
363
+ const providerResult = await resolveProviderModelCommand(content);
352
364
  if (providerResult) return providerResult;
353
365
 
354
366
  // Handle /model command
355
- const modelResult = resolveModelCommand(content);
367
+ const modelResult = await resolveModelCommand(content);
356
368
  if (modelResult) return modelResult;
357
369
 
358
370
  // Handle /pair command
@@ -348,6 +348,7 @@ export class Session {
348
348
  provider,
349
349
  systemPrompt: () => resolveSystemPromptCallback([]).systemPrompt,
350
350
  config: config.contextWindow,
351
+ toolTokenBudget: this.agentLoop.getToolTokenBudget(),
351
352
  });
352
353
 
353
354
  void getHookManager().trigger("session-start", {
@@ -24,6 +24,7 @@ export interface ShutdownDeps {
24
24
  memoryWorker: { stop(): void };
25
25
  qdrantManager: QdrantManager;
26
26
  mcpManager: McpServerManager | null;
27
+ telemetryReporter: { stop(): Promise<void> } | null;
27
28
  cleanupPidFile: () => void;
28
29
  }
29
30
 
@@ -87,6 +88,20 @@ export function installShutdownHandlers(deps: ShutdownDeps): void {
87
88
  log.warn({ err }, "Enrichment service shutdown failed (non-fatal)");
88
89
  }
89
90
 
91
+ if (deps.telemetryReporter) {
92
+ try {
93
+ const timeout = new Promise<void>((_, reject) =>
94
+ setTimeout(
95
+ () => reject(new Error("Telemetry flush timed out")),
96
+ 3_000,
97
+ ),
98
+ );
99
+ await Promise.race([deps.telemetryReporter.stop(), timeout]);
100
+ } catch (err) {
101
+ log.warn({ err }, "Telemetry reporter shutdown failed (non-fatal)");
102
+ }
103
+ }
104
+
90
105
  if (deps.runtimeHttp) await deps.runtimeHttp.stop();
91
106
  await browserManager.closeAllPages();
92
107
  deps.scheduler.stop();
@@ -107,7 +107,7 @@ export async function handleWatchObservation(
107
107
 
108
108
  async function generateCommentary(session: WatchSession): Promise<void> {
109
109
  try {
110
- const provider = getConfiguredProvider();
110
+ const provider = await getConfiguredProvider();
111
111
  if (!provider) {
112
112
  log.warn(
113
113
  { watchId: session.watchId },
@@ -198,7 +198,7 @@ export async function generateSummary(session: WatchSession): Promise<void> {
198
198
  },
199
199
  "generateSummary starting — calling LLM",
200
200
  );
201
- const provider = getConfiguredProvider();
201
+ const provider = await getConfiguredProvider();
202
202
  if (!provider) {
203
203
  log.warn(
204
204
  { watchId: session.watchId },
@@ -104,7 +104,7 @@ export function setDailySendCap(cap: number): void {
104
104
  saveState(state);
105
105
  }
106
106
 
107
- export function isAddressAllowed(email: string): {
107
+ function isAddressAllowed(email: string): {
108
108
  allowed: boolean;
109
109
  reason?: string;
110
110
  rule?: AddressRule;
@@ -382,8 +382,3 @@ export function getEmailService(): EmailService {
382
382
  }
383
383
  return instance;
384
384
  }
385
-
386
- /** @internal Test-only: reset singleton. */
387
- export function _resetEmailService(): void {
388
- instance = null;
389
- }
@@ -25,6 +25,7 @@ export interface ToolDomainEvents {
25
25
  | "allow_10m"
26
26
  | "allow_thread"
27
27
  | "always_allow"
28
+ | "always_allow_high_risk"
28
29
  | "deny"
29
30
  | "always_deny"
30
31
  | "temporary_override";
@@ -18,7 +18,7 @@ const log = getLogger("hooks-templates");
18
18
 
19
19
  /**
20
20
  * Install bundled hook templates into the user's hooks directory.
21
- * Templates are copied from `assistant/hook-templates/` to `~/.vellum/workspace/hooks/`.
21
+ * Templates are copied from `assistant/hook-templates/` to `~/.vellum/hooks/`.
22
22
  * - Never overwrites existing hooks (user modifications are preserved).
23
23
  * - Newly installed hooks are disabled by default.
24
24
  */
@@ -15,6 +15,7 @@ import {
15
15
  buildManagedBaseUrl,
16
16
  resolveManagedProxyContext,
17
17
  } from "../providers/managed-proxy/context.js";
18
+ import { getSecureKeyAsync } from "../security/secure-keys.js";
18
19
  import { getLogger } from "../util/logger.js";
19
20
  import {
20
21
  generateImage,
@@ -36,15 +37,15 @@ export async function generateAppIcon(
36
37
  appDescription?: string,
37
38
  ): Promise<void> {
38
39
  const config = getConfig();
39
- const apiKey = config.apiKeys.gemini ?? process.env.GEMINI_API_KEY;
40
+ const apiKey = await getSecureKeyAsync("gemini");
40
41
 
41
42
  let credentials: ImageGenCredentials | undefined;
42
43
  if (apiKey) {
43
44
  credentials = { type: "direct", apiKey };
44
45
  } else {
45
- const managedBaseUrl = buildManagedBaseUrl("vertex");
46
+ const managedBaseUrl = await buildManagedBaseUrl("vertex");
46
47
  if (managedBaseUrl) {
47
- const ctx = resolveManagedProxyContext();
48
+ const ctx = await resolveManagedProxyContext();
48
49
  credentials = {
49
50
  type: "managed-proxy",
50
51
  assistantApiKey: ctx.assistantApiKey,