Skip to content

🧭 fix: Report Missing Workspace Paths as NOT_FOUND and Create Missing Parents on Write - #301

Merged
danny-avila merged 4 commits into
mainfrom
danny-avila/workspace-not-found
Oct 3, 2026
Merged

danny-avila merged 4 commits into
mainfrom
danny-avila/workspace-not-found

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Agents often get INVALID_PATH ("Invalid workspace path") for a well-formed path, and conclude the path format is wrong. Over 7 days on demo.librechat.ai, 57 workspace file-tool calls failed with 422 INVALID_PATH. Only one of them involved a real path-safety problem; the rest were well-formed paths to files that did not exist:

  • About 30 were read_file calls on log files that a background command had not written yet (.checks/tests.log, .verification/static.log). Several succeeded when the agent read the same path again later.
  • Most of the rest were guessed paths, e.g. packages/client/src/components/Spinner.tsx when the file lives under svgs/.
  • 5 were create_file calls into a directory that did not exist yet: packages/dev-tools/package.json in a new package, __fixtures__/edit-worker.cjs, Handoffs/Notice.tsx.

The cause is in the worker. readConfinedFileBuffer, editWorkspaceFile, searchWorkspace and listWorkspaceFiles fold every filesystem error, including ENOENT, into INVALID_PATH. atomicWriteConfinedFile treats a missing parent directory the same way. So "this file is not there yet" and "this path escapes the workspace" produce the same error.

This PR makes two changes.

NOT_FOUND for absent paths. When a read, edit, preview, search scope or listing scope does not exist, the worker walks the path from the root with lstat. It reports NOT_FOUND ("Workspace path does not exist") only when the walk reaches the missing entry through real directories, root included, and those directories are unchanged (same device and inode) when rechecked after the miss. Everything else keeps INVALID_PATH: any symlink in the chain, a file used as a directory, an ancestor replaced mid-walk, traversal and absolute paths. The split therefore reveals nothing about paths outside the root. A search that skips a file which vanished mid-scan treats NOT_FOUND like INVALID_PATH.

write_file creates missing parent directories. Before installing the file, the worker creates each missing ancestor beneath the workspace or lane root, one level at a time:

  • Creation runs only when the root is held by descriptor: selected project roots, linked-worktree lanes and conversation worktrees. There, mkdir and lstat go through mkdirat/openat from the held root fd, and each parent descriptor is checked for root ancestry, so a swapped symlink cannot redirect creation outside the root. A pathname-only root (legacy --directory without identity) creates nothing and reports the missing parent as NOT_FOUND.
  • An existing symlink or file anywhere in the parent chain stops the walk with INVALID_PATH, as before. A write still never goes through a symlinked directory, even one inside the root.
  • Each new directory, including one another process created between the lstat and the mkdir, is synced into its parent before the write is acknowledged. confirmInstalledMutation still syncs the final parent.
  • New directories use mode 0o777 minus the process umask, so they get the same permissions as mkdir -p. The existing 0o700 default stays for every other caller of the mkdir adapter.
  • Created directories are a committed side effect, like mkdir -p, and are never rolled back. A cancellation that arrives after they exist does not stop the write. Any later failure of the write (a storage error, a raced conflict, or a created directory resolving outside the root) is reported as a possible mutation, so the worker quarantines instead of claiming an atomic rejection. A write cancelled or rejected before creating anything stays atomic.

Compatibility

NOT_FOUND is a new settlement error code, and an older Code API rejects any settlement whose code it does not know (isSettlement returns 400 "Invalid bridge settlement"). Three things keep mixed versions working:

  • Code API lists supportedWorkspaceToolErrorCodes: ['NOT_FOUND'] in its registration response. /workspace-tools/execute returns NOT_FOUND as HTTP 422, the status INVALID_PATH already uses, so existing LibreChat clients take the same path-error branch.
  • A worker sends NOT_FOUND only after the current registration advertised it. Otherwise it settles with INVALID_PATH and keeps the new message ("Workspace path does not exist"). An older Code API can still validate the settlement, and the agent still sees the clearer text.
  • If a settlement carrying NOT_FOUND gets a 400 anyway, for example from an older replica during a rolling deploy, the worker resends it once with INVALID_PATH. WORKSPACE_TOOL_ERROR_CODE_FALLBACKS in protocol.ts is the single source of that mapping.

