Summary
The gateway leaks unclosed fs.FileHandle references on session transcript .jsonl.lock files. On Node 22 (the supported LTS line per the 2026.5.18 release notes) this surfaces only as a deprecation warning, but on Node 24+ V8 promotes FileHandle GC to a fatal ERR_INVALID_STATE, which crashes the gateway process whenever the V8 GC happens to sweep one before OC closes it.
In our deployment this manifests as a chronic ~2-hour crash cadence on a single bot that accumulated a large session backlog (2,446 JSONL files in ~/.openclaw/agents/main/sessions/). Other bots on the same host / OC / Node version don't crash — they have 5–100 sessions on disk and don't generate enough open-handle pressure for V8 to ever GC a leaked one.
Environment
- OC:
2026.5.18 (npm)
- Node:
25.9.0 (three majors above the documented Node 22.19 minimum, but well within what users on rolling-release Homebrew get by default)
- OS: macOS 26.4.1 arm64
- Bot has been running ~30h under sustained activity; heartbeat interval is the gateway default 7,200,000ms (
gateway/heartbeat: {"intervalMs":7200000} heartbeat: started)
- Session count on disk: 2,446
.jsonl files; no stale .lock files at rest
Symptom
Every ~2h a stability bundle is written with reason: uncaught_exception and this error:
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: <N> (/Users/<bot>/.openclaw/agents/main/sessions/<uuid>.jsonl.lock)
The FD number and session UUID rotate; the path pattern is invariant: always a .jsonl.lock file in the agent's `sessions/` directory.
On the next launchd-driven restart the gateway's `main-session-restart-recovery` logs:
```
warn marked 1 interrupted main session(s) from stale transcript locks
warn marked interrupted main session failed: agent:main:main:heartbeat (transcript tail is not resumable)
```
confirming the leaked handle was held by the heartbeat session.
After ~4 crashes in 8h, macOS launchd's anti-thrash kicks in (`Successfully spawned … because inefficient`) — downstream symptom, not the root cause.
Reproducer
- Run gateway on Node ≥24
- Accumulate ~2,000+ session JSONL files in the agent's sessions directory
- Let the default 2h heartbeat fire several times
- Crash typically occurs within 2–4 hours
Likely root cause
OC's session-store code opens FileHandles for transcript locks during heartbeat-session write / scan operations and doesn't always go through an explicit `.close()` on every return path (likely error paths or interrupted writes). On Node 22 this is harmless because GC-close was warn-only; on Node 24+ it's fatal.
The stability bundle includes the diagnostic-phase log up to seq=181 (load → normalize → plugins → channels) but does not include the throwing stack frame, since the crash is in V8's GC sweep rather than user code.
Suggested fix direction
Audit `FileHandle` open call sites in the session-store layer (anywhere transcript locks are taken) for non-explicit close paths, especially:
- Error returns mid-write
- Heartbeat-session interrupt handling on shutdown
- Session metadata scans — the likely dominant pressure source given the 2,446-session correlation
Wrapping with `try { … } finally { await handle.close() }` or porting to `fs.promises.readFile`-style auto-closing helpers should close the leak.
Workaround for affected users
Either downgrade Node to the documented 22.19+ LTS line, or prune old session JSONL files until the on-disk count is in the low hundreds.
Stability bundle excerpt (sanitized)
```json
{
"version": 1,
"generatedAt": "2026-05-21T04:32:40.158Z",
"reason": "uncaught_exception",
"process": { "pid": 72273, "platform": "darwin", "arch": "arm64", "node": "25.9.0", "uptimeMs": 7208113 },
"error": {
"name": "Error",
"code": "ERR_INVALID_STATE",
"message": "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: 38 (/Users//.openclaw/agents/main/sessions/.jsonl.lock)"
}
}
```
I have 10 stability bundles over a 31h window with the same error signature (different FD / UUID each time) — happy to share more if useful.
Summary
The gateway leaks unclosed
fs.FileHandlereferences on session transcript.jsonl.lockfiles. On Node 22 (the supported LTS line per the 2026.5.18 release notes) this surfaces only as a deprecation warning, but on Node 24+ V8 promotes FileHandle GC to a fatalERR_INVALID_STATE, which crashes the gateway process whenever the V8 GC happens to sweep one before OC closes it.In our deployment this manifests as a chronic ~2-hour crash cadence on a single bot that accumulated a large session backlog (2,446 JSONL files in
~/.openclaw/agents/main/sessions/). Other bots on the same host / OC / Node version don't crash — they have 5–100 sessions on disk and don't generate enough open-handle pressure for V8 to ever GC a leaked one.Environment
2026.5.18(npm)25.9.0(three majors above the documented Node 22.19 minimum, but well within what users on rolling-release Homebrew get by default)gateway/heartbeat: {"intervalMs":7200000} heartbeat: started).jsonlfiles; no stale.lockfiles at restSymptom
Every ~2h a stability bundle is written with
reason: uncaught_exceptionand this error:The FD number and session UUID rotate; the path pattern is invariant: always a
.jsonl.lockfile in the agent's `sessions/` directory.On the next launchd-driven restart the gateway's `main-session-restart-recovery` logs:
```
warn marked 1 interrupted main session(s) from stale transcript locks
warn marked interrupted main session failed: agent:main:main:heartbeat (transcript tail is not resumable)
```
confirming the leaked handle was held by the heartbeat session.
After ~4 crashes in 8h, macOS launchd's anti-thrash kicks in (`Successfully spawned … because inefficient`) — downstream symptom, not the root cause.
Reproducer
Likely root cause
OC's session-store code opens FileHandles for transcript locks during heartbeat-session write / scan operations and doesn't always go through an explicit `.close()` on every return path (likely error paths or interrupted writes). On Node 22 this is harmless because GC-close was warn-only; on Node 24+ it's fatal.
The stability bundle includes the diagnostic-phase log up to seq=181 (load → normalize → plugins → channels) but does not include the throwing stack frame, since the crash is in V8's GC sweep rather than user code.
Suggested fix direction
Audit `FileHandle` open call sites in the session-store layer (anywhere transcript locks are taken) for non-explicit close paths, especially:
Wrapping with `try { … } finally { await handle.close() }` or porting to `fs.promises.readFile`-style auto-closing helpers should close the leak.
Workaround for affected users
Either downgrade Node to the documented 22.19+ LTS line, or prune old session JSONL files until the on-disk count is in the low hundreds.
Stability bundle excerpt (sanitized)
```json
{
"version": 1,
"generatedAt": "2026-05-21T04:32:40.158Z",
"reason": "uncaught_exception",
"process": { "pid": 72273, "platform": "darwin", "arch": "arm64", "node": "25.9.0", "uptimeMs": 7208113 },
"error": {
"name": "Error",
"code": "ERR_INVALID_STATE",
"message": "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: 38 (/Users//.openclaw/agents/main/sessions/.jsonl.lock)"
}
}
```
I have 10 stability bundles over a 31h window with the same error signature (different FD / UUID each time) — happy to share more if useful.