@vellumai/assistant 0.12.2-staging.7 → 0.12.2

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 (120) hide show
  1. package/docs/architecture/memory.md +2 -11
  2. package/docs/desktop-browser-cli.md +2 -4
  3. package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
  4. package/node_modules/@vellumai/environments/src/shell.ts +0 -24
  5. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
  6. package/openapi.yaml +2 -6
  7. package/package.json +1 -1
  8. package/scripts/smoke-desktop-browser-cli.ts +0 -1
  9. package/src/__tests__/agent-loop.test.ts +0 -124
  10. package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
  11. package/src/__tests__/channel-approval.test.ts +14 -9
  12. package/src/__tests__/conversation-agent-loop.test.ts +0 -25
  13. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  14. package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
  15. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  16. package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
  17. package/src/__tests__/terminal-tools.test.ts +0 -8
  18. package/src/__tests__/unicode.test.ts +0 -36
  19. package/src/agent/loop.ts +0 -19
  20. package/src/api/index.ts +0 -6
  21. package/src/approvals/approval-primitive.ts +2 -5
  22. package/src/approvals/scoped-approval-grants.ts +2 -6
  23. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
  24. package/src/daemon/conversation-agent-loop.ts +0 -2
  25. package/src/daemon/conversation-tool-setup.ts +1 -57
  26. package/src/daemon/conversation.ts +0 -17
  27. package/src/daemon/daemon-control.ts +6 -2
  28. package/src/daemon/orphan-reaper.ts +3 -4
  29. package/src/daemon/tool-setup-types.ts +0 -6
  30. package/src/daemon/wake-conversation-ops.ts +15 -38
  31. package/src/desktop/desktop-automation-lease.test.ts +0 -143
  32. package/src/desktop/desktop-automation-lease.ts +3 -39
  33. package/src/messaging/provider-message-metadata.ts +3 -3
  34. package/src/notifications/__tests__/copy-composer.test.ts +0 -70
  35. package/src/notifications/copy-composer.ts +3 -11
  36. package/src/oauth/seed-providers.ts +30 -0
  37. package/src/persistence/conversation-plugin-facade.ts +0 -13
  38. package/src/persistence/schema/index.ts +0 -1
  39. package/src/persistence/steps.ts +0 -2
  40. package/src/plugin-api/conversation-turn.ts +7 -31
  41. package/src/plugin-api/index.ts +1 -9
  42. package/src/plugins/defaults/memory/AGENTS.md +2 -14
  43. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
  44. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
  45. package/src/plugins/defaults/memory/buffer-format.ts +0 -40
  46. package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
  47. package/src/plugins/defaults/memory/context-search/format.ts +1 -2
  48. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
  49. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
  50. package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
  51. package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
  52. package/src/plugins/defaults/memory/host-utils.ts +10 -0
  53. package/src/plugins/defaults/memory/injectors.ts +3 -4
  54. package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
  55. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
  56. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
  57. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
  58. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
  59. package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
  60. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
  61. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  62. package/src/plugins/defaults/memory/tools.ts +1 -1
  63. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  64. package/src/plugins/defaults/memory/v1/graph/extraction.ts +1 -2
  65. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  66. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
  67. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
  68. package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
  69. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
  70. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
  71. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
  72. package/src/plugins/defaults/memory/v3/card.ts +1 -2
  73. package/src/plugins/defaults/memory/v3/injector.ts +178 -212
  74. package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
  75. package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
  76. package/src/plugins/defaults/memory/v3/sections.ts +1 -2
  77. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
  78. package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
  79. package/src/runtime/AGENTS.md +1 -1
  80. package/src/runtime/__tests__/agent-wake.test.ts +1 -86
  81. package/src/runtime/agent-wake.ts +4 -20
  82. package/src/runtime/guardian-action-service.ts +17 -2
  83. package/src/runtime/guardian-reply-router.ts +8 -1
  84. package/src/runtime/routes/channel-route-shared.ts +9 -1
  85. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  86. package/src/runtime/routes/desktop-setup-routes.ts +3 -7
  87. package/src/runtime/routes/guardian-approval-interception.ts +0 -24
  88. package/src/runtime/routes/inbound-message-handler.ts +3 -2
  89. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  90. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
  91. package/src/schedule/run-script.ts +2 -2
  92. package/src/tools/host-terminal/host-shell.ts +6 -12
  93. package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
  94. package/src/tools/shared/shell-output.test.ts +0 -10
  95. package/src/tools/shared/shell-output.ts +2 -14
  96. package/src/tools/skills/sandbox-runner.ts +2 -13
  97. package/src/tools/skills/scaffold-managed.ts +1 -2
  98. package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
  99. package/src/tools/terminal/safe-env.ts +1 -30
  100. package/src/tools/terminal/sanitized-bash.ts +2 -15
  101. package/src/tools/terminal/shell.test.ts +0 -29
  102. package/src/tools/terminal/shell.ts +7 -13
  103. package/src/util/host-process.test.ts +1 -17
  104. package/src/util/host-process.ts +0 -24
  105. package/src/util/unicode.ts +0 -29
  106. package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
  107. package/src/api/events/desktop-activity-changed.ts +0 -10
  108. package/src/persistence/conversation-tool-surface.ts +0 -86
  109. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
  110. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
  111. package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
  112. package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
  113. package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
  114. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
  115. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
  116. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
  117. package/src/plugins/defaults/memory/buffer-file.ts +0 -354
  118. package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
  119. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
  120. package/src/tools/terminal/shell-launch.test.ts +0 -162
