@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
@@ -0,0 +1,354 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory buffer file: the two writers of `memory/buffer.md`
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // `memory/buffer.md` has exactly two writers, and both live here so the
6
+ // protocol between them has one home. `remember()` and the sweep job APPEND
7
+ // entries through `appendBufferAndArchive`. The consolidation job CONSUMES
8
+ // the entries it handed to a run through `consumeBufferEntries`, removing
9
+ // exactly those entries and nothing else. Everything else in the plugin
10
+ // reads the file. The entry format itself is owned by `buffer-format.ts`.
11
+ //
12
+ // The writers run in different processes: user turns append from the
13
+ // daemon, while consolidation (and its consume) runs in the memory jobs
14
+ // worker, and the sweep appends from the worker too. No in-process lock can
15
+ // order them, and the buffer must never be rewritten from a stale read: an
16
+ // entry appended between a reader's read and its write would be destroyed
17
+ // without ever being filed. The consumer therefore never trusts a read for
18
+ // longer than one synchronous stretch and never lets an append land where
19
+ // it cannot see it:
20
+ //
21
+ // 1. Open the file and keep the descriptor: it pins the inode the path
22
+ // names at that moment (call it O).
23
+ // 2. Read O through that descriptor, remove the consumed entries from what
24
+ // was read, write the remainder to a sibling temp file, and rename it
25
+ // over the path. Steps 1 and 2 run synchronously, with no await, so
26
+ // nothing in this process interleaves. From the rename on, the path
27
+ // names a new inode (N) and every appender that opens the path lands
28
+ // on N, where nothing is ever rewritten.
29
+ // 3. Drain: an appender that opened O before the rename still writes to O,
30
+ // which the held descriptor keeps alive. Compare O's size against the
31
+ // bytes read; copy anything beyond them onto the path (an append to N,
32
+ // the same operation the appenders use). Drain once immediately, then
33
+ // once more after a short grace window, and only then close O.
34
+ //
35
+ // What can still be lost: an appender whose `open` preceded the rename and
36
+ // whose `write` lands after the final drain. Both calls sit inside one
37
+ // synchronous `appendFileSync`, so that needs the appending process to be
38
+ // stopped between two adjacent syscalls for longer than the grace window (a
39
+ // debugger, SIGSTOP, or a multi-second scheduler stall). Machine sleep
40
+ // freezes both processes together and does not count. The other way out is
41
+ // the drain's own copy failing (disk full, an I/O error) on every retry;
42
+ // the descriptor cannot be held forever, so those bytes are then gone from
43
+ // the buffer and reported as unrecovered, and the entries survive only in
44
+ // the daily archive the same append wrote. Nothing after the rename throws:
45
+ // once the rename has committed the pass is consumed, and the result says
46
+ // so. If either residual ever proves reachable, the escalation is a
47
+ // short-held `wx` lock file taken by both writers around their synchronous
48
+ // critical sections (the idiom `substrate/consolidation-lock.ts` uses);
49
+ // the consume shape above does not change, only its critical section gains
50
+ // a guard.
51
+ //
52
+ // Ordering: consumption removes entries and copies the rest verbatim, so
53
+ // the file stays in append order except for a drained late append, which
54
+ // lands after any entry appended to N in the meantime. Readers only rely
55
+ // on the tail being recent, never on strict order, so that is harmless.
56
+ // The same holds if a late append's bytes were split across the two
57
+ // drains (a partial `write()` on a regular file, well below the residual
58
+ // above): the halves land in order on N, possibly with another appender's
59
+ // entry between them, so the entry is split rather than lost, and the
60
+ // next pass's snapshot selection treats an unterminated tail as
61
+ // in-flight.
62
+
63
+ import {
64
+ appendFileSync,
65
+ closeSync,
66
+ existsSync,
67
+ fstatSync,
68
+ mkdirSync,
69
+ openSync,
70
+ readFileSync,
71
+ readSync,
72
+ renameSync,
73
+ unlinkSync,
74
+ writeFileSync,
75
+ writeSync,
76
+ } from "node:fs";
77
+ import { dirname, join } from "node:path";
78
+
79
+ import {
80
+ type BufferEntryLines,
81
+ bufferEntryText,
82
+ joinBufferEntries,
83
+ splitBufferContent,
84
+ } from "./buffer-format.js";
85
+ import { getLogger } from "./logging.js";
86
+
87
+ const log = getLogger("memory-buffer-file");
88
+
89
+ /**
90
+ * How long the consumer keeps the replaced inode open after the rename
91
+ * before its final drain. Only an appender stalled between its `open` and
92
+ * its `write` for longer than this can lose an entry; a healthy appender
93
+ * completes both within microseconds.
94
+ */
95
+ const LATE_APPEND_GRACE_MS = 500;
96
+
97
+ /** Attempts to copy late-appended bytes back before giving them up. */
98
+ const LATE_APPEND_COPY_ATTEMPTS = 3;
99
+ /** Wait between those attempts; disk pressure and I/O errors are often brief. */
100
+ const LATE_APPEND_COPY_RETRY_MS = 200;
101
+
102
+ /**
103
+ * Append `entry` to `<rootDir>/buffer.md` and `<rootDir>/archive/<today>.md`,
104
+ * creating the archive directory and seeding the archive header if missing.
105
+ *
106
+ * Returns the absolute paths of both files so callers can fan out follow-up
107
+ * work.
108
+ *
109
+ * Shared by `remember()` and the background jobs (`sweep`, future LLM-driven
110
+ * extractors) so every appender writes exactly the same format and
111
+ * downstream consumers (consolidation, search) cannot tell them apart.
112
+ */
113
+ export function appendBufferAndArchive(args: {
114
+ rootDir: string;
115
+ entry: string;
116
+ now: Date;
117
+ }): { bufferPath: string; archivePath: string } {
118
+ const { rootDir, entry, now } = args;
119
+ const archiveDir = join(rootDir, "archive");
120
+ mkdirSync(archiveDir, { recursive: true });
121
+
122
+ const bufferPath = join(rootDir, "buffer.md");
123
+ appendFileSync(bufferPath, entry, "utf-8");
124
+
125
+ const yyyy = now.getFullYear();
126
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
127
+ const dd = String(now.getDate()).padStart(2, "0");
128
+ const archivePath = join(archiveDir, `${yyyy}-${mm}-${dd}.md`);
129
+ if (!existsSync(archivePath)) {
130
+ const month = now.toLocaleString("en-US", { month: "short" });
131
+ appendFileSync(
132
+ archivePath,
133
+ `# ${month} ${now.getDate()}, ${yyyy}\n\n`,
134
+ "utf-8",
135
+ );
136
+ }
137
+ appendFileSync(archivePath, entry, "utf-8");
138
+
139
+ return { bufferPath, archivePath };
140
+ }
141
+
142
+ export interface ConsumeBufferEntriesResult {
143
+ /** Consumed entries found in the live file and removed. */
144
+ removed: number;
145
+ /**
146
+ * Consumed entries not present in the live file. Something other than an
147
+ * appender rewrote the file during the run; the caller decides whether
148
+ * that is worth a warning.
149
+ */
150
+ alreadyAbsent: number;
151
+ /** Bytes appended to the replaced inode after the read and copied back. */
152
+ lateAppendBytesRecovered: number;
153
+ /**
154
+ * Bytes appended to the replaced inode that could not be copied back
155
+ * after every retry. The pass is consumed regardless (the rename had
156
+ * committed); these entries remain only in the daily archive.
157
+ */
158
+ unrecoveredLateAppendBytes: number;
159
+ /**
160
+ * The replaced inode could not be inspected or read after the rename, so
161
+ * any append that landed on it is unaccounted for (not even counted in
162
+ * `unrecoveredLateAppendBytes`). The pass is consumed regardless.
163
+ */
164
+ lateAppendDrainFailed: boolean;
165
+ }
166
+
167
+ /**
168
+ * Remove `consumed` from `bufferPath`, leaving every other entry in place.
169
+ * Never throws once the rename has committed; see the result's
170
+ * `unrecoveredLateAppendBytes`.
171
+ *
172
+ * Each consumed entry is matched by its exact text ({@link bufferEntryText})
173
+ * against the live file, first occurrence, once per consumed entry, so an
174
+ * identical fact appended during the run survives when the snapshot held
175
+ * one copy. An entry that is absent from the live file is counted, never
176
+ * searched for elsewhere. A missing file consumes nothing.
177
+ *
178
+ * The critical section (read through a pinned descriptor, rewrite via temp
179
+ * file + rename) is synchronous; the drain of late appends to the replaced
180
+ * inode follows, with a second drain after `lateAppendGraceMs`. See the
181
+ * module header for why this is lossless and what the residual is.
182
+ *
183
+ * I/O failures other than a missing file throw: the caller (the
184
+ * consolidation job) treats a failed consume as no progress and leaves the
185
+ * buffer for the next pass rather than guessing at its state.
186
+ */
187
+ export async function consumeBufferEntries(
188
+ bufferPath: string,
189
+ consumed: readonly BufferEntryLines[],
190
+ options: { lateAppendGraceMs?: number } = {},
191
+ ): Promise<ConsumeBufferEntriesResult> {
192
+ const graceMs = options.lateAppendGraceMs ?? LATE_APPEND_GRACE_MS;
193
+ const pending = consumed.map(bufferEntryText);
194
+
195
+ let fd: number;
196
+ try {
197
+ fd = openSync(bufferPath, "r");
198
+ } catch (err) {
199
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
200
+ return {
201
+ removed: 0,
202
+ alreadyAbsent: pending.length,
203
+ lateAppendBytesRecovered: 0,
204
+ unrecoveredLateAppendBytes: 0,
205
+ lateAppendDrainFailed: false,
206
+ };
207
+ }
208
+ throw err;
209
+ }
210
+
211
+ try {
212
+ // Synchronous critical section: nothing in this process runs between
213
+ // the read and the rename.
214
+ const before = readFileSync(fd);
215
+ let drainedTo = before.length;
216
+ const live = splitBufferContent(before.toString("utf-8"));
217
+ const remaining: BufferEntryLines[] = [];
218
+ let removed = 0;
219
+ for (const entry of live) {
220
+ const index = pending.indexOf(bufferEntryText(entry));
221
+ if (index === -1) {
222
+ remaining.push(entry);
223
+ continue;
224
+ }
225
+ pending.splice(index, 1);
226
+ removed += 1;
227
+ }
228
+ const tmpPath = join(
229
+ dirname(bufferPath),
230
+ `.buffer.md.${process.pid}.${Date.now()}.tmp`,
231
+ );
232
+ writeFileSync(tmpPath, joinBufferEntries(remaining), "utf-8");
233
+ try {
234
+ renameSync(tmpPath, bufferPath);
235
+ } catch (err) {
236
+ unlinkSafely(tmpPath);
237
+ throw err;
238
+ }
239
+
240
+ // Drain: appends that landed on the replaced inode after the read. The
241
+ // rename has committed, so from here nothing throws: a copy that fails
242
+ // is retried, bytes that cannot be copied are reported, and a replaced
243
+ // inode that cannot even be read is reported, all without raising.
244
+ let lateAppendBytesRecovered = 0;
245
+ let unrecoveredLateAppendBytes = 0;
246
+ let lateAppendDrainFailed = false;
247
+ // Copies through an explicit write loop so a retry resumes from the
248
+ // first unwritten byte: an append that wrote part of the bytes before
249
+ // failing (disk pressure) must not have its prefix appended twice.
250
+ const copyBack = async (bytes: Buffer): Promise<void> => {
251
+ let written = 0;
252
+ for (let attempt = 1; ; attempt++) {
253
+ try {
254
+ const out = openSync(bufferPath, "a");
255
+ try {
256
+ while (written < bytes.length) {
257
+ const n = writeSync(out, bytes, written, bytes.length - written);
258
+ if (n <= 0) {
259
+ // A regular file never returns a zero-byte write without an
260
+ // error, but a spin here would wedge the worker with the
261
+ // consolidation lock held; treat it as a failed attempt.
262
+ throw new Error("writeSync made no progress");
263
+ }
264
+ written += n;
265
+ }
266
+ } finally {
267
+ closeSync(out);
268
+ }
269
+ lateAppendBytesRecovered += bytes.length;
270
+ return;
271
+ } catch (err) {
272
+ if (attempt >= LATE_APPEND_COPY_ATTEMPTS) {
273
+ log.error(
274
+ { err, bufferPath, bytes: bytes.length, written, attempt },
275
+ "buffer consume: could not copy entries appended during the rewrite back into the buffer; giving up the rest",
276
+ );
277
+ lateAppendBytesRecovered += written;
278
+ unrecoveredLateAppendBytes += bytes.length - written;
279
+ return;
280
+ }
281
+ log.warn(
282
+ { err, bufferPath, bytes: bytes.length, written, attempt },
283
+ "buffer consume: copying late-appended entries failed; retrying from the first unwritten byte",
284
+ );
285
+ await new Promise((resolve) =>
286
+ setTimeout(resolve, LATE_APPEND_COPY_RETRY_MS),
287
+ );
288
+ }
289
+ }
290
+ };
291
+ const drain = async (): Promise<void> => {
292
+ let late: Buffer;
293
+ try {
294
+ const size = fstatSync(fd).size;
295
+ if (size <= drainedTo) {
296
+ return;
297
+ }
298
+ late = Buffer.alloc(size - drainedTo);
299
+ const read = readSync(fd, late, 0, late.length, drainedTo);
300
+ late = late.subarray(0, read);
301
+ drainedTo += read;
302
+ } catch (err) {
303
+ log.error(
304
+ { err, bufferPath },
305
+ "buffer consume: could not read the replaced inode for entries appended during the rewrite",
306
+ );
307
+ lateAppendDrainFailed = true;
308
+ return;
309
+ }
310
+ await copyBack(late);
311
+ };
312
+ await drain();
313
+ if (graceMs > 0) {
314
+ await new Promise((resolve) => setTimeout(resolve, graceMs));
315
+ }
316
+ if (!lateAppendDrainFailed) {
317
+ await drain();
318
+ }
319
+ if (lateAppendBytesRecovered > 0) {
320
+ log.info(
321
+ { bufferPath, lateAppendBytesRecovered },
322
+ "buffer consume: recovered entries appended during the rewrite",
323
+ );
324
+ }
325
+
326
+ return {
327
+ removed,
328
+ alreadyAbsent: pending.length,
329
+ lateAppendBytesRecovered,
330
+ unrecoveredLateAppendBytes,
331
+ lateAppendDrainFailed,
332
+ };
333
+ } finally {
334
+ // After the rename, a failing close must not surface as a failure of
335
+ // the consume: the pass is consumed and the drain has reported. Before
336
+ // the rename, the original error is already propagating.
337
+ try {
338
+ closeSync(fd);
339
+ } catch (err) {
340
+ log.error(
341
+ { err, bufferPath },
342
+ "buffer consume: closing the replaced buffer inode failed",
343
+ );
344
+ }
345
+ }
346
+ }
347
+
348
+ function unlinkSafely(path: string): void {
349
+ try {
350
+ unlinkSync(path);
351
+ } catch {
352
+ // best-effort cleanup of a temp file the rename never claimed
353
+ }
354
+ }
@@ -154,6 +154,46 @@ export function splitBufferEntries(
154
154
  return groups;
155
155
  }
