@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
@@ -1,38 +1,229 @@
1
1
  /**
2
- * The memory-v3 {@link Injector}. Reads both v3 flags:
3
- * - `memory-v3-live` on → orchestrate, log, render the working-set selection
4
- * into a `<memory>` block, and return it at v2's dynamic-memory placement.
5
- * - `memory-v3-shadow` on (live off) → orchestrate + log only, return `null`.
6
- * - both off → return `null` (no orchestration).
2
+ * The memory-v3 {@link Injector}s: frozen net-new cards + ephemeral spotlight.
7
3
  *
8
- * Empty selection and any failure return `null` (no v3 injection). v2
9
- * suppression keys off BOTH the flag AND this return value, so a `null` here
10
- * (failure or empty selection) falls back to v2 memory rather than dropping all
11
- * memory.
4
+ * Two injectors share one orchestration result per turn (memoized via
5
+ * {@link observeTurnOnce} so re-entry assemblies — overflow convergence,
6
+ * post-compaction re-injection — reuse the turn's selections instead of
7
+ * re-running the selector):
12
8
  *
13
- * Orchestration and telemetry live in {@link observeTurn}; this module is the
14
- * thin injector wrapper that renders the result for live injection.
9
+ * - {@link memoryV3Injector} (id `memory-v3`, `after-memory-prefix`): the
10
+ * PERSISTENT layer. Renders only this turn's NET-NEW selections — selections
11
+ * not already in the everInjected store — as compact cards inside one
12
+ * `<memory>` block and returns the block. The everInjected store write
13
+ * (`recordInjected`) and the prune-valve schedule are DEFERRED to a commit
14
+ * callback the block carries (`meta[MEMORY_V3_COMMIT_META_KEY]`): runtime
15
+ * assembly invokes it only when the turn's tail is a user message — the
16
+ * same gate as metadata capture — so a turn whose block silently fails to
17
+ * attach never claims its cards in the store (which would suppress them
18
+ * until compaction). Runtime assembly splices the block onto the current
19
+ * user message and the user-prompt-submit hook persists the unwrapped inner
20
+ * text under `metadata.memoryV3InjectedBlock`; `conversation.ts` rehydrates
21
+ * it on load. The block is FROZEN thereafter: prior turns' card blocks stay
22
+ * byte-identical in history, so they ride the provider's cached prefix and
23
+ * survive restarts — mirroring v2's `memoryInjectedBlock` mechanism. An
24
+ * all-repeat turn returns an EMPTY-TEXT block: assembly attaches nothing,
25
+ * but the block's presence still keys v2 suppression (v3 ran and owns the
26
+ * `<memory>` layer this turn). A `null` return (failure / empty selection /
27
+ * every net-new card rendered empty) leaves v2's block intact — fallback to
28
+ * v2 rather than a memory-less turn.
29
+ *
30
+ * - {@link memoryV3SpotlightInjector} (id `memory-v3-spotlight`,
31
+ * `append-user-tail`): the EPHEMERAL layer. Renders the top `spotlight.n`
32
+ * selected finder hits' matched sections, plus the previous
33
+ * `spotlight.windowTurns` turns' entries from an in-memory per-conversation
34
+ * ring (a daemon restart simply re-warms it), as a `<memory_spotlight>`
35
+ * block at the current-message tail. Assembly strip-and-replaces this block
36
+ * every turn (scoped to this block id only); it is never persisted to
37
+ * metadata.
38
+ *
39
+ * Flag gating is unchanged: `memory-v3-live` attaches blocks; `memory-v3-shadow`
40
+ * (live off) logs what WOULD inject (net-new slugs + bytes + spotlight refs)
41
+ * and attaches nothing; both off → no orchestration.
42
+ *
43
+ * Both injectors apply the same personal-memory trust gate as v2
44
+ * ({@link isPersonalMemoryAllowed}): an untrusted remote actor's turn
45
+ * produces nothing — no orchestration, no cards, no spotlight, and nothing
46
+ * recorded or persisted. Memory pages, skill/CLI capability cards, and
47
+ * matched-section spotlights all surface private user content — and because
48
+ * v3 cards are persisted to message metadata and rehydrated forever, the gate
49
+ * must also keep an untrusted turn from recording or persisting anything.
50
+ *
51
+ * Known mirror-of-v2 limitation: cards attached by mid-turn re-entry
52
+ * assemblies (post-compaction re-injection) live only in memory — metadata is
53
+ * persisted at the first-call site only — so a restart drops them while the
54
+ * store still claims them, exactly as v2's `lastInjectedBlock` reinject does.
55
+ * The next compaction clears the store and resets both layers.
15
56
  */