@@ -286,11 +286,6 @@ export {
286
286
  // writes files under the workspace (e.g. its own `plugins/<name>/data/`
287
287
  // directory) resolves them against this instead of hardcoding a base path.
288
288
  export { getWorkspaceDir } from "../util/platform.js";
289
- // `String.prototype.slice` that never cuts a UTF-16 surrogate pair in half.
290
- // Any text a plugin truncates by character budget and hands to a model must
291
- // go through this: an orphaned half is invalid UTF-16 that strict provider
292
- // parsers reject.
293
- export { safeStringSlice } from "../util/unicode.js";
294
289
  // Declarative help for the top-level `assistant` CLI commands that have adopted
295
290
  // the static-help split. Plugins (e.g. the memory capability indexer) read this
296
291
  // to embed CLI command capabilities without importing the CLI action graph.
@@ -364,8 +359,7 @@ export {
364
359
  stringifyMessageContent,
365
360
  } from "../persistence/message-content.js";
366
361
  // Conversation history — reads and writes on the host conversation store
367
- // (rows, message history, processing state, the recorded wire tool surface,
368
- // disk-view paths) plus the lexical
362
+ // (rows, message history, processing state, disk-view paths) plus the lexical
369
363
  // message-search surface. Every operation takes explicit parameters; nothing
370
364
  // is resolved from config. Async because the facade loads the DB store graph
371
365
  // lazily on first call.
@@ -377,7 +371,6 @@ export {
377
371
  getConversation,
378
372
  getConversationDirPath,
379
373
  getConversationProcessingStartedAt,
380
- getConversationToolSurface,
381
374
  getMessages,
382
375
  hasLexicalTokens,
383
376
  isConversationProcessing,
@@ -445,7 +438,6 @@ export type {
445
438
  RunConversationTurnResult,
446
439
  } from "./conversation-turn.js";
447
440
  export { runConversationTurn } from "./conversation-turn.js";
448
- export { PluginTurnNotAdmittedError } from "./plugin-channel-turn-trust.js";
449
441
  // Live voice — drive a single client's real-time voice session (STT → agent
450
442
  // turn → TTS, with server-VAD turn-taking, pauses, and barge-in) over a
451
443
  // transport the plugin owns. The plugin brings only a `send` callback (e.g.
@@ -68,13 +68,6 @@ Everything else under the plugin root is **spine**:
68
68
  the writer plus the one matcher every reader uses. It lives at the root
69
69
  precisely so `substrate/`, `graph/`, and `graph-topology/` can all reach it
70
70
  without a tier importing spine. Do not add a second matcher anywhere),
71
- `buffer-file` (the two writers of `memory/buffer.md`: the append every
72
- `remember()`-shaped path uses and the consume the consolidation job runs
73
- after a pass. Do not write the buffer from anywhere else, and never
74
- rewrite it from a read that is not inside `consumeBufferEntries`'s
75
- synchronous critical section), `memory-run-evidence` (readers of what a
76
- background memory run durably produced from its persisted messages; every
77
- job that gates a state transition on a verified write uses them),
78
71
  `segmenter`, `message-media`, `worker`, `worker-control`,
79
72
  `memory-recall-log-store`, `activation-session-store` (the onboarding
80
73
  activation rail — **not** a memory tier despite the name),
@@ -414,13 +407,8 @@ per distinct matched section, at most `memory.v3.finderSectionsPerPage` in
414
407
  surfacing order (needle, dense, reply, span) plus its entity and rare-term
415
408
  lines, and selecting a line selects that section; the selection log keeps
416
409
  one row per slug, so the pool row is where the per-section verdicts live. A
417
- turn whose selector never judged a pool persists it with `selector_ran = 0`:
418
- an empty pool when the injection gate hard-skipped it or nothing was pooled,
419
- and the pool as the selector was given it, stable-prefix cards chosen and
420
- finder lines not, when the selector's provider failed and the orchestrator
421
- kept that prefix unjudged. That turn writes no `memory_v3_selections` rows:
422
- the hot set's frecency and the learned-edge graph read that table as
423
- judgments, and an unjudged page is not one. A
410
+ turn whose selector never judged a pool (the injection gate hard-skipped it,
411
+ or nothing was pooled) persists an empty pool with `selector_ran = 0`, and a
424
412
  turn that logged no selections is still reachable by its stamped
425
413
  `message_id`, so the inspector shows negative verdicts too. The pool row and
426
414
  the turn's `memory_v3_selections` rows are
@@ -10,13 +10,10 @@
10
10
  import { describe, expect, test } from "bun:test";
11
11
 
12
12
  import {
13
- bufferEntryText,
14
13
  formatBufferTimestamp,
15
14
  formatRememberEntry,
16
15
  isBufferEntryStart,
17
- joinBufferEntries,
18
16
  matchBufferEntryStart,
19
- splitBufferContent,
20
17
  splitBufferEntries,
21
18
  } from "../buffer-format.js";
22
19
 
@@ -205,43 +202,3 @@ describe("splitBufferEntries", () => {
205
202
  expect(splitBufferEntries([])).toEqual([]);
206
203
  });
207
204
  });
