@vellumai/assistant 0.8.10 → 0.8.11-staging.1

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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -143,19 +143,31 @@ mock.module("../memory/onboarding-events-store.js", () => ({
143
143
  queryUnreportedOnboardingEvents: mockQueryUnreportedOnboardingEvents,
144
144
  }));
145
145
 
146
- // The auth-fallback store is intentionally NOT mocked — it has its own
147
- // DB-backed tests, and Bun's `mock.module` is process-global, so mocking it
148
- // here would leak into those tests when files share an invocation. We seed the
149
- // real DB instead so every auth-fallback test stays order-independent.
146
+ // The auth-fallback, tool-executed, and skill-loaded stores are intentionally
147
+ // NOT mocked — they have their own DB-backed tests, and Bun's `mock.module`
148
+ // is process-global, so mocking them here would leak into those tests when
149
+ // files share an invocation. We seed the real DB instead so every test stays
150
+ // order-independent.
150
151
 
151
152
  // ---------------------------------------------------------------------------
152
153
  // Production import (after mocks)
153
154
  // ---------------------------------------------------------------------------
154
155
 
156
+ import {
157
+ seedToolInvocation as seedToolInvocationRow,
158
+ TOOL_INVOCATION_PII_SENTINEL as TOOL_PII_SENTINEL,
159
+ type ToolInvocationSeedSpec,
160
+ } from "../__tests__/test-support/tool-invocation-seed.js";
155
161
  import { recordAuthFallbackCounts } from "../memory/auth-fallback-events-store.js";
156
162
  import { getDb } from "../memory/db-connection.js";
157
163
  import { initializeDb } from "../memory/db-init.js";
158
- import { authFallbackEvents } from "../memory/schema.js";
164
+ import {
165
+ authFallbackEvents,
166
+ conversations,
167
+ skillLoadedEvents,
168
+ toolInvocations,
169
+ } from "../memory/schema.js";
170
+ import { recordSkillLoadedEvent } from "../memory/skill-loaded-events-store.js";
159
171
  import type { UsageEvent } from "../usage/types.js";
160
172
  import {
161
173
  ACTIVATION_FUNNEL_VERSION,
@@ -238,6 +250,36 @@ function makeOnboardingEvent(
238
250
  };
239
251
  }
240
252
 
253
+ const TOOL_CONVERSATION_ID = "conv-reporter-tool-executed";
254
+
255
+ function seedToolInvocation(
256
+ spec: Omit<ToolInvocationSeedSpec, "conversationId">,
257
+ ): void {
258
+ seedToolInvocationRow(
259
+ { db: getDb(), conversations, toolInvocations },
260
+ { ...spec, conversationId: TOOL_CONVERSATION_ID },
261
+ );
262
+ }
263
+
264
+ /**
265
+ * Replace the default null-returning checkpoint mocks with a Map-backed
266
+ * implementation so values persisted by the reporter (e.g. the
267
+ * construction-time tool_executed watermark init) are visible to later
268
+ * reads within the same test.
269
+ */
270
+ function useStatefulCheckpoints(
271
+ seed: Record<string, string> = {},
272
+ ): Map<string, string> {
273
+ const checkpoints = new Map(Object.entries(seed));
274
+ mockGetMemoryCheckpoint.mockImplementation(
275
+ (key) => checkpoints.get(key) ?? null,
276
+ );
277
+ mockSetMemoryCheckpoint.mockImplementation((key, value) => {
278
+ checkpoints.set(key, value);
279
+ });
280
+ return checkpoints;
281
+ }
282
+
241
283
  const originalFetch = globalThis.fetch;
242
284
  let mockFetch: ReturnType<typeof mock>;
243
285
 
