@vellumai/assistant 0.9.0 → 0.10.0-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Tests for UsageTelemetryReporter.
3
3
  *
4
- * Covers both auth modes (authenticated / anonymous), watermark advancement,
5
- * error handling, batch recursion, device ID resolution, and payload shape.
4
+ * Covers the authenticated send path (and the skip behavior when credentials
5
+ * are absent or the platform is disabled), watermark advancement, error
6
+ * handling, batch recursion, device ID resolution, and payload shape.
6
7
  */
7
8
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
8
9
 
@@ -91,18 +92,83 @@ mock.module("../version.js", () => ({
91
92
  APP_VERSION: "1.2.3-test",
92
93
  }));
93
94
 
94
- let mockCollectUsageData = true;
95
+ const mockGetCachedShareAnalytics = mock(() => true);
96
+ // Owner's `share_diagnostics` consent — one part of the trace-collection gate.
97
+ const mockGetCachedShareDiagnostics = mock(() => false);
98
+ // Owner's accepted diagnostics-consent version — the disclosing-version part of
99
+ // the trace gate. Default to a far-future (unconditionally eligible) version so
100
+ // the consent/flag cases drive eligibility on those axes; the version-specific
101
+ // cases override it with old/empty values.
102
+ const mockGetCachedShareDiagnosticsVersion = mock(() => "2999-01-01");
103
+
104
+ mock.module("../platform/consent-cache.js", () => ({
105
+ getCachedShareAnalytics: mockGetCachedShareAnalytics,
106
+ getCachedShareDiagnostics: mockGetCachedShareDiagnostics,
107
+ getCachedShareDiagnosticsVersion: mockGetCachedShareDiagnosticsVersion,
108
+ }));
109
+
110
+ // The `trace-collection` feature flag — the other half of the trace gate.
111
+ const mockIsAssistantFeatureFlagEnabled = mock(
112
+ (_key: string, _config: unknown): boolean => true,
113
+ );
114
+
115
+ mock.module("../config/assistant-feature-flags.js", () => ({
116
+ isAssistantFeatureFlagEnabled: mockIsAssistantFeatureFlagEnabled,
117
+ }));
95
118
 
119
+ // Stub config — the flag checker is mocked, so the contents don't matter; this
120
+ // just keeps `getConfig()` from touching the real loader / filesystem.
96
121
  mock.module("../config/loader.js", () => ({
97
- getConfig: () => ({
98
- ui: {},
99
- model: "test",
100
- provider: "test",
101
- memory: { enabled: false },
102
- rateLimit: { maxRequestsPerMinute: 0 },
103
- secretDetection: { enabled: false },
104
- collectUsageData: mockCollectUsageData,
105
- }),
122
+ getConfig: () => ({}),
123
+ }));
124
+
125
+ interface MockTurnTrace {
126
+ schema_version: 1;
127
+ messages: {
128
+ id: string;
129
+ role: string;
130
+ created_at: number;
131
+ content: unknown;
132
+ }[];
133
+ tool_calls: unknown[];
134
+ }
135
+
136
+ // Returns a non-null bounded trace by default; individual tests override the
137
+ // return value (including null, for the over-cap / assembly-failure path).
138
+ function defaultBoundedTurnTrace(boundary: {
139
+ conversationId: string;
140
+ userMessageId: string;
141
+ userMessageCreatedAt: number;
142
+ }): MockTurnTrace | null {
143
+ return {
144
+ schema_version: 1,
145
+ messages: [
146
+ {
147
+ id: boundary.userMessageId,
148
+ role: "user",
149
+ created_at: boundary.userMessageCreatedAt,
150
+ content: [{ type: "text", text: "hello" }],
151
+ },
152
+ ],
153
+ tool_calls: [],
154
+ };
155
+ }
156
+
157
+ const mockAssembleBoundedTurnTrace = mock(defaultBoundedTurnTrace);
158
+
159
+ // Turn-completeness gate. Default: every turn is settled (complete), so the
160
+ // deferral barrier never fires unless a test opts a turn into "in-flight".
161
+ const mockIsTurnSettled = mock(
162
+ (_boundary: {
163
+ conversationId: string;
164
+ userMessageId: string;
165
+ userMessageCreatedAt: number;
166
+ }): boolean => true,
167
+ );
168
+
169
+ mock.module("../memory/turn-trace-store.js", () => ({
170
+ assembleBoundedTurnTrace: mockAssembleBoundedTurnTrace,
171
+ isTurnSettled: mockIsTurnSettled,
106
172
  }));
107
173
 