16
57
 
17
58
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
18
59
  import { getConfig } from "../../../config/loader.js";
60
+ import { isPersonalMemoryAllowed } from "../../../daemon/trust-context.js";
61
+ import {
62
+ wrapMemoryBlock,
63
+ wrapMemorySpotlightBlock,
64
+ } from "../../../memory/memory-marker.js";
19
65
  import { getLogger } from "../../../util/logger.js";
20
66
  import {
21
67
  type InjectionBlock,
22
68
  type Injector,
23
69
  type TurnContext,
24
70
  } from "../../types.js";
25
- import { renderV3SectionContent } from "./page-content.js";
26
- import { renderMemoryBlock } from "./render-injection.js";
71
+ import { isCapabilitySlug } from "./capabilities.js";
72
+ import { cardBytes } from "./card.js";
73
+ import { getActiveSlugs, recordInjected } from "./ever-injected-store.js";
74
+ import type { OrchestrateResult } from "./orchestrate.js";
75
+ import { renderV3CardContent } from "./page-content.js";
76
+ import { schedulePruneValve } from "./prune.js";
77
+ import {
78
+ renderCardsBlockInner,
79
+ renderSpotlightInner,
80
+ type SpotlightEntry,
81
+ } from "./render-injection.js";
27
82
  import {
28
83
  MEMORY_V3_LIVE,
29
84
  MEMORY_V3_SHADOW,
30
85
  observeTurn,
31
86
  } from "./shadow-plugin.js";
32
- import { MEMORY_V3_BLOCK_ID } from "./types.js";
87
+ import {
88
+ MEMORY_V3_BLOCK_ID,
89
+ MEMORY_V3_COMMIT_META_KEY,
90
+ MEMORY_V3_SPOTLIGHT_BLOCK_ID,
91
+ type Slug,
92
+ } from "./types.js";
33
93
 
34
94
  const log = getLogger("memory-v3-shadow");
35
95
 
