@@ -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.
130133async 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.
153156export type MemoryFilesScanResult = { ok : boolean ; files : string [ ] } ;
154157
155158export 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