@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
@@ -4,23 +4,30 @@
4
4
  * SCOPE / ALTITUDE. A full daemon-assembly integration (plugin registry → flag
5
5
  * read → runtime assembly → provider call) is too heavy and too mock-fragile
6
6
  * for a unit test. Instead this composes the REAL v3 live-path units with a
7
- * mocked select provider + stubbed dense lane and synthetic fixtures:
7
+ * mocked select provider + stubbed dense lane, synthetic fixtures, and a real
8
+ * in-memory everInjected store:
8
9
  *
9
- * orchestrate (needle ∪ dense ∪ edge → selectPool over a shared WorkingSet)
10
- * → renderMemoryBlock (the rendered `<memory>` working-set block)
11
- * → stripAllMemoryInjections (all-turns history strip)
10
+ * orchestrate (cache-ordered pool → ONE selectPool call → this turn's
11
+ * selections)
12
+ * → net-new filter against the everInjected store (`getActiveSlugs`)
13
+ * → renderCard + renderCardsBlockInner (the frozen card block)
14
+ * → recordInjected
12
15
  *
13
- * That is exactly the behavioral contract the live path wires together: the
14
- * plugin's `produce()` renders `orchestrate(...).finalInjection` via
15
- * `renderMemoryBlock`, and assembly strips `<memory>` from every historical user
16
- * message so exactly one block exists. Driving these real units across turns
17
- * exercises carry-forward, eviction, single-source, and strip-all end-to-end at
18
- * the v3 layer without the daemon. The provider is stubbed (no network).
16
+ * That is the behavioral contract the live path wires together: the injector
17
+ * renders only the turn's NET-NEW selections as cards, records them, and the
18
+ * resulting block is FROZEN into history — prior turns' blocks are never
19
+ * re-rendered or stripped (the cache contract; the old `stripAllMemoryInjections`
20
+ * whole-layer replace is gone). The provider is stubbed (no network).
19
21
  */
20
22
 
23
+ import { Database } from "bun:sqlite";
21
24
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
22
25
 
23
- import { stripAllMemoryInjections } from "../../../../memory/graph/conversation-graph-memory.js";
26
+ import { drizzle } from "drizzle-orm/bun-sqlite";
27
+
28
+ import { wrapMemoryBlock } from "../../../../memory/memory-marker.js";
29
+ import { migrateAddMemoryV3EverInjected } from "../../../../memory/migrations/277-add-memory-v3-ever-injected.js";
30
+ import * as schema from "../../../../memory/schema.js";
24
31
  import type { PageIndexEntry } from "../../../../memory/v2/page-index.js";
25
32
  import type {
26
33
  ContentBlock,
@@ -28,6 +35,7 @@ import type {
28
35
  Provider,
29
36
  ProviderResponse,
30
37
  } from "../../../../providers/types.js";
38
+ import { cardBytes, renderCard } from "../card.js";
31
39
  import type { EdgeGraph } from "../edge.js";
32
40
  import { buildEdgeGraph } from "../edge.js";
33
41
  import { buildSectionNeedle } from "../section-needle.js";
@@ -65,9 +73,35 @@ mock.module("../dense.js", () => ({
65
73
  denseMockActive ? [] : realDense.denseLane(...args),
66
74
  }));
67
75
 
76
+ // In-memory everInjected store backing the net-new dedup, swapped in only
77
+ // while this file's tests run.
78
+ const realDbConnection = {
79
+ ...(await import("../../../../memory/db-connection.js")),
80
+ };
81
+ let testSqlite: Database;
82
+ let testDb = makeDb();
83
+ function makeDb() {
84
+ testSqlite = new Database(":memory:");
85
+ const db = drizzle(testSqlite, { schema });
86
+ migrateAddMemoryV3EverInjected(db);
87
+ return db;
88
+ }
89
+ mock.module("../../../../memory/db-connection.js", () => ({
90
+ ...realDbConnection,
91
+ getDb: () => (denseMockActive ? testDb : realDbConnection.getDb()),
92
+ getSqliteFrom: (db: unknown) =>
93
+ denseMockActive
94
+ ? testSqlite
95
+ : realDbConnection.getSqliteFrom(
96
+ db as Parameters<typeof realDbConnection.getSqliteFrom>[0],
97
+ ),
98
+ }));
99
+
68
100
  const { orchestrate } = await import("../orchestrate.js");
69
- const { WorkingSet } = await import("../working-set.js");
70
- const { renderMemoryBlock } = await import("../render-injection.js");
101
+ const { renderCardsBlockInner, V3_CARDS_INJECTION_HEADER } =
102
+ await import("../render-injection.js");
103
+ const { getActiveSlugs, recordInjected, residentBytes } =
104
+ await import("../ever-injected-store.js");
71
105
 
72
106
  // ---------------------------------------------------------------------------
73
107
  // Fixtures: a tiny corpus whose section text contains a distinctive term per
@@ -93,7 +127,6 @@ function makeEntries(): PageIndexEntry[] {
93
127
  }
94
128
 
95
129
  const config = {} as never;
96
- const contentOf = async (slug: Slug): Promise<string> => `body for ${slug}`;
97
130
 
98
131
  async function buildLanes(): Promise<{
99
132
  sectionIndex: SectionIndex;
@@ -130,20 +163,39 @@ function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
130
163
  };
131
164
  }
