feat(memory): allow QMD searches via mcporter keep-alive#19617
feat(memory): allow QMD searches via mcporter keep-alive#19617Takhoffman merged 13 commits intomainfrom
Conversation
63a2fc0 to
64ff46d
Compare
| type McporterGlobal = typeof globalThis & { | ||
| __openclawMcporterDaemonStart?: Promise<void>; | ||
| }; | ||
| const g: McporterGlobal = globalThis; | ||
| if (!g.__openclawMcporterDaemonStart) { | ||
| g.__openclawMcporterDaemonStart = (async () => { | ||
| try { | ||
| await this.runMcporter(["daemon", "start"], { timeoutMs: 10_000 }); | ||
| } catch (err) { | ||
| log.warn(`mcporter daemon start failed: ${String(err)}`); | ||
| } | ||
| })(); | ||
| } | ||
| await g.__openclawMcporterDaemonStart; |
There was a problem hiding this comment.
Failed daemon start is permanently cached
If mcporter daemon start fails (e.g., mcporter not installed, network issue, timeout), the rejected-then-caught promise is cached on globalThis.__openclawMcporterDaemonStart forever. Every subsequent search() call will await this already-resolved promise and never retry the daemon start. The error is swallowed by the catch block on line 829, so the user only sees a single warning in the log.
For a transient failure (e.g., a timeout on first boot), the daemon will never be started for the lifetime of the process, silently degrading every search to cold-start latency — the exact problem this feature is meant to solve.
Consider either (a) not caching the promise when it fails, or (b) adding a retry mechanism with backoff.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/qmd-manager.ts
Line: 821-834
Comment:
**Failed daemon start is permanently cached**
If `mcporter daemon start` fails (e.g., `mcporter` not installed, network issue, timeout), the rejected-then-caught promise is cached on `globalThis.__openclawMcporterDaemonStart` forever. Every subsequent `search()` call will `await` this already-resolved promise and never retry the daemon start. The error is swallowed by the `catch` block on line 829, so the user only sees a single warning in the log.
For a transient failure (e.g., a timeout on first boot), the daemon will never be started for the lifetime of the process, silently degrading every search to cold-start latency — the exact problem this feature is meant to solve.
Consider either (a) not caching the promise when it fails, or (b) adding a retry mechanism with backoff.
How can I resolve this? If you propose a fix, please make it concise.| const wrapped = new Error(combinedReason) as Error & { cause?: unknown }; | ||
| wrapped.cause = fallbackErr; |
There was a problem hiding this comment.
Inconsistent Error cause pattern
This line was changed to manually assign .cause instead of using the Error constructor's cause option, but the same file still uses new Error(message, { cause: err }) on line 188 and new Error(reason, { cause: primaryErr }) on line 244. If this change was for a compatibility reason, the other call sites should be updated too; otherwise this creates an inconsistency where some errors use the constructor option and others use manual assignment.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/embeddings.ts
Line: 231-232
Comment:
**Inconsistent Error `cause` pattern**
This line was changed to manually assign `.cause` instead of using the Error constructor's `cause` option, but the same file still uses `new Error(message, { cause: err })` on line 188 and `new Error(reason, { cause: primaryErr })` on line 244. If this change was for a compatibility reason, the other call sites should be updated too; otherwise this creates an inconsistency where some errors use the constructor option and others use manual assignment.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (1)
The mcporter tests at lines 924 and 965 write to Concretely: the "starts the mcporter daemon only once" test (line 965) caches the daemon promise on This follows the same cleanup pattern used elsewhere in the codebase (e.g., Prompt To Fix With AIThis is a comment left during a code review.
Path: src/memory/qmd-manager.test.ts
Line: 148-151
Comment:
**Missing globalThis cleanup between tests**
The mcporter tests at lines 924 and 965 write to `globalThis.__openclawMcporterDaemonStart` and `globalThis.__openclawMcporterColdStartWarned`, but the `afterEach` hook never cleans them up. Because these are process-global singletons, cached state leaks between test cases.
Concretely: the "starts the mcporter daemon only once" test (line 965) caches the daemon promise on `globalThis.__openclawMcporterDaemonStart`. If any future mcporter-enabled test is added after it, that test will find the stale cached promise and never spawn the daemon — causing false passes or failures depending on what it asserts. Similarly, `__openclawMcporterColdStartWarned` would suppress warnings in subsequent tests.
This follows the same cleanup pattern used elsewhere in the codebase (e.g., `tts.test.ts` saving/restoring `globalThis.fetch`).
```suggestion
afterEach(async () => {
vi.useRealTimers();
delete process.env.OPENCLAW_STATE_DIR;
delete (globalThis as Record<string, unknown>).__openclawMcporterDaemonStart;
delete (globalThis as Record<string, unknown>).__openclawMcporterColdStartWarned;
});
```
How can I resolve this? If you propose a fix, please make it concise. |
ed24468 to
88c6771
Compare
dbac711 to
9a03145
Compare
|
Blocking (HIGH) — env split between qmd and mcporter paths
Code refs:
Why this is risky:
Required fix:
Required test:
|
bbf132c to
43c8160
Compare
|
Merged and verified. What I fixed before landing:
Verification run on PR head:
Merged via squash with pinned head:
|
…nks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…nks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
…nks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
) thanks @vignesh07 Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: vignesh07 <1436853+vignesh07@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
Why
OpenClaw’s QMD backend currently spawns a fresh
qmdprocess per search (QmdMemoryManager.search()), which means QMD’s local GGUF models can cold-start on every query. This adds latency and can exceed the default 4s timeout.What
Adds an optional
memory.qmd.mcporterconfig block that routes QMD searches through mcporter (MCP runtime) instead of spawningqmdper query.When enabled, OpenClaw:
mcporter daemon start(once per process) to keep stdio servers warmmcporter call <serverName>.<tool> --args '{...}' --output jsonMemorySearchResult[]qmdCLI path when disabledThis is designed to work with a mcporter server configured to run
qmd mcpwithlifecycle: keep-alive.Config
Notes / followups
qmdCLI.Testing
pnpm lintpnpm buildpnpm test(repo has existing failing tests on my run; I did not attempt to fix unrelated failures)Greptile Summary
This PR adds an optional
memory.qmd.mcporterconfiguration block that routes QMD memory searches through the mcporter MCP runtime instead of spawning a freshqmdprocess per query, reducing cold-start latency for large GGUF models.Key changes:
MemoryQmdMcporterConfigtype and Zod schema for thememory.qmd.mcporterconfig block withenabled,serverName, andstartDaemonoptionsQmdMemoryManager.search()now branches onmcporter.enabledto route throughrunQmdSearchViaMcporter()instead of the existingrunQmd()pathensureMcporterDaemonStarted()usingglobalThissingletons for once-per-process initializationstructuredContent.resultsor direct array)runQueryAcrossCollections()commands-subagents.tsand its test fileembeddings.tsfor ErrorcauseassignmentIssues found:
globalThisproperties used by mcporter (__openclawMcporterDaemonStart,__openclawMcporterColdStartWarned) are not cleaned up inafterEach, which will cause test ordering dependencies if more mcporter tests are addedmcporter daemon startis permanently cached, so transient failures silently degrade all searches to cold-start latency for the lifetime of the processConfidence Score: 3/5
src/memory/qmd-manager.ts(daemon start caching logic),src/memory/qmd-manager.test.ts(missing globalThis cleanup in afterEach)Last reviewed commit: 64ff46d