Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ff0830d
fix(ui): smooth iPhone PWA bottom-edge bounce in chat
dobby-d-elf May 12, 2026
245288c
fix: bucket long-range daily token charts
Michaelyklam May 12, 2026
a41b4d5
fix: stack analytics usage cards on mobile
Michaelyklam May 12, 2026
7a16d09
fix: add Portuguese session management i18n
Michaelyklam May 12, 2026
2654967
docs: clarify compression anchor helpers
Michaelyklam May 12, 2026
8fa92c6
Fix manual compression proxy timeouts
May 12, 2026
dd543e4
fix: purge missing inflight sessions
Michaelyklam May 12, 2026
f1b2a21
feat: lazy-load full lineage segments
dso2ng May 12, 2026
442f01b
docs: document turn journal fsync tradeoff
Michaelyklam May 12, 2026
e03c197
fix: recover from stale deleted workspaces
dobby-d-elf May 12, 2026
b7c5ba6
Fix custom live model scoping
May 12, 2026
b718220
Fix login health probe credentials
May 12, 2026
f5f59a5
fix: audit turn journal terminal collisions
Michaelyklam May 12, 2026
516d942
refactor: reduce stale workspace recovery fix
dobby-d-elf May 12, 2026
b3f8bee
Fix settings system mobile version wrapping
May 12, 2026
76e611d
Preserve fallback provider credential hints
May 12, 2026
c5bad3e
i18n: add French (fr) locale
legeantbleu May 12, 2026
099fdaf
fix(ui): stabilize chat bottom scrolling on iPhone PWA
dobby-d-elf May 12, 2026
890d65f
Merge pull request #2132 into stage-344
May 12, 2026
effd332
Merge pull request #2123 into stage-344
May 12, 2026
76cf06a
Merge pull request #2137 into stage-344
May 12, 2026
45ac810
Merge pull request #2141 into stage-344
May 12, 2026
5603215
Merge pull request #2143 into stage-344
May 12, 2026
2cccb8a
Merge pull request #2120 into stage-344
May 12, 2026
13c3646
Merge pull request #2121 into stage-344
May 12, 2026
8520755
Merge pull request #2130 into stage-344
May 12, 2026
4c5a246
Merge pull request #2125 into stage-344
May 12, 2026
a06952a
Merge pull request #2140 into stage-344
May 12, 2026
8dd0b4e
Merge pull request #2139 into stage-344
May 12, 2026
23425b2
Merge pull request #2129 into stage-344
May 12, 2026
aa85bd2
Merge pull request #2138 into stage-344
May 12, 2026
1ee8627
Merge pull request #2135 into stage-344
May 12, 2026
c677c19
Merge pull request #2128 into stage-344
May 12, 2026
9c7eb42
Merge pull request #2142 into stage-344
May 12, 2026
7116c68
stage-344: maintainer fix for #2142 fr locale — add LOCALES tuple ent…
May 12, 2026
4ab6cd6
docs: CHANGELOG stage-344 — close v0.51.50, open Unreleased for 16-PR…
May 12, 2026
2def05f
stage-344: apply Opus SHOULD-FIX #1+#2 — #2128 multi-tab race + stale…
May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,48 @@

## [Unreleased]

### Added

- **PR #2130** by @dso2ng — Lazy lineage-report fetch on sidebar segment-badge expand. The sidebar already showed `N segments` for collapsed compression lineage rows (refs #1906, #1943) and the backend report endpoint is now shipped (refs #2012), but some rows only had a backend `_compression_segment_count` from `/api/sessions` while the browser hadn't materialized the older segment rows — clicking the badge couldn't reveal the full bounded list. Adds a small per-sidebar-cache lineage-report cache/inflight map in `static/sessions.js`, invalidates it on each fresh `/api/sessions` refresh, and on expand fetches `GET /api/session/lineage/report?session_id=<tip>` only when `_sessionSegmentCount(s)` exceeds the locally-materialized `_lineage_segments` count. Merges returned report `segments` by `session_id` with existing client segments, skipping the visible tip and `child_session` rows. Leaves report `children` out of the compression-segment list so subagent/fork child semantics remain separate. 132-line regression suite covering fetch-needed detection, report-segment merging/dedup, endpoint construction, and inflight cache de-duping.

