@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
@@ -0,0 +1,39 @@
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
+
3
+ const TABLE = "tool_invocations";
4
+ const COLUMNS: Array<{ name: string; type: string }> = [
5
+ { name: "arg_bytes", type: "INTEGER" },
6
+ { name: "result_bytes", type: "INTEGER" },
7
+ { name: "provider", type: "TEXT" },
8
+ { name: "model", type: "TEXT" },
9
+ { name: "inference_profile", type: "TEXT" },
10
+ { name: "inference_profile_source", type: "TEXT" },
11
+ ];
12
+
13
+ /**
14
+ * Add nullable telemetry columns to `tool_invocations` for the
15
+ * `tool_executed` telemetry projection: serialized payload sizes (only the
16
+ * sizes leave the device, never the payloads) and the conversation's model
17
+ * attribution at invocation time. Nullable so pre-migration and
18
+ * permission-denied rows stay NULL (see the legacy-row filter in
19
+ * tool-executed-events-store.ts).
20
+ *
21
+ * Idempotent pure DDL with a PRAGMA guard, no registry entry needed
22
+ * (matches the 273 / 275 pattern).
23
+ */
24
+ export function migrateToolInvocationsTelemetryColumns(
25
+ database: DrizzleDb,
26
+ ): void {
27
+ const raw = getSqliteFrom(database);
28
+
29
+ const columns = raw.query(`PRAGMA table_info(${TABLE})`).all() as Array<{
30
+ name: string;
31
+ }>;
32
+ const columnNames = new Set(columns.map((c) => c.name));
33
+
34
+ for (const { name, type } of COLUMNS) {
35
+ if (!columnNames.has(name)) {
36
+ raw.exec(`ALTER TABLE ${TABLE} ADD COLUMN ${name} ${type}`);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,84 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { createSkillLoadedEventsTable } from "./279-create-skill-loaded-events.js";
8
+
9
+ function createTestDb() {
10
+ const sqlite = new Database(":memory:");
11
+ return { sqlite, db: drizzle(sqlite, { schema }) };
12
+ }
13
+
14
+ function columnNames(sqlite: Database): string[] {
15
+ return (
16
+ sqlite.query("PRAGMA table_info(skill_loaded_events)").all() as Array<{
17
+ name: string;
18
+ }>
19
+ ).map((c) => c.name);
20
+ }
21
+
22
+ function indexNames(sqlite: Database): string[] {
23
+ return (
24
+ sqlite.query("PRAGMA index_list(skill_loaded_events)").all() as Array<{
25
+ name: string;
26
+ }>
27
+ ).map((i) => i.name);
28
+ }
29
+
30
+ describe("migration 279: skill_loaded_events table", () => {
31
+ test("creates the table with the expected columns", () => {
32
+ const { sqlite, db } = createTestDb();
33
+
34
+ createSkillLoadedEventsTable(db);
35
+
36
+ expect(columnNames(sqlite)).toEqual([
37
+ "id",
38
+ "created_at",
39
+ "conversation_id",
40
+ "skill_name",
41
+ "skill_updated_at",
42
+ "provider",
43
+ "model",
44
+ "inference_profile",
45
+ "inference_profile_source",
46
+ ]);
47
+ });
48
+
49
+ test("creates the (created_at, id) cursor index", () => {
50
+ const { sqlite, db } = createTestDb();
51
+
52
+ createSkillLoadedEventsTable(db);
53
+
54
+ expect(indexNames(sqlite)).toContain(
55
+ "idx_skill_loaded_events_created_at_id",
56
+ );
57
+ const columns = (
58
+ sqlite
59
+ .query("PRAGMA index_info(idx_skill_loaded_events_created_at_id)")
60
+ .all() as Array<{ name: string }>
61
+ ).map((c) => c.name);
62
+ expect(columns).toEqual(["created_at", "id"]);
63
+ });
64
+
65
+ test("is idempotent — re-run is a no-op and preserves existing rows", () => {
66
+ const { sqlite, db } = createTestDb();
67
+
68
+ createSkillLoadedEventsTable(db);
69
+ sqlite.exec(/*sql*/ `
70
+ INSERT INTO skill_loaded_events (id, created_at, skill_name)
71
+ VALUES ('sle-1', 1000, 'web-research')
72
+ `);
73
+
74
+ expect(() => createSkillLoadedEventsTable(db)).not.toThrow();
75
+
76
+ const rows = sqlite.query("SELECT id FROM skill_loaded_events").all();
77
+ expect(rows).toEqual([{ id: "sle-1" }]);
78
+ expect(
79
+ indexNames(sqlite).filter(
80
+ (name) => name === "idx_skill_loaded_events_created_at_id",
81
+ ),
82
+ ).toHaveLength(1);
83
+ });
84
+ });
@@ -0,0 +1,26 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+
3
+ /**
4
+ * Create the skill_loaded_events table for `skill_loaded` telemetry events —
5
+ * one row per activation of a Vellum-produced skill (see
6
+ * skill-loaded-events-store.ts for the data contract). The index backs the
7
+ * telemetry reporter's compound `(created_at, id)` watermark cursor.
8
+ */
9
+ export function createSkillLoadedEventsTable(database: DrizzleDb): void {
10
+ database.run(/*sql*/ `
11
+ CREATE TABLE IF NOT EXISTS skill_loaded_events (
12
+ id TEXT PRIMARY KEY,
13
+ created_at INTEGER NOT NULL,
14
+ conversation_id TEXT,
15
+ skill_name TEXT NOT NULL,
16
+ skill_updated_at TEXT,
17
+ provider TEXT,
18
+ model TEXT,
19
+ inference_profile TEXT,
20
+ inference_profile_source TEXT
21
+ )
22
+ `);
23
+ database.run(
24
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_skill_loaded_events_created_at_id ON skill_loaded_events (created_at, id)`,
25
+ );
26
+ }
@@ -0,0 +1,88 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { migrateConversationsSurfacedAt } from "./280-conversations-surfaced-at.js";
8
+
9
+ function createTestDb() {
10
+ const sqlite = new Database(":memory:");
11
+ // Pre-280 shape: no surfaced_at column (trimmed to the columns the
12
+ // migration and assertions touch).
13
+ sqlite.exec(/*sql*/ `
14
+ CREATE TABLE conversations (
15
+ id TEXT PRIMARY KEY,
16
+ title TEXT,
17
+ created_at INTEGER NOT NULL,
18
+ updated_at INTEGER NOT NULL,
19
+ conversation_type TEXT NOT NULL DEFAULT 'standard',
20
+ archived_at INTEGER
21
+ )
22
+ `);
23
+ return { sqlite, db: drizzle(sqlite, { schema }) };
24
+ }
25
+
26
+ function columnNames(sqlite: Database): string[] {
27
+ return (
28
+ sqlite.query("PRAGMA table_info(conversations)").all() as Array<{
29
+ name: string;
30
+ }>
31
+ ).map((c) => c.name);
32
+ }
33
+
34
+ function indexNames(sqlite: Database): string[] {
35
+ return (
36
+ sqlite
37
+ .query(
38
+ "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'conversations'",
39
+ )
40
+ .all() as Array<{ name: string }>
41
+ ).map((i) => i.name);
42
+ }
43
+
44
+ describe("migration 280: conversations surfaced_at", () => {
45
+ test("adds a nullable surfaced_at column and index", () => {
46
+ const { sqlite, db } = createTestDb();
47
+ expect(columnNames(sqlite)).not.toContain("surfaced_at");
48
+
49
+ migrateConversationsSurfacedAt(db);
50
+
51
+ const column = (
52
+ sqlite.query("PRAGMA table_info(conversations)").all() as Array<{
53
+ name: string;
54
+ notnull: number;
55
+ dflt_value: string | null;
56
+ }>
57
+ ).find((c) => c.name === "surfaced_at");
58
+ expect(column).toBeDefined();
59
+ expect(column?.notnull).toBe(0);
60
+ expect(indexNames(sqlite)).toContain("idx_conversations_surfaced_at");
61
+ });
62
+
63
+ test("existing rows read back with surfaced_at null", () => {
64
+ const { sqlite, db } = createTestDb();
65
+ sqlite.exec(/*sql*/ `
66
+ INSERT INTO conversations (id, title, created_at, updated_at, conversation_type)
67
+ VALUES ('conv-1', 'Background run', 1000, 1000, 'background')
68
+ `);
69
+
70
+ migrateConversationsSurfacedAt(db);
71
+
72
+ const row = sqlite
73
+ .query("SELECT surfaced_at FROM conversations WHERE id = 'conv-1'")
74
+ .get() as { surfaced_at: number | null };
75
+ expect(row.surfaced_at).toBeNull();
76
+ });
77
+
78
+ test("is idempotent — re-run is a no-op", () => {
79
+ const { sqlite, db } = createTestDb();
80
+
81
+ migrateConversationsSurfacedAt(db);
82
+ expect(() => migrateConversationsSurfacedAt(db)).not.toThrow();
83
+
84
+ expect(
85
+ columnNames(sqlite).filter((name) => name === "surfaced_at"),
86
+ ).toHaveLength(1);
87
+ });
88
+ });
@@ -0,0 +1,24 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { tableHasColumn } from "./schema-introspection.js";
3
+
4
+ /**
5
+ * Add a nullable `surfaced_at` column to `conversations`.
6
+ *
7
+ * Non-null marks a background/scheduled conversation as explicitly promoted
8
+ * ("surfaced") into the sidebar's Recents grouping. Promotion is always an
9
+ * explicit API call (`POST /v1/conversations/:id/surface`) — nothing sets
10
+ * this automatically, so all existing rows stay NULL and default sidebar
11
+ * behavior is unchanged.
12
+ *
13
+ * Idempotent — re-running is a no-op once the column and index exist.
14
+ */
15
+ export function migrateConversationsSurfacedAt(database: DrizzleDb): void {
16
+ if (!tableHasColumn(database, "conversations", "surfaced_at")) {
17
+ database.run(
18
+ `ALTER TABLE conversations ADD COLUMN surfaced_at INTEGER DEFAULT NULL`,
19
+ );
20
+ }
21
+ database.run(
22
+ `CREATE INDEX IF NOT EXISTS idx_conversations_surfaced_at ON conversations (surfaced_at)`,
23
+ );
24
+ }
@@ -257,11 +257,21 @@ export { migrateLlmUsageEventsAddAssistantVersion } from "./267-llm-usage-events
257
257
  export { migrateAddMemoryV3Selections } from "./268-add-memory-v3-selections.js";
258
258
  export { migrateScheduleScriptTimeout } from "./269-schedule-script-timeout.js";
259
259
  export { migrateMessagesRoleCreatedAtIndex } from "./270-messages-role-created-at-index.js";
260
+ export {
261
+ downScheduleDescription,
262
+ migrateScheduleDescription,
263
+ } from "./270-schedule-description.js";
260
264
  export { migrateScheduleSourceConversation } from "./270-schedule-source-conversation.js";
261
265
  export { createAuthFallbackEventsTable } from "./271-create-auth-fallback-events.js";
262
266
  export { migrateAcpSessionHistoryCwd } from "./272-acp-session-history-cwd.js";
263
267
  export { migrateOnboardingEventsFunnelColumns } from "./273-onboarding-events-funnel-columns.js";
264
268
  export { createActivationSessionsTable } from "./274-create-activation-sessions.js";
269
+ export { migrateToolInvocationsSkillId } from "./275-tool-invocations-add-skill-id.js";
270
+ export { migrateToolInvocationsCreatedAtIdIndex } from "./276-tool-invocations-created-at-id-index.js";
271
+ export { migrateAddMemoryV3EverInjected } from "./277-add-memory-v3-ever-injected.js";
272
+ export { migrateToolInvocationsTelemetryColumns } from "./278-tool-invocations-telemetry-columns.js";
273
+ export { createSkillLoadedEventsTable } from "./279-create-skill-loaded-events.js";
274
+ export { migrateConversationsSurfacedAt } from "./280-conversations-surfaced-at.js";
265
275
  export {
266
276
  MIGRATION_REGISTRY,
267
277
  type MigrationRegistryEntry,
@@ -57,6 +57,7 @@ import { downRenameCleanedAt } from "./260-rename-cleaned-at.js";
57
57
  import { downLlmUsageAddRawUsage } from "./261-llm-usage-add-raw-usage.js";
58
58
  import { downMemoryV3Coactivation } from "./262-memory-v3-coactivation.js";
59
59
  import { downMemoryV3AutoEdges } from "./263-memory-v3-auto-edges.js";
60
+ import { downScheduleDescription } from "./270-schedule-description.js";
60
61
 
61
62
  export interface MigrationRegistryEntry {
62
63
  /** The checkpoint key written to memory_checkpoints on completion. */
@@ -486,6 +487,13 @@ export const MIGRATION_REGISTRY: MigrationRegistryEntry[] = [
486
487
  "Create memory_v3_auto_edges table — weighted, decaying learned association graph (distinct from curated edges:) accrued by the edge-learning job from used co-activations and consumed above-threshold by edge expansion",
487
488
  down: downMemoryV3AutoEdges,
488
489
  },
490
+ {
491
+ key: "migration_schedule_description_backfill_v1",
492
+ version: 57,
493
+ description:
494
+ "Backfill authored schedule descriptions for legacy non-defer schedules from their existing names",
495
+ down: downScheduleDescription,
496
+ },
489
497
  ];
490
498
 
491
499
  export function getMaxMigrationVersion(): number {
@@ -41,6 +41,13 @@ export const conversations = sqliteTable(
41
41
  scheduleJobId: text("schedule_job_id"),
42
42
  lastMessageAt: integer("last_message_at"),
43
43
  archivedAt: integer("archived_at"),
44
+ /**
45
+ * Epoch-ms timestamp set when a background/scheduled conversation is
46
+ * explicitly promoted into the sidebar's Recents grouping. NULL (the
47
+ * default) means not surfaced. Set/cleared only via the surface API —
48
+ * never automatically.
49
+ */
50
+ surfacedAt: integer("surfaced_at"),
44
51
  inferenceProfile: text("inference_profile"),
45
52
  inferenceProfileSessionId: text("inference_profile_session_id"),
46
53
  inferenceProfileExpiresAt: integer("inference_profile_expires_at"),
@@ -51,6 +58,7 @@ export const conversations = sqliteTable(
51
58
  index("idx_conversations_last_message_at").on(table.lastMessageAt),
52
59
  index("idx_conversations_conversation_type").on(table.conversationType),
53
60
  index("idx_conversations_archived_at").on(table.archivedAt),
61
+ index("idx_conversations_surfaced_at").on(table.surfacedAt),
54
62
  index("idx_conversations_fork_parent_conversation_id").on(
55
63
  table.forkParentConversationId,
56
64
  ),
@@ -93,6 +101,14 @@ export const toolInvocations = sqliteTable(
93
101
  matchedTrustRuleId: text("matched_trust_rule_id"),
94
102
  durationMs: integer("duration_ms").notNull(),
95
103
  createdAt: integer("created_at").notNull(),
104
+ /** Serialized input size in bytes, computed before any redaction. Null pre-migration-278. */
105
+ argBytes: integer("arg_bytes"),
106
+ /** Full serialized result size in bytes, computed before truncation/redaction. Null pre-migration-278 and for denied rows. */
107
+ resultBytes: integer("result_bytes"),
108
+ provider: text("provider"),
109
+ model: text("model"),
110
+ inferenceProfile: text("inference_profile"),
111
+ inferenceProfileSource: text("inference_profile_source"),
96
112
  },
97
113
  (table) => [
98
114
  index("idx_tool_invocations_conversation_id").on(table.conversationId),
@@ -10,6 +10,7 @@ import {
10
10
  export const cronJobs = sqliteTable("cron_jobs", {
11
11
  id: text("id").primaryKey(),
12
12
  name: text("name").notNull(),
13
+ description: text("description").notNull().default(""),
13
14
  enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
14
15
  cronExpression: text("cron_expression"), // nullable for one-shot schedules; e.g. '0 9 * * 1-5'
15
16
  scheduleSyntax: text("schedule_syntax").notNull().default("cron"), // 'cron' | 'rrule'
@@ -307,6 +308,31 @@ export const activationSessions = sqliteTable("activation_sessions", {
307
308
  createdAt: integer("created_at").notNull(),
308
309
  });
309
310
 
311
+ // One row per `skill_loaded` telemetry event, emitted when a Vellum-produced
312
+ // skill is activated in a conversation — see skill-loaded-events-store.ts for
313
+ // the data contract. Flushed by the usage telemetry reporter.
314
+ export const skillLoadedEvents = sqliteTable(
315
+ "skill_loaded_events",
316
+ {
317
+ id: text("id").primaryKey(),
318
+ createdAt: integer("created_at").notNull(),
319
+ conversationId: text("conversation_id"),
320
+ skillName: text("skill_name").notNull(),
321
+ // ISO 8601 timestamp from the merged skill catalog, when known.
322
+ skillUpdatedAt: text("skill_updated_at"),
323
+ provider: text("provider"),
324
+ model: text("model"),
325
+ inferenceProfile: text("inference_profile"),
326
+ inferenceProfileSource: text("inference_profile_source"),
327
+ },
328
+ (table) => [
329
+ index("idx_skill_loaded_events_created_at_id").on(
330
+ table.createdAt,
331
+ table.id,
332
+ ),
333
+ ],
334
+ );
335
+
310
336
  export const traceEvents = sqliteTable(
311
337
  "trace_events",
312
338
  {
@@ -0,0 +1,160 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Silence the logger.
4
+ mock.module("../util/logger.js", () => ({
5
+ getLogger: () =>
6
+ new Proxy({} as Record<string, unknown>, {
7
+ get: () => () => {},
8
+ }),
9
+ }));
10
+
11
+ let collectUsageData = true;
12
+
13
+ mock.module("../config/loader.js", () => ({
14
+ getConfig: () => ({
15
+ ui: {},
16
+ model: "test",
17
+ provider: "test",
18
+ memory: { enabled: false },
19
+ rateLimit: { maxRequestsPerMinute: 0 },
20
+ secretDetection: { enabled: false },
21
+ collectUsageData,
22
+ }),
23
+ }));
24
+
25
+ import { getDb } from "./db-connection.js";
26
+ import { initializeDb } from "./db-init.js";
27
+ import { skillLoadedEvents } from "./schema.js";
28
+ import {
29
+ queryUnreportedSkillLoadedEvents,
30
+ recordSkillLoadedEvent,
31
+ } from "./skill-loaded-events-store.js";
32
+
33
+ initializeDb();
34
+
35
+ function insertEvent(
36
+ id: string,
37
+ createdAt: number,
38
+ skillName = "web-research",
39
+ ): void {
40
+ getDb().insert(skillLoadedEvents).values({ id, createdAt, skillName }).run();
41
+ }
42
+
43
+ describe("skill-loaded-events-store", () => {
44
+ beforeEach(() => {
45
+ collectUsageData = true;
46
+ getDb().delete(skillLoadedEvents).run();
47
+ });
48
+
49
+ test("honors the collectUsageData opt-out (records nothing)", () => {
50
+ collectUsageData = false;
51
+ recordSkillLoadedEvent({ skillName: "web-research" });
52
+ expect(queryUnreportedSkillLoadedEvents(0, undefined, 10)).toHaveLength(0);
53
+ });
54
+
55
+ test("record + query round-trips all fields", () => {
56
+ recordSkillLoadedEvent({
57
+ conversationId: "conv-xyz",
58
+ skillName: "web-research",
59
+ skillUpdatedAt: "2026-06-01T00:00:00.000Z",
60
+ provider: "anthropic",
61
+ model: "model-a",
62
+ inferenceProfile: "balanced",
63
+ inferenceProfileSource: "active-profile",
64
+ });
65
+
66
+ const rows = queryUnreportedSkillLoadedEvents(0, undefined, 10);
67
+ expect(rows).toHaveLength(1);
68
+ const row = rows[0]!;
69
+ expect(row.id).toBeString();
70
+ expect(row.createdAt).toBeGreaterThan(0);
71
+ expect(row).toMatchObject({
72
+ conversationId: "conv-xyz",
73
+ skillName: "web-research",
74
+ skillUpdatedAt: "2026-06-01T00:00:00.000Z",
75
+ provider: "anthropic",
76
+ model: "model-a",
77
+ inferenceProfile: "balanced",
78
+ inferenceProfileSource: "active-profile",
79
+ });
80
+ });
81
+
82
+ test("optional fields persist as null", () => {
83
+ recordSkillLoadedEvent({ skillName: "tasks" });
84
+
85
+ const rows = queryUnreportedSkillLoadedEvents(0, undefined, 10);
86
+ expect(rows).toHaveLength(1);
87
+ expect(rows[0]).toMatchObject({
88
+ skillName: "tasks",
89
+ conversationId: null,
90
+ skillUpdatedAt: null,
91
+ provider: null,
92
+ model: null,
93
+ inferenceProfile: null,
94
+ inferenceProfileSource: null,
95
+ });
96
+ });
97
+
98
+ test("returns rows in (createdAt, id) order", () => {
99
+ insertEvent("sle-b", 2000);
100
+ insertEvent("sle-a", 1000);
101
+
102
+ const rows = queryUnreportedSkillLoadedEvents(0, undefined, 100);
103
+ expect(rows.map((r) => r.id)).toEqual(["sle-a", "sle-b"]);
104
+ });
105
+
106
+ test("query advances past the compound (createdAt, id) cursor", () => {
107
+ // Two rows in the same millisecond: pagination must use the id
108
+ // tiebreaker to make forward progress, not loop.
109
+ insertEvent("sle-1", 5000);
110
+ insertEvent("sle-2", 5000);
111
+ insertEvent("sle-3", 6000);
112
+
113
+ const first = queryUnreportedSkillLoadedEvents(0, undefined, 1);
114
+ expect(first.map((r) => r.id)).toEqual(["sle-1"]);
115
+
116
+ const second = queryUnreportedSkillLoadedEvents(
117
+ first[0]!.createdAt,
118
+ first[0]!.id,
119
+ 100,
120
+ );
121
+ expect(second.map((r) => r.id)).toEqual(["sle-2", "sle-3"]);
122
+
123
+ // Without an id cursor the timestamp-only branch is used.
124
+ expect(
125
+ queryUnreportedSkillLoadedEvents(5000, undefined, 100).map((r) => r.id),
126
+ ).toEqual(["sle-3"]);
127
+
128
+ // Cursor past the last row returns nothing.
129
+ const last = second[second.length - 1]!;
130
+ expect(
131
+ queryUnreportedSkillLoadedEvents(last.createdAt, last.id, 100).length,
132
+ ).toBe(0);
133
+ });
134
+
135
+ test("resumes from a persisted watermark without re-reporting", () => {
136
+ insertEvent("sle-w1", 1000);
137
+ insertEvent("sle-w2", 2000);
138
+
139
+ const batch = queryUnreportedSkillLoadedEvents(0, undefined, 100);
140
+ const watermark = batch[batch.length - 1]!;
141
+
142
+ insertEvent("sle-w3", 3000);
143
+
144
+ const resumed = queryUnreportedSkillLoadedEvents(
145
+ watermark.createdAt,
146
+ watermark.id,
147
+ 100,
148
+ );
149
+ expect(resumed.map((r) => r.id)).toEqual(["sle-w3"]);
150
+ });
151
+
152
+ test("honors the limit", () => {
153
+ insertEvent("sle-l1", 1000);
154
+ insertEvent("sle-l2", 2000);
155
+ insertEvent("sle-l3", 3000);
156
+
157
+ const rows = queryUnreportedSkillLoadedEvents(0, undefined, 2);
158
+ expect(rows.map((r) => r.id)).toEqual(["sle-l1", "sle-l2"]);
159
+ });
160
+ });
@@ -0,0 +1,95 @@
1
+ import { and, asc, eq, gt, or } from "drizzle-orm";
2
+ import { v4 as uuid } from "uuid";
3
+
4
+ import { getConfig } from "../config/loader.js";
5
+ import type { UsageAttributionColumns } from "../usage/attribution.js";
6
+ import { getDb } from "./db-connection.js";
7
+ import { skillLoadedEvents } from "./schema.js";
8
+
9
+ /**
10
+ * Input for one `skill_loaded` telemetry event. Metadata only — never skill
11
+ * output or conversation content. The attribution columns reuse the shared
12
+ * nullable shape from `toAttributionColumns` so producers can spread it
13
+ * directly; null and absent both persist as NULL.
14
+ */
15
+ export interface SkillLoadedEventRecord extends Partial<UsageAttributionColumns> {
16
+ conversationId?: string;
17
+ skillName: string;
18
+ /** ISO 8601 timestamp from the merged skill catalog, when known. */
19
+ skillUpdatedAt?: string;
20
+ }
21
+
22
+ /** A persisted skill_loaded event row. */
23
+ export interface SkillLoadedEvent {
24
+ id: string;
25
+ createdAt: number;
26
+ conversationId: string | null;
27
+ skillName: string;
28
+ skillUpdatedAt: string | null;
29
+ provider: string | null;
30
+ model: string | null;
31
+ inferenceProfile: string | null;
32
+ inferenceProfileSource: string | null;
33
+ }
34
+
35
+ /**
36
+ * Record a `skill_loaded` telemetry event for a skill activation. No-ops when
37
+ * usage data collection is disabled (the event is dropped to honor the
38
+ * opt-out, matching the rest of telemetry).
39
+ */
40
+ export function recordSkillLoadedEvent(record: SkillLoadedEventRecord): void {
41
+ if (!getConfig().collectUsageData) return;
42
+ const db = getDb();
43
+ db.insert(skillLoadedEvents)
44
+ .values({
45
+ id: uuid(),
46
+ createdAt: Date.now(),
47
+ conversationId: record.conversationId,
48
+ skillName: record.skillName,
49
+ skillUpdatedAt: record.skillUpdatedAt,
50
+ provider: record.provider,
51
+ model: record.model,
52
+ inferenceProfile: record.inferenceProfile,
53
+ inferenceProfileSource: record.inferenceProfileSource,
54
+ })
55
+ .run();
56
+ }
57
+
58
+ /**
59
+ * Query skill_loaded events that haven't been reported to telemetry yet.
60
+ * Uses a compound cursor (createdAt + id) for reliable watermarking.
61
+ */
62
+ export function queryUnreportedSkillLoadedEvents(
63
+ afterCreatedAt: number,
64
+ afterId: string | undefined,
65
+ limit: number,
66
+ ): SkillLoadedEvent[] {
67
+ const db = getDb();
68
+ return db
69
+ .select({
70
+ id: skillLoadedEvents.id,
71
+ createdAt: skillLoadedEvents.createdAt,
72
+ conversationId: skillLoadedEvents.conversationId,
73
+ skillName: skillLoadedEvents.skillName,
74
+ skillUpdatedAt: skillLoadedEvents.skillUpdatedAt,
75
+ provider: skillLoadedEvents.provider,
76
+ model: skillLoadedEvents.model,
77
+ inferenceProfile: skillLoadedEvents.inferenceProfile,
78
+ inferenceProfileSource: skillLoadedEvents.inferenceProfileSource,
79
+ })
80
+ .from(skillLoadedEvents)
81
+ .where(
82
+ afterId
83
+ ? or(
84
+ gt(skillLoadedEvents.createdAt, afterCreatedAt),
85
+ and(
86
+ eq(skillLoadedEvents.createdAt, afterCreatedAt),
87
+ gt(skillLoadedEvents.id, afterId),
88
+ ),
89
+ )
90
+ : gt(skillLoadedEvents.createdAt, afterCreatedAt),
91
+ )
92
+ .orderBy(asc(skillLoadedEvents.createdAt), asc(skillLoadedEvents.id))
93
+ .limit(limit)
94
+ .all();
95
+ }