156
156
 
157
+ /**
158
+ * Split a whole buffer FILE into entries. The file is newline-terminated
159
+ * (every append ends in `\n`), so the terminator ends the last line rather
160
+ * than opening an empty one: an entry's lines are the same whether it is
161
+ * currently last in the file or has since been followed by more entries.
162
+ * That stability is what lets a consumer match an entry it snapshotted
163
+ * earlier against the live file by exact text.
164
+ */
165
+ export function splitBufferContent(content: string): BufferEntryLines[] {
166
+ if (content.length === 0) {
167
+ return [];
168
+ }
169
+ const lines = content.split("\n");
170
+ if (lines[lines.length - 1] === "") {
171
+ lines.pop();
172
+ }
173
+ return splitBufferEntries(lines);
174
+ }
175
+
176
+ /**
177
+ * One entry's verbatim text: its lines joined, without a terminating
178
+ * newline. The identity a consumer matches on.
179
+ */
180
+ export function bufferEntryText(entry: BufferEntryLines): string {
181
+ return entry.lines.join("\n");
182
+ }
183
+
184
+ /**
185
+ * Inverse of {@link splitBufferContent}: entries back to newline-terminated
186
+ * file content, or the empty string when there are none.
187
+ */
188
+ export function joinBufferEntries(
189
+ entries: readonly BufferEntryLines[],
190
+ ): string {
191
+ if (entries.length === 0) {
192
+ return "";
193
+ }
194
+ return `${entries.map(bufferEntryText).join("\n")}\n`;
195
+ }
196
+
157
197
  /** One entry's verbatim lines, as returned by {@link splitBufferEntries}. */
