@vellumai/assistant 0.10.9 → 0.10.10-dev.202607162206.d08e98e

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 (456) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/Dockerfile +8 -0
  3. package/docs/activation-funnel-telemetry.md +13 -7
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/redacted-credential.test.ts +200 -0
  12. package/node_modules/@vellumai/service-contracts/src/redacted-credential.ts +226 -0
  13. package/openapi.yaml +530 -107
  14. package/package.json +1 -1
  15. package/scripts/generate-openapi.ts +8 -0
  16. package/src/__tests__/activation-early-marking.test.ts +6 -5
  17. package/src/__tests__/agent-loop-override-profile.test.ts +22 -25
  18. package/src/__tests__/agent-wake-override-profile.test.ts +21 -48
  19. package/src/__tests__/app-builder-tool-scripts.test.ts +0 -1
  20. package/src/__tests__/app-bundler.test.ts +4 -12
  21. package/src/__tests__/app-executors.test.ts +2 -49
  22. package/src/__tests__/app-routes-csp.test.ts +114 -146
  23. package/src/__tests__/auth-fallback-events-store.test.ts +8 -1
  24. package/src/__tests__/build-persisted-content.test.ts +96 -0
  25. package/src/__tests__/bundle-scanner.test.ts +27 -1
  26. package/src/__tests__/call-controller.test.ts +291 -0
  27. package/src/__tests__/call-site-routing-connection-auto-resolve.test.ts +165 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +1395 -0
  29. package/src/__tests__/chat-reveal-guard-priming.test.ts +791 -0
  30. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  31. package/src/__tests__/compactor-image-manifest-trust.test.ts +50 -0
  32. package/src/__tests__/config-loader-backfill.test.ts +9 -4
  33. package/src/__tests__/config-schema-cmd.test.ts +10 -11
  34. package/src/__tests__/config-schema.test.ts +190 -257
  35. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +208 -0
  36. package/src/__tests__/conversation-agent-loop-overflow.test.ts +5 -19
  37. package/src/__tests__/conversation-agent-loop.test.ts +22 -9
  38. package/src/__tests__/conversation-error.test.ts +31 -0
  39. package/src/__tests__/conversation-load-history-repair.test.ts +110 -0
  40. package/src/__tests__/conversation-process-callsite.test.ts +12 -19
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +7 -21
  42. package/src/__tests__/conversation-summarize-route.test.ts +36 -44
  43. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  44. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +7 -7
  45. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +0 -1
  46. package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -1
  47. package/src/__tests__/conversation-usage.test.ts +4 -12
  48. package/src/__tests__/credential-routes.test.ts +250 -0
  49. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  50. package/src/__tests__/db-migration-rollback.test.ts +22 -0
  51. package/src/__tests__/empty-response-hook.test.ts +186 -1
  52. package/src/__tests__/external-plugin-loader.test.ts +11 -5
  53. package/src/__tests__/heartbeat-service.test.ts +0 -28
  54. package/src/__tests__/host-shell-tool.test.ts +2 -0
  55. package/src/__tests__/inactive-tool-error-messages.test.ts +2 -2
  56. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +52 -8
  57. package/src/__tests__/internal-telemetry-routes.test.ts +23 -4
  58. package/src/__tests__/invite-routes-http.test.ts +12 -16
  59. package/src/__tests__/list-all-apps.test.ts +0 -4
  60. package/src/__tests__/llm-context-resolution.test.ts +32 -56
  61. package/src/__tests__/llm-request-log-turn-query.test.ts +109 -0
  62. package/src/__tests__/llm-resolver-override-or-default.test.ts +3 -52
  63. package/src/__tests__/llm-resolver.test.ts +342 -602
  64. package/src/__tests__/llm-schema.test.ts +79 -37
  65. package/src/__tests__/max-tokens-continue-hook.test.ts +19 -0
  66. package/src/__tests__/media-stream-output.test.ts +259 -3
  67. package/src/__tests__/media-stream-server-integration.test.ts +22 -1
  68. package/src/__tests__/media-stream-stt-session.test.ts +47 -0
  69. package/src/__tests__/memory-jobs-worker-cleanup-cadence.test.ts +33 -0
  70. package/src/__tests__/memory-recall-log-store.test.ts +47 -13
  71. package/src/__tests__/mock-gateway-ipc.ts +46 -1
  72. package/src/__tests__/mtime-cache.test.ts +61 -0
  73. package/src/__tests__/navigate-settings-tab.test.ts +2 -0
  74. package/src/__tests__/normalize-onboarding.test.ts +33 -0
  75. package/src/__tests__/onboarding-persona-write.test.ts +26 -0
  76. package/src/__tests__/plugin-api-resolve-credential.test.ts +140 -0
  77. package/src/__tests__/plugin-app-serve-routes.test.ts +166 -14
  78. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +2 -0
  79. package/src/__tests__/post-turn-tool-result-truncation.test.ts +38 -0
  80. package/src/__tests__/provider-commit-message-generator.test.ts +27 -20
  81. package/src/__tests__/provider-connections-backfill.test.ts +138 -0
  82. package/src/__tests__/provider-platform-proxy-integration.test.ts +10 -31
  83. package/src/__tests__/provider-registry-ollama.test.ts +8 -18
  84. package/src/__tests__/provider-send-message-override-profile.test.ts +23 -23
  85. package/src/__tests__/provider-usage-tracking.test.ts +9 -19
  86. package/src/__tests__/prune-old-conversations-job.test.ts +12 -0
  87. package/src/__tests__/published-app-updater.test.ts +22 -16
  88. package/src/__tests__/registry.test.ts +5 -16
  89. package/src/__tests__/retry-openrouter-only-normalization.test.ts +22 -23
  90. package/src/__tests__/retry-thinking-adaptive-only.test.ts +43 -48
  91. package/src/__tests__/retry-thinking-tool-choice.test.ts +57 -66
  92. package/src/__tests__/retry-verbosity-normalization.test.ts +24 -23
  93. package/src/__tests__/reveal-success-registry.test.ts +123 -0
  94. package/src/__tests__/run-conversation-turn-persistence.test.ts +130 -0
  95. package/src/__tests__/secret-fixtures.ts +9 -0
  96. package/src/__tests__/server-history-render.test.ts +28 -0
  97. package/src/__tests__/skills.test.ts +9 -4
  98. package/src/__tests__/slack-share-routes.test.ts +0 -1
  99. package/src/__tests__/stt-stream-session.test.ts +6 -5
  100. package/src/__tests__/subagent-call-site-routing.test.ts +69 -95
  101. package/src/__tests__/subagent-disposal.test.ts +2 -0
  102. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  103. package/src/__tests__/subagent-fork-spawn.test.ts +2 -0
  104. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  105. package/src/__tests__/subagent-role-registry.test.ts +37 -0
  106. package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
  107. package/src/__tests__/subagent-terminal-message.test.ts +50 -0
  108. package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -5
  109. package/src/__tests__/surface-completion-nudge-hook.test.ts +19 -0
  110. package/src/__tests__/telemetry-routes.test.ts +99 -19
  111. package/src/__tests__/tool-audit.test.ts +34 -4
  112. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  113. package/src/__tests__/tool-profiler.test.ts +72 -1
  114. package/src/__tests__/tool-result-spool.test.ts +49 -4
  115. package/src/__tests__/tool-side-effects-slack-dm.test.ts +0 -1
  116. package/src/__tests__/ui-channel-variants.test.ts +108 -0
  117. package/src/__tests__/ui-shape-teaching.test.ts +255 -0
  118. package/src/__tests__/usage-attribution.test.ts +18 -41
  119. package/src/__tests__/user-plugin-loader.test.ts +4 -4
  120. package/src/__tests__/voice-config-update.test.ts +46 -0
  121. package/src/__tests__/voice-session-bridge.test.ts +216 -84
  122. package/src/__tests__/workspace-migration-131-drop-web-fetch-mode.test.ts +120 -0
  123. package/src/agent/loop.ts +4 -3
  124. package/src/api/events/open-conversation.test.ts +64 -0
  125. package/src/api/events/open-conversation.ts +33 -0
  126. package/src/api/index.ts +6 -0
  127. package/src/api/responses/conversation-message.ts +5 -0
  128. package/src/apps/app-store.ts +25 -35
  129. package/src/bundler/app-bundler.ts +34 -48
  130. package/src/bundler/app-compiler.ts +39 -4
  131. package/src/bundler/bundle-scanner.ts +13 -0
  132. package/src/bundler/manifest.ts +1 -1
  133. package/src/calls/__tests__/voice-session-bridge.test.ts +47 -0
  134. package/src/calls/call-constants.ts +5 -0
  135. package/src/calls/call-controller.ts +100 -32
  136. package/src/calls/call-transport.ts +9 -0
  137. package/src/calls/media-stream-output.ts +107 -4
  138. package/src/calls/media-stream-server.ts +29 -9
  139. package/src/calls/media-stream-stt-session.ts +7 -1
  140. package/src/calls/media-turn-detector.ts +11 -1
  141. package/src/calls/voice-session-bridge.ts +84 -63
  142. package/src/cli/commands/__tests__/inference-providers.test.ts +270 -33
  143. package/src/cli/commands/__tests__/notifications.test.ts +24 -3
  144. package/src/cli/commands/config.help.ts +6 -6
  145. package/src/cli/commands/credentials.help.ts +13 -0
  146. package/src/cli/commands/credentials.ts +6 -1
  147. package/src/cli/commands/email.help.ts +7 -0
  148. package/src/cli/commands/email.ts +35 -1
  149. package/src/cli/commands/inference-providers.ts +168 -107
  150. package/src/cli/commands/inference.help.ts +118 -46
  151. package/src/cli/commands/memory/index.help.ts +23 -0
  152. package/src/cli/commands/memory/nodes.ts +146 -0
  153. package/src/cli/commands/notifications.help.ts +10 -10
  154. package/src/cli/commands/oauth/connect-surface-guidance.test.ts +40 -0
  155. package/src/cli/commands/oauth/connect-surface-guidance.ts +54 -0
  156. package/src/cli/commands/oauth/connect.test.ts +126 -0
  157. package/src/cli/commands/oauth/connect.ts +29 -6
  158. package/src/cli/commands/oauth/index.help.ts +7 -1
  159. package/src/cli/commands/oauth/status.test.ts +69 -3
  160. package/src/cli/commands/oauth/status.ts +50 -12
  161. package/src/cli/commands/plugins.help.ts +13 -2
  162. package/src/cli/commands/plugins.ts +61 -7
  163. package/src/cli/commands/telemetry.help.ts +13 -0
  164. package/src/cli/commands/telemetry.ts +45 -5
  165. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +8 -2
  166. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +213 -9
  167. package/src/cli/lib/bundled-marketplace.json +93 -36
  168. package/src/cli/lib/inspect-plugin.ts +5 -1
  169. package/src/cli/lib/install-from-github.ts +68 -4
  170. package/src/cli/lib/plugin-catalog-local.ts +6 -2
  171. package/src/cli/lib/plugin-fingerprint.ts +3 -3
  172. package/src/cli/lib/upgrade-plugin.ts +236 -35
  173. package/src/config/__tests__/default-profile-catalog.test.ts +8 -12
  174. package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +137 -0
  175. package/src/config/__tests__/profile-materialization.test.ts +1 -88
  176. package/src/config/bundled-skills/AGENTS.md +3 -30
  177. package/src/config/bundled-skills/app-builder/SKILL.md +5 -3
  178. package/src/config/bundled-skills/app-builder/TOOLS.json +23 -0
  179. package/src/config/bundled-skills/app-builder/tools/app-open.ts +32 -0
  180. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
  181. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +7 -7
  182. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  183. package/src/config/bundled-skills/schedule/TOOLS.json +1 -1
  184. package/src/config/bundled-skills/settings/TOOLS.json +3 -1
  185. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +2 -0
  186. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +18 -10
  187. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  188. package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
  189. package/src/config/call-site-defaults.ts +3 -3
  190. package/src/config/feature-flag-registry.json +24 -39
  191. package/src/config/llm-resolver.ts +88 -475
  192. package/src/config/profile-materialization.ts +11 -11
  193. package/src/config/schema.ts +93 -127
  194. package/src/config/schemas/__tests__/live-voice.test.ts +24 -6
  195. package/src/config/schemas/__tests__/stt.test.ts +31 -3
  196. package/src/config/schemas/live-voice.ts +10 -4
  197. package/src/config/schemas/llm.ts +52 -15
  198. package/src/config/schemas/memory-lifecycle.ts +1 -1
  199. package/src/config/schemas/memory-retrospective.ts +8 -0
  200. package/src/config/schemas/memory-v2.ts +2 -2
  201. package/src/config/schemas/memory-v3.ts +1 -1
  202. package/src/config/schemas/services.ts +6 -3
  203. package/src/config/schemas/stt.ts +38 -21
  204. package/src/config/schemas/tts.ts +13 -18
  205. package/src/config/skills.ts +31 -21
  206. package/src/context/compactor.ts +44 -7
  207. package/src/context/post-turn-tool-result-truncation.ts +4 -2
  208. package/src/context/tool-result-spool.ts +26 -5
  209. package/src/conversations/__tests__/message-consolidation.test.ts +48 -0
  210. package/src/conversations/message-consolidation.ts +22 -2
  211. package/src/daemon/__tests__/conversation-tool-setup.test.ts +5 -12
  212. package/src/daemon/app-source-watcher.ts +17 -23
  213. package/src/daemon/chat-credential-redaction.ts +1365 -0
  214. package/src/daemon/conversation-agent-loop-handlers.ts +627 -34
  215. package/src/daemon/conversation-agent-loop.ts +71 -31
  216. package/src/daemon/conversation-error.ts +36 -14
  217. package/src/daemon/conversation-process.ts +22 -0
  218. package/src/daemon/conversation-store.ts +35 -0
  219. package/src/daemon/conversation-surfaces.ts +25 -25
  220. package/src/daemon/conversation-tool-setup.ts +33 -7
  221. package/src/daemon/conversation.ts +55 -5
  222. package/src/daemon/handlers/shared.ts +33 -2
  223. package/src/daemon/lifecycle.ts +4 -4
  224. package/src/daemon/message-types/conversations.ts +6 -17
  225. package/src/daemon/providers-setup.ts +8 -0
  226. package/src/daemon/tool-setup-types.ts +7 -1
  227. package/src/daemon/wake-conversation-ops.ts +10 -1
  228. package/src/hooks/types.ts +9 -0
  229. package/src/ipc/__tests__/email-ipc.test.ts +90 -0
  230. package/src/ipc/gateway-client.test.ts +59 -0
  231. package/src/ipc/gateway-client.ts +70 -27
  232. package/src/live-voice/__tests__/live-voice-events.test.ts +14 -2
  233. package/src/live-voice/__tests__/live-voice-integration.test.ts +116 -2
  234. package/src/live-voice/__tests__/live-voice-vad.test.ts +804 -13
  235. package/src/live-voice/__tests__/protocol.test.ts +122 -0
  236. package/src/live-voice/live-voice-session.ts +443 -40
  237. package/src/live-voice/protocol.ts +143 -1
  238. package/src/monitoring/__tests__/plugin-source-watch.test.ts +3 -1
  239. package/src/monitoring/plugin-source-watch.ts +3 -62
  240. package/src/notifications/README.md +1 -1
  241. package/src/permissions/checker.ts +8 -4
  242. package/src/persistence/__tests__/db-init-migrations-ok.test.ts +26 -0
  243. package/src/persistence/conversation-crud.ts +104 -2
  244. package/src/persistence/db-init.ts +14 -3
  245. package/src/persistence/job-handlers/cleanup.ts +15 -7
  246. package/src/persistence/llm-request-log-store.ts +88 -52
  247. package/src/persistence/migrations/298-move-memory-jobs-to-memory-db.ts +7 -31
  248. package/src/persistence/migrations/305-drop-contact-acl-columns.ts +3 -2
  249. package/src/persistence/migrations/326-move-injection-events-to-memory-db.ts +8 -34
  250. package/src/persistence/migrations/336-move-memory-v2-activation-logs-to-memory-db.ts +90 -0
  251. package/src/persistence/migrations/337-move-memory-recall-logs-to-memory-db.ts +114 -0
  252. package/src/persistence/migrations/338-move-memory-v3-selections-to-memory-db.ts +84 -0
  253. package/src/persistence/migrations/339-move-activation-sessions-to-memory-db.ts +52 -0
  254. package/src/persistence/migrations/__tests__/run-migrations.test.ts +155 -0
  255. package/src/persistence/migrations/helpers/relocation.ts +44 -1
  256. package/src/persistence/migrations/run-migrations.ts +25 -1
  257. package/src/persistence/schema/infrastructure.ts +9 -0
  258. package/src/persistence/schema/memory-core.ts +3 -0
  259. package/src/persistence/schema/memory-injection.ts +2 -0
  260. package/src/persistence/steps.ts +36 -0
  261. package/src/platform/client.test.ts +1 -44
  262. package/src/platform/client.ts +10 -20
  263. package/src/platform/consent-cache.test.ts +89 -23
  264. package/src/platform/consent-cache.ts +71 -33
  265. package/src/plugin-api/constants.ts +12 -0
  266. package/src/plugin-api/conversation-turn.ts +37 -14
  267. package/src/plugin-api/index.ts +11 -1
  268. package/src/plugin-api/resolve-credential.ts +75 -0
  269. package/src/plugin-api/vision-support.test.ts +8 -19
  270. package/src/plugin-api/vision-support.ts +26 -17
  271. package/src/plugins/collect-source-versions.ts +77 -0
  272. package/src/plugins/defaults/compaction/compact.ts +6 -0
  273. package/src/plugins/defaults/compaction/window-manager.ts +9 -0
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +18 -24
  275. package/src/plugins/defaults/empty-response/hooks/user-prompt-submit.ts +38 -0
  276. package/src/plugins/defaults/empty-response/refusal-quarantine.ts +99 -0
  277. package/src/plugins/defaults/image-fallback/__tests__/caption-cache-persistence.test.ts +7 -3
  278. package/src/plugins/defaults/index.ts +8 -1
  279. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +4 -1
  280. package/src/plugins/defaults/memory/__tests__/activation-session-store.test.ts +48 -6
  281. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +28 -22
  282. package/src/plugins/defaults/memory/__tests__/memory-log-stores-degraded.test.ts +148 -0
  283. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +80 -8
  284. package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +202 -0
  285. package/src/plugins/defaults/memory/__tests__/memory-v2-activation-log-store.test.ts +64 -25
  286. package/src/plugins/defaults/memory/__tests__/memory-v2-concept-frequency.test.ts +24 -11
  287. package/src/plugins/defaults/memory/__tests__/prompt-override.test.ts +70 -6
  288. package/src/plugins/defaults/memory/__tests__/table-relocation.test.ts +278 -0
  289. package/src/plugins/defaults/memory/activation-session-store.ts +27 -20
  290. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -9
  291. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -8
  292. package/src/plugins/defaults/memory/graph/retriever.test.ts +6 -6
  293. package/src/plugins/defaults/memory/graph/store.ts +114 -0
  294. package/src/plugins/defaults/memory/graph/tool-handlers.ts +36 -1
  295. package/src/plugins/defaults/memory/graph/tools.ts +47 -13
  296. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +16 -35
  297. package/src/plugins/defaults/memory/jobs-worker.ts +10 -1
  298. package/src/plugins/defaults/memory/memory-db.ts +3 -2
  299. package/src/plugins/defaults/memory/memory-recall-log-store.ts +129 -66
  300. package/src/plugins/defaults/memory/memory-retrospective-constants.ts +8 -0
  301. package/src/plugins/defaults/memory/memory-retrospective-job.ts +11 -122
  302. package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +216 -0
  303. package/src/plugins/defaults/memory/memory-v2-activation-log-store.ts +84 -46
  304. package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +42 -32
  305. package/src/plugins/defaults/memory/path-containment.ts +21 -0
  306. package/src/plugins/defaults/memory/prompt-override.ts +51 -13
  307. package/src/plugins/defaults/memory/tools.test.ts +34 -0
  308. package/src/plugins/defaults/memory/tools.ts +12 -2
  309. package/src/plugins/defaults/memory/v2/__tests__/harness-compare.test.ts +19 -15
  310. package/src/plugins/defaults/memory/v2/__tests__/harness-oracle.test.ts +24 -19
  311. package/src/plugins/defaults/memory/v2/__tests__/harness-replay-input.test.ts +19 -15
  312. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +22 -2
  313. package/src/plugins/defaults/memory/v2/__tests__/prompts-consolidation.test.ts +5 -3
  314. package/src/plugins/defaults/memory/v2/harness/oracle.ts +59 -41
  315. package/src/plugins/defaults/memory/v2/harness/replay-input.ts +29 -25
  316. package/src/plugins/defaults/memory/v2/migration.ts +46 -16
  317. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +4 -0
  318. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +17 -15
  319. package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +24 -22
  320. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +9 -5
  321. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +277 -10
  322. package/src/plugins/defaults/memory/v3/__tests__/selection-log-store.test.ts +57 -5
  323. package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +9 -7
  324. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +38 -7
  325. package/src/plugins/defaults/memory/v3/hot-set.test.ts +36 -18
  326. package/src/plugins/defaults/memory/v3/hot-set.ts +12 -14
  327. package/src/plugins/defaults/memory/v3/learned-edges.test.ts +47 -27
  328. package/src/plugins/defaults/memory/v3/learned-edges.ts +12 -14
  329. package/src/plugins/defaults/memory/v3/orchestrate.ts +139 -22
  330. package/src/plugins/defaults/memory/v3/prune.test.ts +10 -3
  331. package/src/plugins/defaults/memory/v3/prune.ts +17 -11
  332. package/src/plugins/defaults/memory/v3/selection-log-store.ts +23 -11
  333. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +70 -53
  334. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +9 -6
  335. package/src/plugins/external-plugin-loader.ts +15 -15
  336. package/src/plugins/mtime-cache.ts +47 -0
  337. package/src/plugins/pipeline.ts +9 -1
  338. package/src/plugins/plugin-execution-context.ts +44 -0
  339. package/src/plugins/plugin-tree-walk.ts +31 -24
  340. package/src/plugins/source-fingerprint.ts +3 -4
  341. package/src/prompts/normalize-onboarding.ts +12 -0
  342. package/src/prompts/persona-resolver.ts +8 -0
  343. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +1 -1
  344. package/src/providers/__tests__/dispatch-connection-routing.test.ts +6 -9
  345. package/src/providers/__tests__/registry-native-web-search.test.ts +4 -10
  346. package/src/providers/__tests__/retry-callsite.test.ts +235 -215
  347. package/src/providers/__tests__/satellite-connection-routing.test.ts +8 -16
  348. package/src/providers/atlascloud/client.ts +10 -49
  349. package/src/providers/baseten/client.ts +43 -0
  350. package/src/providers/call-site-routing.ts +26 -13
  351. package/src/providers/connection-resolution.ts +9 -11
  352. package/src/providers/fetch-provider-catalog.ts +4 -2
  353. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +29 -4
  354. package/src/providers/inference/__tests__/connection-availability-keyless.test.ts +78 -0
  355. package/src/providers/inference/adapter-factory.ts +27 -6
  356. package/src/providers/inference/auth.ts +29 -1
  357. package/src/providers/inference/backfill.ts +61 -10
  358. package/src/providers/inference/connection-availability.ts +3 -2
  359. package/src/providers/inference/resolve-auth.ts +9 -1
  360. package/src/providers/model-catalog.ts +37 -0
  361. package/src/providers/openai/__tests__/api-error-normalization.test.ts +24 -2
  362. package/src/providers/openai/api-key-validation.ts +70 -0
  363. package/src/providers/retry.ts +2 -0
  364. package/src/providers/types.ts +11 -4
  365. package/src/providers/vellum-model-routing.test.ts +26 -0
  366. package/src/providers/vellum-model-routing.ts +30 -0
  367. package/src/providers/voice-error-copy.ts +47 -0
  368. package/src/runtime/agent-wake.ts +40 -24
  369. package/src/runtime/for-chat-mint-registry.ts +118 -0
  370. package/src/runtime/reveal-nonce.ts +49 -0
  371. package/src/runtime/reveal-success-registry.ts +306 -0
  372. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +88 -3
  373. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +161 -30
  374. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +7 -0
  375. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +15 -0
  376. package/src/runtime/routes/app-management-routes.ts +152 -45
  377. package/src/runtime/routes/app-routes.ts +16 -77
  378. package/src/runtime/routes/canned-message-complete.ts +16 -16
  379. package/src/runtime/routes/conversation-management-routes.ts +17 -14
  380. package/src/runtime/routes/conversation-query-routes.ts +54 -6
  381. package/src/runtime/routes/conversation-routes.ts +48 -2
  382. package/src/runtime/routes/credential-routes.ts +116 -3
  383. package/src/runtime/routes/email-routes.ts +17 -1
  384. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +33 -5
  385. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +6 -5
  386. package/src/runtime/routes/inference-provider-connection-routes.ts +77 -28
  387. package/src/runtime/routes/inference-send-routes.ts +1 -1
  388. package/src/runtime/routes/internal-telemetry-routes.ts +7 -6
  389. package/src/runtime/routes/plugins-routes.ts +37 -6
  390. package/src/runtime/routes/publish-routes.ts +15 -18
  391. package/src/runtime/routes/schedule-worker-routes.ts +9 -0
  392. package/src/runtime/routes/secret-routes.ts +10 -0
  393. package/src/runtime/routes/telemetry-routes.ts +149 -45
  394. package/src/schedule/__tests__/schedule-timezone.test.ts +101 -0
  395. package/src/schedule/__tests__/worker-watchdog.test.ts +209 -0
  396. package/src/schedule/schedule-store.ts +12 -1
  397. package/src/schedule/schedule-timezone.ts +63 -0
  398. package/src/schedule/scheduler.ts +100 -6
  399. package/src/schedule/worker-control.ts +19 -0
  400. package/src/security/auth-fallback-events-store.ts +7 -6
  401. package/src/security/secret-scanner.ts +26 -1
  402. package/src/services/published-app-updater.ts +6 -11
  403. package/src/stt/stt-stream-session.ts +36 -16
  404. package/src/subagent/manager.ts +34 -8
  405. package/src/telemetry/AGENTS.md +30 -1
  406. package/src/telemetry/__tests__/config-setting-snapshot.test.ts +18 -0
  407. package/src/telemetry/__tests__/outbox-test-harness.ts +5 -3
  408. package/src/telemetry/config-setting-snapshot.ts +44 -10
  409. package/src/telemetry/telemetry-event-sources.test.ts +124 -30
  410. package/src/telemetry/telemetry-event-sources.ts +137 -83
  411. package/src/telemetry/telemetry-events-outbox.test.ts +46 -1
  412. package/src/telemetry/telemetry-events-outbox.ts +60 -9
  413. package/src/telemetry/telemetry-wire-source.json +1 -1
  414. package/src/telemetry/telemetry-wire-validation.ts +39 -2
  415. package/src/telemetry/telemetry-wire.generated.ts +8 -0
  416. package/src/telemetry/tool-audit.ts +15 -9
  417. package/src/telemetry/tool-executed-events-store.test.ts +1 -1
  418. package/src/telemetry/turn-events-store.ts +20 -0
  419. package/src/telemetry/types.ts +45 -12
  420. package/src/telemetry/usage-telemetry-reporter.test.ts +294 -25
  421. package/src/telemetry/usage-telemetry-reporter.ts +117 -18
  422. package/src/telemetry/watchdog-direct-emit.test.ts +11 -3
  423. package/src/telemetry/watchdog-direct-emit.ts +11 -6
  424. package/src/tools/apps/executors.ts +11 -36
  425. package/src/tools/executor.ts +14 -1
  426. package/src/tools/host-terminal/host-shell.ts +6 -2
  427. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +1 -1
  428. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +25 -0
  429. package/src/tools/network/web-fetch.ts +6 -2
  430. package/src/tools/skills/sandbox-runner.ts +5 -2
  431. package/src/tools/subagent/spawn.ts +7 -11
  432. package/src/tools/terminal/shell.ts +5 -2
  433. package/src/tools/tool-manifest.ts +0 -2
  434. package/src/tools/tool-profiler.ts +37 -6
  435. package/src/tools/ui-surface/channel-variants.ts +101 -0
  436. package/src/tools/ui-surface/definitions.ts +23 -67
  437. package/src/tools/ui-surface/surface-shape-docs.ts +229 -0
  438. package/src/tts/__tests__/provider-adapters.test.ts +18 -0
  439. package/src/tts/provider-catalog.ts +2 -4
  440. package/src/tts/providers/deepgram-provider.ts +10 -1
  441. package/src/types/onboarding-context.ts +8 -0
  442. package/src/usage/attribution.ts +18 -112
  443. package/src/{config/bundled-skills/messaging/tools/gmail-mime-helpers.ts → util/mime-type.ts} +4 -1
  444. package/src/util/provider-error-patterns.ts +7 -1
  445. package/src/util/worker-process.ts +105 -0
  446. package/src/watcher/__tests__/telemetry.test.ts +17 -4
  447. package/src/watcher/telemetry.ts +5 -5
  448. package/src/workspace/migrations/131-drop-web-fetch-mode.ts +61 -0
  449. package/src/workspace/migrations/registry.ts +2 -0
  450. package/src/workspace/provider-commit-message-generator.ts +6 -4
  451. package/src/__tests__/app-open-proxy.test.ts +0 -67
  452. package/src/onboarding/onboarding-research-events-store.test.ts +0 -230
  453. package/src/onboarding/onboarding-research-events-store.ts +0 -104
  454. package/src/runtime/routes/assets/vellum-design-system.css +0 -2236
  455. package/src/tools/apps/definitions.ts +0 -73
  456. package/src/tools/apps/open-proxy.ts +0 -43