96
+ /**
97
+ * Cap on the per-conversation maps below. Least-recently-touched entries are
98
+ * evicted first; an evicted conversation simply re-warms (spotlight) or
99
+ * re-runs orchestration (memo) on its next turn.
100
+ */
101
+ const MAX_TRACKED_CONVERSATIONS = 256;
102
+
103
+ /**
104
+ * LRU-set `key` on `map`: delete-then-set so a re-touched key moves to the
105
+ * back of the Map's insertion order (a plain `set` on an existing key keeps
106
+ * its original position, which would evict the most long-lived ACTIVE
107
+ * conversation first). Eviction only fires when inserting a genuinely new
108
+ * key at the cap.
109
+ */
110
+ function lruSet<V>(map: Map<string, V>, key: string, value: V): void {
111
+ if (map.has(key)) {
112
+ map.delete(key);
113
+ } else if (map.size >= MAX_TRACKED_CONVERSATIONS) {
114
+ const oldest = map.keys().next().value;
115
+ if (oldest !== undefined) map.delete(oldest);
116
+ }
117
+ map.set(key, value);
118
+ }
119
+
120
+ // ─── shared per-turn orchestration memo ─────────────────────────────────────
121
+
122
+ interface ObservedTurn {
123
+ turnIndex: number;
124
+ result: Promise<OrchestrateResult | null>;
125
+ }
126
+
127
+ /** Latest observed turn per conversation (both injectors + re-entry sites
128
+ * share one orchestration per turn). */
129
+ const observedTurns = new Map<string, ObservedTurn>();
130
+
131
+ /**
132
+ * Run {@link observeTurn} once per (conversation, turn) and memoize the
133
+ * promise. The cards and spotlight injectors both consume the result, and
134
+ * re-entry assemblies within the same turn (overflow convergence,
135
+ * post-compaction re-injection) reuse the turn's selections rather than
136
+ * paying a second selector call. A new `turnIndex` replaces the entry, so the
137
+ * memo never holds more than one turn per conversation.
138
+ */
139
+ function observeTurnOnce(
140
+ conversationId: string,
141
+ turnIndex: number,
142
+ ): Promise<OrchestrateResult | null> {
143
+ const cached = observedTurns.get(conversationId);
144
+ if (cached && cached.turnIndex === turnIndex) return cached.result;
145
+ const result = observeTurn(conversationId, turnIndex);
146
+ lruSet(observedTurns, conversationId, { turnIndex, result });
147
+ return result;
148
+ }
149
+
150
+ // ─── ephemeral spotlight ring ────────────────────────────────────────────────
151
+
152
+ interface SpotlightTurn {
153
+ turnIndex: number;
154
+ entries: SpotlightEntry[];
155
+ }
156
+
157
+ /** Recent turns' spotlight entries per conversation, oldest → newest. */
158
+ const spotlightRings = new Map<string, SpotlightTurn[]>();
159
+
160
+ /**
161
+ * Compute this turn's spotlight entries: the top `n` SELECTED finder hits'
162
+ * matched sections, in finder surfacing order (needle → dense → edge
163
+ * precedence). A finder hit on a stable-prefix page keeps its matched-section
164
+ * ref in the orchestrate result, so core/hot pages with current relevance
165
+ * spotlight too.
166
+ */
167
+ function computeSpotlightEntries(
168
+ result: OrchestrateResult,
169
+ n: number,
170
+ ): SpotlightEntry[] {
171
+ const selected = new Set<Slug>(result.selections.map((s) => s.slug));
172
+ const entries: SpotlightEntry[] = [];
173
+ for (const candidate of result.lanes.finder) {
174
+ if (entries.length >= n) break;
175
+ if (!selected.has(candidate.slug)) continue;
176
+ const section = result.matchedSections.get(candidate.slug);
177
+ if (!section) continue;
178
+ entries.push({
179
+ slug: candidate.slug,
180
+ title: section.title,
181
+ text: section.text,
182
+ });
183
+ }
184
+ return entries;
185
+ }
186
+
187
+ /**
188
+ * Fold this turn's entries into the conversation's ring and return the
189
+ * rendered window: current turn first, then previous turns newest-first,
190
+ * deduped by slug § heading, bounded by `n × (windowTurns + 1)` entries by
191
+ * construction. Re-observing the SAME turn (re-entry assembly) replaces that
192
+ * turn's entry rather than accumulating.
193
+ */
194
+ function updateSpotlightWindow(
195
+ conversationId: string,
196
+ turnIndex: number,
197
+ entries: SpotlightEntry[],
198
+ windowTurns: number,
199
+ ): SpotlightEntry[] {
200
+ const prior = (spotlightRings.get(conversationId) ?? []).filter(
201
+ (t) => t.turnIndex < turnIndex && t.turnIndex >= turnIndex - windowTurns,
202
+ );
203
+ lruSet(spotlightRings, conversationId, [...prior, { turnIndex, entries }]);
204
+
205
+ const window: SpotlightEntry[] = [];
206
+ const seen = new Set<string>();
207
+ // Newest first: this turn's entries, then prior turns newest → oldest.
208
+ for (const turn of [{ turnIndex, entries }, ...prior.reverse()]) {
209
+ for (const entry of turn.entries) {
210
+ const key = `${entry.slug}§${entry.title}`;
211
+ if (seen.has(key)) continue;
212
+ seen.add(key);
213
+ window.push(entry);
214
+ }
215
+ }
216
+ return window;
217
+ }
218
+
219
+ /** Test-only reset for the per-turn memo and the spotlight rings. */
220
+ export function resetMemoryV3InjectorStateForTests(): void {
221
+ observedTurns.clear();
222
+ spotlightRings.clear();
223
+ }
224
+
225
+ // ─── injectors ───────────────────────────────────────────────────────────────
226
+
36
227
  export const memoryV3Injector: Injector = {
37
228
  name: "memory-v3-shadow",
38
229
  // High order so it sorts last; the live `<memory>` block uses the
@@ -41,26 +232,97 @@ export const memoryV3Injector: Injector = {
41
232
  order: 1000,
42
233
  async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
43
234
  const config = getConfig();
235
+ if (config.memory.enabled === false) return null;
44
236
  const live = isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config);
45
237
  const shadow = isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config);
