Skip to content

Commit ffb2271

Browse files
committed
fix(memory): prefer exact qmd archive identities
1 parent 4109ab0 commit ffb2271

6 files changed

Lines changed: 131 additions & 29 deletions

File tree

extensions/memory-core/src/session-search-visibility.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,71 @@ describe("filterMemorySearchHitsBySessionVisibility", () => {
392392
expect(filtered).toEqual([hit]);
393393
});
394394

395+
it("keeps QMD .md hits whose live session id looks like an archive name", async () => {
396+
const sessionId = "foo.jsonl.deleted.2026-02-16T22-27-33.000Z";
397+
combinedSessionStore = {
398+
"agent:main:archive-looking": {
399+
sessionId,
400+
updatedAt: 1,
401+
sessionFile: `/tmp/sessions/${sessionId}.jsonl`,
402+
},
403+
};
404+
const hit: MemorySearchResult = {
405+
path: `qmd/sessions-main/${sessionId}.md`,
406+
source: "sessions",
407+
score: 1,
408+
snippet: "x",
409+
startLine: 1,
410+
endLine: 2,
411+
};
412+
const cfg = asOpenClawConfig({
413+
tools: {
414+
sessions: { visibility: "self" },
415+
},
416+
});
417+
418+
const filtered = await filterMemorySearchHitsBySessionVisibility({
419+
cfg,
420+
requesterSessionKey: "agent:main:archive-looking",
421+
sandboxed: false,
422+
hits: [hit],
423+
});
424+
425+
expect(filtered).toEqual([hit]);
426+
});
427+
428+
it("does not authorize QMD archived .md hits through lossy slug fallback", async () => {
429+
combinedSessionStore = {
430+
"agent:main:foo_bar": {
431+
sessionId: "foo_bar",
432+
updatedAt: 1,
433+
sessionFile: "/tmp/sessions/foo_bar.jsonl",
434+
},
435+
};
436+
const hit: MemorySearchResult = {
437+
path: "qmd/sessions-main/foo-bar-jsonl-deleted-2026-02-16t22-26-33-000z.md",
438+
source: "sessions",
439+
score: 1,
440+
snippet: "x",
441+
startLine: 1,
442+
endLine: 2,
443+
};
444+
const cfg = asOpenClawConfig({
445+
tools: {
446+
sessions: { visibility: "self" },
447+
},
448+
});
449+
450+
const filtered = await filterMemorySearchHitsBySessionVisibility({
451+
cfg,
452+
requesterSessionKey: "agent:main:foo_bar",
453+
sandboxed: false,
454+
hits: [hit],
455+
});
456+
457+
expect(filtered).toStrictEqual([]);
458+
});
459+
395460
it("keeps same-agent QMD archived deleted .md hits when no store entry remains", async () => {
396461
combinedSessionStore = {};
397462
const hit: MemorySearchResult = {

extensions/memory-core/src/session-search-visibility.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,25 @@ export async function filterMemorySearchHitsBySessionVisibility(params: {
108108
const archivedOwnerAgentId = archivedOwnerMatchesScope
109109
? (identity.ownerAgentId ?? scopedAgentId)
110110
: undefined;
111+
const liveKeys = identity.liveStem
112+
? resolveTranscriptStemToSessionKeys({
113+
store: combinedSessionStore,
114+
stem: identity.liveStem,
115+
allowQmdSlugFallback: false,
116+
})
117+
: [];
111118
const keys = filterSessionKeysByScopedAgent({
112119
cfg: params.cfg,
113120
scopedAgentId,
114-
keys: resolveTranscriptStemToSessionKeys({
115-
store: combinedSessionStore,
116-
stem: identity.stem,
117-
allowQmdSlugFallback: isQmdSessionHit,
118-
...(archivedOwnerAgentId ? { archivedOwnerAgentId } : {}),
119-
}),
121+
keys:
122+
liveKeys.length > 0
123+
? liveKeys
124+
: resolveTranscriptStemToSessionKeys({
125+
store: combinedSessionStore,
126+
stem: identity.stem,
127+
allowQmdSlugFallback: isQmdSessionHit && !identity.archived,
128+
...(archivedOwnerAgentId ? { archivedOwnerAgentId } : {}),
129+
}),
120130
});
121131
if (keys.length === 0) {
122132
continue;

extensions/memory-wiki/src/query.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ describe("searchMemoryWiki", () => {
820820
]);
821821
});
822822

823-
it("keeps QMD-slugified archived session search hits inside visibility policy", async () => {
823+
it("keeps QMD archived session search hits inside visibility policy", async () => {
824824
const { config } = await createQueryVault({
825825
initialize: true,
826826
config: {
@@ -830,25 +830,25 @@ describe("searchMemoryWiki", () => {
830830
loadCombinedSessionStoreForGatewayMock.mockReturnValue({
831831
storePath: "(test)",
832832
store: {
833-
"agent:main:foo_bar.v1": {
834-
sessionId: "foo_bar.v1",
833+
"agent:main:abc-uuid": {
834+
sessionId: "abc-uuid",
835835
updatedAt: 1,
836-
sessionFile: "/tmp/openclaw/foo_bar.v1.jsonl",
836+
sessionFile: "/tmp/openclaw/abc-uuid.jsonl",
837837
},
838838
},
839839
});
840840
const manager = createMemoryManager({
841841
searchResults: [
842842
{
843-
path: "qmd/sessions-main/foo-bar-v1-jsonl-reset-2026-02-16t22-26-33-000z.md",
843+
path: "qmd/sessions-main/abc-uuid-jsonl-reset-2026-02-16t22-26-33-000z.md",
844844
startLine: 1,
845845
endLine: 2,
846846
score: 30,
847847
snippet: "archived transcript",
848848
source: "sessions",
849849
},
850850
{
851-
path: "foo-bar-v1-jsonl-reset-2026-02-16t22-26-33-000z.md",
851+
path: "abc-uuid-jsonl-reset-2026-02-16t22-26-33-000z.md",
852852
startLine: 3,
853853
endLine: 4,
854854
score: 20,
@@ -862,14 +862,14 @@ describe("searchMemoryWiki", () => {
862862
const results = await searchMemoryWiki({
863863
config,
864864
appConfig: createSessionVisibilityAppConfig(),
865-
agentSessionKey: "agent:main:foo_bar.v1",
865+
agentSessionKey: "agent:main:abc-uuid",
866866
sandboxed: true,
867867
query: "transcript",
868868
maxResults: 10,
869869
});
870870

871871
expect(results.map((result) => result.path)).toEqual([
872-
"qmd/sessions-main/foo-bar-v1-jsonl-reset-2026-02-16t22-26-33-000z.md",
872+
"qmd/sessions-main/abc-uuid-jsonl-reset-2026-02-16t22-26-33-000z.md",
873873
]);
874874
});
875875

extensions/memory-wiki/src/query.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,15 +1310,25 @@ async function createSessionMemoryPathVisibilityChecker(params: {
13101310
const archivedOwnerAgentId = archivedOwnerMatchesScope
13111311
? (identity.ownerAgentId ?? scopedAgentId)
13121312
: undefined;
1313+
const liveKeys = identity.liveStem
1314+
? resolveTranscriptStemToSessionKeys({
1315+
store: combinedSessionStore,
1316+
stem: identity.liveStem,
1317+
allowQmdSlugFallback: false,
1318+
})
1319+
: [];
13131320
const keys = filterSessionKeysByScopedAgent({
13141321
cfg: params.cfg,
13151322
scopedAgentId,
1316-
keys: resolveTranscriptStemToSessionKeys({
1317-
store: combinedSessionStore,
1318-
stem: identity.stem,
1319-
allowQmdSlugFallback: isQmdSessionPath,
1320-
...(archivedOwnerAgentId ? { archivedOwnerAgentId } : {}),
1321-
}),
1323+
keys:
1324+
liveKeys.length > 0
1325+
? liveKeys
1326+
: resolveTranscriptStemToSessionKeys({
1327+
store: combinedSessionStore,
1328+
stem: identity.stem,
1329+
allowQmdSlugFallback: isQmdSessionPath && !identity.archived,
1330+
...(archivedOwnerAgentId ? { archivedOwnerAgentId } : {}),
1331+
}),
13221332
});
13231333
if (!guard) {
13241334
return Boolean(scopedAgentId && keys.length > 0);

src/plugin-sdk/session-transcript-hit.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ describe("extractTranscriptStemFromSessionsMemoryHit", () => {
8080
);
8181
expect(identity).toEqual({
8282
stem: "abc-uuid",
83+
liveStem: "abc-uuid-jsonl-reset-2026-02-16T22-26-33.000Z",
8384
archived: true,
8485
});
8586
});
@@ -106,6 +107,7 @@ describe("extractTranscriptStemFromSessionsMemoryHit", () => {
106107
);
107108
expect(identity).toEqual({
108109
stem: "abc-uuid",
110+
liveStem: "abc-uuid.jsonl.reset.2026-02-16T22-26-33.000Z",
109111
archived: true,
110112
});
111113
});
@@ -120,6 +122,16 @@ describe("extractTranscriptStemFromSessionsMemoryHit", () => {
120122
});
121123
});
122124

125+
it("does not treat non-QMD .md names with archive-looking timestamps as archives", () => {
126+
const identity = extractTranscriptIdentityFromSessionsMemoryHit(
127+
"abc-uuid-jsonl-reset-2026-02-16t22-26-33-000z.md",
128+
);
129+
expect(identity).toEqual({
130+
stem: "abc-uuid-jsonl-reset-2026-02-16t22-26-33-000z",
131+
archived: false,
132+
});
133+
});
134+
123135
it("does not mistake arbitrary suffixes containing .jsonl. for archives", () => {
124136
// Not a real archive pattern: suffix after .jsonl. must be `reset` or `deleted`.
125137
expect(
@@ -148,6 +160,7 @@ describe("extractTranscriptIdentityFromSessionsMemoryHit", () => {
148160
),
149161
).toEqual({
150162
stem: "deleted-uuid",
163+
liveStem: "deleted-uuid-jsonl-deleted-2026-02-16t22-27-33-000z",
151164
archived: true,
152165
});
153166
});

src/plugin-sdk/session-transcript-hit.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function normalizeQmdSessionStem(stem: string): string {
4040

4141
export type SessionTranscriptHitIdentity = {
4242
stem: string;
43+
liveStem?: string;
4344
ownerAgentId?: string;
4445
archived: boolean;
4546
};
@@ -71,6 +72,7 @@ export function extractTranscriptStemFromSessionsMemoryHit(hitPath: string): str
7172
export function extractTranscriptIdentityFromSessionsMemoryHit(
7273
hitPath: string,
7374
): SessionTranscriptHitIdentity | null {
75+
const isQmdPath = hitPath.replace(/\\/g, "/").startsWith("qmd/");
7476
const { base, ownerAgentId } = parseSessionsPath(hitPath);
7577
const archivedStem = parseUsageCountedSessionIdFromFileName(base);
7678
if (archivedStem && base !== `${archivedStem}.jsonl`) {
@@ -85,15 +87,17 @@ export function extractTranscriptIdentityFromSessionsMemoryHit(
8587
if (!mdStem) {
8688
return null;
8789
}
88-
const exportedArchiveStem = parseUsageCountedSessionIdFromFileName(mdStem);
89-
if (exportedArchiveStem && mdStem !== `${exportedArchiveStem}.jsonl`) {
90-
return { stem: exportedArchiveStem, ownerAgentId, archived: true };
91-
}
92-
const restoredArchiveName = restoreQmdNormalizedArchiveName(mdStem);
93-
if (restoredArchiveName) {
94-
const archivedStem = parseUsageCountedSessionIdFromFileName(restoredArchiveName);
95-
if (archivedStem && restoredArchiveName !== `${archivedStem}.jsonl`) {
96-
return { stem: archivedStem, ownerAgentId, archived: true };
90+
if (isQmdPath) {
91+
const exportedArchiveStem = parseUsageCountedSessionIdFromFileName(mdStem);
92+
if (exportedArchiveStem && mdStem !== `${exportedArchiveStem}.jsonl`) {
93+
return { stem: exportedArchiveStem, liveStem: mdStem, ownerAgentId, archived: true };
94+
}
95+
const restoredArchiveName = restoreQmdNormalizedArchiveName(mdStem);
96+
if (restoredArchiveName) {
97+
const archivedStem = parseUsageCountedSessionIdFromFileName(restoredArchiveName);
98+
if (archivedStem && restoredArchiveName !== `${archivedStem}.jsonl`) {
99+
return { stem: archivedStem, liveStem: mdStem, ownerAgentId, archived: true };
100+
}
97101
}
98102
}
99103
return { stem: mdStem, ownerAgentId, archived: false };

0 commit comments

Comments
 (0)