158
198
  export interface BufferEntryLines {
159
199
  /** `null` for the headless group before the first entry opening. */
@@ -4,10 +4,9 @@ import type {
4
4
  ProviderResponse,
5
5
  ToolUseContent,
6
6
  } from "@vellumai/plugin-api";
7
- import { getConfiguredProvider } from "@vellumai/plugin-api";
7
+ import { getConfiguredProvider, safeStringSlice } from "@vellumai/plugin-api";
8
8
 
9
9
  import { redactSecrets } from "../../../../security/secret-scanner.js";
10
- import { safeStringSlice } from "../host-utils.js";
11
10
  import {
12
11
  buildRecallAgentPromptBundle,
13
12
  FINISH_RECALL_TOOL_DEFINITION,
@@ -1,4 +1,5 @@
1
- import { safeStringSlice } from "../host-utils.js";
1
+ import { safeStringSlice } from "@vellumai/plugin-api";
2
+
2
3
  import type {
3
4
  DeterministicRecallSearchResult,
4
5
  DeterministicRecallSourceNote,
@@ -26,8 +26,9 @@
26
26
  import { readdir, readFile, realpath, stat } from "node:fs/promises";
27
27
  import { extname, join } from "node:path";
28
28
 
29
+ import { safeStringSlice } from "@vellumai/plugin-api";
30
+
29
31
  import { embedWithRetry } from "../../../../../persistence/embeddings/embed.js";
30
- import { safeStringSlice } from "../../host-utils.js";
31
32
  import { getLogger } from "../../logging.js";
32
33
  import { isPathInsideRoot } from "../../path-containment.js";
33
34
  import { getEdgeIndex } from "../../substrate/edge-index.js";
@@ -1,7 +1,8 @@
1
1
  import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
2
  import { extname, isAbsolute, join, relative, sep } from "node:path";
3
3
 
4
- import { safeStringSlice } from "../../host-utils.js";
4
+ import { safeStringSlice } from "@vellumai/plugin-api";
5
+
5
6
  import { isPathInsideRoot } from "../../path-containment.js";
6
7
  import type {
7
8
  RecallEvidence,
@@ -16,6 +16,10 @@ export const CONVERSATION_KEYED_MEMORY_TABLES: readonly string[] = [
16
16
  "memory_recall_logs",
17
17
  "memory_v3_selections",
18
18
  "memory_v3_pools",
19
+ // The pool input capture keys its turn rows here; its companion texts
20
+ // table is content-addressed and page-derived, so it has no conversation
21
+ // rows to purge.
22
+ "memory_v3_pool_inputs",
19
23
  "activation_sessions",
20
24
  "activation_state",
21
25
  "conversation_graph_memory_state",
@@ -5,7 +5,7 @@
5
5
  // semantic retrieval.
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
- import { CLI_COMMAND_HELP } from "@vellumai/plugin-api";
8
+ import { CLI_COMMAND_HELP, safeStringSlice } from "@vellumai/plugin-api";
9
9
  import { and, eq, like, sql } from "drizzle-orm";
10
10
 
11
11
  import { isAssistantFeatureFlagEnabled } from "../../../../config/assistant-feature-flags.js";
@@ -26,7 +26,6 @@ import {
26
26
  getCachedCatalogSync,
27
27
  getCatalog,
28
28
  } from "../../../../skills/catalog-cache.js";
29
- import { safeStringSlice } from "../host-utils.js";
30
29
  import { getLogger } from "../logging.js";
31
30
  import { memoryDbOrNull } from "../memory-db.js";
32
31
  import type { SkillCapabilityInput } from "../substrate/skill-content.js";
@@ -6,7 +6,6 @@
6
6
  // into concept pages.
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
- import { appendFileSync, existsSync, mkdirSync } from "node:fs";
10
9
  import { join } from "node:path";
11
10
 
12
11
  import {
@@ -15,6 +14,7 @@ import {
15
14
  } from "../../../../config/memory-v3-gate.js";
16
15
  import type { AssistantConfig } from "../../../../config/types.js";
17
16
  import { enqueueMemoryJob } from "../../../../persistence/jobs-store.js";
17
+ import { appendBufferAndArchive } from "../buffer-file.js";
18
18
  import { formatRememberEntry } from "../buffer-format.js";
19
19
  import { getWorkspaceDir } from "../paths.js";
20
20
  import type { GraphStats } from "./store.js";
@@ -99,47 +99,6 @@ export function handleRemember(
99
99
  return { success: true, message };
100
100
  }
101
101
 
102
- /**
103
- * Append `entry` to `<rootDir>/buffer.md` and `<rootDir>/archive/<today>.md`,
104
- * creating the archive directory and seeding the archive header if missing.
105
- *
106
- * Returns the absolute paths of both files so callers can fan out follow-up
107
- * work.
108
- *
109
- * Exported so background jobs (`sweep`, future LLM-driven extractors) can
110
- * append to `memory/buffer.md` + `memory/archive/<today>.md` with exactly the
111
- * same format `remember()` produces, keeping the two write paths
112
- * byte-compatible for downstream consumers (consolidation, search).
113
- */
114
- export function appendBufferAndArchive(args: {
115
- rootDir: string;
116
- entry: string;
117
- now: Date;
118
- }): { bufferPath: string; archivePath: string } {
119
- const { rootDir, entry, now } = args;
120
- const archiveDir = join(rootDir, "archive");
121
- mkdirSync(archiveDir, { recursive: true });
122
-
123
- const bufferPath = join(rootDir, "buffer.md");
124
- appendFileSync(bufferPath, entry, "utf-8");
125
-
126
- const yyyy = now.getFullYear();
127
- const mm = String(now.getMonth() + 1).padStart(2, "0");
128
- const dd = String(now.getDate()).padStart(2, "0");
129
- const archivePath = join(archiveDir, `${yyyy}-${mm}-${dd}.md`);
130
- if (!existsSync(archivePath)) {
131
- const month = now.toLocaleString("en-US", { month: "short" });
132
- appendFileSync(
133
- archivePath,
134
- `# ${month} ${now.getDate()}, ${yyyy}\n\n`,
135
- "utf-8",
136
- );
137
- }
138
- appendFileSync(archivePath, entry, "utf-8");
139
-
140
- return { bufferPath, archivePath };
141
- }
142
-
143
102
  // ---------------------------------------------------------------------------
144
103
  // Shared helpers for delete / update / list
145
104
  // ---------------------------------------------------------------------------
@@ -25,7 +25,6 @@ import type { SqliteRetryOptions } from "../../../util/sqlite-retry.js";
25
25
  import * as hostSqliteRetry from "../../../util/sqlite-retry.js";
26
26
  import * as hostStripCommentLines from "../../../util/strip-comment-lines.js";
27
27
  import * as hostTruncate from "../../../util/truncate.js";
28
- import * as hostUnicode from "../../../util/unicode.js";
29
28
  import * as hostWorkerCompute from "../../../util/worker-compute.js";
30
29
  import * as hostWorkerMemory from "../../../util/worker-memory.js";
31
30
 
@@ -77,15 +76,6 @@ export function truncate(str: string, maxLen: number, suffix?: string): string {
77
76
  : hostTruncate.truncate(str, maxLen, suffix);
78
77
  }
79
78
 
80
- /** `String.prototype.slice` that never cuts a UTF-16 surrogate pair in half. */
81
- export function safeStringSlice(
82
- str: string,
83
- start?: number,
84
- end?: number,
85
- ): string {
86
- return hostUnicode.safeStringSlice(str, start, end);
87
- }
88
-
89
79
  export function workerMemoryEnv(): Record<string, string | undefined> {
90
80
  return hostWorkerMemory.workerMemoryEnv();
91
81
  }
@@ -374,9 +374,10 @@ const memoryV2StaticInjector: Injector = {
374
374
  if (mode !== "full") {
375
375
  return null;
376
376
  }
377
- // The consolidation agent reads and rewrites memory/buffer.md through
378
- // file tools; injecting the buffer section here would duplicate the
379
- // entire backlog into its context (and go stale as it edits the file).
377
+ // The consolidation run already carries its pass's buffer entries in
378
+ // the prompt itself, and the job removes them from memory/buffer.md
379
+ // after the run; injecting the buffer section here would duplicate the
380
+ // pass and hand the agent entries that are not its to file.
380
381
  const content = readGatedMemoryV2Static(ctx.trust, {
381
382
  excludeBuffer: ctx.callSite === "memoryV2Consolidation",
382
383
  });