openclaw - 💡(How to fix) Fix ERR_INVALID_STATE crash: FileHandle closed during GC in session-write-lock (Node.js v25)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

ERR_INVALID_STATE: A FileHandle object was closed during garbage collection.
This used to be allowed with a deprecation warning but is now considered an error.
Please close FileHandle objects explicitly.
File descriptor: 22 (/Users/song/.openclaw/agents/xiaocai/sessions/1e3848c4-aa7e-4456-8a2c-7854d4c560c5.jsonl.lock)

Root Cause

In @openclaw/fs-safe package, releaseAllLocksSync() calls held.handle.close() without awaiting the returned Promise:

// sidecar-lock-Bv7C32CP.js
function releaseAllLocksSync(state) {
    for (const [normalizedTargetPath, held] of state.held) {
        held.handle.close().catch(() => void 0);  // ← async but not awaited
        // handle.close() returns a Promise
        // function is synchronous
        // → FileHandle GC'd before fd is actually closed
        try {
            if (snapshotMatchesSync(held.lockPath, held.snapshot))
                fs.rmSync(held.lockPath, { force: true });
        } catch {}
        state.held.delete(normalizedTargetPath);
    }
}

The problem: fs.FileHandle.close() is async, but releaseAllLocksSync is synchronous. The Promise is fire-and-forget. When the process continues and the handle reference is deleted, GC collects the unclosed FileHandle, triggering the fatal error in Node.js v25.

Code Example

ERR_INVALID_STATE: A FileHandle object was closed during garbage collection.
This used to be allowed with a deprecation warning but is now considered an error.
Please close FileHandle objects explicitly.
File descriptor: 22 (/Users/song/.openclaw/agents/xiaocai/sessions/1e3848c4-aa7e-4456-8a2c-7854d4c560c5.jsonl.lock)

---

// sidecar-lock-Bv7C32CP.js
function releaseAllLocksSync(state) {
    for (const [normalizedTargetPath, held] of state.held) {
        held.handle.close().catch(() => void 0);  // ← async but not awaited
        // handle.close() returns a Promise
        // function is synchronous
        // → FileHandle GC'd before fd is actually closed
        try {
            if (snapshotMatchesSync(held.lockPath, held.snapshot))
                fs.rmSync(held.lockPath, { force: true });
        } catch {}
        state.held.delete(normalizedTargetPath);
    }
}

---

function releaseAllLocksSync(state) {
    for (const [normalizedTargetPath, held] of state.held) {
        // Use fd.closeSync or handle the async properly
        held.handle.close().catch(() => void 0);
        // Add: prevent GC from seeing the handle
        held.handle = null;
        ...
    }
}

---

process.on('exit', () => {
    // Synchronous close using fd.closeSync
    for (const held of SESSION_LOCKS.heldEntries()) {
        try { held.handle.fd?.closeSync(); } catch {}
    }
});
RAW_BUFFERClick to expand / collapse

Bug Description

Gateway crashes with ERR_INVALID_STATE when a session's .jsonl.lock FileHandle is garbage collected without being explicitly closed. This is a Node.js v25 behavior change — previously a deprecation warning, now a fatal error.

Error

ERR_INVALID_STATE: A FileHandle object was closed during garbage collection.
This used to be allowed with a deprecation warning but is now considered an error.
Please close FileHandle objects explicitly.
File descriptor: 22 (/Users/song/.openclaw/agents/xiaocai/sessions/1e3848c4-aa7e-4456-8a2c-7854d4c560c5.jsonl.lock)

Root Cause

In @openclaw/fs-safe package, releaseAllLocksSync() calls held.handle.close() without awaiting the returned Promise:

// sidecar-lock-Bv7C32CP.js
function releaseAllLocksSync(state) {
    for (const [normalizedTargetPath, held] of state.held) {
        held.handle.close().catch(() => void 0);  // ← async but not awaited
        // handle.close() returns a Promise
        // function is synchronous
        // → FileHandle GC'd before fd is actually closed
        try {
            if (snapshotMatchesSync(held.lockPath, held.snapshot))
                fs.rmSync(held.lockPath, { force: true });
        } catch {}
        state.held.delete(normalizedTargetPath);
    }
}

The problem: fs.FileHandle.close() is async, but releaseAllLocksSync is synchronous. The Promise is fire-and-forget. When the process continues and the handle reference is deleted, GC collects the unclosed FileHandle, triggering the fatal error in Node.js v25.

Reproduction

  1. Agent acquires session write lock (opens FileHandle via fs.open(lockPath, "wx"))
  2. Session terminates abnormally (timeout, kill, crash)
  3. Cleanup path calls releaseAllLocksSync()
  4. handle.close() Promise created but not awaited
  5. Function returns, handle reference dropped
  6. GC collects FileHandle → ERR_INVALID_STATE crash

Environment

  • OpenClaw: 2026.5.27 (27ae826)
  • Node.js: v25.8.1
  • OS: macOS Darwin 25.5.0 (arm64)

Impact

  • Gateway crashes twice on 2026-05-26 (same session, same root cause)
  • Only affects agents with long-running sessions (e.g., adb data collection)
  • Mitigated by external zombie lock cleanup cron (every 5 min)

Suggested Fix

Option A — Make releaseAllLocksSync truly synchronous:

function releaseAllLocksSync(state) {
    for (const [normalizedTargetPath, held] of state.held) {
        // Use fd.closeSync or handle the async properly
        held.handle.close().catch(() => void 0);
        // Add: prevent GC from seeing the handle
        held.handle = null;
        ...
    }
}

Option B — Register process.on('exit') handler that properly awaits:

process.on('exit', () => {
    // Synchronous close using fd.closeSync
    for (const held of SESSION_LOCKS.heldEntries()) {
        try { held.handle.fd?.closeSync(); } catch {}
    }
});

Option C — Suppress the GC error by keeping strong references until close completes (belt-and-suspenders with current approach).

Crash Logs

Two crash files from 2026-05-26, both same root cause:

  • openclaw-stability-2026-05-26T14-53-05-802Z-13681-uncaught_exception.json
  • openclaw-stability-2026-05-26T15-21-37-284Z-7160-uncaught_exception.json

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix ERR_INVALID_STATE crash: FileHandle closed during GC in session-write-lock (Node.js v25)