openclaw - 💡(How to fix) Fix [Bug]: TUI startup creates in-process memory watcher that recursively scans / (entire root filesystem), freezing terminal for ~8 minutes [2 comments, 2 participants]

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…
GitHub stats
openclaw/openclaw#76387Fetched 2026-05-04 05:07:40
View on GitHub
Comments
2
Participants
2
Timeline
5
Reactions
2
Timeline (top)
commented ×2labeled ×2closed ×1

openclaw chat (and tui --local) creates a long-lived in-process chokidar watcher on TUI startup that recursively walks / (the root filesystem) with three concurrent workers. the TUI is unresponsive — including to Ctrl-C — for the duration of the walk (~8–10 minutes on a 1TB disk). after the scan completes, the TUI works normally.

this contradicts the documented behavior in docs/concepts/memory-qmd.md, which says "Gateway startup does not initialize QMD by default, so cold boot avoids importing the memory runtime or creating the long-lived watcher before memory is first used." TUI startup appears to take a different path that does create the watcher, and with a misresolved root.

Root Cause

root cause (high confidence)

Fix Action

Fix / Workaround

DispatchQueue_1: com.apple.main-thread (serial) start → node::Start → node::SpinEventLoopInternal → uv_run → uv__io_poll → uv__async_io → uv__work_done ← libuv work queue → node::MakeLibuvRequestCallback<u v_fs_s, AfterStat> → node::fs::AfterStat ← fs.stat callback → InternalCallbackScope::Close → MicrotaskQueue::PerformCheckpoin tInternal → Builtins_PromiseFulfillReactionJob → Builtins_AsyncFunctionAwaitResolveClosure → Builtins_ArrayMap ← .map() over readdir entries → string concatenation + StringCharCodeAt ← path joining

Code Example

resolveCollectionWatchPath(colle ction) {
    return path.join(path.normalize(collect ion.path), collection.pattern);
  }

---

~/Gits/sams-claw/
  ├── AGENTS.md
  ├── HEARTBEAT.md
  ├── IDENTITY.md
  ├── MEMORY.md
  ├── SOUL.md
  ├── TOOLS.md
  ├── USER.md
  └── memory/
      └── 2026-05-01.md

---

### evidence

1. process state during freeze


  PID 47879  STAT R+  CPU 99.2%  ETIME 01:18  openclaw-tui


2. open file descriptors — three parallel scans of /


  $ lsof -p 47879 | awk '$5=="DIR" && $4 ~ /r$/'
  node 47879 sam 6r DIR 1,18 704 2 /
  node 47879 sam 8r DIR 1,18 704 2 /
  node 47879 sam 10r DIR 1,18 704 2 /


cwd was /tmp for this run — the scan root is not derived from cwd.

3. stack sample (10s, all 7946 samples on main thread)


  DispatchQueue_1: com.apple.main-thread (serial)
    start
    → node::Start
    → node::SpinEventLoopInternal
    → uv_run → uv__io_poll → uv__async_io
    → uv__work_done                                          ← libuv work queue
    → node::MakeLibuvRequestCallback<u v_fs_s, AfterStat>
    → node::fs::AfterStat                                    ← fs.stat callback
InternalCallbackScope::Close
MicrotaskQueue::PerformCheckpoin tInternal
Builtins_PromiseFulfillReactionJob
Builtins_AsyncFunctionAwaitResolveClosure
Builtins_ArrayMap.map() over
readdir entries
    → string concatenation + StringCharCodeAt               ← path joining


every sample is in the same fs.stat → microtask → ArrayMap → string-build chain.
classic recursive readdir+stat pattern.

4. heap footprint


  Physical footprint: 974.1M (peak 974.4M)


~1GB of string objects, consistent with accumulating directory paths during a
deep walk.
RAW_BUFFERClick to expand / collapse

Bug type

Crash (process/app exits or hangs)

Beta release blocker

No

Summary

openclaw version: 2026.4.29 (commit a448042c) platform: macOS 26.4.1 (Darwin 25.4.0, arm64) node: v25.9.0 mode: local embedded (no gateway) install path: /opt/homebrew/lib/node_modules/openclaw

summary

openclaw chat (and tui --local) creates a long-lived in-process chokidar watcher on TUI startup that recursively walks / (the root filesystem) with three concurrent workers. the TUI is unresponsive — including to Ctrl-C — for the duration of the walk (~8–10 minutes on a 1TB disk). after the scan completes, the TUI works normally.

this contradicts the documented behavior in docs/concepts/memory-qmd.md, which says "Gateway startup does not initialize QMD by default, so cold boot avoids importing the memory runtime or creating the long-lived watcher before memory is first used." TUI startup appears to take a different path that does create the watcher, and with a misresolved root.

root cause (high confidence)

dist/qmd-manager-D9gNmXKa.js constructs the watcher path as:

  resolveCollectionWatchPath(colle ction) {
    return path.join(path.normalize(collect ion.path), collection.pattern);
  }

with default pattern "**/*.md" (per dist/memory-mxD4Jzcs.js).

if collection.path is empty, "/", or otherwise unresolved, the watcher target resolves to /**/*.md, which chokidar implements by recursive readdir+stat. the documented ignore list (.git, node_modules, etc.) doesn't help when the root is the entire disk — there are millions of files outside those directories on a real machine.

asks

  1. fix the path resolution. collection.path should never resolve to / for the default workspace collection. add a guard that throws if collection.path is falsy, root, or homedir-without-subpath.
  2. honor the documented behavior. TUI startup should not initialize the QMD watcher; defer until first memory use.
  3. add a config kill-switch. something like memory.qmd.watcher.enabled: false so users with a triggered bug can opt out without disabling memory entirely.
  4. fail loud, not silent. if the watcher detects it's about to scan / or $HOME, log a fatal warning and abort instead of starting the walk.

workspace details (for repro)

  ~/Gits/sams-claw/
  ├── AGENTS.md
  ├── HEARTBEAT.md
  ├── IDENTITY.md
  ├── MEMORY.md
  ├── SOUL.md
  ├── TOOLS.md
  ├── USER.md
  └── memory/
      └── 2026-05-01.md

config has no memory.* block — all defaults.

Steps to reproduce

reproduce

  1. fresh openclaw 2026.4.29 install on macOS, default config
  2. workspace at ~/Gits/sams-claw containing MEMORY.md + memory/2026-05-01.md
  3. from any cwd: openclaw chat
  4. observe: terminal freezes, no input accepted, no Ctrl-C
  5. in another terminal: lsof -p <tui-pid> | grep DIR shows three / handles
  6. wait ~8 minutes; TUI eventually becomes interactive

Expected behavior

expected

per the docs: TUI/gateway cold start should not create the long-lived in-process watcher. memory features should be lazily initialized when first used.

if the watcher is created at startup, its path must resolve to a workspace-relative scope (e.g., ~/Gits/sams-claw/memory/**/*.md plus ~/Gits/sams-claw/MEMORY.md), not /.

Actual behavior

TUI freezes for 8+ mins; see evidence below.

OpenClaw version

2026.4.29 (commit a448042c)

Operating system

macOS 26.4.1 (Darwin 25.4.0, arm64)

Install method

npm

Model

anthropic/claude-opus-4.7

Provider / routing chain

openclaw -> anthropic

Additional provider/model setup details

No response

Logs, screenshots, and evidence

### evidence

1. process state during freeze


  PID 47879  STAT R+  CPU 99.2%  ETIME 01:18  openclaw-tui


2. open file descriptors — three parallel scans of /


  $ lsof -p 47879 | awk '$5=="DIR" && $4 ~ /r$/'
  node 47879 sam 6r DIR 1,18 704 2 /
  node 47879 sam 8r DIR 1,18 704 2 /
  node 47879 sam 10r DIR 1,18 704 2 /


cwd was /tmp for this run — the scan root is not derived from cwd.

3. stack sample (10s, all 7946 samples on main thread)


  DispatchQueue_1: com.apple.main-thread (serial)
    start
    → node::Start
    → node::SpinEventLoopInternal
    → uv_run → uv__io_poll → uv__async_io
    → uv__work_done                                          ← libuv work queue
    → node::MakeLibuvRequestCallback<u v_fs_s, AfterStat>
    → node::fs::AfterStat                                    ← fs.stat callback
    → InternalCallbackScope::Close
    → MicrotaskQueue::PerformCheckpoin tInternal
    → Builtins_PromiseFulfillReactionJob
    → Builtins_AsyncFunctionAwaitResolveClosure
    → Builtins_ArrayMap                                      ← .map() over
readdir entries
    → string concatenation + StringCharCodeAt               ← path joining


every sample is in the same fs.stat → microtask → ArrayMap → string-build chain.
classic recursive readdir+stat pattern.

4. heap footprint


  Physical footprint: 974.1M (peak 974.4M)


~1GB of string objects, consistent with accumulating directory paths during a
deep walk.

Impact and severity

No response

Additional information

No response

extent analysis

TL;DR

The most likely fix is to modify the resolveCollectionWatchPath function to prevent it from resolving to the root filesystem and to defer the initialization of the QMD watcher until first memory use.

Guidance

  • Modify the resolveCollectionWatchPath function to add a guard that throws if collection.path is falsy, root, or homedir-without-subpath.
  • Defer the initialization of the QMD watcher until first memory use to honor the documented behavior.
  • Consider adding a config kill-switch, such as memory.qmd.watcher.enabled: false, to allow users to opt out of the watcher if needed.
  • Log a fatal warning and abort if the watcher detects it's about to scan the root filesystem or $HOME.

Example

resolveCollectionWatchPath(collection) {
  if (!collection.path || collection.path === '/' || collection.path === '~') {
    throw new Error('Invalid collection path');
  }
  return path.join(path.normalize(collection.path), collection.pattern);
}

Notes

The provided code snippet is a possible solution, but it may require additional modifications to fit the specific requirements of the OpenClaw project. The resolveCollectionWatchPath function should be modified to handle edge cases and prevent the watcher from resolving to the root filesystem.

Recommendation

Apply a workaround by modifying the resolveCollectionWatchPath function and deferring the initialization of the QMD watcher until first memory use. This will prevent the TUI from freezing and allow users to opt out of the watcher if needed.

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