Skip to content

Let claude-current fall back from a native Claude Code that does not run to the next one that does, and let claude-restart-check recognise a running native in a custom CLAUDE_CURRENT_NATIVE_DIR, symlinked included - #211

Merged
brettheap merged 8 commits into
mainfrom
fix/claude-resolver-154-155
Oct 19, 2026

Conversation

@brettheap

@brettheap brettheap commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #154
Closes #155

What was wrong

#154, claude-current. native_newest chose the native file with the highest x.y.z name before anything read its --version. When that file did not run (a download cut short, a binary for another libc, a first start that hangs), add_candidate dropped it and no older native was tried, so a machine with a working older native read as having none. claude update was also run through that broken file.

#155, claude-restart-check. The installed versions were read from $CLAUDE_CURRENT_NATIVE_DIR, but a running process was recognised as native only when its exe path contained /claude/versions/. A session launched from a configured directory such as /custom/native/2.1.283 was never found, so no notice was printed even when a newer sibling existed.

What changed

claude-current

  • native_names (lines 427-439) lists the native version names once, highest first, with one sort -t. -k1,1nr -k2,2nr -k3,3nr (POSIX; no per-comparison vercmp process).
  • add_native (lines 441-469) replaces native_newest. It reads native versions from the highest down through the existing add_candidate and keeps the first whose --version answers. A working newest file still costs one read. native_broken (line 414) remembers each file that failed, so the up to four collect passes of a launch do not pay a hung file's bound again each time, and it is forgotten wherever something else may have written the file: after a wait for the update lock (take_lock sets lock_slept, lines 570 and 652; the reset is at line 780, before the post-lock collect), because the launch that held the lock may have finished a native this one read half written; and after update_native (line 796, before the collect that follows), because claude update rewrites files, the failed one among them. Without those two resets a repaired native was skipped and the launch could go to npm and refuse (exit 2) where it had verified. native_path (line 413) now names the native that runs, so claude update runs through that one; with no native that runs, the update goes straight to the npm install it would have fallen to anyway.
  • collect (line 471) calls it. Header step 2 (lines 28-32) says so. Bash 3.2 safe: a newline-delimited string, no associative array.

