357 Commits

Author SHA1 Message Date
nesquena-hermes
5bbddbad1e fix(#3900): false streaming + activity-timer reset on session switch (absorb #3899) + re-anchor regression tests
Production fixes (Tamaz-sujashvili, reviewed sound by maintainer):
- loadSession clears S.busy/S.activeStreamId as soon as metadata confirms no active_stream_id, before the async message-load gap (idle session no longer shows streaming chrome).
- Snapshots the live turn before wiping msgInner + seeds INFLIGHT, restores on the active-stream return path (timer/trace survive switch-back).

Re-anchored the 2 brittle regression tests per maintainer review: test_..snapshots.. now anchors on the unique 'Loading conversation...' marker (was matching the no-space 'Session not available' error path); test_..restores.. now asserts the LIVE Phase 2a restore (after loadInflightState) instead of the unreachable Phase-2b/1184 branch. CHANGELOG stamped v0.51.384 (MW).
2026-06-13 06:39:22 +00:00
nesquena-hermes
89ca46eeb5 harden(#4020): prune lineage cache by collapsed-row key too (Opus SHOULD-FIX)
Opus advisor found a key-space asymmetry: _pruneLineageReportCacheToVisibleSessions
built visibleKeys from RAW rows via _sidebarLineageKeyForRow, but the render loop
keys the lineage-report cache by _sidebarLineageKeyForRow on the COLLAPSED row,
which can differ when collapse merges segments. On a malformed/edge chain the
expanded row's cache could be evicted every payload and re-fetched ~every 5s
(partial regression of the bug #4020 fixes). Fold the collapsed rows' cache keys
into the visible set too, mirroring the _resolveSessionIdFromSidebarLineage
precedent, behind a defensive try/catch.

docs(changelog): stamp #4020+#4055 as v0.51.373 (Release ML)
2026-06-13 00:03:30 +00:00
nesquena-hermes
0fdfa4b9fa Merge #4055 into stage-ml 2026-06-12 23:43:28 +00:00
Rod Boev
6a1109475b fix(sidebar): count inactive rows through the render path 2026-06-12 12:03:04 -04:00
Rod Boev
0ed902bd05 fix(sidebar): drop the dead WebUI count path 2026-06-12 11:55:43 -04:00
Rod Boev
dde9f06b03 test(sidebar): avoid shard churn from static regressions 2026-06-12 09:52:49 -04:00
Rod Boev
19448c3267 fix(sidebar): keep rendered source counts on one path 2026-06-12 09:36:10 -04:00
Rod Boev
c507684b10 fix(sidebar): align session source counts with rendered rows (#3966) 2026-06-12 09:14:23 -04:00
Rod Boev
220d255d5c fix(#4005): preserve expanded lineage segments during streaming refresh 2026-06-11 20:55:35 -04:00
nesquena-hermes
8a849a1bb8 Merge #4009 — open clicked lineage segments without sid rewrite (#4003) 2026-06-11 22:48:36 +00:00
nesquena-hermes
8e7d3b086d fix(#3993): add stale-load guard before self-heal so a superseded boot load can't wipe a healthy session (Codex CORE race)
currentSid is snapshotted before the awaited /api/session fetch; if the user
clicks a healthy session while a boot-time restore is in flight and that boot
load then fails non-404, _clearStuckSessionOnBoot(sid, null) would wipe the
healthy session's localStorage/URL. Guard the catch block on
_loadingSessionId !== sid (a newer load superseded this one) — re-arm the active
stream and bail before any self-heal/DOM mutation. Protects both the non-404 and
404 inline self-heal paths. + regression test.
2026-06-11 22:27:01 +00:00
nesquena-hermes
63c62194a1 Merge #3993 — self-heal stuck session loads on non-404 failures 2026-06-11 22:19:14 +00:00
Rod Boev
749f97144b fix(#4003): open clicked lineage segments without sid rewrite 2026-06-11 16:28:33 -04:00
John Torcivia
8eadfb048f fix(sessions): remove self-heal from if(!data) 401 path
On 401, api() redirects to /login and returns undefined. The browser
navigates away immediately, so this code rarely runs. But even if it
did, clearing localStorage on transient auth expiry is wrong — it wipes
the saved session id and sends users to empty state after re-login.

Keep the self-heal in the catch-block else branch for non-401, non-404
errors (400/403/500/network) which genuinely trap the user on recurring
boot failures. 401 has no such property because the redirect already
breaks the retry loop.

Greptile: 'Keep the if (!data) early-return clearing-free (its old
behavior), and only call _clearStuckSessionOnBoot() from the else
branch of the catch.'

(#4028 follow-up)
2026-06-11 14:54:42 -04:00
John Torcivia
3bf3d6004e fix(sessions): remove unreachable e.status===401 branch in loadSession catch
The api() function returns undefined on 401 (redirects to login) rather
than throwing. So the 401 path exits via the if(!data) guard at line ~988,
not through the catch block — the e.status===401 branch inside the catch
is dead code (#4028 follow-up).
2026-06-11 14:52:25 -04:00
John Torcivia
cea9a749d5 fix(sessions): tighten _clearStuckSessionOnBoot guard to !currentSid only
Greptile flagged that currentSid===sid also fires on same-session
force-reloads (e.g. background poll). If the server returns a transient
500 or network error, currentSid===sid is true — the helper wipes
hermes-webui-session from localStorage and resets the URL, even though
the session still exists on the server.

Limit the self-heal to boot-time failures only (!currentSid), where the
stored session ID is definitely stale. When currentSid is set (already
viewing a session), a non-404 failure could be transient and wiping
localStorage is unnecessarily destructive.

The 404 inline self-heal (line 932) already uses this tighter guard,
so this brings the non-404 path into alignment.
2026-06-11 14:49:32 -04:00
Dima Diall
832ab09324 fix(visibility): close gateway/session SSE on hidden tabs to prevent connection pool exhaustion (#3992) 2026-06-11 18:10:29 +01:00
John Torcivia
906c2ef9fd Merge remote-tracking branch 'origin/master' into fix/session-load-failure-self-heal
# Conflicts:
#	static/sessions.js
2026-06-11 11:43:35 -04:00
John Torcivia
3bca16fdc2 fix: self-heal on non-404 session load failures
When loadSession() fails during boot with a non-404 error (401, 400,
500, network), the session ID stays stuck in localStorage and the URL,
causing repeated failures on every page refresh.

The 404 path already had inline self-heal (clears localStorage + URL).
This extends it to all error cases:

- Added _clearStuckSessionOnBoot() helper to consolidate the self-heal
  logic for non-404 errors.
- On 401 redirect (api() returns undefined): clears the stuck session
  ID and shows a more informative message.
- On other non-404 errors: clears the stuck session ID and shows a
  better error message distinguishing auth failures from other errors.

Preserves the guard that prevents clearing localStorage when clicking
into a *different* dead session while already viewing a healthy one.
2026-06-11 11:35:08 -04:00
nesquena-hermes
6bac70d298 fix: preserve live stream output across session switches (cross-client)
Adds a server-side run-journal live snapshot (_run_journal_live_snapshot) returned
in GET /api/session as runtime_journal_snapshot, so a FRESH client (another device,
or a tab with no in-memory snapshot) opening an in-progress session immediately sees
the already-streamed assistant text + tool cards rebuilt from the server. Composes
with the existing _replay_run_journal cursor path (seeds lastRunJournalSeq so replay
resumes from the snapshot cutoff, not duplicating it) and keys tool cards by the same
5 id aliases (tid/id/tool_call_id/tool_use_id/call_id) as #3763 so SSE replay replaces
rather than duplicates snapshot cards. Payload values truncated; redaction test added.

Co-authored-by: t3chn0pr13st <technopriest@live.ru>
2026-06-10 20:12:05 +00:00
nesquena-hermes
337e3b50ab feat(#3845): add J/K keyboard shortcuts for previous/next session navigation
Global j/k keydown bindings navigate the session list (j=next, k=prev), guarded
by _isInteractiveSwipeTarget so they never fire while typing in the composer or
any input/textarea/contenteditable. Modifier-key combos are ignored.

Closes #3845.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-10 19:49:14 +00:00
nesquena-hermes
6be19804f5 fix(routes): bound session/move lock + safe project-delete unlink during streaming (#3746) (#3922)
Two distinct timeout causes, both surfacing as the client's 30s 'Request timed
out' toast with no server-side signal:

A) /api/session/move acquired the per-session agent lock with a bare unbounded
   'with _get_session_agent_lock(sid):'. The streaming thread holds that same
   lock during checkpoint saves; on slow file I/O (WSL/DrvFs) the move could
   block past the client abort. Now acquires with timeout=5 and returns HTTP 503
   on contention (lock kept, not dropped, since s.save() still races the writer).

B) /api/projects/delete unlinked every assigned session via get_session()+save()
   — O(N) full-messages reserialize. For an actively-streaming session we now
   clear project_id on the LIVE CACHED Session object under LOCK (the streaming
   thread persists it on its next save — the worker always does a final save at
   turn completion) instead of issuing a competing s.save(); falls back to a
   direct save when not cached. Non-streaming sessions unchanged.

Also guards the '+ New project and move' shortcut (sessions.js) against the new
503 so it shows a toast instead of an unhandled rejection, keeping the #2551
authoritative refetch in both the success and catch paths.

Adds tests/test_issue3746_session_move_delete_timeout.py (behavioral lock-timeout
test + structural guards for both handlers + the frontend 503 guard). Widened the
#2551 new-project-refetch test's fixed byte-window to a block-scoped search so the
try/catch wrap (which preserves the refetch) doesn't trip a brittle offset assertion.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-10 02:53:01 -07:00
nesquena-hermes
19080f73b1 Release v0.51.348 — Release LL (Phase 0 hotfix: timeout regression + data-loss + leaks) (#3917)
Some checks failed
Release & Docker / release (push) Has been cancelled
* stage v0.51.348: Phase 0 hotfix — approval/clarify timeout regression (#3913), queue/draft durability (#3906), settings auto-reopen (#3909), kanban FD leak (#3904)

* stage v0.51.348: re-anchor 4 SSE frontend tests to poll-only design (#3913); apply Opus SHOULD-FIX — immediate first poll tick so pending approval/clarify cards show instantly

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-10 01:12:48 -07:00
Tamaz_Sujashvili
3807c247e9 Fix false streaming and live UI reset when switching sessions.
Clear stale busy/stream state before async message loads and restore
snapshotted turn HTML when returning to an active stream.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 03:09:44 +04:00
nesquena-hermes
26e133e3e8 [HELD — independent review pending] Release v0.51.340 — bg_task agent wakeup (trio #2968+#2971+#2979) (#3867)
Some checks failed
Release & Docker / release (push) Has been cancelled
* stage bg_task trio combined (#2979 superset) on master for deep review

* fix(bg_task): unsubscribe SessionChannel on header-write failure (Codex deep-review catch) + regression test

* test: realign on-subscribe-recovery anchor to subscribe_to_session_channel after leak fix

* CHANGELOG: bg_task trio as v0.51.340 LD (HELD pending independent review)

* bg_task trio: apply 3 independent-review (greptile) fixes

1. start_session_turn now threads the session PROFILE model defaults
   (_read_profile_model_config) into the wakeup model-resolve, so a brand-new
   session with an empty model falls back to the profile default not global
   DEFAULT_MODEL. Updated the white-box spy test signature accordingly.
2. /api/session/stream omits the Connection header (HTTP/1.1 keep-alive
   default) to match the #3103 long-lived-SSE pattern.
3. Reaper now prunes _LAST_EMIT_TS for collected sessions so the coalesce
   timestamp map can't grow one permanent entry per session forever.

nesquena APPROVED the PR; these are the 3 non-blocking greptile suggestions.

* test: realign _start_session_turn adapter stub lambda to new profile-defaults signature
2026-06-08 22:36:18 -07:00
nesquena-hermes
de4509702d Release v0.51.337 — Release LA (model-picker keyboard nav #2952 + mobile new-chat #3531) (#3857)
Some checks failed
Release & Docker / release (push) Has been cancelled
Two small, aesthetic-safe UX wins:
- #2952 (@Sanjays2402): model-picker arrow-key navigation + Enter-to-select.
  Highlight reuses existing hover style, invisible until keyboard used.
  Opus SHIP-safe, live key-drive verified (multi-row traversal + wrap + Enter).
- #3531 (@franksong2702): mobile titlebar '+' new-chat button. Shares the
  existing reload-button styling, mobile-only, mirrors new-chat pending state.
  390px screenshot vision-verified: cleanly aligned in the titlebar.

Both rebased onto master (CHANGELOG-only / merge-commit conflicts resolved;
code verified byte-identical to PR heads). Full suite green on each (8336/8334),
ESLint/scope-undef CLEAN, zero blocking bot flags.

Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-08 18:47:55 -07:00
nesquena-hermes
9d94298278 Release v0.51.321 — Release KK (Phase 3 light: load renderable transcript tails, #3790) (#3798)
Some checks failed
Release & Docker / release (push) Has been cancelled
Phase-3-light. #3790 (@ai-ag2026): expand cold-load transcript window to ~msg_limit renderable rows so tool-heavy sessions don't open showing 1-2 messages. Codex CORE fix: explicit expand_renderable flag (cold-load only; Load-earlier keeps raw cap). Also fixed a recurring CI timing flake (git-parallel test → deterministic Barrier). Full suite 8228, Codex SAFE, Opus SHIP, CI 11/11. Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-06-07 15:22:44 -07:00
nesquena-hermes
4b390e115c Release v0.51.310 — Release JZ (#3760 — long-press project chips to manage on touch) (#3767)
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(sidebar): long-press project chips to open the context menu on touch (#3760)

Project filter chips could only be deleted/renamed via the right-click context
menu (oncontextmenu), which has no touch equivalent — so mobile/tablet users had
no way to delete a project from the sidebar; the list grew forever.

Adds a 500ms long-press gesture mirroring the existing session-item long-press
pattern: touchstart schedules the menu, touchmove cancels on >10px drift,
touchend suppresses the synthetic click when the long-press fired, touchcancel
cleans up. `.project-chip.long-pressing` gives accent + slight-scale feedback;
`touch-action:manipulation` + `user-select:none` + `-webkit-touch-callout:none`
prevent the native callout/selection from competing.

Maintainer fix on top of the contributor PR (multi-touch correctness, flagged in
review): touchstart now clears any in-flight `_lpTimer` before scheduling a new
one (a second finger / stray touchstart previously orphaned the prior timer,
which then fired unsuppressed ~500ms later and popped the menu after the gesture
was cancelled), and the timer body bails if `_lpHandled` is already set so a
stale fire is a no-op — matching the session-item belt-and-suspenders. Also
dropped a stale, unrelated issue reference from the original comment.

Co-authored-by: reinocheong <[email protected]>

* docs(changelog): stamp v0.51.310 — Release JZ (stage-3760 long-press project chips)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 21:36:24 -07:00
nesquena-hermes
a20ef5e0c3 Release v0.51.309 — Release JY (#3763 — replay restored live tool cards on reconnect, fixes #3707) (#3766)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(streaming): replay restored live tool cards on reconnect (#3763, fixes #3707)

Post-#3401 (#3400 live-to-final epic) recovery residual. When a running session
is restored from its in-memory live-turn snapshot and then reattached to the SSE
stream, the restore-success path skipped replaying persisted live tool calls,
leaving restored live text/thinking but an EMPTY Worklog until a later SSE event
or the final render rebuilt the turn.

- Extract the persisted-tool-card replay into replayPersistedLiveToolCards()
  (reads S.toolCalls or INFLIGHT[sid].toolCalls); run it on restoredLiveTurn &&
  didReconnect, not only the !restoredLiveTurn fallback.
- Dedup safety: restore-success replay passes {skipUnkeyedRestoredDuplicates:true}
  — when the restored snapshot already has .tool-card-row rows, an UNKEYED
  persisted tool is skipped to avoid a duplicate; keyed cards still replay and
  appendLiveToolCard's tid-dedup replaces the correct restored row.
- appendLiveToolCard() and the new liveToolReplayId() both key on
  tid||id||tool_call_id||tool_use_id||call_id (consistent 5-alias set), so the
  dedup covers all known id shapes.
- Both replay sites pass {sessionId, streamId} so the ownership guard applies.
- Regression coverage: restore-success+reconnect replays tools; unkeyed-restored
  duplicates skipped; all-id-alias dedup; prior ordering invariants preserved.

Correct post-#3401 fix for #3707 (supersedes the closed #3724).

Co-authored-by: franksong2702 <[email protected]>

* docs(changelog): stamp v0.51.309 — Release JY (stage-a5b #3763)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 21:11:13 -07:00
nesquena-hermes
bf088cbbc4 Release v0.51.302 — Release JR (stage-brick — mobile/iOS brick + large-session perf hotfixes) (#3754)
Some checks failed
Release & Docker / release (push) Has been cancelled
* 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-hermes
65c4bc9fa2 Release v0.51.295 — stage-3739/3742 (model-pick revert fix #3739 + session-status revert #3742) (#3743)
Some checks failed
Release & Docker / release (push) Has been cancelled
* 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
nesquena-hermes
e3a7c93dc6 [HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741)
Some checks failed
Release & Docker / release (push) Has been cancelled
* Harden interrupted recovery control filtering

* Redesign live-to-final assistant replies

* Fix live activity anchor test fixture

* Fix CI lint issues for live reply tests

* Strengthen live progress prompt contract

* Recover PR #3401 refresh on origin/master

* Repair live-to-final refresh regressions

* Fix live worklog refresh regressions

* Show live footer timer on initial stream start

* Restore live stream shell after reload

* Preserve per-frame live SSE replay cursors

* Preserve reasoning as Worklog Thinking cards

* Quiet Worklog Thinking card styling

* Align Worklog Thinking card styling

* Scope live Worklog Thinking cards by segment

* Suppress exact duplicate settled Thinking

* Close #3401 merge review test gaps

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

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

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

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

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

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

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

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 12:12:37 -07:00
nesquena-hermes
da5bf69aee fix(sidebar): hoist _sessionAttentionState to fix ReferenceError crash (#3696) + scope-undef prevention gate (#3698)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(sidebar): hoist _sessionAttentionState to top-level scope (#3696)

_sessionAttentionState was declared inside renderSessionListFromCache() and
relied on function hoisting, but the top-level function _sidebarRowHasVisible
Messages (reached via renderSessionListFromCache -> _partitionSidebarSessionRows)
called it bare. Hoisting is scoped to the enclosing function, so every sidebar
cache-render threw 'ReferenceError: _sessionAttentionState is not defined' and
the session list went blank. Regressed in #3672 (v0.51.269) when _sidebarRow
HasVisibleMessages was extracted to top level.

Fix: move _sessionAttentionState to top-level scope (it is pure — only uses its
arg plus the i18n global t), so both the visibility predicate and the nested
per-row renderer can reach it.

Prevention (the durable half): add scripts/scope_undef_gate.py — models the
classic-<script> shared global scope (union of all static files' top-level
symbols) and runs ESLint no-undef per file, flagging a function defined nested
but called from a sibling scope. Wired into CI (.github/workflows/tests.yml lint
job) alongside the existing no-const-assign runtime gate, plus an in-suite test
(test_static_js_scope_undef.py) and a focused structural regression test
(test_issue3696_session_attention_scope.py). RED/GREEN-validated against the
broken tree.

* fix(streaming): thread source param into stale-stream bailout; tighten scope gate

Opus review of #3698 found the new scope_undef_gate's 'source' allowlist entry
was masking a real same-class bug: _bailOutOfTerminalEventsFromStaleStream
(declared inside attachLiveStream, params activeSid/streamId/uploaded/options)
called _closeSource(source) against a 'source' not in its lexical scope. All 5
call sites are inside _wireSSE(source), but JS scope is lexical not dynamic, so
the helper would throw ReferenceError: source is not defined on the stale-stream
terminal-event path (user back in an active session whose old stream finalizes
late).

Fix: thread source as an explicit parameter (declaration + all 5 call sites),
the same make-the-dependency-explicit fix as #3696 — and REMOVE the 'source'
allowlist entry so the gate stays gated against that name (it now passes because
the bug is fixed, not because it's allowlisted). Added the documented
false-negative classes from Opus's review to the gate docstring (name-collision
shadowing, destructuring-regex gap, exposure escape hatches, name-keyed
allowlist) and a focused regression test.

This is the prevention gate catching a real latent bug on its first outing.

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-05 20:49:45 -07:00
nesquena-hermes
0b223e91bc Release v0.51.287 — Release JC (stage-r22 — WeCom session classification #3653 + worker-profile picker hiding #3662) (#3695)
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(sessions): classify WeCom gateway sessions as messaging (#3653)

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

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

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

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

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 18:56:43 -07:00
nesquena-hermes
988348682c Release v0.51.284 — Release IZ (stage-w4 — sidebar status labels + cron-sessions toggle #3570 #3514) (#3692)
Some checks failed
Release & Docker / release (push) Has been cancelled
* feat(sidebar): add show_cron_sessions toggle to surface cron sessions (#3514, #2841)

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

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

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

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

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

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

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

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

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 17:48:27 -07:00
nesquena-hermes
b5caf83ff9 Release v0.51.279 — Release IU (stage-p3h — preserve Activity/streaming turn on mid-stream scroll #3665) (#3686)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(streaming): preserve Activity + streaming turn when loading earlier messages mid-stream (#3665, #3346)

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

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

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <839465496@qq.com>
2026-06-05 14:45:31 -07:00
nesquena-hermes
8ef698ea05 Release v0.51.277 — Release IS (stage-p3f — preserve context-window in usage indicator #3663) (#3683)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(ui): preserve resolved context window in usage indicator (#3663, #3185, #3660)

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

* docs(changelog): v0.51.277 — Release IS (stage-p3f)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Frank Song <franksong2702@gmail.com>
2026-06-05 13:45:15 -07:00
nesquena-hermes
d882949173 Release v0.51.271 — Release IM (stage-m1 — named custom provider binding #3626) (#3676)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(providers): preserve named custom provider binding in model send (#3626)

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

* docs(changelog): v0.51.271 — Release IM (stage-m1, #3626 only; #3629 dropped)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 11:58:25 -07:00
nesquena-hermes
2c7b530071 Release v0.51.269 — Release IK (stage-b2 — sidebar perf + search scope + Windows ctl) (#3672)
Some checks failed
Release & Docker / release (push) Has been cancelled
* perf(ui): single-pass sidebar session row partitioning (#3658)

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

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

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

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

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

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

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 10:53:45 -07:00
nesquena-hermes
9b933e2c83 Release v0.51.266 — Release IH (stage-r16) (#3641)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.266 — Release IH (stage-r16)

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

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

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

Co-authored-by: nesquena <nesquena@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-05 00:44:03 -07:00
nesquena-hermes
4cf40a317a Release v0.51.264 — Release IF (stage-r14) (#3636)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.264 — Release IF (stage-r14)

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

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

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

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

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-05 00:11:48 -07:00
nesquena-hermes
6703978c60 Release v0.51.261 — Release IC (stage-r11) (#3616)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.261 — Release IC (stage-r11)

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

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

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

Co-authored-by: v2psv <v2psv@users.noreply.github.com>
2026-06-04 16:46:20 -07:00
nesquena-hermes
11c0d1667f Release v0.51.254 — Release HV (stage-r2) (#3593)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.254 — Release HV (stage-r2)

Phase-2 medium wave 1 — 4 PRs (UI/mobile/cancel fixes + an un-held model dedup).

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3528 | @franksong2702 | Render partial tool calls after cancel — interrupted turns keep their `_partial_tool_calls` rows in the transcript + fallback tool-cards. (Codex confirmed it stays render-only, not forwarded to the provider API.) |
| #3550 | @lurebat | Android offline recovery soft-reattaches the live stream instead of hard-reloading the page on a transient background/disconnect. |
| #3479 | @mvanhorn | iOS Safari no longer snaps the conversation to the top when a handoff/compression card is inserted mid-stream or on `refreshSession()`. |
| #3478 | @JayC-L | **Un-held:** named custom providers (`@custom:name:model`) dedup against bare model IDs without regressing Ollama multi-colon tags (`qwen2.5:7b-instruct-q4`). Only `@custom:` IDs strip the two-segment prefix. |

### Hold-sweep note
#3478/#3489 was held yesterday for an Ollama multi-colon-tag regression risk (a blanket `lastIndexOf` would lose the model). The author pushed a scoped fix (only `@custom:` IDs use `lastIndexOf`); I verified `_normId` in node against the regression cases — Ollama bare tags are preserved. Un-held + shipped.

### Gate
- Full pytest suite: **7588 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** — #3552 partial-tool-calls verified render-only (no `_API_SAFE_MSG_KEYS` leak / no 400-on-strict-provider, the v0.50.251 #1375 trap); #3551 no EventSource double-subscribe; #3541 no regression vs the #3525 scroll-follow shipped in v0.51.253; #3489 no over-dedup.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Co-authored-by: lurebat <lurebat@users.noreply.github.com>
Co-authored-by: mvanhorn <mvanhorn@users.noreply.github.com>
Co-authored-by: JayC-L <JayC-L@users.noreply.github.com>
2026-06-04 11:13:45 -07:00
nesquena-hermes
ba70926e51 Release v0.51.253 — Release HU (stage-r1) (#3591)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.253 — Release HU (stage-r1)

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

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

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

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

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

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

Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Karlineal <Karlineal@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-04 10:50:34 -07:00
nesquena-hermes
d828be6daa Release v0.51.248 — Release HP (stage-q20) (#3522)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.248 — Release HP (stage-q20)

Bug-fix.

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

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

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

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-03 19:41:22 -07:00
nesquena-hermes
e7930ad9a5 Release v0.51.241 — Release HI (stage-q13) (#3498)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.241 — Release HI (stage-q13)

UX-flow bug-fix — approved via Telegram.

### Fixed
| PR | Author | Fix |
|----|--------|-----|
| #3471 (#3333) | @starGazerK | **New Chat keeps your unsent draft after peeking at history.** Start a New Chat draft → open a previous conversation → click New Chat: the draft is no longer lost. Empty New-Chat sessions are hidden from the sidebar, so there was no way back to the session holding the draft — New Chat just created another fresh empty session. The entrypoint now remembers the candidate empty draft session (one `localStorage` pointer) and, before creating a fresh session, re-validates it via `/api/session`, routing back only if it is still a safe empty draft (zero messages, no active stream, no pending message, not worktree-backed, matching profile, non-empty server-side `composer_draft`). |

### Why it's safe for existing installs
- When there is no remembered draft, it's a **pure no-op fall-through** to the existing `newSession()` path — no behavior change.
- Preserves the "zero-message sessions stay hidden from the sidebar" contract.
- Conservative multi-guard validation; the pointer is cleared on draft-clear (after send) so an emptied draft never traps you on New Chat.

### Verified live (end-to-end on a test server)
- **Positive**: typed a draft → visited a 2-message history session → clicked New Chat → landed back on the draft session with the text restored.
- **Negative**: emptied the draft → New Chat created a fresh session (no accidental trap).

### Absorbed on review (Codex CORE MUST-FIX)
The PR added `await _saveComposerDraftNow(...)` before the session-switch, which opened a rapid-switch race: clicking session B then quickly C could let B's stale continuation blank C's freshly-loaded state. Added `if (_loadingSessionId !== sid) return;` immediately after the awaited save and before the destructive state-clear (mirrors the existing #1060 stale-guard) + a regression test pinning the guard's position.

### Gate
- Full pytest suite: **7510 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint runtime gate: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (rapid-switch race) → fixed → re-reviewed **SAFE TO SHIP**
- `tests/test_issue_new_chat_draft_restore.py` (7 assertions incl. the race-guard; live-verified behavior)

UX-flow change, no visual/layout delta — approved via Telegram.

Co-authored-by: starGazerK <starGazerK@users.noreply.github.com>
2026-06-03 15:24:31 -07:00
nesquena-hermes
1fe8950022 Release v0.51.238 — Release HF (stage-q9) (#3493)
Some checks failed
Release & Docker / release (push) Has been cancelled
## Release v0.51.238 — Release HF (stage-q9)

Phase 3 MEDIUM-ring pick (3-factor: contributor×impact×mitigated-risk) — high-impact perf fix to the most-clicked affordance from a regular contributor (@franksong2702 ★★★), small code surface, CI-green.

### Fixed
| PR | Author | Fix |
|----|--------|-----|
| #2518 follow-up | @franksong2702 | Clicking **New Conversation** on a cold start no longer hangs 3–4s on a catalog rebuild. `newSession()` fills `model_provider` from `window._activeProvider` (then prev-session) when the dropdown carries none, so `POST /api/session/new` takes the fast path on the first click too. |

### Pre-release dual gate caught a wrong-backend routing bug (fixed + regression-tested)
The server fast path passes `(model, provider)` through **without validating the pair**, so naively attaching the active provider to *any* bare model could silently route to the wrong backend (e.g. bare `claude-opus-4.8` + active `openrouter`). **Codex** flagged this; **Opus** had judged it acceptable ("respect the selection over silent swap"). I took the stricter, empirically-grounded path and added a **family-mismatch guard** mirroring the server's own bare-prefix→provider map (`gpt`→openai, `claude`→anthropic, `gemini`→google): when the model's known family differs from the fallback provider, `model_provider` stays `null` so the server slow-path's family repair runs. This keeps the perf win for the common matching case while closing the mis-route. Backend behavioral tests confirm fast-path-on-match + slow-path-on-mismatch. (Also re-anchored the source-shape test assertions on the real `reqBody.model_provider=` assignment per Codex's 2nd note.)

### Gate results
- **Full pytest suite**: 7495 passed, 9 skipped, 3 xpassed, **0 failed**
- **ESLint runtime gate**: CLEAN  ·  **ruff**: CLEAN  ·  **browser-smoke**: CLEAN
- **Codex (regression)**: SHIP ONLY WITH FIXES → guard + test-anchor applied → re-reviewed **SAFE TO SHIP**
- **Opus (correctness)**: reviewed the original (judged acceptable); the shipped version is strictly safer (adds the family guard)

Note: `docs/pr-media/2518/{PR_BODY.md,bench.py}` are the contributor's review aids, included per the tracked `docs/pr-media/` convention (157 files already tracked) — not app code.

Closes #2518.

Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-03 12:23:46 -07:00
nesquena-hermes
fdfb935b5e Release v0.51.227 — Release GU (stage-p11 — keep active New Chat visible in sidebar #3408) (#3461)
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(sidebar): keep active New Chat visible before first message (#3408, @AJV20)

Squashed net diff of #3408. Injects ONLY the active ephemeral session into the
sidebar render rows (when the server list omits it) so a freshly-created New Chat
stays visible/selected before its first turn; inactive empty sessions stay
filtered as before. New Chat also resets a CLI source-filter back to webui so the
active chat isn't immediately hidden.

* fix(sidebar): gate active-row reinjection to 0-message ephemeral only (#3408 Codex follow-up)

Codex review found _ensureActiveSessionRowPresent re-injected ANY active session
after search-merge — so an active conversation WITH messages that was correctly
filtered out by the search query would pollute unrelated search results. Gate the
reinjection to Number(activeRow.message_count||0)<=0 so only the freshly-created
0-message ephemeral chat is re-added; an active chat with messages stays filtered
by search as before. Added a regression test asserting the gate.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 20:24:48 -07:00
nesquena-hermes
7c4c8120a3 fix: preserve loaded transcript width on same-session external refresh (#3326, @viraatdas)
Capture a same-session force-reload hint (loaded renderable/message counts,
known count, truncation flag) BEFORE clearing the in-memory transcript, so the
authoritative reload requests a width that preserves what was loaded instead of
collapsing a long session to the default 30-message tail window mid-read.
Same-session force reloads render with preserveScroll. Resolves conflict with
the shipped #3306 carry-forward snapshot by keeping both (complementary).
Closes #3239.

Co-authored-by: viraatdas <viraatdas@users.noreply.github.com>
2026-06-02 18:53:12 +00:00
Qi Zhou
5c595c08d6 fix(todos): hydrate cold-load state from session snapshot 2026-06-02 17:33:49 +00:00