Cache the PyPI version check and resolve version from dist-info - #654
Cache the PyPI version check and resolve version from dist-info#654j-sperling wants to merge 4 commits into
Conversation
The SessionStart hook spent two Python interpreter starts and a network round trip building its status line. `config list --resolved` already loads the resolved config; querying `--version` separately paid for a second interpreter start (~0.3s warm, several seconds cold) purely to render a display string, and the PyPI lookup blocked every session start on a request whose answer changes at most daily. Resolve the installed version from its dist-info directory name instead, falling back to the CLI where no dist-info is discoverable (uvx, editable installs), and cache the PyPI response for 24h. Both plugins carried the same two patterns, so both are updated. Measured on a warm macOS install: version resolution drops 0.33s -> 0.01s and the PyPI request goes from every start to once a day. Median hook time falls ~1.21s -> ~0.77s, and the tail loses the 2s curl timeout.
The first pass only cached successful lookups, so a machine that cannot reach PyPI re-paid the full curl timeout on every session start — the exact cost the cache was meant to remove. Treat a cache file younger than a day as authoritative even when empty: an empty file records a failed lookup. An unreachable index now costs one timeout per day instead of one per session, and no update hint is claimed while the latest version is unknown.
|
Thanks — the overall direction looks good, and the focused hook tests pass locally. I found one portability issue that blocks the main version-resolution optimization on a common macOS install layout.
readlink -f "$bin"The default BSD that command fails and I reproduced this with a test that creates a symlinked Could you switch this to a portable symlink-resolution approach and add a regression test covering a symlinked entry point when |
BSD readlink on macOS rejects -f, so _installed_version_from_dist_info fell back to the symlink path on uv tool installs, never found the dist-info tree, and re-paid the second CLI start this branch removes. Walk the symlink chain with plain readlink instead and canonicalize the directory with pwd -P. The same helper replaces the readlink/python3 chain in the session-start upgrade-command detection of both hook copies.
|
Good catch — fixed in a6813d0. Regression coverage matches your repro: a parametrized test sources each plugin's |
Summary
dist-infodirectory name instead of a separatememsearch --versioncall, which spawned a second Python interpreter purely to render the status line. Falls back to the CLI when no dist-info is discoverable (uvx, editable installs).plugins/claude-codeandplugins/codexcarried the same two patterns, so both are updated; the two helpers are added to eachhooks/common.sh.Why
SessionStarthooks block the session from starting, so their cost is paid on every launch. Measured across 50 recent sessions on one machine, this hook was the largest single contributor of the several that run:session-start.shBreaking the median down against a warm macOS install:
config list --resolved --json-outputmemsearch --versioncurl pypi.org/pypi/memsearch/json0.32 + 0.33 + 0.12 ≈ 0.77s, which accounts for the observed 1.21s median once bash startup and
stop_watchare included. The tail is the curl hitting its 2s ceiling plus cold interpreter starts.After the change, version resolution measured 0.33s → 0.01s returning the same value (verified against the CLI), and PyPI is contacted once per day rather than once per session. That puts the expected median near 0.77s and removes the 2s timeout from the tail on cached runs. The remaining
config liststart is inherent — the hook genuinely needs the resolved config.This only moves blocking work. The background index spawned further down was already non-blocking and is untouched, as is the update hint's content and format.
Test plan
pytest tests/test_claude_hooks.py— 16 passedpytest— 290 passed, 7 skippedtest_claude_session_start_reads_version_from_dist_info— status version comes from dist-info and--versionis never invokedtest_claude_session_start_falls_back_to_cli_version_without_dist_info— layouts without a discoverable dist-info still report a version via the CLItest_claude_session_start_caches_pypi_lookup— curl runs once across two starts, the cache file is written, and the update hint survives the cache round triptest_claude_session_start_reads_version_from_dist_infobash -nclean on all four modified shell files