Skip to content

Commit 75a1da0

Browse files
committed
refactor(memory): detect failed memory scans via fs-safe failedDirs
Replace the manual fs.readdir root-probes in scanMemoryFiles with walkDirectory's failedDirs (fs-safe 0.4.0). collectMemoryFilesFromDir now reports whether the walk was complete; a failed readdir at any depth (not just the probed root) flags the scan not-ok so destructive prunes skip an incomplete listing rather than wiping unseen files.
1 parent f450fae commit 75a1da0

2 files changed

Lines changed: 25 additions & 18 deletions

File tree

packages/memory-host-sdk/src/host/internal.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ describe("memory host SDK package internals", () => {
190190
});
191191

192192
it("flags the scan when the memory dir readdir fails after a successful lstat", async () => {
193-
// fs-safe walkDirectory swallows readdir errors and returns an empty walk,
194-
// so the scan must probe the root read itself or a permission/NFS blip on
195-
// readdir would masquerade as an authoritatively empty dir.
193+
// fs-safe 0.4.0 surfaces a failed readdir via walkDirectory's failedDirs;
194+
// the scan must flag it so a permission/NFS blip on readdir does not
195+
// masquerade as an authoritatively empty dir.
196196
const tmpDir = getTmpDir();
197197
const memoryDir = path.join(tmpDir, "memory");
198198
fsSync.mkdirSync(memoryDir, { recursive: true });

packages/memory-host-sdk/src/host/internal.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,15 @@ function shouldDescendMemoryEntry(
127127
return entry.kind === "directory" && entry.name !== ".openclaw-repair";
128128
}
129129

130+
// Returns false when fs-safe reported a failed readdir (root or any subtree)
131+
// in `failedDirs`: the listing is then incomplete and destructive callers must
132+
// not treat unseen files as deleted.
130133
async function collectMemoryFilesFromDir(
131134
dir: string,
132135
files: string[],
133136
multimodal?: MemoryMultimodalSettings,
134137
shouldSkipPath?: (absPath: string) => boolean,
135-
): Promise<void> {
138+
): Promise<boolean> {
136139
const scan = await walkDirectory(dir, {
137140
symlinks: "skip",
138141
descend: (entry) => shouldDescendMemoryEntry(entry, shouldSkipPath),
@@ -142,14 +145,14 @@ async function collectMemoryFilesFromDir(
142145
isAllowedMemoryFilePath(entry.path, multimodal),
143146
});
144147
files.push(...scan.entries.map((entry) => entry.path));
148+
return scan.failedDirs.length === 0;
145149
}
146150

147151
// Result of enumerating memory files. `ok` is false when a listing root (the
148-
// workspace memory dir, an extra path, or the root memory file stat) failed
149-
// for a non-missing reason; destructive callers (stale-row pruning) must then
150-
// skip rather than treat unseen files as deleted. Failures in subtrees below
151-
// the probed roots stay invisible (fs-safe walkDirectory swallows them); the
152-
// flag guards the whole-index wipe hazard, not per-file drift.
152+
// workspace memory dir, an extra path, or the root memory file stat) failed for
153+
// a non-missing reason, or when fs-safe walkDirectory reported a failed readdir
154+
// at any depth (failedDirs). Destructive callers (stale-row pruning) must then
155+
// skip rather than treat unseen files as deleted.
153156
export type MemoryFilesScanResult = { ok: boolean; files: string[] };
154157

155158
export async function scanMemoryFiles(
@@ -196,11 +199,15 @@ export async function scanMemoryFiles(
196199
try {
197200
const dirStat = await fs.lstat(memoryDir);
198201
if (!dirStat.isSymbolicLink() && dirStat.isDirectory()) {
199-
// fs-safe walkDirectory swallows readdir errors, even at the root, so
200-
// probe the root read explicitly: a transient failure must flag the
201-
// scan instead of reading as an empty dir and triggering mass pruning.
202-
await fs.readdir(memoryDir);
203-
await collectMemoryFilesFromDir(memoryDir, result, multimodal, shouldSkipWorkspaceMemoryPath);
202+
const scanOk = await collectMemoryFilesFromDir(
203+
memoryDir,
204+
result,
205+
multimodal,
206+
shouldSkipWorkspaceMemoryPath,
207+
);
208+
if (!scanOk) {
209+
ok = false;
210+
}
204211
}
205212
} catch (err) {
206213
if (!isFileMissingError(err)) {
@@ -220,15 +227,15 @@ export async function scanMemoryFiles(
220227
continue;
221228
}
222229
if (stat.isDirectory()) {
223-
// Same root-read probe as the memory dir: walkDirectory cannot
224-
// report a failed readdir.
225-
await fs.readdir(inputPath);
226-
await collectMemoryFilesFromDir(
230+
const scanOk = await collectMemoryFilesFromDir(
227231
inputPath,
228232
result,
229233
multimodal,
230234
shouldSkipWorkspaceMemoryPath,
231235
);
236+
if (!scanOk) {
237+
ok = false;
238+
}
232239
continue;
233240
}
234241
if (stat.isFile() && isAllowedMemoryFilePath(inputPath, multimodal)) {

0 commit comments

Comments
 (0)