@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,683 @@
1
+ /**
2
+ * Tests for the memory-v3 injection layer (`injector.ts`): frozen net-new
3
+ * cards + ephemeral spotlight.
4
+ *
5
+ * - net-new dedup: a turn re-selecting already-injected pages renders zero
6
+ * new cards (empty-text block — still produced, so v2 suppression holds);
7
+ * - commit deferral: the everInjected-store write happens in the block's
8
+ * attachment-commit callback (invoked by assembly on user-tail turns),
9
+ * never in `produce()` itself;
10
+ * - trust gate: an untrusted remote actor's turn produces nothing and
11
+ * records nothing (the v2 personal-memory gate);
12
+ * - fork dedup: a conversation whose everInjected record was seeded from
13
+ * inherited blocks does not re-render those slugs;
14
+ * - prune round-trip: a pruned slug that is re-selected re-injects;
15
+ * - shadow mode: attaches nothing and records nothing, but logs the
16
+ * would-inject set;
17
+ * - spotlight: current-window entries only, re-rendered (never accumulated),
18
+ * bounded by `n × (windowTurns + 1)`, live-only, absent from the
19
+ * persistent card layer.
20
+ *
21
+ * Orchestration is stubbed at the `observeTurn` seam (the injectors' shared
22
+ * input); the everInjected store runs REAL against an in-memory SQLite DB so
23
+ * the dedup contract is exercised end-to-end. `mock.module` is process-global,
24
+ * so every stub delegates to the real implementation unless this file's tests
25
+ * are running (`injectionMockActive`) — mirrors the sibling test files.
26
+ */
27
+
28
+ import { Database } from "bun:sqlite";
29
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
30
+
31
+ import { drizzle } from "drizzle-orm/bun-sqlite";
32
+
33
+ import { unwrapMemoryBlock } from "../../../../memory/memory-marker.js";
34
+ import { migrateAddMemoryV3Selections } from "../../../../memory/migrations/268-add-memory-v3-selections.js";
35
+ import { migrateAddMemoryV3EverInjected } from "../../../../memory/migrations/277-add-memory-v3-ever-injected.js";
36
+ import * as schema from "../../../../memory/schema.js";
37
+ import type { InjectionBlock } from "../../../types.js";
38
+ import type { OrchestrateResult } from "../orchestrate.js";
39
+ import {
40
+ MEMORY_V3_COMMIT_META_KEY,
41
+ type Section,
42
+ type Slug,
43
+ } from "../types.js";
44
+
45
+ const realConfigLoader = { ...(await import("../../../../config/loader.js")) };
46
+ const realFlags = {
47
+ ...(await import("../../../../config/assistant-feature-flags.js")),
48
+ };
49
+ const realDbConnection = {
50
+ ...(await import("../../../../memory/db-connection.js")),
51
+ };
52
+ const realPageContent = { ...(await import("../page-content.js")) };
53
+ const realShadowPlugin = { ...(await import("../shadow-plugin.js")) };
54
+
55
+ let injectionMockActive = false;
56
+
57
+ // ─── mutable test state ──────────────────────────────────────────────────────
58
+
59
+ let liveEnabled = false;
60
+ let shadowEnabled = false;
61
+ let memoryEnabled = true;
62
+ let spotlightConfig = { n: 6, windowTurns: 2 };
63
+ /** `null` disables the prune valve (the default for tests not exercising it —
64
+ * `runPruneValve` bails when the config block is absent). */
65
+ let pruneConfig: {
66
+ maxResidentBytes: number;
67
+ targetResidentBytes: number;
68
+ } | null = null;
69
+ /** Canned orchestrate result per turnIndex; `null` simulates a failed turn. */
70
+ let turnResults = new Map<number, OrchestrateResult | null>();
71
+ const observeTurnSpy = mock(
72
+ async (
73
+ _conversationId: string,
74
+ turnIndex: number,
75
+ ): Promise<OrchestrateResult | null> => turnResults.get(turnIndex) ?? null,
76
+ );
77
+
78
+ const logCalls: Array<{ data: unknown; msg: string }> = [];
79
+ mock.module("../../../../util/logger.js", () => ({
80
+ getLogger: () => ({
81
+ info: (data: unknown, msg: string) => logCalls.push({ data, msg }),
82
+ warn: (data: unknown, msg: string) => logCalls.push({ data, msg }),
83
+ error: () => {},
84
+ debug: () => {},
85
+ }),
86
+ }));
87
+
88
+ let testSqlite: Database;
89
+ let testDb = makeDb();
90
+ function makeDb() {
91
+ testSqlite = new Database(":memory:");
92
+ const db = drizzle(testSqlite, { schema });
93
+ migrateAddMemoryV3EverInjected(db);
94
+ // The prune valve's recency ranking reads `memory_v3_selections`.
95
+ migrateAddMemoryV3Selections(db);
96
+ // The prune valve plans only against slugs whose card sections are
97
+ // locatable in persisted `memoryV3InjectedBlock` rows
98
+ // (`collectPersistedV3Cards`) — minimal `messages` shape it reads.
99
+ testSqlite.run(/*sql*/ `
100
+ CREATE TABLE IF NOT EXISTS messages (
101
+ id TEXT PRIMARY KEY,
102
+ conversation_id TEXT NOT NULL,
103
+ role TEXT NOT NULL,
104
+ content TEXT NOT NULL,
105
+ metadata TEXT,
106
+ created_at INTEGER NOT NULL
107
+ )
108
+ `);
109
+ return db;
110
+ }
111
+
112
+ mock.module("../../../../memory/db-connection.js", () => ({
113
+ ...realDbConnection,
114
+ getDb: () => (injectionMockActive ? testDb : realDbConnection.getDb()),
115
+ getSqliteFrom: (db: unknown) =>
116
+ injectionMockActive
117
+ ? testSqlite
118
+ : realDbConnection.getSqliteFrom(
119
+ db as Parameters<typeof realDbConnection.getSqliteFrom>[0],
120
+ ),
121
+ }));
122
+
123
+ mock.module("../../../../config/loader.js", () => ({
124
+ ...realConfigLoader,
125
+ getConfig: () =>
126
+ injectionMockActive
127
+ ? {
128
+ memory: {
129
+ enabled: memoryEnabled,
130
+ v3: {
131
+ spotlight: spotlightConfig,
132
+ prune: pruneConfig ?? undefined,
133
+ },
134
+ },
135
+ }
136
+ : realConfigLoader.getConfig(),
137
+ }));
138
+
139
+ // The prune valve resolves the live conversation through the daemon registry
140
+ // (dynamically imported). Stub it so the deferred valve never drags the heavy
141
+ // daemon module graph into this test process; `undefined` = "conversation not
142
+ // live" (the valve skips the live strip).
143
+ mock.module("../../../../daemon/conversation-registry.js", () => ({
144
+ findConversationOrSubagent: () => undefined,
145
+ }));
146
+
147
+ mock.module("../../../../config/assistant-feature-flags.js", () => ({
148
+ ...realFlags,
149
+ isAssistantFeatureFlagEnabled: (
150
+ key: string,
151
+ config: Parameters<typeof realFlags.isAssistantFeatureFlagEnabled>[1],
152
+ ) => {
153
+ if (!injectionMockActive) {
154
+ return realFlags.isAssistantFeatureFlagEnabled(
155
+ key as Parameters<typeof realFlags.isAssistantFeatureFlagEnabled>[0],
156
+ config,
157
+ );
158
+ }
159
+ return key === "memory-v3-live"
160
+ ? liveEnabled
161
+ : key === "memory-v3-shadow"
162
+ ? shadowEnabled
163
+ : false;
164
+ },
165
+ }));
166
+
167
+ mock.module("../page-content.js", () => ({
168
+ ...realPageContent,
169
+ renderV3CardContent: async (slug: Slug) =>
170
+ injectionMockActive
171
+ ? slug === "missing-page"
172
+ ? ""
173
+ : `# memory/concepts/${slug}.md\ncard body for ${slug}`
174
+ : realPageContent.renderV3CardContent(slug),
175
+ }));
176
+
177
+ mock.module("../shadow-plugin.js", () => ({
178
+ ...realShadowPlugin,
179
+ observeTurn: (conversationId: string, turnIndex: number) =>
180
+ injectionMockActive
181
+ ? observeTurnSpy(conversationId, turnIndex)
182
+ : realShadowPlugin.observeTurn(conversationId, turnIndex),
183
+ }));
184
+
185
+ const {
186
+ memoryV3Injector,
187
+ memoryV3SpotlightInjector,
188
+ resetMemoryV3InjectorStateForTests,
189
+ } = await import("../injector.js");
190
+ const {
191
+ getActiveSlugs,
192
+ getInjected,
193
+ getPrunedSlugs,
194
+ markPruned,
195
+ recordInjected,
196
+ } = await import("../ever-injected-store.js");
197
+ const { V3_CARDS_INJECTION_HEADER } = await import("../render-injection.js");
198
+ const { flushPruneValveForTests } = await import("../prune.js");
199
+
200
+ // ─── helpers ────────────────────────────────────────────────────────────────
201
+
202
+ function section(slug: Slug, title: string, text: string): Section {
203
+ return { article: slug, title, text, ordinal: 1 };
204
+ }
205
+
206
+ /** An orchestrate result selecting `slugs`, with optional finder-matched
207
+ * sections (slug → section) feeding the spotlight. */
208
+ function result(
209
+ slugs: Slug[],
210
+ matched: Array<[Slug, Section]> = [],
211
+ ): OrchestrateResult {
212
+ return {
213
+ selections: slugs.map((slug) => ({ slug, pinned: false })),
214
+ matchedSections: new Map(matched),
215
+ lanes: {
216
+ core: [],
217
+ hot: [],
218
+ finder: matched.map(([slug]) => ({
219
+ slug,
220
+ descriptor: "",
221
+ lane: "needle" as const,
222
+ })),
223
+ },
224
+ };
225
+ }
226
+
227
+ const GUARDIAN_TRUST = {
228
+ sourceChannel: "vellum",
229
+ trustClass: "guardian",
230
+ } as const;
231
+
232
+ /** Invoke the block's attachment-commit callback — simulating runtime
233
+ * assembly's user-tail commit point, where the everInjected-store write
234
+ * (and the prune-valve schedule) now happens. */
235
+ function commitCardsBlock(block: InjectionBlock | null): void {
236
+ const commit = block?.meta?.[MEMORY_V3_COMMIT_META_KEY];
237
+ if (typeof commit === "function") (commit as () => void)();
238
+ }
239
+
240
+ /** Produce the cards block WITHOUT committing — what assembly observes on a
241
+ * turn whose tail is not a user message (the block never attaches). */
242
+ function produceCardsWithoutCommit(
243
+ conversationId: string,
244
+ turnIndex: number,
245
+ trust: { sourceChannel: string; trustClass: string } = GUARDIAN_TRUST,
246
+ ) {
247
+ return memoryV3Injector.produce({
248
+ requestId: "req-1",
249
+ conversationId,
250
+ turnIndex,
251
+ trust: trust as never,
252
+ });
253
+ }
254
+
255
+ /** Produce the cards block and commit it (the normal user-tail turn). */
256
+ async function produceCards(conversationId: string, turnIndex: number) {
257
+ const block = await produceCardsWithoutCommit(conversationId, turnIndex);
258
+ commitCardsBlock(block);
259
+ return block;
260
+ }
261
+
262
+ /** Persist a produced card block to message metadata, as the conversation
263
+ * assembly does in production (unwrapped, under `memoryV3InjectedBlock`) —
264
+ * the prune valve's plan only counts slugs locatable in persisted rows. */
265
+ let persistedMessageSeq = 0;
266
+ function persistCardBlockMetadata(
267
+ conversationId: string,
268
+ blockText: string,
269
+ ): void {
270
+ testSqlite
271
+ .query(
272
+ /*sql*/ `
273
+ INSERT INTO messages (id, conversation_id, role, content, metadata, created_at)
274
+ VALUES (?, ?, 'user', '[]', ?, 0)
275
+ `,
276
+ )
277
+ .run(
278
+ `m-${persistedMessageSeq++}`,
279
+ conversationId,
280
+ JSON.stringify({ memoryV3InjectedBlock: unwrapMemoryBlock(blockText) }),
281
+ );
282
+ }
283
+
284
+ function produceSpotlight(
285
+ conversationId: string,
286
+ turnIndex: number,
287
+ trust: { sourceChannel: string; trustClass: string } = GUARDIAN_TRUST,
288
+ ) {
289
+ return memoryV3SpotlightInjector.produce({
290
+ requestId: "req-1",
291
+ conversationId,
292
+ turnIndex,
293
+ trust: trust as never,
294
+ });
295
+ }
296
+
297
+ beforeEach(async () => {
298
+ // Drain any prune-valve work the previous test's live injection deferred,
299
+ // so it lands against that test's DB instead of bleeding into this one.
300
+ await flushPruneValveForTests();
301
+ injectionMockActive = true;
302
+ liveEnabled = false;
303
+ shadowEnabled = false;
304
+ memoryEnabled = true;
305
+ spotlightConfig = { n: 6, windowTurns: 2 };
306
+ pruneConfig = null;
307
+ turnResults = new Map();
308
+ observeTurnSpy.mockClear();
309
+ logCalls.length = 0;
310
+ testDb = makeDb();
311
+ resetMemoryV3InjectorStateForTests();
312
+ });
313
+
314
+ afterAll(async () => {
315
+ // Deferred valve work must finish while the mocks are still active.
316
+ await flushPruneValveForTests();
317
+ injectionMockActive = false;
318
+ });
319
+
320
+ // ─── frozen net-new cards ───────────────────────────────────────────────────
321
+
322
+ describe("memoryV3Injector — frozen net-new cards", () => {
323
+ test("global memory disabled → both injectors produce null without orchestration", async () => {
324
+ liveEnabled = true;
325
+ memoryEnabled = false;
326
+ turnResults.set(0, result(["page-a"]));
327
+
328
+ expect(await produceCardsWithoutCommit("conv-1", 0)).toBeNull();
329
+ expect(await produceSpotlight("conv-1", 0)).toBeNull();
330
+ expect(observeTurnSpy).not.toHaveBeenCalled();
331
+ expect(getActiveSlugs("conv-1")).toEqual(new Set());
332
+ });
333
+
334
+ test("turn 1 renders cards; turn 2 re-selecting the same pages renders ZERO new cards", async () => {
335
+ liveEnabled = true;
336
+ turnResults.set(0, result(["page-a", "page-b"]));
337
+ turnResults.set(1, result(["page-a", "page-b"]));
338
+
339
+ const t1 = await produceCards("conv-1", 0);
340
+ expect(t1).not.toBeNull();
341
+ expect(t1!.placement).toBe("after-memory-prefix");
342
+ expect(t1!.text.startsWith("<memory>\n")).toBe(true);
343
+ expect(t1!.text).toContain(V3_CARDS_INJECTION_HEADER);
344
+ expect(t1!.text).toContain("# memory/concepts/page-a.md");
345
+ expect(t1!.text).toContain("# memory/concepts/page-b.md");
346
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a", "page-b"]));
347
+ // Recorded bytes match the rendered card sizes (non-zero).
348
+ for (const entry of getInjected("conv-1").values()) {
349
+ expect(entry.bytes).toBeGreaterThan(0);
350
+ expect(entry.prunedAt).toBeNull();
351
+ }
352
+
353
+ // All-repeat turn: the block is still PRODUCED (its presence keys v2
354
+ // suppression) but carries no text — no new persistent bytes.
355
+ const t2 = await produceCards("conv-1", 1);
356
+ expect(t2).not.toBeNull();
357
+ expect(t2!.text).toBe("");
358
+ });
359
+
360
+ test("a partially-new turn renders only the net-new cards", async () => {
361
+ liveEnabled = true;
362
+ turnResults.set(0, result(["page-a"]));
363
+ turnResults.set(1, result(["page-a", "page-c"]));
364
+
365
+ await produceCards("conv-1", 0);
366
+ const t2 = await produceCards("conv-1", 1);
367
+ expect(t2!.text).toContain("# memory/concepts/page-c.md");
368
+ expect(t2!.text).not.toContain("# memory/concepts/page-a.md");
369
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a", "page-c"]));
370
+ });
371
+
372
+ test("fork-seeded dedup record suppresses re-rendering inherited slugs", async () => {
373
+ liveEnabled = true;
374
+ // PR 4's fork hooks seed the child's record from inherited metadata
375
+ // blocks; from the injector's perspective that is just pre-existing rows.
376
+ recordInjected("conv-fork", [{ slug: "page-a", bytes: 0 }]);
377
+ turnResults.set(0, result(["page-a", "page-b"]));
378
+
379
+ const block = await produceCards("conv-fork", 0);
380
+ expect(block!.text).toContain("# memory/concepts/page-b.md");
381
+ expect(block!.text).not.toContain("# memory/concepts/page-a.md");
382
+ });
383
+
384
+ test("a pruned slug that is re-selected re-injects as a fresh card", async () => {
385
+ liveEnabled = true;
386
+ turnResults.set(0, result(["page-a"]));
387
+ turnResults.set(1, result(["page-a"]));
388
+
389
+ await produceCards("conv-1", 0);
390
+ markPruned("conv-1", ["page-a"], Date.now());
391
+ expect(getActiveSlugs("conv-1")).toEqual(new Set());
392
+
393
+ const t2 = await produceCards("conv-1", 1);
394
+ expect(t2!.text).toContain("# memory/concepts/page-a.md");
395
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
396
+ });
397
+
398
+ test("end-of-turn prune valve: fires deferred after live injection, exempting core/hot lanes", async () => {
399
+ liveEnabled = true;
400
+ // Each stubbed card is ~47 bytes; cap so the second turn tips over and
401
+ // one prune reaches the target.
402
+ pruneConfig = { maxResidentBytes: 60, targetResidentBytes: 50 };
403
+ const t0 = result(["page-a"]);
404
+ t0.lanes.core = ["page-a"]; // page-a is a core-lane member this turn…
405
+ turnResults.set(0, t0);
406
+ turnResults.set(1, result(["page-b"])); // …but not on turn 1's lanes.
407
+
408
+ const b0 = await produceCards("conv-1", 0);
409
+ persistCardBlockMetadata("conv-1", b0!.text);
410
+ await flushPruneValveForTests();
411
+ // Turn 0 is within the cap — nothing pruned.
412
+ expect(getPrunedSlugs("conv-1").size).toBe(0);
413
+
414
+ const b1 = await produceCards("conv-1", 1);
415
+ persistCardBlockMetadata("conv-1", b1!.text);
416
+ // The valve is DEFERRED: nothing pruned synchronously at produce time.
417
+ expect(getPrunedSlugs("conv-1").size).toBe(0);
418
+ await flushPruneValveForTests();
419
+ // Over the cap, page-a (oldest, and no longer lane-exempt) is pruned.
420
+ expect(getPrunedSlugs("conv-1")).toEqual(new Set(["page-a"]));
421
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-b"]));
422
+ });
423
+
424
+ test("slugs whose card renders empty are neither attached nor recorded", async () => {
425
+ liveEnabled = true;
426
+ turnResults.set(0, result(["missing-page", "page-a"]));
427
+
428
+ const block = await produceCards("conv-1", 0);
429
+ expect(block!.text).toContain("# memory/concepts/page-a.md");
430
+ expect(block!.text).not.toContain("missing-page");
431
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
432
+ });
433
+
434
+ test("EVERY net-new card rendering empty → null (v2 fallback), not an empty block", async () => {
435
+ liveEnabled = true;
436
+ turnResults.set(0, result(["missing-page"]));
437
+
438
+ // An empty-text block would suppress v2 with nothing to show — a
439
+ // memory-less turn. Distinct from the all-repeat case (empty netNew),
440
+ // where the empty block correctly keeps v2 suppressed.
441
+ expect(await produceCards("conv-1", 0)).toBeNull();
442
+ expect(getActiveSlugs("conv-1")).toEqual(new Set());
443
+ });
444
+
445
+ test("produce() defers the store write to the commit callback — a never-attached block records nothing", async () => {
446
+ liveEnabled = true;
447
+ turnResults.set(0, result(["page-a"]));
448
+
449
+ // A turn whose tail is not a user message: assembly never invokes the
450
+ // commit, so the store must not claim the cards (which would suppress
451
+ // them until compaction despite never reaching history).
452
+ const block = await produceCardsWithoutCommit("conv-1", 0);
453
+ expect(block).not.toBeNull();
454
+ expect(getActiveSlugs("conv-1")).toEqual(new Set());
455
+
456
+ // Assembly's user-tail commit point records them.
457
+ commitCardsBlock(block);
458
+ expect(getActiveSlugs("conv-1")).toEqual(new Set(["page-a"]));
459
+ });
460
+
461
+ test("untrusted remote actor → both injectors produce null, no orchestration, nothing recorded", async () => {
462
+ liveEnabled = true;
463
+ turnResults.set(0, result(["page-a"]));
464
+ const untrusted = { sourceChannel: "telegram", trustClass: "unknown" };
465
+
466
+ expect(await produceCardsWithoutCommit("conv-1", 0, untrusted)).toBeNull();
467
+ expect(await produceSpotlight("conv-1", 0, untrusted)).toBeNull();
468
+ // The gate runs before orchestration: nothing selected, nothing recorded.
469
+ expect(observeTurnSpy).not.toHaveBeenCalled();
470
+ expect(getActiveSlugs("conv-1")).toEqual(new Set());
471
+ });
472
+
473
+ test("capability cards (skills / CLI commands) record ZERO bytes", async () => {
474
+ liveEnabled = true;
475
+ turnResults.set(0, result(["skills/test-skill", "page-a"]));
476
+
477
+ const block = await produceCards("conv-1", 0);
478
+ // Both cards attach…
479
+ expect(block!.text).toContain("skills/test-skill");
480
+ expect(block!.text).toContain("# memory/concepts/page-a.md");
481
+ // …but the capability card's bytes are recorded as 0: its `# Skill:`
482
+ // header is invisible to the prune valve's `# memory/concepts/<slug>.md`
483
+ // section grammar, so non-zero bytes could never be freed and would
484
+ // loop-fire the valve.
485
+ const injected = getInjected("conv-1");
486
+ expect(injected.get("skills/test-skill")!.bytes).toBe(0);
487
+ expect(injected.get("page-a")!.bytes).toBeGreaterThan(0);
488
+ });
489
+
490
+ test("per-conversation memo LRU: a key refresh evicts nothing; new-key eviction prefers stale entries", async () => {
491
+ liveEnabled = true;
492
+ turnResults.set(0, result(["page-a"]));
493
+ turnResults.set(1, result(["page-a"]));
494
+ // Fill the memo to its 256-entry cap.
495
+ for (let i = 0; i < 256; i++) {
496
+ await produceCards(`conv-${i}`, 0);
497
+ }
498
+ // A new turn for a tracked conversation is a key REFRESH — nothing may be
499
+ // evicted for it (the pre-fix code evicted the oldest entry here).
500
+ await produceCards("conv-5", 1);
501
+ observeTurnSpy.mockClear();
502
+ await produceCards("conv-0", 0);
503
+ expect(observeTurnSpy).toHaveBeenCalledTimes(0); // still memoized
504
+ // A genuinely NEW key at the cap evicts the least-recently-set entry
505
+ // (conv-0); the refreshed conv-5 survives.
506
+ await produceCards("conv-new", 0);
507
+ observeTurnSpy.mockClear();
508
+ await produceCards("conv-5", 1);
509
+ expect(observeTurnSpy).toHaveBeenCalledTimes(0); // refreshed → survived
510
+ await produceCards("conv-0", 0);
511
+ expect(observeTurnSpy).toHaveBeenCalledTimes(1); // evicted → re-observed
512
+ });
513
+
514
+ test("empty selection → null (fallback to v2), nothing recorded", async () => {
515
+ liveEnabled = true;
516
+ turnResults.set(0, result([]));
517
+ expect(await produceCards("conv-1", 0)).toBeNull();
518
+ expect(getActiveSlugs("conv-1")).toEqual(new Set());
519
+ });
520
+
521
+ test("shadow mode (live off) attaches nothing, records nothing, logs the would-inject set", async () => {
522
+ shadowEnabled = true;
523
+ turnResults.set(
524
+ 0,
525
+ result(
526
+ ["page-a"],
527
+ [["page-a", section("page-a", "Heading", "section text")]],
528
+ ),
529
+ );
530
+
531
+ expect(await produceCards("conv-1", 0)).toBeNull();
532
+ expect(await produceSpotlight("conv-1", 0)).toBeNull();
533
+ expect(getActiveSlugs("conv-1")).toEqual(new Set());
534
+ const shadowLog = logCalls.find((c) =>
535
+ c.msg.includes("memory-v3 shadow: would inject"),
536
+ );
537
+ expect(shadowLog).toBeDefined();
538
+ const data = shadowLog!.data as {
539
+ netNew: Array<{ slug: string; bytes: number }>;
540
+ spotlightRefs: string[];
541
+ };
542
+ expect(data.netNew.map((e) => e.slug)).toEqual(["page-a"]);
543
+ expect(data.spotlightRefs).toEqual(["page-a§Heading"]);
544
+ });
545
+
546
+ test("the persistent card block never contains the spotlight wrapper", async () => {
547
+ liveEnabled = true;
548
+ turnResults.set(
549
+ 0,
550
+ result(
551
+ ["page-a"],
552
+ [["page-a", section("page-a", "Heading", "section text")]],
553
+ ),
554
+ );
555
+ const block = await produceCards("conv-1", 0);
556
+ expect(block!.text).not.toContain("<memory_spotlight>");
557
+ });
558
+
559
+ test("both injectors share ONE orchestration per turn (memoized)", async () => {
560
+ liveEnabled = true;
561
+ turnResults.set(0, result(["page-a"]));
562
+ await produceCards("conv-1", 0);
563
+ await produceSpotlight("conv-1", 0);
564
+ expect(observeTurnSpy).toHaveBeenCalledTimes(1);
565
+ });
566
+ });
567
+
568
+ // ─── ephemeral spotlight ────────────────────────────────────────────────────
569
+
570
+ describe("memoryV3SpotlightInjector — ephemeral section spotlight", () => {
571
+ const sectionA = section("page-a", "Alpha", "alpha section text");
572
+ const sectionB = section("page-b", "Beta", "beta section text");
573
+ const sectionC = section("page-c", "Gamma", "gamma section text");
574
+
575
+ test("renders selected finder hits' matched sections at the user tail", async () => {
576
+ liveEnabled = true;
577
+ turnResults.set(0, result(["page-a", "page-b"], [["page-a", sectionA]]));
578
+
579
+ const block = await produceSpotlight("conv-1", 0);
580
+ expect(block).not.toBeNull();
581
+ expect(block!.placement).toBe("append-user-tail");
582
+ expect(block!.text.startsWith("<memory_spotlight>\n")).toBe(true);
583
+ expect(block!.text.endsWith("\n</memory_spotlight>")).toBe(true);
584
+ expect(block!.text).toContain(
585
+ "## memory/concepts/page-a.md § Alpha\nalpha section text",
586
+ );
587
+ });
588
+
589
+ test("unselected finder hits do not spotlight; top-n bound applies", async () => {
590
+ liveEnabled = true;
591
+ spotlightConfig = { n: 1, windowTurns: 0 };
592
+ turnResults.set(
593
+ 0,
594
+ result(
595
+ ["page-a", "page-b"],
596
+ [
597
+ ["page-c", sectionC], // finder hit, NOT selected
598
+ ["page-a", sectionA],
599
+ ["page-b", sectionB], // selected but over the n=1 cut
600
+ ],
601
+ ),
602
+ );
603
+ const block = await produceSpotlight("conv-1", 0);
604
+ expect(block!.text).toContain("page-a.md § Alpha");
605
+ expect(block!.text).not.toContain("page-b.md");
606
+ expect(block!.text).not.toContain("page-c.md");
607
+ });
608
+
609
+ test("carries the previous windowTurns turns' entries, ages older ones out, never accumulates", async () => {
610
+ liveEnabled = true;
611
+ spotlightConfig = { n: 6, windowTurns: 1 };
612
+ turnResults.set(0, result(["page-a"], [["page-a", sectionA]]));
613
+ turnResults.set(1, result(["page-b"], [["page-b", sectionB]]));
614
+ turnResults.set(2, result(["page-c"], [["page-c", sectionC]]));
615
+
616
+ const t1 = await produceSpotlight("conv-1", 0);
617
+ expect(t1!.text).toContain("Alpha");
618
+
619
+ // Turn 1: current (Beta) + previous turn (Alpha).
620
+ const t2 = await produceSpotlight("conv-1", 1);
621
+ expect(t2!.text).toContain("Beta");
622
+ expect(t2!.text).toContain("Alpha");
623
+
624
+ // Turn 2: current (Gamma) + turn 1 (Beta); turn 0 (Alpha) aged out.
625
+ const t3 = await produceSpotlight("conv-1", 2);
626
+ expect(t3!.text).toContain("Gamma");
627
+ expect(t3!.text).toContain("Beta");
628
+ expect(t3!.text).not.toContain("Alpha");
629
+ });
630
+
631
+ test("re-producing the SAME turn replaces its window entry (no duplication)", async () => {
632
+ liveEnabled = true;
633
+ spotlightConfig = { n: 6, windowTurns: 2 };
634
+ turnResults.set(0, result(["page-a"], [["page-a", sectionA]]));
635
+
636
+ await produceSpotlight("conv-1", 0);
637
+ const again = await produceSpotlight("conv-1", 0);
638
+ const occurrences = again!.text.split("page-a.md § Alpha").length - 1;
639
+ expect(occurrences).toBe(1);
640
+ });
641
+
642
+ test("window is bounded by n × (windowTurns + 1) entries", async () => {
643
+ liveEnabled = true;
644
+ spotlightConfig = { n: 2, windowTurns: 1 };
645
+ const sections = (turn: number): Array<[Slug, Section]> =>
646
+ ["w", "x", "y", "z"].map((s) => {
647
+ const slug = `page-${s}${turn}`;
648
+ return [slug, section(slug, `T${turn}${s}`, "text")] as [Slug, Section];
649
+ });
650
+ turnResults.set(
651
+ 0,
652
+ result(
653
+ sections(0).map(([s]) => s),
654
+ sections(0),
655
+ ),
656
+ );
657
+ turnResults.set(
658
+ 1,
659
+ result(
660
+ sections(1).map(([s]) => s),
661
+ sections(1),
662
+ ),
663
+ );
664
+
665
+ await produceSpotlight("conv-1", 0);
666
+ const block = await produceSpotlight("conv-1", 1);
667
+ const entryCount = (block!.text.match(/^## memory\/concepts\//gm) ?? [])
668
+ .length;
669
+ expect(entryCount).toBeLessThanOrEqual(2 * (1 + 1));
670
+ });
671
+
672
+ test("no matched sections in the window → no block", async () => {
673
+ liveEnabled = true;
674
+ turnResults.set(0, result(["page-a"]));
675
+ expect(await produceSpotlight("conv-1", 0)).toBeNull();
676
+ });
677
+
678
+ test("live off → null even when shadow is on", async () => {
679
+ shadowEnabled = true;
680
+ turnResults.set(0, result(["page-a"], [["page-a", sectionA]]));
681
+ expect(await produceSpotlight("conv-1", 0)).toBeNull();
682
+ });
683
+ });