* Recursively sweep stale *.lock directories under the planning dir. * Symlinks are never followed or deleted. Plain files named *.lock are * intentionally left alone (session-end.lock is a regular file).
(dir, nowMs, dryRun, result, depth)
| 109 | * intentionally left alone (session-end.lock is a regular file). |
| 110 | */ |
| 111 | function sweepLockdirs(dir, nowMs, dryRun, result, depth) { |
| 112 | if (depth > MAX_LOCK_SCAN_DEPTH) return; |
| 113 | let entries = []; |
| 114 | try { |
| 115 | entries = fs.readdirSync(dir, { withFileTypes: true }); |
| 116 | } catch { |
| 117 | return; // dir missing or unreadable |
| 118 | } |
| 119 | for (const entry of entries) { |
| 120 | if (entry.isSymbolicLink()) continue; // never follow symlinks |
| 121 | if (!entry.isDirectory()) continue; |
| 122 | const entryPath = path.join(dir, entry.name); |
| 123 | if (entry.name.endsWith('.lock')) { |
| 124 | try { |
| 125 | const st = fs.lstatSync(entryPath); |
| 126 | if (st.isSymbolicLink() || !st.isDirectory()) continue; |
| 127 | if (nowMs - st.mtimeMs < LOCKDIR_STALE_MS) continue; // fresh, maybe live |
| 128 | if (dryRun) { |
| 129 | result.lockdirs++; |
| 130 | result.removed.push(entryPath); |
| 131 | continue; |
| 132 | } |
| 133 | fs.rmSync(entryPath, { recursive: true, force: true }); |
| 134 | result.lockdirs++; |
| 135 | result.removed.push(entryPath); |
| 136 | } catch (err) { |
| 137 | result.errors++; |
| 138 | result.failed.push({ path: entryPath, error: String(err && err.message || err) }); |
| 139 | } |
| 140 | } else { |
| 141 | sweepLockdirs(entryPath, nowMs, dryRun, result, depth + 1); |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Run the hygiene sweep. |