Skip to content

Commit 8f84ed3

Browse files
committed
fix(memory-wiki): skip prune when configured path is transiently unavailable (#97523)
1 parent 27c1685 commit 8f84ed3

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

extensions/memory-wiki/src/unsafe-local.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,49 @@ describe("syncMemoryWikiUnsafeLocalSources", () => {
135135
"# very private",
136136
);
137137
});
138+
139+
it("preserves imported pages when configured path becomes transiently unavailable", async () => {
140+
// Use a nested path where the parent becomes empty after removal,
141+
// simulating a NAS mount point or removable drive being undocked.
142+
const mountPoint = nextCaseRoot("nas-mount");
143+
await fs.mkdir(mountPoint, { recursive: true });
144+
const sourceDir = path.join(mountPoint, "source");
145+
await fs.mkdir(sourceDir, { recursive: true });
146+
const filePath = path.join(sourceDir, "notes.md");
147+
await fs.writeFile(filePath, "# my notes\n", "utf8");
148+
149+
const { rootDir: vaultDir, config } = await createVault({
150+
rootDir: nextCaseRoot("transient-vault"),
151+
config: {
152+
vaultMode: "unsafe-local",
153+
unsafeLocal: {
154+
allowPrivateMemoryCoreAccess: true,
155+
paths: [sourceDir],
156+
},
157+
},
158+
});
159+
160+
const first = await syncMemoryWikiUnsafeLocalSources(config);
161+
expect(first.importedCount).toBe(1);
162+
expect(first.removedCount).toBe(0);
163+
const firstPagePath = first.pagePaths[0] ?? "";
164+
165+
// Simulate the path becoming transiently unavailable (drive undocked, NAS
166+
// rebooting). Remove both sourceDir and mountPoint so the parent of the
167+
// configured path is inaccessible.
168+
await fs.rm(mountPoint, { recursive: true, force: true });
169+
const second = await syncMemoryWikiUnsafeLocalSources(config);
170+
expect(second.artifactCount).toBe(0);
171+
expect(second.removedCount).toBe(0);
172+
await expect(fs.readFile(path.join(vaultDir, firstPagePath), "utf8")).resolves.toContain(
173+
"# my notes",
174+
);
175+
176+
// Restore the path — next sync should re-reconcile cleanly.
177+
await fs.mkdir(mountPoint, { recursive: true });
178+
await fs.mkdir(sourceDir, { recursive: true });
179+
await fs.writeFile(filePath, "# my notes\n", "utf8");
180+
const third = await syncMemoryWikiUnsafeLocalSources(config);
181+
expect(third.removedCount).toBe(0);
182+
});
138183
});

extensions/memory-wiki/src/unsafe-local.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ async function listAllowedFilesRecursive(rootDir: string): Promise<string[]> {
6464
return files.toSorted((left, right) => left.localeCompare(right));
6565
}
6666

67+
async function isParentAccessible(configuredPath: string): Promise<boolean> {
68+
try {
69+
await fs.access(path.dirname(path.resolve(configuredPath)));
70+
return true;
71+
} catch {
72+
return false;
73+
}
74+
}
75+
6776
async function collectUnsafeLocalArtifacts(
6877
configuredPaths: string[],
6978
): Promise<UnsafeLocalArtifact[]> {
@@ -236,12 +245,25 @@ export async function syncMemoryWikiUnsafeLocalSources(
236245
}),
237246
);
238247

239-
const removedCount = await pruneImportedSourceEntries({
240-
vaultRoot: config.vault.path,
241-
group: "unsafe-local",
242-
activeKeys,
243-
state,
244-
});
248+
// Skip pruning when zero artifacts were collected AND at least one configured
249+
// path has an inaccessible parent directory (transiently unavailable —
250+
// undocked drive, NAS rebooting, cloud folder not mounted yet). In that
251+
// state, zero artifacts is indistinguishable from "all sources deleted."
252+
// Pruning would irreversibly delete imported pages and user-authored
253+
// human-notes blocks. When the parent IS accessible but yields no artifacts,
254+
// the user intentionally removed the source files, so pruning proceeds
255+
// normally. See #97523.
256+
const hasInaccessiblePath =
257+
artifacts.length === 0 &&
258+
!(await Promise.all(config.unsafeLocal.paths.map(isParentAccessible))).every(Boolean);
259+
const removedCount = hasInaccessiblePath
260+
? 0
261+
: await pruneImportedSourceEntries({
262+
vaultRoot: config.vault.path,
263+
group: "unsafe-local",
264+
activeKeys,
265+
state,
266+
});
245267
await writeMemoryWikiSourceSyncState(config.vault.path, state);
246268
const importedCount = results.filter((result) => result.changed && result.created).length;
247269
const updatedCount = results.filter((result) => result.changed && !result.created).length;

0 commit comments

Comments
 (0)