Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion plugin/skills/agentmemory-config/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- AUTOGEN:env START - generated by scripts/skills/generate.ts, do not edit by hand -->
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`
Expand Down
15 changes: 15 additions & 0 deletions src/state/index-persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -271,6 +285,7 @@ export class IndexPersistence {
targetIds: string[],
details: Record<string, unknown>,
): Promise<void> {
if (!auditIndexPersistEnabled()) return;
await safeAudit(
this.kv,
"index_persist",
Expand Down
91 changes: 91 additions & 0 deletions test/index-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,3 +791,94 @@ describe("IndexPersistence", () => {
await expect(persistence.load()).resolves.toBeDefined();
});
});

describe("index_persist audit gating", () => {
let kv: ReturnType<typeof mockKV>;
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;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async function indexPersistEntries(): Promise<Array<{ operation: string }>> {
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);
});
});