New Code API with an older worker: nothing changes. Older workers never send NOT_FOUND, and they ignore the extra registration field.

LibreChat will read NOT_FOUND, and the legacy INVALID_PATH from older workers, to give agents a specific message in a separate PR.

Testing

  • packages/code npm test on Node 24.16.0 in WSL: 749 tests. The only failures are the same 9 PTC-watchdog and credential/identity-storage tests that fail identically on unmodified main in this environment (they assert POSIX permission behavior that /mnt/c and this kernel do not provide).
  • New and updated tests:
    • workspace.test.ts:
      • NOT_FOUND for a missing file and a missing nested directory, across read, edit, preview, search and list.
      • INVALID_PATH for the same shapes through an in-root or escaping symlink, a dangling symlink, and a file used as a directory.
      • Traversal is still rejected, and no message contains the host path.
      • On a held root, writes create deep parents in both replace and create mode.
      • Parent creation refuses escaping, dangling and in-root directory symlinks and files in the chain, and creates nothing outside the root.
      • A write cancelled after its parents exist still completes, and a write cancelled before creating anything leaves the workspace unchanged and stays atomic.
      • A pathname-only root reports a missing parent as NOT_FOUND and creates nothing.
    • linked-worktrees.test.ts: in a descriptor-bound lane, a write creates packages/new-package/ inside the lane only, a missing log reads NOT_FOUND, and a symlink from the lane to the checkout is refused for both writes and reads.
    • workspace-worker.test.ts: NOT_FOUND is sent only when registration advertised it and falls back to INVALID_PATH otherwise; a refused NOT_FOUND settlement is resent with INVALID_PATH; a refused legacy settlement is not retried.
    • protocol.test.ts: every added code is valid and maps to a legacy code.
  • service bun test src/workspace-tools/router.test.ts src/bridge/router.test.ts: 73 pass, covering NOT_FOUND and INVALID_PATH mapped to 422 and the registration advertisement.
  • service full bun run test: the 6 failures are timing-sensitive Redis/backend tests that pass when rerun alone and fail intermittently on main too. bun run build succeeds. tsc --noEmit shows only errors that already exist on main.

… Parents on Write

A missing file, directory or search scope no longer reports INVALID_PATH. When
every existing ancestor resolves to a directory inside the workspace, the
worker returns NOT_FOUND ("Workspace path does not exist"); escapes, symlinks,
files used as directories and other path-safety failures keep INVALID_PATH.

write_file creates missing parent directories beneath the workspace or lane
root, one verified level at a time, never through a symlink or an existing
file, and removes the directories it created if the write then fails.

Code API advertises the added code in registration and maps it to HTTP 422
like INVALID_PATH. A worker reports NOT_FOUND only to a Code API that
advertised it, and resends a refused settlement with INVALID_PATH, so older
Code API replicas keep accepting settlements.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 1 of 4 on head 098bf06.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-03T02:15:58.039845Z 4321ba9 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 098bf0679c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +444 to +446
await mkdir(current, 0o777);
created.push(current);
createdHere = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Sync new parent entries before acknowledging the write

When a write creates one or more missing parent directories, only the final containing directory is synced later by confirmInstalledMutation; the directory entries created here are never synced in their respective parents. After an acknowledged nested write, a crash can therefore lose an intermediate directory—and consequently the new file—even though the existing write path promises durable confirmation. Sync each parent after creating its child before returning success.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4e82c9e. Each directory this write creates is now synced into its parent (syncWorkspaceDirectory(dirname(created))) right after mkdir, before the file is installed. confirmInstalledMutation still syncs the final parent, so every new entry in the chain is durable before success is acknowledged.

