From 78029e43d2d4e71ead075308ec9031152956cb58 Mon Sep 17 00:00:00 2001 From: JC Date: Sun, 28 Jun 2026 13:55:27 -0700 Subject: [PATCH] fix(doctor): warn and document QMD session recall gates (#80947) * Warn on QMD session recall export mismatch * Clarify session transcript recall gates --- docs/concepts/memory-qmd.md | 20 ++++- docs/concepts/memory-search.md | 12 ++- docs/reference/memory-config.md | 62 ++++++++++++++- src/commands/doctor-memory-search.test.ts | 93 +++++++++++++++++++++++ src/commands/doctor-memory-search.ts | 18 +++++ 5 files changed, 201 insertions(+), 4 deletions(-) diff --git a/docs/concepts/memory-qmd.md b/docs/concepts/memory-qmd.md index 005bcfffa8ff..bf609ba2806e 100644 --- a/docs/concepts/memory-qmd.md +++ b/docs/concepts/memory-qmd.md @@ -155,10 +155,19 @@ collection root. ## Indexing session transcripts -Enable session indexing to recall earlier conversations: +Enable session indexing to recall earlier conversations. QMD needs both the general +`memorySearch` session source and the QMD transcript exporter: ```json5 { + agents: { + defaults: { + memorySearch: { + experimental: { sessionMemory: true }, + sources: ["memory", "sessions"], + }, + }, + }, memory: { backend: "qmd", qmd: { @@ -169,7 +178,14 @@ Enable session indexing to recall earlier conversations: ``` Transcripts are exported as sanitized User/Assistant turns into a dedicated QMD -collection under `~/.openclaw/agents//qmd/sessions/`. +collection under `~/.openclaw/agents//qmd/sessions/`. Setting only +`memorySearch.experimental.sessionMemory` does not export transcripts into QMD. + +Session hits are still filtered by +[`tools.sessions.visibility`](/gateway/config-tools#toolssessions). The default +`tree` visibility does not expose unrelated same-agent sessions. If a +gateway-dispatched session should be recallable from a separate DM session, set +`tools.sessions.visibility: "agent"` intentionally. ## Search scope diff --git a/docs/concepts/memory-search.md b/docs/concepts/memory-search.md index b7c9996f69d0..fe39546af929 100644 --- a/docs/concepts/memory-search.md +++ b/docs/concepts/memory-search.md @@ -143,7 +143,17 @@ setup. You can optionally index session transcripts so `memory_search` can recall earlier conversations. This is opt-in via -`memorySearch.experimental.sessionMemory`. See the +`memorySearch.experimental.sessionMemory` and `sources: ["sessions"]`; the default +source list is memory-only. The experimental flag enables session transcript +indexing, while `sources` controls whether session chunks are searched. + +Session hits obey `tools.sessions.visibility`: the default `tree` setting only +exposes the current session and sessions it spawned. To recall an unrelated +same-agent gateway-dispatched session from a separate DM session, intentionally +widen visibility to `agent`. + +When using QMD, also set `memory.qmd.sessions.enabled: true` so transcripts are +exported into a QMD collection. See the [configuration reference](/reference/memory-config) for details. ## Troubleshooting diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index e4af5d1e3800..94189a05060a 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -445,6 +445,66 @@ Index session transcripts and surface them via `memory_search`: Session indexing is opt-in and runs asynchronously. Results can be slightly stale. Session logs live on disk, so treat filesystem access as the trust boundary. +Session transcript hits also obey +[`tools.sessions.visibility`](/gateway/config-tools#toolssessions). The default +`tree` visibility only exposes the current session and sessions it spawned. To +recall an unrelated same-agent gateway-dispatched session from a different +session, such as a DM, intentionally widen visibility to `agent` (or `all` only +when cross-agent recall is also required and agent-to-agent policy allows it). + +The examples below place these settings under `agents.defaults`. You can also +apply equivalent `memorySearch` settings in a per-agent override when only one +agent should index and search session transcripts. + +For same-agent gateway-to-DM recall: + + + + ```json5 + { + agents: { + defaults: { + memorySearch: { + experimental: { sessionMemory: true }, + sources: ["memory", "sessions"], + }, + }, + }, + tools: { + sessions: { visibility: "agent" }, + }, + } + ``` + + + ```json5 + { + agents: { + defaults: { + memorySearch: { + experimental: { sessionMemory: true }, + sources: ["memory", "sessions"], + }, + }, + }, + memory: { + backend: "qmd", + qmd: { + sessions: { enabled: true }, + }, + }, + tools: { + sessions: { visibility: "agent" }, + }, + } + ``` + + + +When using QMD, `agents.defaults.memorySearch.experimental.sessionMemory` and +`sources: ["sessions"]` do not by themselves export transcripts into QMD. Set +`memory.qmd.sessions.enabled: true` as well. + --- ## SQLite vector acceleration (sqlite-vec) @@ -480,7 +540,7 @@ Set `memory.backend = "qmd"` to enable. All QMD settings live under `memory.qmd` | `rerank` | `boolean` | -- | Set to `false` with `searchMode: "query"` and QMD 2.1+ to skip QMD reranking | | `includeDefaultMemory` | `boolean` | `true` | Auto-index `MEMORY.md` + `memory/**/*.md` | | `paths[]` | `array` | -- | Extra paths: `{ name, path, pattern? }` | -| `sessions.enabled` | `boolean` | `false` | Index session transcripts | +| `sessions.enabled` | `boolean` | `false` | Export session transcripts into QMD | | `sessions.retentionDays` | `number` | -- | Transcript retention | | `sessions.exportDir` | `string` | -- | Export directory | diff --git a/src/commands/doctor-memory-search.test.ts b/src/commands/doctor-memory-search.test.ts index 3e170a173777..9eb635cd627f 100644 --- a/src/commands/doctor-memory-search.test.ts +++ b/src/commands/doctor-memory-search.test.ts @@ -545,6 +545,99 @@ describe("noteMemorySearchHealth", () => { expect(message).not.toContain("npm install -g @tobilu/qmd"); }); + it("warns when QMD backend uses session sources but QMD session export is disabled", async () => { + const qmdCfg = { memory: { backend: "qmd", qmd: { command: "qmd" } } } as OpenClawConfig; + resolveMemorySearchConfig.mockReturnValue({ + provider: "auto", + sources: ["memory", "sessions"], + experimental: { sessionMemory: true }, + local: {}, + remote: {}, + }); + + await noteMemorySearchHealth(qmdCfg, {}); + + expect(note).toHaveBeenCalledTimes(1); + const message = String(note.mock.calls[0]?.[0] ?? ""); + expect(message).toContain("memorySearch.sources with sessions"); + expect(message).toContain("memory.qmd.sessions.enabled is not true"); + expect(message).toContain("openclaw config set memory.qmd.sessions.enabled true"); + }); + + it("warns when QMD session export is explicitly disabled", async () => { + const qmdCfg = { + memory: { backend: "qmd", qmd: { command: "qmd", sessions: { enabled: false } } }, + } as OpenClawConfig; + resolveMemorySearchConfig.mockReturnValue({ + provider: "auto", + sources: ["memory", "sessions"], + experimental: { sessionMemory: true }, + local: {}, + remote: {}, + }); + + await noteMemorySearchHealth(qmdCfg, {}); + + expect(note).toHaveBeenCalledTimes(1); + const message = String(note.mock.calls[0]?.[0] ?? ""); + expect(message).toContain("QMD session transcript export is not enabled"); + }); + + it("does not warn about QMD session export when session sources are not enabled", async () => { + const qmdCfg = { memory: { backend: "qmd", qmd: { command: "qmd" } } } as OpenClawConfig; + resolveMemorySearchConfig.mockReturnValue({ + provider: "auto", + sources: ["memory"], + experimental: { sessionMemory: true }, + local: {}, + remote: {}, + }); + + await noteMemorySearchHealth(qmdCfg, {}); + + expect(note).not.toHaveBeenCalled(); + }); + + it("reports QMD binary and session export warnings independently", async () => { + const qmdCfg = { memory: { backend: "qmd", qmd: { command: "qmd" } } } as OpenClawConfig; + checkQmdBinaryAvailability.mockResolvedValueOnce({ + available: false, + error: "spawn qmd ENOENT", + }); + resolveMemorySearchConfig.mockReturnValue({ + provider: "auto", + sources: ["memory", "sessions"], + experimental: { sessionMemory: true }, + local: {}, + remote: {}, + }); + + await noteMemorySearchHealth(qmdCfg, {}); + + expect(note).toHaveBeenCalledTimes(2); + expect(String(note.mock.calls[0]?.[0] ?? "")).toContain("spawn qmd ENOENT"); + expect(String(note.mock.calls[1]?.[0] ?? "")).toContain( + "QMD session transcript export is not enabled", + ); + }); + + it("does not warn when QMD session sources and QMD session export are both enabled", async () => { + const qmdCfg = { + memory: { backend: "qmd", qmd: { command: "qmd", sessions: { enabled: true } } }, + } as OpenClawConfig; + resolveMemorySearchConfig.mockReturnValue({ + provider: "auto", + sources: ["memory", "sessions"], + experimental: { sessionMemory: true }, + local: {}, + remote: {}, + }); + + await noteMemorySearchHealth(qmdCfg, {}); + + expect(note).not.toHaveBeenCalled(); + }); + it("does not warn when remote apiKey is configured for explicit provider", async () => { await expectNoWarningWithConfiguredRemoteApiKey("openai"); }); diff --git a/src/commands/doctor-memory-search.ts b/src/commands/doctor-memory-search.ts index 66a4939dcc35..6a4adbd67d99 100644 --- a/src/commands/doctor-memory-search.ts +++ b/src/commands/doctor-memory-search.ts @@ -464,6 +464,24 @@ export async function noteMemorySearchHealth( "Memory search", ); } + if (resolved.sources?.includes("sessions") && cfg.memory?.qmd?.sessions?.enabled !== true) { + note( + [ + "QMD memory backend is configured and the default agent resolves memorySearch.sources with sessions,", + "but QMD session transcript export is not enabled (memory.qmd.sessions.enabled is not true).", + "Session transcript hits will not appear in QMD-backed memory search until QMD session export is enabled.", + "", + "Fix (pick one):", + `- Enable QMD session export: ${formatCliCommand( + "openclaw config set memory.qmd.sessions.enabled true", + )}`, + "- Or remove sessions from the default agent's memorySearch.sources if QMD session recall is not intended.", + "", + `Verify: ${formatCliCommand("openclaw memory status --deep")}`, + ].join("\n"), + "Memory search", + ); + } return; }