Skip to content

fix(banner): include local HEAD in the update-check cache key - #72013

Closed
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:port/banner-head-cache-key
Closed

fix(banner): include local HEAD in the update-check cache key#72013
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:port/banner-head-cache-key

Conversation

@Kyzcreig

Copy link
Copy Markdown
Contributor

Problem

check_for_updates() caches the "commits behind" count for 6 hours, keyed on {rev, ver}:

# hermes_cli/banner.py:296-300
now - cached.get("ts", 0) < _UPDATE_CHECK_CACHE_SECONDS
and cached.get("rev") == embedded_rev
and cached.get("ver") == VERSION

Neither component changes when a user updates in place:

  • HERMES_REVISION is nix-only, so embedded_rev is None for source installs.
  • VERSION only moves on a release bump, not on a git pull.

So an in-place git pull / rebase moves HEAD while the cache key stays constant. The stale count then survives the full TTL, and hermes --version keeps printing e.g. "182 commits behind" immediately after a successful update that brought the checkout up to date. hermes update calls _invalidate_update_cache() explicitly and is unaffected; this is the manual-git path.

Fix

Add the active checkout's HEAD SHA to the cache key, so an in-place update self-invalidates.

Two implementation details that matter:

  • Computed after the docker short-circuit. Docker images ship without .git (see .dockerignore) and already return None earlier in the function; placing the read below that guard means containers never shell out to git for this.
  • Reuses the existing helpers. _resolve_repo_dir() (banner.py:333) and _git_stdout() (banner.py:157) already exist on main and already handle the not-a-git-install and the Windows-encoding cases, so _local_head_sha() is four lines rather than a second subprocess.run wrapper with its own timeout/encoding policy.

No new config key, no new env var.

Test changes

Two existing tests asserted the old call sequence and had to move with it. Both were tightened rather than loosened:

  • test_check_for_updates_uses_cache previously asserted mock_run.assert_not_called() — "no git at all". A git rev-parse HEAD now legitimately runs on the fast path. Rather than relax the assertion to a call count, the test now uses a side_effect that returns the HEAD and raises AssertionError on any other git invocation. That is a strictly stronger claim than the original: it proves no fetch and no rev-list happen, and it names which call is allowed.
  • test_check_for_updates_expired_cache counted 4 subprocess calls (origin probe + shallow probe + fetch + rev-list); it is now 5 with the cache-key HEAD read. The comment enumerating them was updated to match.

New: test_check_for_updates_invalidates_when_head_moved pins the actual bug — fresh timestamp, same VERSION, different HEAD must recompute and must rewrite the cache with the new head. It fails on unpatched main.

Verification

# RED — implementation reverted, tests kept
tests/hermes_cli/test_update_check.py → 2 failed, 13 passed
  FAILED test_check_for_updates_invalidates_when_head_moved
  FAILED test_check_for_updates_expired_cache

# GREEN — with the fix
tests/hermes_cli/test_update_check.py → 15 passed

Neighbor suites on this branch:

tests/hermes_cli/test_banner.py
tests/hermes_cli/test_banner_git_state.py
tests/hermes_cli/test_update_check.py
tests/hermes_cli/test_update_stale_dashboard.py
tests/hermes_cli/test_cmd_update_docker.py
→ 68 passed, 0 failed

test_cmd_update_docker.py is included deliberately, since the placement of the HEAD read relative to the docker short-circuit is the one behavioral risk in the diff.

Supersedes

This replaces #40157 (same fix, same premise). That branch is 3,406 commits behind and hard-conflicts on apply. The premise was re-verified on current main before writing this: the cache key at banner.py:298-299 is still {rev, ver} only, and the write at :324 still omits head, so the bug is unfixed upstream.

The re-port is also smaller than the original: #40157 had to introduce its own subprocess.run block because _resolve_repo_dir()/_git_stdout() did not exist yet at the time. They do now, so this version delegates to them.

The review on #40157 noted one gap — no test for the actual HEAD-mismatch transition. test_check_for_updates_invalidates_when_head_moved is that test.

Happy to close #40157 in favor of this once maintainers confirm the approach.

An in-place `git pull` / rebase moves HEAD without changing VERSION or
HERMES_REVISION -- and for a source install tracking a fork BOTH are None, so
the existing cache key {rev, ver} is constant across the update. A stale
"commits behind" count therefore survived the full 6-hour TTL: `hermes
--version` kept printing e.g. "182 commits behind" immediately after a
successful pull that brought the checkout up to date.

Add the active checkout's HEAD SHA to the cache key so an in-place update
self-invalidates the cached count. The read is computed AFTER the docker
short-circuit, so containers (which ship without .git) never shell out to git
here, and it reuses the existing _resolve_repo_dir() + _git_stdout() helpers
rather than adding a second subprocess wrapper. `hermes update`'s explicit
_invalidate_update_cache() still works; this covers manual git updates too.

Test changes: two existing tests observed the old call sequence.
test_check_for_updates_uses_cache now asserts the stronger property (only the
cheap HEAD read fires; a fetch/rev-list raises) instead of "no git at all", and
test_check_for_updates_expired_cache's count moves 4 -> 5 for the added HEAD
read. New test test_check_for_updates_invalidates_when_head_moved pins the
actual bug: fresh timestamp + same version + different HEAD must recompute.

Verification:
- New test + the expired-cache count test fail on unpatched main (RED),
  pass with the fix.
- tests/hermes_cli/test_banner.py, test_banner_git_state.py,
  test_update_check.py, test_update_stale_dashboard.py,
  test_cmd_update_docker.py: 68 passed, 0 failed.
@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have duplicate This issue or pull request already exists labels Jul 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #20653: both make the update-check cache depend on the local Git HEAD so manual source updates invalidate a stale commits-behind banner.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused cache-correctness fix. The premise remains valid on current main: hermes_cli/banner.py:311-316 returns a fresh cache before the local checkout is resolved at hermes_cli/banner.py:320-335.

Problems

  • The added _local_head_sha() docstring says both VERSION and HERMES_REVISION are None for source installs, but VERSION is defined at hermes_cli/banner.py:68. The code is correct; this is documentation-only. The new regression-test docstring repeats the same statement.

Suggested changes

  • Say that VERSION remains unchanged across an in-place pull and that HERMES_REVISION is normally unset for source installs.
  • GitHub currently reports this branch as conflicted. Main commit 6b81590c55bcc9c1001a33b51b528784f96c6a07 pruned the surrounding update-cache tests, so salvage should retain the new HEAD-mismatch regression in the current test file rather than restore the removed tests.

Automated hermes-sweeper review.

Comment thread hermes_cli/banner.py
def _local_head_sha() -> Optional[str]:
"""Return the active checkout's HEAD SHA, or None for non-git installs.

Used as part of the update-check cache key so an in-place ``git pull`` /

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERSION is not None for source installs (hermes_cli/banner.py:68 defines it as a release string); it simply does not change after a manual pull. Please reword this to distinguish unchanged VERSION from normally-unset HERMES_REVISION.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/install-update Installer, updater, packaging, wheels, doctor labels Jul 30, 2026
@Kyzcreig

Copy link
Copy Markdown
Contributor Author

Superseded by #99294, which carries this change rebased onto current main, unions it with the inconclusive-result guard from #82166 that landed at the same write site, and adds a fresh-cache-with-old-HEAD transition test. The new PR also credits #20653, which diagnosed this bug first. This one has gone stale against main.

@Kyzcreig Kyzcreig closed this Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/install-update Installer, updater, packaging, wheels, doctor comp/cli CLI entry point, hermes_cli/, setup wizard duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants