fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly#64711
Conversation
Greptile SummaryThis one-line fix replaces the recursive glob Confidence Score: 5/5Safe to merge; only P2 style/quality findings remain. Both findings are P2 — spurious syncs on non-.md file changes and inconsistent treatment of additionalPaths. Neither causes data loss or incorrect sync behavior; they are performance/cleanup concerns. extensions/memory-core/src/memory/manager-sync-ops.ts — review the ignored callback and additionalPaths glob handling.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ee2e95b18
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
8ee2e95 to
8bca71a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8bca71aeb4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 564f89daae
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (stats?.isDirectory?.()) { | ||
| return false; | ||
| } | ||
| const extension = normalizeLowercaseStringOrEmpty(path.extname(normalized)); | ||
| if (extension.length === 0 || extension === ".md") { |
There was a problem hiding this comment.
Do not drop directory traversal for dotted folder names
Chokidar v5 calls the ignored predicate before stat information is available for newly discovered entries, so directory paths can arrive without stats. In that case, a folder like memory/topic.v1 falls through to extension filtering and is treated as a non-markdown file, causing it to be ignored and preventing traversal into topic.v1; markdown files created under that folder never emit watch events, so watch-driven memory sync can miss real content updates.
Useful? React with 👍 / 👎.
| if (extension.length === 0 || extension === ".md") { | ||
| return false; |
There was a problem hiding this comment.
Exclude extensionless files from watch-triggered syncs
The new filter allows any path with path.extname(...) === "", which includes common non-indexable files such as .DS_Store, .env, and README. Those files now still fire add/change events under the watched memory directory and trigger watch syncs even though they can never be indexed, so this leaves a churn path that can cause unnecessary repeated sync work on macOS/editor-heavy workspaces.
Useful? React with 👍 / 👎.
…macOS/Node 25 When watching the memory directory directly (introduced in openclaw#64711 to fix recursive glob issues on macOS + Node 25), chokidar fires events for all file types within the directory tree — including .json state files written by the dreaming subsystem under memory/.dreams/. Previously, markDirty had no extension check, so every .json write from dreaming (phase-signals.json, short-term-recall.json, etc.) would set dirty=true and trigger a needless sync. More critically, under high I/O load (e.g. dreaming writing many state files rapidly), the debounce timer kept being reset, which could delay or suppress the sync for .md files. Fix: add an explicit extension check in markDirty so only .md files (and configured multimodal extensions) trigger the dirty flag and schedule a sync. Non-markdown events are silently ignored. This completes the fix started in openclaw#64711 and addresses the reported behaviour where newly created .md files were not being indexed until a manual openclaw memory index or memory_search was performed.
…r directly (openclaw#64711) * fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly * fix(memory-core): ignore non-markdown memory watch churn * fix(memory-core): allow multimodal watch events * test(memory-core): type watcher ignore callback --------- Co-authored-by: Vincent Koc <[email protected]>
…r directly (openclaw#64711) * fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly * fix(memory-core): ignore non-markdown memory watch churn * fix(memory-core): allow multimodal watch events * test(memory-core): type watcher ignore callback --------- Co-authored-by: Vincent Koc <[email protected]>
…r directly (openclaw#64711) * fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly * fix(memory-core): ignore non-markdown memory watch churn * fix(memory-core): allow multimodal watch events * test(memory-core): type watcher ignore callback --------- Co-authored-by: Vincent Koc <[email protected]>
…r directly (openclaw#64711) * fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly * fix(memory-core): ignore non-markdown memory watch churn * fix(memory-core): allow multimodal watch events * test(memory-core): type watcher ignore callback --------- Co-authored-by: Vincent Koc <[email protected]>
…r directly (openclaw#64711) * fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly * fix(memory-core): ignore non-markdown memory watch churn * fix(memory-core): allow multimodal watch events * test(memory-core): type watcher ignore callback --------- Co-authored-by: Vincent Koc <[email protected]>
…ories Closes openclaw#86613. A single authorized POST /tools/invoke memory_search call against a workspace with multi-thousand .md files under <workspace>/memory/ opens one read-only FD per file (~12,400 in our captures), never released until process exit. Root cause: chokidar v5 calls fs.watch(path) per filesystem node (handler.js:126), no recursive option. On macOS each per-file fs.watch opens a kqueue VNODE FD that lsof reports as a regular-file REG FD. So one watcher per .md file = one FD per file = 12k+ FDs. Switch directory watch paths to Node >= 22 native recursive fs.watch(dir, { recursive: true }) — one watcher per directory via FSEvents on macOS / inotify recursive on Linux / ReadDirectoryChangesW on Windows. Individual file paths (MEMORY.md, file-typed extraPaths) continue through chokidar. Same dirty-event semantics, different backend, small constant FD profile regardless of tree size. Regression introduced by openclaw#64711 (jasonxargs-boop, 2026-04-13) which changed the chokidar watch target from glob memory/**/*.md to bare directory memory/, making the recursive walk implicit. PR openclaw#81802 (frankekn, 2026-05-15) removed awaitWriteFinish and added watch-settle on the same path; that reduced write-polling pressure but did not address the per-node fs.watch allocation (verification used 1,000 files, well under the storm-producing scale). Lab verification on clean upstream-main + synthetic 12,391-file workspace: 0 → 1 REG FD (MEMORY.md only; 12,391 file events covered by one recursive kqueue/FSEvents watcher), flat plateau, second memory_search returns 200 OK with zero additional FDs. Complementary to openclaw#86345 which attacks the same failure class by bounding INDEX_CACHE lifetime; this PR is non-overlapping in manager-sync-ops.ts and touches a disjoint region of manager.ts. Changes: - manager-sync-ops.ts: split watchPaths by dir/file; native recursive fs.watch for dirs with shouldIgnoreMemoryWatchPath filter and per-event lstat; chokidar retained for files. New TEST_MEMORY_NATIVE_WATCH_FACTORY_KEY for test injection. Native creation failure falls back into chokidar set. Null filename (Node platform caveat) triggers broad markDirty. Runtime error closes + removes + marks dirty. Idempotence guard expanded. Symlink policy preserved (extraPaths-only skip). - manager.ts: close() tears down nativeMemoryWatchers in addition to chokidar watcher. - manager.watcher-config.test.ts: nativeWatchMock paired with new factory key; assertions split between chokidar (files) and native (dirs); new cases for rename/change dispatch, null filename, native creation failure fallback, runtime error close, ensureWatcher re-entrancy guard. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ories Closes openclaw#86613. A single authorized POST /tools/invoke memory_search call against a workspace with multi-thousand .md files under <workspace>/memory/ opens one read-only FD per file (~12,400 in our captures), never released until process exit. Root cause: chokidar v5 calls fs.watch(path) per filesystem node (handler.js:126), no recursive option. On macOS each per-file fs.watch opens a kqueue VNODE FD that lsof reports as a regular-file REG FD. So one watcher per .md file = one FD per file = 12k+ FDs. Switch directory watch paths to Node >= 22 native recursive fs.watch(dir, { recursive: true }) — one watcher per directory via FSEvents on macOS / inotify recursive on Linux / ReadDirectoryChangesW on Windows. Individual file paths (MEMORY.md, file-typed extraPaths) continue through chokidar. Same dirty-event semantics, different backend, small constant FD profile regardless of tree size. Regression introduced by openclaw#64711 (jasonxargs-boop, 2026-04-13) which changed the chokidar watch target from glob memory/**/*.md to bare directory memory/, making the recursive walk implicit. PR openclaw#81802 (frankekn, 2026-05-15) removed awaitWriteFinish and added watch-settle on the same path; that reduced write-polling pressure but did not address the per-node fs.watch allocation (verification used 1,000 files, well under the storm-producing scale). Lab verification on clean upstream-main + synthetic 12,391-file workspace: 0 → 1 REG FD (MEMORY.md only; 12,391 file events covered by one recursive kqueue/FSEvents watcher), flat plateau, second memory_search returns 200 OK with zero additional FDs. Complementary to openclaw#86345 which attacks the same failure class by bounding INDEX_CACHE lifetime; this PR is non-overlapping in manager-sync-ops.ts and touches a disjoint region of manager.ts. Changes: - manager-sync-ops.ts: split watchPaths by dir/file; native recursive fs.watch for dirs with shouldIgnoreMemoryWatchPath filter and per-event lstat; chokidar retained for files. New TEST_MEMORY_NATIVE_WATCH_FACTORY_KEY for test injection. Native creation failure falls back into chokidar set. Null filename (Node platform caveat) triggers broad markDirty. Runtime error closes + removes + marks dirty. Idempotence guard expanded. Symlink policy preserved (extraPaths-only skip). - manager.ts: close() tears down nativeMemoryWatchers in addition to chokidar watcher. - manager.watcher-config.test.ts: nativeWatchMock paired with new factory key; assertions split between chokidar (files) and native (dirs); new cases for rename/change dispatch, null filename, native creation failure fallback, runtime error close, ensureWatcher re-entrancy guard. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ories Closes openclaw#86613. A single authorized POST /tools/invoke memory_search call against a workspace with multi-thousand .md files under <workspace>/memory/ opens one read-only FD per file (~12,400 in our captures), never released until process exit. Root cause: chokidar v5 calls fs.watch(path) per filesystem node (handler.js:126), no recursive option. On macOS each per-file fs.watch opens a kqueue VNODE FD that lsof reports as a regular-file REG FD. So one watcher per .md file = one FD per file = 12k+ FDs. Switch directory watch paths to Node >= 22 native recursive fs.watch(dir, { recursive: true }) — one watcher per directory via FSEvents on macOS / inotify recursive on Linux / ReadDirectoryChangesW on Windows. Individual file paths (MEMORY.md, file-typed extraPaths) continue through chokidar. Same dirty-event semantics, different backend, small constant FD profile regardless of tree size. Regression introduced by openclaw#64711 (jasonxargs-boop, 2026-04-13) which changed the chokidar watch target from glob memory/**/*.md to bare directory memory/, making the recursive walk implicit. PR openclaw#81802 (frankekn, 2026-05-15) removed awaitWriteFinish and added watch-settle on the same path; that reduced write-polling pressure but did not address the per-node fs.watch allocation (verification used 1,000 files, well under the storm-producing scale). Lab verification on clean upstream-main + synthetic 12,391-file workspace: 0 → 1 REG FD (MEMORY.md only; 12,391 file events covered by one recursive kqueue/FSEvents watcher), flat plateau, second memory_search returns 200 OK with zero additional FDs. Complementary to openclaw#86345 which attacks the same failure class by bounding INDEX_CACHE lifetime; this PR is non-overlapping in manager-sync-ops.ts and touches a disjoint region of manager.ts. Changes: - manager-sync-ops.ts: split watchPaths by dir/file; native recursive fs.watch for dirs with shouldIgnoreMemoryWatchPath filter and per-event lstat; chokidar retained for files. New TEST_MEMORY_NATIVE_WATCH_FACTORY_KEY for test injection. Native creation failure falls back into chokidar set. Null filename (Node platform caveat) triggers broad markDirty. Runtime error closes + removes + marks dirty. Idempotence guard expanded. Symlink policy preserved (extraPaths-only skip). - manager.ts: close() tears down nativeMemoryWatchers in addition to chokidar watcher. - manager.watcher-config.test.ts: nativeWatchMock paired with new factory key; assertions split between chokidar (files) and native (dirs); new cases for rename/change dispatch, null filename, native creation failure fallback, runtime error close, ensureWatcher re-entrancy guard. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…r directly (openclaw#64711) * fix(memory-core): fix macOS chokidar glob issue by watching memory dir directly * fix(memory-core): ignore non-markdown memory watch churn * fix(memory-core): allow multimodal watch events * test(memory-core): type watcher ignore callback --------- Co-authored-by: Vincent Koc <[email protected]>
On some macOS + Node 25 environments, chokidar fails to watch files when using recursive glob patterns like 'memory/**/*.md'. Changing to watch the folder directly resolves the issue while keeping same recursive watch behavior.