@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,471 @@
1
+ /**
2
+ * Memory-v3 prune valve: a structural bound on the resident frozen-card
3
+ * footprint.
4
+ *
5
+ * Frozen cards accumulate in history with no per-turn bound (the injector
6
+ * renders net-new only and never strips prior blocks — the cache contract).
7
+ * The valve is the backstop: when the resident (non-pruned) card bytes exceed
8
+ * `memory.v3.prune.maxResidentBytes`, the least-recently-selected
9
+ * non-core/non-hot cards are pruned, oldest first, until the footprint is at
10
+ * `targetResidentBytes`.
11
+ *
12
+ * Pruning is `markPruned` (the store's audit-preserving tombstone) plus two
13
+ * FILTER points — never a metadata rewrite, so the persisted
14
+ * `metadata.memoryV3InjectedBlock` rows stay intact (auditable, and a
15
+ * re-selected slug re-injects as a fresh card because `recordInjected` clears
16
+ * `pruned_at`):
17
+ *
18
+ * (a) a one-time strip of the pruned cards' sections from the `<memory>`
19
+ * blocks riding the LIVE in-memory history
20
+ * ({@link stripPrunedCardsFromMessages} — per-card boundaries are the
21
+ * `# memory/concepts/<slug>.md` headers within a block, terminated at
22
+ * any other top-level header chunk such as capability content; see
23
+ * {@link parseCardSections}). The strip mutates the shared message
24
+ * objects in place so the agent loop's end-of-turn history fold-back
25
+ * keeps the stripped content;
26
+ * (b) the `loadFromDb` rehydration splice in `daemon/conversation.ts`
27
+ * re-applies {@link filterPrunedCardSections} on every load, so prunes
28
+ * persist across daemon restarts without touching the metadata.
29
+ *
30
+ * The first post-prune request loses the provider prefix cache from the
31
+ * earliest affected message — ONE amortized bust per prune, logged with
32
+ * `prunedSlugs` / `bytesFreed`.
33
+ *
34
+ * v2-coexistence note: v2's dynamic `<memory>` blocks share the exact wrapper
35
+ * and `# memory/concepts/<slug>.md` header convention, so the live strip
36
+ * cannot tell layers apart syntactically. A block is treated as v3-owned only
37
+ * when EVERY card section in it byte-matches a section of some persisted
38
+ * `memoryV3InjectedBlock` for the conversation
39
+ * ({@link collectPersistedV3Cards}) — v2 sections render the page SUMMARY
40
+ * (or full page) rather than the card head+TOC, so pre-flip v2 blocks never
41
+ * qualify and are left untouched, keeping their unfiltered rehydration
42
+ * byte-identical.
43
+ *
44
+ * Capability note: skill / CLI-command content renders under its own
45
+ * `# Skill:` / `# CLI command:` header — not a card section — so it can never
46
+ * be located (and therefore never stripped) by slug. The injector records
47
+ * capability slugs at `bytes: 0`, which keeps them out of the resident
48
+ * measure AND out of candidacy (zero-byte rows are skipped — pruning them
49
+ * frees nothing); capability content riding a card block survives the prune
50
+ * of its neighboring cards as a non-card chunk.
51
+ *
52
+ * Accounting-drift note: a slug whose recorded bytes have no locatable
53
+ * persisted card section (e.g. its metadata row was lost) can be planned and
54
+ * tombstoned — the strip/rehydration filter simply finds nothing to remove,
55
+ * its content (if any) stays in context, and its bytes leave the resident
56
+ * accounting with the tombstone. That self-heals in ONE pass: the next valve
57
+ * run measures the corrected footprint, so the valve never loop-fires against
58
+ * bytes it cannot free.
59
+ */
60
+
61
+ import { getConfig } from "../../../config/loader.js";
62
+ import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
63
+ import {
64
+ unwrapMemoryBlock,
65
+ wrapMemoryBlock,
66
+ } from "../../../memory/memory-marker.js";
67
+ import {
68
+ INJECTED_CONCEPT_HEADER_REGEX,
69
+ readInjectedBlock,
70
+ } from "../../../memory/v2/injected-block-slugs.js";
71
+ import type { ContentBlock, Message } from "../../../providers/types.js";
72
+ import { getLogger } from "../../../util/logger.js";
73
+ import {
74
+ getActiveEntries,
75
+ getPrunedSlugs,
76
+ markPruned,
77
+ MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
78
+ residentBytes,
79
+ } from "./ever-injected-store.js";
80
+
81
+ const log = getLogger("memory-v3-shadow");
82
+
83
+ // ─── card-section parsing & filtering ────────────────────────────────────────
84
+
85
+ /** Matches any top-level `# ` header line — concept card headers AND foreign
86
+ * headers like a capability chunk's `# Skill:` / `# CLI command:` line. */
87
+ const TOP_LEVEL_HEADER_REGEX = /^# /gm;
88
+
89
+ /** One parsed card section: the header line plus everything up to the next
90
+ * chunk boundary (or end of block), trailing whitespace removed. */
91
+ export interface CardSection {
92
+ slug: string;
93
+ /** The section text INCLUDING its `# memory/concepts/<slug>.md` header
94
+ * line, `trimEnd()`ed so re-joining with `\n\n` reproduces the renderer's
95
+ * exact bytes. */
96
+ text: string;
97
+ }
98
+
99
+ /** One ordered chunk of a parsed card block: a concept card (prunable, owned
100
+ * by `slug`) or any other `\n\n`-joined chunk (e.g. capability content under
101
+ * its own `# Skill:` / `# CLI command:` header — never prunable). */
102
+ export type CardBlockPiece =
103
+ | { kind: "card"; slug: string; text: string }
104
+ | { kind: "other"; text: string };
105
+
106
+ /**
107
+ * Split an UNWRAPPED card-block body into its preamble (the instruction
108
+ * header — everything before the first boundary), the ordered chunk pieces,
109
+ * and the card sections (the `kind: "card"` pieces, kept as a convenience
110
+ * view). Returns zero sections/pieces when the text carries no concept card
111
+ * headers.
112
+ *
113
+ * A card's section ends at the next concept header OR at any other top-level
114
+ * `# ` header that starts its own `\n\n`-joined chunk — so a capability chunk
115
+ * trailing a concept card (`renderCardsBlockInner` joins them with `\n\n`) is
116
+ * parsed as a separate non-card piece instead of being absorbed into the
117
+ * card, and pruning the card never deletes it. The blank-line requirement
118
+ * keeps a card head's own `# Title` line (which follows the path header with
119
+ * a single `\n`) from splitting the card, and guarantees splits land on the
120
+ * renderer's `\n\n` seams so re-joins stay byte-identical.
121
+ */
122
+ export function parseCardSections(inner: string): {
123
+ preamble: string;
124
+ sections: CardSection[];
125
+ pieces: CardBlockPiece[];
126
+ } {
127
+ const cardMatches = [...inner.matchAll(INJECTED_CONCEPT_HEADER_REGEX)];
128
+ if (cardMatches.length === 0) {
129
+ return { preamble: inner, sections: [], pieces: [] };
130
+ }
131
+
132
+ const cardStarts = new Set(cardMatches.map((match) => match.index!));
133
+ const boundaries: Array<{ index: number; slug: string | null }> =
134
+ cardMatches.map((match) => ({ index: match.index!, slug: match[1]! }));
135
+ for (const match of inner.matchAll(TOP_LEVEL_HEADER_REGEX)) {
136
+ const i = match.index!;
137
+ if (cardStarts.has(i)) continue;
138
+ // Foreign header on a `\n\n` seam → starts its own chunk.
139
+ if (i >= 2 && inner[i - 1] === "\n" && inner[i - 2] === "\n") {
140
+ boundaries.push({ index: i, slug: null });
141
+ }
142
+ }
143
+ boundaries.sort((a, b) => a.index - b.index);
144
+
145
+ const preamble = inner.slice(0, boundaries[0]!.index).trimEnd();
146
+ const pieces = boundaries.map((boundary, i): CardBlockPiece => {
147
+ const end =
148
+ i + 1 < boundaries.length ? boundaries[i + 1]!.index : undefined;
149
+ const text = inner.slice(boundary.index, end).trimEnd();
150
+ return boundary.slug === null
151
+ ? { kind: "other", text }
152
+ : { kind: "card", slug: boundary.slug, text };
153
+ });
154
+ const sections = pieces.filter(
155
+ (piece): piece is Extract<CardBlockPiece, { kind: "card" }> =>
156
+ piece.kind === "card",
157
+ );
158
+ return { preamble, sections, pieces };
159
+ }
160
+
161
+ /**
162
+ * Remove pruned slugs' card sections from an unwrapped block body.
163
+ *
164
+ * Returns the input string UNCHANGED (same reference) when nothing is
165
+ * removed — callers use identity to detect a no-op — and `""` when every
166
+ * chunk is pruned (the caller drops/skips the whole block; a bare
167
+ * instruction header with no cards carries no content). Non-card chunks
168
+ * (capability content) are always kept, so a block whose concept cards are
169
+ * all pruned but which carries capability content keeps its preamble and
170
+ * that content. Kept chunks are re-joined exactly as the renderer joined
171
+ * them (`\n\n`), so an unpruned remainder stays byte-identical to what a
172
+ * fresh render of those chunks would produce.
173
+ */
174
+ export function filterPrunedCardSections(
175
+ inner: string,
176
+ prunedSlugs: ReadonlySet<string>,
177
+ ): string {
178
+ const { preamble, sections, pieces } = parseCardSections(inner);
179
+ if (sections.length === 0) return inner;
180
+
181
+ const kept = pieces.filter(
182
+ (piece) => piece.kind !== "card" || !prunedSlugs.has(piece.slug),
183
+ );
184
+ if (kept.length === pieces.length) return inner;
185
+ if (kept.length === 0) return "";
186
+
187
+ const texts = kept.map((piece) => piece.text);
188
+ if (preamble.length > 0) texts.unshift(preamble);
189
+ return texts.join("\n\n");
190
+ }
191
+
192
+ // ─── prune planning ──────────────────────────────────────────────────────────
193
+
194
+ export interface PrunePlan {
195
+ /** Slugs to prune, least-recently-selected first. */
196
+ slugs: string[];
197
+ /** Resident bytes the plan frees once executed. */
198
+ bytesFreed: number;
199
+ }
200
+
201
+ export interface PruneDeps {
202
+ maxResidentBytes: number;
203
+ targetResidentBytes: number;
204
+ /** Core + hot lane members — the selector's stable prefix must never be
205
+ * pruned out from under it. */
206
+ exemptSlugs: ReadonlySet<string>;
207
+ }
208
+
209
+ /**
210
+ * Plan a prune for the conversation, or `null` when the resident footprint is
211
+ * within `maxResidentBytes` (or nothing is prunable).
212
+ *
213
+ * The footprint and the candidates both range over the ACTIVE injected slugs.
214
+ * Candidates are ranked by last selection recency — `MAX(created_at)` per
215
+ * slug from `memory_v3_selections`, falling back to the store's `injected_at`
216
+ * for slugs with no selection rows (e.g. rows copied by a full fork) — taken
217
+ * oldest-first until the footprint is at `targetResidentBytes`. Core/hot lane
218
+ * members are exempt, and zero-byte rows (capability slugs, truncated-fork
219
+ * seeds: dedup-only, no byte accounting) are skipped — pruning them frees
220
+ * nothing while discarding inherited context.
221
+ */
222
+ export function planPrune(
223
+ deps: PruneDeps,
224
+ conversationId: string,
225
+ ): PrunePlan | null {
226
+ const activeEntries = getActiveEntries(conversationId);
227
+ const resident = activeEntries.reduce((sum, e) => sum + e.bytes, 0);
228
+ if (resident <= deps.maxResidentBytes) return null;
229
+
230
+ const selectionRows = getSqliteFrom(getDb())
231
+ .query(
232
+ /*sql*/ `
233
+ SELECT slug, MAX(created_at) AS lastSelectedAt FROM memory_v3_selections
234
+ WHERE conversation_id = ?
235
+ GROUP BY slug
236
+ `,
237
+ )
238
+ .all(conversationId) as Array<{ slug: string; lastSelectedAt: number }>;
239
+ const lastSelectedAt = new Map(
240
+ selectionRows.map((row) => [row.slug, row.lastSelectedAt]),
241
+ );
242
+
243
+ const candidates = activeEntries
244
+ .filter((entry) => entry.bytes > 0 && !deps.exemptSlugs.has(entry.slug))
245
+ .map((entry) => ({
246
+ ...entry,
247
+ recency: lastSelectedAt.get(entry.slug) ?? entry.injectedAt,
248
+ }))
249
+ // Oldest first; slug ascending as the deterministic tiebreak.
250
+ .sort((a, b) => a.recency - b.recency || (a.slug < b.slug ? -1 : 1));
251
+
252
+ const slugs: string[] = [];
253
+ let bytesFreed = 0;
254
+ for (const candidate of candidates) {
255
+ if (resident - bytesFreed <= deps.targetResidentBytes) break;
256
+ slugs.push(candidate.slug);
257
+ bytesFreed += candidate.bytes;
258
+ }
259
+ return slugs.length === 0 ? null : { slugs, bytesFreed };
260
+ }
261
+
262
+ // ─── live-history strip ──────────────────────────────────────────────────────
263
+
264
+ /**
265
+ * Collect the conversation's known v3 card SECTION TEXTS from every persisted
266
+ * `metadata.memoryV3InjectedBlock` row — the live strip's v3-ownership test:
267
+ * a live `<memory>` block is v3-owned iff all of its card sections appear
268
+ * here (see the module doc's v2-coexistence note). Capability chunks never
269
+ * contribute: their content renders under `# Skill:` / `# CLI command:`
270
+ * headers, which parse as non-card chunks.
271
+ */
272
+ export function collectPersistedV3Cards(conversationId: string): Set<string> {
273
+ // Substring prefilter (indexable LIKE) mirrors the Slack metadata scan;
274
+ // rows are validated by `readInjectedBlock`'s JSON parse.
275
+ const rows = getSqliteFrom(getDb())
276
+ .query(
277
+ /*sql*/ `
278
+ SELECT metadata FROM messages
279
+ WHERE conversation_id = ? AND metadata LIKE '%' || ? || '%'
280
+ `,
281
+ )
282
+ .all(conversationId, MEMORY_V3_INJECTED_BLOCK_METADATA_KEY) as Array<{
283
+ metadata: string | null;
284
+ }>;
285
+
286
+ const sections = new Set<string>();
287
+ for (const row of rows) {
288
+ const block = readInjectedBlock(
289
+ row.metadata,
290
+ MEMORY_V3_INJECTED_BLOCK_METADATA_KEY,
291
+ );
292
+ if (block === null) continue;
293
+ for (const section of parseCardSections(unwrapMemoryBlock(block))
294
+ .sections) {
295
+ sections.add(section.text);
296
+ }
297
+ }
298
+ return sections;
299
+ }
300
+
301
+ /**
302
+ * One-time strip of pruned cards from the live in-memory history: for every
303
+ * v3-owned `<memory>` text block (ownership per `knownV3Sections` — see the
304
+ * module doc), drop the pruned slugs' card sections; a block whose cards are
305
+ * all pruned is removed outright (matching the rehydration splice, which
306
+ * skips an all-pruned block).
307
+ *
308
+ * Mutates the affected `Message` objects IN PLACE (`message.content`
309
+ * reassignment): the agent loop's working arrays share these object
310
+ * references, so its end-of-turn history fold-back keeps the strip. Returns
311
+ * the number of blocks changed.
312
+ */
313
+ export function stripPrunedCardsFromMessages(
314
+ messages: Message[],
315
+ prunedSlugs: ReadonlySet<string>,
316
+ knownV3Sections: ReadonlySet<string>,
317
+ ): number {
318
+ let strippedBlocks = 0;
319
+ for (const message of messages) {
320
+ if (message.role !== "user") continue;
321
+ let changed = false;
322
+ const nextContent: ContentBlock[] = [];
323
+ for (const block of message.content) {
324
+ if (block.type !== "text") {
325
+ nextContent.push(block);
326
+ continue;
327
+ }
328
+ const inner = unwrapMemoryBlock(block.text);
329
+ if (inner === block.text) {
330
+ // Not a wrapped `<memory>` block (unwrap is identity on anything
331
+ // without the full wrapper pair).
332
+ nextContent.push(block);
333
+ continue;
334
+ }
335
+ const { sections } = parseCardSections(inner);
336
+ const isV3Block =
337
+ sections.length > 0 &&
338
+ sections.every((section) => knownV3Sections.has(section.text));
339
+ if (!isV3Block) {
340
+ nextContent.push(block);
341
+ continue;
342
+ }
343
+ const filtered = filterPrunedCardSections(inner, prunedSlugs);
344
+ if (filtered === inner) {
345
+ nextContent.push(block);
346
+ continue;
347
+ }
348
+ strippedBlocks += 1;
349
+ changed = true;
350
+ if (filtered.length > 0) {
351
+ nextContent.push({ type: "text", text: wrapMemoryBlock(filtered) });
352
+ }
353
+ }
354
+ if (changed) message.content = nextContent;
355
+ }
356
+ return strippedBlocks;
357
+ }
358
+
359
+ // ─── valve execution & trigger ───────────────────────────────────────────────
360
+
361
+ export interface PruneValveOptions {
362
+ /** Core + hot lane members (never pruned). */
363
+ exemptSlugs: ReadonlySet<string>;
364
+ /** Test seam: resolve the conversation's LIVE in-memory message array.
365
+ * Defaults to the daemon conversation registry (dynamically imported — a
366
+ * static import would cycle with `daemon/conversation.ts`, which imports
367
+ * this module for the rehydration filter). `null` skips the live strip
368
+ * (the rehydration filter still applies the prune on next load). */
369
+ liveMessages?: (conversationId: string) => Message[] | null;
370
+ /** Test seam for the `pruned_at` timestamp. */
371
+ now?: number;
372
+ }
373
+
374
+ async function defaultLiveMessages(
375
+ conversationId: string,
376
+ ): Promise<Message[] | null> {
377
+ const { findConversationOrSubagent } =
378
+ await import("../../../daemon/conversation-registry.js");
379
+ return findConversationOrSubagent(conversationId)?.messages ?? null;
380
+ }
381
+
382
+ /**
383
+ * Run the prune valve once: plan against `memory.v3.prune` config, mark the
384
+ * planned slugs pruned, and strip their cards from the live in-memory
385
+ * history. Returns the executed plan, or `null` when the footprint is within
386
+ * bounds (the common case — repeated invocations below the cap are no-ops).
387
+ *
388
+ * The live strip filters with the conversation's FULL pruned set (not just
389
+ * this plan's slugs), so a card an earlier strip could not reach — e.g. a
390
+ * block not yet folded back into the live history when that prune ran —
391
+ * self-heals on the next prune.
392
+ */
393
+ export async function runPruneValve(
394
+ conversationId: string,
395
+ options: PruneValveOptions,
396
+ ): Promise<PrunePlan | null> {
397
+ // Defensive read: test configs may omit the prune block entirely.
398
+ const pruneConfig = getConfig().memory?.v3?.prune;
399
+ if (!pruneConfig) return null;
400
+
401
+ // Planning needs only the store (cheap); the persisted-metadata scan for
402
+ // the live strip's ownership test runs only once a plan exists — a
403
+ // conversation within the cap never pays it (the common case).
404
+ const plan = planPrune(
405
+ {
406
+ maxResidentBytes: pruneConfig.maxResidentBytes,
407
+ targetResidentBytes: pruneConfig.targetResidentBytes,
408
+ exemptSlugs: options.exemptSlugs,
409
+ },
410
+ conversationId,
411
+ );
412
+ if (!plan) return null;
413
+
414
+ markPruned(conversationId, plan.slugs, options.now ?? Date.now());
415
+
416
+ const liveMessages = options.liveMessages
417
+ ? options.liveMessages(conversationId)
418
+ : await defaultLiveMessages(conversationId);
419
+ let strippedBlocks = 0;
420
+ if (liveMessages) {
421
+ strippedBlocks = stripPrunedCardsFromMessages(
422
+ liveMessages,
423
+ getPrunedSlugs(conversationId),
424
+ collectPersistedV3Cards(conversationId),
425
+ );
426
+ }
427
+
428
+ log.info(
429
+ {
430
+ conversationId,
431
+ prunedSlugs: plan.slugs.length,
432
+ bytesFreed: plan.bytesFreed,
433
+ strippedBlocks,
434
+ residentBytes: residentBytes(conversationId),
435
+ },
436
+ "memory-v3 prune valve: pruned least-recently-selected cards (one amortized prefix-cache bust)",
437
+ );
438
+ return plan;
439
+ }
440
+
441
+ /** Pending valve work, chained so runs serialize per process and tests can
442
+ * await completion ({@link flushPruneValveForTests}). */
443
+ let pendingPrune: Promise<unknown> = Promise.resolve();
444
+
445
+ /**
446
+ * End-of-turn trigger: defer a {@link runPruneValve} pass so prune work never
447
+ * delays the in-flight turn's assembly. Failures are logged and swallowed —
448
+ * the valve must never affect the live turn.
449
+ */
450
+ export function schedulePruneValve(
451
+ conversationId: string,
452
+ options: PruneValveOptions,
453
+ ): void {
454
+ pendingPrune = pendingPrune
455
+ .then(() => new Promise((resolve) => setTimeout(resolve, 0)))
456
+ .then(() => runPruneValve(conversationId, options))
457
+ .catch((err) => {
458
+ log.warn(
459
+ {
460
+ err: err instanceof Error ? err.message : String(err),
461
+ conversationId,
462
+ },
463
+ "memory-v3 prune valve failed (non-fatal)",
464
+ );
465
+ });
466
+ }
467
+
468
+ /** Await all scheduled valve work (deterministic teardown for tests). */
469
+ export function flushPruneValveForTests(): Promise<void> {
470
+ return pendingPrune.then(() => undefined);
471
+ }
@@ -2,38 +2,90 @@ import { wrapMemoryBlock } from "../../../memory/memory-marker.js";
2
2
  import { Section, Slug } from "./types.js";
3
3
 
4
4
  /**
5
- * Render a v3 working-set selection into a single `<memory>…</memory>` text
6
- * block, injecting each slug's matched section (progressive disclosure) rather
7
- * than its full page body.
5
+ * Leading instruction line of the frozen card block — byte-identical to v2's
6
+ * `INJECTION_HEADER` (`memory/v2/injection.ts`) so the read affordance the
7
+ * model already knows applies to cards unchanged: every card carries a
8
+ * `# memory/concepts/<slug>.md` path header it can `file_read`.
9
+ */
10
+ export const V3_CARDS_INJECTION_HEADER =
11
+ 'Use `file_read("memory/concepts/path/to/file.md")` to read the full pages for any of the injected memory summaries you want more information on.';
12
+
13
+ /**
14
+ * Render the UNWRAPPED inner text of a frozen net-new card block: the v2-style
15
+ * instruction header followed by the rendered cards, blank-line separated.
16
+ * Returns `""` for an empty card list (the injector attaches no block and the
17
+ * caller persists nothing). The caller wraps the result via
18
+ * {@link wrapMemoryBlock} exactly once at injection time and persists the
19
+ * unwrapped form to message metadata — the same wrap-on-use contract as v2's
20
+ * `memoryInjectedBlock`.
21
+ */
22
+ export function renderCardsBlockInner(cards: string[]): string {
23
+ if (cards.length === 0) return "";
24
+ return [V3_CARDS_INJECTION_HEADER, ...cards].join("\n\n");
25
+ }
26
+
27
+ /**
28
+ * One ephemeral spotlight entry: a selected finder hit's matched section.
29
+ * `title` is the matched section's heading; `text` its indexed section text.
30
+ */
31
+ export interface SpotlightEntry {
32
+ slug: Slug;
33
+ title: string;
34
+ text: string;
35
+ }
36
+
37
+ /**
38
+ * Render the UNWRAPPED inner text of the ephemeral `<memory_spotlight>` block:
39
+ * one `## memory/concepts/<slug>.md § <section heading>` header plus section
40
+ * text per entry, blank-line separated. Returns `""` for an empty window (the
41
+ * injector attaches no block). The caller wraps via
42
+ * `wrapMemorySpotlightBlock`.
43
+ */
44
+ export function renderSpotlightInner(entries: SpotlightEntry[]): string {
45
+ return entries
46
+ .map(
47
+ (entry) =>
48
+ `## memory/concepts/${entry.slug}.md § ${entry.title}\n${entry.text}`,
49
+ )
50
+ .join("\n\n");
51
+ }
52
+
53
+ /**
54
+ * Render a v3 selection set into a single `<memory>…</memory>` text block,
55
+ * injecting each slug's matched section (progressive disclosure) rather than
56
+ * its full page body.
57
+ *
58
+ * INSPECTOR-ONLY: live injection no longer renders the whole selection set —
59
+ * the injector freezes net-new CARDS into history ({@link renderCardsBlockInner})
60
+ * and re-renders the ephemeral spotlight separately. This whole-set render
61
+ * remains for the inspector's selection log (`selection-log-store.ts`), which
62
+ * reconstructs an approximate view of a turn's selection after the fact.
8
63
  *
9
64
  * Pure and side-effect-free: all I/O is delegated to the injected
10
65
  * `pageContent` resolver, which is awaited once per slug in input order with
11
66
  * that slug's matched section (`sectionBySlug.get(slug)`, possibly undefined).
12
- * The resulting block uses the shared {@link wrapMemoryBlock} marker — the same
13
- * wrapper the v2 graph-memory path emits — so the existing strip machinery in
14
- * `conversation-graph-memory.ts` already recognizes (and evicts) it.
15
- *
16
- * Resolution is concurrent; the rendered block preserves `finalInjection`
17
- * order regardless of which fetch settles first.
67
+ * Resolution is concurrent; the rendered block preserves `slugs` order
68
+ * regardless of which fetch settles first.
18
69
  *
19
- * @param finalInjection - The working-set selection for the turn, in the order
20
- * it should appear in the rendered block.
70
+ * @param slugs - The selected slugs for the turn, in the order they should
71
+ * appear in the rendered block.
21
72
  * @param sectionBySlug - The matched `Section` per slug; a slug with no entry
22
- * (e.g. carry-forward with no current match) renders its full/lead page.
73
+ * (e.g. an edge-only or stable-prefix page with no current match) renders its
74
+ * full/lead page.
23
75
  * @param pageContent - Resolver returning a slug's rendered content given its
24
76
  * matched section (or undefined).
25
77
  * @returns The wrapped `<memory>` block, or `""` for an empty selection (the
26
- * caller injects nothing).
78
+ * caller renders nothing).
27
79
  */
