Commit Graph
6740 Commits
Author SHA1 Message Date
nesquena-hermes f36dad7b23 test(tls): drain full stdout for the TLS-fallback assertion (flake fix)
test_tls_startup_failure_fallback_to_http read only the first 2000 bytes of
server stdout to assert 'TLS setup failed'. When the test interpreter lacks
optional deps (requests/websockets), startup emits a burst of plugin/tool
import warnings + the startup-config banner BEFORE the TLS line, pushing the
marker past the 2000-byte window -> spurious failure. Drain all
currently-available stdout (5s deadline, short-circuit on the marker) instead
of a single fixed-size read. Verified failing->passing in a fresh venv without
the optional deps. Test-only; distinct from the agent-package-isolation flake
cause already fixed in the same test.
2026-07-07 04:09:15 +00:00
nesquena-hermes b0c79fd4d3 Merge pull request #5735 from nesquena/release/stage-5567-resolver
Release: #5567 model-picker provider resolution fix
2026-07-06 20:56:50 -07:00
nesquena-hermes 4fb7b03200 docs(changelog): #5567 model-picker provider resolution fix 2026-07-07 03:52:04 +00:00
nesquena-hermesandb3nw 8d496ceaef fix(#5567): resolve model provider by matching value, not the selected option
Frontend fix for the model-picker provider-contamination vector of #5567 (the
backend HERMES_HOME vector shipped in #5577). Credit @b3nw for the root-cause trace.

_modelStateForSelect(sel, modelId) resolved the provider from sel.selectedOptions[0]
(the dropdown's CURRENTLY-selected option) instead of the option whose value matches
modelId. During a profile/tab switch or a model-list rebuild the dropdown transiently
still holds the PREVIOUS profile's default option (e.g. an ollama model), so the wrong
provider was stamped onto a model it doesn't own, persisted into the session's
model_provider, and (since _modelProviderForSend reads the stored value first) re-sent
on every turn -> 'Provider ollama...no API key' for a provider the session never used.

Fix: resolve the provider from the option whose VALUE === modelId; trust the
currently-selected option only when its value matches (preserving the user's exact
pick in the same-value/different-provider collision case). A genuinely-absent model
yields a null provider so the backend re-infers.

This is the WRITE-site fix — it stops all NEW contamination (new-session + send paths
route through _modelStateForSelect). Both Codex and Fable reviewed it SAFE.

Repairing sessions ALREADY poisoned before this shipped is deliberately deferred to a
separate follow-up: a 3-round Codex+Fable gate proved a frontend load-time repair that
reads the shared #modelSelect DOM is a cross-session race surface (it re-manufactured
the reverse-contamination it aimed to fix), and it can't reliably heal live-only-model
sessions anyway. The robust place for that repair is the backend chat-start boundary,
where session + profile identity is unambiguous. Already-poisoned sessions self-heal on
their next healthy send / manual re-pick in the meantime.

Tests: node-behavioral resolver scenarios (tab-switch race, same-value collision,
missing-model null, match-by-value, default->null) + the unrelated-selected-option
case from #5727. Partially addresses #5567 (write-vector); follow-up tracks the
already-poisoned-session repair.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-07 03:42:16 +00:00
nesquena-hermes 1d8208026c Merge pull request #5728 from nesquena/release/stage-5419
Release: auto-switch profiles for cross-profile session links (#5419)
2026-07-06 19:39:22 -07:00
Daniel Harcekandnesquena-hermes 407451977e fix: auto-switch profiles for session links (#5419)
A valid session:// deep link to a session owned by a DIFFERENT Hermes
profile used to look identical to a deleted session (404 -> frontend
self-heals away). Now GET /api/session returns a structured 409
session_profile_mismatch envelope (error/code/session_id/profile ONLY,
no transcript) ONLY when the owning profile is KNOWN, and loadSession()
catches it, switches to the owning profile, and retries once. Truly
missing/deleted or legacy None-profile sessions keep the 404 self-heal.

Gate fixes applied (Codex + Fable):
- Codex CORE: added a post-await stale-load guard after
  _switchProfileForSessionLoad so a navigation during the switch can't
  hijack the UI back to the old session.
- Fable Finding 1: only emit 409 when _session_profile is truthy;
  a None-profile (missing/legacy) session under a non-default active
  profile now keeps 404 instead of a useless profile=null 409 (which
  skipped self-heal + spun the SSE reconnect against a dead sid). Both
  detail branches. + 2 regression tests.
- Fable Finding 2: _switchProfileForSessionLoad now clears the sidebar
  skeleton + re-renders from cache on switch-POST failure (mirrors the
  #4671 canonical-switch catch), then rethrows, so a failed switch can't
  strand the sidebar on the skeleton.

Reconciled tests/test_issue1611_session_profile_filtering.py (4 tests) to
the 409 contract while preserving the no-leak boundary assertion.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-07 02:35:02 +00:00
nesquena-hermes 243c0f698d Merge pull request #5725 from nesquena/release/stage-5692
Release: Windows git subprocess console-window fix (#5692)
2026-07-06 19:25:03 -07:00
nesquena-hermes eb02c8ecd0 docs(changelog): Windows git subprocess console-window fix (#5692) 2026-07-07 02:19:40 +00:00
Jinho Seokandnesquena-hermes f106fcd1f5 fix(workspace-git): hide git subprocess console windows on Windows (#5692)
Add creationflags to _run_git() and _config_names_for_scope() so git
child processes don't accumulate hidden cmd.exe/conhost windows on Windows.

Uses a local module-level _windows_hide_flags() helper (mirrors the
api/updates.py pattern: CREATE_NO_WINDOW on win32, 0 elsewhere) rather than
importing from the OPTIONAL hermes_cli package — a standalone/agent-less
WebUI (and the repo's own CI runners, which don't pip-install hermes-agent)
must keep full workspace-git functionality. windows_hide_flags() is a safe
no-op (0) on non-Windows. Scoped to workspace git commands.

Adds mutation-checked tests asserting the kwarg is wired on both sites.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-07 02:07:39 +00:00
nesquena-hermes 8fee7da3ee Merge pull request #5724 from nesquena/release/stage-5717
Release: iteration-limit graceful summary (#5717)
2026-07-06 18:48:36 -07:00
nesquena-hermes 6c7746aef6 docs(changelog): iteration-limit graceful summary (#5717, #5494) 2026-07-07 01:44:48 +00:00
t 64cee6165d stage 5717 on current master 2026-07-07 01:34:14 +00:00
b3nwandb3nw 34fcc3a387 fix: drop messages-None writeback guard, reframe for invariant lineage
Per Greptile's review on #5717: the previous
`if result.get('messages') is not None` guard created an asymmetric
condition where the silent-failure classifier could miss the injected
fallback when the agent result had `messages: None` (which never
actually happens in practice since `finalize_turn` always returns a
list, but the asymmetry read as code smell). Drop the guard; the
writeback is now unconditional on `isinstance(result, dict)`.

Per the maintainer's review on #5717: there may be a
`finalize_turn` invariant on the agent branch this WebUI ships
against that already appends the fallback as an assistant row, in
which case the live bug is gone and this PR becomes a regression
guard. Reframe the test docstrings as dual-purpose pins of the user-
visible closure-text contract so they read correctly under both
interpretations.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-07 00:04:27 +00:00
b3nwandb3nw 02692cd64a fix: surface agent's graceful summary on iteration-limit exhaustion (closes #5494)
The WebUI already surfaces the model-generated summary when the agent's
tool-iteration budget is exhausted. The remaining gap: when the agent
hits the limit but its summary call yields no usable assistant content
(common with reasoning-only responses), the user sees a bare
`tool_limit_reached` error instead of any closure text.

`hermes-agent.handle_max_iterations()` always returns a non-empty
`final_response` (either the summary or a graceful fallback), and
`AIAgent` exposes that string on the result dict. The WebUI, by
contrast, only reads `result['messages']`, so an empty summary
left no final assistant answer and routed to the error path.

Inject `result['final_response']` as a final assistant turn when
`max_iterations_reached` fires and no usable answer exists in
`messages`. The existing `_mark_latest_assistant_tool_limit_status`
flow then attaches the status card and the user sees the closure
text instead of the bare error. Hermes-agent parity preserved (it's
literally the same string); the bare-error path stays for the case
where the agent died entirely (no fallback either).

Tests cover: fallback-injection surfaces closure text + status card,
no double-injection when a real summary already exists, the existing
'died entirely' apperror path is unchanged, and three unit cases for
the helper's skip conditions.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-07 00:02:27 +00:00
nesquena-hermes c4a3d4f816 Merge pull request #5716 from nesquena/release/stage-5664
Release: preserve workspace file-tree scroll position (#5664)
2026-07-06 16:29:05 -07:00
t 7fbb9bc98d docs(changelog): stamp #5664 workspace file-tree scroll preserve 2026-07-06 23:25:27 +00:00
t 4b93510ef6 stage 5664 on current master 2026-07-06 23:14:53 +00:00
nesquena-hermes 5ecd248e92 Merge pull request #5708 from nesquena/release/stage-5688b
Release: self-update .git/index.lock recovery (#5688)
2026-07-06 14:52:25 -07:00
t 9ae4ec8f09 docs(changelog): stamp #5688 self-update lock recovery 2026-07-06 21:46:39 +00:00
t e2184b7b12 stage 5688 on current master (post parallel release) 2026-07-06 21:45:49 +00:00
nesquena-hermes 8d5d28903c Merge pull request #5546: add Czech (cs) locale support
Complete Czech locale (1642/1642 key parity, all function values, login screen, test guard). Rebased + translation-quality fixes by maintainer; original author @ostravajih. Gate: full suite + Codex SAFE TO SHIP.
2026-07-06 14:42:12 -07:00
nesquena-hermes 16c3b4d91e docs(changelog): add Czech locale entry (#5546) 2026-07-06 21:37:22 +00:00
ostravajih 6da81f7a30 feat(i18n): add Czech (cs) locale support
Add a complete Czech (cs) locale to hermes-webui: full key parity with the
English reference (1642/1642 keys), real Czech translations for all string and
function-valued keys, Slavic plural helpers for tool/worklog summaries, the cs
login-screen locale in api/routes.py, and a dedicated tests/test_czech_locale.py
parity+placeholder+diacritics guard mirroring the other per-locale tests. All
15 locale-count assertions bumped 14->15.

Co-authored-by: ostravajih <ostravajih@users.noreply.github.com>
2026-07-06 21:27:46 +00:00
nesquena-hermes bb0439dcfa Merge pull request #5707 from nesquena/fix/last-two-flakes
Fix the last 2 chronic local full-suite test-isolation flakes (test-only)
2026-07-06 14:24:13 -07:00
nesquena-hermes d5210b7efc lint: drop now-unused importlib.util import 2026-07-06 21:19:20 +00:00
nesquena-hermes acfd0e9719 fix: defensive agent-importable probe (find_spec raises ValueError on spec-less partial agent module on CI) 2026-07-06 21:14:58 +00:00
nesquena-hermes 88d94556d2 docs(changelog): stamp last-2-flakes fix 2026-07-06 21:10:00 +00:00
t cec093eef6 test: fix chronic full-suite agent/hermes_cli __path__ isolation poison (2 flakes)
Convert in-place pkg.__path__=[] mutations on the REAL agent/hermes_cli
packages to monkeypatch.setattr (auto-restored), and make
test_issue1574's _activate_spawn_fake_agent snapshot+restore the
HERMES_WEBUI_AGENT_DIR/PYTHONPATH/sys.path it mutates.

Fixes:
- test_v050259_sessiondb_fd_leak::test_session_db_close_is_idempotent
- test_tls_support::test_tls_startup_failure_fallback_to_http
2026-07-06 21:08:52 +00:00
nesquena-hermes ade6de5294 Merge pull request #5704 from nesquena/fix/5696-profiles-ttl
fix(#5696): revert profiles cache TTL to 4s (stale profile rows)
2026-07-06 13:44:17 -07:00
t 5df8a06800 fix(#5696): revert profiles cache TTL 60s->4s (stale profile rows); contributor-flagged follow-up 2026-07-06 20:40:08 +00:00
nesquena-hermes 116d81d3c8 Merge pull request #5703 from nesquena/release/stage-5696
Release: session-load latency fix (#5696)
2026-07-06 13:36:48 -07:00
t 36176a9ee4 docs(changelog): stamp #5696 session-load perf 2026-07-06 20:21:04 +00:00
t 2454719f2e gate 5696 + fold-in: non-throwing HERMES_DEBUG_SLOW parse (Codex CORE fix) 2026-07-06 20:15:42 +00:00
t 06590556f5 gate 5696 on current master 2026-07-06 20:05:17 +00:00
nesquena-hermes e785264e4e Merge pull request #5702 from nesquena/release/stage-flakefix
Fix chronic full-suite test-isolation flakes (hermes_cli __path__ poison, 8 tests)
2026-07-06 12:57:23 -07:00
nesquena-hermes 44be49ebc7 test: fix chronic full-suite hermes_cli __path__ isolation poison (8 flakes) 2026-07-06 19:53:32 +00:00
b3nw 2ddef230ff fix: drop auto-delete; manual-instruction only (PR #5688 round-3)
Address round-2 gate cert (issuecomment-4896321817) on sha:dcec232d:

1. BRICK (Codex strace-verified): v2's fcntl-flock holder check on
   .git/index.lock was wrong. Codex straced git 2.43.0 and proved
   that git uses O_CREAT|O_EXCL + rename(2), NOT advisory locking,
   so an fcntl-flock probe returns False for a lock file a live git
   process is holding open. The 'fail-closed' claim was wrong:
   auto-delete could still race and corrupt the index.

2. CORE (Codex): if .git/index.lock vanished between _is_lock_held
   and os.remove, the FileNotFoundError surfaced as a 'remove-failed'
   diagnostic instead of 'already gone' and prevented the apply retry.

Fix: stop deleting lock files from the server entirely. The only thing
that removes a lock is the user, on the host, via the manual command
surfaced in the response. Once the lock is gone, the user re-clicks
Update Now and the normal non-destructive apply path runs.

Implementation:

api/updates.py:
  - Removed _is_lock_held (fcntl-flock probe) -- was provably unsafe.
  - Removed _try_remove_lock (os.remove path) -- was provably unsafe.
  - Removed _GIT_LOCK_FILES_REMOVABLE (no longer enumerable by name).
  - Added _inventory_locks(path) -> dict: pure inventory of well-known
    + other lock files under .git/. Never touches anything.
  - Rewrote apply_clear_lock to inventory-only + manual-instruction.
    When .git/index.lock is absent, runs the normal non-destructive
    apply path; when present, returns ok=False with the exact 'rm -f'
    command + an explanation of why the server cannot do this safely
    (O_CREAT|O_EXCL cannot be detected with advisory probes).
    CORE-1 race evaporates: there is no os.remove to race against.

api/routes.py:
  - Updated the /api/updates/clear_lock comment to reflect v2.2.

static/ui.js:
  - applyClearUpdateLock: when res.lock_held, call new
    _renderLockManualInstruction(target, res) which surfaces the exact
    'rm -f <path>' command in a copyable code block plus an
    'I've removed the lock -- retry update' button that POSTs the
    same endpoint again; the second call takes the success branch
    and re-runs the normal apply path. Also lists any other lock
    files present so the operator can investigate.

tests/test_updates.py:
  - Removed all 4 tests for the deleted v2 helpers (_is_lock_held
    Returns*, _try_remove_lock*).
  - Added test_v2_probe_helpers_removed: regression guard that
    fails loud if either helper is ever reintroduced.
  - Rewrote test_apply_clear_lock_removes_unheld_lock_and_runs_normal_update
    and test_apply_clear_lock_refuses_when_lock_held for the v2.2
    contract. The 'refuses' test monkeypatches os.remove to record
    any deletion attempt and asserts the call list is empty -- a
    strict runtime guard against future regressions to auto-delete.
  - Added 7 new tests: test_inventory_locks_* (4), test_apply_clear
    lock_with_no_lock_runs_normal_update, test_apply_clear_lock_with
    lock_present_returns_manual_instruction, test_apply_clear_lock
    listing_includes_other_locks, test_apply_clear_lock_rejects
    unknown_target, test_apply_clear_lock_rejects_not_git_repo.

Verification:
  ./scripts/test.sh tests/test_updates.py          -> 73 passed
  ./scripts/test.sh tests/test_update_banner_fixes.py -> 102 passed
  ./scripts/test.sh tests/test_api_timeout.py      -> 8 passed
  python3 scripts/ruff_lint.py --diff upstream/master -> 0 new violations
  Full ./scripts/test.sh -> 12137 passed, 18 failed (all 18 pre-existing
    flakes in unrelated subsystems; verified not v2.2 regressions)
2026-07-06 19:02:16 +00:00
Konstantin M 2a54e77ef2 fix(webui): drop SQL path in user_message_count — restore sidecar as source of truth
Greptile rerun and the maintainer review both raised the same concern:
state.db and the sidecar self.messages can diverge by hundreds of
messages during recovery / mid-flight writes / pending_user_message.
On the test corpus:
  0db167553ac7  db=11  sidecar=12
  295592fd560e  db=813 sidecar=889
  7478cae31f01  db=2730 sidecar=2405

The previous patch's SQL path was reading state.db. The pre-patch
inline walk was reading self.messages (the sidecar). Sidebar stale-row
detection (_looks_like_stale_zero_message_row,
_row_may_need_sidecar_metadata_refresh) consumes this field as if the
sidecar were the source of truth, so swapping the source silently flips
the field's semantics.

Drop the SQL path entirely. The helper now does the same in-memory
walk the pre-patch code did, extracted into a named method for
visibility. The inline role check (dict.get('role') with default
empty string) is 1.7x faster than calling _message_role() per
iteration on the test corpus (2.57ms -> 1.49ms across 8 sidecars,
2405 msgs total).

Bench (2405-msg sidecar): 0.76ms per call, vs ~1.5s for the pre-patch
inline walk on the same sidecar (the original perf hotspot). The
1.5s figure included JSON parse, dict construction, and the SQLite
overhead of the now-removed SQL path; the bare walk is the
0.76ms shown here.

Correctness: 27/27 webui sidecars produce identical counts vs the
pre-patch inline walk. No behavior change for any caller.

Addresses Greptile findings (P1 silent-zero, P2 docstring, P2 dead
allowlist) by removing the SQL path entirely rather than patching
around its divergent-source issue.
2026-07-06 18:45:32 +00:00
Konstantin M 2a4e86f5c3 fix(webui): preserve user_message_count under DB contention
Greptile flagged that _compute_user_message_count_lazy silently returns 0
when the SQLite query fails (DB locked, missing file, schema drift).
The pre-patch compact() walked self.messages in Python and returned the
correct count without ever touching the DB, so the silent zero
fallback is a regression that breaks _looks_like_stale_zero_message_row
under contention.

Fix: helper now takes an optional messages argument. On SQLite failure
it falls back to the same in-memory walk the original code used, so
callers see the same number they'd have seen before for any session
where Session.load() populated self.messages.

Also drop the dead ('GET', '/api/session') allowlist entry flagged by
Greptile — the /api/session handler already does its own per-stage
logging via the _t0.._t6 block (with auto-log on slow requests), and
adding RequestDiagnostics on top would just duplicate the slow-request
journal entries without adding information.
2026-07-06 18:34:02 +00:00
nesquena-hermes 40ac0c6be2 Merge pull request #5697 from nesquena/release/stage-5674
Release: stabilize sidebar lineage grouping across live refreshes (#5674)
2026-07-06 11:25:36 -07:00
t 1a71779952 docs(changelog): stamp #5674 sidebar lineage grouping stability 2026-07-06 18:21:13 +00:00
t d7b01185ac chore: drop .claude pr-sweep proof artifact from release (gitignored, not for public repo) 2026-07-06 18:20:45 +00:00
t c1c26ca4aa test merge 5674 2026-07-06 18:20:03 +00:00
nesquena-hermes 93b7ed90bf Merge pull request #5695 from nesquena/release/stage-5672
Release: reserve real off-screen height for user rows (#5672)
2026-07-06 11:13:56 -07:00
t 2283babbba docs(changelog): stamp #5672 content-visibility scrollHeight root fix 2026-07-06 18:05:03 +00:00
t efe768fb17 test merge 5672 on master w/ 5685+5681+5666 2026-07-06 17:59:15 +00:00
nesquena-hermes c93672abee Merge pull request #5694 from nesquena/release/stage-5666
Release: refuse stale-anchor scroll restore during streaming, Android (#5666)
2026-07-06 10:56:52 -07:00
t 5b4397e9de docs(changelog): stamp #5666 mobile stale-anchor stream nudge fix 2026-07-06 17:48:11 +00:00
t a7b9b2a132 test merge 5666 on master w/ 5685+5681 2026-07-06 17:47:30 +00:00
b3nw dcec232dfa fix: add btnClearUpdateLock markup for PR #5688 v2 frontend path
Address Greptile P1 review (discussion r3530941293) on commit e8f7e28a:
the v2 frontend recovery path was dead at the DOM level. static/ui.js
references $('btnClearUpdateLock') but no matching element existed in
static/index.html, so users hitting a lock conflict never saw a clickable
affordance -- the error panel was the only visible state.

Add the missing button next to btnForceUpdate in the update-banner action
row:

  <button class="update-btn" id="btnClearUpdateLock"
          style="display:none"
          onclick="applyClearUpdateLock(this)">
    Clear lock and retry update
  </button>

Style mirrors the neutral update-btn (NOT the red --error used by the
destructive Force update) so the recovery affordance doesn't read as a
warning. Hidden by default; revealed by the existing _showUpdateError()
helper when res.lock_conflict is set.

Regression tests in TestClearLockButton (3 tests):

- test_clear_lock_button_exists
- test_clear_lock_button_hidden_by_default
- test_clear_lock_button_calls_applyClearUpdateLock_handler

These lock in the contract that the button element must be present and
correctly bound; future refactors that rename the id or strip the button
will fail loud. Mirrors the existing TestIndexHtmlBanner conventions
for btnForceUpdate.

Verification:

- ./scripts/test.sh tests/test_updates.py  -k lock -> 35 passed
- ./scripts/test.sh tests/test_update_banner_fixes.py  -> 102 passed
- ./scripts/test.sh tests/test_api_timeout.py  -> 8 passed
- ruff_lint --diff upstream/master  -> 0 new violations
2026-07-06 17:46:30 +00:00