[Bug]: [Bug]: #765's "not found" fix misses the filesystem backend's ENOENT message, still crashing the run on page skip
EDIT: Updated description after some more investigation
Description
Follow-up to #765. That issue is fixed for the backend it was reported against, but the same restoreRepositoryPageMarkdown crash still aborts entire runs on v0.5.1 via the filesystem backend. There are two independent layers, and fixing either one alone stops the crash.
Layer 1 - deepagents: resolveDeletePath throws on an expected condition.
FilesystemBackend.delete() handles a missing file gracefully:
const deletePath = await this.resolveDeletePath(resolvedPath, filePath); // line 841
const stat = await fs.lstat(deletePath).catch(() => null); // line 842
if (!stat) return { error: `Error: '${filePath}' not found` }; // line 843Line 842 guards its lstat, and line 843 returns a message containing "not found", which openwiki tolerates. But line 841 runs first, and resolveDeletePath walks each parent segment with an unguarded lstat as a symlink-escape check:
for (const segment of segments.slice(0, -1)) {
current = path.join(current, segment);
if ((await fs.lstat(current)).isSymbolicLink()) throw new Erllowed: ${filePath}`);
}For a page that was never written, the parent directory does not exist - directories are only created by write()'s mkdir(dirname, { recursive: true }), which never ran. So the raw ENOENT escapes to the outer catch and is formatted as Error deleting '${filePath}': ${error.message}, discarding error.code. This is why the error names the directory rather than the file.
A missing parent directory is the normal case when rolling back a page that was never written, so a safety check is throwing on a routine condition.
Layer 2 - openwiki: the guard matches prose, not codes.
function isNotFoundBackendError(error) {
return error === "file_not_found" || error.includes("not found");
}| Case | Error string | Matches |
|---|---|---|
| In-memory backend, missing file | Error: File '<path>' not found |
yes (#765) |
| Filesystem backend, missing file | Error: '<path>' not found |
yes |
| Filesystem backend, missing parent dir | Error deleting '<path>': ENOENT: no such file or directory, lstat '<dir>' |
no |
Only the third case crashes, and it is precisely the case that occurs for a never-written page in a new subdirectory.
Consequences are those #765 documented: a recoverable single-page skip becomes a fatal invalid_state, the run aborts, .run.json is left behind, and completed pages cannot serve as an --update baseline.
Steps to Reproduce
1. openwiki --init on a repository, using the filesystem backend.
2. Have the plan include a page whose directory does not yet exist on disk (for me openwiki/operations/environment-setup.md).
3. Have that page's worker exit without submitting, so skipRepositoryPage() runs.
Expected Behavior
Per #765 and #732: the page is marked skipped, the deferred-page warning is emitted, and the run continues, reconsidering that page on the next update.
Actual Behavior
name RepositoryRunError
message Could not restore /openwiki/operations/environment-setup.md: Error deleting
'/openwiki/operations/environment-setup.md': ENOENT: no such file or directory,
lstat '/Users/.../openwiki/operations'
code invalid_state
at restoreRepositoryPageMarkdown (dist/generation/repository-run.js:618:15)
at async skipRepositoryPage (dist/generation/repository-run.js:586:5)
at async runPageAgent (dist/agent/repository-runner.js:342:5)
at async runPendingPageAgents (dist/agent/repository-runner.js:244:33)
at async runNativeRepositoryGeneration (dist/agent/repository-runner.js:117:34)Environment
- OS: macOS 15.7.9
- Node.js version: 26.7.0
- OpenWiki version: 0.5.1
- Provider: openrouter (provider-independent; the failure is after generation)
Additional Context
#765's offered an alternative the merged fix did not adopt - on the delete branch, tolerate any error, because the intent is only "ensure the file is gone" and it already is:
if (result.error && snapshot.markdown !== null) {
throw new RepositoryRunError("invalid_state", `Could not restore ${snapshot.path}: ${result.error}`);
}Better still would be preserving a structured code through the backend result so the guard can test code === "ENOENT" instead of reading English.
Secondary issue: the root cause is discarded. runPageAgent() swallows the original error on the skip path (src/agent/repository-runner.ts, around line 331):
catch (error) {
if (submitted) return null;
if (fatalSubmissionFailure) throw error;
await skipRepositoryPage(run, snapshot); // throws, replacing `error`
emitDeferredPageWarning(job.path, onEvent);
return snapshot;
}error is never logged or re-thrown, and skipRepositoryPage() throws from inside the handler, so the secondary failure replaces the cause. Even with OPENWIKI_DEBUG=1 there is no way to learn why the page bailed. Once the crash is fixed, the skip then becomes silent, and a page that defers on every run gives no signal at all.
Happy to open a PR
Source: langchain-ai/openwiki