fix(memory): preserve session corpus labels (#71898)

Summary:
- The PR updates memory-core `memory_search` result shaping to surface `corpus` from each hit's `source`, adds ... session corpus-label coverage, adds a changelog entry, and includes a small tempdir test assertion cleanup.
- Reproducibility: yes. Current main has a high-confidence source-level reproduction: session hits keep `sourc ... the final mapper hard-codes `corpus: "memory"`; the PR body also supplies live Gateway before/after output.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test(memory): clarify corpus label regression
- PR branch already contained follow-up commit before automerge: fix(memory): type session corpus results
- PR branch already contained follow-up commit before automerge: fix(memory): preserve session corpus labels
- PR branch already contained follow-up commit before automerge: fix(clawsweeper): address review for automerge-openclaw-openclaw-7189…

Validation:
- ClawSweeper review passed for head 02d0db0861.
- Required merge gates passed before the squash merge.

Prepared head SHA: 02d0db0861
Review: https://github.com/openclaw/openclaw/pull/71898#issuecomment-4340800992

Co-authored-by: Ruben Cuevas <hi@rubencu.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
Rubén Cuevas 2026-05-10 22:42:18 -04:00 committed by GitHub
parent 15cf49222f
commit 00bb0dde4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 85 additions and 7 deletions

View file

@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Memory: keep `memory_search` result `corpus` labels aligned with the hit source, so session transcript hits surface as `sessions` and memory-file hits stay `memory`. Fixes #72885. (#71898, #72886) Thanks @rubencu.
- Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while converting manifest catalog rows into emitted provider config, so `google/gemini-3.1-pro-preview` is used for testing instead of `google/gemini-3-pro-preview`.
- Native apps: advertise the Gateway protocol compatibility range so chat and node sessions can connect to v3 gateways after additive v4 client updates.
- Gateway/agents: keep stale `sessions_send` ACP manager and `web_fetch` runtime chunks importable after package updates, preventing live gateways from breaking before restart. Fixes #78804. Thanks @Gomesy72.

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
getMemorySearchManagerMockConfigs,
getMemorySearchManagerMockParams,
@ -13,6 +13,26 @@ import {
expectUnavailableMemorySearchDetails,
} from "./tools.test-helpers.js";
const sessionStore = vi.hoisted(() => ({
"agent:main:main": {
sessionId: "thread-1",
updatedAt: 1,
sessionFile: "/tmp/sessions/thread-1.jsonl",
},
}));
vi.mock("openclaw/plugin-sdk/session-transcript-hit", async (importOriginal) => {
const actual =
await importOriginal<typeof import("openclaw/plugin-sdk/session-transcript-hit")>();
return {
...actual,
loadCombinedSessionStoreForGateway: vi.fn(() => ({
storePath: "(test)",
store: sessionStore,
})),
};
});
describe("memory_search unavailable payloads", () => {
beforeEach(() => {
resetMemoryToolMockState({ searchImpl: async () => [] });
@ -107,6 +127,12 @@ describe("memory_search unavailable payloads", () => {
(result.details as { debug?: { searchMs?: number } }).debug?.searchMs,
).toBeGreaterThanOrEqual(0);
});
});
describe("memory_search corpus labels", () => {
beforeEach(() => {
resetMemoryToolMockState({ searchImpl: async () => [] });
});
it("uses explicit plugin context agent over synthetic active-memory session keys", async () => {
const tool = createMemorySearchToolOrThrow({
@ -170,4 +196,57 @@ describe("memory_search unavailable payloads", () => {
expect(getMemorySearchManagerMockConfigs()).toEqual([patchedConfig]);
});
it("preserves source corpus labels for memory and session transcript hits", async () => {
setMemorySearchImpl(async () => [
{
path: "MEMORY.md",
startLine: 3,
endLine: 4,
score: 0.95,
snippet: "Durable memory note",
source: "memory" as const,
},
{
path: "sessions/thread-1.jsonl",
startLine: 1,
endLine: 2,
score: 0.9,
snippet: "Thread transcript note",
source: "sessions" as const,
},
]);
const tool = createMemorySearchToolOrThrow({
config: {
agents: { list: [{ id: "main", default: true }] },
memory: { citations: "off" },
tools: { sessions: { visibility: "all" } },
},
agentSessionKey: "agent:main:main",
});
const result = await tool.execute("mixed", { query: "thread note" });
const details = result.details as { results: Array<{ corpus: string; path: string }> };
expect(details.results).toEqual([
{
corpus: "memory",
path: "MEMORY.md",
startLine: 3,
endLine: 4,
score: 0.95,
snippet: "Durable memory note",
source: "memory",
},
{
corpus: "sessions",
path: "sessions/thread-1.jsonl",
startLine: 1,
endLine: 2,
score: 0.9,
snippet: "Thread transcript note",
source: "sessions",
},
]);
});
});

View file

@ -37,7 +37,7 @@ import {
} from "./tools.shared.js";
type MemorySearchToolResult =
| (Record<string, unknown> & { corpus: "memory"; score: number; path: string })
| (MemorySearchResult & { corpus: MemorySource })
| MemoryCorpusSearchResult;
function sortMemorySearchToolResults<T extends { score: number; path: string }>(results: T[]): T[] {
@ -267,9 +267,7 @@ export function createMemorySearchTool(options: {
});
const searchStartedAt = Date.now();
let rawResults: MemorySearchResult[] = [];
let surfacedMemoryResults: Array<
Record<string, unknown> & { corpus: "memory"; score: number; path: string }
> = [];
let surfacedMemoryResults: Array<MemorySearchResult & { corpus: MemorySource }> = [];
let provider: string | undefined;
let model: string | undefined;
let fallback: unknown;
@ -326,7 +324,7 @@ export function createMemorySearchTool(options: {
: decorated;
surfacedMemoryResults = memoryResults.map((result) => ({
...result,
corpus: "memory" as const,
corpus: result.source,
}));
const sleepTimezone = resolveMemoryDeepDreamingConfig({
pluginConfig: resolveMemoryCorePluginConfig(cfg),

View file

@ -34,7 +34,7 @@ describe("image-ops temp dir", () => {
expect(prefix?.endsWith("-")).toBe(true);
const uuid = prefix?.slice(uuidPrefix.length, -1) ?? "";
expect(uuid).toHaveLength(36);
expect(Array.from(uuid).every((char) => /[0-9a-f-]/u.test(char))).toBe(true);
expect(/^[0-9a-f-]+$/u.test(uuid)).toBe(true);
expect([8, 13, 18, 23].map((index) => uuid[index])).toEqual(["-", "-", "-", "-"]);
expect(path.dirname(prefix ?? "")).toBe(secureRoot);
expect(createdTempDir.startsWith(prefix ?? "")).toBe(true);