132
165
 
133
- /** Parse the numbered `<candidates>` block into an ordered slug list. */
166
+ /**
167
+ * Parse the two-segment selector input into the globally-numbered pool slug
168
+ * list: stable-prefix cards (`<candidate_cards>`, identified by their
169
+ * `[i] # memory/concepts/<slug>.md` header line) then finder lines
170
+ * (`<candidates>`, `[i] slug — descriptor`).
171
+ */
134
172
  function candidateSlugs(messages: Message[]): Slug[] {
173
+ const entries: Array<{ id: number; slug: string }> = [];
135
174
  for (const msg of messages) {
136
175
  for (const block of msg.content) {
137
176
  if (block.type !== "text") continue;
138
- const m = /<candidates>\n([\s\S]*?)\n<\/candidates>/.exec(block.text);
139
- if (!m) continue;
140
- return m[1]
141
- .split("\n")
142
- .map((line) => /^\[\d+\] (\S+) —/.exec(line)?.[1])
143
- .filter((s): s is string => !!s);
177
+ const cards = /<candidate_cards>\n([\s\S]*?)\n<\/candidate_cards>/.exec(
178
+ block.text,
179
+ );
180
+ if (cards) {
181
+ for (const m of cards[1].matchAll(
182
+ /^\[(\d+)\] # memory\/concepts\/(.+)\.md$/gm,
183
+ )) {
184
+ entries.push({ id: Number(m[1]), slug: m[2]! });
185
+ }
186
+ }
187
+ const finder = /<candidates>\n([\s\S]*?)\n<\/candidates>/.exec(
188
+ block.text,
189
+ );
190
+ if (finder) {
191
+ for (const line of finder[1].split("\n")) {
192
+ const m = /^\[(\d+)\] (\S+)(?: — |$)/.exec(line);
193
+ if (m) entries.push({ id: Number(m[1]), slug: m[2]! });
194
+ }
195
+ }
144
196
  }
145
197
  }
146
- return [];
198
+ return entries.sort((a, b) => a.id - b.id).map((e) => e.slug);
147
199
  }
148
200
 
149
201
  /** Provider that selects (and optionally pins) the pooled candidates in `keep`. */
@@ -163,41 +215,45 @@ function selectProvider(keep: Slug[], pin: Slug[] = []): Provider {
163
215
  };
164
216
  }
165
217
 