@@ -13,6 +13,10 @@ import {
13
13
  isValidScheduleExpression,
14
14
  } from "./recurrence-engine.js";
15
15
  import type { ScheduleSyntax } from "./recurrence-types.js";
16
+ import {
17
+ expressionCarriesOwnTimezone,
18
+ resolveScheduleTimezone,
19
+ } from "./schedule-timezone.js";
16
20
 
17
21
  const logger = getLogger("schedule-store");
18
22
 
@@ -154,7 +158,14 @@ export async function createSchedule(params: {
154
158
  const id = uuid();
155
159
  const now = Date.now();
156
160
  const enabled = params.enabled ?? true;
157
- const timezone = params.timezone ?? null;
161
+ // A recurring wall-clock schedule with no zone otherwise evaluates in the host
162
+ // clock (UTC on managed containers). Default it to the user's configured/detected
163
+ // zone so it fires at the intended local hour. One-shot schedules (absolute epoch)
164
+ // and expressions that carry their own zone keep the caller's value verbatim.
165
+ const timezone =
166
+ isOneShot || expressionCarriesOwnTimezone(syntax, expression)
167
+ ? (params.timezone ?? null)
168
+ : resolveScheduleTimezone(params.timezone);
158
169
  const mode = params.mode ?? "execute";
159
170
  const routingIntent = params.routingIntent ?? "all_channels";
160
171
  const routingHints = params.routingHints ?? {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Resolve the timezone a recurring schedule should be evaluated in.
3
+ *
4
+ * A cron/RRULE wall-clock recurrence needs a zone to disambiguate; when none is
5
+ * supplied the recurrence engine falls back to the daemon host clock (UTC on a
6
+ * managed container), firing at the wrong hour. The rest of the assistant grounds
7
+ * time via the user's configured/detected zone (see `resolveTurnTimezoneContext`
8
+ * in `daemon/date-context.ts`); this brings recurring schedules in line.
9
+ */
10
+
11
+ import { getConfigReadOnly } from "../config/loader.js";
12
+ import { canonicalizeTimeZone } from "../daemon/date-context.js";
13
+ import { hasSetConstructs } from "./recurrence-engine.js";
14
+ import type { ScheduleSyntax } from "./recurrence-types.js";
15
+
16
+ /**
17
+ * True when the expression already determines its own evaluation zone, so the
18
+ * `timezone` field must not be resolved/overridden:
19
+ * - an RRULE with an embedded `DTSTART;TZID=…` or a Z-anchored UTC `DTSTART`; or
20
+ * - an RRULE set-construct (RDATE/EXDATE/multi-RRULE). `rrulestr` does not thread
21
+ * a caller-supplied tzid into a constructed `RRuleSet`, so a resolved zone would
22
+ * have no effect on firing — persisting one would be misleading.
23
+ *
24
+ * Cron never carries its own zone.
25
+ */
26
+ export function expressionCarriesOwnTimezone(
27
+ syntax: ScheduleSyntax,
28
+ expression: string | null | undefined,
29
+ ): boolean {
30
+ if (syntax !== "rrule" || !expression) {
31
+ return false;
32
+ }
33
+ if (
34
+ /TZID=/i.test(expression) ||
35
+ /DTSTART[^:]*:\d{8}T\d{6}Z/i.test(expression)
36
+ ) {
37
+ return true;
38
+ }
39
+ return hasSetConstructs(expression);
40
+ }
41
+
42
+ /**
43
+ * Resolve the effective zone for a recurring schedule. An explicit caller value
44
+ * wins (canonicalized); otherwise the user's configured zone, then the
45
+ * client-detected zone. Returns null only when nothing is known — preserving
46
+ * host-local evaluation for local installs where the host clock is the user's.
47
+ *
48
+ * Precedence mirrors `resolveTurnTimezoneContext` (configured beats detected).
49
+ */
50
+ export function resolveScheduleTimezone(
51
+ explicit: string | null | undefined,
52
+ ): string | null {
53
+ const explicitTz = canonicalizeTimeZone(explicit);
54
+ if (explicitTz) {
55
+ return explicitTz;
56
+ }
57
+ const ui = getConfigReadOnly().ui;
58
+ return (
59
+ canonicalizeTimeZone(ui.userTimezone) ??
60
+ canonicalizeTimeZone(ui.detectedTimezone) ??
61
+ null
62
+ );
63
+ }
@@ -16,7 +16,12 @@ import { runBackgroundJob } from "../runtime/background-job-runner.js";
16
16
  import { publishConversationListChanged } from "../runtime/sync/resource-sync-events.js";
17
17
  import { runSequencesOnce } from "../sequence/engine.js";
18
18
  import type { TurnFailure } from "../telemetry/turn-outcome.js";
19
+ import { recordWatchdogEvent } from "../telemetry/watchdog-events-store.js";
19
20
  import { getLogger } from "../util/logger.js";
21
+ import {
22
+ createWorkerSupervisor,
23
+ type WorkerSupervisor,
24
+ } from "../util/worker-process.js";
20
25
  import { runWatchersOnce } from "../watcher/engine.js";
21
26
  import { normalizeCapabilityManifest } from "../workflows/capabilities.js";
22
27
  import { getWorkflowRunManager } from "../workflows/run-manager.js";
@@ -37,7 +42,13 @@ import {
37
42
  scheduleRetry,
38
43
  setScheduleRunConversationId,
39
44
  } from "./schedule-store.js";
40
- import { startScheduleWorker, stopScheduleWorker } from "./worker-control.js";
45
+ import {
46
+ isScheduleWorkerAdministrativelyStopped,
47
+ probeScheduleWorker,
48
+ spawnScheduleWorkerProcess,
49
+ startScheduleWorker,
50
+ stopScheduleWorker,
51
+ } from "./worker-control.js";
41
52
 
42
53
  const log = getLogger("scheduler");
43
54
 
@@ -227,6 +238,42 @@ async function handleExecutionFailure(params: {
227
238
  /** The running scheduler, retained so shutdown can stop it. */
228
239
  let instance: SchedulerHandle | null = null;
229
240
 
241
+ /** The schedule worker liveness watchdog, disposed by {@link stopScheduler}. */
242
+ let scheduleWorkerSupervisor: WorkerSupervisor | null = null;
243
+
244
+ /**
245
+ * Notify the user once per outage when the worker cannot be respawned after
246
+ * repeated attempts, so schedules being paused is not silent. A stable
247
+ * hourly dedupe key keeps it to one notification per outage window rather than
248
+ * one per tick.
249
+ */
250
+ function emitScheduleWorkerDownNotification(consecutiveFailures: number): void {
251
+ void emitNotificationSignal({
252
+ sourceEventName: "activity.failed",
253
+ sourceChannel: "scheduler",
254
+ sourceContextId: "schedule-worker",
255
+ dedupeKey: `schedule-worker-down:${Math.floor(Date.now() / 3_600_000)}`,
256
+ contextPayload: {
257
+ jobName: "Scheduled tasks",
258
+ errorMessage:
259
+ "The assistant could not restart its schedule runner, so scheduled tasks are paused. They resume automatically once it recovers.",
260
+ errorKind: "exception",
261
+ consecutiveFailures,
262
+ },
263
+ attentionHints: {
264
+ requiresAction: false,
265
+ urgency: "medium",
266
+ isAsyncBackground: true,
267
+ visibleInSourceNow: false,
268
+ },
269
+ }).catch((emitErr) => {
270
+ log.warn(
271
+ { err: emitErr },
272
+ "Failed to emit schedule-worker-down notification",
273
+ );
274
+ });
275
+ }
276
+
230
277
  export function startScheduler(): SchedulerHandle {
231
278
  // Schedule execution is owned by the schedule worker process; spawn it now as
232
279
  // a child of the daemon so it is running immediately. Fire-and-forget — a
@@ -234,6 +281,46 @@ export function startScheduler(): SchedulerHandle {
234
281
  // watchers and sequences.
235
282
  startScheduleWorker();
236
283
 
284
+ // Liveness watchdog: the worker calls process.exit() on any uncaught error
285
+ // and nothing respawned it, so schedules could stop for days silently. Probe
286
+ // and respawn off the existing tick — recovery latency drops from days to
287
+ // ~one tick. Detects process death, not a wedged-but-alive worker.
288
+ scheduleWorkerSupervisor = createWorkerSupervisor({
289
+ label: "Schedule worker",
290
+ probe: probeScheduleWorker,
291
+ respawn: () => spawnScheduleWorkerProcess({ detached: false }),
292
+ isSuppressed: isScheduleWorkerAdministrativelyStopped,
293
+ killChild: (pid) => {
294
+ try {
295
+ process.kill(pid, "SIGTERM");
296
+ } catch {
297
+ // Worker already gone — nothing to kill.
298
+ }
299
+ },
300
+ onRespawn: (pid) => {
301
+ log.warn(
302
+ { pid },
303
+ "Schedule worker was not running — respawned by watchdog",
304
+ );
305
+ recordWatchdogEvent({
306
+ checkName: "schedule_worker_respawn",
307
+ detail: { pid },
308
+ });
309
+ },
310
+ onPersistentFailure: (consecutiveFailures, err) => {
311
+ log.error(
312
+ { err, consecutiveFailures },
313
+ "Schedule worker repeatedly failed to respawn — schedules are paused",
314
+ );
315
+ recordWatchdogEvent({
316
+ checkName: "schedule_worker_down",
317
+ value: consecutiveFailures,
318
+ detail: { consecutiveFailures },
319
+ });
320
+ emitScheduleWorkerDownNotification(consecutiveFailures);
321
+ },
322
+ });
323
+
237
324
  let stopped = false;
238
325
  let tickRunning = false;
239
326
 
@@ -243,6 +330,9 @@ export function startScheduler(): SchedulerHandle {
243
330
  }
244
331
  tickRunning = true;
245
332
  try {
333
+ // Respawn the schedule worker if it has died (fire-and-forget; never
334
+ // throws). Idempotent — a live PID file short-circuits to alreadyRunning.
335
+ void scheduleWorkerSupervisor?.ensureAlive();
246
336
  await runScheduleOnce();
247
337
  } catch (err) {
248
338
  log.error({ err }, "Schedule tick failed");
@@ -284,12 +374,16 @@ export function startScheduler(): SchedulerHandle {
284
374
  * worker process if one is running.
285
375
  */
286
376
  export function stopScheduler(): void {
287
- stopScheduleWorker();
288
- if (!instance) {
289
- return;
377
+ // Stop the tick FIRST so no tick can respawn the worker we are about to kill.
378
+ if (instance) {
379
+ instance.stop();
380
+ instance = null;
290
381
  }
291
- instance.stop();
292
- instance = null;
382
+ // No further respawns; a respawn already in flight kills its child on resolve.
383
+ scheduleWorkerSupervisor?.dispose();
384
+ scheduleWorkerSupervisor = null;
385
+ // Then SIGTERM the running worker via its PID file.
386
+ stopScheduleWorker();
293
387
  }
294
388
 
295
389
  /** The running scheduler, or null if one was never started. */
@@ -21,6 +21,25 @@ import {
21
21
 
22
22
  const log = getLogger("schedule-worker-control");
23
23
 
24
+ /**
25
+ * True when an operator explicitly stopped the worker (via
26
+ * `assistant schedules worker stop`). The liveness watchdog must not respawn it
27
+ * while this is set, or a manual stop would be silently undone within one tick.
28
+ * Process-local: it resets to false on daemon restart and is cleared by the
29
+ * `schedules worker start` route — a restart is treated as a fresh start.
30
+ */
31
+ let administrativelyStopped = false;
32
+
33
+ /** Whether the schedule worker has been administratively stopped by an operator. */
34
+ export function isScheduleWorkerAdministrativelyStopped(): boolean {
35
+ return administrativelyStopped;
36
+ }
37
+
38
+ /** Set/clear the administratively-stopped flag (set by stop, cleared by start). */
39
+ export function setScheduleWorkerAdministrativelyStopped(value: boolean): void {
40
+ administrativelyStopped = value;
41
+ }
42
+
24
43
  /**
25
44
  * Inspect the PID file to determine whether the schedule worker process is
26
45
  * alive. A stale PID file (pointing at a dead process) is cleaned up and
@@ -1,6 +1,6 @@
1
1
  import { v4 as uuid } from "uuid";
2
2
 
3
- import { getCachedShareAnalytics } from "../platform/consent-cache.js";
3
+ import { getRawShareAnalytics } from "../platform/consent-cache.js";
4
4
  import { insertTelemetryOutboxEvents } from "../telemetry/telemetry-events-outbox.js";
5
5
  import type { AuthFallbackTelemetryEvent } from "../telemetry/types.js";
6
6
  import { APP_VERSION } from "../version.js";
@@ -19,17 +19,18 @@ export interface AuthFallbackCount {
19
19
  * flush window, each carrying its full wire event built at record time. The
20
20
  * whole batch inserts atomically, so a failure never leaves a partial batch
21
21
  * committed (the gateway retries the full batch on `recorded === 0`). Returns
22
- * the number of rows recorded, or 0 when usage data collection is disabled
23
- * (the counts are dropped to honor the opt-out, matching the rest of
24
- * telemetry) or the telemetry database is unavailable (degraded mode). Callers
25
- * that must tell those apart check `getCachedShareAnalytics()` themselves.
22
+ * the number of rows recorded, or 0 when `share_analytics` is a confirmed
23
+ * opt-out (the counts are dropped to honor it, matching the rest of
24
+ * telemetry — an unknown consent state records) or the telemetry database is
25
+ * unavailable (degraded mode). Callers that must tell those apart check
26
+ * `getRawShareAnalytics()` themselves.
26
27
  */
27
28
  export function recordAuthFallbackCounts(
28
29
  windowStart: number,
29
30
  windowEnd: number,
30
31
  counts: AuthFallbackCount[],
31
32
  ): number {
32
- if (!getCachedShareAnalytics()) {
33
+ if (getRawShareAnalytics() === false) {
33
34
  return 0;
34
35
  }
35
36
  if (counts.length === 0) {
@@ -274,6 +274,31 @@ export function scanText(text: string): SecretMatch[] {
274
274
  * Returns the modified text.
275
275
  */
276
276
  export function redactSecrets(text: string): string {
277
+ return redactSecretsWith(
278
+ text,
279
+ (match) => `<redacted type="${match.type}" />`,
280
+ );
281
+ }
282
+
283
+ /**
284
+ * Core redaction walk with a caller-supplied marker builder.
285
+ *
286
+ * `markerFor` receives the {@link SecretMatch} plus the raw matched value so
287
+ * a caller can vary the marker per match (e.g. the chat-persist path emits a
288
+ * revealable sentinel when the value byte-matches a known vault credential).
289
+ * The default `<redacted type="…" />` marker produced via
290
+ * {@link redactSecrets} is consumed by ingress blocking, tool-output
291
+ * scanning, and log redaction — its shape must not change (see
292
+ * `security/AGENTS.md`), which is why this variant exists as a separate
293
+ * additive entry point instead of an option on `redactSecrets`.
294
+ *
295
+ * This module stays pure: `markerFor` is a plain callback, and no credential
296
+ * store types leak in here.
297
+ */
298
+ export function redactSecretsWith(
299
+ text: string,
300
+ markerFor: (match: SecretMatch, rawValue: string) => string,
301
+ ): string {
277
302
  const matches = scanText(text);
278
303
  if (matches.length === 0) return text;
279
304
 
@@ -289,7 +314,7 @@ export function redactSecrets(text: string): string {
289
314
  continue;
290
315
  }
291
316
  result += text.slice(lastIndex, match.startIndex);
292
- result += `<redacted type="${match.type}" />`;
317
+ result += markerFor(match, text.slice(match.startIndex, match.endIndex));
293
318
  lastIndex = match.endIndex;
294
319
  }
295
320
  result += text.slice(lastIndex);
@@ -9,7 +9,6 @@ import { join } from "node:path";
9
9
  import {
10
10
  getApp,
11
11
  getAppDirPath,
12
- isMultifileApp,
13
12
  resolveEffectiveAppHtml,
14
13
  } from "../apps/app-store.js";
15
14
  import {
@@ -30,23 +29,19 @@ export async function updatePublishedAppDeployment(
30
29
  const publishedPage = getActivePublishedPageByAppId(appId);
31
30
  if (!publishedPage) return;
32
31
 
33
- // 2. Load the app and resolve its deployable HTML. For multifile apps the
34
- // real content lives in dist/index.html (compiled from src/), not in
35
- // htmlDefinition (which is "" for them) — using htmlDefinition directly
36
- // would deploy a blank page.
32
+ // 2. Load the app and resolve its deployable HTML: the real content
33
+ // lives in dist/index.html (compiled from src/), inlined into a
34
+ // self-contained page.
37
35
  const app = getApp(appId);
38
36
  if (!app) {
39
37
  log.warn({ appId }, "Published app not found");
40
38
  return;
41
39
  }
42
40
 
43
- // Skip rather than deploy the compile-failure fallback when a multifile
44
- // app has no compiled output (e.g. a concurrent compile failed); a later
41
+ // Skip rather than deploy the compile-failure fallback when the app has
42
+ // no compiled output (e.g. a concurrent compile failed); a later
45
43
  // successful compile re-triggers this path.
46
- if (
47
- isMultifileApp(app) &&
48
- !existsSync(join(getAppDirPath(app.id), "dist", "index.html"))
49
- ) {
44
+ if (!existsSync(join(getAppDirPath(app.id), "dist", "index.html"))) {
50
45
  log.warn({ appId }, "Skipping auto-redeploy: compiled output missing");
51
46
  return;
52
47
  }
@@ -34,10 +34,12 @@ import {
34
34
  listProviderIds,
35
35
  supportsBoundary,
36
36
  } from "../providers/speech-to-text/provider-catalog.js";
37
+ import { describeSttFailure } from "../providers/voice-error-copy.js";
37
38
  import { getLogger } from "../util/logger.js";
39
+ import { normalizeSttError } from "./daemon-batch-transcriber.js";
38
40
  import {
39
41
  type StreamingTranscriber,
40
- SttError,
42
+ type SttProviderId,
41
43
  type SttStreamServerEvent,
42
44
  } from "./types.js";
43
45
 
@@ -228,17 +230,17 @@ export class SttStreamSession {
228
230
  "STT stream session started",
229
231
  );
230
232
  } catch (err) {
231
- const message = err instanceof Error ? err.message : String(err);
233
+ // normalizeSttError preserves an adapter-thrown SttError's category
234
+ // (e.g. a relay auth rejection at dial time) and classifies raw errors.
235
+ const sttErr = normalizeSttError(err);
232
236
  log.error(
233
- { provider: this.provider, error: message },
237
+ { provider: this.provider, error: sttErr.message },
234
238
  "Failed to start STT stream session",
235
239
  );
236
240
  this.sendEvent({
237
241
  type: "error",
238
- // Adapters reject with SttError when they know better than the
239
- // generic bucket (e.g. a relay auth rejection at dial time).
240
- category: err instanceof SttError ? err.category : "provider-error",
241
- message: `Failed to start streaming session: ${message}`,
242
+ category: sttErr.category,
243
+ message: describeSttFailure(sttErr, this.provider as SttProviderId),
242
244
  });
243
245
  this.sendEvent({ type: "closed" });
244
246
  this.state = "closed";
@@ -254,7 +256,9 @@ export class SttStreamSession {
254
256
  * {@link handleBinaryAudio}.
255
257
  */
256
258
  handleMessage(raw: string): void {
257
- if (this.state === "closed") {return;}
259
+ if (this.state === "closed") {
260
+ return;
261
+ }
258
262
 
259
263
  this.resetIdleTimer();
260
264
 
@@ -267,7 +271,9 @@ export class SttStreamSession {
267
271
  return;
268
272
  }
269
273
 
270
- if (!parsed || typeof parsed !== "object") {return;}
274
+ if (!parsed || typeof parsed !== "object") {
275
+ return;
276
+ }
271
277
 
272
278
  const event = parsed as {
273
279
  type?: string;
@@ -312,7 +318,9 @@ export class SttStreamSession {
312
318
  * frames rather than base64-encoded JSON.
313
319
  */
314
320
  handleBinaryAudio(data: Buffer | ArrayBuffer | Uint8Array): void {
315
- if (this.state !== "active") {return;}
321
+ if (this.state !== "active") {
322
+ return;
323
+ }
316
324
 
317
325
  this.resetIdleTimer();
318
326
 
@@ -328,7 +336,9 @@ export class SttStreamSession {
328
336
  * Tears down the provider session and cleans up resources.
329
337
  */
330
338
  handleClose(code: number, reason?: string): void {
331
- if (this.state === "closed") {return;}
339
+ if (this.state === "closed") {
340
+ return;
341
+ }
332
342
 
333
343
  log.info(
334
344
  { provider: this.provider, code, reason },
@@ -343,7 +353,9 @@ export class SttStreamSession {
343
353
  * ensure deterministic cleanup of all active sessions.
344
354
  */
345
355
  destroy(): void {
346
- if (this.state === "closed") {return;}
356
+ if (this.state === "closed") {
357
+ return;
358
+ }
347
359
 
348
360
  log.info({ provider: this.provider }, "STT stream session destroyed");
349
361
  this.teardown();
@@ -395,7 +407,9 @@ export class SttStreamSession {
395
407
  * Handle events emitted by the streaming transcriber.
396
408
  */
397
409
  private handleTranscriberEvent(event: SttStreamServerEvent): void {
398
- if (this.state === "closed") {return;}
410
+ if (this.state === "closed") {
411
+ return;
412
+ }
399
413
 
400
414
  this.sendEvent(event);
401
415
 
@@ -436,10 +450,14 @@ export class SttStreamSession {
436
450
  private resetIdleTimer(): void {
437
451
  this.clearIdleTimer();
438
452
 
439
- if (this.state === "closed" || this.state === "stopping") {return;}
453
+ if (this.state === "closed" || this.state === "stopping") {
454
+ return;
455
+ }
440
456
 
441
457
  this.idleTimer = setTimeout(() => {
442
- if (this.state === "closed") {return;}
458
+ if (this.state === "closed") {
459
+ return;
460
+ }
443
461
 
444
462
  log.warn({ provider: this.provider }, "STT stream session idle timeout");
445
463
  this.sendEvent({
@@ -469,7 +487,9 @@ export class SttStreamSession {
469
487
  * Idempotent — safe to call multiple times.
470
488
  */
471
489
  private teardown(): void {
472
- if (this.state === "closed") {return;}
490
+ if (this.state === "closed") {
491
+ return;
492
+ }
473
493
  this.state = "closed";
474
494
 
475
495
  this.clearIdleTimer();
@@ -107,7 +107,7 @@ function extractDeltaText(msg: ServerMessage): string | null {
107
107
 
108
108
  // ── Default subagent system prompt ──────────────────────────────────────
109
109
 
110
- function buildSubagentSystemPrompt(
110
+ export function buildSubagentSystemPrompt(
111
111
  config: SubagentConfig,
112
112
  role: SubagentRole,
113
113
  ): string {
@@ -126,7 +126,8 @@ function buildSubagentSystemPrompt(
126
126
  "## Constraints",
127
127
  `- Role: ${role}`,
128
128
  "- You cannot spawn nested subagents.",
129
- "- Use notify_parent to report important findings or if you are blocked.",
129
+ "- Use notify_parent to report important findings, or if you are blocked.",
130
+ '- If the objective needs a capability your role\'s tools do not provide (for example, writing or editing a file, or running a command, with a read-only role), do NOT fabricate a completed result — call notify_parent with urgency "blocked", name the capability you lack (e.g. file_write), and stop.',
130
131
  );
131
132
  return sections.join("\n");
132
133
  }
@@ -152,6 +153,8 @@ export function buildSubagentTerminalMessage(opts: {
152
153
  error?: string;
153
154
  /** A follow-up turn is still queued, so the current synthesis is a snapshot. */
154
155
  deferred?: boolean;
156
+ /** Tools the subagent attempted but that its role allowlist denied. */
157
+ deniedTools?: string[];
155
158
  }): string {
156
159
  const {
157
160
  label,
@@ -162,9 +165,19 @@ export function buildSubagentTerminalMessage(opts: {
162
165
  finalText,
163
166
  error,
164
167
  deferred,
168
+ deniedTools,
165
169
  } = opts;
166
170
  const prefix = isFork ? "Fork" : "Subagent";
167
171
 
172
+ // When the subagent reached for tools its role does not permit, tell the
173
+ // parent so it re-spawns with a capable role instead of blindly retrying (a
174
+ // read-only role produces nothing). Only attached to completed outcomes.
175
+ const many = (deniedTools?.length ?? 0) > 1;
176
+ const deniedNote =
177
+ deniedTools && deniedTools.length > 0
178
+ ? `\n\nNote: this ${prefix.toLowerCase()} attempted ${deniedTools.join(", ")} but its role does not permit ${many ? "them" : "it"}. If the objective requires ${many ? "those" : "that"}, re-spawn with a role that includes ${many ? "them" : "it"} (e.g. \`coder\`).`
179
+ : "";
180
+
168
181
  if (outcome === "failed") {
169
182
  return (
170
183
  `[${prefix} "${label}" failed]\n\n` +
@@ -180,7 +193,8 @@ export function buildSubagentTerminalMessage(opts: {
180
193
  `${trimmed}\n\n` +
181
194
  (silent
182
195
  ? `(Use these findings internally; do not relay the raw ${prefix.toLowerCase()} output to the user.)`
183
- : `(Incorporate this into your reply to the user as appropriate.)`)
196
+ : `(Incorporate this into your reply to the user as appropriate.)`) +
197
+ deniedNote
184
198
  );
185
199
  }
186
200
 
@@ -194,7 +208,8 @@ export function buildSubagentTerminalMessage(opts: {
194
208
  return (
195
209
  `[${prefix} "${label}" completed]\n\n` +
196
210
  `${reason}. Use subagent_read with subagent_id "${subagentId}"${lastN} for the latest output.` +
197
- (silent ? ` Keep the result internal.` : ``)
211
+ (silent ? ` Keep the result internal.` : ``) +
212
+ deniedNote
198
213
  );
199
214
  }
200
215
 
@@ -367,8 +382,9 @@ export class SubagentManager {
367
382
  // ── Build conversation dependencies ─────────────────────────────
368
383
  const appConfig = getConfig();
369
384
  // Connection-aware default-provider resolution. Throws
370
- // `ConnectionResolutionError` if `llm.default.provider_connection` is
371
- // unset or the connection row is missing/mismatched (config bugs).
385
+ // `ConnectionResolutionError` if the resolved default config carries no
386
+ // provider_connection or the connection row is missing/mismatched
387
+ // (config bugs).
372
388
  // Returns null on soft credential failures (missing credential,
373
389
  // platform auth unavailable).
374
390
  const baseProvider = await resolveDefaultProvider(appConfig);
@@ -376,7 +392,7 @@ export class SubagentManager {
376
392
  throw await mainAgentResolutionError(appConfig.llm, listProviders());
377
393
  }
378
394
  // Per-call `options.config.callSite` (e.g. `subagentSpawn`) can resolve
379
- // to a profile that differs from `llm.default`. The shared wrapper
395
+ // to a profile that differs from the default's. The shared wrapper
380
396
  // threads `appConfig` through so per-call alternate-profile routing is
381
397
  // also connection-aware (matches the canonical dispatch path).
382
398
  let provider = wrapWithCallSiteRouting(baseProvider, appConfig);
@@ -771,6 +787,9 @@ export class SubagentManager {
771
787
  // Capture the trailing assistant text before any release nulls the
772
788
  // conversation reference. The fire-and-forget caller ignores the return.
773
789
  finalText = extractFinalAssistantText(conversation.messages);
790
+ // Capture any tools the subagent reached for but its role denied, before a
791
+ // release nulls the conversation reference, so we can tell the parent.
792
+ const deniedTools = [...conversation.subagentDeniedToolNames];
774
793
  // Copy usage stats from the conversation before sending status (which includes usage).
775
794
  managed.state.usage = { ...conversation.usageStats };
776
795
  // Only update state + notify if still non-terminal (guards against abort race).
@@ -785,7 +804,12 @@ export class SubagentManager {
785
804
  // round-trip. Skipped on the synchronous path — the awaiting caller
786
805
  // receives the final text directly.
787
806
  if (!managed.synchronous) {
788
- this.notifyParentTerminal(managed, "completed", finalText);
807
+ this.notifyParentTerminal(
808
+ managed,
809
+ "completed",
810
+ finalText,
811
+ deniedTools,
812
+ );
789
813
  }
790
814
  }
791
815
  } catch (err) {
@@ -1396,6 +1420,7 @@ export class SubagentManager {
1396
1420
  managed: ManagedSubagent,
1397
1421
  outcome: "completed" | "failed",
1398
1422
  finalText?: string,
1423
+ deniedTools?: string[],
1399
1424
  ): void {
1400
1425
  const { config } = managed.state;
1401
1426
  const isFork = managed.state.isFork;
@@ -1416,6 +1441,7 @@ export class SubagentManager {
1416
1441
  // A queued follow-up turn means the snapshot we hold is stale; defer to a
1417
1442
  // read pointer so the parent picks up the queued turn's output instead.
1418
1443
  deferred: managed.hadEnqueuedMessages === true,
1444
+ deniedTools,
1419
1445
  });
1420
1446
 
1421
1447
  const notification: SubagentNotificationInfo = {
@@ -12,4 +12,33 @@ Pre-flush validation (`telemetry-wire-validation.ts`) checks outgoing events aga
12
12
 
13
13
  The ingest endpoint silently skips events whose type has no registered serializer (the batch still 2xxes and the daemon acks away its outbox rows), so an emitter shipped before its platform serializer loses every event it records — pre-flush validation logs the drop but does not prevent it.
14
14
 
15
- The full cross-repo checklist lives with the serializer registry it governs, in `vellum-assistant-platform` at `django/app/assistant/self_hosted_local/AGENTS.md` ("Adding a new telemetry ingest event type"). The daemon emitter is the **last** step — added only after the platform serializer has merged and the wire sync PR has landed here. A new emitter is an outbox store plus registration in `telemetry-event-sources.ts` (or a watermark source for high-volume tables); if the type previously existed daemon-only, also move it out of `Extensions` in `types.ts`.
15
+ The full cross-repo checklist lives with the serializer registry it governs, in `vellum-assistant-platform` at `django/app/assistant/self_hosted_local/AGENTS.md` ("Adding a new telemetry ingest event type"). The daemon emitter is the **last** step — added only after the platform serializer has merged and the wire sync PR has landed here.
16
+
17
+ ### Emitting an outbox-backed event
18
+
19
+ For a normal (outbox-backed) event, the sync PR is all the daemon needs — **no manual edits in this directory**. `OUTBOX_TELEMETRY_EVENT_NAMES` (in `types.ts`) and its flush source (in `telemetry-event-sources.ts`) are both **derived from the wire contract**, so a newly-synced type is outbox-backed and flushed automatically. Just call the generic, fully-typed recorder from your feature code:
20
+
21
+ ```ts
22
+ import { recordTelemetryEvent } from "./telemetry-events-outbox.js";
23
+
24
+ recordTelemetryEvent(
25
+ "my_new_event",
26
+ { some_field: value, conversation_id: conversationId ?? null },
27
+ { conversationId },
28
+ );
29
+ ```
30
+
31
+ `recordTelemetryEvent` stamps the base fields (`type`, `daemon_event_id`, `recorded_at`, `assistant_version`) and gates on consent — dropping only on a confirmed `share_analytics` opt-out; an unknown consent state (cold cache) records, because consent is enforced again at flush time and a record-time drop would destroy the event permanently. Unknown-state buffering is bounded: the reporter prunes outbox rows older than `OUTBOX_MAX_ROW_AGE_MS` (30 days) at the start of every flush cycle, in every consent state, so a permanently-unknown state cannot grow the outbox forever. The `fields` argument is typed as everything except the base fields.
32
+
33
+ Manual edits in this directory are needed **only** to override a default:
34
+
35
+ - **Watermark-flushed** (the type has its own high-volume table, not the outbox): add it to `WATERMARK_TELEMETRY_EVENT_NAMES` in `types.ts` and add its source to `WATERMARK_TELEMETRY_EVENT_SOURCES`.
36
+ - **Custom flush behavior** (a non-default per-type source — e.g. re-checking consent at flush time): map the type to a custom source factory in `OUTBOX_SOURCE_FACTORY` in `telemetry-event-sources.ts`. (No type currently needs one — every outbox event, `onboarding_research` included, flushes through the default `outboxSource` under `share_analytics`.)
37
+ - **Daemon-partition flush** (needs live in-process state, like `turn`): add its source to `DAEMON_TELEMETRY_EVENT_SOURCES`.
38
+ - **Richer daemon type** than the wire: add an `Overrides` entry in `types.ts` with the drift guards (see the wire-contract section above).
39
+
40
+ If the type previously existed daemon-only, also move it out of `Extensions` in `types.ts`.
41
+
42
+ **For a new event type, prefer calling `recordTelemetryEvent` directly** rather than adding a `record<Thing>Event` store wrapper. A wrapper that only renames camelCase fields to the snake-case wire shape is gratuitous indirection now that `recordTelemetryEvent`'s `fields` argument is fully typed from the wire. Add a store module only when it earns its keep: clamp values to server bounds (`config-setting-events-store.ts`), stamp a custom deterministic `daemon_event_id` via `recordTelemetryOutboxEvent` (`onboarding-research-events-store.ts`), aggregate multiple events (`auth-fallback-events-store.ts`), centralize a non-trivial mapping reused across many call sites (`watchdog-events-store.ts`), or provide a stable, mockable seam for tests (`skill-loaded-events-store.ts`).
43
+
44
+ The generic outbox also self-heals: rows for a type the platform later removes or renames are drained by the synthetic orphan-drain source (`orphanOutboxDrainSource` in `telemetry-event-sources.ts`), so a retired type needs no cleanup step here.