Comment thread packages/code/src/workspace.ts Outdated
Comment on lines +468 to +470
async function removeCreatedDirectories(created: readonly string[]): Promise<void> {
for (let index = created.length - 1; index >= 0; index -= 1) {
await rmdir(created[index]).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report failed parent cleanup as a possible mutation

If cleanup fails because of an I/O or permission error, or because another process makes a created directory nonempty, swallowing the rmdir failure leaves directories created by this rejected write in the workspace. The original error still has mutationMayHaveCommitted === false, while LocalWorkspaceTools advertises mutationFailuresAreAtomic, so the worker will not quarantine the now-mutated workspace. A cleanup failure must instead be surfaced as mutation uncertainty.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4e82c9e. Cleanup now goes through withCreatedDirectoriesRemoved. A directory that is already gone (ENOENT) or that another writer has filled (ENOTEMPTY/EEXIST) no longer belongs to this write, so the rejection stays atomic. Any other rmdir failure re-raises the original error with mutationMayHaveCommitted: true, so the worker quarantines instead of reporting an atomic failure. A new test aborts the write after the parents exist and checks that they are removed and that the rejection stays atomic.

Comment on lines +434 to +439
let entry: Awaited<ReturnType<typeof lstat>> | undefined;
try {
entry = await lstat(current);
} catch (error) {
if (!isMissingEntry(error)) throw error;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hold each verified parent while creating descendants

For a workspace without a descriptor-bound identity, an existing directory is accepted from this pathname-based lstat but no handle to it is retained. If a background process renames that directory and replaces it with a symlink before the next loop iteration, the later mkdir(current, ...) follows the replacement and can create the next directory outside the workspace; the post-creation realpath check only detects the escape after the external mutation has occurred. Traverse and create relative to held, no-follow directory descriptors rather than reusing the verified pathname.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4e82c9e. Parent creation now runs only when the root is held by descriptor (holdsWorkspaceRoot()). That covers selected project roots, linked-worktree lanes and conversation worktrees, which all carry a captured identity. On those roots, mkdir and lstat go through mkdirat/openat from the held root fd, and each parent descriptor is checked for root ancestry before use, so a swapped symlink cannot redirect creation outside the root.

A pathname-only root (legacy --directory without identity) creates nothing. Its write reports the missing parent as NOT_FOUND, as before this PR except for the clearer code. Tests cover both cases: deep creation on a held root, and no creation on a pathname-only root.

…nup Failure

Parent directories are created only under a held root descriptor, where
mkdirat cannot follow a swapped path out of the root. A pathname-only root
creates nothing and reports the missing parent as NOT_FOUND.

Each created directory is synced into its parent before the write is
acknowledged, and a rejected write that cannot remove a directory it created
now reports a possible mutation instead of an atomic failure.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 2 of 4 on head 4e82c9e.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e82c9e83b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/code/src/workspace.ts Outdated
let removed = true;
for (let index = created.length - 1; index >= 0; index -= 1) {
try {
await rmdir(created[index]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fsync parent removals before declaring cleanup atomic

When a write is rejected after creating parent directories, each successful rmdir changes an entry in the containing directory, but that parent is never synced. A crash after the rejection can therefore restore one of the already-fsynced created directories even though LocalWorkspaceTools advertises mutationFailuresAreAtomic, so the worker will not quarantine the mutated workspace. Sync each containing directory after removal and report any sync failure as mutation uncertainty.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d0f6d78. withCreatedDirectoriesRemoved now syncs the containing directory after each successful rmdir. A failure in the removal or in its sync leaves the rejection uncertain (mutationMayHaveCommitted: true), so the worker quarantines instead of reporting an atomic failure.

Comment thread packages/code/src/workspace.ts Outdated
await rmdir(created[index]);
} catch (cleanupError) {
const code = (cleanupError as NodeJS.ErrnoException).code;
if (code !== 'ENOENT' && code !== 'ENOTEMPTY' && code !== 'EEXIST') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat a write-owned ENOTEMPTY cleanup failure as uncertain

Fresh evidence beyond the earlier cleanup thread is that ENOTEMPTY need not mean another writer filled the directory: if atomicWriteConfinedFile fails after staging, its finally block suppresses failure to unlink its own .librechat-code-*.tmp file. That leaked temporary then makes this rmdir return ENOTEMPTY, which is ignored, leaving both the temporary and created directories while the error still says no mutation may have committed. Treat this result as benign only after establishing that no write-owned temporary remains; otherwise mark the failure uncertain.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d0f6d78. ENOTEMPTY/EEXIST are no longer treated as benign. The only cleanup result accepted as clean is a directory that is already gone (ENOENT). Any directory left in place, including one holding a leaked .librechat-code-*.tmp, marks the rejection as a possible mutation. A new test leaves a staging file in the created directory and checks that the rejection reports mutationMayHaveCommitted.

Comment thread packages/code/src/workspace.ts Outdated
Comment on lines +399 to +400
return isMissingEntry(error)
? new WorkspaceToolError('Workspace path does not exist', 'NOT_FOUND')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate ancestry before classifying a raced miss

For a pathname-only workspace, a background process can replace an ancestor that was just observed as a directory with an escaping symlink before the next lstat. If the corresponding outside child is absent, that lstat returns ENOENT and this branch reports NOT_FOUND without confirming that the miss occurred inside the workspace; if the outside child exists, it instead returns INVALID_PATH, creating the outside-path existence distinction this classifier is intended to prevent. Revalidate the resolved ancestry before returning NOT_FOUND, or conservatively return INVALID_PATH when traversal cannot be descriptor-anchored.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d0f6d78. The classifier now records the device and inode of each real directory on the walk. After a miss, it re-runs lstat on all of them, and returns NOT_FOUND only if each is still the same non-symlink directory. Otherwise, including when an ancestor was replaced, it returns INVALID_PATH.

It also no longer follows directory symlinks at all. Any symlink in the chain, in-root or not, is INVALID_PATH, so the walk never resolves a path outside the root to decide which code to return. On held roots the lstat calls are already descriptor-anchored, with an ancestry check on each parent.

A miss is reported as NOT_FOUND only when the walk reached it through real
directories that still have the same identity afterwards; any symlink in the
chain stays INVALID_PATH. Cleanup syncs each removal into its parent and
treats any directory it could not durably remove as a possible mutation.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

Pass 3 of 4 on head d0f6d78.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d0f6d78e46

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

): Promise<WorkspaceToolError> {
const invalid = new WorkspaceToolError('Invalid workspace path', 'INVALID_PATH');
const segments = relative(root, candidate).split(sep).filter(Boolean);
const ancestors: DirectoryIdentity[] = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the workspace root in ancestry revalidation

For a pathname-only workspace, the root itself is never added to ancestors, so replacing the registered root with an escaping symlink before a request still leaks outside-path existence: a missing outside child reaches the first lstat catch with an empty ancestry list and returns NOT_FOUND, while an existing child is rejected as INVALID_PATH. Fresh evidence beyond the earlier ancestry thread is that the added recheck starts below root; capture and revalidate the root identity as well before returning NOT_FOUND.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4321ba9. The classifier now runs lstat on the root first and records its device and inode as the first ancestor. A root that is a symlink or not a directory is INVALID_PATH, and the root is rechecked with the other ancestors before NOT_FOUND is returned.

Comment thread packages/code/src/workspace.ts Outdated
Comment on lines +509 to +510
} catch (cleanupError) {
if (!isMissingEntry(cleanupError)) removed = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not treat cleanup ENOENT as durable removal

When another process removes a newly created directory without syncing its parent, rmdir returns ENOENT here and cleanup is considered successful even though the write previously fsynced that directory's creation; a crash can therefore restore it while the rejection still claims no mutation may have committed. This catch also suppresses ENOENT thrown by syncWorkspaceDirectory after a successful rmdir. Fresh evidence beyond the earlier cleanup fix is that both cases bypass durable confirmation, so they should sync/revalidate the containing directory or mark the mutation uncertain.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obsolete as of 4321ba9. Created directories are no longer rolled back, so cleanup and its ENOENT handling are gone. They are a committed side effect, like mkdir -p. Once one exists, any later failure of the write is reported with mutationMayHaveCommitted: true and the worker quarantines. A cancellation that arrives after creation no longer interrupts the write, so the common case still finishes normally.

if (entry == null) {
throwIfAborted(signal);
try {
await mkdir(current, 0o777);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate the held parent at the mkdir operation

If a background process renames the parent directory outside the held workspace after WorkspaceRootAccess.parent() checks its ancestry but before its mkdirat, this call creates the child through the still-open descriptor in that now-external directory. The later pathname-based realpath only detects the problem after the mutation, and cleanup cannot reach the child through the old pathname. Fresh evidence beyond the earlier parent-creation thread is that the held descriptor itself can be moved after its one-time ancestry check; creation must remain atomically rooted or retain enough descriptor state to remove and quarantine any externally installed child.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partly addressed in 4321ba9; the remaining part is a property of the shared held-root primitive, not something this PR adds.

The window you describe applies to every descriptor-anchored mutation in WorkspaceRootAccess: parent() checks ancestry once and then acts through the held fd. The existing write path already relies on it. It stages .librechat-code-*.tmp with openat through the same held parent and installs it with renameat, so a parent moved outside the root between the ancestry check and the call places a file outside today. mkdirat here has the same exposure, and only for an empty directory. Closing the window generally would mean re-proving ancestry after each *at call inside root-access.ts, which belongs in its own change covering every caller.

What this PR does change is what happens when the escape is detected. The realpath check after mkdir throws INVALID_PATH, and because created directories are now a committed side effect, that failure is reported with mutationMayHaveCommitted: true. The worker therefore quarantines instead of reporting an atomic rejection. That is the "quarantine any externally installed child" outcome, without trying to remove anything through a path that no longer reaches it.

Comment thread packages/code/src/workspace.ts Outdated
let removed = true;
for (let index = created.length - 1; index >= 0; index -= 1) {
try {
await rmdir(created[index]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify directory identity before cleanup removal

Cleanup records only path strings, so if another process removes a directory created by this write and recreates an empty directory at the same path, this rmdir deletes the other process's replacement and then reports the rejected write as atomic. Fresh evidence beyond the earlier nonempty-cleanup discussion is that rmdir protects concurrent content but not an empty replacement inode; retain the created directory identity and avoid removing a path that no longer names the write-owned directory.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obsolete as of 4321ba9. Cleanup was removed: created directories are never deleted, so a same-path empty replacement created by another process is never touched. A failure after creation is reported as a possible mutation instead.

Comment thread packages/code/src/workspace.ts Outdated
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
}
entry = await lstat(current);
if (createdHere) await syncWorkspaceDirectory(dirname(current));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Sync a parent directory after a raced EEXIST

When lstat observes a missing ancestor but mkdir then returns EEXIST because another process just created it, createdHere remains false and this conditional skips syncing the new entry into its parent. The write can subsequently sync descendants and the final file yet still lose the entire raced ancestor after an acknowledged write if the machine crashes. Fresh evidence beyond the earlier parent-sync thread is that its fix covers only this process's successful mkdir; sync the containing directory before relying on the directory accepted after EEXIST.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4321ba9. Whenever lstat found the ancestor missing, its parent is now synced after the directory is confirmed, whether this write's mkdir created it or a racing mkdir did (EEXIST). So the write never acknowledges a file under an entry that was not synced.

Directories a write creates are no longer rolled back. Once one exists the
write finishes despite cancellation, and any later failure is reported as a
possible mutation. A raced EEXIST parent is synced too, and the NOT_FOUND
ancestry recheck now includes the workspace root.
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

This is the final review for this PR (pass 4 of 4) of head 4321ba9. Please flag only blocking issues: correctness, security, or data loss.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 4321ba9318

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila merged commit d6ac1b6 into main Oct 3, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant