Commit graph

1469 commits

Author SHA1 Message Date
nesquena-hermes
e3a7c93dc6
[HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741)
* Harden interrupted recovery control filtering

* Redesign live-to-final assistant replies

* Fix live activity anchor test fixture

* Fix CI lint issues for live reply tests

* Strengthen live progress prompt contract

* Recover PR #3401 refresh on origin/master

* Repair live-to-final refresh regressions

* Fix live worklog refresh regressions

* Show live footer timer on initial stream start

* Restore live stream shell after reload

* Preserve per-frame live SSE replay cursors

* Preserve reasoning as Worklog Thinking cards

* Quiet Worklog Thinking card styling

* Align Worklog Thinking card styling

* Scope live Worklog Thinking cards by segment

* Suppress exact duplicate settled Thinking

* Close #3401 merge review test gaps

* fix(#3401): resolve 4 deep-review regressions (inline-think, reconnect-dup, neon skin, busy-gate worklog)

Deep review (Codex diff-vs-master + live-browser drive) of the live-to-final refactor
surfaced 4 regressions vs master that the rewritten suite no longer guarded:

1. Inline <think>…</think>answer reasoning vanished — _assistantReasoningPayloadText
   used $-anchored regexes so a leading think block + visible answer extracted nothing
   and the Thinking card never rendered. Removed the 3 $ anchors to match the
   (non-anchored) display stripper. Live: inline-think thinking-only turn now renders.
2. (CORE) reconnect/reload duplicated the live reply — _rememberRunJournalCursor advanced
   a closure-local seq but never wrote INFLIGHT[activeSid].lastRunJournalSeq, so a reload
   replayed the journal from after_seq=0 over restored lastAssistantText. Now mirrors the
   cursor onto INFLIGHT + schedules a throttled persist.
3. Neon skin silently broke — PR deleted the :root[data-skin="neon"] CSS but left Neon in
   the picker. Restored the neon CSS block from master.
4. Settled tool-worklog rebuild gated purely on !S.busy — dropped every prior settled
   turn's worklog when renderMessages re-ran during an active stream (switch-back to an
   in-progress session). Restored master's !S.busy || (S.toolCalls && S.toolCalls.length).
   Live: busy re-render now preserves tool cards (4→4, was 4→0).

Live-verified all 4 + confirmed #3709/#3592 invariants still hold (1 thinking card, none
below footer; distinct siblings preserved). + tests/test_issue3401_deep_review_fixes.py (7).

