clawmem 0.2.3 → 0.2.5
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 +12 -0
- package/CLAUDE.md +12 -0
- package/SKILL.md +12 -0
- package/package.json +1 -1
- package/src/clawmem.ts +1 -1
- package/src/indexer.ts +1 -1
- package/src/watcher.ts +91 -30
package/AGENTS.md
CHANGED
|
@@ -636,6 +636,18 @@ 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
|
+
→ v0.2.4 fix: hook's SQLite busy_timeout was 500ms — too tight. During A-MEM enrichment
|
|
640
|
+
or heavy indexing, watcher write locks exceed 500ms, causing SQLITE_BUSY. Raised to
|
|
641
|
+
5000ms (matches MCP server). Still completes within the 8s outer timeout.
|
|
642
|
+
|
|
643
|
+
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
644
|
+
→ Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
|
645
|
+
including excluded dirs (gits/, node_modules/, .git/). Broad collection paths like
|
|
646
|
+
~/Projects with 67K subdirs exhausted inotify limits.
|
|
647
|
+
→ v0.2.3 fix: watcher walks dir trees at startup, skips excluded subtrees, watches
|
|
648
|
+
non-excluded dirs individually. 500-dir cap per collection path.
|
|
649
|
+
→ Diagnosis: `ls /proc/$(pgrep -f "clawmem.*watch")/fd | wc -l` — healthy < 15K.
|
|
650
|
+
→ If still high: narrow broad collection paths. See docs/troubleshooting.md for details.
|
|
639
651
|
```
|
|
640
652
|
|
|
641
653
|
## CLI Reference
|
package/CLAUDE.md
CHANGED
|
@@ -636,6 +636,18 @@ 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
|
+
→ v0.2.4 fix: hook's SQLite busy_timeout was 500ms — too tight. During A-MEM enrichment
|
|
640
|
+
or heavy indexing, watcher write locks exceed 500ms, causing SQLITE_BUSY. Raised to
|
|
641
|
+
5000ms (matches MCP server). Still completes within the 8s outer timeout.
|
|
642
|
+
|
|
643
|
+
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
644
|
+
→ Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
|
645
|
+
including excluded dirs (gits/, node_modules/, .git/). Broad collection paths like
|
|
646
|
+
~/Projects with 67K subdirs exhausted inotify limits.
|
|
647
|
+
→ v0.2.3 fix: watcher walks dir trees at startup, skips excluded subtrees, watches
|
|
648
|
+
non-excluded dirs individually. 500-dir cap per collection path.
|
|
649
|
+
→ Diagnosis: `ls /proc/$(pgrep -f "clawmem.*watch")/fd | wc -l` — healthy < 15K.
|
|
650
|
+
→ If still high: narrow broad collection paths. See docs/troubleshooting.md for details.
|
|
639
651
|
```
|
|
640
652
|
|
|
641
653
|
## CLI Reference
|
package/SKILL.md
CHANGED
|
@@ -642,6 +642,18 @@ 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
|
+
-> v0.2.4 fix: hook's SQLite busy_timeout was 500ms — too tight. During A-MEM enrichment
|
|
646
|
+
or heavy indexing, watcher write locks exceed 500ms, causing SQLITE_BUSY. Raised to
|
|
647
|
+
5000ms (matches MCP server). Still completes within the 8s outer timeout.
|
|
648
|
+
|
|
649
|
+
Symptom: WSL hangs or becomes unresponsive during long sessions / watcher has 100K+ FDs
|
|
650
|
+
-> Pre-v0.2.3: fs.watch(recursive: true) registered inotify watches on EVERY subdirectory,
|
|
651
|
+
including excluded dirs (gits/, node_modules/, .git/). Broad collection paths like
|
|
652
|
+
~/Projects with 67K subdirs exhausted inotify limits.
|
|
653
|
+
-> v0.2.3 fix: watcher walks dir trees at startup, skips excluded subtrees, watches
|
|
654
|
+
non-excluded dirs individually. 500-dir cap per collection path.
|
|
655
|
+
-> Diagnosis: `ls /proc/$(pgrep -f "clawmem.*watch")/fd | wc -l` — healthy < 15K.
|
|
656
|
+
-> If still high: narrow broad collection paths. See docs/troubleshooting.md.
|
|
645
657
|
```
|
|
646
658
|
|
|
647
659
|
---
|
package/package.json
CHANGED
package/src/clawmem.ts
CHANGED
|
@@ -772,7 +772,7 @@ async function cmdSurface(args: string[]) {
|
|
|
772
772
|
if (!input) process.exit(0);
|
|
773
773
|
|
|
774
774
|
// Open store: writable for both (context-surfacing writes dedupe data)
|
|
775
|
-
const s = createStore(undefined, { busyTimeout:
|
|
775
|
+
const s = createStore(undefined, { busyTimeout: 5000 });
|
|
776
776
|
|
|
777
777
|
try {
|
|
778
778
|
if (isBootstrap) {
|
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
|
|