You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two related defects in the memory-wiki bridge import path:
wiki.palace (and the bridge write inside wiki.importInsights) hard-fails with Refusing to write imported source page (path-mismatch): <path>: path changed during read when the vault file at the target path is replaced between the fs.lstat pre-check and the fs.open/handle.stat/fs.stat re-check in @openclaw/fs-safe's openVerifiedLocalFile. No retry, the write is silently dropped on that call, and the gateway surfaces a misleading error.
wiki.importInsights is consistently slow (30-100+ seconds) whenever any bridge source is dirty, because every imported source triggers three fs-safe stat-and-verify operations and the function does not coalesce concurrent invocations.
When the import takes 100+ seconds the gateway's RSS climbs past the 1.5 GB memory-warning threshold and enters a V8 GC death spiral — observed pushing RSS from 1.9 GB to 2.13 GB and CPU to 40-110% sustained before a manual restart.
Frontend repeatedly polls wiki.palace causing unknown method WebSocket error logs on backend #65088 — Frontend repeatedly polls wiki.palace and wiki.importInsights. Confirmed by capture: the user's frontend was open during the incident, and wiki.palace was called at 18:31:49 HKT triggering the path-mismatch error. Polling cadence is the trigger; the bridge import under load is the failure mode.
Steps to reproduce
Install OpenClaw 2026.6.1 with the memory-wiki bundled plugin enabled and bridge mode importing from one or more workspaces.
Have several bridge sources per workspace (in this report: 1914 memory markdown files across 7+ workspaces, producing 1955 vault pages).
Open the OpenClaw web UI (or any client that polls wiki.palace).
Trigger a wiki.importInsights call (the frontend will, on its own).
Modify a memory file in any workspace's memory/ dir to invalidate its import entry, or just wait for a periodic refresh.
Observe: a single wiki.palace call returns a (path-mismatch) error after ~13s; the next wiki.importInsights call succeeds but takes 30-100+ seconds.
After the restart, the bridge source page wiki/main/sources/bridge-jordi-6080193a-memory-2d62c497.md exists, mode 0600, last modified 18:32:44 — confirming the next import run did successfully re-write the file the failed palace call was complaining about. The 101s import covered 20+ vault pages written in the 18:30-18:35 window.
Root cause analysis
A) The path-mismatch race in @openclaw/fs-safe's openVerifiedLocalFile
In node_modules/@openclaw/fs-safe/dist/root-impl.js (lines 75-141), the function does:
constpreStat=awaitfs.lstat(filePath);// [1] stat the path
...
handle=awaitfs.open(filePath,openFlags);// [2] open (gets inode X)conststat=awaithandle.stat();// [3] stat the OPENED handle (inode X)
...
constpathStat=awaitfs.lstat(filePath);// [4] stat the path AGAINif(!sameFileIdentity(stat,pathStat)){// [5] compare X vs pathStatthrownewFsSafeError("path-mismatch","path changed during read");}
The race window is between [1]/[2] and [4]. If the file at filePath is atomically replaced (different inode) in that window — which is what fs.writeFile/rename style atomic writes do — the comparison fails and the write is rejected.
The wrapper at dist/cli-BC1g2VJh.js:3471:
if(error.code!=="symlink"&&error.code!=="path-alias")thrownewError(`Refusing to write imported source page (${error.code}): ${params.pagePath}: ${error.message}`,{cause: error});
…propagates this to the gateway's WS error. No retry, no in-process recovery, no log of the race cause. A retry on path-mismatch would almost certainly succeed (the file at that path is now the new inode, which is the right one to read).
B) The 100s+ import duration
syncMemoryWikiBridgeSources in dist/cli-BC1g2VJh.js:3611-3676 iterates every public memory artifact in every workspace (listActiveMemoryPublicArtifacts) and calls writeBridgeSourcePage for each. For each artifact it does, in writeImportedSourcePage:
vault.readText(pagePath) (fs-safe: 5 more stats if file exists)
vault.write(pagePath, rendered) (fs-safe: 5 more stats on the open+write path)
That's up to 15 stat-equivalent calls per bridge source. With ~20 newly-dirty sources per import run, this is 300 stats per wiki.importInsights call. Each stat on a system with the workboard plugin holding 46 sqlite handles is contending with sqlite I/O.
The shouldSkipImportedSourceWrite short-circuit (line 3447) helps when the source hasn't changed, but it requires a DB hit per source. There's no per-call coalescing, no per-vault-file mutex, and no batching of consecutive source-page writes.
C) Why it tipped into a memory spiral
The 101s wall-clock + the 46 workboard file handles + the wiki.palace path-mismatch failure all happening in the same minute pushed V8 over its 1.5 GB heap warning threshold. V8 then spent the next 5+ minutes doing GC, RSS climbed 1.94 → 2.13 GB, CPU stayed at 40-110%, and the only recovery was a manual restart. None of the individual observations are fatal; their combination at this scale is.
Suggested fix (sketch)
For (A), the minimal fix in fs-safe:
// In openVerifiedLocalFile, after the path-mismatch check fails:// - On path-mismatch, treat the new path's inode as the canonical one// and retry the open once. The racing writer has finished; the new// file is the one the caller wants.
The cleaner architectural fix is to push the retry into the caller (writeImportedSourcePage and writeBridgeSourcePage): catch (path-mismatch) from vault.write, re-stat the page path once, and retry the write. One retry resolves ~all observed cases because the racing writer has already finished.
For (B), three options, in increasing order of effort:
Batch the stat calls in writeImportedSourcePage. Currently we do vault.stat + vault.readText + vault.write — three separate fs-safe round-trips. The existing read is only needed to decide whether to write. If we just always write, we save 5 stats per source.
Pre-filter artifacts whose stats.mtimeMs hasn't changed since the last successful import (we already have state in readMemoryWikiSourceSyncState). The code already does this per-source via shouldSkipImportedSourceWrite, but the check is gated on a DB hit. A cheap in-memory Map<syncKey, {mtimeMs, size}> in the sync state would skip the DB hit.
For the V8 spiral specifically, a process-level RSS limit + auto-recycle (or a hard backpressure on wiki.importInsights when memory pressure is warning) would prevent the 100s import from being the first domino.
[ws] ⇄ res ✗ wiki.palace 13187ms
errorCode=internal_error
errorMessage=Refusing to write imported source page (path-mismatch):
sources/bridge-jordi-6080193a-memory-2d62c497.md: path changed during read
| path changed during read
The doubled path changed during read is the wrapper message + cause.message from the FsSafeError. Useful for grep in existing log captures.
Happy to send a PR
If maintainers confirm the preferred direction (caller-side retry on path-mismatch vs. fs-safe-level retry, and coalesce-only vs. coalesce+batch+prefilter for the import speed), I'm happy to send a PR with the chosen scope.
Summary
Two related defects in the memory-wiki bridge import path:
wiki.palace(and the bridge write insidewiki.importInsights) hard-fails withRefusing to write imported source page (path-mismatch): <path>: path changed during readwhen the vault file at the target path is replaced between thefs.lstatpre-check and thefs.open/handle.stat/fs.statre-check in@openclaw/fs-safe'sopenVerifiedLocalFile. No retry, the write is silently dropped on that call, and the gateway surfaces a misleading error.wiki.importInsightsis consistently slow (30-100+ seconds) whenever any bridge source is dirty, because every imported source triggers three fs-safe stat-and-verify operations and the function does not coalesce concurrent invocations.When the import takes 100+ seconds the gateway's RSS climbs past the 1.5 GB memory-warning threshold and enters a V8 GC death spiral — observed pushing RSS from 1.9 GB to 2.13 GB and CPU to 40-110% sustained before a manual restart.
Related issues
Refusing to write imported source page through symlinkfires for non-symlink FsSafeError causes. The fix in 2026.6.1 added theerror.codeto the wrapper message (seecli-BC1g2VJh.js:3471), but the underlyingpath-mismatchrace that this issue covers is not addressed.wiki.palaceandwiki.importInsights. Confirmed by capture: the user's frontend was open during the incident, andwiki.palacewas called at 18:31:49 HKT triggering the path-mismatch error. Polling cadence is the trigger; the bridge import under load is the failure mode.Steps to reproduce
wiki.palace).wiki.importInsightscall (the frontend will, on its own).memory/dir to invalidate its import entry, or just wait for a periodic refresh.wiki.palacecall returns a(path-mismatch)error after ~13s; the nextwiki.importInsightscall succeeds but takes 30-100+ seconds.Observed timeline (2026-06-07 18:30-18:38 HKT, +08:00)
After the restart, the bridge source page
wiki/main/sources/bridge-jordi-6080193a-memory-2d62c497.mdexists, mode 0600, last modified 18:32:44 — confirming the next import run did successfully re-write the file the failed palace call was complaining about. The 101s import covered 20+ vault pages written in the 18:30-18:35 window.Root cause analysis
A) The
path-mismatchrace in@openclaw/fs-safe'sopenVerifiedLocalFileIn
node_modules/@openclaw/fs-safe/dist/root-impl.js(lines 75-141), the function does:The race window is between [1]/[2] and [4]. If the file at
filePathis atomically replaced (different inode) in that window — which is whatfs.writeFile/renamestyle atomic writes do — the comparison fails and the write is rejected.The wrapper at
dist/cli-BC1g2VJh.js:3471:…propagates this to the gateway's WS error. No retry, no in-process recovery, no log of the race cause. A retry on
path-mismatchwould almost certainly succeed (the file at that path is now the new inode, which is the right one to read).B) The 100s+ import duration
syncMemoryWikiBridgeSourcesindist/cli-BC1g2VJh.js:3611-3676iterates every public memory artifact in every workspace (listActiveMemoryPublicArtifacts) and callswriteBridgeSourcePagefor each. For each artifact it does, inwriteImportedSourcePage:vault.stat(pagePath)(fs-safe: preStat + open + handle.stat + pathStat + realStat = 5 stats)fs.readFile(sourcePath)(raw fs)vault.readText(pagePath)(fs-safe: 5 more stats if file exists)vault.write(pagePath, rendered)(fs-safe: 5 more stats on the open+write path)That's up to 15 stat-equivalent calls per bridge source. With ~20 newly-dirty sources per import run, this is 300 stats per
wiki.importInsightscall. Each stat on a system with the workboard plugin holding 46 sqlite handles is contending with sqlite I/O.The
shouldSkipImportedSourceWriteshort-circuit (line 3447) helps when the source hasn't changed, but it requires a DB hit per source. There's no per-call coalescing, no per-vault-file mutex, and no batching of consecutive source-page writes.C) Why it tipped into a memory spiral
The 101s wall-clock + the 46 workboard file handles + the wiki.palace path-mismatch failure all happening in the same minute pushed V8 over its 1.5 GB heap warning threshold. V8 then spent the next 5+ minutes doing GC, RSS climbed 1.94 → 2.13 GB, CPU stayed at 40-110%, and the only recovery was a manual restart. None of the individual observations are fatal; their combination at this scale is.
Suggested fix (sketch)
For (A), the minimal fix in
fs-safe:The cleaner architectural fix is to push the retry into the caller (
writeImportedSourcePageandwriteBridgeSourcePage): catch(path-mismatch)fromvault.write, re-stat the page path once, and retry the write. One retry resolves ~all observed cases because the racing writer has already finished.For (B), three options, in increasing order of effort:
wiki.importInsightscalls. Add a per-process promise cache keyed on{vaultRoot, syncKey}; if a call is in flight, await it instead of starting a new one. The frontend polling cadence (Frontend repeatedly polls wiki.palace causing unknown method WebSocket error logs on backend #65088) means this alone could cut the import volume by 5-10x.writeImportedSourcePage. Currently we dovault.stat+vault.readText+vault.write— three separate fs-safe round-trips. Theexistingread is only needed to decide whether to write. If we just always write, we save 5 stats per source.stats.mtimeMshasn't changed since the last successful import (we already havestateinreadMemoryWikiSourceSyncState). The code already does this per-source viashouldSkipImportedSourceWrite, but the check is gated on a DB hit. A cheap in-memoryMap<syncKey, {mtimeMs, size}>in the sync state would skip the DB hit.For the V8 spiral specifically, a process-level RSS limit + auto-recycle (or a hard backpressure on
wiki.importInsightswhen memory pressure iswarning) would prevent the 100s import from being the first domino.Environment
/home/mlaih/.openclaw/wiki/main(~1955 pages)~/.openclaw/agents/<id>/memory/wiki.palacepoll)Sample error payload (from gateway journal)
The doubled
path changed during readis the wrapper message +cause.messagefrom the FsSafeError. Useful for grep in existing log captures.Happy to send a PR
If maintainers confirm the preferred direction (caller-side retry on
path-mismatchvs. fs-safe-level retry, and coalesce-only vs. coalesce+batch+prefilter for the import speed), I'm happy to send a PR with the chosen scope.