- **PR #2142** by @legeantbleu — French (`fr`) locale. ~938 UI strings translated via Google Translate then sanitized for JS string escaping. Inserted at the end of `static/i18n.js`'s `LOCALES` map (insertion-order convention used by every locale since `it` landed). Stage-344 maintainer fix added the matching tuple entries in `tests/test_issue1488_composer_voice_buttons.py:TestComposerVoiceButtonI18n.LOCALES` + sibling `TestVoiceModePreferenceGate.LOCALES`, plus the matching `_LOGIN_LOCALE['fr']` block in `api/routes.py` so the login page localizes for French users (issue #1442 parity contract), plus an inverted `_resolve_login_locale_key('fr')` assertion in `tests/test_login_locale_parity.py` that previously assumed fr falls back to en. Mirrors the stage-340 fix for the `it` locale (PR #2067).

### Fixed

- **PR #2120** by @Michaelyklam (closes #2103) — Daily Tokens chart no longer overflows its card on 90/365 day ranges. Adds `_bucketDailyTokensForChart()` in `static/panels.js` that keeps ≤30 rows per-day and buckets longer ranges into summed chart rows (90→45 bars at 2-day buckets, 365→46 bars at 8-day buckets, ≤52 ceiling). Updates the Daily Tokens render loop to use bucketed chart rows, date-range labels, and summed tooltip values. Switched chart columns to shrink-safe `minmax(0,1fr)` so the bars stay inside the card. Backend `/api/insights` payload unchanged. 130-line regression suite covering short-range preservation, long-range bounding, label/title shape on bucketed rows, render-loop usage, and shrink-safe CSS.

- **PR #2121** by @Michaelyklam (refs #2104) — Token Breakdown + Models row stacks on mobile instead of forcing horizontal page overflow. New `insights-usage-grid` class wraps the row with a scoped `@media (max-width: 640px)` rule that flips it to `grid-template-columns: 1fr`. Contains remaining model-table overflow inside the card. 27-line regression suite covering the mobile breakpoint, single-column layout, contained `overflow-x`, and presence of the scoped rule.

- **PR #2123** by @Michaelyklam (closes #2112) — Portuguese (`pt`) locale parity: 5 missing session-management keys (bulk delete/archive, select mode, select all, selected count, no-selection text) added so Portuguese users stop silently falling back to English. Extended `tests/test_login_locale_parity.py` with a session-management key parity check across all locale blocks.

- **PR #2125** by @Michaelyklam (closes #2093) — Renamed `_patch_skill_home_modules` → `patch_skill_home_modules` in `api/profiles.py` since the helper is imported by streaming code and asserted by tests across modules. Updated streaming import/fallback/call sites in `api/streaming.py` and the env-lock regression test expectations. Expanded `api/compression_anchor.py`'s module docstring to explain manual vs automatic compression anchoring and `auto_compression=True` behavior. Documentation/rename-only — no runtime behavior change.

- **PR #2128** by @franksong2702 (closes #2087) — Manual `/compress` no longer fails behind reverse proxies that time out long synchronous requests. Adds `POST /api/session/compress/start` (start or reuse an in-process manual compression job keyed by `session_id`) + `GET /api/session/compress/status?session_id=...` (poll `running`/`done`/`error`/`idle`). Reuses the existing `_handle_session_compress` implementation inside the worker so the save path, provider resolution, sanitization, and the legacy synchronous endpoint stay aligned. Adds a stream-state guard before save so a compression worker can't overwrite a session that started another stream while compression was running. 10-minute cleanup for terminal job results, with successful `done` payloads released after first status consumption. `static/commands.js` `/compress` and `/compact` now start, poll, and apply the saved compressed session; session-load resume wiring picks up in-flight compression on page reload.

- **PR #2129** by @Michaelyklam (closes #2092) — `_purgeStaleInflightEntries()` now iterates `INFLIGHT` keys and explicitly drops ids absent from the current session list. Pre-fix the cleanup only removed entries for sessions still present in `_allSessions` and marked non-streaming, so deleted/archived/filtered-out sessions left ghost entries indefinitely. Preserves still-streaming sessions. 124-line regression suite covering absent/present-non-streaming/present-streaming cases.

