Skip to content

fix: key Codex state on the target basename, not its whole path (Windows MAX_PATH) - #7

Open
mkampus wants to merge 1 commit into
PiLastDigit:masterfrom
mkampus:fix-state-key-maxpath
Open

fix: key Codex state on the target basename, not its whole path (Windows MAX_PATH)#7
mkampus wants to merge 1 commit into
PiLastDigit:masterfrom
mkampus:fix-state-key-maxpath

Conversation

@mkampus

@mkampus mkampus commented Aug 21, 2026

Copy link
Copy Markdown

The problem

target_key folds 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-char MAX_PATH limit.

The failure is silent and easy to misattribute. Past the limit:

  • bash still writes the events file — MSYS handles long paths transparently, so .events.ndjson lands on disk correctly;
  • jq cannot read it — it's 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 demonstrably present;
  • start.sh sends jq's stderr to /dev/null, so THREAD_ID comes back empty and .thread is never written;
  • every later resume.sh exits with no review session for <target> — run start.sh first, leaving the previous .review.txt in 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.ndjson in codex-plan-review/state/:

checkout before after
normal 246 (253 with the .stderr sibling) 176
inside a git worktree 320 — over the limit, silently broken 211

Worth 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 $HOME or 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.

  • Uniqueness is unchanged. The cksum is 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.
  • The basename is capped at 80 chars, so one very long target filename cannot reintroduce the problem on its own.
  • Degenerate targets (/, trailing slashes, .) fall back to a placeholder rather than producing an empty key.

Single function changed; thread_file / review_file / events_file / plan_key all route through it, so nothing else needed touching.

⚠️ Compatibility 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. (Note that v2.7.2's own cksum addition already had this property.)

Verification

Reproduced the failure before fixing it, rather than reasoning about it: at 320 chars jq returned Could not open file … No such file or directory for a file sitting on disk at 48 bytes. After the change, the identical probe read the thread id back.

Then, on the branch:

  • key shape — no path separators, 72 chars for a realistic plan
  • uniqueness — same basename in two directories → different keys, matching readable half
  • determinism — stable across calls; relative and absolute paths resolve identically
  • non-path targets — feature/branch-x, feature__branch-x, main..HEAD, v1.2.3; no / vs __ collision
  • degenerate targets — /, ., trailing slash all produce sane keys
  • basename cap — 300-char filename → 91-char key
  • end-to-end — jq reads the thread id back from the events file

11/11 passing. Also exercised through the shipped show.sh / reset.sh downstream, 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 .thread is missing after every start.sh" and re-extracting the id with grep. Fixing the key retires that workaround.

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>
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