diff --git a/plugin/skills/agentmemory-config/REFERENCE.md b/plugin/skills/agentmemory-config/REFERENCE.md index ed128793d..d152a7c62 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). 42 recognized variables: +Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 43 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..ba47c3ef7 100644 --- a/test/index-persistence.test.ts +++ b/test/index-persistence.test.ts @@ -791,3 +791,94 @@ describe("IndexPersistence", () => { await expect(persistence.load()).resolves.toBeDefined(); }); }); + +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(); + if (previousFlag === undefined) { + delete process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST; + } else { + process.env.AGENTMEMORY_AUDIT_INDEX_PERSIST = previousFlag; + } + }); + + 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.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(); + + 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( + 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); + }); +});