46
238
  if (!live && !shadow) return null;
239
+ if (!isPersonalMemoryAllowed(ctx.trust)) return null;
47
240
 
48
- const result = await observeTurn(ctx.conversationId, ctx.turnIndex);
49
- if (!live || !result) return null;
241
+ const result = await observeTurnOnce(ctx.conversationId, ctx.turnIndex);
242
+ // Empty selection falls back to v2 (return null): v2 suppression keys off
243
+ // BOTH the flag AND a produced block, so a selector failure or a turn with
244
+ // nothing selected ships v2 memory rather than no memory.
245
+ if (!result || result.selections.length === 0) return null;
50
246
 
51
247
  try {
52
- // `renderMemoryBlock` returns "" for an empty selection; inject nothing.
53
- const text = await renderMemoryBlock(
54
- result.finalInjection,
55
- result.sectionBySlug,
56
- renderV3SectionContent,
57
- );
58
- if (text.length === 0) return null;
248
+ const active = getActiveSlugs(ctx.conversationId);
249
+ const netNew = result.selections
250
+ .map((s) => s.slug)
251
+ .filter((slug) => !active.has(slug));
252
+
253
+ // Render net-new cards, skipping slugs that resolve to no content
254
+ // (deleted pages, unresolvable capabilities) — nothing is attached for
255
+ // them, so nothing is recorded either.
256
+ const cards: Array<{ slug: Slug; card: string }> = [];
257
+ for (const slug of netNew) {
258
+ const card = await renderV3CardContent(slug);
259
+ if (card.trim().length > 0) cards.push({ slug, card });
260
+ }
261
+ // Every net-new card rendered empty: return null (fall back to v2)
262
+ // rather than an empty-text block — suppressing v2 with nothing to show
263
+ // would ship a memory-less turn. Distinct from the all-repeat case
264
+ // (empty `netNew`), where the empty block correctly keeps v2 suppressed
265
+ // because the cards already ride history.
266
+ if (netNew.length > 0 && cards.length === 0) return null;
267
+ const entries = cards.map(({ slug, card }) => ({
268
+ slug,
269
+ // Capability cards (skills / CLI commands) render with their own
270
+ // `# Skill:` / `# CLI command:` headers, which the prune valve's
271
+ // `# memory/concepts/<slug>.md` section grammar can never locate to
272
+ // free. Record them at zero bytes so they never inflate the freeable
273
+ // resident accounting (the valve would otherwise loop-fire on bytes
274
+ // it cannot free).
275
+ bytes: isCapabilitySlug(slug) ? 0 : cardBytes(card),
276
+ }));
277
+
278
+ if (!live) {
279
+ // Shadow mode: log what WOULD inject, attach and record nothing.
280
+ const spotlightRefs = computeSpotlightEntries(
281
+ result,
282
+ config.memory.v3.spotlight.n,
283
+ ).map((e) => `${e.slug}§${e.title}`);
284
+ log.info(
285
+ {
286
+ conversationId: ctx.conversationId,
287
+ turnIndex: ctx.turnIndex,
288
+ netNew: entries,
289
+ netNewBytes: entries.reduce((sum, e) => sum + e.bytes, 0),
290
+ spotlightRefs,
291
+ },
292
+ "memory-v3 shadow: would inject net-new cards + spotlight",
293
+ );
294
+ return null;
295
+ }
296
+
297
+ // The everInjected store write and the prune-valve schedule are
298
+ // DEFERRED to this commit callback, invoked by runtime assembly at the
299
+ // point where attachment is guaranteed (the turn's tail is a user
300
+ // message — the same gate as metadata capture). Recording here in
301
+ // `produce()` would let a never-attached turn (non-user tail) claim
302
+ // cards in the store, suppressing them until compaction. The valve is
303
+ // scheduled after `recordInjected` so the resident accounting includes
304
+ // this turn's cards; core/hot lane members are exempt — the selector's
305
+ // stable prefix must never be pruned out from under it.
306
+ const commit = (): void => {
307
+ recordInjected(ctx.conversationId, entries);
308
+ schedulePruneValve(ctx.conversationId, {
309
+ exemptSlugs: new Set<Slug>([
310
+ ...result.lanes.core,
311
+ ...result.lanes.hot,
312
+ ]),
313
+ });
314
+ };
315
+
316
+ // Empty net-new → empty-text block: assembly attaches no content
317
+ // (`applyInjectionBlock` no-ops empty text) but the block's presence
318
+ // still marks v3 as this turn's `<memory>` source for v2 suppression.
319
+ const inner = renderCardsBlockInner(cards.map((c) => c.card));
59
320
  return {
60
321
  id: MEMORY_V3_BLOCK_ID,
61
- text,
322
+ text: inner.length === 0 ? "" : wrapMemoryBlock(inner),
62
323
  // Mirror v2's dynamic `<memory>` block placement.
63
324
  placement: "after-memory-prefix",
325
+ meta: { [MEMORY_V3_COMMIT_META_KEY]: commit },
64
326
  };
65
327
  } catch (err) {
66
328
  log.warn(
@@ -74,3 +336,48 @@ export const memoryV3Injector: Injector = {
74
336
  }
75
337
  },
76
338
  };
