mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user