@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,116 @@
1
+ import { injectedConceptHeader } from "../../../memory/v2/injected-block-slugs.js";
2
+ import {
3
+ FRONTMATTER_REGEX,
4
+ parseFrontmatterFields,
5
+ } from "../../../skills/frontmatter.js";
6
+ import type { Slug } from "./types.js";
7
+
8
+ /**
9
+ * Compact card renderer for memory-v3: a page's head section (the `# Title`
10
+ * line plus lead paragraphs, everything before the first `## ` heading) plus a
11
+ * one-line section TOC. Cards are the compact injection unit — they carry
12
+ * enough signal to act on (or `file_read` the full page) at a fraction of the
13
+ * full-page byte cost.
14
+ *
15
+ * The `# memory/concepts/<slug>.md` header (shared builder:
16
+ * `injectedConceptHeader` in `memory/v2/injected-block-slugs.ts`) matches the
17
+ * v2 memory-block page convention, so the existing `file_read` affordance
18
+ * instruction applies to cards unchanged.
19
+ *
20
+ * Pure text → text: no I/O, no LLM calls. Callers load the raw page text via
21
+ * the existing page-store helpers (see `page-content.ts`).
22
+ */
23
+
24
+ /** Max characters of a `links:` entry note carried into the `[linked: …]`
25
+ * line. Notes longer than this are truncated with an ellipsis. */
26
+ const LINK_NOTE_MAX_CHARS = 80;
27
+
28
+ /** Matches a `## ` section heading line; capture group is the heading text. */
29
+ const SECTION_HEADING_REGEX = /^## (.*)$/m;
30
+
31
+ /**
32
+ * Render one `links:` frontmatter entry (`"<target-slug> — <note>"`) for the
33
+ * `[linked: …]` TOC line, truncating the note at {@link LINK_NOTE_MAX_CHARS}.
34
+ * Entries with no ` — ` separator are bare target slugs and pass through.
35
+ */
36
+ function renderLinkEntry(entry: string): string {
37
+ const sep = entry.indexOf(" — ");
38
+ if (sep === -1) return entry.trim();
39
+ const target = entry.slice(0, sep).trim();
40
+ let note = entry.slice(sep + " — ".length).trim();
41
+ if (note.length > LINK_NOTE_MAX_CHARS) {
42
+ note = `${note.slice(0, LINK_NOTE_MAX_CHARS).trimEnd()}…`;
43
+ }
44
+ return note.length > 0 ? `${target} — ${note}` : target;
45
+ }
46
+
47
+ /**
48
+ * Build the one-line TOC for a card.
49
+ *
50
+ * Index pages (`kind: index` frontmatter) render their `links:` map instead of
51
+ * section names — index sections have generic names ("Pages", "See also");
52
+ * the link map is the real signal. All other pages (including an index with no
53
+ * usable `links:`) render their `## ` heading names. Returns `null` when there
54
+ * is nothing to list (the TOC line is omitted).
55
+ */
56
+ function renderTocLine(
57
+ body: string,
58
+ fields: Record<string, unknown>,
59
+ ): string | null {
60
+ if (fields.kind === "index" && Array.isArray(fields.links)) {
61
+ const entries = fields.links
62
+ .filter((entry): entry is string => typeof entry === "string")
63
+ .map(renderLinkEntry)
64
+ .filter((entry) => entry.length > 0);
65
+ if (entries.length > 0) return `[linked: ${entries.join(" · ")}]`;
66
+ }
67
+
68
+ const headings = [...body.matchAll(new RegExp(SECTION_HEADING_REGEX, "gm"))]
69
+ .map((match) => match[1]!.trim())
70
+ .filter((heading) => heading.length > 0);
71
+ if (headings.length === 0) return null;
72
+ return `[sections: ${headings.map((h) => `§${h}`).join(" · ")}]`;
73
+ }
74
+
75
+ /**
76
+ * Render a page's compact card: header marker, head section (uncapped — the
77
+ * corpus's rare long leads inject whole; a length cap is a deliberate
78
+ * non-feature), and one-line TOC.
79
+ *
80
+ * ```
81
+ * # memory/concepts/<slug>.md
82
+ * <head section verbatim>
83
+ *
84
+ * [sections: §… · §…]
85
+ * ```
86
+ *
87
+ * The TOC line is omitted when the page has no `## ` sections and no usable
88
+ * `links:`. Deterministic for a given (slug, rawPageText).
89
+ */
90
+ export function renderCard(slug: Slug, rawPageText: string): string {
91
+ const parsed = parseFrontmatterFields(rawPageText);
92
+ // `parseFrontmatterFields` returns null both for "no frontmatter block" and
93
+ // "block present but YAML failed to parse" — strip the block either way so a
94
+ // malformed page never leaks raw frontmatter into the card head.
95
+ const body = parsed
96
+ ? parsed.body
97
+ : rawPageText.replace(FRONTMATTER_REGEX, "");
98
+ const fields = parsed?.fields ?? {};
99
+
100
+ const firstHeading = SECTION_HEADING_REGEX.exec(body);
101
+ const head = (firstHeading ? body.slice(0, firstHeading.index) : body).trim();
102
+
103
+ let card = injectedConceptHeader(slug);
104
+ if (head.length > 0) card += `\n${head}`;
105
+
106
+ const toc = renderTocLine(body, fields);
107
+ if (toc !== null) card += `\n\n${toc}`;
108
+
109
+ return card;
110
+ }
111
+
112
+ /** UTF-8 byte length of a rendered card (prune-valve and footprint
113
+ * accounting both budget in bytes, not characters). */
114
+ export function cardBytes(card: string): number {
115
+ return Buffer.byteLength(card, "utf8");
116
+ }
@@ -0,0 +1,104 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+
6
+ import { loadCoreSet } from "./core-set.js";
7
+
8
+ let workspaceDir: string;
9
+
10
+ function writeCorePages(content: string): void {
11
+ mkdirSync(join(workspaceDir, "memory"), { recursive: true });
12
+ writeFileSync(join(workspaceDir, "memory", "core-pages.md"), content);
13
+ }
14
+
15
+ beforeEach(() => {
16
+ workspaceDir = mkdtempSync(join(tmpdir(), "core-set-test-"));
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(workspaceDir, { recursive: true, force: true });
21
+ });
22
+
23
+ describe("loadCoreSet", () => {
24
+ test("missing file yields []", () => {
25
+ expect(loadCoreSet(workspaceDir)).toEqual([]);
26
+ });
27
+
28
+ test("accepts both wikilink and bare-slug list forms", () => {
29
+ writeCorePages(["- [[page-a]]", "- page-b"].join("\n"));
30
+ expect(loadCoreSet(workspaceDir)).toEqual(["page-a", "page-b"]);
31
+ });
32
+
33
+ test("accepts inline annotations after the slug", () => {
34
+ writeCorePages(
35
+ [
36
+ "- [[page-a]] — register frame, never matches lexically",
37
+ "- [[page-b]] keep while the project is live",
38
+ "- page-c — calibration rules",
39
+ "- page-d – en-dash annotation",
40
+ "- page-e - hyphen annotation",
41
+ ].join("\n"),
42
+ );
43
+ expect(loadCoreSet(workspaceDir)).toEqual([
44
+ "page-a",
45
+ "page-b",
46
+ "page-c",
47
+ "page-d",
48
+ "page-e",
49
+ ]);
50
+ });
51
+
52
+ test("ignores headings, blank lines, and prose annotations", () => {
53
+ writeCorePages(
54
+ [
55
+ "# Core pages",
56
+ "",
57
+ "Curated during consolidation; keep this list short.",
58
+ "- [[page-a]]",
59
+ "",
60
+ "## Identity frames",
61
+ "These never match lexically but should always be candidates.",
62
+ "- page-b",
63
+ ].join("\n"),
64
+ );
65
+ expect(loadCoreSet(workspaceDir)).toEqual(["page-a", "page-b"]);
66
+ });
67
+
68
+ test("dedupes while preserving first-seen order", () => {
69
+ writeCorePages(
70
+ ["- page-b", "- [[page-a]]", "- [[page-b]]", "- page-a", "- page-c"].join(
71
+ "\n",
72
+ ),
73
+ );
74
+ expect(loadCoreSet(workspaceDir)).toEqual(["page-b", "page-a", "page-c"]);
75
+ });
76
+
77
+ test("skips entries outside the slug-safe charset", () => {
78
+ writeCorePages(
79
+ [
80
+ "- [[Page A]]",
81
+ "- page_b",
82
+ "- [[topics/page-a]]",
83
+ "- UPPER-CASE",
84
+ "- page-b",
85
+ "- [[]]",
86
+ ].join("\n"),
87
+ );
88
+ expect(loadCoreSet(workspaceDir)).toEqual(["topics/page-a", "page-b"]);
89
+ });
90
+
91
+ test("malformed list lines are skipped, not fatal", () => {
92
+ writeCorePages(
93
+ [
94
+ "-",
95
+ "-no-space",
96
+ "- [[unclosed",
97
+ "- two words here",
98
+ "- prose that mentions — a dash mid-sentence",
99
+ "- page-a",
100
+ ].join("\n"),
101
+ );
102
+ expect(loadCoreSet(workspaceDir)).toEqual(["page-a"]);
103
+ });
104
+ });
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { Slug } from "./types.js";
5
+
6
+ /**
7
+ * Workspace-relative path of the maintainer-curated core-set file. Edited
8
+ * during memory consolidation — the loader never creates or modifies it.
9
+ */
10
+ const CORE_PAGES_RELATIVE_PATH = join("memory", "core-pages.md");
11
+
12
+ /**
13
+ * A list line is `- <item>`; the item may be a bare slug or a `[[slug]]`
14
+ * wikilink, optionally followed by an inline annotation. After a wikilink the
15
+ * annotation is free-form (`- [[some-slug]] — why this matters`); after a
16
+ * bare slug it must be introduced by a dash (`- some-slug — why this
17
+ * matters`), so a multi-word prose line (`- two words here`) is still read as
18
+ * the maintainer's notes, not a slug. Anything else on the line (headings,
19
+ * prose) is ignored.
20
+ */
21
+ const LIST_LINE = /^-\s+(?:\[\[([^\]]+)\]\](?:\s+.*)?|(\S+)(?:\s+[—–-].*)?)$/;
22
+
23
+ /** Slug-safe charset; existence against the page store is checked at lane init. */
24
+ const SLUG_SHAPE = /^[a-z0-9-/]+$/;
25
+
26
+ /**
27
+ * Load the maintainer-curated core set from `<workspaceDir>/memory/core-pages.md`.
28
+ *
29
+ * The core lane answers the associative-texture gap (pages with no
30
+ * lexical/semantic match to the message), so its membership is curated, not
31
+ * computed. Parsing is tolerant: list lines in `- [[some-slug]]` or
32
+ * `- some-slug` form are accepted, with optional inline annotations (see
33
+ * {@link LIST_LINE}); headings, blank lines, and prose are skipped so the
34
+ * maintainer can annotate the file; malformed or non-slug-shaped entries are
35
+ * dropped, never fatal.
36
+ *
37
+ * Returns slugs deduped in first-seen file order — that order is the stable
38
+ * sort the selector uses for the core prefix. A missing file yields `[]`.
39
+ */
40
+ export function loadCoreSet(workspaceDir: string): Slug[] {
41
+ let raw: string;
42
+ try {
43
+ raw = readFileSync(join(workspaceDir, CORE_PAGES_RELATIVE_PATH), "utf8");
44
+ } catch {
45
+ return [];
46
+ }
47
+
48
+ const seen = new Set<Slug>();
49
+ const slugs: Slug[] = [];
50
+ for (const line of raw.split("\n")) {
51
+ const match = LIST_LINE.exec(line.trim());
52
+ if (!match) continue;
53
+ const slug = (match[1] ?? match[2] ?? "").trim();
54
+ if (!SLUG_SHAPE.test(slug) || seen.has(slug)) continue;
55
+ seen.add(slug);
56
+ slugs.push(slug);
57
+ }
58
+ return slugs;
59
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Tests for `ever-injected-store.ts` — memory-v3's per-conversation
3
+ * everInjected record:
4
+ * - record/get/active-set round-trip and re-record clearing `pruned_at`;
5
+ * - `markPruned` excluding rows from the active set and `residentBytes`;
6
+ * - `clearConversation` (compaction reset);
7
+ * - fork hooks: full-row copy (pruned state included) and truncated-fork
8
+ * seeding (`bytes = 0`, dedup-only);
9
+ * - migration idempotence (run twice).
10
+ *
11
+ * `mock.module` is process-global and leaks into sibling files in a directory
12
+ * run, so the db-connection stub DELEGATES to the real implementation unless
13
+ * this test is actively running (`storeMockActive`, toggled in
14
+ * beforeEach/afterAll). Mirrors `__tests__/selection-log-store.test.ts`.
15
+ */
16
+
17
+ import { Database } from "bun:sqlite";
18
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
19
+
20
+ import { drizzle } from "drizzle-orm/bun-sqlite";
21
+
22
+ import { migrateAddMemoryV3EverInjected } from "../../../memory/migrations/277-add-memory-v3-ever-injected.js";
23
+ import * as schema from "../../../memory/schema.js";
24
+
25
+ const realDb = { ...(await import("../../../memory/db-connection.js")) };
26
+
27
+ let storeMockActive = false;
28
+
29
+ let testSqlite: Database;
30
+ let testDb = makeDb();
31
+ function makeDb() {
32
+ testSqlite = new Database(":memory:");
33
+ const db = drizzle(testSqlite, { schema });
34
+ migrateAddMemoryV3EverInjected(db);
35
+ return db;
36
+ }
37
+
38
+ mock.module("../../../memory/db-connection.js", () => ({
39
+ ...realDb,
40
+ getDb: () => (storeMockActive ? testDb : realDb.getDb()),
41
+ getSqliteFrom: (db: unknown) =>
42
+ storeMockActive
43
+ ? testSqlite
44
+ : realDb.getSqliteFrom(db as Parameters<typeof realDb.getSqliteFrom>[0]),
45
+ }));
46
+
47
+ const {
48
+ clearConversation,
49
+ forkEverInjected,
50
+ getActiveSlugs,
51
+ getInjected,
52
+ getPrunedSlugs,
53
+ markPruned,
54
+ MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
55
+ recordInjected,
56
+ residentBytes,
57
+ seedEverInjectedFromSlugs,
58
+ } = await import("./ever-injected-store.js");
59
+
60
+ beforeEach(() => {
61
+ storeMockActive = true;
62
+ testDb = makeDb();
63
+ });
64
+
65
+ afterAll(() => {
66
+ storeMockActive = false;
67
+ });
68
+
69
+ describe("metadata key constant", () => {
70
+ test("exports the v3 injected-block metadata key", () => {
71
+ expect(MEMORY_V3_INJECTED_BLOCK_METADATA_KEY).toBe("memoryV3InjectedBlock");
72
+ });
73
+ });
74
+
75
+ describe("recordInjected / getInjected / getActiveSlugs", () => {
76
+ test("round-trips the recorded entries", () => {
77
+ recordInjected(
78
+ "conv-1",
79
+ [
80
+ { slug: "topics/page-a", bytes: 100 },
81
+ { slug: "topics/page-b", bytes: 250 },
82
+ ],
83
+ 1_000,
84
+ );
85
+
86
+ expect(getInjected("conv-1")).toEqual(
87
+ new Map([
88
+ ["topics/page-a", { bytes: 100, prunedAt: null }],
89
+ ["topics/page-b", { bytes: 250, prunedAt: null }],
90
+ ]),
91
+ );
92
+ expect(getActiveSlugs("conv-1")).toEqual(
93
+ new Set(["topics/page-a", "topics/page-b"]),
94
+ );
95
+ // Other conversations see nothing.
96
+ expect(getInjected("conv-other").size).toBe(0);
97
+ expect(getActiveSlugs("conv-other").size).toBe(0);
98
+ });
99
+
100
+ test("re-recording a pruned slug clears pruned_at and refreshes bytes", () => {
101
+ recordInjected("conv-1", [{ slug: "topics/page-a", bytes: 100 }], 1_000);
102
+ markPruned("conv-1", ["topics/page-a"], 2_000);
103
+ expect(getInjected("conv-1").get("topics/page-a")).toEqual({
104
+ bytes: 100,
105
+ prunedAt: 2_000,
106
+ });
107
+
108
+ recordInjected("conv-1", [{ slug: "topics/page-a", bytes: 140 }], 3_000);
109
+
110
+ expect(getInjected("conv-1").get("topics/page-a")).toEqual({
111
+ bytes: 140,
112
+ prunedAt: null,
113
+ });
114
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["topics/page-a"]));
115
+ const row = testSqlite
116
+ .query(
117
+ "SELECT injected_at FROM memory_v3_ever_injected WHERE conversation_id = ? AND slug = ?",
118
+ )
119
+ .get("conv-1", "topics/page-a") as { injected_at: number };
120
+ expect(row.injected_at).toBe(3_000);
121
+ });
122
+
123
+ test("empty entries are a no-op", () => {
124
+ recordInjected("conv-1", []);
125
+ expect(getInjected("conv-1").size).toBe(0);
126
+ });
127
+ });
128
+
129
+ describe("markPruned / residentBytes", () => {
130
+ test("pruned rows leave the active set and resident bytes but stay on record", () => {
131
+ recordInjected(
132
+ "conv-1",
133
+ [
134
+ { slug: "topics/page-a", bytes: 100 },
135
+ { slug: "topics/page-b", bytes: 250 },
136
+ { slug: "topics/page-c", bytes: 50 },
137
+ ],
138
+ 1_000,
139
+ );
140
+ expect(residentBytes("conv-1")).toBe(400);
141
+
142
+ markPruned("conv-1", ["topics/page-a", "topics/page-c"], 2_000);
143
+
144
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["topics/page-b"]));
145
+ expect(residentBytes("conv-1")).toBe(250);
146
+ // Rows are never deleted — the record stays auditable.
147
+ expect([...getInjected("conv-1").keys()].sort()).toEqual([
148
+ "topics/page-a",
149
+ "topics/page-b",
150
+ "topics/page-c",
151
+ ]);
152
+ });
153
+
154
+ test("empty slug list is a no-op and residentBytes is 0 for unknown conversations", () => {
155
+ recordInjected("conv-1", [{ slug: "topics/page-a", bytes: 100 }], 1_000);
156
+ markPruned("conv-1", [], 2_000);
157
+ expect(residentBytes("conv-1")).toBe(100);
158
+ expect(residentBytes("conv-unknown")).toBe(0);
159
+ });
160
+ });
161
+
162
+ describe("clearConversation", () => {
163
+ test("deletes all rows for the conversation only", () => {
164
+ recordInjected("conv-1", [{ slug: "topics/page-a", bytes: 100 }], 1_000);
165
+ recordInjected("conv-2", [{ slug: "topics/page-b", bytes: 200 }], 1_000);
166
+
167
+ clearConversation("conv-1");
168
+
169
+ expect(getInjected("conv-1").size).toBe(0);
170
+ expect(getInjected("conv-2").size).toBe(1);
171
+ });
172
+ });
173
+
174
+ describe("forkEverInjected", () => {
175
+ test("copies the parent's full record, pruned state included", () => {
176
+ recordInjected(
177
+ "conv-parent",
178
+ [
179
+ { slug: "topics/page-a", bytes: 100 },
180
+ { slug: "topics/page-b", bytes: 250 },
181
+ ],
182
+ 1_000,
183
+ );
184
+ markPruned("conv-parent", ["topics/page-b"], 2_000);
185
+
186
+ forkEverInjected(testDb, "conv-parent", "conv-child");
187
+
188
+ expect(getInjected("conv-child")).toEqual(
189
+ new Map([
190
+ ["topics/page-a", { bytes: 100, prunedAt: null }],
191
+ ["topics/page-b", { bytes: 250, prunedAt: 2_000 }],
192
+ ]),
193
+ );
194
+ expect(residentBytes("conv-child")).toBe(100);
195
+ // Parent record is untouched.
196
+ expect(getInjected("conv-parent").size).toBe(2);
197
+ });
198
+
199
+ test("is a no-op when the parent has no rows", () => {
200
+ forkEverInjected(testDb, "conv-empty", "conv-child");
201
+ expect(getInjected("conv-child").size).toBe(0);
202
+ });
203
+ });
204
+
205
+ describe("seedEverInjectedFromSlugs", () => {
206
+ test("seeds dedup-only rows with bytes = 0 stamped at the given time", () => {
207
+ seedEverInjectedFromSlugs(
208
+ testDb,
209
+ "conv-parent",
210
+ "conv-child",
211
+ ["topics/page-a", "topics/page-b"],
212
+ 5_000,
213
+ );
214
+
215
+ expect(getInjected("conv-child")).toEqual(
216
+ new Map([
217
+ ["topics/page-a", { bytes: 0, prunedAt: null }],
218
+ ["topics/page-b", { bytes: 0, prunedAt: null }],
219
+ ]),
220
+ );
221
+ expect(getActiveSlugs("conv-child")).toEqual(
222
+ new Set(["topics/page-a", "topics/page-b"]),
223
+ );
224
+ // Inherited cards carry no byte accounting — resident accounting
225
+ // restarts from the fork's own injections.
226
+ expect(residentBytes("conv-child")).toBe(0);
227
+ const row = testSqlite
228
+ .query(
229
+ "SELECT injected_at FROM memory_v3_ever_injected WHERE conversation_id = ? AND slug = ?",
230
+ )
231
+ .get("conv-child", "topics/page-a") as { injected_at: number };
232
+ expect(row.injected_at).toBe(5_000);
233
+ });
234
+
235
+ test("carries the parent's pruned_at tombstones for inherited slugs", () => {
236
+ // Parent injected both pages, then pruned page-a: the metadata block the
237
+ // child inherits still contains page-a's section, so the fork scan seeds
238
+ // both slugs — but page-a must arrive tombstoned, not active, or the
239
+ // child's rehydration would resurrect a card the parent's live view lost.
240
+ recordInjected(
241
+ "conv-parent",
242
+ [
243
+ { slug: "topics/page-a", bytes: 100 },
244
+ { slug: "topics/page-b", bytes: 200 },
245
+ ],
246
+ 1_000,
247
+ );
248
+ markPruned("conv-parent", ["topics/page-a"], 2_000);
249
+
250
+ seedEverInjectedFromSlugs(
251
+ testDb,
252
+ "conv-parent",
253
+ "conv-child",
254
+ ["topics/page-a", "topics/page-b"],
255
+ 5_000,
256
+ );
257
+
258
+ expect(getInjected("conv-child")).toEqual(
259
+ new Map([
260
+ ["topics/page-a", { bytes: 0, prunedAt: 2_000 }],
261
+ ["topics/page-b", { bytes: 0, prunedAt: null }],
262
+ ]),
263
+ );
264
+ expect(getActiveSlugs("conv-child")).toEqual(new Set(["topics/page-b"]));
265
+ expect(getPrunedSlugs("conv-child")).toEqual(new Set(["topics/page-a"]));
266
+
267
+ // Re-selection clears the inherited tombstone, same as in the parent.
268
+ recordInjected("conv-child", [{ slug: "topics/page-a", bytes: 50 }], 6_000);
269
+ expect(getActiveSlugs("conv-child")).toEqual(
270
+ new Set(["topics/page-a", "topics/page-b"]),
271
+ );
272
+ });
273
+
274
+ test("is a no-op for an empty slug list and never overwrites existing rows", () => {
275
+ seedEverInjectedFromSlugs(testDb, "conv-parent", "conv-child", [], 5_000);
276
+ expect(getInjected("conv-child").size).toBe(0);
277
+
278
+ recordInjected(
279
+ "conv-child",
280
+ [{ slug: "topics/page-a", bytes: 100 }],
281
+ 1_000,
282
+ );
283
+ seedEverInjectedFromSlugs(
284
+ testDb,
285
+ "conv-parent",
286
+ "conv-child",
287
+ ["topics/page-a"],
288
+ 5_000,
289
+ );
290
+ expect(getInjected("conv-child").get("topics/page-a")).toEqual({
291
+ bytes: 100,
292
+ prunedAt: null,
293
+ });
294
+ });
295
+ });
296
+
297
+ describe("migration", () => {
298
+ test("is idempotent — running twice leaves a usable table", () => {
299
+ // makeDb() already ran the migration once; run it again.
300
+ migrateAddMemoryV3EverInjected(testDb);
301
+
302
+ recordInjected("conv-1", [{ slug: "topics/page-a", bytes: 100 }], 1_000);
303
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["topics/page-a"]));
304
+ });
305
+ });