339
+
340
+ export const memoryV3SpotlightInjector: Injector = {
341
+ name: "memory-v3-spotlight",
342
+ // After the cards injector so the shared memo is (usually) already primed.
343
+ order: 1001,
344
+ async produce(ctx: TurnContext): Promise<InjectionBlock | null> {
345
+ const config = getConfig();
346
+ if (config.memory.enabled === false) return null;
347
+ // Live-only: shadow mode logs spotlight refs from the cards injector and
348
+ // must keep the turn untouched (no ring state either, so a later
349
+ // live-flag flip starts from a clean window).
350
+ if (!isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config)) return null;
351
+ if (!isPersonalMemoryAllowed(ctx.trust)) return null;
352
+
353
+ try {
354
+ const result = await observeTurnOnce(ctx.conversationId, ctx.turnIndex);
355
+ if (!result || result.selections.length === 0) return null;
356
+
357
+ const { n, windowTurns } = config.memory.v3.spotlight;
358
+ const current = computeSpotlightEntries(result, n);
359
+ const window = updateSpotlightWindow(
360
+ ctx.conversationId,
361
+ ctx.turnIndex,
362
+ current,
363
+ windowTurns,
364
+ );
365
+ if (window.length === 0) return null;
366
+
367
+ return {
368
+ id: MEMORY_V3_SPOTLIGHT_BLOCK_ID,
369
+ text: wrapMemorySpotlightBlock(renderSpotlightInner(window)),
370
+ placement: "append-user-tail",
371
+ };
372
+ } catch (err) {
373
+ log.warn(
374
+ {
375
+ err: err instanceof Error ? err.message : String(err),
376
+ conversationId: ctx.conversationId,
377
+ },
378
+ "memory-v3 spotlight render failed (non-fatal) — skipping spotlight",
379
+ );
380
+ return null;
381
+ }
382
+ },
383
+ };
@@ -2,7 +2,7 @@
2
2
  * Memory v3 — `memory_v3_maintain` job handler.
3
3
  *
4
4
  * A flag-gated, best-effort self-maintenance pass over the v3 section dense
5
- * store and the in-memory lanes. It runs three independent stages, in order:
5
+ * store and the in-memory lanes. It runs four independent stages, in order:
6
6
  *
7
7
  * 1. **Section re-embed** — diff the page index by `modifiedAt` against the
8
8
  * last successful pass (the high-water mark below), and for every page that
@@ -22,7 +22,12 @@
22
22
  * (it only names live pages), so without this its section points would
23
23
  * linger in Qdrant and the dense lane could still surface the deleted page.
