Skip to content

Add run-journal replay timeline parity checks - #2377

Merged
2 commits merged into
nesquena:masterfrom
franksong2702:franksong2702/replay-timeline-parity
May 16, 2026
Merged

2 commits merged into
nesquena:masterfrom
franksong2702:franksong2702/replay-timeline-parity

Conversation

@franksong2702

Copy link
Copy Markdown
Contributor

Thinking Path

What Changed

  • Added frontend static regression coverage that verifies replay-relevant long-task events enter the same _wireSSE() EventSource handler pipeline as live streaming:
    • reasoning
    • interim_assistant
    • tool
    • tool_complete
    • compressing
    • compressed
    • metering
    • terminal events such as done, apperror, and cancel
  • Added coverage that those user-visible long-task events are included in the run-journal cursor tracking loop so replay starts after the latest rendered event instead of duplicating timeline content.
  • Clarified the Document WebUI run state consistency contract #2363 run-state consistency RFC: replayed long-task events should use the same browser-facing timeline renderer as live SSE events.
  • Added a changelog entry for the contract/test hardening.

Why It Matters

This is the concrete follow-up that connects the shipped run-journal replay slice (#2283) to the run-state consistency contract (#2363) without expanding #2347 or starting the RuntimeAdapter/sidecar work.

The intended architecture is:

live SSE events      ┐
                     ├─ shared EventSource handlers / timeline renderer ─ user-visible timeline
journal replay events┘

Not:

live stream renderer
journal replay renderer

That keeps WebUI thin in execution ownership without making replay a separate presentation/runtime truth.

Verification

  • node --check static/messages.js
  • git diff --check
  • uv run --python 3.12 --with pytest pytest -q tests/test_run_journal_frontend_static.py
    • 5 passed
  • uv run --python 3.12 --with pytest pytest -q tests/test_run_journal.py tests/test_run_journal_routes.py tests/test_run_journal_frontend_static.py tests/test_run_journal_streaming_static.py
    • 24 passed

Note: python3 -m pytest ... on this Mac's system Python 3.9 fails while importing current master because the repo uses Python 3.10+ union syntax (str | None). CI targets Python 3.11/3.12/3.13, so verification used Python 3.12 via uv.

Risks / Follow-ups

  • This is intentionally a contract/test/doc PR. It does not change runtime behavior.
  • It does not make WebUI-owned execution survive a WebUI process restart.
  • It does not change Preserve live agent timeline across session switches #2347's live timeline/session-switch behavior.
  • If future SSE event names are added, the cursor-tracking event list still needs to be updated. This PR makes that drift visible.
  • No before/after screenshots are included because there is no user-visible UI change in this PR.

Refs #2376.
Refs #1925, #2283, #2361, #2363, #2347.

Model Used

OpenAI Codex (GPT-5) assisted with the analysis, implementation, tests, and PR preparation.

@Michaelyklam

Copy link
Copy Markdown
Contributor

Reviewed this from the #1925 / #2283 replay boundary and it looks like the right narrow contract hardening slice.

What I checked:

  • The diff is limited to static regression coverage, the Document WebUI run state consistency contract #2363 consistency RFC note, and the changelog.
  • The tests pin the key invariant: replayed long-task events continue through the same _wireSSE() EventSource handler path as live streaming rather than creating a second replay renderer.
  • Cursor tracking covers the user-visible long-task events needed to avoid duplicate replay (token, interim_assistant, reasoning, tool/compression events, done, apperror, cancel).
  • Scope stays within the accepted production-observation/test-contract lane: no RuntimeAdapter/sidecar, no new runtime ownership claim, and no behavioral rewrite of the live runner.

Local verification from the PR head:

env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_run_journal_frontend_static.py tests/test_run_journal.py tests/test_run_journal_routes.py tests/test_run_journal_streaming_static.py -q
# 24 passed

git diff --check origin/master...HEAD
# clean

LGTM for the focused #2376 / #1925 follow-up. This should remain a replay parity/regression-contract PR, not the start of slice-2 RuntimeAdapter or runner-survives-restart work.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Read the PR worktree against origin/master and the _wireSSE block in static/messages.js. The contract this PR pins down is real and the test shape is the right one.

What the PR actually claims

Two invariants for the replay path:

  1. Every long-task SSE event that the live stream renders (reasoning, interim_assistant, tool, tool_complete, compressing, compressed, metering, done, apperror) must flow through the same _wireSSE() handlers when replayed, not a parallel renderer.
  2. Every user-visible timeline event must be in the run-journal cursor-tracking loop, otherwise replay re-emits already-rendered events and the timeline duplicates.

Both invariants are property checks on the source — they don't actually run replay, they enforce that the code wiring still goes through the shared handlers. That's the right depth of test for this layer.

Confirming the live handlers exist

static/messages.js:1182 defines _wireSSE(source) and the file has every handler the test enumerates:

1198:    source.addEventListener('token',
1210:    source.addEventListener('interim_assistant',
1236:    source.addEventListener('reasoning',
1254:    source.addEventListener('tool',
1286:    source.addEventListener('tool_complete',
1402:    source.addEventListener('done',
1598:    source.addEventListener('compressing',
1616:    source.addEventListener('compressed',
1643:    source.addEventListener('metering',
1661:    source.addEventListener('apperror',
1759:    source.addEventListener('cancel',

And the cursor loop at static/messages.js:1801 matches the test's expectation:

for(const _runJournalEventName of ['token','interim_assistant','reasoning','tool','tool_complete','approval','clarify','title','title_status','goal','goal_continue','done','stream_end','pending_steer_leftover','compressing','compressed','metering','apperror','warning','error','cancel']){
  source.addEventListener(_runJournalEventName,_rememberRunJournalCursor);
}

The PR's test asserts the subset relevant to long-task timeline parity (drops approval, clarify, title*, goal*, stream_end, warning, error — which are intentional non-timeline events). That's the right scoping: those events have their own re-entry semantics on replay and shouldn't be lumped into a "timeline parity" assertion.

Test shape commentary

wire_pos = MESSAGES_SRC.index("function _wireSSE(source)")
wire_block = MESSAGES_SRC[wire_pos : MESSAGES_SRC.index("async function _restoreSettledSession", wire_pos)]

Slicing on the _restoreSettledSession anchor is brittle in the abstract — if that function ever moves or renames, the test fails loudly. That's fine here because the cursor-tracking loop sits inside _wireSSE and the next function down is in fact _restoreSettledSession. If someone reorganizes the file they'll need to update both the function and this test; that's a feature, not a bug, because it forces explicit acknowledgement of the contract.

Two small refinements that would harden the test further (not blockers for this PR, but worth a follow-up):

  1. The _wireSSE block-detection could grep for function _wireSSE then walk balanced braces, rather than relying on the next function name. Pros: survives function reorder. Cons: more complex. I'd leave it as-is given the existing static test culture in this file.
  2. assert "updateThinking(" in wire_block and assert "appendLiveToolCard(tc)" in wire_block are good — they pin specific renderer functions, not just the handler registration. That catches the regression where a handler exists but its body got rewired to a separate replay renderer. Useful.

Scope check

The PR diff is exactly the lane the comment from @Michaelyklam describes: regression coverage + RFC clarification + changelog. No _wireSSE body changes, no RuntimeAdapter, no behavioral changes to the live runner. The associated docs/RFC change in docs/rfcs/webui-run-state-consistency-contract.md extends the contract to say "replayed long-task events must use the same browser-facing timeline renderer as live SSE events" — that matches what the tests pin.

Verdict

LGTM. The tests give #2283 (already-merged replay slice) and #2376 (the consistency-tracking issue) a real ratchet: if a future slice-2 RuntimeAdapter PR tries to introduce a parallel replay renderer, these tests fail. That's the right shape for a "before we land more replay infra, lock the contract" PR.

One nit: in test_run_journal_cursor_tracks_every_long_task_timeline_event, the 700-byte window after the cursor_loop_pos index is a magic number. If the cursor-loop list ever grows past that, the assertion silently misses entries past byte 700. A safer bound would be cursor_loop = MESSAGES_SRC[cursor_loop_pos : MESSAGES_SRC.index(']', cursor_loop_pos)]. Trivially fixable in a follow-up.

@franksong2702

Copy link
Copy Markdown
Contributor Author

Follow-up PR #2390 is now open as the live-behavior companion to this replay parity contract.

#2377 is still the right narrow contract/test PR: replayed long-task events should go through the same _wireSSE() timeline path and cursor tracking as live streaming.

The new finding from real 8787 validation was one layer below that contract: live streaming itself still had a boundary bug where a tool start could close the current Activity group. That meant a valid backend sequence like:

interim_assistant: visible progress note
tool: terminal
tool_complete: terminal
tool: terminal
tool_complete: terminal

could render as repeated one-tool Activity rows instead of one grouped Activity row.

#2390 fixes that live grouping invariant:

  • visible interim_assistant text closes the previous Activity burst;
  • hidden reasoning updates do not split tool bursts;
  • tool starts reset the next assistant text segment but do not close the current Activity group.

So the relationship is:

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 3de4338 May 16, 2026
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 16, 2026
… 0.51.75) (#527)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.74` → `0.51.75` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.75`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05175--2026-05-16--Release-AY-stage-368--11-PR-safe-lane-batch--storage--i18n--run-journal-parity--attachments--compression-sidebar--restart-recovery--text-mode-images--tables--settings-i18n--German-labels)

[Compare Source](nesquena/hermes-webui@v0.51.74...v0.51.75)

##### Test infrastructure

- Stage-368 maintainer fix — pytest no longer self-loops on the `_schedule_restart` daemon thread. Several existing tests in `tests/test_update_banner_fixes.py` call `api.updates._schedule_restart()`, which spawns a daemon thread that eventually calls `os.execv()`. Those tests monkeypatch `os.execv` for the test scope, but monkeypatch teardown can win the race against the daemon thread, restoring the real `os.execv` before the thread fires it — at which point the daemon re-execs the entire pytest process with the original argv, looking from the outside like pytest hangs at 99 % then restarts the suite from 0 % in an infinite loop. `tests/conftest.py` now installs a permanent no-op wrapper on `os.execv` at module-import time so late-firing daemon threads cannot re-exec pytest. New `tests/test_pytest_execv_guard.py` pins the guard against future regressions.

##### Added

- **PR [#&#8203;2377](nesquena/hermes-webui#2377 by [@&#8203;franksong2702](https://github.com/franksong2702) (refs [#&#8203;2283](nesquena/hermes-webui#2283), refs [#&#8203;2363](nesquena/hermes-webui#2363), refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Run-journal replay timeline parity checks. After [#&#8203;2283](nesquena/hermes-webui#2283) shipped the first run-journal replay slice and [#&#8203;2363](nesquena/hermes-webui#2363) documented the cross-layer state consistency contract, this PR adds explicit parity assertions over the replayed timeline so divergences between the journal and the visible transcript (Thinking → tool calls → assistant text) surface as test failures instead of silent drift.

##### Fixed

- **PR [#&#8203;2391](nesquena/hermes-webui#2391 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2389](nesquena/hermes-webui#2389)) — Reduce browser storage pressure during service-worker updates and over long-running sessions. `static/sw.js` now calls `deleteOldShellCaches()` BEFORE `caches.open(CACHE_NAME)` in the install handler so the new \~2.2 MB shell cache no longer overlaps the old one during a version bump (especially painful on shared-origin quota accounting). A new `_clearSessionViewedCount()` helper plus extended `_clearHandoffStorageForSession()` prune `hermes-session-viewed-counts`, `hermes-session-completion-unread`, and `hermes-session-observed-streaming` on every single-session delete and batch-delete so per-session tracking maps no longer grow unbounded.

- **PR [#&#8203;2387](nesquena/hermes-webui#2387 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2386](nesquena/hermes-webui#2386)) — Guard `localStorage.setItem('hermes-webui-session', ...)` and workspace-panel runtime-state writes with `try { … } catch (_) {}` across `static/boot.js`, `static/sessions.js`, `static/commands.js`, and `static/messages.js`. These convenience writes were previously fatal UI operations on quota-exhausted browsers (especially Firefox public-domain setups where shared quota fills up after a service-worker shell rotation).

- **PR [#&#8203;2368](nesquena/hermes-webui#2368 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Hybridize background profile env routing so background title generation, manual compression, and update-summary workers honor a session's non-default profile. The pure thread-local refactor for [#&#8203;2321](nesquena/hermes-webui#2321) was reverted because `hermes_cli.config.load_config()` still reads `HERMES_HOME` from process env. This PR keeps the thread-local layer for WebUI helpers and adds an `os.environ.update(runtime_env)` mirror under a narrow `_ENV_LOCK` for the worker body, with proper restore of prior values. New test asserts `OPENROUTER_API_KEY` is visible from the worker against a non-default profile.

- **PR [#&#8203;2382](nesquena/hermes-webui#2382 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2380](nesquena/hermes-webui#2380)) — Serve raw chat attachments from the per-session inbox in addition to the session workspace. Chat uploads were intentionally moved out of workspaces into a per-session attachment inbox in an earlier release; the transcript renderer still emits stable `api/file/raw?session_id=...&path=<filename>` URLs, but `_handle_file_raw` only checked `session.workspace` so inbox-backed uploads rendered as broken images. The URL surface is preserved and a session-attachment fallback is added with path-traversal guards intact.

- **PR [#&#8203;2385](nesquena/hermes-webui#2385 by [@&#8203;franksong2702](https://github.com/franksong2702) — Keep fuller compression snapshots reachable in the sidebar. The default behavior hides `pre_compression_snapshot: true` rows so archived compression segments do not duplicate the active continuation. A real long Kanban session exposed a narrower failure: the fuller transcript was still present on disk but remained marked as `pre_compression_snapshot`, so the sidebar surfaced a shorter row and the fuller transcript became unreachable. The fix preserves discoverability without re-introducing duplication in normal cases.

- **PR [#&#8203;2371](nesquena/hermes-webui#2371 by [@&#8203;franksong2702](https://github.com/franksong2702) — Clarify interrupted turn recovery after a WebUI restart. WebUI executes browser-originated agent turns inside the WebUI process; if that process restarts mid-turn, the worker dies with it. Run journal replay can only replay events that were already emitted, so the stale-pending repair path is now annotated and refined to make the post-restart state explicit (interrupted, recoverable, or terminal) instead of leaving the user with a half-rendered turn and no signal.

- **PR [#&#8203;2378](nesquena/hermes-webui#2378 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Strip historical images in text-only mode. Current-turn uploads already respect `agent.image_input_mode: text`, but saved conversation history still passed native `image_url` content parts back into later provider calls, breaking text-only providers on replayed turns. `_sanitize_messages_for_api()` gains a `cfg=` keyword argument so the API-history sanitizer can strip historical native image parts when the mode is text. Default `cfg=None` preserves prior behavior for callers that don't pass the new argument.

- **PR [#&#8203;2375](nesquena/hermes-webui#2375 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Keep Markdown tables block-level. Pipe tables were already converted to `<table>` markup, but the final paragraph pass did not treat generated tables as block-level output, occasionally wrapping them in `<p>` and breaking the surrounding layout. The fix isolates generated tables and adds `table` to the paragraph-wrap skip list so valid CommonMark tables render predictably.

- **PR [#&#8203;2372](nesquena/hermes-webui#2372 by [@&#8203;mccxj](https://github.com/mccxj) — Settings → Conversation page action buttons now respect locale selection. Pre-fix, the JSON export, MD export, and Copy buttons had hardcoded English labels/titles. Adds `data-i18n` / `data-i18n-title` attributes plus the missing translation keys so non-English locales no longer see English labels stuck in the middle of a translated screen.

- **PR [#&#8203;2381](nesquena/hermes-webui#2381 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2379](nesquena/hermes-webui#2379)) — German relative session-time labels now interpolate the elapsed value instead of rendering the literal `{n}` placeholder in the sidebar/header. The German locale now uses function-valued translations for minutes, hours, and days, matching the other locale bundles.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/527
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 16, 2026
@franksong2702

Copy link
Copy Markdown
Contributor Author

Follow-up issue #2423 is open for the next replay/restart gap found in real validation.

This PR correctly pinned the replay contract: journaled long-task events should go through the same _wireSSE() timeline handlers and cursor tracking as live events.

The new issue is not that the replay endpoint failed. It worked: the stale stream reported replay_available=true and /api/chat/stream?...&replay=1 returned journaled events.

The gap is higher in the reload/repair path: after WebUI process restart, the stale stream is cleared and an interrupted marker is written to the session, but the already-journaled partial timeline is not surfaced to the user.

That makes #2423 a natural follow-up to this contract PR: replay exists, but restart recovery needs to use it before telling the user "no agent output was recovered."

eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 17, 2026
… 0.51.82) (#528)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.75` → `0.51.82` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.82`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05182--2026-05-17--Release-BF-stage-375--2-PR-batch--table-renderer-pipe-protection--Catppuccin-appearance-skin)

[Compare Source](nesquena/hermes-webui@v0.51.81...v0.51.82)

##### Added

- **PR [#&#8203;2432](nesquena/hermes-webui#2432 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2426](nesquena/hermes-webui#2426)) — Add a Catppuccin skin to Appearance settings. The single opt-in skin maps light mode to Catppuccin Latte and dark mode to Catppuccin Mocha, using Mauve as the accent while preserving the existing theme/skin persistence and no-build-step architecture.

##### Fixed

- **PR [#&#8203;2428](nesquena/hermes-webui#2428 by [@&#8203;bengdan](https://github.com/bengdan) — Protect pipes inside parens / brackets / braces from naive `split('|')` in the Markdown table renderer. Cells like `` `(a|b)` ``, `` `Union[int|float]` ``, `` `(a|b|c)` ``, and `` `Union[int|float|str]` `` now stay in a single column instead of mis-splitting. The fix uses an iterative `_protectPipes` loop so all pipes inside one bracket pair are caught, not just the first. Also adds a `$...$` guard so a KaTeX inline-math span straddling `|` column separators is left alone instead of being stashed as math. Stage-fix on the contributor branch (a) swapped the literal `}` glyphs in the regex character classes for `\x7d` hex escapes (semantically identical, but the JS source no longer carries bare close-brace glyphs that confused the brace-counting `extractFunc` in `tests/test_renderer_js_behaviour.py`); (b) dropped a stray apostrophe stop that would have mis-split `('a'|'b')`-style string-literal unions; (c) dropped angle brackets `<` / `>` from the protected-bracket set, after Opus advisor flagged that `| x < 5 | y > 10 |` would otherwise collapse into a single cell (comparison-operator usage dominates content-grouping usage in real LLM table output); and (d) added `tests/test_issue2428_table_pipe_protection.py` with 12 regression cases covering single-pipe, multi-pipe-in-brackets, apostrophes-with-pipes, the KaTeX-in-table guard, and the angle-bracket comparison-operator case.

### [`v0.51.81`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05181--2026-05-17--Release-BE-stage-374--6-PR-batch--cost-history-POSIX-lock--prompt-cache-tokens--Plugins-panel-i18n--pending-placeholder-chat--journal-replay-partial-recovery--default-off-RuntimeAdapter-Slice-2-seam)

[Compare Source](nesquena/hermes-webui@v0.51.80...v0.51.81)

##### Added

- **PR [#&#8203;2424](nesquena/hermes-webui#2424 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Add the default-off `RuntimeAdapter` Slice 2 seam. `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` now routes chat start through a `LegacyJournalRuntimeAdapter` facade over the existing legacy streaming path, while the default remains `legacy-direct`. The new adapter interface/payload classes expose start/observe/status/cancel/approval/clarify methods and delegate controls to existing handlers without introducing a runner, sidecar, new process-local queues, cached agents, cancellation registries, or callback registries.
- **PR [#&#8203;2421](nesquena/hermes-webui#2421 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2419](nesquena/hermes-webui#2419)) — Surface provider prompt-cache read/write tokens in WebUI usage displays. Cache-miss cost issues are now visible in the context tooltip and per-turn usage footer; counters carry through session persistence, SSE usage payloads, and live snapshots so deltas remain accurate across the active turn.
- **PR [#&#8203;2425](nesquena/hermes-webui#2425 by [@&#8203;mccxj](https://github.com/mccxj) — Wire Settings → Plugins panel into the existing i18n system. Panel title, description, empty state, and per-plugin labels (hooks, enabled/disabled, load failures) now respect the user's language preference; 10 new keys ship in English with `TODO: translate` placeholders in 9 additional locales.

##### Fixed

- **PR [#&#8203;2418](nesquena/hermes-webui#2418 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2402](nesquena/hermes-webui#2402)) — OpenRouter cost-history snapshot updates now take a provider-specific POSIX file lock around the read-modify-write cycle, preserving the existing process-local lock while preventing lost snapshot updates if WebUI is deployed with multiple worker processes sharing one Hermes home/state directory.
- **PR [#&#8203;2431](nesquena/hermes-webui#2431 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2429](nesquena/hermes-webui#2429)) — Chat sends now render the assistant-side pending `Thinking…` placeholder immediately after the user turn is echoed, before `/api/chat/start` returns a stream id or the first SSE event arrives. The existing stale-stream guard remains in place for ordinary reasoning updates — only the explicit pre-stream placeholder path is allowed through.
- **PR [#&#8203;2427](nesquena/hermes-webui#2427 by [@&#8203;franksong2702](https://github.com/franksong2702) (fixes [#&#8203;2423](nesquena/hermes-webui#2423)) — Recover already-journaled visible assistant text and tool cards when a WebUI process restart interrupts an in-flight browser-originated turn. The stale-stream repair path now materializes run-journal output before the explicit interrupted marker instead of collapsing the turn to "no agent output was recovered."

##### Documentation

- **PR [#&#8203;2416](nesquena/hermes-webui#2416 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Expand the runtime-adapter RFC with the concrete Slice 2 adapter-seam contract: minimal `RuntimeAdapter` methods, payload fields, `legacy-direct` / `legacy-journal` feature-flag rollback path, legacy-backend mapping, explicit non-goals, and adapter-seam acceptance tests. Keeps the next step scoped to a reversible protocol-translator boundary over the journaled legacy path, not a runner/sidecar or execution-ownership move.

### [`v0.51.80`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05180--2026-05-17--Release-BD-stage-373--2-PR-batch--provider-config-flag-filter--stale-compaction-greeting-heuristic)

[Compare Source](nesquena/hermes-webui@v0.51.79...v0.51.80)

##### Fixed

- **PR [#&#8203;2415](nesquena/hermes-webui#2415 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2399](nesquena/hermes-webui#2399)) — `providers.only_configured` and other scalar flags under the top-level `providers:` config mapping no longer appear as fake provider groups in the model picker. Provider detection now only seeds picker groups from known provider ids/aliases or dict-shaped provider configs, so filtering flags cannot render as `Only-Configured`. The gating contract is documented inline in `api/config.py` (within the existing `_PROVIDER_MODELS`/`_PROVIDER_DISPLAY` membership block) so the test\_issue604 source-scan stays satisfied.
- **PR [#&#8203;2417](nesquena/hermes-webui#2417 by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) (co-authored by [@&#8203;franksong2702](https://github.com/franksong2702), supersedes [#&#8203;2309](nesquena/hermes-webui#2309), closes [#&#8203;2308](nesquena/hermes-webui#2308)) — Compressed sessions with hidden "resume active task" context no longer treat a short fresh greeting (`hi`, `hello`, plus 6 CJK greetings) as implicit permission to continue an old agent task. Explicit continuation prompts (`continue`, `resume`, plus 4 CJK continuation phrases) still keep the compacted task context. The new helpers (`_normalize_fresh_chat_text`, `_is_casual_fresh_chat_message`, `_has_task_resume_compaction_marker`, `_context_messages_for_new_turn`) require BOTH the compaction phrase AND a task-resume keyword in the SAME message before treating it as a stale-task marker (precision-preserving guard). Length cap of 24 chars + workspace-prefix normalization + exact greeting-set match prevent false positives. CJK greetings/continuation terms are stored as Python `\u`-escape sequences so `api/streaming.py` passes the `test_title_sanitization::test_title_generation_source_has_no_cjk_literals` English-only-source invariant; runtime values are unchanged. Stage-372 Opus advisor pass caught two CJK codepoint typos (`嘖→嗨`, `哈喂→哈喽`) in the maintainer rebase and corrected them; new regression test `test_all_cjk_greetings_drop_stale_compaction_context` pins all 6 CJK greetings against future codepoint drift with `U+XXXX` failure messages.

### [`v0.51.79`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05179--2026-05-16--Release-BC-stage-372--5-PR-batch--text-mode-image-history-fix--Activity-group-compression-boundary--named-custom-provider-routing--quota-chip-Settings-toggle--RFC-docs)

[Compare Source](nesquena/hermes-webui@v0.51.78...v0.51.79)

##### Added

- **PR [#&#8203;2413](nesquena/hermes-webui#2413 (self-built follow-up to v0.51.78's [#&#8203;2082](nesquena/hermes-webui#2082), closes the quota-chip default-on regression) — New "Show provider quota chip in composer" checkbox in Settings → Preferences, default off. When disabled (the new default), the chip is hidden at all viewports and the `/api/provider/quota` fetch is skipped entirely. When enabled, the existing `@media (max-width:1399.98px)` gate from stage-371 still restricts the chip to wide desktops only. Per Nathan's directive 2026-05-16 immediately after stage-371 shipped — users get explicit agency over an ambient composer-chrome element. Wired through `api/config.py` `_SETTINGS_DEFAULTS`, `static/boot.js`, `static/panels.js` round-trip, `static/ui.js` short-circuit-when-disabled, `static/index.html` Settings field, and 11 locales in `static/i18n.js`.

##### Fixed

- **PR [#&#8203;2406](nesquena/hermes-webui#2406 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2398](nesquena/hermes-webui#2398)) — The fallback synchronous `POST /api/chat` route now passes the active WebUI config into the conversation-history sanitizer, so text-mode providers do not receive historical native `image_url` content parts when direct API callers use the legacy chat endpoint. This brings the sync route in line with the streaming chat path fixed for [#&#8203;2297](nesquena/hermes-webui#2297).
- **PR [#&#8203;2408](nesquena/hermes-webui#2408 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2404](nesquena/hermes-webui#2404)) — Auto-compression cards now close the current live Activity burst before rendering, so post-compression tools start a fresh `Activity` row instead of joining the pre-compression tool group across a real timeline/context boundary. Adds a `closeCurrentLiveActivityGroup()` helper that clears the `data-live-activity-current` marker before `appendLiveCompressionCard()` inserts the compression card. Resolves the DEFER from stage-370 Opus advisor review of PR [#&#8203;2390](nesquena/hermes-webui#2390).
- **PR [#&#8203;2411](nesquena/hermes-webui#2411 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2405](nesquena/hermes-webui#2405)) — Named `custom:*` providers no longer lose vendor-prefixed model selections when the static model picker has not hydrated that model yet. The frontend now treats named custom providers as routable aggregators for both mismatch-warning suppression and missing-dropdown fallback, and live-fetched models keep explicit `@custom:name:` provider context so selections persist instead of snapping back to the configured default.

##### Documentation

- **PR [#&#8203;2407](nesquena/hermes-webui#2407 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Document the [#&#8203;1925](nesquena/hermes-webui#1925) runtime-adapter gate update: Slice 1 run-journal replay has now passed a 100-trial synthetic replay/restart validation pass on current `origin/master`, [#&#8203;2313](nesquena/hermes-webui#2313 selected-session chat SSE cap is shipped, and Slice 2 is ready for a reversible adapter-seam planning PR without moving execution ownership yet.

##### Test infrastructure

- New regression test `tests/test_quota_chip_settings_toggle.py` (6 cases) pins the quota-chip toggle invariants: Settings field present with i18n labels, `show_quota_chip` default-`False` in `_SETTINGS_DEFAULTS` + `_SETTINGS_BOOL_KEYS`, render/refresh both short-circuit when disabled (no wasted API calls), boot initializes `window._showQuotaChip` from settings + default-false on settings-fetch failure, full panels.js round-trip, 11 locale strings present.

### [`v0.51.78`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05178--2026-05-16--Release-BB-stage-371--stuck-PR-sweep-salvage--RTL-chat--ambient-quota-chip-with-composer-clutter-gate)

[Compare Source](nesquena/hermes-webui@v0.51.77...v0.51.78)

##### Added

- **PR [#&#8203;2409](nesquena/hermes-webui#2409 (maintainer follow-up from 2026-05-16 stuck-PR sweep, co-authored by [@&#8203;malulian](https://github.com/malulian) and [@&#8203;ai-ag2026](https://github.com/ai-ag2026), closes [#&#8203;1721](nesquena/hermes-webui#1721) and [#&#8203;2082](nesquena/hermes-webui#2082)) — Two stalled contributor PRs absorbed into one self-built release after Telegram UX approval across mobile/laptop/desktop/wide viewports.
  - **Right-to-left chat layout (salvaged from [#&#8203;1721](nesquena/hermes-webui#1721) by [@&#8203;malulian](https://github.com/malulian))** — New Settings → Preferences toggle, default off, flips the chat-area direction for Arabic and Hebrew users. Honors [@&#8203;aronprins](https://github.com/aronprins)' design review on PR [#&#8203;1721](nesquena/hermes-webui#1721) (May 13 2026): drops the contributor's composer footer toggle button to keep composer real estate clean. Implementation includes a flash-prevention bootstrap `<script>` in `<head>` (applies `chat-content-rtl` class synchronously before any chat content paints), scoped CSS that only flips `.msg-row`, `.msg-body` tables, `.tool-call-group-summary`, and the composer `textarea#msg` — the sidebar, workspace panel, settings panel, and any other UI element stay left-to-right. Code blocks (`pre`, `code`, `kbd`, `samp`, `tt`, `.hljs`, `.code-block`) and tool-call group bodies force `direction:ltr; text-align:left; unicode-bidi:isolate` even under RTL, because Arabic and Hebrew developers still write English code, command lines, and JSON the same way English developers do (visually verified with embedded Python in an Arabic SSE conversation). Localized in 11 locales (en, it, ja, ru, es, de, zh-CN, zh-TW, pt, ko, fr).
  - **Ambient provider quota chip (overridden from [#&#8203;2082](nesquena/hermes-webui#2082) by [@&#8203;ai-ag2026](https://github.com/ai-ag2026))** — New green pill chip in the composer footer that surfaces the active provider's remaining quota (OpenRouter credit balance shaped as `$X.YZ`, or account-limit-shaped providers as `N%`), with click-through to Settings → Providers. Fetches `/api/provider/quota` on boot and on tab visibility return. Hidden below 1400px viewport via `@media (max-width:1400px) { display:none !important }` because the composer footer at 1280px laptop and 1440px standard desktop was already tight and the chip squeezed adjacent chips (model picker truncated from `Claude Sonnet 4 7` to `Claude Sonnet 4`, workspace dropdown lost text). Mobile users find quota through the dedicated mobile-config drawer; laptop users follow the chip's click-target into Settings → Providers anyway. The chip's value proposition (ambient quota visibility) is preserved on wide displays where there's genuine composer room without trading off existing chip readability.

##### Test infrastructure

- New regression test `tests/test_pr1721_rtl_salvage.py` (8 cases) pins the RTL salvage invariants: Settings field + i18n keys present, no composer footer button (negative assertion encoding [@&#8203;aronprins](https://github.com/aronprins)' design objection), bootstrap script runs synchronously in `<head>` before paint, CSS scoped to chat only (negative tests against `.sidebar`, `.settings-panel`, `.workspace-panel`, `html`, `body` rules), code blocks force LTR under RTL, tool-call bodies force LTR under RTL, panels.js load/save round-trip, `rtl` in `api/config.py` DEFAULTS and writable-key allow-list, 11 locale strings present.

### [`v0.51.77`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05177--2026-05-16--Release-BA-stage-370--1-PR-follow-up--live-Activity-grouping-boundary-fix)

[Compare Source](nesquena/hermes-webui@v0.51.76...v0.51.77)

##### Fixed

- **PR [#&#8203;2390](nesquena/hermes-webui#2390 by [@&#8203;franksong2702](https://github.com/franksong2702) (refs [#&#8203;2376](nesquena/hermes-webui#2376), [#&#8203;2344](nesquena/hermes-webui#2344), [#&#8203;2347](nesquena/hermes-webui#2347), [#&#8203;2377](nesquena/hermes-webui#2377)) — Live progress Activity grouping no longer degrades consecutive tool calls into repeated `Activity: 1 tool` rows. The frontend was using one reset helper for two different jobs — resetting where the next assistant text segment should render, and closing the current live Activity group — but those are not the same operation. Tool starts now only reset the next-text-segment anchor; the live Activity group closes only when the model emits a visible `interim_assistant` progress update (the actual timeline boundary). The flow stays:

  ```text
  Thinking card
  visible progress note
  Activity: N related tools
  visible progress note
  Activity: N related tools
  final answer
  ```

  Adds a WebUI-only ephemeral progress contract in `api/streaming.py` that asks multi-step tool-heavy turns to emit concise visible progress notes in the user's language, while explicitly forbidding exposure of hidden reasoning, chain-of-thought, scratchpads, secrets, raw logs, or long tool output. Any selected personality prompt is preserved. New regressions cover the progress-contract reach-through, the interim-assistant split boundary, and the consecutive-tools-in-one-Activity-row invariant.

### [`v0.51.76`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05176--2026-05-16--Release-AZ-stage-369--4-PR-safe-lane-batch--live-timeline-preservation--OpenRouter-cost-history--chat-stream-cap--credential-pool-cache)

[Compare Source](nesquena/hermes-webui@v0.51.75...v0.51.76)

##### Added

- **PR [#&#8203;2195](nesquena/hermes-webui#2195 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;692](nesquena/hermes-webui#692)) — OpenRouter cost history backend. New `GET /api/providers/openrouter/cost_history` endpoint backed by daily snapshots from OpenRouter's `/auth/key` cumulative spend. Process-local lock around the snapshot read-modify-write critical section so concurrent dashboard refreshes or multiple tabs cannot overwrite newer reads with stale ones. Delta computation handles cumulative-counter resets (key rotation, OpenRouter-side reset) by starting a fresh series and using the current value as that day's delta rather than emitting negative spend. Backend-only slice; the 7-day daily cost chart UI is a separate follow-up.

##### Fixed

- **PR [#&#8203;2347](nesquena/hermes-webui#2347 by [@&#8203;franksong2702](https://github.com/franksong2702) (fixes [#&#8203;2344](nesquena/hermes-webui#2344)) — Preserve live agent timeline across session switches. Previously, switching away from an active stream and returning rebuilt the turn from the persisted `INFLIGHT` tail, which is enough to reconnect the stream but is not a full-fidelity DOM timeline — Thinking/tool grouping flattened, interim assistant text moved away from its surrounding context, auto-compression cards could project twice. The restore path now snapshots the live assistant turn DOM during the active stream and, on return, loads the persisted transcript first then merges the live snapshot back in so the on-screen scene is preserved as the user left it. Stamping `row.dataset.sessionId` at turn creation prevents the new live-turn sites from re-triggering the lossy rebuild path.

- **PR [#&#8203;2393](nesquena/hermes-webui#2393 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;2313](nesquena/hermes-webui#2313)) — Cap live chat stream transports to the selected conversation. Previously, keeping many sessions open accumulated one long-lived `/api/chat/stream` EventSource per session. New `closeOtherLiveStreams(activeSid)` helper in `static/messages.js`; `attachLiveStream()` now reuses an existing same-session transport first, closes other sessions' chat SSE transports, then opens or replaces the selected session's stream. Background sessions still reattach normally when the user selects them — only the SSE transport is pruned, not the server-side stream ownership. New regression test pins the ordering (reuse first, prune background streams next, replace active transport last).

- **PR [#&#8203;2396](nesquena/hermes-webui#2396 by [@&#8203;starship-s](https://github.com/starship-s) — Preserve session agents for credential pools. The per-session `AIAgent` cache signature previously mixed stable agent identity with the volatile resolved API key, so credential-pool providers (where each request can resolve a different runtime token even when provider/model config is unchanged) missed the cache every turn and rebuilt the agent — losing warmed cross-turn state such as memory-provider prefetch results for providers like Hindsight. New credential-aware cache-signature helper uses a stable sentinel for credential-pool routes while preserving hashed API-key identity for non-pool routes; reused cached agents refresh runtime credentials in place; `AIAgent._primary_runtime` stays aligned after refresh so fallback/transport recovery cannot resurrect an old token; agents still in fallback-active state rebuild rather than mutate to avoid mixed primary/fallback runtime state. Static non-pool API keys still participate in the cache signature so explicit credential changes continue to invalidate.

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/528
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants