fix(update-cache): invalidate cache on git HEAD movement, surface shallow -1 - #54874
fix(update-cache): invalidate cache on git HEAD movement, surface shallow -1#54874lynchest wants to merge 2 commits into
Conversation
…llow -1
The 6h update-check cache was invalidated only on (a) TTL expiry, (b)
HERMES_REVISION env change, or (c) installed VERSION change. For git
checkouts this meant that as long as VERSION stayed pinned, the cache
returned a stale 'behind: 0' for up to 6h after upstream gained new
commits — even though `git rev-parse origin/main` would have shown
the truth in microseconds. The TUI banner, CLI `hermes version`, and
the dashboard's polled /api/hermes/update/check all read this cache,
so all three surfaces could lie about being 'Up to date' for hours.
Two changes:
1. Persist upstream_head and local_head in the cache payload; on a hit,
re-read them and reject the cache if either has moved. Sub-ms cost
(no network), honest answer.
2. Lower TTL from 6h to 1h as a backstop for install paths that don't
have git HEAD to compare (PyPI, nix). Power users running `hermes`
all day will see one fetch per hour instead of one per six.
Also fix a related bug in `hermes version`: when the local checkout
is shallow (installer clone --depth 1) the behind-count function
returns UPDATE_AVAILABLE_NO_COUNT (-1). The old `if behind and
behind > 0` check swallowed -1 silently, so even an immediate fresh
check would show no message at all — making the shallow case
indistinguishable from 'nothing to update'. Split the three cases
(none / up-to-date / behind) and surface the shallow case as a
plain 'Update available' line.
Repro on the v0.17.0 release:
- `git clone --depth 1 .../hermes-agent`, pip install -e .
- wait 30s for cache to be written
- `git fetch origin` (or run `hermes update`)
- `hermes version` reports 'Up to date' for up to 6h, hiding
the new commits.
tonydwb
left a comment
There was a problem hiding this comment.
LGTM! Well-scoped cache invalidation fix — two-layer invalidation (time + content) with upstream/local HEAD tracking is solid. Clean separation of concerns.
|
cc @teknium1 — opening this for review. The cache was returning |
Duplicate of #9670 — same core mechanism (invalidate the update-check cache when the local/upstream git HEAD moves). #9670 is the earliest still-open PR in this chain (also #20653, #40157 open; #21675, #40985 closed). This PR adds useful extras (surfacing shallow |
|
Closing my duplicate flag — keeping this PR open after maintainer feedback. #9670 has the same core mechanism, agreed. But this PR adds three things the others don't, and I'd rather they ship than die in a closed PR:
Offer to maintainer: happy to fold all three into #9670's branch as a force-push, or send as a 3-commit stack on top, or leave this PR open as-is and let you cherry-pick. Your call. If you just want the smallest correct fix and the extras can wait, also fine — just leave a comment and I'll stop nudging. Not closing as duplicate. The extras have user-visible impact and I haven't seen any of the other three PRs carry them. |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: LGTM — clean update cache invalidation on git HEAD movement.
Well-documented changes with good test coverage. The implementation correctly detects upstream and local HEAD changes for cache invalidation.
Reviewed by Hermes Agent
teknium1
left a comment
There was a problem hiding this comment.
Thanks for addressing a real stale-cache path. Current main still returns a fresh cache entry without checking Git heads (hermes_cli/banner.py:330-344) and suppresses the shallow-clone sentinel in hermes version (hermes_cli/main.py:4421-4429).
Problems
- This diff changes the cache contract and CLI rendering but adds no tests. Please cover local/upstream head movement and legacy cache payloads in
tests/hermes_cli/test_update_check.py, plusNone/0/ positive /UPDATE_AVAILABLE_NO_COUNToutput cases for_print_version_info(). .gitignoreincludes unrelatedskills/directory patterns. Those paths are repository source paths, so this would conceal unrelated changes; please remove them.
Suggested changes
- Keep the upstream-head work separate from #9670's local-head-only approach, as described in the discussion, but preserve the existing shallow-check contract (
hermes_cli/banner.py:228-239) in regression tests.
Automated hermes-sweeper review.
| # pr-infographic-workflow reference (storage rule + lapse #8 / #COMMIT-1). | ||
| infographic/ | ||
|
|
||
| # User-installed skills (sourced from ~/.hermes/skills/ or skills.sh, not upstream) |
There was a problem hiding this comment.
This update-cache PR should not add unrelated ignore rules for repository skills/ paths. Please remove these entries so legitimate skill changes remain visible to Git.
…dation and _print_version_info tests
Addresses review feedback:
1. Remove the unrelated directory patterns from
that concealed repository source paths.
2. Add tests for the new head-movement cache guard:
- Upstream HEAD movement invalidates cache
- Local HEAD movement invalidates cache
- Stable heads (no movement) serve from cache
- Legacy cache payloads (no upstream_head/local_head keys)
are treated as cache misses
3. Add tests for output:
- None → no update message
- 0 → 'Up to date'
- positive count → 'Update available: N commits behind'
- UPDATE_AVAILABLE_NO_COUNT → 'Update available (shallow checkout, ...)'
4. Update pre-existing tests to account for the new rev-parse probes
on cache-hit paths.
|
@teknium1 review feedback addressed in 42b3ad1. Changes in the latest commit:
23/23 tests passing. |
Problem
The update-check cache in
hermes_cli/banner.pywas only invalidated on:HERMES_REVISIONenv change (nix)VERSIONchange (pip upgrade)For a git checkout, this meant that as long as
VERSIONstayed pinned (v0.17.0 → v0.17.0 across a hotfix stream, or just same-version upstream commits), the cache returned a stale "behind: 0" for up to 6 hours after upstream gained new commits.git rev-parse origin/mainwould have shown the truth in microseconds, but the cache never asked.Three surfaces read this cache and so all three could lie:
hermes versionCLI output/api/hermes/update/check(the System page "Check now" button bypasses the cache, so the bug was less visible there, but the auto-poll still hit it)Repro on v0.17.0
Reproduced on 2026-06-29 against v0.17.0 with 146 commits of drift.
Fix
Persist
upstream_headandlocal_headin the cache payload. On a cache hit, re-read both withgit rev-parse(sub-ms, no network) and reject the cache if either has moved. The TTL becomes a backstop rather than the only invalidation trigger.Lower the TTL from 6h to 1h for non-git install paths (PyPI, nix) that don't have HEAD to compare. Power users running
hermesall day see one fetch per hour instead of one per six — negligible.Split the three cases in
hermes version's output:None(no check possible, e.g. docker image) → silent0(up to date) → "Up to date"> 0or-1(behind, possibly shallow) → "Update available"The old
if behind and behind > 0check silently swallowed -1, which is what a shallow installer clone returns when it's behind — so the shallow case was indistinguishable from "nothing to update", the exact opposite of what the user needs to see.Test
Also tested live on the v0.17.0 → v0.17.0+146 stream where the cache was returning "Up to date" with 146 commits of drift.
Risk
Very low. The new code paths only:
git rev-parseon cache hits (sub-ms, no network)hermes versionfor a previously-silent caseThe TTL change is a network-cost regression of 6x for power users, which is the only trade-off worth flagging.