Environment
- OS: Windows 11 Home 25H2 (Build 26200)
- Node: v24.14.1
- OpenClaw: 2026.5.2
- State dir:
C:\Users\<user>\.openclaw (default, inside Windows user profile)
Symptoms (real production case, 2026-07-19)
Two related failures observed within a 16-minute window on the same host, same directory tree:
A. Telegram voice-note fetch fails with EPERM: fsync
07:37:15 WARN {module:telegram-auto-reply} media fetch failed
{ chatId: <...>, error: 'Error: EPERM: operation not permitted, fsync' }
at bot-CW5ZEQ0V.js:2001 processInboundMessage
Downstream effect: the voice .ogg is not persisted, STT never runs, the user gets zero feedback — no error message, no retry, no incident record. In my case that was 3+ hours of unexplained silence.
B. exec-approvals.json atomic rewrite fails with EPERM: rename
07:21:54 ERROR [tools] exec failed: EPERM: operation not permitted, rename
'.exec-approvals.<pid>.<uuid>.tmp' -> 'exec-approvals.json'
Downstream effect: the exec approvals state file isn't updated; tool approvals get out of sync.
Both errors were transient — subsequent operations succeeded — but neither retries.
Root cause
Well-documented Windows semantic: FlushFileBuffers (fsync) and MoveFileEx(REPLACE_EXISTING) return ERROR_ACCESS_DENIED (mapped by libuv to UV_EPERM) when any other process holds a handle on the target — even briefly. Windows file-system filter drivers (Search Indexer, AV mini-filters, cloud sync clients, backup helpers) routinely open handles on new files under the user profile for a few tens of milliseconds.
References:
Proposed fixes
1. Retry-with-backoff on transient fs errors inside .openclaw
Wrap fs.rename/fs.fsync/fs.renameSync/fs.close with an exponential backoff retry (25/50/100/200/400/800 ms, six attempts) on EPERM, EBUSY, EACCES. Only retry on Windows; other platforms don't hit this class. This is the graceful-fs pattern and is battle-tested by npm.
Two callsites are known to fail:
- Telegram media fetch:
bot-CW5ZEQ0V.js:~2001 (processInboundMessage)
- exec-approvals writer (temp → rename pattern)
There are likely more (any atomic-replace of a file inside .openclaw).
2. Reply to sender on inbound processing failure
processInboundMessage currently swallows the error and logs media fetch failed. Nothing is sent back to Telegram. Suggested UX:
⚠️ I couldn't process your voice note (media fetch failed on my end). Please try sending it again.
Plus a structured entry in incidents.jsonl so the failure is visible to health monitors / RCA agents.
3. Refactor Telegram STT to stream in-memory
For voice notes destined for Whisper STT, there's no reason to persist the OGG to local disk at all. Fetch bytes into a Buffer, wrap as a Blob/Uploadable, and pass directly to openai.audio.transcriptions.create({ file: buffer }). This eliminates the failure mode entirely for STT — no disk, no fsync, no EPERM.
- OpenAI SDK accepts
Buffer / Uploadable: see OpenAI community thread
- Telegram bot APIs support streaming from CDN without saving to disk
4. Allow relocating .openclaw off the user profile
For Windows, allow the state directory to be moved outside C:\Users\<user>\ (e.g., C:\ProgramData\openclaw\ or via OPENCLAW_HOME env var). This isolates OpenClaw's I/O from every user-profile scanner and dramatically reduces the transient-lock surface area.
Willing to submit a PR
Fixes 1 and 2 are small and localized. Happy to open a PR — please let me know if this project accepts external contributions and if there's a preferred pattern (graceful-fs dependency vs. inline retry helper).
Impact summary
- User-visible: Voice-to-text silently broken on Windows hosts under any transient file lock. Users have no way to know it failed.
- Reliability: exec-approvals state file can desync under the same class of race.
- Diagnosability: Silent error-swallow makes this class of failure very hard to detect without external log grepping.
Related: workarounds users are applying today
While waiting for an upstream fix, I've locally:
- Added
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED recursively on .openclaw (blocks Windows Search Indexer)
- Installed Sysinternals
procmon/handle for future locker attribution
- Added a 15-min log watchdog for
EPERM.*\.openclaw patterns
- Added a 6-hour synthetic write+fsync health check into
.openclaw\media\inbound
These are environmental band-aids. The real fix is application-layer retry + memory-only STT pipeline + non-silent failure UX.
Environment
C:\Users\<user>\.openclaw(default, inside Windows user profile)Symptoms (real production case, 2026-07-19)
Two related failures observed within a 16-minute window on the same host, same directory tree:
A. Telegram voice-note fetch fails with
EPERM: fsyncDownstream effect: the voice
.oggis not persisted, STT never runs, the user gets zero feedback — no error message, no retry, no incident record. In my case that was 3+ hours of unexplained silence.B.
exec-approvals.jsonatomic rewrite fails withEPERM: renameDownstream effect: the exec approvals state file isn't updated; tool approvals get out of sync.
Both errors were transient — subsequent operations succeeded — but neither retries.
Root cause
Well-documented Windows semantic:
FlushFileBuffers(fsync) andMoveFileEx(REPLACE_EXISTING)returnERROR_ACCESS_DENIED(mapped by libuv toUV_EPERM) when any other process holds a handle on the target — even briefly. Windows file-system filter drivers (Search Indexer, AV mini-filters, cloud sync clients, backup helpers) routinely open handles on new files under the user profile for a few tens of milliseconds.References:
write-file-atomic)graceful-fswhich retries these exact errors on Windows for exactly this reasonProposed fixes
1. Retry-with-backoff on transient fs errors inside
.openclawWrap
fs.rename/fs.fsync/fs.renameSync/fs.closewith an exponential backoff retry (25/50/100/200/400/800 ms, six attempts) onEPERM,EBUSY,EACCES. Only retry on Windows; other platforms don't hit this class. This is the graceful-fs pattern and is battle-tested by npm.Two callsites are known to fail:
bot-CW5ZEQ0V.js:~2001(processInboundMessage)There are likely more (any atomic-replace of a file inside
.openclaw).2. Reply to sender on inbound processing failure
processInboundMessagecurrently swallows the error and logsmedia fetch failed. Nothing is sent back to Telegram. Suggested UX:Plus a structured entry in
incidents.jsonlso the failure is visible to health monitors / RCA agents.3. Refactor Telegram STT to stream in-memory
For voice notes destined for Whisper STT, there's no reason to persist the OGG to local disk at all. Fetch bytes into a
Buffer, wrap as aBlob/Uploadable, and pass directly toopenai.audio.transcriptions.create({ file: buffer }). This eliminates the failure mode entirely for STT — no disk, no fsync, no EPERM.Buffer/Uploadable: see OpenAI community thread4. Allow relocating
.openclawoff the user profileFor Windows, allow the state directory to be moved outside
C:\Users\<user>\(e.g.,C:\ProgramData\openclaw\or viaOPENCLAW_HOMEenv var). This isolates OpenClaw's I/O from every user-profile scanner and dramatically reduces the transient-lock surface area.Willing to submit a PR
Fixes 1 and 2 are small and localized. Happy to open a PR — please let me know if this project accepts external contributions and if there's a preferred pattern (graceful-fs dependency vs. inline retry helper).
Impact summary
Related: workarounds users are applying today
While waiting for an upstream fix, I've locally:
FILE_ATTRIBUTE_NOT_CONTENT_INDEXEDrecursively on.openclaw(blocks Windows Search Indexer)procmon/handlefor future locker attributionEPERM.*\.openclawpatterns.openclaw\media\inboundThese are environmental band-aids. The real fix is application-layer retry + memory-only STT pipeline + non-silent failure UX.