Four code-review comments from the automated Copilot reviewer on this PR:
1. `_journal_tool_already_present` dedupe was session-wide, so a
legitimately-repeated tool (e.g. a second `terminal: ls` in an
earlier turn) could cause the retry path to falsely skip
materializing the recovered tool card. The helper now takes a
keyword `stream_id` argument; when supplied, a tool card whose
`_recovered_stream_id` is set AND differs from the candidate is no
longer treated as a duplicate. Untagged tool cards (live tools, or
tool cards carried over from a pre-tagging core transcript) still
match, preserving the existing 'core transcript already has this
tool, don't duplicate' invariant. Two new tests in
`TestJournalToolDedupeScoping` cover both legs of the rule.
2./3. The troubleshooting FAQ pointed at `~/.hermes/webui/sessions/session_<sid>.json`
and `~/.hermes/_run_journal/...`. The actual sidecar filename has
no `session_` prefix and the run-journal lives under the WebUI
sessions dir (`~/.hermes/webui/sessions/_run_journal/<sid>/<stream>.jsonl`,
default). Both paths fixed and an explicit note added about
`HERMES_WEBUI_STATE_DIR` overriding the state root.
4. Drop unused `json` / `queue` / `Path` imports from
`tests/test_session_lost_response_regression.py` so the file stops
carrying noise that future linting would flag.
Adds five test classes that together pin down the contract added in the
previous commit and protect pre-fix session shapes:
* `TestInterruptedRecoveryMarker` — pure-function tests for the new
`pending_retry=True` keyword and the mutual-exclusion rule between
`recovered_output=True` and `pending_retry=True`.
* `TestRetryJournalRecoveryInPlace` — promote-on-success,
increment-on-failure, demote-after-max-attempts,
demote-after-giveup-seconds, no-op when no pending marker, and the
`_session_has_pending_journal_retry` short-circuit (which stops at
the most recent normal assistant turn).
* `TestGetSessionLazyRetryHook` — both `get_session()` entry paths
(cache-hit and cold-load) trigger the helper when a pending marker
is present; the short-circuit avoids the helper when nothing is
pending; and `metadata_only=True` skips the helper to keep sidebar
refresh free.
* `TestLazyRetryBackwardsCompat` — pre-fix sessions whose markers
use the legacy "no agent output" wording (no flag) are not touched
by `get_session()`. The four retry-meta keys round-trip cleanly
through `Session.save()` / `Session.load()`.
* `TestWslPageCacheRace` — covers the WSL2 / network-FS shape: a
first `read_run_events` raising IOError followed by a successful
read; a journal that grows visible tokens between sidecar repair
and retry; and two concurrent `get_session(sid)` calls converging
on a single promoted marker with a single recovered body
(deduped by `dedupe_existing=True`).
Two pre-existing assertions had to be relaxed because they encoded the
buggy contract (permanent "no agent output was recovered" / "user
message above was preserved" wording in the journal-empty + stream-id
known case). Both tests now accept either the legacy wording or the
new "Recovering the partial output…" wording with the pending-retry
flag, reflecting the broader fact that the old wording was the bug.
Reproduces the production failure mode:
1. Stage 1 — sidecar repair runs while the run-journal for the dead
stream is empty on disk. Assert the marker arms the lazy-retry
hook (`_pending_journal_recovery=True`,
`_journal_retry_stream_id`, `_journal_retry_attempts=0`,
`_journal_retry_first_seen_ts`) and does NOT carry the legacy
"no agent output was recovered" wording. Pending sidecar fields
are cleared regardless.
2. Stage 2 — journaled token / tool / tool_complete / token events
appear on disk. Call `get_session(sid)` and assert the marker
self-heals: wording promotes to "recovered from the run journal",
journaled assistant rows + tool card land above the marker in
chronological order, all retry meta is stripped.
Without the lazy-retry path this test fails at the very first
assertion (marker still carries the legacy no-output wording).
PR #2521 (Geist Contrast skin) adds a scoped
`:root[data-skin="geist-contrast"] .session-item.active .session-title` rule
that legitimately uses its own palette values. The existing assertion in
test_sprint40_ui_polish.py matched on any line containing the
`.session-item.active .session-title` substring, picking up the skin-scoped
override and asserting against its palette.
Exclude lines containing `:root[data-skin=` from the base-rule scan so
skin-scoped overrides are free to use their own design tokens, while the
global rule still enforces var(--gold) / var(--accent-text).
PR #2521 (Geist Contrast skin) legitimately adds a scoped
`:root[data-skin="geist-contrast"] .theme-pick-btn.active` override that
appears earlier in style.css than the global `#mainSettings .theme-pick-btn.active`
rule. The naive substring search in tests/test_1059_settings_picker_active_state.py
found the skin-specific override first (which correctly uses --border2 for its
palette), failing the global assertion that wanted --accent.
Tighten both assertions to anchor on the `#mainSettings` selector prefix so
they always match the global rule regardless of how many skin-specific
overrides land in the file.
During active streaming, dangerous-command approvals go through the
gateway path and are stored in _gateway_queues as _ApprovalEntry
objects, not in _pending. The _resolve_approval_legacy helper only
looked at _pending, so 'Allow for this session' never called
approve_session() — the user clicked Allow, the card vanished, but
the next dangerous command asked again.
Now when _pending has no matching entry, the helper peeks into
_gateway_queues to extract pattern_keys, calls approve_session(),
and marks found_target=True so resolve_gateway_approval also fires.
This commit is re-scoped to peek-only (no agent_session_key round-trip,
no state_db metadata changes).
Includes:
- Import + fallback for _gateway_queues
- Null-safe key filtering in all_keys
- Source-contract test (static) + functional test with
@requires_agent_modules skip marker for CI
- All comments and docstrings in English
Force same-session external refreshes to dismiss stale approval and clarification prompts immediately so completed state.db updates do not leave the composer blocked.
When API server runs append messages directly to state.db, reconcile WebUI sidecar sessions with those canonical rows across API responses, model-facing streaming context, and active browser refresh.
Add append-only state.db merge helpers, metadata-only counts for refresh polling, and regression coverage for API visibility, context incorporation, and frontend refresh behavior.
Adds a "Download Folder" item to the workspace file-tree right-click
menu and a GET /api/folder/download endpoint that streams the
directory as a zip with Content-Disposition: attachment.
Configurable caps:
HERMES_WEBUI_FOLDER_ZIP_MAX_MB (default 1024)
HERMES_WEBUI_FOLDER_ZIP_MAX_FILES (default 50000)
Pre-flights the walk so cap-exceeded returns 413 + JSON BEFORE any
zip bytes are sent. Symlinks resolving outside the workspace are
skipped. Mirrors the existing _handle_file_raw shape (session_id
resolution, safe_resolve, RFC 5987 filename via
_content_disposition_value). Stdlib zipfile only; no new dependencies.
Tests: 11 static-inspection tests matching the style of
tests/test_issue1867_upload_size_preflight.py. All passing on
Python 3.11/3.12/3.13.
When an anonymous custom endpoint (bare base_url, not a named custom_providers[] entry) fails its /v1/models probe, the provider group was silently dropped from the model picker entirely. This made the endpoint unusable even when /v1/chat/completions would work fine.
The fix adds an elif branch: if pid == 'custom', a cfg_base_url is configured, but no models were returned by the probe, the group is still added with an empty model list. Users can then select the Custom group and type a model ID manually in the picker's free-form input.
Closes#2542.
handle_upload_extract() used Path(s.workspace) as the extraction root,
bypassing HERMES_WEBUI_ATTACHMENT_DIR entirely. Route through
_session_attachment_dir(session_id) so archives land alongside
single-file uploads and session cleanup covers them.
Add tests and CHANGELOG entry.
Ref #2247