diff --git a/CHANGELOG.md b/CHANGELOG.md index 34087d86c28..8b5eceabcc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/memory-core/src/tools.test.ts b/extensions/memory-core/src/tools.test.ts index e0336eadbf0..9db5e8e1790 100644 --- a/extensions/memory-core/src/tools.test.ts +++ b/extensions/memory-core/src/tools.test.ts @@ -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(); + 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", + }, + ]); + }); }); diff --git a/extensions/memory-core/src/tools.ts b/extensions/memory-core/src/tools.ts index 56216bf932e..c9a7fba1295 100644 --- a/extensions/memory-core/src/tools.ts +++ b/extensions/memory-core/src/tools.ts @@ -37,7 +37,7 @@ import { } from "./tools.shared.js"; type MemorySearchToolResult = - | (Record & { corpus: "memory"; score: number; path: string }) + | (MemorySearchResult & { corpus: MemorySource }) | MemoryCorpusSearchResult; function sortMemorySearchToolResults(results: T[]): T[] { @@ -267,9 +267,7 @@ export function createMemorySearchTool(options: { }); const searchStartedAt = Date.now(); let rawResults: MemorySearchResult[] = []; - let surfacedMemoryResults: Array< - Record & { corpus: "memory"; score: number; path: string } - > = []; + let surfacedMemoryResults: Array = []; 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), diff --git a/src/media/image-ops.tempdir.test.ts b/src/media/image-ops.tempdir.test.ts index 83090cd736c..5ef0de93c52 100644 --- a/src/media/image-ops.tempdir.test.ts +++ b/src/media/image-ops.tempdir.test.ts @@ -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);