108
174
  const mockQueryUnreportedLifecycleEvents = mock(
@@ -290,7 +356,25 @@ let mockFetch: ReturnType<typeof mock>;
290
356
 
291
357
  beforeEach(() => {
292
358
  eventIdCounter = 0;
293
- mockCollectUsageData = true;
359
+ // Default consent ON so the happy-path send tests exercise the flush.
360
+ mockGetCachedShareAnalytics.mockReset();
361
+ mockGetCachedShareAnalytics.mockReturnValue(true);
362
+ // Default `share_diagnostics` consent OFF — most tests don't expect a trace;
363
+ // the trace-specific tests opt in explicitly.
364
+ mockGetCachedShareDiagnostics.mockReset();
365
+ mockGetCachedShareDiagnostics.mockReturnValue(false);
366
+ // Default the accepted consent version eligible so trace tests drive the gate
367
+ // via the flag + share_diagnostics knobs; version cases override it.
368
+ mockGetCachedShareDiagnosticsVersion.mockReset();
369
+ mockGetCachedShareDiagnosticsVersion.mockReturnValue("2999-01-01");
370
+ // Default the `trace-collection` flag ON so trace tests can drive eligibility
371
+ // via the consent knob alone; the flag-gating test flips it explicitly.
372
+ mockIsAssistantFeatureFlagEnabled.mockReset();
373
+ mockIsAssistantFeatureFlagEnabled.mockReturnValue(true);
374
+ mockAssembleBoundedTurnTrace.mockReset();
375
+ mockAssembleBoundedTurnTrace.mockImplementation(defaultBoundedTurnTrace);
376
+ mockIsTurnSettled.mockReset();
377
+ mockIsTurnSettled.mockReturnValue(true);
294
378
  mockGetMemoryCheckpoint.mockReset();
295
379
  mockSetMemoryCheckpoint.mockReset();
296
380
  mockQueryUnreportedUsageEvents.mockReset();
@@ -303,7 +387,8 @@ beforeEach(() => {
303
387
  getDb().delete(authFallbackEvents).run();
304
388
  getDb().delete(toolInvocations).run();
305
389
  getDb().delete(skillLoadedEvents).run();
306
- mockPlatformClient = null;
390
+ delete process.env.VELLUM_DISABLE_PLATFORM;
391
+ delete process.env.IS_PLATFORM;
307
392
  mockGetPlatformBaseUrl.mockReset();
308
393
  mockGetDeviceId.mockReset();
309
394
  mockGetDeviceId.mockReturnValue("test-device-id");
@@ -320,10 +405,23 @@ beforeEach(() => {
320
405
  Promise.resolve(new Response('{"accepted":0}', { status: 200 })),
321
406
  );
322
407
  globalThis.fetch = mockFetch as unknown as typeof fetch;
408
+
409
+ // Default to an authenticated client whose fetch delegates to the mockFetch
410
+ // spy, so the existing payload/body assertions keep working. The reporter
411
+ // sends authenticated-only; tests that exercise the no-credentials path
412
+ // override this with `mockPlatformClient = null`.
413
+ mockPlatformClient = {
414
+ baseUrl: "https://test.vellum.ai",
415
+ assistantApiKey: "test-key",
416
+ platformAssistantId: "asst-123",
417
+ fetch: (path: string, init?: RequestInit) => mockFetch(path, init),
418
+ };
323
419
  });
324
420
 
325
421
  afterEach(() => {
326
422
  globalThis.fetch = originalFetch;
423
+ delete process.env.VELLUM_DISABLE_PLATFORM;
424
+ delete process.env.IS_PLATFORM;
327
425
  });
328
426
 
329
427
  // ---------------------------------------------------------------------------
@@ -354,26 +452,57 @@ describe("UsageTelemetryReporter", () => {
354
452
  expect(mockFetch).not.toHaveBeenCalled();
355
453
  });
356
454
 
357
- test("anonymous flush sends request without auth headers", async () => {
455
+ test("flush is skipped when no platform credentials are available", async () => {
456
+ // Authenticated-only: with no client, nothing is sent and the watermark is
457
+ // left intact so the backlog ships once credentials resolve.
358
458
  mockPlatformClient = null;
359
- mockGetPlatformBaseUrl.mockReturnValue("https://platform.test.ai");
360
459
 
361
460
  const events = [makeUsageEvent()];
362
461
  mockQueryUnreportedUsageEvents.mockReturnValue(events);
363
- mockFetch.mockImplementation(() =>
364
- Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
462
+
463
+ const reporter = new UsageTelemetryReporter();
464
+ await reporter.flush();
465
+
466
+ expect(mockFetch).not.toHaveBeenCalled();
467
+ const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
468
+ (c) => c[0] === "telemetry:usage:last_reported_at",
365
469
  );
470
+ expect(watermarkCalls.length).toBe(0);
471
+ });
472
+
473
+ test("flush is skipped when VELLUM_DISABLE_PLATFORM is set in local mode", async () => {
474
+ // The platform-disabled toggle suppresses the send. Unlike the opt-out
475
+ // branch, watermarks are NOT advanced, so the backlog ships once the flag
476
+ // is cleared.
477
+ process.env.VELLUM_DISABLE_PLATFORM = "true";
478
+
479
+ const events = [makeUsageEvent()];
480
+ mockQueryUnreportedUsageEvents.mockReturnValue(events);
481
+
482
+ const reporter = new UsageTelemetryReporter();
483
+ // Construction initializes the absent tool_executed watermark; clear that
484
+ // call so the assertion below covers only the flush.
485
+ mockSetMemoryCheckpoint.mockClear();
486
+ await reporter.flush();
487
+
488
+ expect(mockFetch).not.toHaveBeenCalled();
489
+ expect(mockSetMemoryCheckpoint).not.toHaveBeenCalled();
490
+ });
491
+
492
+ test("VELLUM_DISABLE_PLATFORM is ignored when IS_PLATFORM is set (managed mode)", async () => {
493
+ // Platform-managed assistants always connect to the platform; an inherited
494
+ // VELLUM_DISABLE_PLATFORM must not suppress telemetry for them (matches
495
+ // arePlatformFeaturesEnabled / VellumPlatformClient.create()).
496
+ process.env.IS_PLATFORM = "true";
497
+ process.env.VELLUM_DISABLE_PLATFORM = "true";
498
+
499
+ const events = [makeUsageEvent()];
500
+ mockQueryUnreportedUsageEvents.mockReturnValue(events);
366
501
 
367
502
  const reporter = new UsageTelemetryReporter();
368
503
  await reporter.flush();
369
504
 
370
505
  expect(mockFetch).toHaveBeenCalledTimes(1);
371
- const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit];
372
- expect(url).toStartWith("https://platform.test.ai");
373
- expect(url).toEndWith("/telemetry/ingest/");
374
- const headers = opts.headers as Record<string, string>;
375
- expect(headers["Content-Type"]).toBe("application/json");
376
- expect(headers["X-Telemetry-Token"]).toBeUndefined();
377
506
  });
378
507
 
379
508
  test("watermark advances on successful upload", async () => {
@@ -921,6 +1050,406 @@ describe("UsageTelemetryReporter", () => {
921
1050
  });
922
1051
  });
923
1052
 
1053
+ // -------------------------------------------------------------------------
1054
+ // Per-turn trace collection (gated on the trace-collection flag AND
1055
+ // share_diagnostics consent)
1056
+ // -------------------------------------------------------------------------
1057
+
1058
+ function singleTurnEvent() {
1059
+ return [
1060
+ {
1061
+ id: "evt-turn-trace",
1062
+ createdAt: 1700000050000,
1063
+ conversationId: "conv-trace",
1064
+ conversationType: "standard",
1065
+ turnIndex: 2,
1066
+ interfaceId: "macos",
1067
+ channelId: "vellum",
1068
+ clientMetadata: null,
1069
+ },
1070
+ ];
1071
+ }
1072
+
1073
+ test("attaches the assembled trace when the trace-collection flag, share_diagnostics, and an eligible consent version are all true", async () => {
1074
+ mockIsAssistantFeatureFlagEnabled.mockReturnValue(true);
1075
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1076
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1077
+ mockQueryUnreportedTurnEvents.mockReturnValue(singleTurnEvent());
1078
+ mockFetch.mockImplementation(() =>
1079
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1080
+ );
1081
+
1082
+ const reporter = new UsageTelemetryReporter();
1083
+ await reporter.flush();
1084
+
1085
+ // The gate consults the `trace-collection` flag.
1086
+ expect(mockIsAssistantFeatureFlagEnabled).toHaveBeenCalledWith(
1087
+ "trace-collection",
1088
+ expect.anything(),
1089
+ );
1090
+
1091
+ // The assembler is called with the turn's (conversationId, id, createdAt)
1092
+ // boundary so the window lines up with the turn event.
1093
+ expect(mockAssembleBoundedTurnTrace).toHaveBeenCalledTimes(1);
1094
+ expect(mockAssembleBoundedTurnTrace.mock.calls[0][0]).toEqual({
1095
+ conversationId: "conv-trace",
1096
+ userMessageId: "evt-turn-trace",
1097
+ userMessageCreatedAt: 1700000050000,
1098
+ });
1099
+
1100
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1101
+ const body = JSON.parse(
1102
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1103
+ );
1104
+ // Still exactly one event — the single turn event, now carrying `trace`.
1105
+ expect(body.events.length).toBe(1);
1106
+ const turn = body.events[0];
1107
+ expect(turn.type).toBe("turn");
1108
+ expect(turn.daemon_event_id).toBe("evt-turn-trace");
1109
+ expect(turn.trace).toBeDefined();
1110
+ expect(turn.trace.schema_version).toBe(1);
1111
+ expect(turn.trace.messages[0].id).toBe("evt-turn-trace");
1112
+ expect(Array.isArray(turn.trace.tool_calls)).toBe(true);
1113
+ });
1114
+
1115
+ test("omits the trace when share_diagnostics is false even though the flag is on (and still emits the turn event)", async () => {
1116
+ mockIsAssistantFeatureFlagEnabled.mockReturnValue(true);
1117
+ mockGetCachedShareDiagnostics.mockReturnValue(false);
1118
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1119
+ mockQueryUnreportedTurnEvents.mockReturnValue(singleTurnEvent());
1120
+ mockFetch.mockImplementation(() =>
1121
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1122
+ );
1123
+
1124
+ const reporter = new UsageTelemetryReporter();
1125
+ await reporter.flush();
1126
+
1127
+ // Gate off → no assembly at all (no PII touched).
1128
+ expect(mockAssembleBoundedTurnTrace).not.toHaveBeenCalled();
1129
+
1130
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1131
+ const body = JSON.parse(
1132
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1133
+ );
1134
+ // The single turn event still flushes — just without a trace.
1135
+ expect(body.events.length).toBe(1);
1136
+ const turn = body.events[0];
1137
+ expect(turn.type).toBe("turn");
1138
+ expect(turn.daemon_event_id).toBe("evt-turn-trace");
1139
+ expect("trace" in turn).toBe(false);
1140
+ });
1141
+
1142
+ test("omits the trace when the accepted consent version predates the disclosure threshold (flag + share_diagnostics on)", async () => {
1143
+ mockIsAssistantFeatureFlagEnabled.mockReturnValue(true);
1144
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1145
+ // Consent recorded under an older version that never disclosed trace
1146
+ // collection → gate closed, mirroring the platform ingest gate.
1147
+ mockGetCachedShareDiagnosticsVersion.mockReturnValue("2000-01-01");
1148
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1149
+ mockQueryUnreportedTurnEvents.mockReturnValue(singleTurnEvent());
1150
+ mockFetch.mockImplementation(() =>
1151
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1152
+ );
1153
+
1154
+ const reporter = new UsageTelemetryReporter();
1155
+ await reporter.flush();
1156
+
1157
+ // Version below threshold → no assembly at all (no PII touched).
1158
+ expect(mockAssembleBoundedTurnTrace).not.toHaveBeenCalled();
1159
+
1160
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1161
+ const body = JSON.parse(
1162
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1163
+ );
1164
+ expect(body.events.length).toBe(1);
1165
+ expect("trace" in body.events[0]).toBe(false);
1166
+ });
1167
+
1168
+ test("omits the trace when the owner never accepted a versioned consent (empty version)", async () => {
1169
+ mockIsAssistantFeatureFlagEnabled.mockReturnValue(true);
1170
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1171
+ // Empty version (never accepted / no-row default where share_diagnostics is
1172
+ // true but unversioned) fails closed.
1173
+ mockGetCachedShareDiagnosticsVersion.mockReturnValue("");
1174
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1175
+ mockQueryUnreportedTurnEvents.mockReturnValue(singleTurnEvent());
1176
+ mockFetch.mockImplementation(() =>
1177
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1178
+ );
1179
+
1180
+ const reporter = new UsageTelemetryReporter();
1181
+ await reporter.flush();
1182
+
1183
+ expect(mockAssembleBoundedTurnTrace).not.toHaveBeenCalled();
1184
+ const body = JSON.parse(
1185
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1186
+ );
1187
+ expect(body.events.length).toBe(1);
1188
+ expect("trace" in body.events[0]).toBe(false);
1189
+ });
1190
+
1191
+ test("omits the trace when the trace-collection flag is off even though share_diagnostics is true", async () => {
1192
+ mockIsAssistantFeatureFlagEnabled.mockReturnValue(false);
1193
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1194
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1195
+ mockQueryUnreportedTurnEvents.mockReturnValue(singleTurnEvent());
1196
+ mockFetch.mockImplementation(() =>
1197
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1198
+ );
1199
+
1200
+ const reporter = new UsageTelemetryReporter();
1201
+ await reporter.flush();
1202
+
1203
+ // Flag off → gate closed → no assembly at all (no PII touched).
1204
+ expect(mockAssembleBoundedTurnTrace).not.toHaveBeenCalled();
1205
+
1206
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1207
+ const body = JSON.parse(
1208
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1209
+ );
1210
+ // The single turn event still flushes — just without a trace.
1211
+ expect(body.events.length).toBe(1);
1212
+ expect("trace" in body.events[0]).toBe(false);
1213
+ });
1214
+
1215
+ test("omits the trace (key absent) when the assembler returns null (over-cap / failure) but still emits the turn event", async () => {
1216
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1217
+ // Over-cap / assembly-failure path: the bounded assembler returns null.
1218
+ mockAssembleBoundedTurnTrace.mockReturnValueOnce(null);
1219
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1220
+ mockQueryUnreportedTurnEvents.mockReturnValue(singleTurnEvent());
1221
+ mockFetch.mockImplementation(() =>
1222
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1223
+ );
1224
+
1225
+ const reporter = new UsageTelemetryReporter();
1226
+ await reporter.flush();
1227
+
1228
+ expect(mockAssembleBoundedTurnTrace).toHaveBeenCalledTimes(1);
1229
+ const body = JSON.parse(
1230
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1231
+ );
1232
+ expect(body.events.length).toBe(1);
1233
+ expect("trace" in body.events[0]).toBe(false);
1234
+ });
1235
+
1236
+ test("no trace is assembled or attached when the whole flush is gated off by share_analytics", async () => {
1237
+ // The analytics gate short-circuits the entire flush; trace assembly must
1238
+ // never run (and nothing is sent) even when the trace gate is fully on
1239
+ // (flag + share_diagnostics both true).
1240
+ mockGetCachedShareAnalytics.mockReturnValue(false);
1241
+ mockIsAssistantFeatureFlagEnabled.mockReturnValue(true);
1242
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1243
+ mockQueryUnreportedTurnEvents.mockReturnValue(singleTurnEvent());
1244
+
1245
+ const reporter = new UsageTelemetryReporter();
1246
+ await reporter.flush();
1247
+
1248
+ expect(mockAssembleBoundedTurnTrace).not.toHaveBeenCalled();
1249
+ expect(mockFetch).not.toHaveBeenCalled();
1250
+ });
1251
+
1252
+ // -------------------------------------------------------------------------
1253
+ // Trace completeness barrier — don't emit partial traces mid-turn
1254
+ // -------------------------------------------------------------------------
1255
+
1256
+ function turnEvent(id: string, createdAt: number, conversationId: string) {
1257
+ return {
1258
+ id,
1259
+ createdAt,
1260
+ conversationId,
1261
+ conversationType: "standard",
1262
+ turnIndex: 1,
1263
+ interfaceId: "macos",
1264
+ channelId: "vellum",
1265
+ clientMetadata: null,
1266
+ };
1267
+ }
1268
+
1269
+ test("a flush during an in-progress turn defers it (no partial trace, watermark held)", async () => {
1270
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1271
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1272
+ const checkpoints = useStatefulCheckpoints();
1273
+ mockQueryUnreportedTurnEvents.mockReturnValue([
1274
+ turnEvent("evt-inflight", 1700000050000, "conv-1"),
1275
+ ]);
1276
+ // The turn's response is still streaming — not settled.
1277
+ mockIsTurnSettled.mockReturnValue(false);
1278
+
1279
+ const reporter = new UsageTelemetryReporter();
1280
+ await reporter.flush();
1281
+
1282
+ // No trace assembled, and the turn is NOT sent (the only event was deferred).
1283
+ expect(mockAssembleBoundedTurnTrace).not.toHaveBeenCalled();
1284
+ expect(mockFetch).not.toHaveBeenCalled();
1285
+ // The turn watermark must NOT advance past the deferred turn.
1286
+ expect(checkpoints.get("telemetry:turns:last_reported_at")).toBeUndefined();
1287
+ expect(checkpoints.get("telemetry:turns:last_reported_id")).toBeUndefined();
1288
+ });
1289
+
1290
+ test("a later flush emits the COMPLETE trace once the turn settles", async () => {
1291
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1292
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1293
+ const checkpoints = useStatefulCheckpoints();
1294
+ mockQueryUnreportedTurnEvents.mockReturnValue([
1295
+ turnEvent("evt-inflight", 1700000050000, "conv-1"),
1296
+ ]);
1297
+
1298
+ // Flush 1: turn in progress -> deferred, nothing sent.
1299
+ mockIsTurnSettled.mockReturnValue(false);
1300
+ const reporter = new UsageTelemetryReporter();
1301
+ await reporter.flush();
1302
+ expect(mockFetch).not.toHaveBeenCalled();
1303
+
1304
+ // Flush 2: the response + tool results have landed; the turn is settled and
1305
+ // the full trace assembles. The same (still-unreported) turn now ships.
1306
+ mockIsTurnSettled.mockReturnValue(true);
1307
+ mockAssembleBoundedTurnTrace.mockReturnValue({
1308
+ schema_version: 1,
1309
+ messages: [
1310
+ {
1311
+ id: "evt-inflight",
1312
+ role: "user",
1313
+ created_at: 1700000050000,
1314
+ content: [{ type: "text", text: "do a thing" }],
1315
+ },
1316
+ {
1317
+ id: "asst-1",
1318
+ role: "assistant",
1319
+ created_at: 1700000051000,
1320
+ content: [{ type: "text", text: "done" }],
1321
+ },
1322
+ ],
1323
+ tool_calls: [{ id: "ti-1" }],
1324
+ });
1325
+ await reporter.flush();
1326
+
1327
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1328
+ const body = JSON.parse(
1329
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1330
+ );
1331
+ expect(body.events.length).toBe(1);
1332
+ const turn = body.events[0];
1333
+ expect(turn.daemon_event_id).toBe("evt-inflight");
1334
+ // The COMPLETE transcript: user message + assistant response + tool call.
1335
+ expect(turn.trace.messages.map((m: { id: string }) => m.id)).toEqual([
1336
+ "evt-inflight",
1337
+ "asst-1",
1338
+ ]);
1339
+ expect(turn.trace.tool_calls).toHaveLength(1);
1340
+ // Watermark now advances to the (now-complete) turn.
1341
+ expect(checkpoints.get("telemetry:turns:last_reported_at")).toBe(
1342
+ String(1700000050000),
1343
+ );
1344
+ expect(checkpoints.get("telemetry:turns:last_reported_id")).toBe(
1345
+ "evt-inflight",
1346
+ );
1347
+ });
1348
+
1349
+ test("the final turn of a conversation still gets its complete trace once its own response finishes", async () => {
1350
+ // No successor turn exists; `isTurnSettled` returns true purely because the
1351
+ // conversation is no longer processing. The trace must still be sent (not
1352
+ // deferred forever for lack of a next user turn).
1353
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1354
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1355
+ mockQueryUnreportedTurnEvents.mockReturnValue([
1356
+ turnEvent("evt-final", 1700000060000, "conv-final"),
1357
+ ]);
1358
+ mockIsTurnSettled.mockReturnValue(true);
1359
+ mockFetch.mockImplementation(() =>
1360
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1361
+ );
1362
+
1363
+ const reporter = new UsageTelemetryReporter();
1364
+ await reporter.flush();
1365
+
1366
+ expect(mockAssembleBoundedTurnTrace).toHaveBeenCalledTimes(1);
1367
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1368
+ const body = JSON.parse(
1369
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1370
+ );
1371
+ expect(body.events.length).toBe(1);
1372
+ expect(body.events[0].daemon_event_id).toBe("evt-final");
1373
+ expect(body.events[0].trace).toBeDefined();
1374
+ });
1375
+
1376
+ test("barrier: a complete turn ordered AFTER an in-flight turn is also deferred (no watermark skip)", async () => {
1377
+ // The turn watermark is a single monotonic cursor, so a complete turn that
1378
+ // sorts after a deferred in-flight turn cannot be reported without skipping
1379
+ // the deferred one. Both wait; the earlier complete turn (before the
1380
+ // barrier) is reported.
1381
+ mockGetCachedShareDiagnostics.mockReturnValue(true);
1382
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1383
+ const checkpoints = useStatefulCheckpoints();
1384
+ mockQueryUnreportedTurnEvents.mockReturnValue([
1385
+ turnEvent("evt-a-complete", 1700000010000, "conv-a"),
1386
+ turnEvent("evt-b-inflight", 1700000020000, "conv-b"),
1387
+ turnEvent("evt-c-complete", 1700000030000, "conv-c"),
1388
+ ]);
1389
+ // Only the middle turn is in-flight.
1390
+ mockIsTurnSettled.mockImplementation(
1391
+ (b) => b.userMessageId !== "evt-b-inflight",
1392
+ );
1393
+ mockFetch.mockImplementation(() =>
1394
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1395
+ );
1396
+
1397
+ const reporter = new UsageTelemetryReporter();
1398
+ await reporter.flush();
1399
+
1400
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1401
+ const body = JSON.parse(
1402
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1403
+ );
1404
+ // Only the turn BEFORE the in-flight barrier is reported.
1405
+ expect(
1406
+ body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
1407
+ ).toEqual(["evt-a-complete"]);
1408
+ // Watermark stops at the last reported turn, NOT the later complete turn —
1409
+ // so the deferred middle turn is never skipped.
1410
+ expect(checkpoints.get("telemetry:turns:last_reported_at")).toBe(
1411
+ String(1700000010000),
1412
+ );
1413
+ expect(checkpoints.get("telemetry:turns:last_reported_id")).toBe(
1414
+ "evt-a-complete",
1415
+ );
1416
+ });
1417
+
1418
+ test("with tracing disabled, an in-progress turn is NOT deferred (reporting timing unchanged)", async () => {
1419
+ // The completeness barrier is trace-eligible-only. With trace collection
1420
+ // off, turn telemetry / turn_raw behavior is unchanged: the turn ships
1421
+ // immediately and `isTurnSettled` is never consulted.
1422
+ mockGetCachedShareDiagnostics.mockReturnValue(false);
1423
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1424
+ const checkpoints = useStatefulCheckpoints();
1425
+ mockQueryUnreportedTurnEvents.mockReturnValue([
1426
+ turnEvent("evt-inflight", 1700000050000, "conv-1"),
1427
+ ]);
1428
+ // Even though the turn would be "in-flight", the gate is off so this is
1429
+ // never consulted.
1430
+ mockIsTurnSettled.mockReturnValue(false);
1431
+ mockFetch.mockImplementation(() =>
1432
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1433
+ );
1434
+
1435
+ const reporter = new UsageTelemetryReporter();
1436
+ await reporter.flush();
1437
+
1438
+ expect(mockIsTurnSettled).not.toHaveBeenCalled();
1439
+ // The turn ships immediately, trace-free.
1440
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1441
+ const body = JSON.parse(
1442
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1443
+ );
1444
+ expect(body.events.length).toBe(1);
1445
+ expect(body.events[0].daemon_event_id).toBe("evt-inflight");
1446
+ expect("trace" in body.events[0]).toBe(false);
1447
+ // Watermark advances as usual.
1448
+ expect(checkpoints.get("telemetry:turns:last_reported_at")).toBe(
1449
+ String(1700000050000),
1450
+ );
1451
+ });
1452
+
924
1453
  test("llm_usage events carry conversation_id, conversation_type, and turn_index", async () => {
925
1454
  // Three LLM calls across the spectrum of the new fields:
926
1455
  // - tied to a conversation, mid-turn (typical foreground)
@@ -1001,8 +1530,8 @@ describe("UsageTelemetryReporter", () => {
1001
1530
  });
1002
1531
  });
1003
1532
 
1004
- test("flush is skipped and watermarks advanced when collectUsageData is false", async () => {
1005
- mockCollectUsageData = false;
1533
+ test("flush is skipped and watermarks advanced when share_analytics consent is off", async () => {
1534
+ mockGetCachedShareAnalytics.mockReturnValue(false);
1006
1535
  const events = [makeUsageEvent()];
1007
1536
  mockQueryUnreportedUsageEvents.mockReturnValue(events);
1008
1537
  mockFetch.mockImplementation(() =>
@@ -1044,16 +1573,37 @@ describe("UsageTelemetryReporter", () => {
1044
1573
  }
1045
1574
  });
1046
1575
 
1047
- test("events sent normally after re-enabling collectUsageData", async () => {
1576
+ test("platform disabled takes precedence over consent off — watermarks NOT advanced", async () => {
1577
+ // VELLUM_DISABLE_PLATFORM keeps the consent cache false (the consent
1578
+ // refresh can't create a platform client), so both gates would fire. The
1579
+ // platform-disabled gate runs first and returns without advancing
1580
+ // watermarks, preserving the backlog until the flag is cleared — a
1581
+ // deployment toggle must not be treated as a privacy opt-out.
1582
+ process.env.VELLUM_DISABLE_PLATFORM = "true";
1583
+ mockGetCachedShareAnalytics.mockReturnValue(false);
1584
+ const events = [makeUsageEvent()];
1585
+ mockQueryUnreportedUsageEvents.mockReturnValue(events);
1586
+
1587
+ const reporter = new UsageTelemetryReporter();
1588
+ // Construction initializes the absent tool_executed watermark; clear that
1589
+ // call so the assertion below covers only the flush.
1590
+ mockSetMemoryCheckpoint.mockClear();
1591
+ await reporter.flush();
1592
+
1593
+ expect(mockFetch).not.toHaveBeenCalled();
1594
+ expect(mockSetMemoryCheckpoint).not.toHaveBeenCalled();
1595
+ });
1596
+
1597
+ test("events sent normally after re-granting share_analytics consent", async () => {
1048
1598
  // First flush with opt-out — watermarks advance, nothing sent
1049
- mockCollectUsageData = false;
1599
+ mockGetCachedShareAnalytics.mockReturnValue(false);
1050
1600
  const reporter = new UsageTelemetryReporter();
1051
1601
  await reporter.flush();
1052
1602
  expect(mockFetch).not.toHaveBeenCalled();
1053
1603
  mockSetMemoryCheckpoint.mockReset();
1054
1604
 
1055
- // Re-enable and flush with new events
1056
- mockCollectUsageData = true;
1605
+ // Re-grant consent and flush with new events
1606
+ mockGetCachedShareAnalytics.mockReturnValue(true);
1057
1607
  const events = [makeUsageEvent({ id: "evt-after-reenable" })];
1058
1608
  mockQueryUnreportedUsageEvents.mockReturnValue(events);
1059
1609
  mockFetch.mockImplementation(() =>
@@ -1587,8 +2137,8 @@ describe("UsageTelemetryReporter", () => {
1587
2137
 
1588
2138
  // Rows accumulated before any flush ever advanced the watermark — e.g.
1589
2139
  // an opt-out period under an older build that gated reporter
1590
- // construction on collectUsageData while the always-on audit listener
1591
- // kept writing.
2140
+ // construction on the usage-data opt-out while the always-on audit
2141
+ // listener kept writing.
1592
2142
  seedToolInvocation({
1593
2143
  id: "ti-opt-out-window",
1594
2144
  createdAt: Date.now() - 60_000,
@@ -1667,7 +2217,7 @@ describe("UsageTelemetryReporter", () => {
1667
2217
  // constructs and runs the reporter; the always-on audit listener keeps
1668
2218
  // writing rows. Every opted-out flush (5-minute cycle plus the final
1669
2219
  // flush in stop()) advances the watermark past them without sending.
1670
- mockCollectUsageData = false;
2220
+ mockGetCachedShareAnalytics.mockReturnValue(false);
1671
2221
  const optOutRowCreatedAt = Date.now() - 5_000;
1672
2222
  seedToolInvocation({
1673
2223
  id: "ti-opt-out-window",
@@ -1683,7 +2233,7 @@ describe("UsageTelemetryReporter", () => {
1683
2233
 
1684
2234
  // Session 2: the user opts back in and restarts. Only rows recorded
1685
2235
  // after the opt-out epoch ship — the opt-out-window row never does.
1686
- mockCollectUsageData = true;
2236
+ mockGetCachedShareAnalytics.mockReturnValue(true);
1687
2237
  seedToolInvocation({ id: "ti-after-opt-in", createdAt: advanced + 1000 });
1688
2238
  const reporter = new UsageTelemetryReporter();
1689
2239
  await reporter.flush();
@@ -1708,7 +2258,7 @@ describe("UsageTelemetryReporter", () => {
1708
2258
 
1709
2259
  // Opted-out flush: advances the timestamp watermark to Date.now() and
1710
2260
  // must also pin the ID watermark to the high-sorting sentinel.
1711
- mockCollectUsageData = false;
2261
+ mockGetCachedShareAnalytics.mockReturnValue(false);
1712
2262
  const optedOutReporter = new UsageTelemetryReporter();
1713
2263
  await optedOutReporter.flush();
1714
2264
  expect(mockFetch).not.toHaveBeenCalled();
@@ -1726,7 +2276,7 @@ describe("UsageTelemetryReporter", () => {
1726
2276
  });
1727
2277
 
1728
2278
  // Re-opt-in: only rows strictly after the opt-out epoch ship.
1729
- mockCollectUsageData = true;
2279
+ mockGetCachedShareAnalytics.mockReturnValue(true);
1730
2280
  seedToolInvocation({ id: "ti-after-opt-in", createdAt: watermark + 1000 });
1731
2281
  const reporter = new UsageTelemetryReporter();
1732
2282
  await reporter.flush();