fix: key Codex state on the target basename, not its whole path (Windows MAX_PATH) - #7
Open
mkampus wants to merge 1 commit into
Open
fix: key Codex state on the target basename, not its whole path (Windows MAX_PATH)#7mkampus wants to merge 1 commit into
mkampus wants to merge 1 commit into
Conversation
target_key folds every directory component of the resolved target into the
state filename. That makes state paths grow with the depth of the checkout,
and on Windows they cross the 260-char MAX_PATH limit.
The failure is silent and hard to attribute. Past the limit, bash still
writes the events file (MSYS handles long paths transparently), but jq is a
native Windows binary with no long-path support and fails with
Could not open file <path>: No such file or directory
for a file that is present on disk. start.sh redirects jq's stderr to
/dev/null, so THREAD_ID comes back empty and .thread is never written. Every
later resume.sh then exits with "no review session for <target> - run
start.sh first", leaving the previous .review.txt in place and the working
tree unchanged, so it reads as "Codex did nothing" rather than as a
path-length problem.
It bites hardest when the target lives inside a git worktree, because the
worktree path is encoded into the filename on top of the real directory
path, roughly doubling it. Measured on a Windows checkout with a 60-char
plan filename: 320 chars inside a worktree (broken), and 246 in a normal
checkout - only 7 chars under the limit once the .events.ndjson.stderr
sibling is counted, so a slightly longer plan name breaks the normal case
too.
Fix: build the key from the target's basename plus the checksum. The
checksum is still computed over the full resolved path, so uniqueness is
unchanged - two same-named plans in different checkouts still get different
keys. Only the human-readable half shrinks. The basename is capped at 80
chars so one very long target filename cannot reintroduce the problem, and
degenerate targets ("/", trailing slashes, ".") fall back instead of
producing an empty key.
Same measurements after the change: 211 chars inside a worktree, 176 in a
normal checkout.
Note for release notes: this changes every existing state key, so any
in-flight Codex thread is orphaned by the upgrade. Clearing the state
directories as part of the upgrade avoids leaving unreachable files behind.
Verified by reproducing the failure first (jq refusing a 320-char path that
existed on disk), then re-running the same probe after the change; plus
uniqueness, determinism, relative-vs-absolute, non-path targets, degenerate
targets, and the basename cap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
target_keyfolds every directory component of the resolved target into the state filename. State paths therefore grow with the depth of the checkout, and on Windows they cross the 260-charMAX_PATHlimit.The failure is silent and easy to misattribute. Past the limit:
.events.ndjsonlands on disk correctly;jqcannot read it — it's a native Windows binary with no long-path support, and fails withCould not open file <path>: No such file or directoryfor a file that is demonstrably present;start.shsends jq's stderr to/dev/null, soTHREAD_IDcomes back empty and.threadis never written;resume.shexits withno review session for <target> — run start.sh first, leaving the previous.review.txtin place and the tree unchanged.The net effect reads as "Codex did nothing" rather than as a path-length bug, which is what makes it expensive — the run itself often completed and its output is sitting in the events file.
It bites hardest inside a git worktree, because the worktree path gets encoded into the filename on top of the real directory path, roughly doubling it.
Measurements
Windows checkout, 60-char plan filename,
.events.ndjsonincodex-plan-review/state/:.stderrsibling)git worktreeWorth noting the normal-checkout row: at 253 of 260 it is 7 characters from the same failure. A plan filename ~8 characters longer breaks the non-worktree case too, with the same silent signature. On a deeper
$HOMEor a longer repo path it would already be broken today.The fix
Build the key from the target's basename plus the checksum, instead of the whole path.
cksumis still computed over the full resolved path, so the same plan filename in two different checkouts still produces two different keys. Only the human-readable half shrinks./, trailing slashes,.) fall back to a placeholder rather than producing an empty key.Single function changed;
thread_file/review_file/events_file/plan_keyall route through it, so nothing else needed touching.This changes every existing state key, so any in-flight Codex thread is orphaned by the upgrade. Clearing the
state/directories as part of the upgrade avoids leaving unreachable files behind. (Note that v2.7.2's owncksumaddition already had this property.)Verification
Reproduced the failure before fixing it, rather than reasoning about it: at 320 chars
jqreturnedCould not open file … No such file or directoryfor a file sitting on disk at 48 bytes. After the change, the identical probe read the thread id back.Then, on the branch:
feature/branch-x,feature__branch-x,main..HEAD,v1.2.3; no/vs__collision/,., trailing slash all produce sane keysjqreads the thread id back from the events file11/11 passing. Also exercised through the shipped
show.sh/reset.shdownstream, which agreed on the key.Context
Found while upgrading a project from v2.6.0 to v2.7.2. This has been costing wasted resumes on Windows for weeks, under a local workaround of "in a worktree, assume
.threadis missing after everystart.sh" and re-extracting the id withgrep. Fixing the key retires that workaround.