## 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>
* Release v0.51.230 (stage-p14): extract <think> to m.reasoning #3455 + LLM Wiki last-writer (#1257)
Salvage of #3455 (@gsurenull): dropped the stale api/config.py bits (MiniMax-M3 +
SCHEMA_VERSION 3->4 — both already on master via #3374). Kept the two genuine fixes:
(1) _splitThinkFromContent persist-path extraction of inline <think> blocks into
m.reasoning (fixes 30-50% session bloat for reasoning-only providers like MiniMax-M3);
(2) LLM Wiki status Last-writer 3-tier fallback (was always 'Not available' since #1257).
Added 9 Node-driven think-split regression tests (data-loss guards: content-before/after
preserved, unclosed blocks intact, lookalike tags not extracted).
* fix(#3455): renderer-matching think extraction + wiki symlink/bounded-read guards (Codex review)
Codex review of stage-p14 found 3 SILENT bugs, all fixed:
(1) DATA-LOSS: _splitThinkFromContent's Pass-2 whole-body scan extracted a CLOSED literal
<think>...</think> from visible prose/code (e.g. inside a fenced code block) into m.reasoning,
emptying it — more aggressive than the renderer (which only strips LEADING blocks). Removed
Pass 2; extraction now matches _streamDisplay semantics (leading-only, loop captures
consecutive leading blocks). +fenced-code regression test.
(2) PRIVACY: _llm_wiki_last_writer followed symlinked .md pages resolving OUTSIDE the wiki
(is_file follows symlinks), leaking external frontmatter. Now requires resolved path under
wiki_root. +symlink-containment regression test.
(3) CONTRACT/PERF: replaced full read_text() with bounded line-by-line reads (frontmatter
block only / capped log-heading scan), never page bodies.
* fix(#3455): think-split is leading-single (renderer-matching) + fix 2 stale source-match tests
Codex re-review finding #2: looping consecutive leading blocks diverged from the renderer
(_streamDisplay/_parseStreamState strip ONE leading block). Now extracts exactly one leading
block. Also updated 2 tests that asserted pre-split implementation strings:
test_live_stream_tokens_persist (content:assistantText -> content:split.content, invariant
preserved) and the consecutive-blocks test. NOTE: Codex finding #1 (client-only split doesn't
persist server-side) is a separate architectural decision pending Nathan.
* feat(#3455): split inline <think> server-side before s.save() so persisted file is compacted (Codex #1)
Codex finding #1: the think-split was client-only, so the SAVED session file still
carried inline <think> blocks (bloat) — the fix only compacted the browser copy.
Added _split_thinking_from_content (api/streaming.py), a server-side twin of the JS
helper with identical leading-only/single-block semantics, applied to the final
assistant message before s.save() (extended the existing reasoning-persist block).
Merges with on_reasoning-stream reasoning. +8 backend-parity regression tests covering
the mid-body-code-block data-loss guard, unclosed-intact, single-leading, none-content.
* test: update 3 save-path source-assertion tests for #3455 server-side think-split
The backend think-split (api/streaming.py reasoning-persist block) changed the literal
code shape + grew the pre-save block, breaking 8 source-assertion tests that anchor on it:
- test_sprint42: assert _rm['reasoning']=_reasoning_text -> now _merged_reasoning/_existing_reasoning
+ _split_thinking_from_content present (intent preserved: reasoning persisted before save).
- test_pr1318 (6) + test_pr1341: re-anchored the locator from the changed 'if _reasoning_text
and s.messages:' line to the stable 'Persist reasoning trace in the session' comment marker;
bumped the 1341 byte-distance limit 15000->16000 (the test self-documents bumping on legit
pre-save growth). All behavioral invariants (reasoning persisted + context fields before save)
unchanged.
---------
Co-authored-by: nesquena-hermes <[email protected]>
Supersedes the v0.51.199 proximity-re-pin (#3330) and the #3250 upward-intent
timeout with a sticky-unpin model (ChatGPT/Claude/Codex behavior): scroll up =
stay put until you return to the bottom or click the scroll-to-bottom control.
Reconciled against the shipped #3330 code: removed the now-dead
_recentMessageUpwardIntent reference from the #3319 rAF retry, kept the
load-time -Infinity intent-init fix, kept the pinned-only >500 catch-up.
Co-authored-by: pamnard <pamnard@users.noreply.github.com>
Codex+Opus regression-gate finding: _isRecoveryControlMessage /
_streamRecoveryControlMessage fell back to matching
provider_details_label==='interruption details'. But a GENUINE 'Response
interrupted' card (Stop button, real provider crash) carries that exact label,
so the filter would drop a real user-facing interruption from the transcript on
the next render/restore — the inverse of the #3300 data-loss class. Require the
explicit server-set recovery_control marker; keep only the two fully-anchored
synthetic-text matches for pre-marker backward-compat. Adds a node-driven
regression test (revert-verified) asserting a label-only interruption card and a
real user turn stay visible while marker + strict text are filtered.
Track write/edit tool paths per turn, refresh the open preview on
tool_complete and after preservePreview loadDir on stream done, without
closing preview for unrelated responses or wiping unsaved local edits.
Chrome's password manager aggressively autofills the clarify card's
input field with saved credentials (e.g. provider base URLs) despite
autocomplete='off'. This causes two bugs:
1. 'Clarification closed. Your draft was kept in the composer.' appears
on every session completion because _stashClarifyDraft reads the
autofilled value and treats it as a user draft.
2. The autofilled URL gets injected into the main composer, confusing
the user.
Fix: add readonly attribute to the clarify input element so Chrome's
autofill ignores it. When showClarifyCard() makes the card visible,
readonly is removed programmatically so the user can type normally.
Both the static HTML (index.html) and the dynamic DOM creation
(_ensureClarifyCardDom in messages.js) are patched.
Keeps the messages.js fix from @mysoul12138 (set _streamFinalized=true right
after the early-return guard, before the fade window, so a stream_end arriving
mid-fade can't call _restoreSettledSession and overwrite live messages with a
stale server snapshot). Drops the contributor's dead empty `else {}` block in
ui.js (no behavior). Adds a regression test pinning the immediate-finalize order.
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
Adds a distinct two-tone attention sound (880->660Hz) for approval and
clarify prompts so they are not confused with the existing completion sound,
plus sidebar attention badges + colored rails driven by `attention` metadata
on /api/sessions. Includes lock-safety note: the in-lock
publish_session_list_changed() calls in clarify.py are safe because publish()
only takes the leaf _SESSION_EVENTS_LOCK and never re-acquires clarify._lock
(verified by Opus advisor review).
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
#3020: Sync viewed-count in the polling path for actively-viewed sessions
so navigating away doesn't show a stale unread dot. Defensive clear of
completion-unread marker in _setSessionViewedCount.
#2973: Clear elapsed-timer attributes and interval when a live compression
card transitions from running to done, preventing the orphan timer from
overwriting the completed card state. Guarded by active-session check.
Tool card duplication: Route tool_complete result to tc.snippet (detail)
instead of tc.preview (header) to prevent identical content appearing in
both the card header and expanded detail section.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`_turnUsage` (and `_turnDuration`, `_turnTps`, `_gatewayRouting`,
`_statusCard`) are computed client-side in `_finishDone()` and attached
to the last assistant message in `S.messages`. Three code paths replace
`S.messages` wholesale with fresh API data that lacks these fields:
1. `_restoreSettledSession()` after a late `stream_end` or SSE error.
2. The late-restore branch after `done` (messages.js ~L2247).
3. `loadSession()` for active-session external refresh / focus-change.
When any of these fire after `done`, the badge/duration/gateway-pill
flashes for ~1s and disappears, never returning until the next message
or page reload.
Add `_carryForwardEphemeralTurnFields(prev, next)` that matches messages
by `(role, timestamp, content prefix)` — the same identity the renderer
already uses — and copies forward the ephemeral fields when the server
payload is missing them. Wire it into all three replace sites. The fix
is conservative: it only fills slots that are `null` on the new message,
so an authoritative server-side value (if/when the API ever surfaces
per-turn usage) wins automatically.
Picked Option A from the bug report (preserve on the client side) over
Option B (synthesize from `S.lastUsage`) because `lastUsage` is a
session-level aggregate; reconstructing per-turn breakdowns from it is
lossy. Option C (set `_streamFinalized` earlier) would suppress legit
late-arriving server data on transient errors.
Cherry-picked via 3-way apply onto stage HEAD.
Resolved workspace.js conflict: kept master's #2716 sessionId-capture
stale-session guard (closure-scoped sessionId check after await), AND
added PR's renderSessionArtifacts() call to refresh the new Artifacts
tab when the file tree updates. Wrapped in typeof check for defense.
Co-authored-by: AJV20 <abdielvc@me.com>
MUST-FIX:
- tests/test_2735_open_in_vscode.py: bump expected open_in_vscode locale
counter from 10 to 11 (Turkish locale added in #2772). The bump fell
out of an in-rebase test edit but never got committed; tagging without
this would have shipped a failing test in the release commit.
SHOULD-FIX inline:
- api/updates.py: case-D drift in _select_apply_compare_ref. The original
#2855 fix used latest_tag in the past-tag predicate; the check side
uses current_tag (HEAD's nearest reachable tag) plus a 'behind == 0'
gate. They drift when HEAD is on an OLDER release tag with commits on
top AND a NEWER tag exists ('case D'): check correctly suggests
advancing to the newer tag, but apply fell through to origin/<branch>.
Mirror the check-side predicate exactly. Adds regression test
test_select_apply_compare_ref_case_d_older_tag_with_commits_and_newer_tag_exists.
- static/messages.js: post-await race guard in _restoreSettledSession.
stream_end without preceding 'done' enters the settlement path, awaits
/api/session, then sets _streamFinalized=true. If a late 'done' event
arrives during that await, it sees _streamFinalized still false and
double-runs the finalize. The guard returns early when done won the
race, avoiding double renderMessages() + double notification.
- server.py: CORS preflight Access-Control-Allow-Methods now includes PUT.
#2776 wired PUT into the router for /api/mcp/servers/{name} but didn't
update the OPTIONS response. Same-origin only in practice, but cosmetic
completeness for CORS-aware deployments.
Opus advisor verdict: all 5 risk areas reviewed, 1 MUST-FIX + 3 SHOULD-FIX
all addressed inline. Net: +69/-9, no new architecture, no behavior risk.