@@ -258,6 +300,8 @@ beforeEach(() => {
258
300
  mockQueryUnreportedOnboardingEvents.mockReset();
259
301
  mockQueryUnreportedOnboardingEvents.mockReturnValue([]);
260
302
  getDb().delete(authFallbackEvents).run();
303
+ getDb().delete(toolInvocations).run();
304
+ getDb().delete(skillLoadedEvents).run();
261
305
  mockPlatformClient = null;
262
306
  mockGetPlatformBaseUrl.mockReset();
263
307
  mockGetDeviceId.mockReset();
@@ -961,22 +1005,38 @@ describe("UsageTelemetryReporter", () => {
961
1005
  );
962
1006
 
963
1007
  const reporter = new UsageTelemetryReporter();
1008
+ // Construction initializes the absent tool_executed watermark; clear that
1009
+ // call so the count below covers only the flush's advancement.
1010
+ mockSetMemoryCheckpoint.mockClear();
964
1011
  await reporter.flush();
965
1012
 
966
1013
  // No HTTP call should have been made
967
1014
  expect(mockFetch).not.toHaveBeenCalled();
968
1015
 
969
- // All 5 timestamp watermarks should have been advanced (IDs left untouched
970
- // so the compound-cursor branch stays active)
971
- expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(5);
1016
+ // All 7 timestamp watermarks should have been advanced, and all 7 ID
1017
+ // watermarks pinned to the high-sorting sentinel (a truthy value keeps
1018
+ // the compound-cursor branch active while closing its same-millisecond
1019
+ // arm against opt-out rows).
1020
+ expect(mockSetMemoryCheckpoint).toHaveBeenCalledTimes(14);
972
1021
 
973
1022
  const calls = mockSetMemoryCheckpoint.mock.calls;
974
1023
  const keys = calls.map((c) => c[0]);
975
- expect(keys).toContain("telemetry:usage:last_reported_at");
976
- expect(keys).toContain("telemetry:turns:last_reported_at");
977
- expect(keys).toContain("telemetry:lifecycle:last_reported_at");
978
- expect(keys).toContain("telemetry:onboarding:last_reported_at");
979
- expect(keys).toContain("telemetry:auth_fallback:last_reported_at");
1024
+ const eventTypes = [
1025
+ "usage",
1026
+ "turns",
1027
+ "lifecycle",
1028
+ "onboarding",
1029
+ "auth_fallback",
1030
+ "tool_executed",
1031
+ "skill_loaded",
1032
+ ];
1033
+ for (const eventType of eventTypes) {
1034
+ expect(keys).toContain(`telemetry:${eventType}:last_reported_at`);
1035
+ const idCall = calls.find(
1036
+ (c) => c[0] === `telemetry:${eventType}:last_reported_id`,
1037
+ );
1038
+ expect(idCall?.[1]).toBe("ffffffff-ffff-ffff-ffff-ffffffffffff");
1039
+ }
980
1040
  });
981
1041
 
982
1042
  test("events sent normally after re-enabling collectUsageData", async () => {
@@ -1338,4 +1398,473 @@ describe("UsageTelemetryReporter", () => {
1338
1398
  expect(e.completed_at).toBeUndefined();
1339
1399
  expect(e.funnel_version).toBeUndefined();
1340
1400
  });
1401
+
1402
+ // -------------------------------------------------------------------------
1403
+ // Tool-executed events
1404
+ // -------------------------------------------------------------------------
1405
+
1406
+ test("tool_executed events project decision to status and never carry raw args/results", async () => {
1407
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1408
+ seedToolInvocation({
1409
+ id: "ti-ok",
1410
+ createdAt: 1700000001000,
1411
+ toolName: "calendar_list_events",
1412
+ decision: "allow",
1413
+ durationMs: 12,
1414
+ argBytes: 42,
1415
+ resultBytes: 9001,
1416
+ provider: "anthropic",
1417
+ model: "model-a",
1418
+ inferenceProfile: "balanced",
1419
+ inferenceProfileSource: "active",
1420
+ });
1421
+ // Errored invocation without LLM attribution — the attribution columns
1422
+ // are null and must pass through as null.
1423
+ seedToolInvocation({
1424
+ id: "ti-err",
1425
+ createdAt: 1700000002000,
1426
+ toolName: "web_search",
1427
+ decision: "error",
1428
+ durationMs: 7,
1429
+ argBytes: 17,
1430
+ resultBytes: 33,
1431
+ });
1432
+ // Permission-denied rows are filtered in the store and must never ship.
1433
+ seedToolInvocation({
1434
+ id: "ti-denied",
1435
+ createdAt: 1700000003000,
1436
+ decision: "denied",
1437
+ });
1438
+ mockFetch.mockImplementation(() =>
1439
+ Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
1440
+ );
1441
+
1442
+ const reporter = new UsageTelemetryReporter();
1443
+ await reporter.flush();
1444
+
1445
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1446
+ const rawBody = (mockFetch.mock.calls[0] as [string, RequestInit])[1]
1447
+ .body as string;
1448
+ // ToS/PII contract: raw tool args/outputs never leave the device.
1449
+ expect(rawBody).not.toContain(TOOL_PII_SENTINEL);
1450
+
1451
+ const body = JSON.parse(rawBody);
1452
+ expect(body.events.length).toBe(2);
1453
+
1454
+ const byId: Record<string, Record<string, unknown>> = {};
1455
+ for (const e of body.events as Array<{ daemon_event_id: string }>) {
1456
+ byId[e.daemon_event_id] = e;
1457
+ }
1458
+
1459
+ expect(byId["ti-ok"]).toEqual({
1460
+ type: "tool_executed",
1461
+ daemon_event_id: "ti-ok",
1462
+ recorded_at: 1700000001000,
1463
+ tool_name: "calendar_list_events",
1464
+ status: "fulfilled",
1465
+ duration_ms: 12,
1466
+ arg_bytes: 42,
1467
+ result_bytes: 9001,
1468
+ conversation_id: TOOL_CONVERSATION_ID,
1469
+ provider: "anthropic",
1470
+ model: "model-a",
1471
+ inference_profile: "balanced",
1472
+ inference_profile_source: "active",
1473
+ assistant_version: "1.2.3-test",
1474
+ });
1475
+ expect(byId["ti-err"]).toMatchObject({
1476
+ type: "tool_executed",
1477
+ tool_name: "web_search",
1478
+ status: "errored",
1479
+ duration_ms: 7,
1480
+ arg_bytes: 17,
1481
+ result_bytes: 33,
1482
+ provider: null,
1483
+ model: null,
1484
+ inference_profile: null,
1485
+ inference_profile_source: null,
1486
+ });
1487
+ expect(byId["ti-denied"]).toBeUndefined();
1488
+ });
1489
+
1490
+ test("tool_executed watermark advances to the last reported row on success", async () => {
1491
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1492
+ seedToolInvocation({ id: "ti-w1", createdAt: 1700000001000 });
1493
+ seedToolInvocation({ id: "ti-w2", createdAt: 1700000002000 });
1494
+ mockFetch.mockImplementation(() =>
1495
+ Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
1496
+ );
1497
+
1498
+ const reporter = new UsageTelemetryReporter();
1499
+ await reporter.flush();
1500
+
1501
+ const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
1502
+ (c) => c[0] === "telemetry:tool_executed:last_reported_at",
1503
+ );
1504
+ expect(watermarkCalls.length).toBeGreaterThanOrEqual(1);
1505
+ expect(watermarkCalls[watermarkCalls.length - 1][1]).toBe(
1506
+ String(1700000002000),
1507
+ );
1508
+
1509
+ const idCalls = mockSetMemoryCheckpoint.mock.calls.filter(
1510
+ (c) => c[0] === "telemetry:tool_executed:last_reported_id",
1511
+ );
1512
+ expect(idCalls.length).toBeGreaterThanOrEqual(1);
1513
+ expect(idCalls[idCalls.length - 1][1]).toBe("ti-w2");
1514
+ });
1515
+
1516
+ test("tool_executed resumes from a stored watermark — reported rows are not re-shipped", async () => {
1517
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1518
+ seedToolInvocation({ id: "ti-r1", createdAt: 1700000001000 });
1519
+ seedToolInvocation({ id: "ti-r2", createdAt: 1700000002000 });
1520
+ // A previous flush already reported ti-r1.
1521
+ mockGetMemoryCheckpoint.mockImplementation((key) => {
1522
+ if (key === "telemetry:tool_executed:last_reported_at") {
1523
+ return String(1700000001000);
1524
+ }
1525
+ if (key === "telemetry:tool_executed:last_reported_id") return "ti-r1";
1526
+ return null;
1527
+ });
1528
+ mockFetch.mockImplementation(() =>
1529
+ Promise.resolve(new Response('{"accepted":1}', { status: 200 })),
1530
+ );
1531
+
1532
+ const reporter = new UsageTelemetryReporter();
1533
+ await reporter.flush();
1534
+
1535
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1536
+ const body = JSON.parse(
1537
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1538
+ );
1539
+ expect(
1540
+ body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
1541
+ ).toEqual(["ti-r2"]);
1542
+ });
1543
+
1544
+ test("rows recorded after construction but before the first flush are shipped", async () => {
1545
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1546
+ // Regression coverage for the review finding: the reporter delays its
1547
+ // first flush by 30s+, so a tool used right after daemon startup is
1548
+ // recorded before any flush has run. The construction-time watermark
1549
+ // init must not drop it — only rows from before construction (rows
1550
+ // predating the reporter) stay behind the watermark.
1551
+ const checkpoints = useStatefulCheckpoints();
1552
+
1553
+ const reporter = new UsageTelemetryReporter();
1554
+ const rowCreatedAt =
1555
+ Number(checkpoints.get("telemetry:tool_executed:last_reported_at")) +
1556
+ 1000;
1557
+ seedToolInvocation({ id: "ti-pre-first-flush", createdAt: rowCreatedAt });
1558
+
1559
+ await reporter.flush();
1560
+
1561
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1562
+ const body = JSON.parse(
1563
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1564
+ );
1565
+ expect(body.events.length).toBe(1);
1566
+ expect(body.events[0]).toMatchObject({
1567
+ type: "tool_executed",
1568
+ daemon_event_id: "ti-pre-first-flush",
1569
+ recorded_at: rowCreatedAt,
1570
+ });
1571
+
1572
+ // The watermark advances to the shipped row, so the next flush resumes
1573
+ // after it instead of re-shipping.
1574
+ expect(checkpoints.get("telemetry:tool_executed:last_reported_at")).toBe(
1575
+ String(rowCreatedAt),
1576
+ );
1577
+ });
1578
+
1579
+ test("absent tool_executed checkpoint is initialized at construction — rows predating the reporter never ship", async () => {
1580
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1581
+ const checkpoints = useStatefulCheckpoints();
1582
+
1583
+ // Rows accumulated before any flush ever advanced the watermark — e.g.
1584
+ // an opt-out period under an older build that gated reporter
1585
+ // construction on collectUsageData while the always-on audit listener
1586
+ // kept writing.
1587
+ seedToolInvocation({
1588
+ id: "ti-opt-out-window",
1589
+ createdAt: Date.now() - 60_000,
1590
+ });
1591
+
1592
+ const reporter = new UsageTelemetryReporter();
1593
+
1594
+ // The checkpoint is persisted immediately at construction so a crash
1595
+ // before the first flush can't re-initialize later.
1596
+ const initialized = checkpoints.get(
1597
+ "telemetry:tool_executed:last_reported_at",
1598
+ );
1599
+ expect(initialized).toBeDefined();
1600
+
1601
+ // A tool that runs after construction ships normally.
1602
+ seedToolInvocation({
1603
+ id: "ti-post-construction",
1604
+ createdAt: Number(initialized) + 1000,
1605
+ });
1606
+
1607
+ await reporter.flush();
1608
+
1609
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1610
+ const body = JSON.parse(
1611
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1612
+ );
1613
+ expect(
1614
+ body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
1615
+ ).toEqual(["ti-post-construction"]);
1616
+ });
1617
+
1618
+ test("existing tool_executed checkpoint is respected — construction does not re-initialize", async () => {
1619
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1620
+ const checkpoints = useStatefulCheckpoints({
1621
+ "telemetry:tool_executed:last_reported_at": String(1700000001000),
1622
+ "telemetry:tool_executed:last_reported_id": "ti-already-reported",
1623
+ });
1624
+
1625
+ seedToolInvocation({
1626
+ id: "ti-already-reported",
1627
+ createdAt: 1700000001000,
1628
+ });
1629
+ // Legitimate backlog past the stored watermark — a re-initialization to
1630
+ // Date.now() at construction would silently drop it. Rows past the
1631
+ // watermark are always legitimately shippable: opted-out sessions keep
1632
+ // the watermark advancing via the opt-out flush branch (the reporter
1633
+ // runs even when collection is disabled), so the backlog can only hold
1634
+ // opted-in rows.
1635
+ seedToolInvocation({ id: "ti-backlog", createdAt: 1700000002000 });
1636
+
1637
+ const reporter = new UsageTelemetryReporter();
1638
+ expect(checkpoints.get("telemetry:tool_executed:last_reported_at")).toBe(
1639
+ String(1700000001000),
1640
+ );
1641
+
1642
+ await reporter.flush();
1643
+
1644
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1645
+ const body = JSON.parse(
1646
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1647
+ );
1648
+ expect(
1649
+ body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
1650
+ ).toEqual(["ti-backlog"]);
1651
+ });
1652
+
1653
+ test("opt-out window never ships across restarts — opted-out flushes keep the watermark ahead of audit rows", async () => {
1654
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1655
+ // A previous opted-in session shipped through this watermark.
1656
+ const checkpoints = useStatefulCheckpoints({
1657
+ "telemetry:tool_executed:last_reported_at": String(1700000001000),
1658
+ "telemetry:tool_executed:last_reported_id": "ti-shipped-opted-in",
1659
+ });
1660
+
1661
+ // Session 1: the user opted out and restarted. The daemon still
1662
+ // constructs and runs the reporter; the always-on audit listener keeps
1663
+ // writing rows. Every opted-out flush (5-minute cycle plus the final
1664
+ // flush in stop()) advances the watermark past them without sending.
1665
+ mockCollectUsageData = false;
1666
+ const optOutRowCreatedAt = Date.now() - 5_000;
1667
+ seedToolInvocation({
1668
+ id: "ti-opt-out-window",
1669
+ createdAt: optOutRowCreatedAt,
1670
+ });
1671
+ const optedOutReporter = new UsageTelemetryReporter();
1672
+ await optedOutReporter.stop(); // shutdown path: runs the final flush
1673
+ expect(mockFetch).not.toHaveBeenCalled();
1674
+ const advanced = Number(
1675
+ checkpoints.get("telemetry:tool_executed:last_reported_at"),
1676
+ );
1677
+ expect(advanced).toBeGreaterThan(optOutRowCreatedAt);
1678
+
1679
+ // Session 2: the user opts back in and restarts. Only rows recorded
1680
+ // after the opt-out epoch ship — the opt-out-window row never does.
1681
+ mockCollectUsageData = true;
1682
+ seedToolInvocation({ id: "ti-after-opt-in", createdAt: advanced + 1000 });
1683
+ const reporter = new UsageTelemetryReporter();
1684
+ await reporter.flush();
1685
+
1686
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1687
+ const body = JSON.parse(
1688
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1689
+ );
1690
+ expect(
1691
+ body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
1692
+ ).toEqual(["ti-after-opt-in"]);
1693
+ });
1694
+
1695
+ test("opt-out row written in the same millisecond as the opt-out flush never ships after re-opt-in", async () => {
1696
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1697
+ // A previous opted-in session left a low-sorting ID watermark behind.
1698
+ const checkpoints = useStatefulCheckpoints({
1699
+ "telemetry:tool_executed:last_reported_at": String(1700000001000),
1700
+ "telemetry:tool_executed:last_reported_id":
1701
+ "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
1702
+ });
1703
+
1704
+ // Opted-out flush: advances the timestamp watermark to Date.now() and
1705
+ // must also pin the ID watermark to the high-sorting sentinel.
1706
+ mockCollectUsageData = false;
1707
+ const optedOutReporter = new UsageTelemetryReporter();
1708
+ await optedOutReporter.flush();
1709
+ expect(mockFetch).not.toHaveBeenCalled();
1710
+ const watermark = Number(
1711
+ checkpoints.get("telemetry:tool_executed:last_reported_at"),
1712
+ );
1713
+
1714
+ // An audit row written in the SAME millisecond as the opt-out flush's
1715
+ // Date.now(), with a UUID sorting above the stale pre-opt-out ID
1716
+ // watermark. With only the timestamp watermark advanced, the compound
1717
+ // cursor's `createdAt == watermark AND id > afterId` arm would match it.
1718
+ seedToolInvocation({
1719
+ id: "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
1720
+ createdAt: watermark,
1721
+ });
1722
+
1723
+ // Re-opt-in: only rows strictly after the opt-out epoch ship.
1724
+ mockCollectUsageData = true;
1725
+ seedToolInvocation({ id: "ti-after-opt-in", createdAt: watermark + 1000 });
1726
+ const reporter = new UsageTelemetryReporter();
1727
+ await reporter.flush();
1728
+
1729
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1730
+ const body = JSON.parse(
1731
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1732
+ );
1733
+ expect(
1734
+ body.events.map((e: { daemon_event_id: string }) => e.daemon_event_id),
1735
+ ).toEqual(["ti-after-opt-in"]);
1736
+ });
1737
+
1738
+ test("checkpoint store failure during construction is non-fatal — degraded-DB daemons still start", () => {
1739
+ // initializeDb() failures are tolerated at daemon startup (degraded
1740
+ // mode), so the constructor's checkpoint init must never throw out of
1741
+ // the constructor and abort the daemon.
1742
+ mockGetMemoryCheckpoint.mockImplementation(() => {
1743
+ throw new Error("database disk image is malformed");
1744
+ });
1745
+ expect(() => new UsageTelemetryReporter()).not.toThrow();
1746
+
1747
+ // The write path failing is equally non-fatal.
1748
+ mockGetMemoryCheckpoint.mockImplementation(() => null);
1749
+ mockSetMemoryCheckpoint.mockImplementation(() => {
1750
+ throw new Error("database disk image is malformed");
1751
+ });
1752
+ expect(() => new UsageTelemetryReporter()).not.toThrow();
1753
+ });
1754
+
1755
+ // -------------------------------------------------------------------------
1756
+ // Skill-loaded events
1757
+ // -------------------------------------------------------------------------
1758
+
1759
+ test("skill_loaded events ship metadata with attribution and null passthrough", async () => {
1760
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1761
+ recordSkillLoadedEvent({
1762
+ conversationId: "conv-skill",
1763
+ skillName: "web-research",
1764
+ skillUpdatedAt: "2026-06-01T00:00:00.000Z",
1765
+ provider: "anthropic",
1766
+ model: "model-a",
1767
+ inferenceProfile: "balanced",
1768
+ inferenceProfileSource: "active",
1769
+ });
1770
+ // Minimal record — optional fields persist as null and ship as null.
1771
+ recordSkillLoadedEvent({ skillName: "tasks" });
1772
+ mockFetch.mockImplementation(() =>
1773
+ Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
1774
+ );
1775
+
1776
+ const reporter = new UsageTelemetryReporter();
1777
+ await reporter.flush();
1778
+
1779
+ expect(mockFetch).toHaveBeenCalledTimes(1);
1780
+ const body = JSON.parse(
1781
+ (mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string,
1782
+ );
1783
+ expect(body.events.length).toBe(2);
1784
+
1785
+ const byName: Record<string, Record<string, unknown>> = {};
1786
+ for (const e of body.events as Array<{ skill_name: string }>) {
1787
+ byName[e.skill_name] = e;
1788
+ }
1789
+
1790
+ expect(byName["web-research"]).toMatchObject({
1791
+ type: "skill_loaded",
1792
+ skill_name: "web-research",
1793
+ skill_updated_at: "2026-06-01T00:00:00.000Z",
1794
+ conversation_id: "conv-skill",
1795
+ provider: "anthropic",
1796
+ model: "model-a",
1797
+ inference_profile: "balanced",
1798
+ inference_profile_source: "active",
1799
+ assistant_version: "1.2.3-test",
1800
+ });
1801
+ expect(typeof byName["web-research"].daemon_event_id).toBe("string");
1802
+ expect(typeof byName["web-research"].recorded_at).toBe("number");
1803
+ expect(byName["tasks"]).toMatchObject({
1804
+ type: "skill_loaded",
1805
+ skill_name: "tasks",
1806
+ skill_updated_at: null,
1807
+ conversation_id: null,
1808
+ provider: null,
1809
+ model: null,
1810
+ inference_profile: null,
1811
+ inference_profile_source: null,
1812
+ });
1813
+ });
1814
+
1815
+ test("skill_loaded watermark advances to the last reported row on success", async () => {
1816
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1817
+ recordSkillLoadedEvent({ skillName: "web-research" });
1818
+ recordSkillLoadedEvent({ skillName: "tasks" });
1819
+ mockFetch.mockImplementation(() =>
1820
+ Promise.resolve(new Response('{"accepted":2}', { status: 200 })),
1821
+ );
1822
+
1823
+ // The last row by the reporter's (createdAt, id) cursor order is the one
1824
+ // whose watermark should be persisted after a successful upload.
1825
+ const rows = getDb()
1826
+ .select()
1827
+ .from(skillLoadedEvents)
1828
+ .orderBy(skillLoadedEvents.createdAt, skillLoadedEvents.id)
1829
+ .all();
1830
+ const lastRow = rows[rows.length - 1];
1831
+
1832
+ const reporter = new UsageTelemetryReporter();
1833
+ await reporter.flush();
1834
+
1835
+ const watermarkCalls = mockSetMemoryCheckpoint.mock.calls.filter(
1836
+ (c) => c[0] === "telemetry:skill_loaded:last_reported_at",
1837
+ );
1838
+ expect(watermarkCalls.length).toBeGreaterThanOrEqual(1);
1839
+ expect(watermarkCalls[watermarkCalls.length - 1][1]).toBe(
1840
+ String(lastRow.createdAt),
1841
+ );
1842
+
1843
+ const idCalls = mockSetMemoryCheckpoint.mock.calls.filter(
1844
+ (c) => c[0] === "telemetry:skill_loaded:last_reported_id",
1845
+ );
1846
+ expect(idCalls.length).toBeGreaterThanOrEqual(1);
1847
+ expect(idCalls[idCalls.length - 1][1]).toBe(lastRow.id);
1848
+ });
1849
+
1850
+ test("batch recursion when skill_loaded returns a full batch, capped at 10", async () => {
1851
+ mockQueryUnreportedUsageEvents.mockReturnValue([]);
1852
+ // Exactly BATCH_SIZE rows; the mocked checkpoints never persist, so each
1853
+ // recursion re-reads the same full batch until the cap.
1854
+ const fullBatch = Array.from({ length: 500 }, (_, i) => ({
1855
+ id: `sle-batch-${String(i).padStart(3, "0")}`,
1856
+ createdAt: 1700000000000 + i,
1857
+ skillName: "web-research",
1858
+ }));
1859
+ getDb().insert(skillLoadedEvents).values(fullBatch).run();
1860
+ mockFetch.mockImplementation(() =>
1861
+ Promise.resolve(new Response('{"accepted":500}', { status: 200 })),
1862
+ );
1863
+
1864
+ const reporter = new UsageTelemetryReporter();
1865
+ await reporter.flush();
1866
+
1867
+ // MAX_CONSECUTIVE_BATCHES = 10
1868
+ expect(mockFetch).toHaveBeenCalledTimes(10);
1869
+ });
1341
1870
  });