- **PR #2135** by @franksong2702 (closes #2126, refs #2131) — `/api/models/live?provider=custom:<slug>` now only returns models from the requested named provider entry instead of every `custom_providers[].model`. Direct `/v1/models` fallback uses the matched named provider's `base_url`+`api_key` pair instead of the active profile's `model.base_url`/`model.api_key`. `custom:<slug>` reads only the matching named entry; bare `custom` reads only unnamed entries. Includes model IDs from both singular `model` and plural `models` config forms. Cache key behavior preserved (already provider-scoped). Regression coverage for named-provider scoping, bare-custom scoping, and direct fetch endpoint/key selection.

- **PR #2137** by @franksong2702 (closes #2122) — Login page health probe now sends `credentials: 'same-origin'` instead of `credentials: 'omit'`. Cloudflare Access and similar same-origin reverse proxies need the access cookie to reach the proxy, so the prior omit caused WebUI to falsely disable login before `/health` ever resolved. Keeps the health URL mount-relative (`health`) for subpath deployments. Static regression test pins same-origin credentials and forbids the omit variant.

- **PR #2138** by @dobby-d-elf — Live Hermes WebUI chats no longer get stuck with `Error: Path does not exist: ...` when the session points at a deleted workspace. Workspace fallback now looks up the live `DEFAULT_WORKSPACE` instead of using a stale import-time snapshot. Old sessions with deleted implicit workspaces are repaired to the current valid workspace during chat start, so the next send recovers instead of erroring. 71-line regression suite for both the stale-fallback and missing-session-workspace recovery paths.

- **PR #2139** by @Michaelyklam (refs #2097) — Turn-journal terminal-collision audit slice. `derive_turn_journal_states()` now returns `(states, terminal_collisions)`; collisions carry the `turn_id` plus terminal events in timestamp order when a turn records more than one terminal event (completed + interrupted both fire). Latest-by-timestamp derived state behavior preserved for existing callers; session recovery audit and existing tests updated to unpack the new tuple. Audit-only: no multi-process append safety in this PR.

- **PR #2140** by @franksong2702 (closes #2133) — WebUI fallback activation now passes `api_key` and `key_env` in the normalized fallback entry to `AIAgent`, matching what the CLI path preserves. Hermes Agent fallback resolution already knew how to use these — WebUI was dropping them, leaving env-backed fallback providers unauthenticated after a primary provider 401. Legacy single-dict `fallback_model` and list-form `fallback_providers` selection behavior unchanged.

- **PR #2141** by @franksong2702 (closes #2102) — Settings → System header no longer clips off the right edge on phones. Section header now stacks vertically under the existing Settings mobile breakpoint; the System update/version control group wraps to use available width; individual version badges keep their text intact while the group wraps. CSS-only change inside the existing breakpoint scope. Mobile layout static regression added.

- **PR #2143** by @dobby-d-elf — iPhone PWA chat bottom-scroll stutter fixed. Removed the Start/End scroll controls from the transcript scroll layout — they were sticky children inside `#messages`, which on iOS momentum/elastic scrolling perturbed the scroll surface at the bottom boundary. Now the transcript is wrapped in a `.messages-shell` and the controls render as absolute overlays outside `#messages`, so `#messages` is back to a plain native scrolling container. Adds a small visibility dead zone for the down-arrow button so elastic bottom pulls don't flash the button while already at the bottom.

- **PR #2132** by @Michaelyklam (refs #2096) — Docs-only: added `Synchronous durability design rationale` to `docs/rfcs/turn-journal.md`. Documents why submitted-event journaling stays synchronously fsync-backed today, qualitative fsync latency expectations for SSD/HDD/Docker-overlay filesystems, and maintainer benchmark guidance for measuring p50/p95/p99 append/fsync latency before any future async lifecycle journaling.

### Stage-344 maintainer fixes

