@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,278 @@
1
+ /**
2
+ * Per-conversation everInjected record for memory-v3's frozen-card carry.
3
+ *
4
+ * Backed by `memory_v3_ever_injected` (migration 277): one row per
5
+ * (conversation, page-slug) the v3 injector ever attached as a card. The
6
+ * active (non-pruned) slug set is the injection dedup record — a slug present
7
+ * here rides the cached message prefix and must not be re-rendered — and
8
+ * `bytes` sums into the resident footprint the prune valve bounds. Rows are
9
+ * never deleted by pruning (`pruned_at` is set instead) so the record stays
10
+ * auditable; a pruned page that is re-selected re-injects by clearing
11
+ * `pruned_at` on upsert. `clearConversation` is the compaction reset: the
12
+ * cached blocks those slugs lived on are gone, so future turns are free to
13
+ * re-inject them.
14
+ *
15
+ * Fork semantics mirror v2's activation-store hooks (both run synchronously
16
+ * inside the `forkConversation()` transaction — see
17
+ * `assistant/src/memory/conversation-crud.ts`):
18
+ * - full-history forks copy the parent's rows wholesale
19
+ * (`forkEverInjected`), pruned state included;
20
+ * - truncated forks seed from the slugs scanned out of the inherited
21
+ * messages' persisted card blocks (`seedEverInjectedFromSlugs`) — a
22
+ * wholesale copy would over-claim slugs injected on turns the child does
23
+ * not contain, suppressing their re-injection forever.
24
+ */
25
+
26
+ import {
27
+ type DrizzleDb,
28
+ getDb,
29
+ getSqliteFrom,
30
+ } from "../../../memory/db-connection.js";
31
+
32
+ /**
33
+ * Message-metadata key the v3 injector persists each turn's card block under
34
+ * (the v3 counterpart of v2's `memoryInjectedBlock`). Shared by the writer,
35
+ * the conversation-load rehydration splice, and the truncated-fork seed scan
36
+ * in `conversation-crud.ts` so all three agree on the key.
37
+ */
38
+ export const MEMORY_V3_INJECTED_BLOCK_METADATA_KEY = "memoryV3InjectedBlock";
39
+
40
+ export interface EverInjectedEntry {
41
+ bytes: number;
42
+ /** Epoch ms the prune valve removed the card, or `null` while resident. */
43
+ prunedAt: number | null;
44
+ }
45
+
46
+ /**
47
+ * The full per-conversation record, pruned rows included. Test oracle only —
48
+ * no production code path reads it; production consumers use the narrower
49
+ * accessors ({@link getActiveSlugs}, {@link getActiveEntries},
50
+ * {@link getPrunedSlugs}, {@link residentBytes}).
51
+ */
52
+ export function getInjected(
53
+ conversationId: string,
54
+ ): Map<string, EverInjectedEntry> {
55
+ const rows = getSqliteFrom(getDb())
56
+ .query(
57
+ /*sql*/ `
58
+ SELECT slug, bytes, pruned_at AS prunedAt FROM memory_v3_ever_injected
59
+ WHERE conversation_id = ?
60
+ `,
61
+ )
62
+ .all(conversationId) as Array<{
63
+ slug: string;
64
+ bytes: number;
65
+ prunedAt: number | null;
66
+ }>;
67
+
68
+ return new Map(
69
+ rows.map((row) => [row.slug, { bytes: row.bytes, prunedAt: row.prunedAt }]),
70
+ );
71
+ }
72
+
73
+ /** The injection dedup set: slugs whose cards are currently resident. */
74
+ export function getActiveSlugs(conversationId: string): Set<string> {
75
+ const rows = getSqliteFrom(getDb())
76
+ .query(
77
+ /*sql*/ `
78
+ SELECT slug FROM memory_v3_ever_injected
79
+ WHERE conversation_id = ? AND pruned_at IS NULL
80
+ `,
81
+ )
82
+ .all(conversationId) as Array<{ slug: string }>;
83
+ return new Set(rows.map((row) => row.slug));
84
+ }
85
+
86
+ /** One active (resident) row of the prune valve's candidate set. */
87
+ export interface ActiveInjectedEntry {
88
+ slug: string;
89
+ bytes: number;
90
+ /** Epoch ms the card was (last) injected — the recency fallback for slugs
91
+ * with no selection rows (e.g. rows copied by a full fork). */
92
+ injectedAt: number;
93
+ }
94
+
95
+ /**
96
+ * Active (non-pruned) rows with byte and injection-time accounting — the
97
+ * prune valve's candidate set ({@link ActiveInjectedEntry}).
98
+ */
99
+ export function getActiveEntries(
100
+ conversationId: string,
101
+ ): ActiveInjectedEntry[] {
102
+ return getSqliteFrom(getDb())
103
+ .query(
104
+ /*sql*/ `
105
+ SELECT slug, bytes, injected_at AS injectedAt FROM memory_v3_ever_injected
106
+ WHERE conversation_id = ? AND pruned_at IS NULL
107
+ `,
108
+ )
109
+ .all(conversationId) as ActiveInjectedEntry[];
110
+ }
111
+
112
+ /**
113
+ * Slugs currently marked pruned — the card-section skip set shared by the
114
+ * live-history strip and the `loadFromDb` rehydration filter (see
115
+ * `prune.ts` / `daemon/conversation.ts`).
116
+ */
117
+ export function getPrunedSlugs(conversationId: string): Set<string> {
118
+ const rows = getSqliteFrom(getDb())
119
+ .query(
120
+ /*sql*/ `
121
+ SELECT slug FROM memory_v3_ever_injected
122
+ WHERE conversation_id = ? AND pruned_at IS NOT NULL
123
+ `,
124
+ )
125
+ .all(conversationId) as Array<{ slug: string }>;
126
+ return new Set(rows.map((row) => row.slug));
127
+ }
128
+
129
+ /**
130
+ * Upsert this turn's injected cards. Re-recording an existing slug clears
131
+ * `pruned_at` and refreshes `bytes`/`injected_at` — a pruned page that is
132
+ * re-selected re-injects as a fresh card.
133
+ */
134
+ export function recordInjected(
135
+ conversationId: string,
136
+ entries: Array<{ slug: string; bytes: number }>,
137
+ at: number = Date.now(),
138
+ ): void {
139
+ if (entries.length === 0) return;
140
+ const stmt = getSqliteFrom(getDb()).query(/*sql*/ `
141
+ INSERT INTO memory_v3_ever_injected (conversation_id, slug, injected_at, bytes, pruned_at)
142
+ VALUES (?, ?, ?, ?, NULL)
143
+ ON CONFLICT (conversation_id, slug) DO UPDATE SET
144
+ injected_at = excluded.injected_at,
145
+ bytes = excluded.bytes,
146
+ pruned_at = NULL
147
+ `);
148
+ for (const entry of entries) {
149
+ stmt.run(conversationId, entry.slug, at, entry.bytes);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Mark cards pruned from the live context. Rows are never deleted — the
155
+ * record stays auditable and the slugs stay eligible for re-injection.
156
+ */
157
+ export function markPruned(
158
+ conversationId: string,
159
+ slugs: string[],
160
+ at: number,
161
+ ): void {
162
+ if (slugs.length === 0) return;
163
+ const placeholders = slugs.map(() => "?").join(", ");
164
+ getSqliteFrom(getDb())
165
+ .query(
166
+ /*sql*/ `
167
+ UPDATE memory_v3_ever_injected SET pruned_at = ?
168
+ WHERE conversation_id = ? AND slug IN (${placeholders})
169
+ `,
170
+ )
171
+ .run(at, conversationId, ...slugs);
172
+ }
173
+
174
+ /**
175
+ * Delete the conversation's entire record. Compaction reset: the cached
176
+ * blocks are gone from history, so every slug must become re-injectable.
177
+ */
178
+ export function clearConversation(conversationId: string): void {
179
+ getSqliteFrom(getDb())
180
+ .query(
181
+ /*sql*/ `
182
+ DELETE FROM memory_v3_ever_injected WHERE conversation_id = ?
183
+ `,
184
+ )
185
+ .run(conversationId);
186
+ }
187
+
188
+ /** Total bytes of resident (non-pruned) cards — the prune-valve input. */
189
+ export function residentBytes(conversationId: string): number {
190
+ const row = getSqliteFrom(getDb())
191
+ .query(
192
+ /*sql*/ `
193
+ SELECT COALESCE(SUM(bytes), 0) AS total FROM memory_v3_ever_injected
194
+ WHERE conversation_id = ? AND pruned_at IS NULL
195
+ `,
196
+ )
197
+ .get(conversationId) as { total: number };
198
+ return row.total;
199
+ }
200
+
201
+ /**
202
+ * Copy the parent conversation's rows to a new conversation id, pruned state
203
+ * included. No-op if the parent has no rows. Mirrors `forkActivationState`:
204
+ * synchronous so it can run inside the bun:sqlite transaction that wraps
205
+ * `forkConversation()`, keeping the copy atomic with the message copies.
206
+ * Full-history forks only — truncated forks must use
207
+ * `seedEverInjectedFromSlugs` instead.
208
+ */
209
+ export function forkEverInjected(
210
+ db: DrizzleDb,
211
+ parentConversationId: string,
212
+ newConversationId: string,
213
+ ): void {
214
+ getSqliteFrom(db)
215
+ .query(
216
+ /*sql*/ `
217
+ INSERT INTO memory_v3_ever_injected (conversation_id, slug, injected_at, bytes, pruned_at)
218
+ SELECT ?, slug, injected_at, bytes, pruned_at FROM memory_v3_ever_injected
219
+ WHERE conversation_id = ?
220
+ ON CONFLICT (conversation_id, slug) DO UPDATE SET
221
+ injected_at = excluded.injected_at,
222
+ bytes = excluded.bytes,
223
+ pruned_at = excluded.pruned_at
224
+ `,
225
+ )
226
+ .run(newConversationId, parentConversationId);
227
+ }
228
+
229
+ /**
230
+ * Seed a truncated fork's record from the slugs whose card blocks the child
231
+ * actually inherited (scanned out of the copied messages'
232
+ * `MEMORY_V3_INJECTED_BLOCK_METADATA_KEY` metadata). Mirrors
233
+ * `seedForkActivationState`: a wholesale copy would over-claim, while seeding
234
+ * nothing would re-attach every inherited card as a duplicate.
235
+ *
236
+ * Rows are stamped `injected_at = at` and `bytes = 0` — the rendered size of
237
+ * inherited cards is unknown, so `residentBytes` accounting restarts from the
238
+ * fork's own injections.
239
+ *
240
+ * The parent's `pruned_at` tombstones are carried over: pruning leaves the
241
+ * persisted metadata block intact and relies on the tombstone to filter the
242
+ * section out at rehydration, so the metadata scan feeding this seed
243
+ * necessarily sees pruned cards' sections too. Seeding those slugs as active
244
+ * would resurrect parent-pruned cards in the child on its next load (and
245
+ * diverge from the full-fork copy path, which preserves tombstones). A
246
+ * tombstoned seed keeps the child's rehydrated view identical to the parent's
247
+ * live view at fork time; re-selection clears the tombstone and re-injects,
248
+ * same as in the parent.
249
+ *
250
+ * No-op when the child inherited no card blocks. Synchronous so it can run
251
+ * inside the `forkConversation()` transaction.
252
+ */
253
+ export function seedEverInjectedFromSlugs(
254
+ db: DrizzleDb,
255
+ parentConversationId: string,
256
+ newConversationId: string,
257
+ slugs: string[],
258
+ at: number,
259
+ ): void {
260
+ if (slugs.length === 0) return;
261
+ const prunedRows = getSqliteFrom(db)
262
+ .query(
263
+ /*sql*/ `
264
+ SELECT slug, pruned_at AS prunedAt FROM memory_v3_ever_injected
265
+ WHERE conversation_id = ? AND pruned_at IS NOT NULL
266
+ `,
267
+ )
268
+ .all(parentConversationId) as Array<{ slug: string; prunedAt: number }>;
269
+ const parentPrunedAt = new Map(prunedRows.map((r) => [r.slug, r.prunedAt]));
270
+ const stmt = getSqliteFrom(db).query(/*sql*/ `
271
+ INSERT INTO memory_v3_ever_injected (conversation_id, slug, injected_at, bytes, pruned_at)
272
+ VALUES (?, ?, ?, 0, ?)
273
+ ON CONFLICT (conversation_id, slug) DO NOTHING
274
+ `);
275
+ for (const slug of slugs) {
276
+ stmt.run(newConversationId, slug, at, parentPrunedAt.get(slug) ?? null);
277
+ }
278
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Tests for the frecency hot-set lane (`hot-set.ts`) and its config schema.
3
+ *
4
+ * `computeHotSet` takes the db handle directly (no module mocks needed):
5
+ * each test seeds an in-memory SQLite db with `memory_v3_selections` rows
6
+ * (via the real migration) and asserts the decayed-frequency ranking.
7
+ */
8
+
9
+ import { Database } from "bun:sqlite";
10
+ import { beforeEach, describe, expect, test } from "bun:test";
11
+
12
+ import { drizzle } from "drizzle-orm/bun-sqlite";
13
+
14
+ import { MemoryV3ConfigSchema } from "../../../config/schemas/memory-v3.js";
15
+ import { migrateAddMemoryV3Selections } from "../../../memory/migrations/268-add-memory-v3-selections.js";
16
+ import * as schema from "../../../memory/schema.js";
17
+ import { computeHotSet, type HotSetOptions } from "./hot-set.js";
18
+
19
+ const HALF_LIFE_MS = 1000;
20
+ const NOW = 100_000;
21
+
22
+ let sqlite: Database;
23
+ let db: ReturnType<typeof drizzle<typeof schema>>;
24
+
25
+ beforeEach(() => {
26
+ sqlite = new Database(":memory:");
27
+ db = drizzle(sqlite, { schema });
28
+ migrateAddMemoryV3Selections(db);
29
+ });
30
+
31
+ let nextTurn = 0;
32
+ /** Insert one selection row per createdAt (distinct turns keep the PK unique). */
33
+ function seed(slug: string, createdAts: number[]): void {
34
+ const stmt = sqlite.query(
35
+ `INSERT INTO memory_v3_selections (conversation_id, turn, slug, source, pinned, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
36
+ );
37
+ for (const createdAt of createdAts) {
38
+ stmt.run("conv-1", nextTurn++, slug, "needle", 0, createdAt);
39
+ }
40
+ }
41
+
42
+ function hotSet(overrides: Partial<HotSetOptions> = {}) {
43
+ return computeHotSet(
44
+ { db },
45
+ {
46
+ k: 10,
47
+ halfLifeMs: HALF_LIFE_MS,
48
+ now: NOW,
49
+ excludeSlugs: new Set<string>(),
50
+ ...overrides,
51
+ },
52
+ );
53
+ }
54
+
55
+ describe("computeHotSet", () => {
56
+ test("empty table yields empty result", () => {
57
+ expect(hotSet()).toEqual([]);
58
+ });
59
+
60
+ test("newer selections outweigh older ones at the same count", () => {
61
+ seed("page-old", [NOW - 10 * HALF_LIFE_MS, NOW - 10 * HALF_LIFE_MS]);
62
+ seed("page-new", [NOW, NOW]);
63
+ const result = hotSet();
64
+ expect(result.map((e) => e.slug)).toEqual(["page-new", "page-old"]);
65
+ expect(result[0]!.score).toBeGreaterThan(result[1]!.score);
66
+ });
67
+
68
+ test("decay halves at exactly one half-life", () => {
69
+ seed("page-a", [NOW]);
70
+ seed("page-b", [NOW - HALF_LIFE_MS]);
71
+ const result = hotSet();
72
+ const bySlug = new Map(result.map((e) => [e.slug, e.score]));
73
+ expect(bySlug.get("page-a")).toBeCloseTo(1, 10);
74
+ expect(bySlug.get("page-b")).toBeCloseTo(0.5, 10);
75
+ });
76
+
77
+ test("scores sum across a slug's selections", () => {
78
+ seed("page-a", [NOW, NOW - HALF_LIFE_MS]);
79
+ expect(hotSet()[0]!.score).toBeCloseTo(1.5, 10);
80
+ });
81
+
82
+ test("k cut keeps only the top-k", () => {
83
+ seed("page-a", [NOW, NOW, NOW]);
84
+ seed("page-b", [NOW, NOW]);
85
+ seed("page-c", [NOW]);
86
+ expect(hotSet({ k: 2 }).map((e) => e.slug)).toEqual(["page-a", "page-b"]);
87
+ });
88
+
89
+ test("excludeSlugs filters before the k cut", () => {
90
+ seed("page-a", [NOW, NOW, NOW]);
91
+ seed("page-b", [NOW, NOW]);
92
+ seed("page-c", [NOW]);
93
+ const result = hotSet({ k: 2, excludeSlugs: new Set(["page-a"]) });
94
+ expect(result.map((e) => e.slug)).toEqual(["page-b", "page-c"]);
95
+ });
96
+
97
+ test("ties break deterministically by slug ascending", () => {
98
+ seed("page-b", [NOW]);
99
+ seed("page-a", [NOW]);
100
+ seed("page-c", [NOW]);
101
+ expect(hotSet().map((e) => e.slug)).toEqual(["page-a", "page-b", "page-c"]);
102
+ });
103
+
104
+ test("deterministic for fixed inputs", () => {
105
+ seed("page-a", [NOW, NOW - 3 * HALF_LIFE_MS]);
106
+ seed("page-b", [NOW - HALF_LIFE_MS]);
107
+ expect(hotSet()).toEqual(hotSet());
108
+ });
109
+
110
+ test("future-dated rows are clamped to weight 1", () => {
111
+ seed("page-a", [NOW + 50 * HALF_LIFE_MS]);
112
+ expect(hotSet()[0]!.score).toBeCloseTo(1, 10);
113
+ });
114
+ });
115
+
116
+ describe("MemoryV3ConfigSchema hotSet", () => {
117
+ test("defaults apply when omitted", () => {
118
+ const config = MemoryV3ConfigSchema.parse({});
119
+ expect(config.hotSet).toEqual({ k: 40, halfLifeDays: 14 });
120
+ });
121
+
122
+ test("explicit values parse", () => {
123
+ const config = MemoryV3ConfigSchema.parse({
124
+ hotSet: { k: 5, halfLifeDays: 7 },
125
+ });
126
+ expect(config.hotSet).toEqual({ k: 5, halfLifeDays: 7 });
127
+ });
128
+
129
+ test("rejects negative k", () => {
130
+ expect(() => MemoryV3ConfigSchema.parse({ hotSet: { k: -1 } })).toThrow();
131
+ });
132
+
133
+ test("rejects negative halfLifeDays", () => {
134
+ expect(() =>
135
+ MemoryV3ConfigSchema.parse({ hotSet: { halfLifeDays: -1 } }),
136
+ ).toThrow();
137
+ });
138
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Frecency hot-set lane.
3
+ *
4
+ * Scores every page slug in `memory_v3_selections` (all conversations) by its
5
+ * exponentially-decayed selection count and returns the top-K. The hot set is
6
+ * the lane that keeps *seasonally recurrent* pages in the candidate pool even
7
+ * when the current message gives the finder lanes nothing to match on; the
8
+ * curated core set (a separate lane) covers the associative-texture class
9
+ * frecency cannot discriminate.
10
+ *
11
+ * Each selection row contributes `2^(−age / halfLifeMs)` to its slug's score,
12
+ * so a selection exactly one half-life old counts half as much as one made
13
+ * now. Slugs in `excludeSlugs` (the core set) are dropped before the K cut —
14
+ * the hot lane never duplicates core.
15
+ */
16
+
17
+ import {
18
+ type DrizzleDb,
19
+ getSqliteFrom,
20
+ } from "../../../memory/db-connection.js";
21
+ import type { Slug } from "./types.js";
22
+
23
+ export interface HotSetDeps {
24
+ /** Handle to the database containing `memory_v3_selections`. */
25
+ db: DrizzleDb;
26
+ }
27
+
28
+ export interface HotSetOptions {
29
+ /** Maximum number of slugs returned. */
30
+ k: number;
31
+ /** Decay half-life in milliseconds: a selection this old scores 0.5. */
32
+ halfLifeMs: number;
33
+ /** Reference timestamp (ms epoch) ages are measured against. */
34
+ now: number;
35
+ /** Slugs excluded from the result (the core set — hot never duplicates core). */
36
+ excludeSlugs: Set<string>;
37
+ }
38
+
39
+ export interface HotSetEntry {
40
+ slug: Slug;
41
+ score: number;
42
+ }
43
+
44
+ interface SelectionAgeRow {
45
+ slug: string;
46
+ created_at: number;
47
+ }
48
+
49
+ /**
50
+ * Compute the top-`k` hot slugs by exponentially-decayed selection frequency.
51
+ *
52
+ * Deterministic for fixed inputs: ties break score desc, then slug asc. The
53
+ * decay sum runs in TS over `(slug, created_at)` pairs — the selections table
54
+ * is small enough that reading it directly beats pushing the math into SQL.
55
+ */
56
+ export function computeHotSet(
57
+ deps: HotSetDeps,
58
+ opts: HotSetOptions,
59
+ ): HotSetEntry[] {
60
+ const { k, halfLifeMs, now, excludeSlugs } = opts;
61
+ if (k <= 0) return [];
62
+
63
+ const rows = getSqliteFrom(deps.db)
64
+ .query(
65
+ /*sql*/ `
66
+ SELECT slug, created_at FROM memory_v3_selections
67
+ `,
68
+ )
69
+ .all() as SelectionAgeRow[];
70
+
71
+ const scores = new Map<Slug, number>();
72
+ for (const row of rows) {
73
+ if (excludeSlugs.has(row.slug)) continue;
74
+ // Clamp future-dated rows (clock skew) to "now" rather than letting them
75
+ // score above 1 per selection.
76
+ const age = Math.max(0, now - row.created_at);
77
+ const weight = 2 ** (-age / halfLifeMs);
78
+ scores.set(row.slug, (scores.get(row.slug) ?? 0) + weight);
79
+ }
80
+
81
+ return [...scores.entries()]
82
+ .map(([slug, score]) => ({ slug, score }))
83
+ .sort((a, b) => b.score - a.score || (a.slug < b.slug ? -1 : 1))
84
+ .slice(0, k);
85
+ }