From 77515a64a2aaa36961eb8c18ed6a3912c8ecc7a2 Mon Sep 17 00:00:00 2001 From: Dmitrii Zhukov Date: Sun, 2 Aug 2026 15:14:56 +0700 Subject: [PATCH 1/2] fix(audit): make index_persist audit entries opt-in Signed-off-by: Dmitrii Zhukov --- plugin/skills/agentmemory-config/REFERENCE.md | 3 +- src/state/index-persistence.ts | 15 +++++ test/index-persistence.test.ts | 58 +++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/plugin/skills/agentmemory-config/REFERENCE.md b/plugin/skills/agentmemory-config/REFERENCE.md index d12aaed73..45081ca6a 100644 --- a/plugin/skills/agentmemory-config/REFERENCE.md +++ b/plugin/skills/agentmemory-config/REFERENCE.md @@ -3,10 +3,11 @@ Generated by scanning `src/` for `AGENTMEMORY_*` usage. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing a variable. Internal markers ending in two underscores are excluded. -Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 37 recognized variables: +Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 38 recognized variables: - `AGENTMEMORY_AGENT_SCOPE` - `AGENTMEMORY_ALLOW_AGENT_SDK` +- `AGENTMEMORY_AUDIT_INDEX_PERSIST` - `AGENTMEMORY_AUTO_COMPRESS` - `AGENTMEMORY_COMMIT_SHA` - `AGENTMEMORY_CONSOLIDATION_COOLDOWN_MS` diff --git a/src/state/index-persistence.ts b/src/state/index-persistence.ts index 6df0e2fda..19b05cb59 100644 --- a/src/state/index-persistence.ts +++ b/src/state/index-persistence.ts @@ -17,6 +17,20 @@ const VECTOR_SHARD_SCOPE_PREFIX = `${KV.bm25Index}:vectors:`; const INDEX_SHARD_KEY = "data"; const DEFAULT_INDEX_SHARD_CHARS = 2_000_000; +// mem:audit exists to record structural deletions of user data — that is +// the policy stated at the top of src/functions/audit.ts. Index shard +// writes and manifest publishes remove no user rows, so they fall outside +// it, yet a single save() emits three of them: on a real store they +// reached 59876 of 84028 entries (71%), which is what makes the audit log +// slow to query and bloats startup. Off by default; set +// AGENTMEMORY_AUDIT_INDEX_PERSIST=1 when debugging index persistence. +function auditIndexPersistEnabled(): boolean { + const raw = process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST; + if (!raw) return false; + const normalized = raw.trim().toLowerCase(); + return normalized === "1" || normalized === "true"; +} + type IndexShardManifest = { v: 1; generation?: string; @@ -271,6 +285,7 @@ export class IndexPersistence { targetIds: string[], details: Record, ): Promise { + if (!auditIndexPersistEnabled()) return; await safeAudit( this.kv, "index_persist", diff --git a/test/index-persistence.test.ts b/test/index-persistence.test.ts index 929791657..c8065cb0a 100644 --- a/test/index-persistence.test.ts +++ b/test/index-persistence.test.ts @@ -791,3 +791,61 @@ describe("IndexPersistence", () => { await expect(persistence.load()).resolves.toBeDefined(); }); }); + +describe("index_persist audit gating", () => { + let kv: ReturnType; + + beforeEach(() => { + vi.useFakeTimers(); + kv = mockKV(); + }); + + afterEach(() => { + vi.useRealTimers(); + delete process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST; + }); + + async function indexPersistEntries(): Promise> { + const entries = await kv.list<{ operation: string }>("mem:audit"); + return entries.filter((entry) => entry.operation === "index_persist"); + } + + it("writes no index_persist audit entries by default", async () => { + const persistence = new IndexPersistence( + kv as never, + makeBm25("obs_1", "auth handler"), + null, + ); + + await persistence.save(); + + expect(await indexPersistEntries()).toEqual([]); + }); + + it("writes index_persist audit entries when explicitly enabled", async () => { + process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST = "1"; + const persistence = new IndexPersistence( + kv as never, + makeBm25("obs_1", "auth handler"), + null, + ); + + await persistence.save(); + + expect((await indexPersistEntries()).length).toBeGreaterThan(0); + }); + + it("still persists the index when auditing is off", async () => { + const persistence = new IndexPersistence( + kv as never, + makeBm25("obs_1", "auth handler"), + null, + ); + + await persistence.save(); + + const loaded = await persistence.load(); + expect(loaded.bm25).not.toBeNull(); + expect(loaded.bm25!.size).toBe(1); + }); +}); From 887350ee8d21383f7f4ab892d9291ad62f830387 Mon Sep 17 00:00:00 2001 From: Dmitrii Zhukov Date: Mon, 17 Aug 2026 11:29:09 +0700 Subject: [PATCH 2/2] test(audit): isolate the index_persist gate tests from the ambient env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTMEMORY_* variables are documented as living in ~/.agentmemory/.env, so the suite can inherit AGENTMEMORY_AUDIT_INDEX_PERSIST from a configured machine. The default-off case then ran with auditing on and failed, and afterEach deleted whatever value the developer had set. Capture and restore around each case, and cover the whole vocabulary the parser accepts — trimmed and case-insensitive 1/true — plus the values an operator would reach for to turn it off. Signed-off-by: Dmitrii Zhukov --- test/index-persistence.test.ts | 55 +++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/test/index-persistence.test.ts b/test/index-persistence.test.ts index c8065cb0a..ba47c3ef7 100644 --- a/test/index-persistence.test.ts +++ b/test/index-persistence.test.ts @@ -794,15 +794,26 @@ describe("IndexPersistence", () => { describe("index_persist audit gating", () => { let kv: ReturnType; + let previousFlag: string | undefined; beforeEach(() => { + // AGENTMEMORY_* variables are documented as living in + // ~/.agentmemory/.env, so a developer running the suite on a + // configured machine can inherit this one. Clear it going in and put + // whatever was there back on the way out. + previousFlag = process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST; + delete process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST; vi.useFakeTimers(); kv = mockKV(); }); afterEach(() => { vi.useRealTimers(); - delete process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST; + if (previousFlag === undefined) { + delete process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST; + } else { + process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST = previousFlag; + } }); async function indexPersistEntries(): Promise> { @@ -822,18 +833,40 @@ describe("index_persist audit gating", () => { expect(await indexPersistEntries()).toEqual([]); }); - it("writes index_persist audit entries when explicitly enabled", async () => { - process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST = "1"; - const persistence = new IndexPersistence( - kv as never, - makeBm25("obs_1", "auth handler"), - null, - ); + it.each(["1", " 1 ", "true", "TRUE", " true "])( + "writes index_persist audit entries when set to %j", + async (value) => { + process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST = value; + const persistence = new IndexPersistence( + kv as never, + makeBm25("obs_1", "auth handler"), + null, + ); - await persistence.save(); + await persistence.save(); - expect((await indexPersistEntries()).length).toBeGreaterThan(0); - }); + expect((await indexPersistEntries()).length).toBeGreaterThan(0); + }, + ); + + // Anything that is not an affirmative stays off. "0" and "false" are the + // ones an operator is likely to reach for to disable it, and they must + // not read as "present, therefore enabled". + it.each(["0", "false", "yes", "", " "])( + "keeps auditing off when set to %j", + async (value) => { + process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST = value; + const persistence = new IndexPersistence( + kv as never, + makeBm25("obs_1", "auth handler"), + null, + ); + + await persistence.save(); + + expect(await indexPersistEntries()).toEqual([]); + }, + ); it("still persists the index when auditing is off", async () => { const persistence = new IndexPersistence(