Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
48 changes: 34 additions & 14 deletions apps/memos-local-plugin/core/pipeline/memory-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2630,22 +2630,42 @@ export function createMemoryCore(
const needle = (input?.q ?? "").trim().toLowerCase();
const visible = (r: TraceRow) => visibleToCurrent(r);
if (!needle) {
const rows = handle.repos.traces.list({ sessionId: input?.sessionId, limit: 100_000 }).filter(visible);
if (!input?.groupByTurn) return rows.length;
const turnKeys = new Set<string>();
for (const r of rows) turnKeys.add(`${r.episodeId ?? "_"}:${r.turnId}`);
return turnKeys.size;
// Use a dedicated SELECT COUNT(*) instead of `list().length`.
// The previous implementation called `repos.traces.list({ limit: 100_000 })`
// and counted the returned rows, but `buildPageClauses` silently clamps
// every list `limit` to 500. That capped the Memories total at 500
// even when the database held 1400+ traces (#1593). The repo's
// `count` / `countTurns` issue real COUNT queries with no page-size
// cap, so they return the actual total.
return input?.groupByTurn
? handle.repos.traces.countTurns({ sessionId: input?.sessionId })
: handle.repos.traces.count({ sessionId: input?.sessionId });
}
// q substring scan — mirror `listTraces`. Walk all matching
// traces from the repo (no limit) and apply the same filter.
const rows = handle.repos.traces.list({ sessionId: input?.sessionId }).filter(visible);
const matched = rows.filter((r) => {
return traceSearchHaystack(r).includes(needle);
});
if (!input?.groupByTurn) return matched.length;
const turnKeys = new Set<string>();
for (const r of matched) turnKeys.add(`${r.episodeId ?? "_"}:${r.turnId}`);
return turnKeys.size;
// traces from the repo and apply the same filter. We page through
// explicitly because the underlying `list` clamps each call at
// 500 rows, which would otherwise silently truncate the count.
const matchedTurnKeys = new Set<string>();
let matchedCount = 0;
const PAGE = 500;
for (let offset = 0; ; offset += PAGE) {
const page = handle.repos.traces.list({
sessionId: input?.sessionId,
limit: PAGE,
offset,
});
if (page.length === 0) break;
for (const r of page) {
if (!visible(r)) continue;
if (!traceSearchHaystack(r).includes(needle)) continue;
matchedCount++;
if (input?.groupByTurn) {
matchedTurnKeys.add(`${r.episodeId ?? "_"}:${r.turnId}`);
}
}
if (page.length < PAGE) break;
}
return input?.groupByTurn ? matchedTurnKeys.size : matchedCount;
}

async function listTraces(input?: {
Expand Down
68 changes: 68 additions & 0 deletions apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,74 @@ describe("MemoryCore façade", () => {
expect(grouped.map((tr) => tr.id)).toEqual(["tr-late", "tr-early"]);
});

it("countTraces returns the real total even when more than 500 rows exist (regression for #1593)", async () => {
pipeline = createPipeline(buildDeps(db!));
core = createMemoryCore(
pipeline,
resolveHome("openclaw", "/tmp/memos-mc-test"),
"test",
);
db!.repos.sessions.upsert({
id: "s-count",
agent: "openclaw",
ownerAgentKind: "openclaw",
ownerProfileId: "main",
ownerWorkspaceId: null,
startedAt: 1_000,
lastSeenAt: 2_000,
meta: {},
});
db!.repos.episodes.insert({
id: "ep-count",
sessionId: "s-count",
ownerAgentKind: "openclaw",
ownerProfileId: "main",
ownerWorkspaceId: null,
startedAt: 1_000,
endedAt: 2_000,
traceIds: [] as never,
rTask: null,
status: "closed",
meta: {},
});
const baseTrace = {
episodeId: "ep-count",
sessionId: "s-count",
ownerAgentKind: "openclaw",
ownerProfileId: "main",
ownerWorkspaceId: null,
userText: "u",
agentText: "a",
summary: null,
toolCalls: [],
reflection: null,
agentThinking: null,
value: 0,
alpha: 0,
rHuman: null,
priority: 0,
tags: [],
errorSignatures: [],
vecSummary: null,
vecAction: null,
schemaVersion: 1,
} as const;
// Insert 600 rows split across 6 turns so we cross the 500-row
// page-size cap that previously truncated the count.
for (let i = 0; i < 600; i++) {
db!.repos.traces.insert({
...baseTrace,
id: `tr-count-${i}`,
ts: 1_000 + i,
turnId: Math.floor(i / 100), // 6 distinct turns
} as never);
}

await core.init();
expect(await core.countTraces({})).toBe(600);
expect(await core.countTraces({ groupByTurn: true })).toBe(6);
});

it("deleteTrace removes FTS entries and episode trace references", async () => {
pipeline = createPipeline(buildDeps(db!));
core = createMemoryCore(
Expand Down
Loading