clawmem 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -0
- package/CLAUDE.md +9 -0
- package/SKILL.md +9 -0
- package/package.json +1 -1
- package/src/indexer.ts +1 -1
- package/src/watcher.ts +91 -30
package/AGENTS.md
CHANGED
|
@@ -636,6 +636,15 @@ Symptom: "UserPromptSubmit hook error" on context-surfacing hook (intermittent)
|
|
|
636
636
|
→ Default hook timeout is 8s (since v0.1.1). If you have an older install, re-run
|
|
637
637
|
`clawmem setup hooks`. If persistent, restart the watcher: `systemctl --user restart
|
|
638
638
|
clawmem-watcher.service`. Healthy memory is under 100MB — if 400MB+, restart clears it.
|
|
639
|
+
|
|
640
|
+
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
641
|
+
→ Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
|
642
|
+
including excluded dirs (gits/, node_modules/, .git/). Broad collection paths like
|
|
643
|
+
~/Projects with 67K subdirs exhausted inotify limits.
|
|
644
|
+
→ v0.2.3 fix: watcher walks dir trees at startup, skips excluded subtrees, watches
|
|
645
|
+
non-excluded dirs individually. 500-dir cap per collection path.
|
|
646
|
+
→ Diagnosis: `ls /proc/$(pgrep -f "clawmem.*watch")/fd | wc -l` — healthy < 15K.
|
|
647
|
+
→ If still high: narrow broad collection paths. See docs/troubleshooting.md for details.
|
|
639
648
|
```
|
|
640
649
|
|
|
641
650
|
## CLI Reference
|
package/CLAUDE.md
CHANGED
|
@@ -636,6 +636,15 @@ Symptom: "UserPromptSubmit hook error" on context-surfacing hook (intermittent)
|
|
|
636
636
|
→ Default hook timeout is 8s (since v0.1.1). If you have an older install, re-run
|
|
637
637
|
`clawmem setup hooks`. If persistent, restart the watcher: `systemctl --user restart
|
|
638
638
|
clawmem-watcher.service`. Healthy memory is under 100MB — if 400MB+, restart clears it.
|
|
639
|
+
|
|
640
|
+
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
641
|
+
→ Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
|
642
|
+
including excluded dirs (gits/, node_modules/, .git/). Broad collection paths like
|
|
643
|
+
~/Projects with 67K subdirs exhausted inotify limits.
|
|
644
|
+
→ v0.2.3 fix: watcher walks dir trees at startup, skips excluded subtrees, watches
|
|
645
|
+
non-excluded dirs individually. 500-dir cap per collection path.
|
|
646
|
+
→ Diagnosis: `ls /proc/$(pgrep -f "clawmem.*watch")/fd | wc -l` — healthy < 15K.
|
|
647
|
+
→ If still high: narrow broad collection paths. See docs/troubleshooting.md for details.
|
|
639
648
|
```
|
|
640
649
|
|
|
641
650
|
## CLI Reference
|
package/SKILL.md
CHANGED
|
@@ -642,6 +642,15 @@ Symptom: "UserPromptSubmit hook error" on context-surfacing hook (intermittent)
|
|
|
642
642
|
-> Default hook timeout is 8s (since v0.1.1). If you have an older install, re-run
|
|
643
643
|
`clawmem setup hooks`. If persistent, restart the watcher: `systemctl --user restart
|
|
644
644
|
clawmem-watcher.service`. Healthy memory is under 100MB — if 400MB+, restart clears it.
|
|
645
|
+
|
|
646
|
+
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
647
|
+
-> Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
|
648
|
+
including excluded dirs (gits/, node_modules/, .git/). Broad collection paths like
|
|
649
|
+
~/Projects with 67K subdirs exhausted inotify limits.
|
|
650
|
+
-> v0.2.3 fix: watcher walks dir trees at startup, skips excluded subtrees, watches
|
|
651
|
+
non-excluded dirs individually. 500-dir cap per collection path.
|
|
652
|
+
-> Diagnosis: `ls /proc/$(pgrep -f "clawmem.*watch")/fd | wc -l` — healthy < 15K.
|
|
653
|
+
-> If still high: narrow broad collection paths. See docs/troubleshooting.md.
|
|
645
654
|
```
|
|
646
655
|
|
|
647
656
|
---
|
package/package.json
CHANGED
package/src/indexer.ts
CHANGED
package/src/watcher.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ClawMem File Watcher - fs.watch with debounce for incremental reindex
|
|
3
|
+
*
|
|
4
|
+
* Walks each directory tree at startup, skipping excluded dirs (gits/,
|
|
5
|
+
* node_modules/, .git/, etc.), and watches only non-excluded directories.
|
|
6
|
+
* This prevents inotify FD exhaustion on trees with large cloned repos.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
|
-
import { watch, type WatchEventType } from "fs";
|
|
6
|
-
import {
|
|
9
|
+
import { watch, readdirSync, statSync, type WatchEventType } from "fs";
|
|
10
|
+
import { join, relative } from "path";
|
|
11
|
+
import { shouldExclude, EXCLUDED_DIRS } from "./indexer.ts";
|
|
7
12
|
|
|
8
13
|
export type WatcherOptions = {
|
|
9
14
|
debounceMs?: number;
|
|
@@ -11,6 +16,42 @@ export type WatcherOptions = {
|
|
|
11
16
|
onError?: (error: Error) => void;
|
|
12
17
|
};
|
|
13
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Walk a directory tree, returning only directories that are NOT excluded.
|
|
21
|
+
* Stops recursion into excluded subtrees (gits/, node_modules/, .git/, etc.).
|
|
22
|
+
*/
|
|
23
|
+
function walkNonExcludedDirs(root: string): string[] {
|
|
24
|
+
const dirs: string[] = [root];
|
|
25
|
+
const queue: string[] = [root];
|
|
26
|
+
|
|
27
|
+
while (queue.length > 0) {
|
|
28
|
+
const current = queue.pop()!;
|
|
29
|
+
let entries: string[];
|
|
30
|
+
try {
|
|
31
|
+
entries = readdirSync(current);
|
|
32
|
+
} catch {
|
|
33
|
+
continue; // Permission denied or deleted
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
// Skip excluded directory names before stat
|
|
38
|
+
if (EXCLUDED_DIRS.has(entry) || (entry.startsWith(".") && entry !== ".")) continue;
|
|
39
|
+
|
|
40
|
+
const fullPath = join(current, entry);
|
|
41
|
+
try {
|
|
42
|
+
if (statSync(fullPath).isDirectory()) {
|
|
43
|
+
dirs.push(fullPath);
|
|
44
|
+
queue.push(fullPath);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// stat failed — skip
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return dirs;
|
|
53
|
+
}
|
|
54
|
+
|
|
14
55
|
export function startWatcher(
|
|
15
56
|
directories: string[],
|
|
16
57
|
options: WatcherOptions
|
|
@@ -20,34 +61,54 @@ export function startWatcher(
|
|
|
20
61
|
const watchers: ReturnType<typeof watch>[] = [];
|
|
21
62
|
|
|
22
63
|
for (const dir of directories) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
// Walk the tree, skipping excluded dirs — watch each non-excluded dir individually
|
|
65
|
+
const watchableDirs = walkNonExcludedDirs(dir);
|
|
66
|
+
|
|
67
|
+
// Safety: warn and cap if a single collection path produces too many dirs
|
|
68
|
+
const MAX_WATCH_DIRS = 500;
|
|
69
|
+
if (watchableDirs.length > MAX_WATCH_DIRS) {
|
|
70
|
+
console.log(`[watcher] WARNING: ${dir} has ${watchableDirs.length} dirs — capping at ${MAX_WATCH_DIRS} to prevent FD exhaustion. Consider narrowing the collection path.`);
|
|
71
|
+
watchableDirs.length = MAX_WATCH_DIRS;
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`[watcher] ${dir}: watching ${watchableDirs.length} dirs`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const watchDir of watchableDirs) {
|
|
77
|
+
try {
|
|
78
|
+
// Non-recursive watch — each dir watched individually
|
|
79
|
+
const watcher = watch(watchDir, (event, filename) => {
|
|
80
|
+
if (!filename) return;
|
|
81
|
+
// Accept .md files (indexing) and .jsonl only within .beads/ (Dolt backend)
|
|
82
|
+
const isMd = filename.endsWith(".md");
|
|
83
|
+
const isBeadsJsonl = filename.endsWith(".jsonl") && filename.includes(".beads/");
|
|
84
|
+
if (!isMd && !isBeadsJsonl) return;
|
|
85
|
+
|
|
86
|
+
const relativeToDirRoot = relative(dir, join(watchDir, filename));
|
|
87
|
+
if (shouldExclude(relativeToDirRoot)) return;
|
|
88
|
+
|
|
89
|
+
const fullPath = join(watchDir, filename);
|
|
90
|
+
const existing = pending.get(fullPath);
|
|
91
|
+
if (existing) clearTimeout(existing);
|
|
92
|
+
|
|
93
|
+
pending.set(fullPath, setTimeout(async () => {
|
|
94
|
+
pending.delete(fullPath);
|
|
95
|
+
try {
|
|
96
|
+
await onChanged(fullPath, event);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
99
|
+
}
|
|
100
|
+
}, debounceMs));
|
|
101
|
+
});
|
|
102
|
+
watcher.on("error", (err) => {
|
|
103
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
104
|
+
});
|
|
105
|
+
watchers.push(watcher);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Individual dir watch failure is non-fatal — skip it
|
|
108
|
+
if (onError) {
|
|
109
|
+
onError(err instanceof Error ? err : new Error(`Failed to watch ${watchDir}: ${err}`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
51
112
|
}
|
|
52
113
|
}
|
|
53
114
|
|