24
24
  * Synthetic capability rows are in the page index, so they are never pruned.
25
- * 3. **Lane invalidation** — `invalidateLanes()` so the next turn rebuilds the
25
+ * 3. **Core-set validation** — load the maintainer-curated core set
26
+ * (`memory/core-pages.md`) and report entries whose page no longer exists
27
+ * in the page index (dangling slugs) via the log + outcome. The file is
28
+ * maintainer-owned, so this stage NEVER edits it — the maintainer fixes
29
+ * dangling entries at the next consolidation pass.
30
+ * 4. **Lane invalidation** — `invalidateLanes()` so the next turn rebuilds the
26
31
  * in-memory section index, needle, and edge graph from the freshly-updated
27
32
  * pages.
28
33
  *
@@ -35,8 +40,8 @@
35
40
  *
36
41
  * Dependency-injectable: `deps` lets tests substitute the page-index reader,
37
42
  * section builder, dense-store ops (including the prune-stage
38
- * `listSectionArticles`/`listIndexedSlugs` collaborators), and `invalidateLanes`
39
- * without process-global module mocks.
43
+ * `listSectionArticles`/`listIndexedSlugs` collaborators), the core-set loader,
44
+ * and `invalidateLanes` without process-global module mocks.
40
45
  */
41
46
 
42
47
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
@@ -50,7 +55,8 @@ import { getPageIndex } from "../../../memory/v2/page-index.js";
50
55
  import { readPage } from "../../../memory/v2/page-store.js";
51
56
  import { getLogger } from "../../../util/logger.js";
52
57
  import { getWorkspaceDir } from "../../../util/platform.js";
53
- import { capabilityOrDiskBody } from "./capabilities.js";
58
+ import { capabilityOrDiskBody, isCapabilitySlug } from "./capabilities.js";
59
+ import { loadCoreSet as realLoadCoreSet } from "./core-set.js";
54
60
  import {
55
61
  deleteSectionsForArticle as realDeleteSectionsForArticle,
56
62
  ensureSectionCollection as realEnsureSectionCollection,
@@ -126,6 +132,12 @@ export interface MaintainJobDeps {
126
132
  * from this set. Includes synthetic capability rows so they are never pruned.
127
133
  */
128
134
  listIndexedSlugs: () => Promise<Slug[]>;
135
+ /**
136
+ * The maintainer-curated core set from `memory/core-pages.md`. The validation
137
+ * stage diffs it against the live page-index slugs and REPORTS dangling
138
+ * entries only — the file is maintainer-owned and is never edited here.
139
+ */
140
+ loadCoreSet: () => Slug[];
129
141
  /** Drop the memoized v3 lanes so the next turn rebuilds them. */
130
142
  invalidateLanes: () => void;
131
143
  /** Active assistant config (for the dense-store/embedding calls). */
@@ -140,7 +152,12 @@ export interface MaintainJobDeps {
140
152
  * collection-ensure step rather than the change-delta machinery.
141
153
  */
142
154
  export interface BackfillJobDeps {
143
- /** All page-index slugs, including synthetic skill/CLI capability rows. */
155
+ /**
156
+ * All page-index slugs, including synthetic skill/CLI capability rows.
157
+ * Called once at the start and once after the main loop — the second call
158
+ * catches capability rows whose startup seed had not yet listed them in the
159
+ * index when the first snapshot was taken.
160
+ */
144
161
  selectAllPages: () => Promise<Slug[]>;
145
162
  /** Create the section collection if absent before the first upsert. */
146
163
  ensureSectionCollection: typeof realEnsureSectionCollection;
@@ -188,6 +205,12 @@ export interface MaintainOutcome {
188
205
  pruned: number;
189
206
  /** Prune deletes that threw (and were contained). */
190
207
  pruneFailures: number;
208
+ /**
209
+ * Core-set entries (`memory/core-pages.md`) whose page no longer exists in
210
+ * the page index. Reported for the maintainer to fix at the next
211
+ * consolidation pass — the file itself is never mutated.
212
+ */
213
+ danglingCoreSlugs: Slug[];
191
214
  /** Whether the in-memory lanes were invalidated. */
192
215
  invalidated: boolean;
193
216
  /** Stages that threw (and were contained). */
@@ -284,6 +307,7 @@ function defaultDeps(config: AssistantConfig): MaintainJobDeps {
284
307
  commitEmbedHighWater,
285
308
  listSectionArticles: () => realListSectionArticles(config),
286
309
  listIndexedSlugs: () => selectAllPagesFromWorkspace(workspaceDir),
310
+ loadCoreSet: () => realLoadCoreSet(workspaceDir),
287
311
  invalidateLanes: realInvalidateLanes,
288
312
  config,
289
313
  };
@@ -393,6 +417,22 @@ async function pruneDeletedPages(
393
417
  * checkpoint is held (not advanced): a failed page was delete-then-upsert'd and
394
418
  * so has no sections, and advancing past its mtime would hide it from the
395
419
  * incremental selector forever — holding the mark lets the next pass re-embed it.
420
+ *
421
+ * Capability rows get an extra guard. Their stores (skill/CLI caches) are
422
+ * seeded fire-and-forget at daemon startup, so a backfill fired shortly after a
423
+ * restart can race the seed in two ways: (a) the rows are missing from the page
424
+ * index entirely when `selectAllPages` snapshots it, so they are silently
425
+ * absent from the work list; (b) the rows are listed but their content cache is
426
+ * still cold, so they render `""` — and the section chunker synthesizes a junk
427
+ * one-line head section from the bare slug, which would be embedded as if it
428
+ * were real content. Both end with capabilities effectively missing from the
429
+ * dense lane while the pass reports zero failures. So: a capability row whose
430
+ * body resolves empty is skipped without deleting its existing points (never
431
+ * replace good points with a blank), the page index is re-enumerated after the
432
+ * main loop (which spans long enough for the startup seed to finish) to pick up
433
+ * late-appearing capability rows, cold rows are retried, and any capability row
434
+ * that still resolves empty counts as a failure — holding the checkpoint and
435
+ * surfacing in the outcome instead of silently succeeding.
396
436
  */
397
437
  export async function backfillAllSections(
398
438
  config: AssistantConfig,
@@ -407,29 +447,65 @@ export async function backfillAllSections(
407
447
  let articles = 0;
408
448
  let sections = 0;
409
449
  let failures = 0;
410
- for (const slug of slugs) {
450
+
451
+ // "cold" is only reported for capability rows whose body resolves empty —
452
+ // their store has not seeded yet. Real pages never report cold: an empty
453
+ // on-disk body legitimately replaces the page's sections.
454
+ const embedOne = async (
455
+ slug: Slug,
456
+ ): Promise<"embedded" | "cold" | "failed"> => {
411
457
  try {
458
+ if (isCapabilitySlug(slug)) {
459
+ const body = await deps.readPageBody(slug);
460
+ if (body.trim().length === 0) return "cold";
461
+ }
412
462
  const index = await deps.buildSectionIndex([slug], deps.readPageBody);
413
463
  await deps.deleteSectionsForArticle(deps.config, slug);
414
464
  await deps.upsertSections(deps.config, index.sections);
415
465
  articles += 1;
416
466
  sections += index.sections.length;
467
+ return "embedded";
417
468
  } catch (err) {
418
469
  failures += 1;
419
470
  log.warn(
420
471
  { slug, err: err instanceof Error ? err.message : String(err) },
421
472
  "memory-v3 backfill: page embed failed (non-fatal)",
422
473
  );
474
+ return "failed";
475
+ }
476
+ };
477
+
478
+ const coldCapabilities: Slug[] = [];
479
+ for (const slug of slugs) {
480
+ if ((await embedOne(slug)) === "cold") coldCapabilities.push(slug);
481
+ }
482
+
483
+ // Late-capability sweep + cold retry. The main loop spans minutes on a real
484
+ // corpus, so the startup-seeded capability stores have typically warmed by
485
+ // now: re-enumerate the index to catch capability rows that were not listed
486
+ // yet at snapshot time, and retry the ones that rendered cold. A row that
487
+ // still resolves empty is a real failure.
488
+ const processed = new Set(slugs);
489
+ const lateCapabilities = (await deps.selectAllPages()).filter(
490
+ (slug) => isCapabilitySlug(slug) && !processed.has(slug),
491
+ );
492
+ for (const slug of [...coldCapabilities, ...lateCapabilities]) {
493
+ if ((await embedOne(slug)) === "cold") {
494
+ failures += 1;
495
+ log.warn(
496
+ { slug },
497
+ "memory-v3 backfill: capability row still empty after retry (capability store not seeded?) — re-run backfill-sections once the store seeds",
498
+ );
423
499
  }
424
500
  }
425
501
 
426
502
  // Advance the maintain high-water mark only when every page embedded cleanly,
427
503
  // so the next incremental pass deltas from here rather than re-embedding
428
- // everything. A failed page was delete-then-upsert'd, so it now has no
429
- // sections; advancing past its mtime would hide it from `computeChangedPages`
430
- // forever. Holding the checkpoint keeps failed pages above the mark so the
431
- // next pass re-embeds them — re-embedding the succeeded pages too is
432
- // idempotent and cheap.
504
+ // everything. A thrown failure was delete-then-upsert'd (it now has no
505
+ // sections) and an empty capability row was never embedded at all; advancing
506
+ // past them would hide them from `computeChangedPages` forever. Holding the
507
+ // checkpoint keeps failed pages above the mark so the next pass re-embeds
508
+ // them — re-embedding the succeeded pages too is idempotent and cheap.
433
509
  if (failures === 0) {
434
510
  deps.commitEmbedHighWater(startedAtMs);
435
511
  } else {
@@ -463,6 +539,7 @@ export async function maintainJob(
463
539
  reembedFailures: 0,
464
540
  pruned: 0,
465
541
  pruneFailures: 0,
542
+ danglingCoreSlugs: [],
466
543
  invalidated: false,
467
544
  failures: [],
468
545
  };
@@ -527,7 +604,35 @@ export async function maintainJob(
527
604
  );
528
605
  }
529
606
 
530
- // Stage 3: rebuild section index + needle + edge graph on the next turn.
607
+ // Stage 3: validate the maintainer-curated core set. Diff `core-pages.md`
608
+ // entries against the live page-index slugs and REPORT dangling entries
609
+ // (pages that were renamed or deleted) through the log + outcome — the file
610
+ // is maintainer-owned, so it is never auto-edited; the maintainer fixes it at
611
+ // the next consolidation pass. No hot/core recompute is needed in this job:
612
+ // the lanes (including the core and hot prefixes) are computed at lane init,
613
+ // and the invalidateLanes() stage below already forces a next-turn rebuild —
614
+ // that rebuild IS the consolidation-cadence refresh.
615
+ try {
616
+ const coreSlugs = deps.loadCoreSet();
617
+ if (coreSlugs.length > 0) {
618
+ const live = new Set(await deps.listIndexedSlugs());
619
+ outcome.danglingCoreSlugs = coreSlugs.filter((slug) => !live.has(slug));
620
+ if (outcome.danglingCoreSlugs.length > 0) {
621
+ log.warn(
622
+ { dangling: outcome.danglingCoreSlugs },
623
+ "memory-v3 maintain: core-pages.md lists pages missing from the index — review the file at the next consolidation",
624
+ );
625
+ }
626
+ }
627
+ } catch (err) {
628
+ outcome.failures.push("core-validate");
629
+ log.warn(
630
+ { err: err instanceof Error ? err.message : String(err) },
631
+ "memory-v3 maintain: core-set validation failed (non-fatal)",
632
+ );
633
+ }
634
+
635
+ // Stage 4: rebuild section index + needle + edge graph on the next turn.
531
636
  try {
532
637
  deps.invalidateLanes();
533
638
  outcome.invalidated = true;
@@ -545,6 +650,7 @@ export async function maintainJob(
545
650
  reembedFailures: outcome.reembedFailures,
546
651
  pruned: outcome.pruned,
547
652
  pruneFailures: outcome.pruneFailures,
653
+ danglingCoreSlugs: outcome.danglingCoreSlugs,
548
654
  invalidated: outcome.invalidated,
549
655
  failures: outcome.failures,
550
656
  },