* test(#3401): realign 3 stale source-shape assertions to the deep-review fixes

Fix commit changed two source literals that existing stage tests scanned for:
- test_live_activity_timeline.py (x2): split anchor 'if(!S.busy){' → the restored
  'if(!S.busy || (S.toolCalls&&S.toolCalls.length)){' guard (fix 4).
- test_run_journal_frontend_static.py: 'after_seq=0' not in source — fix 2's comment
  contained that literal; rephrased the comment to 'the zero floor (after_seq of 0)'.
Intent of all three assertions unchanged; only the matched string updated. No code
behavior change.

* docs(changelog): v0.51.294 — Release JJ (stage-3401, #3401 live-to-final redesign)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 12:12:37 -07:00
nesquena-hermes
9720e0b03d
Release v0.51.292 — Release JH (stage-s4 — compression-exhausted turns surface as errors #3316 fixes #3315) (#3705)
* fix(#3315): surface compression-exhausted/no-final-answer turns as errors (#3316)

When Hermes Agent exhausts context compression in a long tool-heavy turn, the
streamed result can end on a tool result / assistant(tool_calls) turn with no
final assistant answer. WebUI was finalizing that as a completed response.
Now _session_lacks_final_assistant_answer() + _agent_result_terminal_failure()
classify these as terminal failures and surface an apperror instead. The
compression session-id migration + pre-compression snapshot now run BEFORE the
terminal-failure return (ordering bug from the prior hold) so state stays
consistent when exhaustion fires after the agent rotated session_id.

Co-authored-by: Frank Song <franksong2702@gmail.com>

* docs(changelog): v0.51.292 — Release JH (stage-s4, #3316 fixes #3315)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 22:45:09 -07:00
nesquena-hermes
47020925d5
Release v0.51.290 — Release JF (stage-s1 — profile provider/model resolution #3448 fixes #3405) (#3703)
* fix(#3405): respect profile provider/model in session resolution (#3448)

Profile-bound sessions now resolve their provider/model from the profile
instead of silently falling back to the global active provider — fixes wrong
credentials/billing and silent context truncation. Repairs stale models under
the profile provider (incl. the openai-codex + openai/ slash-model case) while
preserving native slash IDs on openrouter/custom.

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.290 — Release JF (stage-s1, #3448 fixes #3405)

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 22:17:27 -07:00
nesquena-hermes
0b223e91bc
Release v0.51.287 — Release JC (stage-r22 — WeCom session classification #3653 + worker-profile picker hiding #3662) (#3695)
* feat(sessions): classify WeCom gateway sessions as messaging (#3653)

Co-authored-by: Frank Song <franksong2702@gmail.com>

* feat(profiles): hide worker profiles from chat picker (#3662)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.287 — Release JC (stage-r22)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 18:56:43 -07:00
nesquena-hermes
2736f443b1
Release v0.51.286 — Release JB (stage-r21 — sidebar tab reordering #3067) (#3694)
* feat: allow sidebar tab reordering via drag (#3067)

Drag-reorder for sidebar tab chips in Settings, persisted via a sanitized
tab_order setting (collapses duplicates, rejects chat/settings, strips
non-strings).

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>

* docs(changelog): v0.51.286 — Release JB (stage-r21)

---------

Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 18:39:42 -07:00
nesquena-hermes
e9bb354908
Release v0.51.285 — Release JA (stage-r19 — update-reload server-identity race fix #3654) (#3693)
* Fix update reload readiness race — poll /health server identity before reload (#3654)

Replaces the raw-uptime comparison (couldn't distinguish a fresh old process
from the restarted one) with a stable server_started_at identity read before
the update POST; reloads only when the identity changes. Both the force-update
and regular apply paths read + pass the baseline. (#874, #3654)

Co-authored-by: Frank Song <franksong2702@gmail.com>

* docs(changelog): v0.51.285 — Release JA (stage-r19)

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 18:13:35 -07:00
nesquena-hermes
988348682c
Release v0.51.284 — Release IZ (stage-w4 — sidebar status labels + cron-sessions toggle #3570 #3514) (#3692)
* feat(sidebar): add show_cron_sessions toggle to surface cron sessions (#3514, #2841)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* feat(sidebar): add manual session status labels (#3570)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.284 — Release IZ (stage-w4)

* fix(settings): persist show_cron_sessions in the explicit Save Settings path too (#3514)

Codex regression-gate follow-up: the autosave path (_preferencesPayloadFromUi)
included show_cron_sessions but the explicit saveSettings() button path read/saved
show_cli_sessions and dropped the cron checkbox — clicking Save Settings silently
omitted it. Read settingsShowCronSessions + add body.show_cron_sessions (gated on
CLI sessions, mirroring autosave).

* fix(settings): gate show_cron_sessions identically in BOTH save paths (#3514)

Codex round-2: my saveSettings() gate exposed that the autosave path
(_preferencesPayloadFromUi) posted the raw cron checkbox state ungated, so
show_cli_sessions=false + show_cron_sessions=true could persist via autosave.
Gate autosave on showCliCb too; update the regression test to assert both
paths gate on settingsShowCliSessions.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 17:48:27 -07:00
nesquena-hermes
10413b4d65 Release v0.51.281 — Release IW (stage-verdigris — Verdigris emerald/bronze skin #3602)
Adds the Verdigris dark-only appearance skin (emerald/forest-green + bronze-gold),
renamed from the contributor's 'Hermes Agent' to a descriptive material name per
maintainer naming convention. Registered across all 5 skin sites (config allowlist,
boot.js swatch, index.html FOUC map, i18n in 12 locales, scoped CSS palette) + test.
Also fixes the zeus i18n test (zeus is no longer the trailing skin token).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-05 22:18:47 +00:00
nesquena-hermes
ffc1ab6fd6
Release v0.51.280 — Release IV (stage-p3i — Windows self-update restart fix #3647) (#3687)
* fix(updates): Windows self-update restart via detached Popen + bind-retry (os.execv doesn't replace proc on Windows) (#3647)

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

* docs(changelog): v0.51.280 — Release IV (stage-p3i)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: jja881 <jja881@users.noreply.github.com>
2026-06-05 14:54:59 -07:00
nesquena-hermes
b5caf83ff9
Release v0.51.279 — Release IU (stage-p3h — preserve Activity/streaming turn on mid-stream scroll #3665) (#3686)
* fix(streaming): preserve Activity + streaming turn when loading earlier messages mid-stream (#3665, #3346)

Co-authored-by: mysoul12138 <839465496@qq.com>

* docs(changelog): v0.51.279 — Release IU (stage-p3h)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <839465496@qq.com>
2026-06-05 14:45:31 -07:00
nesquena-hermes
8f89b4f825
Release v0.51.278 — Release IT (stage-p3g — repair inline PDF preview #3652) (#3684)
* fix(ui): repair inline PDF preview (blob module loader + CSP worker-src) (#3652, #3649)

Co-authored-by: sky <example@email.com>

* docs(changelog): v0.51.278 — Release IT (stage-p3g, #3652 only); widen CSP test window

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: sky <example@email.com>
2026-06-05 14:27:38 -07:00
nesquena-hermes
87084dfebf
Release v0.51.276 — Release IR (stage-p3e — preserve manually-named session titles #3542) (#3682)
* feat(sessions): skip adaptive auto-rename for manually-named sessions (#3542, #3230)

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

* docs(changelog): v0.51.276 — Release IR (stage-p3e)

* fix(sessions): clear manual_title lock on /api/session/clear (#3542)

Codex regression-gate follow-up: the clear endpoint reset the title to
Untitled directly, stranding manual_title=True so the reused session never
auto-named again. Route the reset through apply_session_title_rename (which
clears the lock for auto-labels) + add a behavioral and a static-guard test.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-05 13:34:54 -07:00
nesquena-hermes
165454dd7b
Release v0.51.275 — Release IQ (stage-p3d — approval SSE state extraction #3575) (#3681)
* refactor(routes): extract approval SSE state into api/route_approvals.py (#3575)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.275 — Release IQ (stage-p3d)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 13:10:45 -07:00
nesquena-hermes
9c6a96f483
Release v0.51.274 — Release IP (stage-p3c — symlink-swap TOCTOU hardening #3630) (#3680)
* fix(security): harden routes file APIs against symlink swaps (#3630, #3450)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.274 — Release IP (stage-p3c)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 12:58:11 -07:00
nesquena-hermes
9d605c41e2
Release v0.51.273 — Release IO (stage-p3b — cron-output traversal guard #3661) (#3679)
* fix(security): reject traversal-shaped job_id in cron output endpoint (#3661)

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>

* docs(changelog): v0.51.273 — Release IO (stage-p3b)

* test(cron): guard new cron-output tests with @requires_agent_modules (#3661)

The two new direct-handler tests import cron.jobs, which lives in hermes-agent
and is NOT installed in CI — without the marker they error/hang in the no-agent
CI shard (caught by the shard-0 timeout). Mirrors how the other 30 agent-dependent
tests skip cleanly when hermes-agent modules aren't importable.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
2026-06-05 12:45:47 -07:00
nesquena-hermes
405c38b7d2
Release v0.51.272 — Release IN (stage-p3a — conflict-safe self-update recovery #3667) (#3678)
* fix(updates): use stash apply (not pop) for conflict-safe self-update recovery (#3667)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.272 — Release IN (stage-p3a, #3667 only)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 12:28:31 -07:00
nesquena-hermes
58528a4d88
Release v0.51.270 — Release IL (stage-u1 — un-hold batch: #3517 #3624 #3613) (#3674)
* feat(commands): add /use to force a skill for the next turn (#3517, #2977)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(auth): cap pending passkey challenges by evicting oldest, not rejecting (#3624)

Co-authored-by: Hinotobi <paperlantern.agent@gmail.com>

* fix(providers): expose model-provider plugins in WebUI (#3613)

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

* docs(changelog): v0.51.270 — Release IL (stage-u1, 3-PR un-hold batch; #3448 + #3618 dropped)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hinotobi <paperlantern.agent@gmail.com>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
2026-06-05 11:35:26 -07:00
nesquena-hermes
2c7b530071
Release v0.51.269 — Release IK (stage-b2 — sidebar perf + search scope + Windows ctl) (#3672)
* perf(ui): single-pass sidebar session row partitioning (#3658)

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

* fix(search): scope session search to active profile (#3646)

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>

* fix(ctl): tree-kill ctl.sh stop on Windows (#3670)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.269 — Release IK (stage-b2)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 10:53:45 -07:00
nesquena-hermes
442b033e67
Release v0.51.268 — Release IJ (stage-b1 — low-risk perf + provider/clarify fixes) (#3671)
* perf(providers): O(1) codex cache merge membership checks (#3656)

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

* fix(models): add MiniMax-M3 to WebUI MiniMax fallback catalog test (#3627)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* fix(config): make DeepSeek reasoning-effort heuristic position-independent (#3650)

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

* fix(clarify): don't stash clarify draft while submission is in flight (#3651)

Co-authored-by: carryzuo00 <carryzuo00@gmail.com>

* perf(sessions): batch lineage report child fetch by parent id (#3659)

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

* perf(sessions): batch orphan sidecar state.db existence probes (#3657)

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

* test(streaming): pin DOM-INFLIGHT reattach invariant (#3572)

Co-authored-by: Rod Boev <rod.boev@gmail.com>

* docs(changelog): v0.51.268 — Release IJ (stage-b1)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: carryzuo00 <carryzuo00@gmail.com>
2026-06-05 10:44:02 -07:00
nesquena-hermes
f1211e1f0c
Release v0.51.267 — Release II (stage-r17) (#3648)
## Release v0.51.267 — Release II (stage-r17)

Security hardening cluster — 3 @zapabob PRs (forwarded-header trust + TTS prosody validation).

### Security
| Issue/PR | Author | Hardening |
|----------|--------|-----------|
| #3640 | @zapabob | `/api/tts` per-client throttle no longer trusts `X-Forwarded-For` by default (can't spoof to evade the rate limit); forwarded IP honored only behind a trusted-proxy opt-in. |
| #3642 | @zapabob | CSRF same-origin check no longer trusts `X-Forwarded-Host`/`X-Real-Host` by default (closes a forwarded-host CSRF bypass); opt-in keeps legit reverse-proxy deploys working; default uses the real `Host`. |
| #3643 | @zapabob | Browser-provided TTS prosody (rate/pitch/volume) validated against the `±N%` / `±NHz` grammar before `edge_tts.Communicate`. |

### Attribution
Each contributor branch was **rebased onto current master and pushed back to @zapabob's fork** (native authorship preserved), so the source PRs are current/mergeable. Shipped here as one release because all three add a `[Unreleased]` CHANGELOG entry at the same location (merging individually would force a rebase-cascade). Source PRs #3640/#3642/#3643 closed as merged-via-release with credit.

### Gate
- Full pytest suite: **7779 passed, 0 failed**
- ruff: CLEAN
- revert-guard: PASS (all 3 branches rebased; master is an ancestor)
- Codex (regression): **SAFE TO SHIP** — each hardening is **default-secure AND opt-in-compatible** (no legit reverse-proxy/tunnel deploy breaks on update): CSRF forwarded-host default-off + opt-in works + normal same-origin still passes; TTS prosody rejects out-of-grammar input, legit `+N%` passes; TTS throttle ignores spoofed XFF by default.

Co-authored-by: zapabob <1920071390@campus.ouj.ac.jp>
2026-06-05 01:19:59 -07:00
nesquena-hermes
9b933e2c83
Release v0.51.266 — Release IH (stage-r16) (#3641)
## Release v0.51.266 — Release IH (stage-r16)

One agent-authored APPROVED fix + two un-held streaming/SSE fixes.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3635 (#3637) | @nesquena-hermes (nesquena APPROVED) | Composer profile chip reads `S.activeProfile` again — a #3331 regression keyed it on the loaded session's profile, so opening a cross-profile session made the chip disagree with the dropdown checkmark and misrepresent where the next message routes. #3331's project/session-op scoping is unaffected. |
| #3587 (#3605) | @rodboev | Reasoning persists to the correct intermediate assistant message in multi-turn tool flows. The index only advanced in `on_interim_assistant` (suppressed for contentless tool-call messages) → post-tool reasoning was mis-attributed; it now also advances at the `on_tool` boundary, guarded against over-increment. **(un-held — finding resolved)** |
| #2660 (#3558) | @franksong2702 | Session-event SSE no longer wakes every tab across profiles and never drops a relevant refresh — profile attached when known, root/`default` aliases stay unscoped, and the `maxsize=1` queue falls back to unscoped refresh-all on a profile-mismatch coalesce. **(un-held — both findings resolved)** |

### Gate
- Full pytest suite: **7770 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** — chip matches dropdown/routing (no #3331 scoping regression), reasoning-index advance composes with the agent's tool/interim callback ordering, session-events coalesce safely with no dropped refresh and no profile data leak (`/api/sessions` still server-side filtered).

Co-authored-by: nesquena <nesquena@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-05 00:44:03 -07:00
nesquena-hermes
4cf40a317a
Release v0.51.264 — Release IF (stage-r14) (#3636)
## Release v0.51.264 — Release IF (stage-r14)

Un-held sibling pair (#3585 + #3586) — both addressed the findings from the earlier hold; re-reviewed fresh.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3585 | @rodboev | Cron sessions no longer flood the CLI sidebar window (restored the `("cron","webui")` exclusion in `_load_cli_sessions_uncached`). |
| #3586 | @rodboev | Messaging sessions keep their source label after a refresh **and open + send correctly** — `is_cli_session_row()` classifies them non-CLI, and the sidebar open/import path now uses `_isMessagingSession()` so a reclassified Discord/Telegram/Slack row is imported on open (no transient stub → no `/api/chat/start` 404). |

### Un-hold note
These were held earlier today because the `is_cli_session_row()` reclassification (#3586) created a CORE open-path regression — opening a reclassified messaging session 404'd on the next send. The author pushed a fix adding the `_isMessagingSession()` import gate at all open/lineage/refresh paths (+ regression test `test_issue3603_external_session_import_gate.py`), and Codex confirmed both that AND the secondary webui-recovery concern (cron-only exclusion now keeps `source='webui'` sidecar-less recovery rows) are resolved.

### Gate
- Full pytest suite: **7729 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** — open→import→send path verified (messaging rows go through `/api/session/import_cli` before `/api/chat/start`); `is_cli_session_row` classification correct; the pair composes in `_load_cli_sessions_uncached`.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-05 00:11:48 -07:00
nesquena-hermes
52261ebdaf
Release v0.51.263 — Release IE (stage-r13) (#3625)
## Release v0.51.263 — Release IE (stage-r13)

Batch 1 (fresh) — trimmed to the clean pair after the gate held two.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3621 | @luanxu-dev | `/codex-runtime` + `/codex_runtime` now run as a WebUI slash command (routed through the executor reusing the agent's `codex_runtime_switch`) instead of being sent to the model as a chat message. |

### Tests
| Issue/PR | Author | Change |
|----------|--------|--------|
| #3595 | @rodboev | Regression coverage for the already-shipped `activity_feed_expanded_default` setting. |

### Held back from this batch (Codex regression gate)
- **#3624** (passkey-challenge cap, security) — the cap **raises** `PasskeyRateLimitError` when full instead of **evicting oldest**, so an attacker (or 8 abandoned legit attempts per context) can lock out genuine registration/login until TTL — the protection becomes a lockout DoS. Held with the oldest-first-eviction fix.
- **#3618** (prefer server-side STT) — forcing MediaRecorder by default breaks browser `SpeechRecognition` dictation on installs with **no** server STT configured (`_transcribeBlob` only toasts on failure, never falls back). Held with the graceful-fallback fix.

### Gate
- Full pytest suite: **7714 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (#3624 DoS-lockout + #3618 STT-no-fallback) → both dropped/held → **SAFE TO SHIP** (verified no passkeys.py/boot.js remnants, codex-runtime dispatch reaches the allowlist).

Co-authored-by: luanxu-dev <luanxu-dev@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-04 23:56:44 -07:00
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
8066d384ca
Release v0.51.255 — Release HW (stage-r3) (#3594)
## Release v0.51.255 — Release HW (stage-r3)

Backend hardening — single PR.

### Fixed
| PR | Author | Fix |
|----|--------|-----|
| #3561 | @rodboev | Turn journal (crash-recovery backbone) writes **pid-scoped shards** (`{sid}~{pid}.jsonl`) instead of one shared `{sid}.jsonl`, so concurrent processes (e.g. a self-restart overlap) can't interleave-corrupt large JSON lines. `read_turn_journal` merges all shards + the legacy file and sorts by `created_at` — recovery unchanged, backward-compatible. |

### Gate
- Full pytest suite: **7593 passed, 0 failed**
- ruff: CLEAN · 18 turn-journal tests pass
- Codex (regression): **SAFE TO SHIP** — verified legacy+shard merge (no data loss on upgrade), `~` separator can't collide with a session id, the cross-shard `created_at` sort doesn't break recovery (it derives state by timestamp; stream lookup keys by unique `stream_id`), and no reader/writer bypasses `_journal_path`.
- *Non-blocking note:* old `{sid}~{oldpid}.jsonl` shards aren't pruned, so the journal dir can grow across restarts — storage hygiene, not a core-flow regression. Worth a follow-up cleanup (e.g. drop shards with no live pid on session delete).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-04 11:24:25 -07:00
nesquena-hermes
ba70926e51
Release v0.51.253 — Release HU (stage-r1) (#3591)
## Release v0.51.253 — Release HU (stage-r1)

Phase-1 low-risk batch — 7 PRs (no intervention beyond apply + one inline MUST-FIX).

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3525 | @TomBanksAU | Streaming DOM-replace "follow" window tightened 1200px→120px — a reader who scrolled up mid-stream no longer gets snapped to the bottom on completion. |
| #3556 | @ai-ag2026 | Topbar count distinguishes a partially-loaded transcript ("loaded of total" via server `message_count`); fully-loaded keeps the tool-row-filtered count. |
| #3502 follow-up | @rodboev | Sidebar messaging source badges (Telegram/Discord/…) render as chips, not just CLI ones. |
| — | @Karlineal | `.pre-header+pre` margin override scoped under `.msg-body` (removes a 10px gap above code blocks). |

### Tests
- `test_ctl_script.py` kills orphan fake-python trees on Windows; conftest `_discover_python` checks the Windows venv layout (`Scripts/python.exe`). (#3537, #3577, @rodboev)

### Docs
- Explicit WebUI–Agent compatibility policy + Docker pinning guidance. (#3232, @franksong2702)

### Dropped from this batch
- **#3538** (self-update stash-pop recovery) — the Codex regression gate found a **BRICK-class data-loss**: the recovery path runs `git reset --merge` then `git stash drop`, permanently discarding the user's local modifications while returning `ok:true` + scheduling a restart. Held with `changes-requested` + a repro and the fix (keep the stash, return `ok:false`, no restart). Concept is good; the destructive `stash drop` must go.

### Gate
- Full pytest suite: **7575 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (BRICK data-loss #3538 + tool-row count regression #3556) → #3538 dropped, #3556 fixed inline → **SAFE TO SHIP**

Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Karlineal <Karlineal@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-04 10:50:34 -07:00
nesquena-hermes
703aba3f3e
Release v0.51.250 — Release HR (stage-q22) (#3526)
## Release v0.51.250 — Release HR (stage-q22)

UX-approved (dark + light-fallback screenshots).

### Added
| PR | Author | Feature |
|----|--------|---------|
| #3328 | @heagandev | **Zeus appearance skin** — OLED-near-black dark surfaces that keep the default **gold accent** (a high-contrast "gold on black" look no existing skin offered). Selectable from Settings → Appearance or `/theme skin zeus`. Dark-focused; falls back to the default light palette in light mode. |

### Notes
- The PR was 2 days / ~19 releases stale and CONFLICTING; re-applied surgically onto current master (CSS palette + `zeus` registered at all 5 sites: config allowlist, boot.js swatch, index.html boot-map, i18n `cmd_theme` ×12 locales, picker) + THEMES.md doc row. The PR's own `test_zeus_skin.py` (6 tests) passes against the re-applied version.
- Fully scoped + additive: Codex verified every new CSS rule is under `:root.dark[data-skin="zeus"]` — no bleed into the default appearance or other skins.

### Gate
- Full pytest suite: **7563 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · vision-verified dark (OLED+gold) + light (clean fallback)
- Codex (regression): **SAFE TO SHIP**

Co-authored-by: heagandev <heagandev@users.noreply.github.com>
2026-06-03 20:54:02 -07:00
nesquena-hermes
0501564098
Release v0.51.249 — Release HQ (stage-q21) (#3523)
## Release v0.51.249 — Release HQ (stage-q21)

Small opt-in feature. UX-approved (screenshot of the toggle in Settings → Preferences).

### Added
| Issue | Author | Feature |
|-------|--------|---------|
| #2974 | @rodboev | **"Auto-expand terminal on output"** preference (Settings → Preferences, **off by default**). When enabled, the collapsed embedded terminal panel expands automatically the first time a running command emits output. Fires once per stream (guarded on open && collapsed — not per chunk), and uses `expandComposerTerminal({focus:false})` so it doesn't steal focus from the composer. Backend-persisted boolean mirroring the `simplified_tool_calling` pattern; default-off = no behavior change on upgrade. |

### Gate
- Full pytest suite: **7557 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · screenshot vision-verified (toggle renders cleanly under "Compact tool activity", unchecked by default)
- Codex (regression): **SAFE TO SHIP** (clean first pass) — no-arg `expandComposerTerminal` callers unchanged, default-off incl. settings-load-failure path, all 5 plumb sites mirror the existing pattern

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-03 20:15:53 -07:00
nesquena-hermes
d828be6daa
Release v0.51.248 — Release HP (stage-q20) (#3522)
## Release v0.51.248 — Release HP (stage-q20)

Bug-fix.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #2782 | @rodboev | **A WebUI session whose sidecar was deleted server-side (e.g. `docker compose --force-recreate`) but whose messages remain in `state.db` no longer bricks the chat.** It used to look alive (`GET` 200 from a CLI stub) while every action 404'd (`POST /api/session/draft`, `/api/chat/start`). The GET handler now consults `_index.json`: a deleted **WebUI-origin** session (webui/fork/blank-non-CLI source) returns 404 so the client self-heals (clears saved id, strips the stale `/session/<id>` URL, falls through to the welcome screen). Genuine CLI/imported sessions keep their 200 read-only stub. Client self-heal now also covers mid-session sidecar deletion of the current session. |

### Review fix absorbed (Codex CORE catch)
The first cut collapsed `source_tag or raw_source or session_source or ""`, defaulting a **blank-source** row to WebUI — which would wrongly 404 a **legacy CLI/imported** session that carries `is_cli_session:true` with blank source fields. Now classified **per-field**: any `webui`/`fork` → 404; any explicit non-WebUI source → keep the 200 CLI stub; all-blank → 404 only when NOT `is_cli_session` and NOT `read_only`. + 2 regression tests.

### Gate
- Full pytest suite: **7555 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN · 10 stale-session-restore tests (incl. 2 Codex-catch regressions)
- Codex (regression): CORE legacy-CLI false-404 → per-field fix → **SAFE TO SHIP**

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-03 19:41:22 -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
772a5c17ed
Release v0.51.246 — Release HN (stage-q18) (#3513)
## Release v0.51.246 — Release HN (stage-q18)

Backend bug-fix.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #3225 | @rodboev | **WebUI session rename now syncs the new title to the agent's `state.db`**, so the TUI/CLI stop showing the stale name. `/api/session/rename` now calls `_sync_session_title_to_insights(s)` after `s.save()` and before `publish_session_list_changed` — exactly mirroring the sibling `/api/session/title/regenerate` handler. Gated on the `sync_to_insights` setting and exception-contained (a sync failure can't break the rename). |

### Gate
- Full pytest suite: **7544 passed, 0 failed**
- ruff: CLEAN · 1 new regression test (call present + sync-before-publish ordering) + 82 rename/title-sync tests pass
- Codex (regression): **SAFE TO SHIP** — mirrors the regenerate handler (sync after lock release, gated, exception-contained), no deadlock, no stale data

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-03 18:56:24 -07:00
nesquena-hermes
a7f60689a8
Release v0.51.243 — Release HK (stage-q15) (#3507)
## Release v0.51.243 — Release HK (stage-q15)

UX-approved direction via Telegram (workspace feature you named). Backend security hardened through 3 Codex rounds.

### Added
| PR | Author | Feature |
|----|--------|---------|
| #3402 / #3422 | @pamnard | **Drag a file or folder in the workspace tree onto another folder row (or breadcrumb segment) to move it** within the workspace. New `POST /api/file/move`. Drop handlers use `stopPropagation` so the composer `@path` drag (#1097) and OS-file upload-drop (#3411) are unchanged. |

### Verified
- Live end-to-end: `notes.txt` → `docs/` confirmed on disk; legit moves return the correct `new_path`.
- 13 tests (incl. folder-into-self/descendant guard, existing-target collision, and 3 security regressions).

### Security hardening absorbed (3 Codex rounds — all fixed + regression-tested)
1. **TOCTOU symlink race on destination**: a path-based `source.rename(dest)` could be raced by swapping `dest_dir` to an external symlink between validation and rename. Now opens both parent dirs via the workspace-anchored `open_anchored_fd` (openat + `O_NOFOLLOW`, same helper as the upload hardening) and uses `os.rename(leaf, leaf, src_dir_fd=…, dst_dir_fd=…)` with an fd-based collision check; path-based fallback only where `dir_fd` is unsupported.
2. **Symlinked workspace root** returned a confusing 400 after a successful move — returned `new_path` now computed against `ws_root.resolve()`.
3. **Symlinked source entry**: `safe_resolve` follows the final symlink, so moving `link.txt` would move its *target* and dangle the link — now rejected via no-follow `lstat` on the lexical path.

### Gate
- Full pytest suite: **7530 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES ×3 → all applied + tested → **SAFE TO SHIP**

Co-authored-by: pamnard <pamnard@users.noreply.github.com>
2026-06-03 17:41:11 -07:00
nesquena-hermes
c0d86db5a8
Release v0.51.242 — Release HJ (stage-q14) (#3500)
## Release v0.51.242 — Release HJ (stage-q14)

UX-approved via Telegram (Nathan — dark/light/mobile screenshots).

### Added
| PR | Author | Feature |
|----|--------|---------|
| #3440 | @t3chn0pr13st | **Graphite appearance skin** — a quiet, neutral-gray "workbench" alternative to the default gold/cream. Selectable from Settings → Appearance and `/theme skin graphite`. Both light + dark palettes on the existing CSS-variable token system; tightened typography, shadows, active-sidebar spacing, code-block framing. |

### Why it's safe
- **Fully scoped + additive**: every new CSS rule (and every `!important`) is under `[data-skin="graphite"]` — Codex verified zero bleed into the default appearance or other skins. `api/config.py` keeps the default skin as `default` and only *adds* `graphite` to the allowed set. No i18n keys dropped (only the `/theme` help string gains `graphite`).
- Opt-in; a user has to select it. Default experience unchanged.

### Test-robustness fix (absorbed)
The new graphite scoped selectors (e.g. `:root[data-skin="graphite"] .session-item.active .session-time{…}`) appear in `style.css` *before* the canonical unscoped rules, which broke 3 naive first-occurrence CSS-contract tests (`test_issue677` scroll-btn-overlay, `test_issue856_pinned_indicator_layout`, `test_workspace_panel_session_list`). Fixed those 3 to anchor on the canonical **unscoped** rule (start-of-line regex) instead of the first `.selector` match — robust against this and future skins. Verified they still pass on clean master CSS (invariant preserved, not weakened).

### Gate
- Full pytest suite: **7517 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint runtime gate: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** (verified all CSS scoped, default skin unchanged, no i18n key loss)
- Vision-verified dark + light + mobile; UX-approved by Nathan via Telegram

Co-authored-by: t3chn0pr13st <t3chn0pr13st@users.noreply.github.com>
2026-06-03 16:46:53 -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
6f68fdb6ff
Release v0.51.236 — Release HD (stage-q7) (#3491)
## Release v0.51.236 — Release HD (stage-q7)

First Phase 3 (deep-review) release — picked by the 3-factor framework (contributor × impact × mitigated-risk): high-impact (#1952 native Windows support), backend-only (no screenshots), well-mitigated risk (POSIX path provably unchanged), from a contributor active this session (@rodboev, #3446/#3486 shipped earlier today).

### Added
| PR | Author | Fix |
|----|--------|-----|
| #1952 | @rodboev | Native Windows support for `bootstrap.py` + the embedded terminal: POSIX-only `fcntl`/`termios`/`select` guarded behind `_TERMINAL_SUPPORTED`; terminal entry points raise `NotImplementedError`/no-op on Windows; bootstrap Windows block → warning; auto-install errors clearly on native Windows (WSL unaffected); foreground uses `Popen`+exit on Windows instead of `os.execv`. **POSIX behavior unchanged on every path.** |

### Absorbed on the way in (fix-it-ourselves, reviewed fresh)
- `subprocess.CREATE_NEW_PROCESS_GROUP` → `getattr(subprocess, ..., 0)` — the constant is Windows-only, so a win32-simulating test `AttributeError`'d on Linux. Mirrors the `SO_EXCLUSIVEADDRUSE` getattr guard.
- Fixed 2 over-reaching tests in `test_windows_native_support.py` — one was launching a **real installer subprocess** via an unstubbed `subprocess.run` (now stubbed; harness 2.8s vs 80s); removed unused imports.
- Updated `test_onboarding_static.py` — it asserted the OLD "Native Windows is not supported" hard-block string this PR intentionally replaces; now asserts the new experimental-warning + auto-install guard.
- Help-text accuracy: `--foreground` help now describes the Windows Popen path (Opus nit).

### Gate results
- **Full pytest suite**: 7478 passed, 9 skipped, 3 xpassed, **0 failed**
- **ruff forward gate**: CLEAN
- **browser-smoke gate**: CLEAN (gate hardened mid-release to auto-detect the cached chromium revision)
- **Codex (regression)**: SAFE TO SHIP (simulated `sys.platform=win32`, verified POSIX modules not imported + all terminal guards complete + POSIX foreground still uses execv)
- **Opus (correctness)**: SAFE TO SHIP (POSIX path provably unchanged, all fcntl/termios/select guarded, Popen+exit correct; noted inherent-Windows trade-offs that aren't PR bugs)

Note: the Windows *runtime* path can't be executed on the Linux CI box; it was reviewed statically by both reviewers + the contributor's 209-line test (win32 simulated via monkeypatch). Linux/POSIX no-regression is fully verified.

Closes #1952.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-03 10:46:06 -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
ed46c65719
Release v0.51.234 — Release HB (stage-q4) (#3488)
## Release v0.51.234 — Release HB (stage-q4)

Two medium-risk backend/infra fixes. All gates green.

### Fixes
| PR | Author | Fix |
|----|--------|-----|
| #3289 | @rodboev | Refuse server startup when a live instance already serves the port (Windows/macOS silent port-sharing hazard). Live-listener probe (`GET /health`, 2s timeout) + Windows `SO_EXCLUSIVEADDRUSE` — **preserves fast restart** (POSIX keeps `allow_reuse_address=True`; a dying socket in the kernel backlog times out → startup proceeds). |
| #3486 | @dso2ng | Allow remote/SSH terminal profiles to use target-side workspace paths under `terminal.cwd` without a server-local `stat()`. Local profiles unchanged — bypass only fires for remote backends and only for paths contained within `terminal.cwd`. |

### History note on #3289
This PR was **held earlier this sweep** — its original form globally disabled `SO_REUSEADDR`, which a Codex gate flagged as breaking fast restart (TIME_WAIT bricks rebind for ~60s). The contributor reworked it along the suggested lines (live-listener probe instead of the global disable). This release ships the reworked version. Unheld → full pickup → full gate.

### Gate results
- **Full pytest suite**: 7458 passed, 8 skipped, 3 xpassed, **0 failed**
- **ruff forward gate**: CLEAN
- **browser-smoke gate**: CLEAN (real server boots fine with the new startup probe)
- **Codex (regression)**: SAFE TO SHIP (verified fast-rebind preserved + remote bypass gated on backend+containment, local validation unchanged)
- **Opus (correctness + security)**: SAFE TO SHIP (probe false-positive, `_is_within` containment, local-profile bypass all hold up; applied its one minor double-call cleanup note)

Closes #3289.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: dso2ng <dso2ng@users.noreply.github.com>
2026-06-03 09:53:32 -07:00
nesquena-hermes
7b02883dcf
Release v0.51.233 — Release HA (stage-q3) (#3487)
## Release v0.51.233 — Release HA (stage-q3)

Single high-impact data-integrity fix.

### Fix
| PR | Author | Fix |
|----|--------|-----|
| #3472 | @Mubashirrrr | Guard `/api/session/truncate` `keep_count` against a **silent persisted transcript wipe** (negative value sliced as `messages[:-N]`, deleting the newest N messages and saving to disk) and an HTTP 500 (non-numeric). Now validates before the destructive slice — non-int → 400, negative → 400 — mirroring the existing `/api/session/branch` guard. `keep_count=0` "clear all" semantics preserved. |

### Gate results
- **Full pytest suite**: 7450 passed, 7 skipped, 3 xpassed, **0 failed**
- **ruff forward gate**: CLEAN
- **browser-smoke gate**: CLEAN (`/`, `/#settings`, `/#sessions` — zero console errors)
- **Codex (regression)**: SAFE TO SHIP (probed the route directly — negative/non-numeric → 400 without mutation; `keep_count=2`/`0` still work)
- **Opus (correctness)**: SAFE TO SHIP (guard placed before slice + save; both front-end callers compute from a non-negative DOM index)

Co-authored-by: Mubashirrrr <Mubashirrrr@users.noreply.github.com>
2026-06-03 09:34:31 -07:00
nesquena-hermes
89b034b925
Release v0.51.232 — Release GZ (stage-q2) (#3485)
## Release v0.51.232 — Release GZ (stage-q2)

Two low-risk fixes batched into one release. All gates green.

### Fixes
| PR | Author | Fix |
|----|--------|-----|
| #3473 | @Mubashirrrr | Guard malformed/negative numeric query params on the cron endpoints (`/api/crons/output`, `/api/crons/recent`) — no more 500 on `?limit=abc`, no oldest-entry drop on a negative limit |
| #3480 | @koshikai | Translate 80 previously-untranslated Japanese (`ja`) locale strings (MCP controls, tool summaries, toasts) — interpolation placeholders preserved, no keys added/removed |

### Gate results
- **Full pytest suite**: 7446 passed, 7 skipped, 3 xpassed, **0 failed**
- **ESLint runtime gate**: CLEAN
- **ruff forward gate**: CLEAN
- **browser-smoke gate**: CLEAN (`/`, `/#settings`, `/#sessions` — zero console errors)
- **Codex (regression)**: SAFE TO SHIP (flagged a CHANGELOG endpoint-path typo `/api/cron/` → `/api/crons/`, fixed)
- **Opus (correctness)**: SAFE TO SHIP (flagged an inline-comment imprecision about which entries a negative slice drops — fixed in both the comment and CHANGELOG)

Co-authored-by: Mubashirrrr <Mubashirrrr@users.noreply.github.com>
Co-authored-by: koshikai <koshikai@users.noreply.github.com>
2026-06-03 09:24:51 -07:00
nesquena-hermes
0b0a179e4c
Release v0.51.231 — Release GY (stage-q1) (#3484)
## Release v0.51.231 — Release GY (stage-q1)

Four independent, low-risk fixes batched into one release. All gates green.

### Fixes
| PR | Author | Fix |
|----|--------|-----|
| #3368 | @nesquena-hermes | `/model` resolves models from the truncated `extra_models` tail of a large provider catalog (completes the half-done #3368 fix; @garyd9's `mimo-v2.5` case) |
| #3457 | @pix0127 | Auto-hide the Plugins settings tab when no plugins are installed; deep-link fallback to Conversation |
| #3474 | @Mubashirrrr | Guard malformed/negative `depth` on `/api/sessions/search` (no more 500 on `?depth=deep`, no silent newest-message drop on negative) |
| #3433 | @sjh9714 | Resolve symlinked home before the trust comparison so `~/` workspace suggestions work when the process home is a symlink |

### Note on #3457
The original contributor patch reassigned a `const section` (a **runtime `TypeError` brick** on the settings panel) and placed the hidden-tab fallback *after* the panes were already toggled. Refactored to `let` + hoist the fallback above all consumers of `section`. ESLint runtime gate now clean.

### Gate results
- **Full pytest suite**: 7441 passed, 7 skipped, 3 xpassed, **0 failed**
- **ESLint runtime gate**: CLEAN (caught + fixed the #3457 const-reassign)
- **ruff forward gate**: CLEAN (no new violations on changed lines)
- **browser-smoke gate**: CLEAN (`/`, `/#settings`, `/#sessions` — zero console errors)
- **Codex (regression)**: SAFE TO SHIP
- **Opus (correctness)**: SAFE TO SHIP

Closes #3368. Closes #3457. Closes #3433.

Co-authored-by: pix0127 <pix0127@users.noreply.github.com>
Co-authored-by: Mubashirrrr <Mubashirrrr@users.noreply.github.com>
Co-authored-by: sjh9714 <sjh9714@users.noreply.github.com>
2026-06-03 09:13:01 -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
9374e66855
Release v0.51.228 — Release GV (stage-p12 — workspace tree-drop #3411 + large-markdown preview #3378) (#3463)
* Release v0.51.228 (stage-p12): workspace tree-drop #3411 + large-markdown preview #3378

#3411 (@pamnard): stopPropagation on workspace file-tree OS-file dragenter/dragover/drop
so a tree drop uploads to the workspace WITHOUT also attaching to the composer.
#3378 (@starGazerK): raise md rich-render ceiling 64KB/1500L->256KB/5000L + backend
file read 200KB->400KB, add 'Render as markdown anyway' force button (reuses cached
raw content, no extra fetch).

* fix(workspace): force-render uses fresh path-scoped cache, blocked while dirty (#3378 Codex follow-up)

Codex review of #3378 found the markdown force-render path had two SILENT bugs:
(1) saving a md file from the plain-text fallback didn't update _previewRawContent,
so a later force-render showed stale pre-save content; (2) the cache-reuse check
'path===_previewCurrentPath' was tautological (var just assigned), so a force-render
after a file switch could render the previous file's cached content. Fixes: track
_previewRawContentPath (set on fetch AND save), reuse cache only when it matches the
requested path, and block force-render while the editor is dirty/open. +3 regression
tests. (#3411 was cleared clean by Codex.)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 20:58:22 -07:00
nesquena-hermes
0b5458f3da
Release v0.51.226 — Release GT (stage-p9 — mobile composer context-usage ring #3062 + activity-feed default-expand setting #3080) (#3459)
* feat(composer): replace mobile config-button sliders icon with context-usage ring (#3062, @NottheGuy007)

Squashed net diff of #3062 (the PR branch's tip commits were deletes of files not
present in our tree). Replaces the composerMobileCtxBadge text badge with an SVG
progress ring (ctx-arc + ctx-num) showing real-time context-window usage: ring
fill via stroke-dashoffset, centered percentage, color-coded green<=50%
orange<=85% red>85%. Ring resets to 0%/green on new session.

* feat(activity): add 'expand activity feed by default' appearance setting (#3080, @AJV20)

Squashed net diff of #3080. Adds a Settings -> Appearance checkbox
(activity_feed_expanded_default, default off) to expand new Activity disclosures
by default; preserves manual per-turn collapse/expand (explicit toggle still
wins); live 'Waiting on model' rows explain what the agent is doing before/after
tool calls. i18n keys for all locales.

* docs(changelog): v0.51.226 — context-usage ring (#3062) + activity-feed default-expand setting (#3080)

* test(mobile): update touch-target test for #3062 ring (badge removed)

#3062 replaced the composerMobileCtxBadge text badge with the SVG context-usage
ring (composerMobileCtxRing) but left 3 stale references in tests/test_mobile_layout.py.
The full suite caught test_mobile_composer_primary_controls_keep_touch_friendly_sizing
asserting the removed .composer-mobile-ctx-badge CSS rule + #composerMobileCtxBadge
element. Updated the assertion to the new ring: confirm composerMobileCtxRing exists,
the old badge is fully gone (not dangling), and the ring SVG is aria-hidden so it
stays a decorative overlay that doesn't steal the config button's 44px touch target
(which is still asserted via .composer-mobile-config-btn above). 56/56 mobile-layout
tests pass. Codex: no production JS dereferences the removed badge.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 19:48:01 -07:00
nesquena-hermes
ec46f05c92
Release v0.51.225 — Release GS (stage-p7 — remote gateway health probe resolves gateway_state #3355) (#3458)
* fix(health): probe /health/detailed first and unify gateway env vars (#3355)

* docs(changelog): v0.51.225 — remote gateway health probe gateway_state fix (#3355)

* fix(health): normalize gateway URL health-suffix + cap remote body read (#3355 Codex follow-up)

Codex review of #3355 found two issues:
(1) SILENT — a gateway env var already pointing at a health path (e.g.
    GATEWAY_HEALTH_URL=http://host/health) produced doubled paths like
    /health/health/detailed once probe paths were appended. Now strip a trailing
    /health/detailed, /health, /v1/health, /status suffix before appending
    (mirrors api/updates.py).
(2) CORE — the new resp.read() on a 2xx body was unbounded; a large/trickled
    remote response could hang /api/health/agent or balloon memory. Cap the read
    at _REMOTE_PROBE_BODY_LIMIT_BYTES (64KB)+1 and skip JSON parse when over cap
    (still report the gateway alive, just without parsed gateway_state).

Adds regression tests for both (no doubled /health/health path; oversized body
is capped + does not hang). Also updated _FakeResp.read to accept the size arg.

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 18:56:25 -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