Commit Graph
1477 Commits
Author SHA1 Message Date
3a8a51e507 Release v0.51.304 — Release JT (stage-p2a — un-held terminal reaper + opt-in Docker GPU) (#3757)
* fix(terminal): reap reparented terminal descendants by process group (#3725, #2577)

Embedded-terminal descendants reparented to the WebUI process could linger as
zombies. The reaper now calls os.waitpid(-terminal_pgid, WNOHANG) scoped to the
terminal's own process group (terminals spawn with start_new_session=True, so
proc.pid == pgid) rather than process-wide waitpid(-1), which would otherwise
reap unrelated WebUI subprocess children and silently coerce their exit codes to
0. Bounded by a 64-iteration limit and lock-guarded. Runs on reader cleanup and
terminal close.

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

* docs(docker): add opt-in GPU runtime image path (#3721, #3243)

The default image stays CPU-only. A new INSTALL_GPU_LIBS=1 build arg installs
VA-API user-space libraries for users passing through host GPU devices, and
docker_init.bash preserves Docker --group-add supplemental groups (e.g. render/
video for /dev/dri) when dropping privileges to the runtime user. Default
(INSTALL_GPU_LIBS=0) is a no-op. Docs + regression test included.

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

* docs(changelog): stamp v0.51.304 — Release JT (stage-p2a #3725 #3721)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-06 18:04:16 -07:00
4580f58496 Release v0.51.303 — Release JS (stage-p1a — cron toggle + config var expansion + git-discard hardening) (#3756)
* fix(cron): toggle run output rows instead of re-fetching when already open (#3732)

_loadRunContent() only ever expanded, so clicking an already-open cron run row
re-fetched its content pointlessly. It now toggles: an open row collapses (clears
the expansion state + resets the toggle button) and returns early, avoiding the
redundant API call.

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

* feat(config): expand ${VAR} references in config.yaml at load time (#3736)

hermes-agent already expands ${ENV_VAR} in config.yaml, but the WebUI's own
loader stored the raw dict, leaving literal ${...} strings. Recursively expand
${VAR} against os.environ on both config load paths (reload_config and
_load_yaml_config_file); unset vars are left untouched (${VAR} preserved).

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

* fix(security): anchor untracked-file deletes in git_discard (#3702)

git_discard(delete_untracked=True) used raw shutil.rmtree / Path.unlink after a
separate safe_resolve_ws validation, leaving a validation-to-use symlink-swap
window. Route untracked deletes through the anchored helpers (rmtree_anchored /
unlink_anchored) so a swapped path component is rejected at delete time; preserve
the prior missing_ok tolerance for benign concurrent-removal races. Adds
regression coverage for both the symlink-swap block and the concurrent-missing case.

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

* docs(changelog): stamp v0.51.303 — Release JS (stage-p1a #3732 #3736 #3702)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
Co-authored-by: Carry00 <Carry00@users.noreply.github.com>
Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
2026-06-06 17:24:18 -07:00
bf088cbbc4 Release v0.51.302 — Release JR (stage-brick — mobile/iOS brick + large-session perf hotfixes) (#3754)
* fix(ui): stop hidden toast from intercepting clicks on mobile (#3735)

The .toast container kept pointer-events:auto while hidden (opacity:0), so its
fixed padding sat over mobile profile action buttons and ate their clicks. Set
pointer-events:none when hidden; restore auto on .toast.show.

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

* fix(sessions): rename saves on blur so iOS Safari rename works (#3729)

iOS Safari has no Enter key; the keyboard 'Done' button fires blur, and the old
onblur=cancel discarded the rename. Flip blur to save (Escape still cancels) for
session rename and project create/rename, with a _finishDone guard to prevent a
double-fire between blur and the API callback.

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

* perf(session): skip fuzzy dedup matching for giant merge payloads (#3730)

Large tool/log payloads made _matching_visible_duplicate() casefold+regex-tokenize
multi-megabyte contents on every visible key, so /api/session took 10s+ and blocked
/api/sessions for ~19s. Keep loose normalization lazy+cached and skip substring/fuzzy
matching for non-exact payloads >200KB; exact visible-key matches still short-circuit.

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

* docs(changelog): stamp v0.51.302 — Release JR (stage-brick brick/perf hotfixes #3735 #3729 #3730)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: timlawrenz <timlawrenz@users.noreply.github.com>
Co-authored-by: reinocheong <reinocheong@users.noreply.github.com>
Co-authored-by: alvistar <alvistar@users.noreply.github.com>
2026-06-06 16:58:13 -07:00
nesquena-hermesandnesquena-hermes 1cb9d3b550 fix(#3717): context-length indicator honors provider per-model overrides (#3726) (#3752)
@rodboev. providers.<name>.models.<model>.context_length overrides (standard provider,
no base_url) were invisible to the session context resolver → wrong window shown/persisted,
could trip auto-compression at the wrong threshold. New _context_length_lookup_inputs_for_model
helper resolves provider config / base_url / custom_providers across route-load, session-save,
and SSE-usage paths; provider-scoped overrides match by provider name and forward as
config_context_length (returned before any base-url-gated probe).

Maintainer pre-merge items both already satisfied in PR head: no-base_url regression test
(test_route_resolver_uses_provider_model_context_length_without_base_url) present; session-save
_cfg_base_url assigned before the helper call (safe-bound, no NameError on TypeError fallback).
Verified api code byte-identical to PR head; 14 context-length tests pass. + CHANGELOG v0.51.300.

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 15:57:09 -07:00
nesquena-hermesandnesquena-hermes ce4c2c1de1 Release v0.51.298 — stage-3719 (live model probe for custom providers with model config #3719) (#3747)
* fix(#3718): /api/models/live probes upstream for custom providers with model config (#3719)

@DanielMaly. Config model IDs were added to the ids list before the 'if not ids:' guard,
so a custom provider with a model: field skipped the live /v1/models probe and Settings'
refresh returned only the config entry. Now collects config IDs separately, always probes
for custom providers, merges live (priority) + config (fallback). Includes the maintainer
review follow-ups (CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant + behavioral tests).

Captured all 3 logical PR commits' net effect; routes.py + test verified byte-identical
to the PR head. + CHANGELOG v0.51.298.

* test(#3718): remove unused BytesIO import (ruff F401)

* test(#3719): update timeout assertion to CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS

The #3719 maintainer-review commit replaced the hardcoded urlopen timeout=8 with the
CUSTOM_MODELS_ENDPOINT_TIMEOUT_SECONDS constant (5.0). test_named_custom_live_fetch_uses_matching_entry_endpoint
asserted the old literal 8. Reference the constant directly now so the assertion can't
drift again. Not a behavior change — only the live-probe timeout value (8s -> 5s) moved,
URL + auth unchanged.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 14:48:39 -07:00
4f842d5095 Release v0.51.297 — stage-3711 (terminal remote-backend guard #3711) (#3745)
* fix(terminal): guard embedded terminal on remote backends (#3673)

* fix(terminal): add missing remote-backend locale key

* fix(terminal): add missing remote-backend locale coverage (#3673)

* docs(changelog): v0.51.297 — terminal remote-backend guard (#3711) only

Dropped #3725 (descendant reaper) from this stage: Codex caught a SILENT exit-code
clobber — its process-wide os.waitpid(-1, WNOHANG) can reap a sibling WebUI child that
another subsystem is waiting on, coercing that child's returncode to 0 (failures become
successes). Held for the contributor to scope the reaper to terminal PGIDs
(os.waitpid(-term.proc.pid, WNOHANG)) or a terminal-PGID registry.

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 14:17:17 -07:00
91a89fb5d5 Release v0.51.296 — stage-3731 (remote-workspace blocked-root security fix #3731) (#3744)
* fix: reject blocked roots for remote workspaces

* test: cover remote blocked root subpaths

* docs(changelog): v0.51.296 security fix + backfill v0.51.295 entries

- v0.51.296: #3731 remote-workspace blocked-root rejection.
- Backfill the v0.51.295 release block (the #3739 model-pick entry + promote the
  #3570 revert out of [Unreleased]) which a stage-rebuild dropped from the prior
  release's CHANGELOG. git-describe versioning makes CHANGELOG-after-tag acceptable.

---------

Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 13:49:59 -07:00
65c4bc9fa2 Release v0.51.295 — stage-3739/3742 (model-pick revert fix #3739 + session-status revert #3742) (#3743)
* fix: honor explicit model pick, suppress silent revert on cross-family selection (#3737)

When a user changes the model in the composer dropdown and sends,
_resolve_compatible_session_model_state previously had no way to
distinguish an explicit user pick from stale session state. The
profile-aware branch (v0.51.290, PR #3448) and the legacy block
both rewrote bare cross-family models to the profile default, and
the client unconditionally applied effective_model — silently
discarding the user's choice.

Backend: accept explicit_model_pick flag (default False) on
_resolve_compatible_session_model_state. Guard both the
profile-aware branch (routes.py:2024) and the legacy block
(routes.py:2124) to skip cross-provider normalization when set.
_handle_chat_start extracts the flag and passes it through.

Frontend: consult _readPendingSessionModel (sessionStorage, 10-min
window) to detect explicit picks and include the flag. Add a toast
as defense-in-depth when the server still returns effective_model.

Closes #3737

* fix: tighten explicit-pick detection and add regression tests (#3737)

Greptile P2-1: compare model_provider in pending pick detection,
not just model name, to avoid false-positive flag when the
session provider changes between pick and send.

Greptile P2-2: only show the defense-in-depth toast when an
explicit pick was actually overridden — stale-session
normalizations are expected behavior and should be silent.

Add two regression tests for the profile-branch guard:
- explicit_model_pick=True → cross-family model survives
- explicit_model_pick=False → existing normalization preserved

* revert(sidebar): remove manual session status labels (#3570)

The manual per-session status labels (Todo / In Progress / Done) added in
v0.51.284 (#3570) stored state only in browser localStorage keyed by session
id, with no server-side backing — so labels did not persist across browsers
or devices (a user who labeled sessions on one machine saw none after moving
to a laptop). They also rendered as three flat top-level entries in the
session context menu, crowding the root menu.

Per maintainer decision, remove the feature entirely for now. It can be
reintroduced later with proper server-side persistence and a less intrusive
menu treatment.

Removes:
- JS state/cycle helpers + SESSION_MANUAL_STATUS_KEY (static/sessions.js)
- context-menu status entries + sidebar status badge render
- .session-manual-status* CSS (static/style.css)
- session_status_* locale strings across all locales (static/i18n.js)

Full suite: 8084 passed, 0 failed. ESLint runtime gate: clean.

reverts #3570

* fix(#3737): keep explicit-pick marker until send consumes it (Codex catch)

Codex found the explicit_model_pick flag never engaged in the normal flow: boot.js
modelSelect.onchange cleared the pending-pick marker right after /api/session/update,
so by the time send() ran _readPendingSessionModel returned null, _explicitPick was
false, and the server's profile-provider branch still reverted the cross-family pick
(the exact #3737 bug). The flag only worked in the rare race where send beat the
session-update round-trip.

Fix (Codex prescription): do NOT clear the marker in onchange; clear it in send()
immediately after reading a matching pending pick, so it's consumed for that send only.
onchange still RECORDS the pick (_rememberPendingSessionModel) — only the premature
clear is removed.

* test(#3737): lock client clear-timing wiring (onchange records, send consumes)

Static source guards for the Codex clear-timing fix: onchange must record the
pending pick and NOT clear it post-session-update; send() must consume (clear) it
only after reading a matching _explicitPick, and send the flag only when truthy.
Complements the author's resolver-level tests in test_provider_mismatch.py.

* test(#3737): realign refresh-persistence test to the moved pending-pick clear

The Codex clear-timing fix moved the pending-pick clear out of modelSelect.onchange
into send() (consume-on-send). test_model_selection_records_pending_state_before_async_session_update
asserted the OLD onchange-clears behavior (assert _clearPendingSessionModel in body).
Updated to assert the NEW correct behavior (onchange must NOT clear it — it survives to
send). The test's core refresh-survives invariant (marker recorded before the async
session-update; reapplied on load) is unchanged and still passes; only the stale
clear-location assertion is flipped. Not a regression-blessing: the refresh-survives
feature is intact, the marker lifecycle is more correct.

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 13:39:14 -07:00
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
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
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
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
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
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
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-hermesandrodboev 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
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
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
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
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
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
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
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
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
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
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
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-hermesandzapabob 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
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-hermesandrodboev 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
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
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-hermesandv2psv 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
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-hermesandrodboev 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
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-hermesandnesquena 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-hermesandrodboev 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
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-hermesandheagandev 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-hermesandrodboev 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-hermesandrodboev 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-hermesandfranksong2702 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-hermesandrodboev 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-hermesandpamnard 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-hermesandt3chn0pr13st 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-hermesandfranksong2702 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-hermesandrodboev 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-hermesandjasonjcwu 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
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