Keep distinct generated summary categories, route update-summary generation through the configured auxiliary model first, disclose capped large-range summary input, and constrain long summary panels.
The FakeAgent in test_issue1857_usage_overwrite returned only 2 messages
(user + assistant) without the conversation history. The real agent always
returns the full history plus new messages. This mismatch caused the new
_has_new_assistant_reply helper (which checks only messages beyond the
pre-turn offset) to see len(result)==len(prev) and incorrectly flag the
turn as a silent failure.
Fix: prepend conversation_history to the FakeAgent's response so the
message list mirrors production behavior.
When a provider error (401/429/rate-limit) causes the agent to return
without producing a new assistant reply, the WebUI should emit an
apperror event so the user sees an inline error. However, the detection
logic scanned ALL messages in result['messages'] — which includes the
full conversation history. If any prior turn had an assistant response,
_assistant_added would be True and the apperror would be silently
skipped, leaving the user staring at a blank response.
Extract a helper _has_new_assistant_reply(all_messages, prev_count)
that only inspects messages beyond the pre-turn history offset. Apply
it to both the main detection path and the self-heal/retry path.
Tests: 15 new cases covering history masking, empty content, whitespace,
edge-case shrinks, and multi-assistant scenarios.
Opus identified that PR #2227's preservation block had two related bugs in
the parent_session_id handling:
1. During preservation save: code did
_old_parent = s.parent_session_id
s.parent_session_id = None
s.save(touch_updated_at=False, skip_index=True)
s.parent_session_id = _old_parent
The save persisted parent=None to disk. The in-memory restoration didn't
reach the disk copy. Result: a /branch fork session that subsequently
compressed lost its 'Forked from X' badge on the preserved old snapshot.
2. Stamping the continuation: code did
if not s.parent_session_id:
s.parent_session_id = old_sid
The 'if not' guard skipped the stamp when the session already had a
parent_session_id from a prior fork. Result: fork-of-fork compression
broke lineage — the continuation jumped back to the original fork parent
instead of the just-preserved immediate predecessor snapshot.
Fix (matches Opus's recommendation):
- Remove the parent clearing during preservation save (preserve as-is)
- Drop the 'if not' guard; always stamp continuation to old_sid
This makes the lineage chain consistent: new → old → old.parent → ... root.
Traversal from the continuation always walks through the just-preserved
snapshot to get to its parent's parent, never jumping over the snapshot.
Two new regression tests pin both invariants:
- test_parent_session_id_stamped_unconditionally (no 'if not' guard)
- test_old_session_parent_preserved_during_archive_save (no parent=None)
Both pass against the fix. All 8 tests in the file pass.
fix: preserve session history during compression rotation (theh4v0c, closes#2223 — critical data-loss)
+ duplicate Appearance label-markup fix overlapping with #2222
Conflict resolution:
(1) static/index.html line ~896 — PR #2222 and PR #2227 both fix the same
endless-scroll checkbox label issue with different structures.
- #2222 (HEAD): wraps the input+span in an in-place <label> inside the
shared session-jump settings-field div.
- #2227 (incoming): closes the existing settings-field, opens a NEW
settings-field for endless-scroll, then wraps in <label>. Strictly
better — each preference owns its own settings-field, matching the
rest of the Settings → Appearance layout.
Adopted #2227's structural variant. Regression test
test_session_endless_scroll.py was updated by #2222 to match the new
structure and passes against the resolved markup.
(2) CHANGELOG.md — both PRs added a Fixed entry. Merged both attributions,
leading with the critical #2223 data-loss fix from #2227 (immutable
pre-compression archive + parent_session_id lineage stamping) and
noting that the maintainer chose #2227's structural variant for the
label-markup overlap.
11/11 tests pass across test_session_endless_scroll.py +
test_issue2223_compression_no_rename.py after resolution.
The previous implementation renamed old_sid.json → new_sid.json during
context compression, destroying the only persistent copy of the full
conversation history. If the summarisation LLM call also failed, the
user was left with zero recoverable messages.
Fix:
- Remove the destructive old_path.rename(new_path) call
- Preserve old_sid.json as an immutable pre-compression archive
- Create new_sid.json as a fresh file via s.save()
- Set parent_session_id on the continuation session for lineage
- Save in-memory messages to old_sid.json if they're newer than disk
Test: test_issue2223_compression_no_rename.py (6 tests, all passing)
Refs #2215 Fix A: replace plain dict _summary_cache with OrderedDict-based LRU capped at 16 entries to prevent unbounded memory growth from long-running update summary generations.
Add regression coverage for the bounded LRU behavior: cache hits refresh recency, a new entry at capacity evicts the least-recently used key, and cache size never exceeds the cap.
Refs #2215 Fix B: remove the mid-response stripping hazard without losing leading multi-line wrapper cleanup.
The pattern now strips only a leading 'the user is asking' wrapper line and preserves the visible answer that follows. Add regression coverage for both the leading-wrapper and mid-response prose cases.
perf(sessions): cache CLI session scans (starship-s)
Conflict resolution on api/routes.py:
(1) Master grew a new helper '_messages_include_tool_metadata()' that
pr-2149 doesn't have. Kept it (unrelated function — detects whether
returned messages contain tool metadata, used elsewhere).
(2) pr-2149 renames the CLI-metadata gate from '_needs_cli_session_metadata'
to '_session_requires_cli_metadata_lookup' AND broadens it to cover
legacy-imported sidecars with 'read_only=False' but persisted 'is_cli_session'
or session_source markers. The new gate is strictly more inclusive than
the master version — covers (a) is_cli_session, (b) read_only=True,
(c) session_source in {messaging, external_agent}, AND (d) source_tag,
raw_source, source, source_label, platform markers. All sessions that
previously took the slow path still do, plus a few more legacy shapes
that needed CLI metadata for correct display.
(3) Removed the obsolete '_needs_cli_session_metadata()' definition from
master (only consumer migrated to the new name).
29/29 tests pass across test_session_cli_scan_fast_path (new), claude_code
session import, session_index, and session_lineage_full_transcript.
Opus flagged that PR #2151's cancel-handler partial-dedup loop used a
substring check that was too broad: any short prior assistant reply
('OK', 'Here is the answer:') would dedup a longer new partial containing
it, silently dropping the partial and resurrecting the #893 data-loss bug.
Tightened to only dedup against actual prior _partial=True markers with
exact (whitespace-stripped) content match. Three new regression tests
added (short-non-partial-prefix-does-not-dedup, exact-partial-match-still-
dedups, same-content-non-partial-does-not-dedup).
10/10 partial-cancel tests pass after the fix. Also updated CHANGELOG with
the conflict-resolution notes for #2151 vs #2136 and the #2178 test-fix.
PR #2178 added an 'allowOllamaFormat' guard (resolves to false for non-ollama
@-provider prefixes like '@custom:ai_gateway') to stop the ollama label
formatter from reformatting custom-provider model IDs with dashes. The
existing test asserted on the pre-PR code shape and didn't pick up the new
guard.
Updated the assertion to match the actual post-PR code at static/ui.js:2202,
with an extended docstring explaining the bug class the guard fixes (bare
custom-provider model IDs like 'Qwen3.6-35B-A3B' had hyphens stripped to
spaces + last letter lowercased by the formatter).
fix: clarify cancelled chat turn status (Jordan-SkyLF)
Conflict resolution on api/streaming.py:4549-4567 (the cancel-handler
ownership guard). Both this PR and the already-shipped PR #2136 add a
guard at the same site against stale stream writebacks, from different
angles:
- PR #2136 (HEAD): _stream_writeback_is_current(_cs, stream_id) — strictly
dominates by checking the active_stream_id token equality.
- PR #2151: 'worker won the race' check via (active_stream_id != stream_id
and not pending_user_message), with _emit_cancel_event = False to suppress
the terminal cancel event.
Resolution merges both: keep #2136's strictly-stronger condition for skip
detection, and adopt #2151's _emit_cancel_event = False semantic so the
cancel event isn't emitted in addition to skipping the writeback (when
client may have already received the successful done payload).
55/55 tests pass across cancelled-turn-status + stale-stream-writeback +
the four cancel/data-loss sibling test files.
The original tests asserted on the final output of _redact_text(), which
exercises agent.redact.redact_sensitive_text() from the hermes-agent venv.
That function's URL-userinfo / query-param redaction is available locally
but not in the CI test environment (different agent install version).
Rewrite the tests to assert on the prefilter routing decision instead:
_might_contain_sensitive_text() must return True for URL-shaped strings.
That's the actual contract #2171 establishes and the regression Opus
flagged. The downstream agent redactor behavior is its own contract.
Sanity-checked: 5 of 6 URL cases fail when '://' marker reverted, all
pass when restored. 62 redaction tests total pass.
Opus advisor flagged that PR #2171's credential prefilter only listed
specific DB scheme prefixes and form keys, letting OAuth callback URLs,
URL userinfo, signed-URL query params bypass the hard agent redactor.
Adding the generic '://' marker restores the WebUI-as-hard-safety-boundary
contract. Plain URLs without sensitive substrings still pass through
unchanged because the redactor itself only mutates sensitive substrings.
Regression-pinned with 5 new parametric cases in test_security_redaction.py
plus 1 negative-case companion. Verified test FAILS without the fix and
PASSES with it.
_showSteerIndicator function added before _trySteer extends the total
capture region. Widen helper_body 1500→2000 and try_body 1200→1600 so
assertions on cmd_steer_fallback and S.pendingFiles=[] still land within
the window.
Problem: When two messages are sent in rapid succession, the second
send() can pass the S.busy check because setBusy(true) only runs after
the first await inside send(). This creates a window where two async
send() calls run concurrently, leading to:
- Streaming output from the first response getting swallowed when the
second response's done event overwrites S.messages
- User messages disappearing when server returns 409 for the duplicate
chat/start request
Root cause: send() is async and has awaits (uploadPendingFiles,
api('/api/chat/start')) before setBusy(true) at line 198. During those
await yields, S.busy is still false, allowing a second send() to enter.
Fix: Add a synchronous _sendInProgress guard at the very top of send()
(before any await). Concurrent calls re-queue the message instead of
silently dropping it. try/finally ensures the flag resets on all exit
paths.
Also widens the text-extraction window in
test_1062_busy_input_modes.py from 3000 to 5000 chars to accommodate
the new guard block at the top of send().
fix(config): preserve nvidia/ prefix on NVIDIA NIM (closes#2177)
Self-built. nesquena APPROVED with extensive end-to-end trace including
cross-tool agent CLI verification and 12-shape behavioural harness.
Move the `_PORTAL_PROVIDERS` guard in `resolve_model_provider()` to run
BEFORE the `prefix == config_provider` strip branch. The guard was added
for NVIDIA (along with the Nous portal cases in #854 / #894) but was
placed after the strip, so it never fired when `config_provider == "nvidia"`
and the model id started with `nvidia/`.
For `model_id="nvidia/nemotron-3-super-120b-a12b"`,
`config_provider="nvidia"`:
- prefix = "nvidia", bare = "nemotron-3-super-120b-a12b"
- prefix == config_provider → True → strip branch returned bare name
- `_PORTAL_PROVIDERS` guard never reached
- bare "nemotron-3-super-120b-a12b" sent to NVIDIA NIM → HTTP 404
NIM requires the full namespaced path. The fix moves the portal guard
to run first, so all portal providers (Nous, OpenCode-Zen, OpenCode-Go,
NVIDIA NIM) always preserve the full `provider/model` id regardless of
whether the prefix happens to equal the provider name.
This also closes a latent symmetric bug for the Nous case if a
`nous/<model>` id ever existed in the catalog.
Test plan:
- New `tests/test_issue2177_nvidia_prefix_preservation.py` covers:
- nvidia/nemotron-... under nvidia (the reported case)
- cross-namespace qwen/ and meta/ under nvidia (regression pin)
- every static nvidia model in `_PROVIDER_MODELS` resolves to itself
- latent nous/<model> under nous (structural ordering pin)
- non-portal providers (anthropic) still strip — fix doesn't over-correct
- Existing portal-routing suites (test_nous_portal_routing.py,
test_issue895_894_nous_prefix.py) continue to pass.
- Full test suite: 5320 passed, 4 skipped, 3 xpassed.
Reported on Discord by @vishnu (Nathan forwarded as #2177).
get_password_hash() computes PBKDF2-SHA256 with 600k iterations to
hash the HERMES_WEBUI_PASSWORD env var. This is called on nearly every
HTTP request via check_auth -> is_auth_enabled -> get_password_hash.
Before: ~1s of PBKDF2 per request, regardless of how many times the
same env-var value has already been hashed. A page load hitting 5+
API endpoints would burn 5+ seconds purely on password hashing.
After: compute once on first call, cache the hex result in a module-
level variable. Subsequent calls are a single global-variable read
(~50ns). The env var is immutable for the process lifetime, so there
is nothing to invalidate.
Thread-safe: double-checked locking ensures that under a burst of
concurrent requests only one thread computes PBKDF2, while the fast
path (after initialisation) requires zero locks.
10 unit tests covering all branches, cache-lifetime semantics, and
concurrent burst safety (8 threads, exactly 1 PBKDF2 call).
Test isolation: reloads only api.auth via importlib.reload, leaving
api.config untouched so test_pytest_state_isolation.py is unaffected.
Security analysis: zero regression. The hash is derived from a static
env var and a static signing key — both already readable from process
memory. Caching does not introduce any new disclosure or replay
vector. PBKDF2 is still used for the initial computation and for
verify_password() on login.
AI: deepseek/deepseek-v4-flash
get_password_hash() computes PBKDF2-SHA256 with 600k iterations to
hash the HERMES_WEBUI_PASSWORD env var. This is called on nearly every
HTTP request via check_auth -> is_auth_enabled -> get_password_hash.
Before: ~1s of PBKDF2 per request, regardless of how many times the
same env-var value has already been hashed. A page load hitting 5+
API endpoints would burn 5+ seconds purely on password hashing.
After: compute once on first call, cache the hex result in a module-
level variable. Subsequent calls are a single global-variable read
(~50ns). The env var is immutable for the process lifetime, so there
is nothing to invalidate.
Thread-safe: double-checked locking ensures that under a burst of
concurrent requests only one thread computes PBKDF2, while the fast
path (after initialisation) requires zero locks.
Security analysis: zero regression. The hash is derived from a static
env var and a static signing key — both already readable from process
memory. Caching does not introduce any new disclosure or replay
vector. PBKDF2 is still used for the initial computation and for
verify_password() on login.
AI: deepseek/deepseek-v4-flash
feat: add manual provider usage refresh (Jordan-SkyLF)
Adds a 'Refresh usage' button on the Provider quota card in Settings → Providers,
with cache: 'no-store' fetch + browser cache-bust query string. Pure browser-side
cache-busting; the server-side /api/provider/quota endpoint has no cache layer
yet (refresh=1 query param is currently a no-op server-side; the win is bypassing
browser/proxy/SW caches).
fix: guard stale stream writebacks (LumenYoung)
Prevents stale WebUI stream workers from writing old results into a session
after that session has already moved on to another stream. Adds new helper
_stream_writeback_is_current() (a token equality check against the session's
active_stream_id) and short-circuits the two finalize/cancel paths when the
worker no longer owns the session writeback.
#2142 (legeantbleu) added the fr locale to static/i18n.js but didn't update:
1. tests/test_issue1488_composer_voice_buttons.py: two TestComposerVoiceButtonI18n + TestVoiceModePreferenceGate LOCALES tuples needed 'fr'
2. api/routes.py: _LOGIN_LOCALE needed an 'fr' block so the login page localizes for French users (issue #1442 parity contract)
3. tests/test_login_locale_parity.py: the test asserting 'fr' falls-back-to-'en' is inverted — fr now resolves to fr, with sibling assertions for fr-FR and fr-CA
Mirrors the stage-340 fix for the it locale (PR #2067 → maintainer adds tuple entries). 46/46 i18n tests pass after fix.