208
-
209
- describe("splitBufferContent / joinBufferEntries", () => {
210
- const content =
211
- formatRememberEntry("Alice prefers dark mode", new Date(2026, 0, 1, 9, 0)) +
212
- formatRememberEntry(
213
- "Bob's plan:\n- [ ] step one\n\nend",
214
- new Date(2026, 0, 1, 9, 1),
215
- );
216
-
217
- test("round-trips newline-terminated content byte for byte", () => {
218
- expect(joinBufferEntries(splitBufferContent(content))).toBe(content);
219
- });
220
-
221
- test("an entry's text is the same whether or not it is last in the file", () => {
222
- const [first] = splitBufferContent(content);
223
- const [alone] = splitBufferContent(
224
- formatRememberEntry(
225
- "Alice prefers dark mode",
226
- new Date(2026, 0, 1, 9, 0),
227
- ),
228
- );
229
- expect(bufferEntryText(alone!)).toBe(bufferEntryText(first!));
230
- expect(bufferEntryText(alone!)).toBe(
231
- "- [Jan 1, 9:00 AM] Alice prefers dark mode",
232
- );
233
- });
234
-
235
- test("empty content has no entries and joins back to empty", () => {
236
- expect(splitBufferContent("")).toEqual([]);
237
- expect(joinBufferEntries([])).toBe("");
238
- });
239
-
240
- test("content without a terminating newline keeps its last line intact", () => {
241
- const entries = splitBufferContent("- [Jan 1, 9:00 AM] partial appe");
242
- expect(entries).toHaveLength(1);
243
- expect(bufferEntryText(entries[0]!)).toBe(
244
- "- [Jan 1, 9:00 AM] partial appe",
245
- );
246
- });
247
- });
@@ -117,17 +117,6 @@ let loadedConversations: Record<string, { processing: boolean }> = {};
117
117
  // the stale-flag override's age check. Absent ids read as null (stampless).
118
118
  let processingStartedAtById: Record<string, number | null> = {};
119
119
 
120
- // The source conversation's recorded wire tool surface (what its last live
121
- // turn sent to the provider). `null` = no live turn has recorded one yet.
122
- let mockSourceToolSurface: Array<Record<string, unknown>> | null = null;
123
- let toolSurfaceReads: string[] = [];
124
- mock.module("../../../../persistence/conversation-tool-surface.js", () => ({
125
- getConversationToolSurface: (conversationId: string) => {
126
- toolSurfaceReads.push(conversationId);
127
- return mockSourceToolSurface;
128
- },
129
- }));
130
-
131
120
  const watchdogEvents: Array<{
132
121
  checkName: string;
133
122
  value?: number | null;
@@ -520,8 +509,6 @@ describe("memoryRetrospectiveJob", () => {
520
509
  resolveUserSlugCalls = [];
521
510
  mockV3TierActive = true;
522
511
  mockSkillImprovementActive = false;
523
- mockSourceToolSurface = null;
524
- toolSurfaceReads = [];
525
512
  });
526
513
 
527
514
  test("first-run happy path: no state row, no prior retrospective, both pointer fields set on success", async () => {
@@ -1855,39 +1842,6 @@ describe("memoryRetrospectiveJob", () => {
1855
1842
  });
1856
1843
  });
1857
1844
 
1858
- test("replays the source's recorded wire tool surface verbatim on the fork wake", async () => {
1859
- mockSourceToolSurface = [
1860
- { name: "bash", description: "Run", input_schema: {} },
1861
- { name: "remember", description: "Save", input_schema: {} },
1862
- { name: "bell_jingle", description: "Ring", input_schema: {} },
1863
- { type: "web_search_20250305", name: "web_search", max_uses: 5 },
1864
- ];
1865
-
1866
- await memoryRetrospectiveJob(makeJob(), stubConfig);
1867
-
1868
- expect(toolSurfaceReads).toEqual(["src-conv-1"]);
1869
- expect(wakeCalls).toHaveLength(1);
1870
- // The exact recorded array rides to the wake; the fork sends it in place
1871
- // of the surface it would resolve for itself.
1872
- expect(wakeCalls[0]!.opts.wireToolDefinitions).toEqual(
1873
- mockSourceToolSurface,
1874
- );
1875
- // The execution-side pin still rides alongside it.
1876
- expect(wakeCalls[0]!.opts.toolGateMode).toBe("execution");
1877
- expect(wakeCalls[0]!.opts.toolContextPin).toBeDefined();
1878
- });
1879
-
1880
- test("no recorded source surface → no wireToolDefinitions; the pin alone shapes the wire", async () => {
1881
- mockSourceToolSurface = null;
1882
-
1883
- await memoryRetrospectiveJob(makeJob(), stubConfig);
1884
-
1885
- expect(toolSurfaceReads).toEqual(["src-conv-1"]);
1886
- expect(wakeCalls).toHaveLength(1);
1887
- expect("wireToolDefinitions" in wakeCalls[0]!.opts).toBe(false);
1888
- expect(wakeCalls[0]!.opts.toolContextPin).toBeDefined();
1889
- });
1890
-
1891
1845
  test("execution mode is unconditional → toolContextPin rides even without a resolved profile", async () => {
1892
1846
  await memoryRetrospectiveJob(
1893
1847
  makeJob(),
@@ -154,46 +154,6 @@ 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
-
197
157
  /** One entry's verbatim lines, as returned by {@link splitBufferEntries}. */
198
158
  export interface BufferEntryLines {
199
159
  /** `null` for the headless group before the first entry opening. */
@@ -4,9 +4,10 @@ import type {
4
4
  ProviderResponse,
5
5
  ToolUseContent,
6
6
  } from "@vellumai/plugin-api";
7
- import { getConfiguredProvider, safeStringSlice } from "@vellumai/plugin-api";
7
+ import { getConfiguredProvider } from "@vellumai/plugin-api";
8
8
 
9
9
  import { redactSecrets } from "../../../../security/secret-scanner.js";
10
+ import { safeStringSlice } from "../host-utils.js";
10
11
  import {
11
12
  buildRecallAgentPromptBundle,
12
13
  FINISH_RECALL_TOOL_DEFINITION,
@@ -1,5 +1,4 @@
1
- import { safeStringSlice } from "@vellumai/plugin-api";
2
-
1
+ import { safeStringSlice } from "../host-utils.js";
3
2
  import type {
4
3
  DeterministicRecallSearchResult,
5
4
  DeterministicRecallSourceNote,
@@ -26,9 +26,8 @@
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
-
31
29
  import { embedWithRetry } from "../../../../../persistence/embeddings/embed.js";
30
+ import { safeStringSlice } from "../../host-utils.js";
32
31
  import { getLogger } from "../../logging.js";
33
32
  import { isPathInsideRoot } from "../../path-containment.js";
34
33
  import { getEdgeIndex } from "../../substrate/edge-index.js";
@@ -1,8 +1,7 @@
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 "@vellumai/plugin-api";
5
-
4
+ import { safeStringSlice } from "../../host-utils.js";
6
5
  import { isPathInsideRoot } from "../../path-containment.js";
7
6
  import type {
8
7
  RecallEvidence,
@@ -5,7 +5,7 @@
5
5
  // semantic retrieval.
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
- import { CLI_COMMAND_HELP, safeStringSlice } from "@vellumai/plugin-api";
8
+ import { CLI_COMMAND_HELP } 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,6 +26,7 @@ import {
26
26
  getCachedCatalogSync,
27
27
  getCatalog,
28
28
  } from "../../../../skills/catalog-cache.js";
29
+ import { safeStringSlice } from "../host-utils.js";
29
30
  import { getLogger } from "../logging.js";
30
31
  import { memoryDbOrNull } from "../memory-db.js";
31
32
  import type { SkillCapabilityInput } from "../substrate/skill-content.js";
@@ -6,6 +6,7 @@
6
6
  // into concept pages.
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
+ import { appendFileSync, existsSync, mkdirSync } from "node:fs";
9
10
  import { join } from "node:path";
10
11
 
11
12
  import {
@@ -14,7 +15,6 @@ import {
14
15
  } from "../../../../config/memory-v3-gate.js";
15
16
  import type { AssistantConfig } from "../../../../config/types.js";
16
17
  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,6 +99,47 @@ 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
+
102
143
  // ---------------------------------------------------------------------------
103
144
  // Shared helpers for delete / update / list
104
145
  // ---------------------------------------------------------------------------
@@ -25,6 +25,7 @@ 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";
28
29
  import * as hostWorkerCompute from "../../../util/worker-compute.js";
29
30
  import * as hostWorkerMemory from "../../../util/worker-memory.js";
30
31
 
@@ -76,6 +77,15 @@ export function truncate(str: string, maxLen: number, suffix?: string): string {
76
77
  : hostTruncate.truncate(str, maxLen, suffix);
77
78
  }
78
79
 
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
+
79
89
  export function workerMemoryEnv(): Record<string, string | undefined> {
80
90
  return hostWorkerMemory.workerMemoryEnv();
81
91
  }
@@ -374,10 +374,9 @@ const memoryV2StaticInjector: Injector = {
374
374
  if (mode !== "full") {
375
375
  return null;
376
376
  }
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.
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).
381
380
  const content = readGatedMemoryV2Static(ctx.trust, {
382
381
  excludeBuffer: ctx.callSite === "memoryV2Consolidation",
383
382
  });