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
46 changes: 46 additions & 0 deletions src/core/store/tcvdb.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { TcvdbMemoryStore } from "./tcvdb.js";

function createStoreWithCapturedQueryFilters(): {
store: TcvdbMemoryStore;
filters: string[];
} {
const filters: string[] = [];
const store = new TcvdbMemoryStore({
url: "http://127.0.0.1",
username: "root",
apiKey: "test-key",
database: "testdb",
embeddingModel: "bge-large-zh",
timeout: 1000,
});

Object.assign(store as unknown as Record<string, unknown>, {
client: {
query: async (_collection: string, params: Record<string, unknown>) => {
filters.push(String(params.filter ?? ""));
return { documents: [] };
},
},
_initPromise: Promise.resolve(),
});

return { store, filters };
}

describe("TcvdbMemoryStore filter expressions", () => {
it("escapes session string literals before building query filters", async () => {
const { store, filters } = createStoreWithCapturedQueryFilters();
const sessionKey = 'alpha" or session_key = "beta';
const sessionId = 'sid\\quote" or session_id = "other';
const updatedAfter = "2026-01-02T03:04:05.000Z";

await store.queryL1Records({ sessionKey, sessionId, updatedAfter });
await store.queryL0ForL1(sessionKey, 123);

expect(filters).toEqual([
`session_key = "alpha\\" or session_key = \\"beta" and session_id = "sid\\\\quote\\" or session_id = \\"other" and updated_time_ms > ${Date.parse(updatedAfter)}`,
`session_key = "alpha\\" or session_key = \\"beta" and recorded_at_ms > 123`,
]);
});
});
10 changes: 7 additions & 3 deletions src/core/store/tcvdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ function epochMsToIso(ms: number): string {
return new Date(ms).toISOString();
}

function tcvdbStringLiteral(value: string): string {
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
}

/**
* Extract agent ID from a sessionKey like `agent:<agentId>:<channel>`.
* Returns empty string if the format doesn't match.
Expand Down Expand Up @@ -573,8 +577,8 @@ export class TcvdbMemoryStore implements IMemoryStore {

// Build TCVDB filter expression from L1QueryFilter
const conditions: string[] = [];
if (filter?.sessionKey) conditions.push(`session_key = "${filter.sessionKey}"`);
if (filter?.sessionId) conditions.push(`session_id = "${filter.sessionId}"`);
if (filter?.sessionKey) conditions.push(`session_key = ${tcvdbStringLiteral(filter.sessionKey)}`);
if (filter?.sessionId) conditions.push(`session_id = ${tcvdbStringLiteral(filter.sessionId)}`);
if (filter?.updatedAfter) {
const afterMs = isoToEpochMs(filter.updatedAfter);
if (afterMs > 0) conditions.push(`updated_time_ms > ${afterMs}`);
Expand Down Expand Up @@ -873,7 +877,7 @@ export class TcvdbMemoryStore implements IMemoryStore {
await this._ensureInit();
if (this.degraded) return [];

const conditions: string[] = [`session_key = "${sessionKey}"`];
const conditions: string[] = [`session_key = ${tcvdbStringLiteral(sessionKey)}`];
if (afterRecordedAtMs && afterRecordedAtMs > 0) {
conditions.push(`recorded_at_ms > ${afterRecordedAtMs}`);
}
Expand Down
Loading