Commit graph

378 commits

Author SHA1 Message Date
nesquena-hermes
163df17410
Release v0.51.262 — Release ID (stage-r12) (#3617)
## Release v0.51.262 — Release ID (stage-r12)

Phase-3 light slice (no-screenshot items) — 3 PRs.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3432 (#3532) | @franksong2702 | Normalize the recall-prefill terminal `user` turn so WebUI doesn't send adjacent `user` roles to strict chat templates (Mistral/Gemma/Jinja). `_normalize_prefill_messages_before_user_turn()` in both `streaming.py` + `gateway_chat.py`; drops only the terminal user tail, preserves assistant/system/mid-list context. **Rebased onto master** (was CONFLICTING). |
| #2558 (#3516) | @rodboev | "Reveal in file manager" now translates container workspace paths (`/workspace`) back to the host mount path for Docker deployments (traversal-safe via `safe_resolve` + sibling-prefix guard). |

### Changed
| Issue/PR | Author | Change |
|----------|--------|--------|
| (#3539) | @Lyr-GW | Completed the Chinese (Simplified) `zh` localization (MCP controls, tool-list pagination) with all interpolations preserved, and the language dropdown now applies the locale **instantly** on change. |

### Review fix absorbed (Codex)
#3539 also added an `allowed=['en','zh']` filter to the Settings language dropdown, which dropped the 9 other shipped locales (it/ja/ru/es/de/pt/ko/fr/tr) — and since save falls back to `en` when the select has no matching option, an existing user of those locales would be **silently reset to English** on a Settings save. Removed the filter (dropdown enumerates all `LOCALES` again, matching master); partially-translated locales fall back per-key to English at render. + regression test `test_issue3539_language_dropdown_all_locales.py`.

### Gate
- Full pytest suite: **7701 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (dropdown drops-locales) → fixed → **SAFE TO SHIP** (verified prefill drops only terminal user tail in both paths, Docker path-translation traversal-safe, zh interpolations preserved)

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Lyr-GW <Lyr-GW@users.noreply.github.com>
2026-06-04 17:16:31 -07:00
nesquena-hermes
6703978c60
Release v0.51.261 — Release IC (stage-r11) (#3616)
## Release v0.51.261 — Release IC (stage-r11)

Live Todos panel via an explicit `todo_state` SSE contract.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3373 follow-up (#3454) | @v2psv | The Todos side panel now tracks `todo` tool state **live during an active run** instead of staying stale until settle / rolling back on a mid-stream reload. A dedicated `todo_state` SSE event sends a full, redacted, idempotent snapshot on todo-tool completion (no more truncated `tool_complete.preview`); the same `api.todo_state` parser feeds live + cold-load; live snapshots persist into INFLIGHT so reload/reattach restores the panel; cold-load vs INFLIGHT reconciled by timestamp (incl. the `coldTs===0` compressed-session edge); legacy reverse-scan kept as fallback for old servers. |

### Gate
- Full pytest suite: **7692 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** — verified the new `todo_state` SSE handler composes with existing dispatch (no double-subscribe), INFLIGHT persistence is cleared on terminal/cancel (composes with discard_session + turn-journal), timestamp reconciliation can't let a stale local snapshot win, redaction holds, the legacy reverse-scan fallback still works with no double-render, and the `models.py` change is todo-scoped (no CLI-classification interaction).

Co-authored-by: v2psv <v2psv@users.noreply.github.com>
2026-06-04 16:46:20 -07:00
nesquena-hermes
ba987040c7
Release v0.51.260 — Release IB (stage-r8) (#3614)
## Release v0.51.260 — Release IB (stage-r8)

Un-held safety fixes (author resolved my earlier hold findings; re-reviewed fresh) + a clean fix batch. 6 PRs.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3535 (#3538) | @rodboev | **Self-update recovers from a stash-pop conflict without data loss.** Was a BRICK bug (`git reset --merge` + `git stash drop` discarded local mods while reporting success). Now keeps the stash, returns `ok:false` + "preserved in `stash@{0}`", no restart on conflict. *(was held — fix verified)* |
| #1909 s3 (#3562) | @rodboev | **Auth `Secure` cookie no longer locks out plain-HTTP LAN/Tailscale users.** Secure now keys only on real TLS evidence (env / TLS socket / opt-in `TRUST_FORWARDED_PROTO`); non-loopback plain-HTTP is no longer force-Secure. SameSite back to `Lax`. *(was held — fix verified)* |
| #2785 (#3559) | @franksong2702 | Clearer cron/gateway diagnostics for single-container Docker (gateway configured, no daemon → jobs silently don't fire). |
| #3555 | @lambyangzhao | Long TTS responses chunked at sentence boundaries (works around the browser's ~32K silent-truncation). |
| #3340 (#3342) | @rly09 | Persistent-state toast when a turn has saved memory / created-updated a skill. |
| #3533 | @franksong2702 | `/reload-mcp` marked `cli_only` so the WebUI doesn't dispatch it as an LLM prompt. |

### Gate
- Full pytest suite: **7681 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** — confirmed the stash-conflict path never drops the stash / never restarts on conflict, auth Secure handles LAN-HTTP correctly with no header-forgery hole, `/reload-mcp` allowlisted, state-toast has a real backend writer + active-session guard, diagnostics leak no paths, TTS chunking preserves order.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Co-authored-by: lambyangzhao <lambyangzhao@users.noreply.github.com>
Co-authored-by: rly09 <rly09@users.noreply.github.com>
2026-06-04 15:21:41 -07:00
nesquena-hermes
efbb0a5bda
Release v0.51.259 — Release IA (stage-r7) (#3612)
## Release v0.51.259 — Release IA (stage-r7)

Two ship-ready @rodboev bug-fixes from today (the clean subset of the prioritized 6).

### Fixed
| Issue | Fix |
|-------|-----|
| #3582 | **Edge-TTS playback no longer has a ~31s delay / playback error** — `_handle_tts` streamed audio without `Content-Length` on an HTTP/1.0 server; audio is now buffered and sent with an exact `Content-Length`. |
| #3583 | **CLI-bridge message reconstruction strips orphaned `tool_calls`** (assistant `tool_calls` with no matching `tool` response, left by an aborted bridge) so the next request no longer 400s on strict providers. |

### Held back from the 6-PR batch (Codex regression gate caught a real defect in each)
- **#3586/#3603** (`is_cli_session_row` reclassification) — CORE: messaging rows become non-CLI, but the sidebar open path only imports when `is_cli_session`, so opening a Discord/Telegram session shows a transient stub and the next send 404s on `/api/chat/start`. Needs a client import-gate fix + live verify. **Held.**
- **#3585/#3604** (cron-overflow) — removing `exclude_sources=None` also re-excludes `source='webui'` rows, dropping sidecarless WebUI session recovery from `/api/sessions`. Needs a separate webui recovery pass. **Held.**
- **#3587** (intermediate reasoning) — `on_interim_assistant` is suppressed upstream for contentless tool-call assistant messages (`run_agent.py:3834`), so advancing the reasoning index there never fires at tool-call boundaries → mis-attribution. **Held.**
- **#3538** (self-update stash-pop) — BRICK data-loss (`git reset --merge` + `git stash drop` discards user mods), still unaddressed. **Held.**

### Gate
- Full pytest suite: **7645 passed, 0 failed**
- ruff: CLEAN
- Codex (regression): 3 rounds — 4 PRs dropped/held for real regressions → **SAFE TO SHIP** on the clean 2

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-04 15:04:45 -07:00
nesquena-hermes
6903891139
Release v0.51.257 — Release HY (stage-r5) (#3598)
## Release v0.51.257 — Release HY (stage-r5)

Two rebased ★★★ fixes.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #3546 | @rodboev | **"Refresh Models" on a provider card no longer returns "Error: Not found".** Sent `POST /api/models/refresh` but no route matched (404). Added the route, wired to the existing `invalidate_provider_models_cache(provider_id)`. |
| #3548 | @franksong2702 | **Credential self-heal no longer writes to a dead `SessionDB` handle.** A credential-refresh evicted/closed the cached agent, but the retry rebuilt a new agent from kwargs still holding the old closed `SessionDB` → persistence silently targeted a dead handle. Per-request `SessionDB` construction centralized + refreshed on the retry. |

### Dropped from this batch
- **#2660** (session-event SSE profile scoping) — the Codex regression gate found **two SILENT dropped-refresh bugs** the scoping introduced: (1) the `maxsize=1` subscriber-queue coalescing overwrites a pending profile-A event with a profile-B event → A-tabs filter B out and never refresh for the A change; (2) renamed-root/`default` alias mismatch (backend `_profiles_match` treats them equal, the client filter uses a literal `!==`). A dropped refresh (stale sidebar) is worse than the extra refreshes the PR removes. Held with `changes-requested` + repros + the fail-safe fix (coalesce to unscoped on a profile mismatch; normalize root aliases).

### Gate
- Full pytest suite: **7622 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (#2660 dropped-refresh bugs) → #2660 dropped + held → **SAFE TO SHIP**

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-04 13:04:01 -07:00
nesquena-hermes
3ca1188f64
Release v0.51.256 — Release HX (stage-r4) (#3596)
## Release v0.51.256 — Release HX (stage-r4)

Performance — bound WebUI memory growth & idle CPU on large installs.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #3506 | @nesquena-hermes (reported w/ profiling by @djenttleman) | On a large install (~615 sessions / 40k messages / 454 MB state.db) the WebUI process climbed ~100 MB → ~1.5 GB RSS over days and held high idle CPU. Three root causes fixed: (1) `session_lifecycle._sessions` grew unbounded → new `discard_session()` drops the entry at agent-eviction boundaries, only when no in-flight commit / no uncommitted memory work (retry invariant preserved); (2) cache caps now operator-tunable (`HERMES_WEBUI_AGENT_CACHE_MAX` default 50→25, `HERMES_WEBUI_SESSIONS_MAX`); (3) GatewayWatcher computes a cheap fingerprint before the expensive per-session `MAX(messages.timestamp)` projection and only re-projects on change. |

### Rebase + review notes
- Rebased onto current master; the code diff was verified **byte-identical to the nesquena-APPROVED head** at rebase time (only CHANGELOG re-resolved).
- The Codex regression gate then surfaced **two correctness gaps** the approval didn't catch, both fixed here with regression tests:
  1. **Watcher fingerprint missed same-count transcript rewrites.** `/retry`,`/undo`,`/compress` (`SessionDB.replace_messages`) rewrite messages with new timestamps but can leave `message_count` unchanged → stale sidebar `last_activity`. Fixed with a **per-session** grouped message aggregate (`id, count, user_count, MAX(timestamp)`) over the same non-excluded sessions (a global MAX would miss a rewrite of an older, non-newest session); cron/webui stay excluded so idle churn still doesn't re-project.
  2. **LRU agent-cache eviction could close a live worker's agent** (`popitem(last=False)`, liveness-blind — pre-existing, but the lower 50→25 cap made it more likely). Eviction now snapshots `ACTIVE_RUNS` session_ids (before the cache lock — no nested lock) and skips live sessions, deferring (temporarily exceeding cap) rather than closing a live agent.

### Gate
- Full pytest suite: **7612 passed, 0 failed** (one boot-cascade flake re-run; clean on re-run)
- ruff: CLEAN · Codex (regression): 4 rounds → both gaps + a stale test fixed → **SAFE TO SHIP**

Co-authored-by: nesquena <nesquena@users.noreply.github.com>
2026-06-04 12:28:06 -07:00
nesquena-hermes
81e748b455
Release v0.51.247 — Release HO (stage-q19) (#3521)
## Release v0.51.247 — Release HO (stage-q19)

Backend correctness fix.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #3505 | @franksong2702 | **Reasoning effort is coerced to a level the active model/provider actually supports** before each request, instead of being sent verbatim and rejected. `openai-codex` `gpt-5` no longer gets `max` (→ `xhigh`); `o1`/`o3`/`o4` clamp to `low`/`medium`/`high`. Coercion only steps *down* (never escalates); `none`/unset preserved. The capability filter is applied across heuristic / models.dev / Copilot / LM Studio paths. |

This is the narrow, correct fix for the detection gap that #3431 tried to address by removing the chip-visibility gate (which we shelved). The chip-visibility gate is **untouched** (Codex confirmed) — `get_reasoning_status`/`_applyReasoningChip` still hide the chip for unconfirmed models.

### Review fix absorbed (Codex + self-flagged)
The first cut **dropped** a configured effort for *unrecognized* models, because capability detection returns `[]` for both "known-unsupported" and "simply-unknown" (custom providers, aggregator-rewritten ids, new releases) — that's a behavior change vs master (which sent it verbatim) and would silently disable reasoning. Fixed: an **empty** capability set now **preserves** the configured effort (provider stays the final authority; worst case = the same rejected request master already produces, i.e. no regression). Known-bad clamps return *non-empty* filtered sets, so they still degrade correctly. Nathan chose this "preserve-for-unknown" behavior. + regression test.

### Gate
- Full pytest suite: **7548 passed, 0 failed**
- ruff: CLEAN · 48 reasoning tests pass (incl. preserve-for-unknown + codex-clamp + never-escalate)
- Codex (regression): SHIP-ONLY-WITH-FIXES (unknown-model drop) → fixed → **SAFE TO SHIP**
- Verified empirically: gpt-5/codex max→xhigh, o3 max/xhigh→high, unknown high→high (preserved), none/unset preserved

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-03 19:21:26 -07:00
nesquena-hermes
74dcc4fe6f
Release v0.51.237 — Release HE (stage-q8): reconcile early-cancel against live worker state (#3492)
## Release v0.51.237 — Release HE (stage-q8)

Phase 3 MEDIUM-ring pick (3-factor framework): a real concurrency/state-consistency fix from a regular contributor (@franksong2702, ★★★ 38 merges). The hardest review of the sweep — the dual gate caught **two** silent data-loss bugs across two review rounds.

### Fixed
| PR | Author | Fix |
|----|--------|-----|
| #3475 | @franksong2702 | Cancelling a live turn immediately after send now reliably stops the worker and settles the session to a cancelled state (was: spinner over a blank page). `cancel_stream()` falls back to the live active-run registry (`ACTIVE_RUNS`) + session agent cache when `STREAMS` has already detached, so the worker still receives `interrupt("Cancelled by user")`. `/api/session` reports run-journal active state from the live registry instead of trusting a persisted `active_stream_id`. |

### Pre-release dual gate caught TWO silent data-loss bugs (both fixed + regression-tested)
The PR's refactor moved `agent.interrupt()` ahead of the partial-text/reasoning/tool-call snapshot, and that snapshot was no longer under `streams_lock`:
1. **Codex round 1** — the worker's `finally` (which pops `STREAM_PARTIAL_TEXT`/`STREAM_REASONING_TEXT`/`STREAM_LIVE_TOOL_CALLS` under `STREAMS_LOCK`) could clear those buffers the instant `interrupt()` wakes it, so a cancelled turn **silently lost its already-streamed text**. (Opus reviewed the original and said ship — it assumed the snapshot was still lock-protected; the stale comment claimed so but the code wasn't. Verified against the actual code → Codex was right.)
2. **Codex round 2** — my first fix only snapshotted on the `STREAMS`-present path; the detached `ACTIVE_RUNS`-only path (the case this PR adds) still lost text. Fixed by hoisting the snapshot above the `if stream_present` branch so it runs unconditionally under the lock.

Both fixes have regression tests **verified to fail against the buggy versions** (`test_cancel_preserves_partial_text_when_interrupt_pops_buffers` + `test_cancel_preserves_partial_text_on_detached_active_run_path`).

### Gate results
- **Full pytest suite**: 7481 passed, 9 skipped, 3 xpassed, **0 failed**
- **ruff forward gate**: CLEAN
- **browser-smoke gate**: CLEAN
- **Codex (regression)**: SHIP ONLY WITH FIXES (×2 rounds) → both MUST-FIXes applied → re-reviewed **SAFE TO SHIP**
- **Opus (correctness)**: reviewed the original (SAFE); the shipped version is strictly safer (adds the under-lock snapshot Opus deemed unnecessary)
- Deadlock concern cleared: no path takes `ACTIVE_RUNS_LOCK` then `STREAMS_LOCK`.

Rebased onto current master (streaming.py/routes.py merged clean — no overlap with the #3468 dedup change shipped in HC).

Closes #3475.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-03 11:26:12 -07:00
nesquena-hermes
aae584ba57
Release v0.51.235 — Release HC (stage-q5) (#3490)
## Release v0.51.235 — Release HC (stage-q5)

Single fix in the data-sensitive transcript-merge path, with a Codex-found MUST-FIX applied.

### Fix
| PR | Author | Fix |
|----|--------|-----|
| #3468 | @jasonjcwu | `_find_current_user_turn` returns the **last** matching user turn instead of the first, so post-compression `result_messages` (which carries the full history) no longer replays the entire conversation when the user repeats a similar question (137-msg session → 89 duplicate replays). |

### Pre-release review caught a CORE issue (fixed before ship)
Codex's regression gate found that a naive last-match could be overridden by a **synthetic `role:"user"` continuation prompt** (the agent loop injects "Continue"/empty-recovery nudges — verified at `conversation_loop.py:1763/4183/4356`) that only *substring*-matches the user text — anchoring the merge **past** the real turn and dropping the assistant/tool output in between. Applied the fix: track **strong** (exact `_looks_like_current_user_turn`) and **weak** (substring) matches separately and return `last_strong → last_weak → fallback`. The real turn (strong) always wins over a later synthetic continuation (weak).

Opus reviewed the original and said ship; the applied fix is strictly safer than what it reviewed.

### Gate results
- **Full pytest suite**: 7465 passed, 8 skipped, 3 xpassed, **0 failed**
- **ruff forward gate**: CLEAN
- **browser-smoke gate**: CLEAN
- **Codex (regression)**: SHIP ONLY WITH FIXES → fix applied → re-reviewed **SAFE TO SHIP**
- **Opus (correctness)**: SHIP IT (on the pre-fix code; applied fix is strictly safer)
- **Regression test** (`tests/test_issue3468_duplicate_after_compression.py`, 7 cases): pins the last-match behavior, the strong-beats-later-weak invariant, and the end-to-end no-duplicate-replay invariant — each **verified to fail against the pre-fix logic**.

Co-authored-by: jasonjcwu <jasonjcwu@users.noreply.github.com>
2026-06-03 10:11:32 -07:00
nesquena-hermes
2d20d336ea
Release v0.51.230 — Release GX (stage-p14 — extract <think> to m.reasoning #3455 + LLM Wiki last-writer #1257) (#3466)
* Release v0.51.230 (stage-p14): extract <think> to m.reasoning #3455 + LLM Wiki last-writer (#1257)

Salvage of #3455 (@gsurenull): dropped the stale api/config.py bits (MiniMax-M3 +
SCHEMA_VERSION 3->4 — both already on master via #3374). Kept the two genuine fixes:
(1) _splitThinkFromContent persist-path extraction of inline <think> blocks into
m.reasoning (fixes 30-50% session bloat for reasoning-only providers like MiniMax-M3);
(2) LLM Wiki status Last-writer 3-tier fallback (was always 'Not available' since #1257).
Added 9 Node-driven think-split regression tests (data-loss guards: content-before/after
preserved, unclosed blocks intact, lookalike tags not extracted).

* fix(#3455): renderer-matching think extraction + wiki symlink/bounded-read guards (Codex review)

Codex review of stage-p14 found 3 SILENT bugs, all fixed:
(1) DATA-LOSS: _splitThinkFromContent's Pass-2 whole-body scan extracted a CLOSED literal
<think>...</think> from visible prose/code (e.g. inside a fenced code block) into m.reasoning,
emptying it — more aggressive than the renderer (which only strips LEADING blocks). Removed
Pass 2; extraction now matches _streamDisplay semantics (leading-only, loop captures
consecutive leading blocks). +fenced-code regression test.
(2) PRIVACY: _llm_wiki_last_writer followed symlinked .md pages resolving OUTSIDE the wiki
(is_file follows symlinks), leaking external frontmatter. Now requires resolved path under
wiki_root. +symlink-containment regression test.
(3) CONTRACT/PERF: replaced full read_text() with bounded line-by-line reads (frontmatter
block only / capped log-heading scan), never page bodies.

* fix(#3455): think-split is leading-single (renderer-matching) + fix 2 stale source-match tests

Codex re-review finding #2: looping consecutive leading blocks diverged from the renderer
(_streamDisplay/_parseStreamState strip ONE leading block). Now extracts exactly one leading
block. Also updated 2 tests that asserted pre-split implementation strings:
test_live_stream_tokens_persist (content:assistantText -> content:split.content, invariant
preserved) and the consecutive-blocks test. NOTE: Codex finding #1 (client-only split doesn't
persist server-side) is a separate architectural decision pending Nathan.

* feat(#3455): split inline <think> server-side before s.save() so persisted file is compacted (Codex #1)

Codex finding #1: the think-split was client-only, so the SAVED session file still
carried inline <think> blocks (bloat) — the fix only compacted the browser copy.
Added _split_thinking_from_content (api/streaming.py), a server-side twin of the JS
helper with identical leading-only/single-block semantics, applied to the final
assistant message before s.save() (extended the existing reasoning-persist block).
Merges with on_reasoning-stream reasoning. +8 backend-parity regression tests covering
the mid-body-code-block data-loss guard, unclosed-intact, single-leading, none-content.

* test: update 3 save-path source-assertion tests for #3455 server-side think-split

The backend think-split (api/streaming.py reasoning-persist block) changed the literal
code shape + grew the pre-save block, breaking 8 source-assertion tests that anchor on it:
- test_sprint42: assert _rm['reasoning']=_reasoning_text -> now _merged_reasoning/_existing_reasoning
  + _split_thinking_from_content present (intent preserved: reasoning persisted before save).
- test_pr1318 (6) + test_pr1341: re-anchored the locator from the changed 'if _reasoning_text
  and s.messages:' line to the stable 'Persist reasoning trace in the session' comment marker;
  bumped the 1341 byte-distance limit 15000->16000 (the test self-documents bumping on legit
  pre-save growth). All behavioral invariants (reasoning persisted + context fields before save)
  unchanged.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 22:48:35 -07:00
nesquena-hermes
bb5aa02523
Release v0.51.224 — Release GR (stage-p6 — profile tool/skill config authoritative on streaming worker #3294) (#3456)
* fix: respect profile toolset/skill config on WebUI streaming worker (#3294)

The streaming agent runs on a detached worker thread that does not inherit
the per-request thread-local profile context (set from the hermes_profile
cookie on the HTTP handler thread). On that worker, the ambient get_config()
resolves via get_active_profile_name() which falls back to the process-global
_active_profile (usually 'default'). A session under a non-default profile
with an empty platform_toolsets.cli therefore loaded the DEFAULT profile's
full toolset list, inflating a tools-disabled profile's prompt from ~400 to
~15K input tokens.

Add api.config.get_config_for_profile_home() — a race-free direct disk read
of an explicit profile home's config.yaml (no shared-cache mutation), which
defers to get_config() when the requested home matches the ambient path so
in-memory test overrides are preserved. The streaming worker now resolves
_cfg from the session's own profile home, fixing toolsets, prefill context,
and fallback chains in one place.

Closes #3294

Co-authored-by: gottipx <gottipx@users.noreply.github.com>

* docs(changelog): v0.51.224 — #3294 profile toolset config (dropped #3405 to hold per Codex stale-model-repair finding)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: gottipx <gottipx@users.noreply.github.com>
2026-06-02 18:33:22 -07:00
nesquena-hermes
cccb97d970
Release v0.51.222 — Release GP (stage-p4 — backend bugfix batch: title language drift #3293 + orphaned CLI sidecar prune #3238 + pin-quota lineage #3288) (#3452)
* fix: reject cross-script drifted auto-generated session titles (#3293)

The title-language mismatch guard only knew two states: German (de) or empty,
and _title_language_mismatch early-returned False whenever the user start
wasn't German. So an English conversation whose LLM-generated title came back
in Chinese / Spanish / Russian sailed through and persisted with
llm_title_generated=true. The German case was the only one covered because
that's the one prior report it was built for.

Generalize from a German-specific binary to a language-agnostic cross-script
check. Add _script_counts() + _dominant_script() (cheap, dependency-free
Unicode-block classification: latin / cjk / cyrillic / arabic / hebrew / greek
/ devanagari). _title_language_mismatch now rejects a title that introduces a
substantial amount (>=35% of alphabetic chars, min 2) of a script different
from the conversation start's dominant script — so short titles that embed a
borrowed Latin technical term still trip, while an English title with a single
foreign place-name does not. The legacy German->English same-script heuristic
is preserved verbatim.

Kept api/streaming.py ASCII-only (the test_title_generation_source_has_no_cjk_
literals guard) — all CJK examples live in the test file, not the source.

Closes #3293

Co-authored-by: andrewkangkr <andrewkangkr@users.noreply.github.com>

* fix: prune orphaned imported-CLI sidecars from the WebUI sidebar (#3238)

When a CLI/agent session is opened in WebUI it gets a WebUI-owned sidecar
(webui/sessions/<id>.json + _index.json row) so it can render and reopen;
all_sessions() then returns it independently of the agent state.db. If the
user later deletes that session from the CLI / local Hermes storage, nothing
pruned the sidecar — the merge loop only overlays CLI metadata when a matching
state.db row exists and otherwise continues, so the stale row lingered in the
sidebar indefinitely (there is no WebUI delete affordance for CLI rows).

Add api.models.agent_session_row_exists(): an exact, uncapped existence probe
against the state.db sessions table. The sidebar merge loop now drops a row
that is_cli_session_row + not WebUI-native + absent from cli_by_id + whose
state.db row is genuinely gone, and calls prune_session_from_index() so
_index.json self-heals.

The state.db probe is deliberate: get_cli_sessions() caps at
CLI_VISIBLE_SESSION_LIMIT (20), so a still-existing session can fall out of
that window and look deleted — pruning on cli_by_id absence alone would delete
live sessions. WebUI-native rows with a CLI ancestor are never pruned, and any
probe error degrades to keep-the-row so a transient failure can't lose data.

Closes #3238

Co-authored-by: Luxciax <Luxciax@users.noreply.github.com>

* fix: count pin quota by visible session lineage

* docs(changelog): v0.51.222 — backend bugfix batch (#3293 title drift, #3238 sidecar prune, #3288 pin lineage)

* fix(pins): forks count as own pin lineage, not collapsed to parent (#3288 Codex follow-up)

Codex review of the batch found a pin-limit UNDERCOUNT: _session_row_lineage_root_id
followed any parent_session_id to the root, but /api/session/branch creates
independent visible fork sessions that also carry parent_session_id (session_source=
'fork'). Two pinned forks of the same parent collapsed to one quota lineage, letting
a user exceed pinned_sessions_limit with no 400. Fix: a fork returns its own id as
its lineage root (it's a separately-visible session); only compression/continuation
rows still collapse to a shared root. Adds a regression test with two pinned forks +
the parent counting as three distinct lineages, and confirms the existing
pre-compression-snapshot collapse case still passes.

* test(pins): update #2508/#2821 source-match tests for #3288 lineage rename

#3288 replaced the raw-session-id pin counter (pinned_ids set) with a
visible-lineage counter (pinned_lineage_ids via _visible_pinned_lineage_ids over
persisted_rows/candidate_rows). Two pre-existing source-string-matching tests
asserted the OLD implementation literals (pinned_ids = {, _session_field(existing,
session_id...), len(pinned_ids) >=). Updated both to assert the new mechanism while
preserving the invariants they actually guard: snapshot computed BEFORE LOCK (no
all_sessions()-inside-LOCK deadlock), quota filtering via the shared
_session_counts_toward_pin_quota helper, and the limit/400 guard. Behaviour
unchanged; these were implementation-detail assertions, not behaviour tests.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: andrewkangkr <andrewkangkr@users.noreply.github.com>
Co-authored-by: Luxciax <Luxciax@users.noreply.github.com>
Co-authored-by: Andy Kang <andrewkang.kr@gmail.com>
2026-06-02 17:35:18 -07:00
nesquena-hermes
c556d649b7 fix: strip @provider: prefix from auxiliary title_generation model id (#3430, @pamnard)
Manual title regeneration (POST /api/session/title/regenerate) and background
aux title generation failed with 422 / llm_error_aux when
auxiliary.title_generation.model in config.yaml used the WebUI-internal
@provider:model picker format (e.g. @gemini:gemini-3.1-flash-lite) — the raw
@-qualified id was forwarded to the provider API verbatim. Normalize it through
the canonical _split_webui_provider_model_value() helper before the aux call.

Co-authored-by: pamnard <pamnard@users.noreply.github.com>
2026-06-02 21:14:49 +00:00
nesquena-hermes
1630fb8d95 fix(streaming): inject WebUI delivery context via system prompt, not prefill user message (#3324, @aether-agent)
Consecutive user turns (session-context prefill + actual message) made models
with strict chat templates (Mistral, Gemma via llama.cpp) reject the request
with a Jinja 500. Move the platform/delivery context (connected platforms, home
channels, scheduled-task delivery hints) from _webui_session_context_message
(a prefill user message) into _webui_delivery_context_prompt, appended to the
ephemeral system prompt. Session framing stays in _webui_surface_context_prompt.
Context is preserved — just role-alternation-safe. Closes #3276.

Co-authored-by: aether-agent <aether-agent@users.noreply.github.com>
2026-06-02 19:36:25 +00:00
nesquena-hermes
ebfff8e074 fix(stream): preserve repeated identical visible turns in gateway backfill (Codex MUST-FIX on #3300)
_merge_display_messages_after_agent_result shared one _emitted set between
context-backfill rows and the visible-display backbone; _message_identity
ignores timestamps, so a second identical visible user turn was suppressed as
already-emitted. Now the display backbone is always appended in order; a
separate _context_inserted set + the _display_id_set guard dedupe only the
spliced-in context rows. Adds a regression test with two identical visible
user turns + a context-only gap.
2026-06-02 18:43:25 +00:00
nesquena-hermes
952930c10e fix: keep gateway context visible in chat transcripts (#3300, @AJV20)
Backfill model-context turns into the visible transcript before saving the
latest reply while keeping hidden [context compaction] markers out of the
visible transcript, so sidebar/header message counts no longer collapse a
compacted gateway session to a 2-message conversation. Resolves the
transcript-loss regression the v0.51.197 Codex gate flagged on the original.

Co-authored-by: AJV20 <24819659+AJV20@users.noreply.github.com>
2026-06-02 18:28:41 +00:00
nesquena-hermes
546afe8374 feat: add manual session title regeneration (#3223, @AJV20)
Closes #3106. Adds /api/session/title/regenerate endpoint + session-action
menu item. Preserves chronology (touch_updated_at=False), guards read-only
and imported sessions, syncs to state.db when Insights sync enabled.

Maintainer refinement (Opus SHOULD-FIX): scope the is_imported guard to the
regenerate action only instead of broadening the shared _isReadOnlySession()
helper, which also gates rename/pin/archive/move/fork. Matches the backend
403 guard. Test updated to assert the scoped shape.
2026-06-01 21:39:51 +00:00
AJV20
ca99d12e04 fix: keep WebUI reply guidance language-neutral 2026-06-01 04:42:23 +00:00
AJV20
7a1b2bfe72 fix: carry WebUI reply polish into gateway chat 2026-06-01 04:42:23 +00:00
nesquena-hermes
4147e7f3df fix(#3263): provider-aware default-model match (Codex final-gate MUST-FIX)
The default-only context_length guard compared model.default to the session
model with exact string equality. But model.default and the session model can
be stored in equivalent-but-different shapes (bare 'claude-opus-4.8',
provider-prefixed 'anthropic/claude-opus-4.8', or '@anthropic:claude-opus-4.8').
An exact compare wrongly treats the actual default model as non-default and
drops its configured context_length cap for provider-prefixed configs.

Add api/routes._model_matches_configured_default(session_model, cfg_default,
provider) that normalizes all three shapes, and use it at all 6 guard sites
(routes resolver + the 5 api/streaming.py sites: live-usage snapshot, persistence
_skip_cc_cl, persistence fallback _apply_cfg_ctx, SSE-done _dropped_stale_cap_sse,
SSE fallback _apply_cfg_ctx). Imported function-scoped in streaming to avoid the
routes<->streaming module-level circular import. 10 helper unit tests + a
behavioral test that a prefixed default still receives its cap.

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 01:14:22 +00:00
nesquena-hermes
17e7794923 fix(#3263): init _skip_cc_cl/_cc_cl before compressor block (UnboundLocalError on no-compressor path) + relax brittle #1318 source-assertion
The threshold-rescale block runs unconditionally after the fallback and
references _skip_cc_cl/_cc_cl, which were only defined inside 'if _cc_for_save:'.
On the no-compressor path (fresh agent / interrupted stream) that raised
UnboundLocalError (caught by test_issue1857_usage_overwrite). Hoist both inits
above the block (no-op rescale when no compressor). Also widen the #1318
source-assertion test to accept the widened fallback gate (still asserts the
falsy-check invariant).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 00:14:13 +00:00
nesquena-hermes
18a459726b fix(#3263): apply Codex dual-gate MUST-FIX — recompute persisted stale cap + rescale terminal threshold
Codex regression gate (+Opus, both independently) found the default-only guard
dropped the stale compressor cap but two sibling paths stayed inconsistent:
1. Per-turn persistence: fallback resolver only ran when context_length was
   falsy, so a previously-persisted stale 232K survived forever on non-default
   sessions. Now also runs when _skip_cc_cl, and rescales threshold_tokens to
   the recomputed real cap (or clears it).
2. Terminal SSE 'done' payload: re-emitted the stale compressor threshold, so
   messages.js overwrote S.lastUsage and the indicator reverted on stream end.
   Now rescales threshold to the resolved window when the stale cap was dropped.
Added 3 source-structure regression tests pinning both fixes; bumped the brittle
test_pr1341 distance limit 13000→15000 (+ noted it should become structural).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-06-01 00:09:21 +00:00
allenliang2022
cba69cd415 fix(context): rescale threshold_tokens to real cap in live usage snapshot
The default-only guard corrected context_length to the real per-model cap
(e.g. 1M for claude-opus-4.7-1m) but left threshold_tokens pointing at the
ContextCompressor's stale value (computed from the global 232K cap → 197.2k
@ 85%). UI then showed 'auto-compress at 197.2k / 1M' which is misleading.

Rescale threshold_tokens by the real/orig ratio so the displayed trigger
reflects the actual window (e.g. ~850k @ 1M).

NOTE: this only corrects the SSE display payload. The real auto-compress
trigger lives inside ContextCompressor in hermes-agent (agent_init.py:1446
constructs it with the global cap). A full fix requires a parallel change
upstream — tracked separately.
2026-05-31 23:49:41 +00:00
allenliang2022
1263cf03cf perf(context): cache real per-model context_length in live usage snapshot
_live_usage_snapshot() runs on every metering tick (~10x/sec while streaming).
The #3256 default-only guard recomputed get_model_context_length() there on
every tick for non-default models, which does a config read + potential
metadata/network probe — freezing claude-opus-4.7-1m streams while the default
model (4.8) stayed fast (guard not triggered for it). Resolve the real cap at
most once per stream via _real_ctx_cache. Backend-only, no frontend changes.
2026-05-31 23:49:41 +00:00
allenliang2022
4785aa542a fix(context): re-apply default-only context_length guard after v0.51.185 upgrade
Re-applies cb0065eb + b34311b3 (context_length default-only guard) which were
dropped by the upgrade reset to v0.51.185. Fixes 4.7-1m context window showing
as stale global cap (232K) instead of real 1M metadata. Backend-only: touches
api/routes.py + api/streaming.py, zero frontend/render changes.
2026-05-31 23:49:40 +00:00
AJV20
6ecc32be00 Close evicted cached agents on identity mismatch 2026-05-30 19:25:51 -04:00
nesquena-hermes
22e628be07 Merge #3191: guard session cache ownership across compression
# Conflicts:
#	CHANGELOG.md
2026-05-30 21:57:48 +00:00
nesquena-hermes
bc50e95ec7 Merge #3166: close evicted agents from WebUI cache (commit memory + shutdown provider + close db) 2026-05-30 21:57:13 +00:00
ai-ag2026
9932899b88 fix: guard session cache ownership across compression 2026-05-30 18:19:31 +02:00
ai-ag2026
09d2f4be2f fix: exclude hidden snapshots from pin quota 2026-05-30 11:48:57 +02:00
ai-ag2026
b3483d943c fix: close evicted agents from WebUI cache 2026-05-30 10:43:13 +02:00
nesquena-hermes
11ba36beb9 Merge PR #3135 into stage-batch41
# Conflicts:
#	CHANGELOG.md
#	api/routes.py
2026-05-29 22:15:46 +00:00
AJV20
3c2f98eb23 fix: polish WebUI assistant replies 2026-05-29 13:01:14 -04:00
AJV20
35f89c4e5b fix: harden external notes guardrails 2026-05-29 12:30:04 -04:00
nesquena-hermes
c1a7668bdf Merge PR #3091 2026-05-28 19:51:29 +00:00
nesquena-hermes
6719f35930 Merge PR #3094
# Conflicts:
#	CHANGELOG.md
#	api/streaming.py
2026-05-28 19:51:29 +00:00
nesquena-hermes
20f8bc3290 Merge PR #3090
# Conflicts:
#	CHANGELOG.md
2026-05-28 19:50:10 +00:00
Hermes Agent
b92204a7b7 fix(sidebar): keep newer continuation visible over older snapshot 2026-05-28 13:38:06 -06:00
AJV20
d539d5c079 fix: budget webui prefill context 2026-05-28 15:26:35 -04:00
AJV20
d5b36267f7 Merge remote-tracking branch 'origin/master' into fix/selective-durable-writeback
# Conflicts:
#	CHANGELOG.md
2026-05-28 14:59:12 -04:00
AJV20
c494e41220 Merge remote-tracking branch 'origin/master' into fix/webui-gateway-context-prefill
# Conflicts:
#	CHANGELOG.md
2026-05-28 14:52:15 -04:00
AJV20
00f4b11227 fix: limit webui durable memory guidance 2026-05-28 14:48:30 -04:00
AJV20
609a0c887b fix: carry WebUI context through gateway chat 2026-05-28 14:33:32 -04:00
nesquena-hermes
ad9b38c945 Merge PR #3086
# Conflicts:
#	CHANGELOG.md
2026-05-28 18:27:03 +00:00
ai-ag2026
07aed6b7ff fix(session): preserve subsecond message timestamp order 2026-05-28 19:58:18 +02:00
nesquena-hermes
4412aea9e8 Merge PR #3059
# Conflicts:
#	CHANGELOG.md
2026-05-28 17:47:34 +00:00
nesquena-hermes
921b94a287 Merge PR #3046
# Conflicts:
#	CHANGELOG.md
2026-05-28 17:47:34 +00:00
nesquena-hermes
d446a6c304 Merge PR #3049
# Conflicts:
#	CHANGELOG.md
2026-05-28 16:10:49 +00:00
nesquena-hermes
0fd12b2365 Merge PR #3056
# Conflicts:
#	CHANGELOG.md
2026-05-28 16:10:12 +00:00
Frank Song
9190ab4449 Fix empty partial activity tail recency 2026-05-28 15:30:49 +08:00
ai-ag2026
62efbfb13f fix: use generic title language prompt 2026-05-28 09:14:46 +02:00
ai-ag2026
b046cb42c3 fix: remove German-only fallback title override 2026-05-28 09:09:59 +02:00
ai-ag2026
2aeebf56ac fix: tighten title language detection 2026-05-28 08:21:58 +02:00
ai-ag2026
5f42e87aa9 fix: skip stale repair for compression parents 2026-05-28 08:02:24 +02:00
ai-ag2026
ad564f2cde fix: keep generated titles in conversation language (#2984)
Squash-merged pr-2984 into stage-batch33.
2026-05-28 02:47:40 +00:00
AJV20
2a96b2acc2 fix: wrap WebUI script prefill text as user context 2026-05-27 03:09:01 -04:00
nesquena-hermes
7f66f72514 Merge pull request #2993
# Conflicts:
#	CHANGELOG.md
2026-05-27 03:25:04 +00:00
Frank Song
3a9948d6cf Fix streaming SessionDB profile path 2026-05-27 10:29:31 +08:00
george-andraws
f1b04be8f7 Fix WebUI fallback provider chain merging 2026-05-26 17:51:49 -07:00
nesquena-hermes
aa6cd48318 Merge pull request #2888
# Conflicts:
#	CHANGELOG.md
#	tests/test_webui_prefill_context.py
2026-05-27 00:38:24 +00:00
nesquena-hermes
c2fc390723 Merge pull request #2915 2026-05-25 17:46:56 +00:00
nesquena-hermes
f6dc7aad6f Merge pull request #2907 2026-05-25 17:17:44 +00:00
nesquena-hermes
68afa0e44a Merge pull request #2932
# Conflicts:
#	CHANGELOG.md
2026-05-25 17:17:44 +00:00
Frank Song
524ed6cce3 fix(streaming): preserve profile env for checkpoints 2026-05-25 21:29:06 +08:00
Frank Song
89d8c3a94c fix: cap live tool prompt estimate per turn 2026-05-25 16:04:40 +08:00
fxd-jason
dd09e1c2a8 Merge remote-tracking branch 'origin/master' into fix/compression-anchor-marker-based
# Conflicts:
#	tests/test_issue2028_compression_anchor_helpers.py
2026-05-25 15:23:17 +08:00
fxd-jason
90dfbf2f2d fix: marker-based compression anchor calculation
Instead of using len(visible_after)-1 (which points to the last visible
message and gets pushed behind the render window as more turns accumulate),
find the last [CONTEXT COMPACTION] marker in s.messages and compute the
anchor from visible messages before it.

This keeps the compression reference card at the correct boundary even
after 50+ subsequent turns have scrolled the render window past the old
anchor position.

Fixes a bug where the assistant's output message appeared to disappear
after automatic context compression because the reference card was placed
at the wrong position.
2026-05-25 15:16:26 +08:00
ai-ag2026
5b9484b816 fix(streaming): suppress visible progress echoes 2026-05-25 06:18:06 +02:00
Simonas Jakubonis
7aae822872 fix(compression): ignore tool output for compaction cards 2026-05-25 11:27:15 +08:00
AJV20
894ad9ed90 Merge origin/master into feat/webui-prefill-script 2026-05-24 21:04:27 -04:00
Frank Song
535c238285 fix: preserve cached agent prefill context 2026-05-25 08:51:44 +08:00
AJV20
befee0e035 test(chat): harden WebUI prefill script hook 2026-05-24 20:20:28 -04:00
Lumen Yang
5934c2fe8a fix: address context replay review feedback 2026-05-25 00:10:27 +00:00
Lumen Yang
c616c8e788 fix: cap live tool prompt estimates 2026-05-25 00:10:27 +00:00
Lumen Yang
15cde132f3 fix: dedupe replayed context summaries 2026-05-25 00:10:27 +00:00
ai-ag2026
efe3d7c296 fix(chat): avoid false restart wording for interrupted responses
(cherry picked from commit ef8fd879682aeb729a7b7afa1e7c46478ca5ebb6)
2026-05-25 02:06:42 +02:00
AJV20
fa57868431 feat(chat): add WebUI prefill script hook 2026-05-24 20:05:20 -04:00
AJV20
d7b98d87cd Merge remote-tracking branch 'origin/master' into maint/pr-2547
# Conflicts:
#	CHANGELOG.md
2026-05-24 17:48:09 -04:00
hermes-agent
fd1c4eaeaf Stage 406: PR #2827 — fix(state-sync): pass profile explicitly so background-thread DB writes hit the right state.db (#2762) by @Koraji95-coder 2026-05-24 18:57:40 +00:00
Simonas Jakubonis
35c55e1268 fix(compression): ignore tool output for compaction cards 2026-05-24 03:55:42 +00:00
AJV20
50195c229b fix(chat): keep WebUI prefill file-only 2026-05-22 14:46:17 -04:00
AJV20
19565b6022 Merge remote-tracking branch 'origin/master' into fix/webui-context-parity
# Conflicts:
#	CHANGELOG.md
2026-05-22 14:13:42 -04:00
Hermes Agent
3874781a57 Stage 399: PR #2704 — fix(streaming): prevent exponential empty _partial accumulation across reasoning-only cancellations
Co-authored-by: wirtsi <wirtsi@users.noreply.github.com>
2026-05-21 17:56:46 +00:00
Hermes Agent
7152da2756 Stage 399: PR #2705 — fix(api): dedupe replayed context_messages so the agent doesn't see duplicates (follow-up to v0.51.96 #2620)
Co-authored-by: AlexeyDsov <AlexeyDsov@users.noreply.github.com>
2026-05-21 17:56:43 +00:00
nesquena-hermes
fa459aa01e Stage 392: PR #2651 2026-05-20 21:48:04 +00:00
Lumen Yang
71fbc796b2 fix: dedupe replayed context tail after compression 2026-05-20 23:15:54 +02:00
starship-s
153e035d12 fix: forward title generation api key 2026-05-20 14:39:38 -06:00
AJV20
8109bcb7e4 fix: honor configured title generation model 2026-05-19 22:35:11 -04:00
nesquena-hermes
7c3dcce1d0 Stage 388: PR #2598 2026-05-20 00:17:47 +00:00
AJV20
35da27bc53 Merge remote-tracking branch 'origin/master' into fix/webui-context-parity
# Conflicts:
#	CHANGELOG.md
2026-05-19 18:54:38 -04:00
AJV20
612fcd30fe fix: avoid duplicate live tool events 2026-05-19 18:41:08 -04:00
nesquena-hermes
e63de7c15f Stage 387: PR #2593
# Conflicts:
#	CHANGELOG.md
2026-05-19 22:08:56 +00:00
AJV20
ebb4dffc7d fix: stream live tool callback events 2026-05-19 14:55:19 -04:00
nesquena-hermes
6c0f864b10 Stage 386: PR #2587
# Conflicts:
#	CHANGELOG.md
2026-05-19 18:20:47 +00:00
nesquena-hermes
86f52f67b8 Stage 386: PR #2581
# Conflicts:
#	api/streaming.py
2026-05-19 18:20:47 +00:00
nesquena-hermes
0585881511 Stage 386: PR #2583 2026-05-19 18:20:07 +00:00
Michael Lam
0736e45485 fix: dedupe tool-only partial recovery markers 2026-05-19 11:16:21 -07:00
Lumen Yang
a8d429775c fix(webui): preserve casual chat compaction guard 2026-05-19 14:34:58 +00:00
AJV20
f93e288214 Fix stale stream recovery writeback race 2026-05-19 10:26:45 -04:00
dobby-d-elf
2a95c1e482 Fix profile-aware assistant display names 2026-05-19 07:17:11 -06:00