MUST-FIX:
- tests/test_2735_open_in_vscode.py: bump expected open_in_vscode locale
counter from 10 to 11 (Turkish locale added in #2772). The bump fell
out of an in-rebase test edit but never got committed; tagging without
this would have shipped a failing test in the release commit.
SHOULD-FIX inline:
- api/updates.py: case-D drift in _select_apply_compare_ref. The original
#2855 fix used latest_tag in the past-tag predicate; the check side
uses current_tag (HEAD's nearest reachable tag) plus a 'behind == 0'
gate. They drift when HEAD is on an OLDER release tag with commits on
top AND a NEWER tag exists ('case D'): check correctly suggests
advancing to the newer tag, but apply fell through to origin/<branch>.
Mirror the check-side predicate exactly. Adds regression test
test_select_apply_compare_ref_case_d_older_tag_with_commits_and_newer_tag_exists.
- static/messages.js: post-await race guard in _restoreSettledSession.
stream_end without preceding 'done' enters the settlement path, awaits
/api/session, then sets _streamFinalized=true. If a late 'done' event
arrives during that await, it sees _streamFinalized still false and
double-runs the finalize. The guard returns early when done won the
race, avoiding double renderMessages() + double notification.
- server.py: CORS preflight Access-Control-Allow-Methods now includes PUT.
#2776 wired PUT into the router for /api/mcp/servers/{name} but didn't
update the OPTIONS response. Same-origin only in practice, but cosmetic
completeness for CORS-aware deployments.
Opus advisor verdict: all 5 risk areas reviewed, 1 MUST-FIX + 3 SHOULD-FIX
all addressed inline. Net: +69/-9, no new architecture, no behavior risk.
Fixes#2846. After PR #2758 (the #2653 fix) the update check correctly
falls through to the branch comparison when HEAD has moved past the
latest `v*` tag — so the banner reports the real commit count against
`origin/<branch>`. But `_select_apply_compare_ref` was never updated to
mirror that decision: as long as any `v*` tag exists, it returns
`tags[0]`, even when HEAD is far past it.
Result for everyone running hermes-agent past `v2026.5.16` (i.e. anyone
on agent master between tagged releases):
1. Banner: `Agent (origin/main): 254 updates available` ← correct
2. User clicks Update Now
3. `_select_apply_compare_ref` picks `v2026.5.16` because tags exist
4. `git pull --ff-only origin v2026.5.16` — no-op (HEAD is already past it)
5. `_schedule_restart()` fires anyway, server bounces
6. Next check still reports 254 behind — banner reappears unchanged
`apply_force_update` had the same bug, except worse: `git reset --hard
v2026.5.16` would have actively rewound the user's checkout 254 commits.
The root cause is the same bug class as #2653 — two parallel paths
(`_check_repo_release` and `_select_apply_compare_ref`) that should make
the same decision but didn't. Pre-fix, the "is HEAD past the latest
tag?" predicate lived inline inside `_check_repo_release` only.
Fix
---
Extract `_head_is_past_latest_tag(path, current_tag)` and have both
paths consult it. When HEAD is past the latest tag:
- check path: release check returns None → branch check runs (#2653,
unchanged behaviour, just refactored)
- apply path: falls through to upstream / `origin/<branch>`, never the
stale tag (#2846, new behaviour)
Tests
-----
- `test_select_apply_compare_ref_uses_tag_when_head_is_on_tag` —
unchanged behaviour pinned: HEAD exactly on tag → advance to tag.
- `test_select_apply_compare_ref_falls_through_when_head_is_past_tag` —
the #2846 repro: HEAD = v2026.5.16 + 608 commits → advance to
`origin/main`, not the tag.
- `test_select_apply_compare_ref_no_tags_uses_upstream` — unchanged.
- `test_select_apply_compare_ref_no_tags_no_upstream_uses_default_branch`
— unchanged.
- `test_check_and_apply_paths_agree_when_head_is_past_tag` — symmetry
test, ensures the two paths can't drift apart again.
All 21 tests in `tests/test_updates.py` pass locally (16 existing + 5
new).
Refs #2846, #2653.
When current_tag == latest_tag, _check_repo_release returned behind=0
and reported 'Up to date' even if master had moved hundreds of commits
past the tag. This was visible as Agent: v2026.5.16-593-gedb2d9105
alongside a green 'Up to date' pill in Settings.
Run 'git describe --tags --always' after computing behind==0. If the
output includes a -N-gSHA suffix the tag is not at HEAD; return None so
_check_repo_branch runs and counts the real commit gap via rev-list.
When HEAD is exactly on the latest tag the new branch is never taken and
behaviour is unchanged.
Fixes#2653.
Without --force, git fetch origin --tags refuses to overwrite divergent
local tags and returns 'would clobber existing tag', jamming the entire
WebUI update path indefinitely. The WebUI is a release-tracking consumer
that never pushes tags, so it should always defer to whatever the remote
says a release tag points to. Add --force to all three fetch-tag call
sites:
- _check_repo (the 'Check now' button + periodic check)
- apply_force_update (force-reset to remote HEAD)
- apply_update (stash + pull --ff-only)
Tests:
- Updated 3 existing tests in test_updates.py whose fake_git mocks
asserted the exact ['fetch', 'origin', '--tags'] args list.
- Updated 1 existing test in test_update_banner_fixes.py that asserted
the same shape for apply_update.
- Added 4 new regression tests:
- test_check_repo_fetches_tags_with_force
- test_apply_force_update_fetches_tags_with_force
- test_apply_update_fetches_tags_with_force
- test_check_repo_recovers_from_remote_retag (end-to-end,
proves the bare --tags fetch shape is no longer used)
Closes#2756.
Rebased and enhanced version of PR #287 by @ccqqlo:
- _run_git() now returns stderr on failure instead of empty string,
so the UI can surface actionable git error messages
- Added _split_remote_ref() to split tracking refs like origin/master
into separate remote + branch args for git pull
- Ignore untracked files in stash decision (--untracked-files=no) to
prevent misleading stash-pop failures
- Fail early with clear message on unresolved merge conflicts
- 4 unit tests covering stderr, stdout fallback, exit code, and ref splitting
Based on work by @ccqqlo in PR #287.
Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>