fix(doctor): warn and document QMD session recall gates (#80947)

* Warn on QMD session recall export mismatch

* Clarify session transcript recall gates
This commit is contained in:
JC
2026-06-28 13:55:27 -07:00
committed by GitHub
parent f37e45ecc1
commit 78029e43d2
5 changed files with 201 additions and 4 deletions
+18 -2
View File
@@ -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/<id>/qmd/sessions/`.
collection under `~/.openclaw/agents/<id>/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
+11 -1
View File
@@ -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
+61 -1
View File
@@ -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.
</Warning>
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:
<Tabs>
<Tab title="Builtin backend">
```json5
{
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"],
},
},
},
tools: {
sessions: { visibility: "agent" },
},
}
```
</Tab>
<Tab title="QMD backend">
```json5
{
agents: {
defaults: {
memorySearch: {
experimental: { sessionMemory: true },
sources: ["memory", "sessions"],
},
},
},
memory: {
backend: "qmd",
qmd: {
sessions: { enabled: true },
},
},
tools: {
sessions: { visibility: "agent" },
},
}
```
</Tab>
</Tabs>
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 |
+93
View File
@@ -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");
});
+18
View File
@@ -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;
}