28
80
  export async function renderMemoryBlock(
29
- finalInjection: Slug[],
81
+ slugs: Slug[],
30
82
  sectionBySlug: Map<Slug, Section>,
31
83
  pageContent: (slug: Slug, section: Section | undefined) => Promise<string>,
32
84
  ): Promise<string> {
33
- if (finalInjection.length === 0) return "";
85
+ if (slugs.length === 0) return "";
34
86
 
35
87
  const contents = await Promise.all(
36
- finalInjection.map((slug) => pageContent(slug, sectionBySlug.get(slug))),
88
+ slugs.map((slug) => pageContent(slug, sectionBySlug.get(slug))),
37
89
  );
38
90
 
39
91
  return wrapMemoryBlock(contents.join("\n"));
@@ -1,10 +1,17 @@
1
1
  /**
2
2
  * Read-side store for the inspector's Memory V3 section. Reads the persisted
3
- * `memory_v3_selections` rows for a turn and re-renders the `<memory>` block
4
- * the v3 working set selected, so the inspector can show what v3 chose (and,
5
- * in live mode, what it actually injected) without re-running orchestration —
6
- * which would be wrong anyway, since the working set is stateful (carry-forward
7
- * across turns) and can't be reproduced after the fact.
3
+ * `memory_v3_selections` rows for a turn and re-renders an APPROXIMATE
4
+ * `<memory>` block for what v3 selected, so the inspector can show the turn's
5
+ * selection without re-running orchestration — which would be wrong anyway,
6
+ * since the hot lane is frecency-stateful and can't be reproduced after the
7
+ * fact.
8
+ *
9
+ * The rendered text is inspector-only and NOT byte-identical to live
10
+ * injection: the live injector freezes net-new compact CARDS into history
11
+ * (`renderV3CardContent`) plus an ephemeral spotlight, while this store
12
+ * re-renders the whole selection set as full/lead pages
13
+ * (`renderV3SectionContent` with no persisted section identity — see the
14
+ * inline note at `injectedText`).
8
15
  */
9
16
 
10
17
  import type { MemoryV3SelectionLog } from "../../../api/responses/memory-v3-selection-log.js";
@@ -57,9 +64,8 @@ function rowsForTurn(conversationId: string, turn: number): SelectionRow[] {
57
64
  * wrong data. Tying v3 rows to a message id for exact per-message attribution
58
65
  * regardless of counter drift is a documented follow-up.
59
66
  *
60
- * Selection rows are stored in `finalInjection` order (this turn's L2
61
- * selections, then carry-forward), so rendering them in row order reproduces
62
- * the block v3 would inject.
67
+ * Selection rows are stored in selection order, so rendering them in row order
68
+ * reproduces the block v3 would inject.
63
69
  */
64
70
  export async function getMemoryV3SelectionForInspector(
65
71
  conversationId: string,
@@ -106,13 +112,14 @@ export async function getMemoryV3SelectionForInspector(
106
112
  * `memory_v3_selections` row for the conversation (all turns) and rolls them up
107
113
  * for shadow-vs-v2 inspection without re-rendering any blocks:
108
114
  *
109
- * - `bySource`: count of selection rows per lane source (`needle` / `dense` /
110
- * `edge` / `carry-forward`). Every known source is present (zero when
115
+ * - `bySource`: count of selection rows per lane source (`core` / `hot` /
116
+ * `needle` / `dense` / `edge`). Every known source is present (zero when
111
117
  * unused) so callers can diff two runs without null-guarding; an unknown
112
- * historical/free-text source is ignored (the column is permissive).
118
+ * historical/free-text source — including retired labels like the old
119
+ * per-turn carry source — is ignored (the column is permissive).
113
120
  * - `turns`: number of distinct turns that logged at least one selection.
114
121
  * - `distinctSlugs`: number of distinct page slugs selected across all turns —
115
- * the conversation's working-set footprint.
122
+ * the conversation's selection footprint.
116
123
  *
117
124
  * This is read-only telemetry for comparing a shadow run's lane mix against
118
125
  * v2's logged selections offline; it never re-runs orchestration.