- **`api/routes.py:_handle_session_compress_start/status` (#2128 polish)** — Opus SHOULD-FIX from stage-344 review. Two related UX bugs in the new async manual-compression flow: (1) `compress/status` popped the `done` job entry on first read, which left a second open tab with `{status:"idle"}` and a "Compression job is no longer available" toast — fixed by letting the existing 10-minute TTL handle eviction so all tabs see the same terminal payload; (2) re-invoking `compress/start` within the 10-minute TTL returned the stale prior `done` payload instead of running a new compression — fixed by always dropping the existing entry and starting a fresh worker, so a user closing a tab mid-compress and re-running `/compress` on a fresh open gets a new result. Both are 1-block tweaks; existing `tests/test_sprint46.py` 10/10 still passes. The third Opus SHOULD-FIX (#2135 `cfg["model"]` fallback when `provider=custom:X` doesn't match any entry) is deferred to a follow-up — it's strictly no-worse-than-master behavior, but worth tightening to skip the URL probe when no entry matched.

## [v0.51.50] — 2026-05-12 — Release Z (stage-343 — single-PR — ctl.sh bash 3.2 macOS compat fix + regression test suite)

### Fixed

- **PR #2117** by @ayushere — `ctl.sh start` no longer crashes on macOS (bash 3.2) with `preserved[@]: unbound variable`. The dotenv-preserve loop in `_load_repo_dotenv_preserving_env()` iterated `"${preserved[@]}"` under `set -euo pipefail`, which bash 4+ silently allows on empty arrays but bash 3.2 (still the default `/usr/bin/bash` on macOS) treats as an unbound-variable error. Guards the iteration with `if [[ ${#preserved[@]} -gt 0 ]]; then ... fi` — matches the canonical bash 3.2 strict-mode pattern. This is the third bash 3.2 compat fix to land in `ctl.sh` (prior: `025f137f` guarded `CTL_BOOTSTRAP_ARGS[@]` with the `${arr[@]+...}` pass-through pattern, `630981a0` replaced `[[ -v ${key} ]]` with `[[ -n "${!key+x}" ]]`). Defense-in-depth: added `tests/test_ctl_bash32_compat.py` (5 static-pattern regressions) pinning both empty-array guards plus a denylist for bash 4+ syntax (`declare -A`, `mapfile`, `[[ -v ]]`, `${var^^}`, `${var,,}`) so the next regression surfaces in CI instead of a macOS user's terminal. Stage-343 reviewer added the regression-test file alongside the contributor's 5-LOC fix to ctl.sh.
Expand Down
31 changes: 31 additions & 0 deletions api/compression_anchor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
"""
Shared helpers for session compression anchor metadata.

Manual compression anchoring versus automatic compression paths
===============================================================

When ``auto_compression=True`` is passed to ``visible_messages_for_anchor()``,
the function accepts a broader set of message content types (including
provider-style ``input_text`` / ``output_text`` parts) and metadata markers
(``reasoning``, ``thinking``, etc.) from any non-tool role. This enables the
streaming auto-compression path to determine which messages should anchor
compression UI metadata without being limited to the legacy manual-compression
rules.

When ``auto_compression=False`` (the default), the function applies the
historical manual-compression rules: only plain ``text`` content parts from
non-assistant roles are counted.

Why this module exists
======================

Compression anchoring needs to identify which messages in a session transcript
are semantically significant enough to seed the compression UI metadata (e.g.,
message count, token budget display). The original implementation hard-coded
these rules in multiple places. This module consolidates the logic so that:

1. Manual compression anchoring (CLI/legacy path) uses the stricter ruleset.
2. Automatic compression (streaming/agent path) can leverage the relaxed ruleset
when it knows it is handling provider-style messages.

Callers specify ``auto_compression=True`` when the messages may originate from
an automatic/compression-aware pipeline, and ``False`` (default) for manual
compression contexts.
"""


Expand Down
4 changes: 2 additions & 2 deletions api/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
_SKILL_HOME_MODULES = ("tools.skills_tool", "tools.skill_manager_tool")


def _patch_skill_home_modules(home: Path) -> None:
def patch_skill_home_modules(home: Path) -> None:
"""Patch imported skill modules that cache HERMES_HOME at import time."""
for module_name in _SKILL_HOME_MODULES:
module = sys.modules.get(module_name)
Expand Down Expand Up @@ -628,7 +628,7 @@ def _set_hermes_home(home: Path):
"""Set HERMES_HOME env var and monkey-patch cached module-level paths."""
os.environ['HERMES_HOME'] = str(home)

_patch_skill_home_modules(home)
patch_skill_home_modules(home)

# Patch cron/jobs module-level cache
try:
Expand Down
Loading
Loading