166
- /** Run one turn through orchestrate over the shared working set + render it. */
218
+ /**
219
+ * Run one turn through the live composition: orchestrate → net-new filter
220
+ * against the store → card render → record. Returns the wrapped block ("" when
221
+ * the turn had no net-new cards) — the same shape the injector attaches.
222
+ */
167
223
  async function runTurn(
224
+ conversationId: string,
168
225
  turnNumber: number,
169
226
  query: string,
170
227
  keep: Slug[],
171
- pin: Slug[],
172
- deps: {
173
- lanes: Awaited<ReturnType<typeof buildLanes>>;
174
- workingSet: InstanceType<typeof WorkingSet>;
175
- },
176
- ) {
177
- providerStub = selectProvider(keep, pin);
228
+ deps: { lanes: Awaited<ReturnType<typeof buildLanes>> },
229
+ ): Promise<{ block: string; netNew: Slug[] }> {
230
+ providerStub = selectProvider(keep);
178
231
  const result = await orchestrate(makeTurn(turnNumber, query), {
179
232
  sectionIndex: deps.lanes.sectionIndex,
180
233
  needle: deps.lanes.needle,
181
234
  denseConfig: config,
182
235
  edgeGraph: deps.lanes.edgeGraph,
183
- workingSet: deps.workingSet,
236
+ coreSlugs: [],
237
+ hotSlugs: [],
238
+ prefixCards: new Map(),
184
239
  });
185
- const block = await renderMemoryBlock(
186
- result.finalInjection,
187
- result.sectionBySlug,
188
- contentOf,
240
+ const active = getActiveSlugs(conversationId);
241
+ const netNew = result.selections
242
+ .map((s) => s.slug)
243
+ .filter((slug) => !active.has(slug));
244
+ const cards = netNew.map((slug) => renderCard(slug, PAGES[slug]!));
245
+ recordInjected(
246
+ conversationId,
247
+ cards.map((card, i) => ({ slug: netNew[i]!, bytes: cardBytes(card) })),
189
248
  );
190
- return { result, block };
191
- }
192
-
193
- /** Count `<memory>\n…\n</memory>` blocks in a rendered string. */
194
- function countMemoryBlocks(text: string): number {
195
- return (text.match(/<memory>\n/g) ?? []).length;
249
+ const inner = renderCardsBlockInner(cards);
250
+ return { block: inner.length === 0 ? "" : wrapMemoryBlock(inner), netNew };
196
251
  }
197
252
 
198
253
  beforeEach(() => {
199
254
  denseMockActive = true;
200
255
  providerStub = null;
256
+ testDb = makeDb();
201
257
  });
202
258
 
203
259
  afterAll(() => {
@@ -205,139 +261,104 @@ afterAll(() => {
205
261
  });
206
262
 
207
263
  // ---------------------------------------------------------------------------
208
- // Carry-forward: a page pinned in turn 1 appears in turn 2's injected block
209
- // WITHOUT being re-selected in turn 2 (same WorkingSet reused across turns).
264
+ // Net-new accumulation: each turn renders ONLY pages not already frozen into
265
+ // history; an all-repeat turn renders nothing new.
210
266
  // ---------------------------------------------------------------------------
211
267
 
212
- describe("memory-v3 live — carry-forward across turns", () => {
213
- test("a page pinned in turn 1 is injected in turn 2 without re-selection", async () => {
268
+ describe("memory-v3 live — net-new card accumulation", () => {
269
+ test("turn 2 re-selecting turn 1's page renders zero new cards", async () => {
214
270
  const lanes = await buildLanes();
215
- const workingSet = new WorkingSet();
216
271
 
217
- const t1 = await runTurn(1, "apple", ["page-a"], ["page-a"], {
218
- lanes,
219
- workingSet,
220
- });
221
- expect(t1.result.currentSelections.map((s) => s.slug)).toContain("page-a");
222
- expect(t1.block).toContain("body for page-a");
272
+ const t1 = await runTurn("conv-1", 1, "apple", ["page-a"], { lanes });
273
+ expect(t1.netNew).toEqual(["page-a"]);
274
+ expect(t1.block).toContain("# memory/concepts/page-a.md");
275
+ expect(t1.block).toContain("lead a");
223
276
 
224
- const t2 = await runTurn(2, "cherry", ["page-c"], [], {
225
- lanes,
226
- workingSet,
227
- });
228
- expect(t2.result.currentSelections.map((s) => s.slug)).not.toContain(
229
- "page-a",
230
- );
231
- expect(t2.result.finalInjection).toContain("page-a");
232
- expect(t2.block).toContain("body for page-a");
277
+ // All-repeat turn: no new persistent bytes.
278
+ const t2 = await runTurn("conv-1", 2, "apple", ["page-a"], { lanes });
279
+ expect(t2.netNew).toEqual([]);
280
+ expect(t2.block).toBe("");
281
+ expect(residentBytes("conv-1")).toBeGreaterThan(0);
233
282
  });
234
- });
235
283
 
236
- // ---------------------------------------------------------------------------
237
- // Eviction reflected: a non-pinned page selected only early ages past the
238
- // eviction window and drops out of a later turn's injected block.
239
- // ---------------------------------------------------------------------------
240
-
241
- describe("memory-v3 live — eviction reflected in the injected block", () => {
242
- test("a stale non-pinned page drops out; the pinned page persists", async () => {
284
+ test("a topic shift renders only the newly-selected page's card", async () => {
243
285
  const lanes = await buildLanes();
244
- // Small window: a non-pinned entry unseen for >2 turns evicts. page-b is
245
- // selected only in turn 1, so by turn 4 (4-1=3 > 2) it ages out; pinned
246
- // page-a never evicts; page-c is re-selected every later turn.
247
- const workingSet = new WorkingSet(150, 2);
248
-
249
- const t1 = await runTurn(
250
- 1,
251
- "apple banana",
252
- ["page-a", "page-b"],
253
- ["page-a"],
254
- { lanes, workingSet },
255
- );
256
- expect(t1.result.finalInjection).toContain("page-b");
257
- expect(t1.block).toContain("body for page-b");
258
-
259
- // Turns 2–3 keep page-b inside the window (3-1=2, not > 2); re-select page-c.
260
- await runTurn(2, "cherry", ["page-c"], [], { lanes, workingSet });
261
- const t3 = await runTurn(3, "cherry", ["page-c"], [], {
262
- lanes,
263
- workingSet,
264
- });
265
- expect(t3.result.finalInjection).toContain("page-b");
266
286
 
267
- // Turn 4: page-b is now stale (4-1=3 > 2) and must be gone from the block;
268
- // pinned page-a and freshly-selected page-c remain.
269
- const t4 = await runTurn(4, "cherry", ["page-c"], [], {
287
+ await runTurn("conv-1", 1, "apple", ["page-a"], { lanes });
288
+ const t2 = await runTurn("conv-1", 2, "cherry", ["page-a", "page-c"], {
270
289
  lanes,
271
- workingSet,
272
290
  });
273
- expect(t4.result.finalInjection).not.toContain("page-b");
274
- expect(t4.block).not.toContain("body for page-b");
275
- expect(t4.result.finalInjection).toContain("page-a");
276
- expect(t4.result.finalInjection).toContain("page-c");
291
+ expect(t2.netNew).toEqual(["page-c"]);
292
+ expect(t2.block).toContain("# memory/concepts/page-c.md");
293
+ expect(t2.block).not.toContain("# memory/concepts/page-a.md");
277
294
  });
278
- });
279
-
280
- // ---------------------------------------------------------------------------
281
- // Single source: orchestrate → render produces exactly one coherent `<memory>`
282
- // block per turn.
283
- // ---------------------------------------------------------------------------
284
295
 
285
- describe("memory-v3 live — single memory source", () => {
286
- test("each turn renders exactly one <memory> block", async () => {
296
+ test("each non-empty turn block is one <memory> block with the read-affordance header", async () => {
287
297
  const lanes = await buildLanes();
288
- const workingSet = new WorkingSet();
289
-
290
298
  const queries = ["apple", "banana", "cherry"];
291
299
  for (const [i, query] of queries.entries()) {
292
- const { block } = await runTurn(i + 1, query, [SLUGS[i]!], [], {
300
+ const { block } = await runTurn("conv-1", i + 1, query, [SLUGS[i]!], {
293
301
  lanes,
294
- workingSet,
295
302
  });
296
- expect(countMemoryBlocks(block)).toBe(1);
303
+ expect((block.match(/<memory>\n/g) ?? []).length).toBe(1);
297
304
  expect(block.startsWith("<memory>\n")).toBe(true);
298
305
  expect(block.endsWith("\n</memory>")).toBe(true);
306
+ expect(block).toContain(V3_CARDS_INJECTION_HEADER);
299
307
  }
300
308
  });
301
309
  });
302
310
 
303
311
  // ---------------------------------------------------------------------------
304
- // Strip-all: the rendered block injected into historical user messages is
305
- // stripped each turn, leaving byte-stable history (only the live block differs).
312
+ // Frozen history: blocks splice onto their own turn's user message and are
313
+ // NEVER touched on later turns — prior messages stay byte-identical (the
314
+ // cache contract that replaced the old all-turns strip).
306
315
  // ---------------------------------------------------------------------------
307
316
 
308
- describe("memory-v3 live — all-turns history strip", () => {
309
- test("historical <memory> blocks strip back to byte-stable user history", async () => {
317
+ describe("memory-v3 live — frozen card blocks in history", () => {
318
+ test("prior turns' messages stay byte-identical as new turns inject", async () => {
310
319
  const lanes = await buildLanes();
311
- const workingSet = new WorkingSet();
312
-
313
- const baseHistory: Message[] = [
314
- { role: "user", content: [{ type: "text", text: "first user message" }] },
315
- { role: "assistant", content: [{ type: "text", text: "ok" }] },
316
- {
317
- role: "user",
318
- content: [{ type: "text", text: "second user message" }],
319
- },
320
- { role: "assistant", content: [{ type: "text", text: "sure" }] },
321
- { role: "user", content: [{ type: "text", text: "third user message" }] },
322
- ];
323
-
324
320
  const memBlock = (text: string): ContentBlock => ({ type: "text", text });
321
+
322
+ const history: Message[] = [];
323
+ const snapshots: string[] = [];
325
324
  const queries = ["apple", "banana", "cherry"];
326
- let injected: Message[] = baseHistory;
327
325
  for (const [i, query] of queries.entries()) {
328
- const { block } = await runTurn(i + 1, query, [SLUGS[i]!], [], {
326
+ history.push({
327
+ role: "user",
328
+ content: [{ type: "text", text: `user message ${i + 1}` }],
329
+ });
330
+ const { block } = await runTurn("conv-1", i + 1, query, [SLUGS[i]!], {
329
331
  lanes,
330
- workingSet,
331
332
  });
332
- const stripped = stripAllMemoryInjections(injected);
333
- injected = stripped.map((m) =>
334
- m.role === "user"
335
- ? { ...m, content: [memBlock(block), ...m.content] }
336
- : m,
337
- );
333
+ // The injector splices the block onto the CURRENT tail only; prior
334
+ // messages are never revisited.
335
+ if (block.length > 0) {
336
+ const tail = history[history.length - 1]!;
337
+ tail.content = [memBlock(block), ...tail.content];
338
+ }
339
+ snapshots.push(JSON.stringify(history.slice(0, -1)));
340
+ history.push({
341
+ role: "assistant",
342
+ content: [{ type: "text", text: "ok" }],
343
+ });
344
+ }
345
+
346
+ // Every snapshot of the pre-tail history is a byte-prefix of the final
347
+ // history: nothing a later turn did rewrote an earlier message.
348
+ const finalJson = JSON.stringify(history);
349
+ for (const snapshot of snapshots) {
350
+ expect(finalJson.startsWith(snapshot.slice(0, -1))).toBe(true);
338
351
  }
339
352
 
340
- const finalStripped = stripAllMemoryInjections(injected);
341
- expect(finalStripped).toEqual(baseHistory);
353
+ // Accumulation: all three turns' cards are present, one block per turn.
354
+ const allText = history
355
+ .flatMap((m) => m.content)
356
+ .filter((b): b is { type: "text"; text: string } => b.type === "text")
357
+ .map((b) => b.text)
358
+ .join("\n");
359
+ expect((allText.match(/<memory>\n/g) ?? []).length).toBe(3);
360
+ for (const slug of SLUGS) {
361
+ expect(allText).toContain(`# memory/concepts/${slug}.md`);
362
+ }
342
363
  });
343
364
  });
@@ -84,6 +84,9 @@ describe("maintainJob", () => {
84
84
  // dedicated prune tests below override both collaborators.
85
85
  listSectionArticles: async () => [],
86
86
  listIndexedSlugs: async () => [],
87
+ // Core-validation stage off by default: empty core set ⇒ nothing to
88
+ // check. The dedicated core tests below override this.
89
+ loadCoreSet: () => [],
87
90
  invalidateLanes: () => {
88
91
  calls.invalidate += 1;
89
92
  },
@@ -271,6 +274,68 @@ describe("maintainJob", () => {
271
274
  expect(calls.invalidate).toBe(1);
272
275
  });
273
276
 
277
+ test("reports dangling core entries without mutating anything", async () => {
278
+ setOverridesForTesting({ [FLAG_SHADOW]: true });
279
+ // The core file lists a live page, a renamed/deleted page, and a synthetic
280
+ // capability slug; only the missing page is reported. The stage is
281
+ // report-only: no deletes, no upserts, and the maintainer-owned file is
282
+ // untouched (the injected loader is read-only by construction).
283
+ const { deps: d, calls } = deps({
284
+ loadCoreSet: () => ["page-live", "page-gone", "skills/example"],
285
+ listIndexedSlugs: async () => ["page-live", "skills/example"],
286
+ });
287
+ const outcome = await maintainJob(JOB, CONFIG, d);
288
+
289
+ expect(outcome.danglingCoreSlugs).toEqual(["page-gone"]);
290
+ expect(outcome.failures).toEqual([]);
291
+ // Report-only: the dangling entry triggered no dense-store mutation.
292
+ expect(calls.deleted).toEqual([]);
293
+ expect(calls.upserted).toEqual([]);
294
+ // The pass still invalidates the lanes.
295
+ expect(calls.invalidate).toBe(1);
296
+ });
297
+
298
+ test("reports nothing when every core entry is still in the index", async () => {
299
+ setOverridesForTesting({ [FLAG_SHADOW]: true });
300
+ const { deps: d } = deps({
301
+ loadCoreSet: () => ["page-a", "page-b"],
302
+ listIndexedSlugs: async () => ["page-a", "page-b", "page-c"],
303
+ });
304
+ const outcome = await maintainJob(JOB, CONFIG, d);
305
+ expect(outcome.danglingCoreSlugs).toEqual([]);
306
+ });
307
+
308
+ test("an empty core set skips validation entirely", async () => {
309
+ setOverridesForTesting({ [FLAG_SHADOW]: true });
310
+ let indexReads = 0;
311
+ const { deps: d } = deps({
312
+ loadCoreSet: () => [],
313
+ listIndexedSlugs: async () => {
314
+ indexReads += 1;
315
+ return [];
316
+ },
317
+ });
318
+ const outcome = await maintainJob(JOB, CONFIG, d);
319
+ expect(outcome.danglingCoreSlugs).toEqual([]);
320
+ // The prune stage reads the index once; the core stage adds no second read.
321
+ expect(indexReads).toBe(1);
322
+ });
323
+
324
+ test("a thrown core-validation stage is contained and does not abort lane invalidation", async () => {
325
+ setOverridesForTesting({ [FLAG_SHADOW]: true });
326
+ const { deps: d, calls } = deps({
327
+ loadCoreSet: () => {
328
+ throw new Error("core boom");
329
+ },
330
+ });
331
+ const outcome = await maintainJob(JOB, CONFIG, d);
332
+
333
+ expect(outcome.failures).toContain("core-validate");
334
+ expect(outcome.danglingCoreSlugs).toEqual([]);
335
+ expect(calls.invalidate).toBe(1);
336
+ expect(outcome.invalidated).toBe(true);
337
+ });
338
+
274
339
  test("a thrown prune stage is contained and does not abort lane invalidation", async () => {
275
340
  setOverridesForTesting({ [FLAG_SHADOW]: true });
276
341
  const { deps: d, calls } = deps({
@@ -453,4 +518,99 @@ describe("backfillAllSections", () => {
453
518
  // next pass re-embed it.
454
519
  expect(calls.committed).toEqual([]);
455
520
  });
521
+
522
+ test("capability row that renders empty embeds on the post-loop retry once the store seeds", async () => {
523
+ // Models the startup race: the skill store is cold when the main loop
524
+ // reaches the capability row (renders ""), and has seeded by the time the
525
+ // retry pass runs.
526
+ let skillReads = 0;
527
+ const readPageBody = async (slug: Slug): Promise<string> => {
528
+ if (slug === "skills/example") {
529
+ skillReads += 1;
530
+ return skillReads === 1
531
+ ? ""
532
+ : "# Skill: example\nexample capability body";
533
+ }
534
+ return `body for ${slug}`;
535
+ };
536
+
537
+ const { deps: d, calls } = deps({
538
+ selectAllPages: async () => ["skills/example", "page-a"],
539
+ readPageBody,
540
+ });
541
+ const outcome = await backfillAllSections(CONFIG, d);
542
+
543
+ // The cold row was skipped without building on the first pass, then built
544
+ // and embedded on the retry.
545
+ expect(calls.built).toEqual([["page-a"], ["skills/example"]]);
546
+ // The cold render never deleted the row's existing points; the retry did.
547
+ expect(calls.deleted).toEqual(["page-a", "skills/example"]);
548
+ expect(
549
+ calls.upserted.flat().filter((s) => s.article === "skills/example")
550
+ .length,
551
+ ).toBeGreaterThan(0);
552
+ expect(outcome.articles).toBe(2);
553
+ expect(outcome.failures).toBe(0);
554
+ expect(calls.committed).toEqual([4242]);
555
+ });
556
+
557
+ test("capability row still empty after retry is a failure: never deleted, checkpoint HELD", async () => {
558
+ const { deps: d, calls } = deps({
559
+ selectAllPages: async () => ["page-a", "skills/example"],
560
+ readPageBody: async (slug) =>
561
+ slug === "skills/example" ? "" : `body for ${slug}`,
562
+ });
563
+ const outcome = await backfillAllSections(CONFIG, d);
564
+
565
+ // The empty render must never wipe previously-good points with nothing.
566
+ expect(calls.deleted).toEqual(["page-a"]);
567
+ expect(
568
+ calls.upserted.flat().filter((s) => s.article === "skills/example"),
569
+ ).toEqual([]);
570
+ expect(outcome.articles).toBe(1);
571
+ expect(outcome.failures).toBe(1);
572
+ expect(calls.committed).toEqual([]);
573
+ });
574
+
575
+ test("capability row missing from the first index snapshot is swept up after the main loop", async () => {
576
+ // Models the other face of the startup race: the capability store had not
577
+ // listed its rows in the page index yet when `selectAllPages` first ran,
578
+ // and has by the time the pass re-enumerates.
579
+ let listCalls = 0;
580
+ const { deps: d, calls } = deps({
581
+ selectAllPages: async () => {
582
+ listCalls += 1;
583
+ return listCalls === 1 ? ["page-a"] : ["page-a", "skills/example"];
584
+ },
585
+ readPageBody: async (slug) =>
586
+ slug === "skills/example"
587
+ ? "# Skill: example\nexample capability body"
588
+ : `body for ${slug}`,
589
+ });
590
+ const outcome = await backfillAllSections(CONFIG, d);
591
+
592
+ expect(listCalls).toBe(2);
593
+ // Only the late CAPABILITY row is swept in; page-a is not re-embedded.
594
+ expect(calls.built).toEqual([["page-a"], ["skills/example"]]);
595
+ expect(calls.deleted).toEqual(["page-a", "skills/example"]);
596
+ expect(outcome.articles).toBe(2);
597
+ expect(outcome.failures).toBe(0);
598
+ expect(calls.committed).toEqual([4242]);
599
+ });
600
+
601
+ test("a real page with an empty body still empties its sections (capability guard does not apply)", async () => {
602
+ const { deps: d, calls } = deps({
603
+ selectAllPages: async () => ["page-empty"],
604
+ readPageBody: async () => "",
605
+ });
606
+ const outcome = await backfillAllSections(CONFIG, d);
607
+
608
+ // Existing behavior for on-disk pages: an empty body still replaces the
609
+ // page's sections (the chunker synthesizes a minimal head section) — only
610
+ // capability rows treat an empty body as "store not seeded".
611
+ expect(calls.deleted).toEqual(["page-empty"]);
612
+ expect(outcome.articles).toBe(1);
613
+ expect(outcome.failures).toBe(0);
614
+ expect(calls.committed).toEqual([4242]);
615
+ });
456
616
  });