claude-restart-check

  • native_dir and native_real (lines 204-214): the native versions directory spelled exactly as claude-current spells it ($CLAUDE_CURRENT_NATIVE_DIR, else $HOME/.local/share/claude/versions, nothing when both are unset), and its canonical path by cd -P and pwd -P, which is the path /proc/<pid>/exe reports. That covers a symlinked configured directory and also a symlinked default (~/.local/share/claude linked to /x/persist/cl-native, which the kernel reports as /x/persist/cl-native/versions/<x.y.z>). No readlink -f, so it stays bash 3.2 and BSD safe. It runs after the existing no-/proc exit.
  • is_native_binary (lines 216-223) accepts */claude/versions/* as before, or <x.y.z> directly in the canonical directory. It serves both is_claude_binary (line 225) and the installed-version branch (line 331), which formerly each spelled the */claude/versions/* test; that branch's directory read (line 344) uses the same native_dir. The (deleted) replacement check is untouched.

docs/README-claude-current.md: candidate 1, the update paragraph and the claude-restart-check walk paragraph say the above. No line of README.md was added.

Tests

tests/test_claude_current.py (10 new functions, 13 cases): a broken newest native falls back, for each of four failures (exits non-zero, silent, garbled, hung and bounded by CLAUDE_CURRENT_VERSION_TIMEOUT); the fall back goes down every native; a working newest native is the only one probed; a native that failed is probed once per launch, not once per read; a native that claude update repaired in place is read again (the npm install fails, so reading it again is the only way to a launch); a native another launch finished while this one waited for the lock is read again and the answer is "updated by another launch"; claude update runs through the native that runs; every native broken still gives exit 1 with nothing else installed, and the user prefix install still recovers it; a broken newest native behind a stale user copy is still the exit 2 refusal.

tests/test_claude_restart_check.py (11 new functions, 14 cases), on fake process tables: a custom directory session behind, current (asked twice, so its silence is a recognised session and not a missed one) and replaced; a trailing slash, a .. spelling and a symlinked directory (the exe path is the canonical one, as the kernel reports it); a symlinked default directory with no variable set; the default directory still found when another is set; and the narrow negatives (a non-version name, a nested path, another directory, a missing directory, no /proc).

Verification.

flock -w 7200 ${TMPDIR:-/tmp}/openrepotools-pytest.lock python3 -m pytest tests/test_claude_current.py tests/test_claude_restart_check.py -q -p no:cacheprovider
  • CI, the suite of record, for this head 055f19f: run 39072812551, all green. The Linux tests job ran the merge of 055f19f into aa14de3 (main when the run started): 1471 passed, 9 skipped in 2981.90s (0:49:41). tests-no-submodule and guard-launch-mode also passed.
  • The update-repair regression (a failure remembered for the run was skipped after claude update repaired the file in place, or after another launch finished it during the lock wait, so the launch could go to npm and refuse): proved red then green under the shared lock.
    • Against a1b9f91's claude-current, the two new tests fail (2 failed, 100 deselected): test_a_native_the_update_repaired_in_place_is_read_again exits 2 with REFUSED: npm publishes claude 2.1.284 and the newest installed is 2.1.283 ..., and test_a_native_another_launch_finished_while_this_one_waited_is_read_again answers the user prefix install instead of the finished native.
    • On this head, tests/test_claude_current.py in full: 102 passed in 78.30s.
    • Reproduced by hand against the real script too, with fake natives and a failing npm install: a1b9f91 exits 2; this head exits 0, status=verified, at the repaired native 2.1.284.
  • At a1b9f91, both scoped files: 137 passed in 64.88s. The new tests of the earlier rounds against f87fd5a's two scripts: 13 fail and 11 pass (the 11 are the "still as today" guards: a working newest native probed alone, every native broken still refusing, the narrow negatives, the default directory, no /proc).
  • The seven bash-hygiene rules test_claude_current.py applies, called directly on both scripts at a1b9f91: 0 failures; /bin/bash -n on both at this head: clean.

Out of scope: #149.

🤖 Generated with Claude Code

Summary by Sourcery

Improve native Claude discovery and restart detection so broken installations no longer hide runnable versions and native sessions in custom directories are correctly identified.

Bug Fixes:

  • Make claude-current fall back to the newest older native installation when a newer native cannot provide a valid version.
  • Ensure claude-restart-check recognises running native Claude processes installed in configured or symlinked native directories.

Enhancements:

  • Reuse native candidate detection for update operations and refresh failed-native state after updates or lock waits so repaired installations are reconsidered.
  • Canonicalize native directories while preserving compatibility with Bash 3.2 and continue recognizing the default native directory independently of custom configuration.

Documentation:

  • Document native fallback behavior and native process detection in custom and symlinked directories.

Tests:

  • Add coverage for failed, hung, repaired, and lock-updated native installations, update selection, and custom or symlinked native process paths.

brettheap and others added 2 commits October 11, 2026 16:56
…155 written (not yet run); left: the fixes in claude-current and claude-restart-check, the docs lines, the test run, the PR

Tests only, no fix yet, so they are expected to fail on main where the fixes matter.
tests/test_claude_current.py: a broken newest native falls back to the next that
runs (exits, silent, garbled, hung), the fall back goes down every native, only
the newest working native is probed, update runs through the native that runs,
and every-native-broken still refuses (exit 1 or 2) as today.
tests/test_claude_restart_check.py: a session from a CLAUDE_CURRENT_NATIVE_DIR
directory is recognised (current, behind, replaced), canonical comparison for a
trailing slash, a dot-dot spelling and a symlinked directory, and the narrow
negatives (a non-version name, a nested path, another directory, a missing
directory, no process table).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… recognise a running native from CLAUDE_CURRENT_NATIVE_DIR in claude-restart-check

claude-current chose the newest native file by name before reading its
--version, so a newest file that did not run was dropped with every older
native that did (#154). add_native now reads from the highest name down and
keeps the first that answers, which also makes it the native that `claude
update` runs through. A working newest file still costs one read.

claude-restart-check read installed versions from CLAUDE_CURRENT_NATIVE_DIR but
recognised a running native only under .../claude/versions/, so a session
launched from a configured directory was missed (#155). is_native_binary also
accepts <x.y.z> directly in the canonical (cd -P) path of that directory, which
is the path /proc/<pid>/exe reports, and serves both places that tested for a
native.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Sorry @brettheap, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 22 hours and 59 minutes by commenting @sourcery-ai review. Upgrade to get a review now.

@sourcery-ai

sourcery-ai Bot commented Oct 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

The pull request hardens native Claude discovery by probing descending native versions until one actually answers, ensuring updates use that runnable installation, and extends restart detection to canonicalized custom native directories while preserving existing default and npm behavior.

Sequence diagram for runnable Claude native selection

sequenceDiagram
    participant User
    participant Current as claude-current
    participant Native285 as Native 2.1.285
    participant Native284 as Native 2.1.284
    participant NPM as npm install

    User->>Current: collect
    Current->>Native285: --version
    Native285-->>Current: failure or timeout
    Current->>Native284: --version
    Native284-->>Current: version answers
    Current-->>User: select Native 2.1.284
    User->>Current: claude update
    Current->>Native284: update
    alt native update does not reach target
        Current->>NPM: install target version
    end
Loading

Flow diagram for canonical custom native restart detection

flowchart TD
    A[claude-restart-check starts] --> B{ /proc available? }
    B -- No --> C[Exit without notice]
    B -- Yes --> D[Canonicalize CLAUDE_CURRENT_NATIVE_DIR]
    D --> E[Read process exe path]
    E --> F{Native binary?}
    F -- Default versions path --> G[Use existing native recognition]
    F -- Canonical custom directory and x.y.z filename --> H[Recognize running native]
    F -- No --> I[Check npm binary recognition]
    G --> J[Compare running and installed versions]
    H --> J
    I --> J
    J --> K{Newer installed version?}
    K -- Yes --> L[Print restart notice]
    K -- No --> M[No notice]
Loading

File-Level Changes

Change Details Files
Make native candidate selection resilient to unusable newest installations and route updates through the first native that successfully responds.
  • Replace newest-only selection with descending version probing via the existing candidate validation and timeout behavior.
  • Record the runnable native path for subsequent update operations.
  • Fall back to npm installation or the existing refusal behavior when no native is runnable.
  • Add coverage for failed, silent, malformed, and hanging binaries, update routing, fallback ordering, and stale user copies.
claude-current
tests/test_claude_current.py
docs/README-claude-current.md
Recognize native Claude processes installed in a configured custom directory using canonical paths, including symlinked directory spellings.
  • Canonicalize CLAUDE_CURRENT_NATIVE_DIR with portable cd/pwd logic after the existing /proc guard.
  • Accept version files directly under the configured canonical directory while retaining the default native path pattern and npm detection.
  • Reuse the native-binary predicate for process walking and installed-version checks without changing deleted-executable handling.
  • Add coverage for custom, canonicalized, symlinked, default, invalid, missing, and no-/proc cases.
claude-restart-check
tests/test_claude_restart_check.py
docs/README-claude-current.md

Assessment against linked issues

Issue Objective Addressed Explanation
#154 Make claude-current fall back to the highest older native executable whose bounded --version probe succeeds when a newer native is broken or unresponsive. ✅
#154 Add an offline regression test covering a broken or hanging newest native and a working older native. ✅
#155 Recognize running native Claude executables in a configured CLAUDE_CURRENT_NATIVE_DIR using canonical paths, including when the configured directory is a symlink, while preserving default-directory detection and executable-replacement checks. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Brett Heap and others added 4 commits October 13, 2026 19:46
…ce per launch, and a recognised current session

Tests only, ahead of the fixes they prove: a session in a symlinked default
directory with CLAUDE_CURRENT_NATIVE_DIR unset, a native whose --version failed
being probed once and not again on each of a launch's later reads, and the
newest-custom-version test asking the same process twice so its silence is a
recognised session and not a missed one.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…robe each failed native once per launch

claude-restart-check canonicalised only a configured CLAUDE_CURRENT_NATIVE_DIR,
so with ~/.local/share/claude a symlink the kernel's real path held no
/claude/versions/ and the session was missed until the variable was set to the
default. The effective directory, spelled as claude-current spells it, is now
canonicalised whether it is configured or not, and the installed-version read
uses the same one.

claude-current read the native directory again, in full, for each of the up to
four collect passes of a launch, and a native whose --version had failed cost
its whole bound each time. Names are now ordered once with sort and each
failure is remembered for the run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd after a wait for the lock

Tests only, ahead of the fix. A native that failed is remembered for the run, which
is wrong in two places the checker found at a1b9f91: claude update can rewrite the
very file that failed, and another launch can finish a file this one read half
written while this one waited for the lock. Both must be read again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…it for the update lock

The failure memory added in a1b9f91 lasted the whole launch, so a native that
claude update repaired in place, or one another launch finished while this one
waited for the lock, was skipped by the read that follows and the launch went to
npm and could refuse (exit 2) where 149f298 verified. take_lock now records that
it slept, and the memory is dropped after that wait and after update_native.
A hung native still costs its bound once per read that can differ, not once per
read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@brettheap brettheap added ready landing gate: runs tests-macos once before the squash and removed ready landing gate: runs tests-macos once before the squash labels Oct 18, 2026
Refreshes the test-merge ref for #211, whose first parent was still aa14de3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready landing gate: runs tests-macos once before the squash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claude-restart-check: recognize processes from custom native directories claude-current: fall back from a broken newest native executable

1 participant