Commit graph

471 commits

Author SHA1 Message Date
Rod Boev
a82eb1622f fix(#3958): keep reasoning effort status on the active session model 2026-06-11 18:46:17 -04:00
Hermes Agent
0a561f240a chore(streaming): move dataset.sessionId stamp to the whole-turn restore paths only (Opus/Codex nit)
Both reviewers noted the _preservedLiveTurn.dataset.sessionId stamp was a dead
write in the segment-swap path (only the segment enters the DOM there; the
rebuilt turn was already stamped at build time). Move the stamp into the two
whole-turn paths where the preserved turn actually enters the DOM. No behavior
change; removes the dead write.
2026-06-11 03:20:17 +00:00
Hermes Agent
3ec93ff6bf fix(streaming): count live-worklog shell/reason as preserved structure (Codex r2)
Codex round 2 caught that _structuralCount() only enumerated .tool-call-group/
.tool-card-row/.tool-worklog-group but NOT the live Worklog shell
(.live-worklog[data-live-worklog-shell=1]) or its .wl-reason content (built by
_flushPendingSegmentRender / ensureLiveWorklogContainer, see messages.js:2386 +
ui.js:6712/6655). So a turn whose live-ahead structure is a Worklog (not a bare
tool-call-group) undercounted preserved structure → wrongly took the segment-swap
path → detached the visible live Worklog container for a frame.

Fix: add .live-worklog[data-live-worklog-shell=1] and .wl-reason to the structural
count (mirrors the canonical 'has current worklog content' selector set at
sessions.js:1125-1131). Added a structural-test assertion + verified live: a
preserved turn with a live worklog .wl-reason the rebuild lacks now takes the
whole-turn restore path and the worklog + parser both survive.

Convergence: Codex findings narrowing each round (tie-guard → tail-segment →
worklog-selector), all real, all fixed + empirically verified.
2026-06-11 03:12:40 +00:00
Hermes Agent
c37c0d1397 fix(streaming): whole-turn restore when live DOM is structurally ahead (Opus)
Opus flagged a tool-card-frame-loss edge in the segment-only swap: the premise of
#3877 is that the live DOM can be AHEAD of S.messages, so a tool/worklog group can
land in the live turn between the last throttled persist and a mid-stream rebuild.
The rebuild (built from the lagging S.messages) would then lack that group, and a
segment-only swap would drop it for a frame (empirically confirmed: a live-only
tool card present before the rebuild was absent after).

Fix: count structural blocks ([data-live-assistant=1] + tool/worklog/thinking
groups) on both turns. Segment-swap only when the rebuild is the structural
SUPERSET (rebuilt >= preserved); otherwise restore the WHOLE preserved turn so
live-only structure the user already saw never vanishes. Verified live: tool-card-
ahead → whole-turn restore keeps the card + parser; single-segment tie → segment
swap, parser connected; multi-live-segment tail → tail parser connected, both
segments kept.

Also addresses Opus's dead-code note implicitly — the dataset.sessionId stamp now
matters on the whole-turn restore path (it's the branch that actually inserts the
preserved wrapper).
2026-06-11 03:03:55 +00:00
Hermes Agent
95e874d7be fix(streaming): target the parser-owned TAIL live segment in the #3877 re-attach (Codex CORE)
Codex caught an asymmetry in the first cut: the rebuilt side selected the tail
live segment ([...].pop()) but the preserved side used querySelector() = the
FIRST [data-live-assistant=1] segment. In a multi-live-segment turn (reconnect /
post-tool activity boundaries — see messages.js ensureAssistantRow re-attaching
to the LAST live segment), the smd parser writes into the tail segment, so
swapping the first preserved segment would move the wrong node and leave the
parser-owned tail detached — re-introducing the flicker for multi-segment turns.

Fix: select the preserved segment from querySelectorAll, defaulting to the LAST
(tail), and prefer the one whose data-live-segment-seq matches the rebuilt tail;
compute _preservedLen from that segment. Added a structural regression test
pinning the tail selection.

Verified live: a 2-live-segment turn (seq 1 + seq 2, parser on seq 2) keeps the
parser-owned tail connected across a mid-stream rebuild (was orphaned with the
first-segment selection).
2026-06-11 02:57:46 +00:00
Hermes Agent
9803a037fe fix(streaming): re-attach preserved live turn on length tie (#3877 reopen)
The #3877 fix (#3892) preserves the live assistant turn's DOM node across a
mid-stream renderMessages() rebuild and swaps it back in when the rebuilt turn
has less streamed text than the preserved (smd-parser-referenced) node. But the
guard was strict: _rebuiltLen < _preservedLen. At the throttled session
write-back boundary the rebuilt turn's live segment can carry EXACTLY the same
text length as the preserved node, so the strict guard skipped the swap and left
the streaming parser writing into the now-detached original node — the residual
'disappears, then reappears' frame the reporter still saw on v0.51.347.

Fix:
- Relax the guard to _rebuiltLen <= _preservedLen so the equal-length tie also
  restores the parser-referenced node (on a tie the preserved node is strictly
  preferable: it holds the live parser reference and nothing is lost). When the
  rebuilt turn genuinely has MORE content (reconnect where S.messages caught up
  past the parser) the guard still skips and the parser re-resolves to the fuller
  node.
- Swap at the SEGMENT level (replace only the rebuilt live segment with the
  preserved one) instead of a whole-turn replaceWith, so a multi-segment turn
  (earlier settled segments + tool/worklog groups built by the rebuild) keeps
  that rebuilt-only structure. Whole-turn replace remains the fallback when the
  rebuilt turn has no live segment to target.

Verified live (isolated server on the edited ui.js, real shipped renderMessages):
- single-segment tie: parser node stays connected (was orphaned on master);
- multi-segment tie: parser connected AND both assistant segments preserved;
- rebuilt-longer: correctly keeps the fuller rebuilt node, no content loss.

Updated the #3877 structural tests to pin the <= guard + segment-level swap.

Closes #3877.
2026-06-11 02:51:14 +00:00
nesquena-hermes
97f92b3a4b fix(#3852): jump-to-question button targets response start
The per-turn jump button now scrolls to the START OF THE RESPONSE (the assistant
segment) instead of the user's question, with a fail-safe fallback to the question
row when the assistant segment isn't found. Label updated to 'to response' across
all 13 locales.

Closes #3852.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-10 19:48:41 +00:00
nesquena-hermes
f00d1bd1cd fix(#3863): reattach live stream after mobile app-switch offline recovery
_recoverFromOfflineSoftly() now probes stream status and calls attachLiveStream()
after refreshSession() so a live stream resumes after Android PWA backgrounding.

Closes #3863.

Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-10 19:11:58 +00:00
nesquena-hermes
19080f73b1
Release v0.51.348 — Release LL (Phase 0 hotfix: timeout regression + data-loss + leaks) (#3917)
* 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
nesquena-hermes
e8d71a2fb8
Release v0.51.347 — Release LK (streaming & render reliability cluster #3892 #3898 #3885 #3882 #3868) (#3902)
* stage v0.51.347: render/stream cluster (#3892 #3898 #3885 #3882 #3868) + 2 Opus SHOULD-FIX

* stage v0.51.347: trim #3885 error-guard comment to fit diagnostic-test window

* Stamp v0.51.347 — Release LK (streaming & render reliability cluster)

* Remove stray uv.lock accidentally staged (not part of any cluster PR)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-09 17:02:35 -07:00
nesquena-hermes
f44d8c971d
Release v0.51.342 — Release LF (transcript + sidebar reliability: #3875 + #3887 + #3831) (#3889)
* Release v0.51.342 — Release LF (blank-transcript brick fix #3875)

Fixes #3875: chat transcript rendering as only a stack of date separators
with no message bodies. The live-to-final/Worklog redesign (#3401) folds
intermediate assistant segments into a collapsed Worklog and hides the source
segment; when a turn's ONLY content is folded into a collapsed Worklog (empty
final assistant message from an interrupted/autonomous run, or a reload where
S.toolCalls did not hydrate so the Worklog has no expandable steps), every
segment is hidden and the turn paints blank — leaving a bare column of date
dividers.

Adds a defensive fail-safe invariant at the end of renderMessages(): a settled
assistant turn never renders with zero visible content. Blank turns get their
folded Worklog expanded (or hidden segments un-hidden as a last resort). Turns
with any visible answer are untouched, preserving the intended collapsed-Worklog
UX. Reproduced + verified fixed in an isolated browser (clean Chrome profile to
defeat the ?v= asset-cache); RED on master (blank 'Worklog' chip), GREEN with
the fix (Worklog expanded, content visible).

Includes #3875 structural regression coverage.

* docs(ui): clarify revealed-flag intent in #3875 fail-safe (greptile P2)

Address greptile review on PR #3889: the 'revealed' flag means 'turn has a
visible non-empty Worklog group' not 'we just expanded one'. An already-open
non-empty group is itself visible, so the last-resort un-hide is correctly
skipped. Comment-only; no behavior change.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-09 12:43:18 -07:00
nesquena-hermes
35098632ea
Release v0.51.341 — Release LE (stale thinking-dot placeholder fix #3869/#3876) (#3886)
Fixes #3869: empty legacy three-dot thinking spinners piled up as stale
rows after the agent finished thinking. The live-to-final redesign (#3401)
made the thinking-card-row wrapper class unconditional, which broke
finalizeThinkingCard()'s dots-only detection — it treated the wrapper class
itself as a "has content" signal, so the dots-only removal branch went dead.
Narrow hasContent to the actual .thinking-card element so dots-only spinners
are removed on finalize while real Worklog Thinking Cards are preserved.

Includes #3869 regression coverage (brace-walks finalizeThinkingCard, asserts
the narrowed check + that real thinking cards are not removed).

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-09 10:19:22 -07:00
nesquena-hermes
cf4bed3d35
Release v0.51.339 — Release LC (targeted workspace create actions, #3855 + #3858) (#3865)
#3855 (@b3nw): workspace context menus (root/dir/file rows) gain New File/New
Folder targeting the clicked location instead of always S.currentDir. promptNewFile/
promptNewFolder take a targetDir param; prompt title names the target. i18n complete
(13 locales). Live-verified end-to-end: New File from src/ menu creates inside src/
(on disk), not root. Opus SHIP-safe (backend safe_resolve is the enforced trust
boundary; frontend join is naive but backend-gated). Context menu screenshot-approved.

#3858 (@b3nw): adds cancelLabel:t('status_no') ('No') to the post-folder-create
'Add as space?' confirm (was an unlabeled 'Cancel').

Both rebased onto master; combined cleanly (both touch promptNewFolder, no conflict).
Suite 8349, ESLint/scope-undef/ruff CLEAN. greptile flags evaluated → stale/resolved.

Co-authored-by: b3nw <b3nw@duck.com>
2026-06-08 20:55:15 -07:00
nesquena-hermes
de4509702d
Release v0.51.337 — Release LA (model-picker keyboard nav #2952 + mobile new-chat #3531) (#3857)
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
066f727271
Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633) (#3853)
* fix(streaming): normalize inline thinking extraction across live and persisted turns (#3599)

# Conflicts:
#	api/streaming.py
#	static/messages.js
#	static/ui.js

* fix(streaming): code-aware inline-thinking extraction + position-aware unclosed handling

Codex deep-review caught two regressions in the leading-only -> full-scan
rewrite (both silent data-mangling on the persist/reload path):

1. Code-span unawareness: the scanner only protected triple fences, so a
   literal <think> in an inline single-backtick code span or an indented
   (>=4-space/tab) code block got silently extracted into reasoning. Added
   _inline_thinking_indented_code_at + inline-backtick tracking (Python +
   the JS twin _thinkingIndentedCodeAt), so all three code contexts now keep
   thinking tags visible.

2. Unclosed-tag truncation: any unmatched open tag moved the trailing prose
   into reasoning. Now position-aware — a LEADING unclosed block (cut off
   mid-thought) is still reasoning (#3455 intent), but an unclosed tag AFTER
   visible content stays visible so literal typed tags don't truncate prose.
   Gated partial handling on the previously-unused options.streaming param
   (live streaming keeps 'still thinking' behavior; persist/reload does not).

Updated 2 tests that pinned the buggy behavior + added 4 regression tests
(inline-backtick, indented-code, mid-body-unclosed-visible, leading-unclosed-
extracted). Updated the node driver harness to include the new helper.

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

* fix(streaming): recognize fenced code blocks indented 1-3 spaces

Codex round-3: a fence indented 1-3 spaces is valid Markdown but the fence
detector only matched at column 0, so a literal think tag inside such a fence
(not 4+-space indented code either) was still extracted. Both detectors
(_inline_thinking_fence_marker_at / _thinkingFenceMarkerAt) now walk back over
up to 3 leading spaces to a line start. Added backtick + tilde indented-fence
regression tests.

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

* fix(streaming): O(n) inline-thinking scan + merge separate reasoning on reload

Round-4 Codex deep-review caught two real issues in my own fixes:

1. PERF (O(n^2)): the indented-code check (_inline_thinking_indented_code_at /
   _thinkingIndentedCodeAt) scanned to line boundaries at EVERY character index,
   plus the leading check sliced+stripped the whole prefix per unclosed tag. On
   long no-newline content this was quadratic (~8.4s @ 200k, called repeatedly
   on the streaming path). Replaced with incremental O(1)-per-iteration line
   state (_line_is_indented_code / _lineIsIndentedCode evaluated only at line
   starts) + a seen_nonspace flag. 200k now extracts in ~55-140ms.

2. RELOAD reasoning-drop: renderMessages() seeded the shared extractor with ''
   so a message with BOTH an inline <think> block AND a separate m.reasoning
   payload showed only the inline part — the separate payload was dropped
   because the !thinkingText worklog resolution was then skipped. Now seeds with
   the message's direct reasoning (m.reasoning_content||m.reasoning||...) so the
   two MERGE (deduped); separate-only reasoning is preserved without promoting
   it into visible prose.

Python + JS twins kept line-for-line parity. Added merge + perf + reload
regression tests; updated the reload structure test and the node driver harness
for the renamed helper.

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

* fix(streaming): revert reload reasoning-seed; keep O(n) perf fix

Codex round-4 finding #2 (seed renderMessages' inline extractor with
m.reasoning so a separate payload merges) turned out to VIOLATE a deliberate
architectural invariant pinned by test_issue2565 +
test_sprint42: the reload content-extraction path must NOT touch
m.reasoning/m.reasoning_content — reasoning metadata is owned exclusively by
the Worklog Thinking Card path (_worklogReasoningTextFromMessage /
_assistantReasoningPayloadText), never conflated with inline-content
extraction (which would risk promoting provider reasoning into final-answer
prose). Reverted the ui.js seed to the PR's original `thinkingText` arg.

The inline+separate merge is still a genuine extractor capability (exercised
by the live streaming path via liveReasoningText) and is covered by a unit
test, just not invoked from the reload render path by design.

The O(n) perf fix (finding #1) and the code-awareness + position-aware
unclosed handling (rounds 1-3) are all retained.

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

* fix(streaming): only lstrip extracted content when a leading block was removed

Codex round-5 catch: the extractor unconditionally lstripped the final content
(.lstrip() / .replace(/^\s+/,'')) even when NO thinking block was extracted, so
an assistant reply that legitimately starts with an indented code block or blank
lines lost its leading whitespace on live display, reload, and persistence. This
was a real regression vs master (master returned non-thinking content unchanged).

Now track leading_removed (set only when a LEADING thinking block/prefix is
actually extracted) and lstrip only in that case. Mid-body / no-thinking content
keeps its exact leading whitespace. Python + JS twins kept in parity; added
backend regression tests (indented-first preserved, leading-blank preserved,
leading-think still strips).

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

* fix(streaming): reconnect restore prefers raw inflight accumulator

Codex round-6 CORE catch: on reconnect, the single-live-message restore used
(_liveInflightAssistant.content || ''). Because the PR now splits a leading
unclosed <think> into empty content, restoring from the split content dropped
the open tag — so a later </think> token leaked into the visible reply and
corrupted the live accumulator. Restore from
(_fullInflightAssistant || _liveInflightAssistant.content || '') so the raw
open tag survives reconnect and the accumulator stays correct. Added a
reconnect-restore regression test.

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

* Release v0.51.335 — Release KY (normalize inline thinking extraction, #3633)

Unify inline-thinking (<think>/<|channel>/<|turn|>) extraction across live,
reload, and persisted turns (#3599/#3633, @rodboev). Deep-reviewed: Opus +
6 Codex rounds; maintainer fixes resolved every Codex finding — code-awareness
(inline-backtick/indented/1-3-space fences keep literal tags visible),
position-aware unclosed handling, O(n) line scanning (was O(n^2) on long
content), conditional lstrip (preserve leading whitespace when no leading block
removed), and a reconnect-restore CORE fix (raw accumulator preferred so an open
<think> tag survives reconnect). Python + JS twins in parity. Full suite 8330,
Opus SHIP-SAFE, Codex SAFE-TO-SHIP, ESLint/scope-undef/ruff clean.

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

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-08 17:28:05 -07:00
nesquena-hermes
a71dbcd6bb
Release v0.51.334 — Release KX (new-message cue when scrolled up, #3631) (#3849)
* fix(streaming): show new-message cue when preserving scroll position (#3545)

# Conflicts:
#	static/ui.js
#	tests/test_issue1690_scroll_completion.py
#	tests/test_tars_scroll_reset_regressions.py

* i18n: add missing Polish (pl) translation for session_new_message keys

The PR added session_new_message / session_new_message_label to 12 of 13
locales; Polish was missing both, which fails the per-locale parity test.

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

* fix(streaming): keep forced follow path for pinned users in preserve-scroll branch

Codex CORE catch: the PR's preserve-scroll branch used
'if(_scrollPinned) scrollIfPinned()' which skips the synchronous bottom
write unless distance>500 and can have its delayed settles cancelled by the
DOM-rebuild scroll event — leaving a pinned reader a few lines above the
settled final response. Restore master's _followMessagesAfterDomReplace()
forced-scrollToBottom() path for pinned/near-bottom users; only genuinely
scrolled-up (unpinned, not near bottom) users restore their viewport and
get the new-message cue. Updated the 3 structure-pinning tests to assert the
corrected (safer) shape while preserving their behavioral intent.

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

* Release v0.51.334 — Release KX (new-message cue when scrolled up, #3631)

New-message cue on the jump-to-bottom button when the user has scrolled up
during a live turn (#3545/#3631, @rodboev). Deep-reviewed (Opus+Codex);
maintainer fixes during re-gate: (1) restored master's forced follow path
for pinned/near-bottom users (Codex CORE: scrollIfPinned could leave a pinned
reader short of the settled response) + updated 3 structure-pinning tests to
the corrected shape; (2) added missing Polish (pl) i18n keys (PR had 12/13).
Full suite 8308, ESLint/scope-undef CLEAN, Opus SHIP-safe, Codex SAFE-TO-SHIP.

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

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-08 15:58:13 -07:00
nesquena-hermes
d560a312f2
Release v0.51.331 — Release KU (dismissible error toasts, #3844) (#3846)
Error toasts now render an explicit Dismiss button + non-error toasts
dismiss on click (#3844, fixes #3842). Frontend-only (ui.js + style.css +
structural test). Full suite 8278 passed, ESLint runtime + scope-undef +
ruff gates CLEAN, Opus SHIP-safe.

Co-authored-by: claw-io <claw-io@users.noreply.github.com>
2026-06-08 14:39:34 -07:00
nesquena-hermes
a20ef5e0c3
Release v0.51.309 — Release JY (#3763 — replay restored live tool cards on reconnect, fixes #3707) (#3766)
* 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
1fed11c085
Release v0.51.299 — stage-3713 (update flow waits for new server instance #3713) (#3749)
* fix(#3619): update flow waits for a genuinely new server instance before reload (#3713)

@rodboev. _waitForServerThenReload() reloaded on the first healthy /health response
without confirming the server instance changed, so a slow restart could reload onto
the old process or show a premature error toast. Now captures a baseline server identity
from /health's existing server_started_at and only reloads on a changed identity.
Frontend-only (server_started_at already in /health on master). ui.js verified
byte-identical to PR head; test_update_banner_fixes 82 passed. + CHANGELOG v0.51.299.

* fix(#3713): reload on observed outage when uptime-only identity not lower (Codex catch)

Codex found a CORE edge case: when a deployment strips server_started_at and BOTH the
baseline and replacement /health expose only uptime_seconds, the reload trigger relied
solely on 'next.uptime < baseline.uptime'. If the old server's baseline uptime was very
low (~0) and the new instance is first seen at equal/higher uptime, that never fires and
the user is stranded on the restart banner.

Fix: track _observedOutage (set in the /health probe catch arm) and, for the
uptime-only-on-both-sides case, treat an outage-then-healthy sequence as the new instance
and reload. A real restart almost always produces an outage given the pre-probe interval,
so this closes the gap without weakening the identity check. + regression test.

* fix(#3713): harden outage fallback — require >=2 consecutive outages, count non-OK (Codex round 2)

Codex round 2 found two refinements to the outage fallback: (1) a single transient
fetch blip could set the flag and reload onto the still-running old server; (2) a
reverse-proxy 502/503 returns r.ok===false (not a thrown error) so the proxy-outage
case didn't register. Replaced the boolean with a _consecutiveOutages counter: require
>=2 consecutive outages before the uptime-only reload; count BOTH thrown errors and
non-OK/non-status-ok responses; reset to 0 when the old server answers healthy (so
unrelated blips can't accumulate). Updated the regression test to the counter contract.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 15:38:51 -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)
* 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
32d46f4450
Release v0.51.293 — Release JI (stage-s5 — thinking card no longer renders twice #3709) (#3715)
* fix(#3709): thinking card no longer renders twice (in Activity + below answer)

The #3592 inline-render branch (v0.51.258) emitted a thinking card for a
thinking-only message even when a sibling tool-message in the same turn already
built an Activity group carrying that turn's thinking — so the card showed twice,
the second one stranded below the answer + 'Done in …' footer (insertAdjacentHTML
'beforeend' on a segment that already had body+footer).

Fix (keeps #3592, does NOT revert it):
- A1: precompute turnsWithActivityGroup (turns whose segments have tool cards);
  the inline branch only renders when the anchor turn is NOT in that set.
- A2: when it does render inline, insert 'beforebegin' the .msg-body/.msg-foot so
  the card sits above the answer, not orphaned below the footer.
- B: strip thinking against the TURN's combined visible answer
  (_turnVisibleTextByRawIdx), so a trailing thinking-only message that echoes the
  answer gets de-duped even though its own body is empty.

Live-verified in browser: #3709 repro (tool+trailing-thinking) → exactly 1 card in
Activity, above footer; #3592 repro (thinking-only) → exactly 1 inline card, not
buried in a collapsed group. + regression test tests/test_issue3709_*.

Supersedes #3708 (which deleted the inline branch outright, re-breaking #3592).

* fix(#3709): merge suppressed sibling thinking into the Activity group (Codex re-gate)

Codex caught a content-loss edge in the first cut: when A1 suppresses a
thinking-only sibling's inline card (its turn has an Activity group), the group
only rendered assistantThinking.get(aIdx) for the TOOL message — so a sibling
with DISTINCT reasoning was neither inline nor in the group → dropped.

Fix: aggregate all of a turn's thinking (turnThinkingParts, de-duped, index
order) and render that merged text once per turn in the Activity group
(_renderedTurnThinking guard). Live-verified: tool-thinking A + distinct
sibling-thinking B → 1 merged node carrying both, no loss. + regression test.

* fix(#3709): shared anchor resolver so inline-suppression & group placement agree (Codex re-gate #2)

Codex caught a fallback-anchor mismatch: turnsWithActivityGroup was populated only
from assistantSegments.get(tcIdx) (direct segment), but the group-render path falls
back to a nearby earlier segment when a tool's assistant_msg_idx has no directly
rendered segment (legacy/rebased). So a fallback-anchored group's turn wasn't in
turnsWithActivityGroup → the sibling rendered inline AND the group rendered → dup
again. Fix: one shared _anchorRowForActivityIdx(aIdx) helper (direct-or-fallback)
used by the precompute, the inline branch, and the group render — they now agree.
Live-verified all three repros still pass.

* test(#3709): update test_compact_activity assertion to mergedThinking var

The brittle source-scan asserted _thinkingActivityNode(thinkingText, false) — the
#3709 fix renders the turn's MERGED thinking via _thinkingActivityNode(mergedThinking,
false) into the same Activity body. Intent (settled thinking renders inside the
Activity disclosure alongside tools) unchanged; only the source variable. Updated to
assert the new variable, kept all intent assertions.

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 00:19:29 -07:00
nesquena-hermes
9720e0b03d
Release v0.51.292 — Release JH (stage-s4 — compression-exhausted turns surface as errors #3316 fixes #3315) (#3705)
* fix(#3315): surface compression-exhausted/no-final-answer turns as errors (#3316)

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

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

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

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 22:45:09 -07:00
nesquena-hermes
e9bb354908
Release v0.51.285 — Release JA (stage-r19 — update-reload server-identity race fix #3654) (#3693)
* Fix update reload readiness race — poll /health server identity before reload (#3654)

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

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

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

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 18:13:35 -07:00
nesquena-hermes
a7241f75fc
Release v0.51.283 — Release IY (stage-w2 — composer queue hint during auto-compaction #3512) (#3690)
* feat(composer): surface that messages queue during auto-compaction (#3512, #3079)

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

* docs(changelog): v0.51.283 — Release IY (stage-w2)

* fix(composer): restore placeholder on ALL compaction-exit paths, not just clearCompressionUi (#3512)

Codex+Opus both caught: setCompressionUi(done) and the live-anchored SSE
window._compressionUi=null paths bypassed clearCompressionUi, leaving the
'will queue' placeholder stuck after compaction. Factor restore into
_restoreCompressionPlaceholder() + call from every compaction-exit path.

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 16:52:28 -07:00
nesquena-hermes
b26bb559d5 feat(ux): surface memory/skill saves in Activity summary (#3544)
Absorbs contributor PR #3544 (@rodboev, closes #3340) with two fixes:

1. DETECTION VOCAB (would never fire): the original gated on action names
   {save,create,update,upsert}, which don't match the real agent tool enums —
   memory.action is add|replace|remove, skill_manage.action is
   create|patch|edit|delete|write_file|remove_file. Split into per-tool
   predicates with the correct vocabularies: _isMemorySave gates memory on
   {add,replace}; _isSkillUpdate gates skill_manage on {create,patch,edit,
   write_file}. Deletions excluded so the saved/updated verbs stay accurate;
   running/errored excluded.

2. SNAPSHOT/RESTORE PERSISTENCE (Codex catch): classification lived only on the
   row._tcData JS property, which does NOT survive the outerHTML/innerHTML
   snapshot+restore the live tool-call group uses on session switch/restore —
   a restored memory/skill row would be re-counted as a generic tool and the
   suffix would silently vanish. buildToolCard now also stamps durable
   data-memory-save / data-skill-update attributes, and _syncToolCallGroupSummary
   counts them as a fallback when _tcData is absent. Verified live across a real
   outerHTML round-trip: label identical before/after.

Replaces the PR's static source assertions with a node-driven behavioral test
(11 cases) covering the real action vocabularies, exclusions, case-insensitivity,
null-arg safety, and the durable-attribute persistence guard.

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

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

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

---------

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

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

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

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: sky <example@email.com>
2026-06-05 14:27:38 -07:00
nesquena-hermes
8ef698ea05
Release v0.51.277 — Release IS (stage-p3f — preserve context-window in usage indicator #3663) (#3683)
* 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
405c38b7d2
Release v0.51.272 — Release IN (stage-p3a — conflict-safe self-update recovery #3667) (#3678)
* fix(updates): use stash apply (not pop) for conflict-safe self-update recovery (#3667)

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

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

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
2026-06-05 12:28:31 -07:00
nesquena-hermes
d882949173
Release v0.51.271 — Release IM (stage-m1 — named custom provider binding #3626) (#3676)
* 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
9b933e2c83
Release v0.51.266 — Release IH (stage-r16) (#3641)
## Release v0.51.266 — Release IH (stage-r16)

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

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

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

Co-authored-by: nesquena <nesquena@users.noreply.github.com>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
2026-06-05 00:44:03 -07:00
nesquena-hermes
6703978c60
Release v0.51.261 — Release IC (stage-r11) (#3616)
## Release v0.51.261 — Release IC (stage-r11)

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

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

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

Co-authored-by: v2psv <v2psv@users.noreply.github.com>
2026-06-04 16:46:20 -07:00
nesquena-hermes
ba987040c7
Release v0.51.260 — Release IB (stage-r8) (#3614)
## Release v0.51.260 — Release IB (stage-r8)

Un-held safety fixes (author resolved my earlier hold findings; re-reviewed fresh) + a clean fix batch. 6 PRs.

### Fixed
| Issue/PR | Author | Fix |
|----------|--------|-----|
| #3535 (#3538) | @rodboev | **Self-update recovers from a stash-pop conflict without data loss.** Was a BRICK bug (`git reset --merge` + `git stash drop` discarded local mods while reporting success). Now keeps the stash, returns `ok:false` + "preserved in `stash@{0}`", no restart on conflict. *(was held — fix verified)* |
| #1909 s3 (#3562) | @rodboev | **Auth `Secure` cookie no longer locks out plain-HTTP LAN/Tailscale users.** Secure now keys only on real TLS evidence (env / TLS socket / opt-in `TRUST_FORWARDED_PROTO`); non-loopback plain-HTTP is no longer force-Secure. SameSite back to `Lax`. *(was held — fix verified)* |
| #2785 (#3559) | @franksong2702 | Clearer cron/gateway diagnostics for single-container Docker (gateway configured, no daemon → jobs silently don't fire). |
| #3555 | @lambyangzhao | Long TTS responses chunked at sentence boundaries (works around the browser's ~32K silent-truncation). |
| #3340 (#3342) | @rly09 | Persistent-state toast when a turn has saved memory / created-updated a skill. |
| #3533 | @franksong2702 | `/reload-mcp` marked `cli_only` so the WebUI doesn't dispatch it as an LLM prompt. |

### Gate
- Full pytest suite: **7681 passed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): **SAFE TO SHIP** — confirmed the stash-conflict path never drops the stash / never restarts on conflict, auth Secure handles LAN-HTTP correctly with no header-forgery hole, `/reload-mcp` allowlisted, state-toast has a real backend writer + active-session guard, diagnostics leak no paths, TTS chunking preserves order.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Co-authored-by: lambyangzhao <lambyangzhao@users.noreply.github.com>
Co-authored-by: rly09 <rly09@users.noreply.github.com>
2026-06-04 15:21:41 -07:00
nesquena-hermes
1bad85feb4
Release v0.51.258 — Release HZ (stage-r6) (#3611)
## Release v0.51.258 — Release HZ (stage-r6)

Fresh-arrival low-risk pair (both @rodboev, landed in the last sweep window).

### Fixed
| Issue | Fix |
|-------|-----|
| #3597 | The "update available" banner now shows from **any panel** (Settings → System "Check now", etc.), not just the Chat view — it was positioned inside the chat surface so it only rendered there. |
| #3592 | Under Simplified Tool Calling, an assistant turn with **thinking but no tool calls** now renders that thinking inline on settlement instead of burying it in an empty collapsed activity group. |

### Review fix absorbed (Codex)
#3592's inline-render `continue` skipped the activity-group creation that carried the turn's `data-turn-duration`, but the footer still suppressed the "Done in …" duration for any `assistantThinking` turn → thinking-only turns silently lost their duration display. Fixed: footer duration is now suppressed **only** for turns that actually build an activity group (`toolCallAssistantIdxs.has(mi)`), so thinking-only inline turns keep "Done in …". + regression test.

### Gate
- Full pytest suite: **7631 passed, 0 failed**
- ESLint: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (duration-drop) → fixed → **SAFE TO SHIP**

### Sweep note
#3603 + #3604 (sidebar CLI-session classification, same author/area) were **not** included — they assert contradictory models for a sidecar-less `source='cli'` recovery row; flagged on both PRs for the author to reconcile.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-04 14:28:00 -07:00
nesquena-hermes
11c0d1667f
Release v0.51.254 — Release HV (stage-r2) (#3593)
## 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)
## 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
c243126ccb
Release v0.51.245 — Release HM (stage-q17) (#3511)
## Release v0.51.245 — Release HM (stage-q17)

Small UX bug-fix.

### Fixed
| Issue | Author | Fix |
|-------|--------|-----|
| #3338 | @rodboev (#3502) | **Messaging sessions (Telegram, Discord, WeChat, …) now show their platform source badge in the chat-pane topbar**, not just the sidebar. The topbar badge was gated on `is_cli_session` (intentionally `false` for messaging sources), so it vanished once the session opened. Gate removed; a recovered native session stamped `source_label:"WebUI"` stays un-badged. Reuses the existing `.topbar-source-badge` styling — no new chrome. |

Picked #3502 over the duplicate **#3499** (same issue/files) — #3502 adds the `WebUI` self-source suppression and a stronger regression test. #3499 closed as superseded with credit.

### Gate
- Full pytest suite: **7543 passed, 0 failed** (first run hit the known boot-cascade flake — 376 connection-refused across 29 files; clean on re-run, as expected for a JS-only change)
- ESLint: CLEAN · browser-smoke: CLEAN · 8 source-contract tests pin the fix
- Codex (regression): **SAFE TO SHIP** — both renderers (panels.js + ui.js) fixed consistently, WebUI-suppression correct, read-only suffix intact, `textContent` XSS-safe

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-03 18:46:42 -07:00
nesquena-hermes
c8f8ca3428
Release v0.51.244 — Release HL (stage-q16) (#3509)
## Release v0.51.244 — Release HL (stage-q16)

UX-approved direction via Telegram (workspace drag-drop polish you requested). All 4 drag-drop flows verified live in-browser.

### Added
| PR | Author | Feature |
|----|--------|---------|
| #3402 / #3424 | @pamnard | **Drop OS files/folders onto a specific workspace folder row or breadcrumb** to upload into that directory (not just the current dir). OS folder drops are traversed (`webkitGetAsEntry`/`readEntries`) preserving nested structure. Uploads via the existing `/api/workspace/upload` (no new backend). |

### Fixed
- **Composer drop-zone jank**: dragging a workspace file (or OS file) over the composer footer rendered a translucent overlay that let the textarea/chips/icons bleed through and collide with the hint text. Now a clean, fully-opaque box with a single centered **context-aware** label — *"Drop to insert workspace reference"* (workspace file → `@path` insert) vs *"Drop files to attach"* (OS file → message attach).
- **Drag-drop handler coexistence (CORE, caught in review)**: #3424's OS-upload binding assigned `el.ondrop` on folder rows, which **overwrote** the drag-to-move handler from #3422 (also `el.ondrop`) — silently breaking move-to-folder (the ws-path drop fell through to the composer as an `@path` insert). Fixed by binding the OS-upload handlers via `addEventListener` so they compose; each handler gates on its own drag type.

### Drag-drop matrix — all verified LIVE in-browser (real drag→drop, asserted on disk)
| Flow | Result |
|------|--------|
| OS image → composer footer | ✓ attaches |
| workspace file → composer footer | ✓ inserts `@path` |
| workspace file → workspace folder | ✓ moves on disk (report.md → docs/) |
| OS file → workspace folder | ✓ uploads into target folder |

### Scope note
#3424's PR branch carried the OLD pre-hardening `_handle_file_move`. Applied **frontend-only** — master's hardened move backend (v0.51.243, TOCTOU/symlink fixes) is untouched (Codex confirmed no `api/routes.py` diff).

### Gate
- Full pytest suite: **7542 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint: CLEAN · ruff: CLEAN · browser-smoke: CLEAN
- Codex (regression): CORE handler-clobber → fixed → **SAFE TO SHIP**

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

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

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

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

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

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

Co-authored-by: pamnard <pamnard@users.noreply.github.com>
2026-06-03 17:41:11 -07:00
nesquena-hermes
1d7a139c26
Release v0.51.240 — Release HH (stage-q12) (#3496)
## Release v0.51.240 — Release HH (stage-q12)

Mobile UX bug-fix — UX-approved via Telegram.

### Fixed
| PR | Author | Fix |
|----|--------|-----|
| #3470 | @cnogrin | On mobile/touch, you can now **swipe up to stop streaming auto-scroll**. Previously the view snapped to the bottom on every token with no way to read earlier content while a response streamed — `_recordNonMessageScrollIntent()` only detected upward intent on the wheel path (`e.deltaY`), and touch events have no `deltaY`. Now tracks `_touchStartY` on `touchstart` and treats a `touchmove` that drags the finger down >8px (= scroll up into history, `scrollTop` decreases) as upward intent, setting the same `_messageUserUnpinned` flag the wheel path + scroll listener use. |

### Why this is safe for existing installs
- **Only ADDS a touch-unpin path** — wheel + desktop behavior completely untouched, no existing branch modified.
- New `touchstart`/`touchend`/`touchcancel` listeners are **passive + capture-only** (they only write `_touchStartY`), so they can't interfere with existing touch handling.
- Net effect for users: mobile users *gain* the ability to scroll up during streaming (which was simply broken before). No one's working flow changes.

### Absorbed on review (Codex CORE MUST-FIX)
The contributor's gesture sign was inverted (`dy<-8` = finger up = *follow* the stream), which would have unpinned in the wrong direction. Corrected to `dy>8` to match the existing scroll listener's `movedUp` semantics; fixed the comment; strengthened the regression test to pin the gesture direction.

### Gate
- Full pytest suite: **7503 passed, 9 skipped, 3 xpassed, 0 failed**
- ESLint runtime gate: CLEAN · browser-smoke: CLEAN
- Codex (regression): SHIP-ONLY-WITH-FIXES (inverted sign) → fixed → re-reviewed **SAFE TO SHIP**
- Regression test `tests/test_issue3470_touch_unpin_streaming_scroll.py` (verified to fail against master)

UX-approved via Telegram (mobile touch-gesture behavior, no visual/layout delta to screenshot).

Co-authored-by: cnogrin <cnogrin@users.noreply.github.com>
2026-06-03 14:33:50 -07:00
nesquena-hermes
7c6fb33549
Release v0.51.229 — Release GW (stage-p13 — /model never silently snaps a versioned name to a -tier variant #3368) (#3465)
* Release v0.51.229 (stage-p13): /model never silently snaps versioned name to -tier variant (#3368, @nesquena-hermes)

Agent-authored, nesquena-APPROVED. Rebased onto current master. Both _findModelInDropdown
(ui.js) and _bestModelMatch (commands.js) reject a prefix-snap when the typed target ends
in a version digit and the longer option's extra text is a variant/tier suffix (.pro) rather
than a version continuation (.digit). Adds _nearestModelSuggestion + 'did you mean?' toast.
34 tests pass (14 new #3368 + 20 regression: #1188 fuzzy + #3360 collision).

* fix(commands): /model did-you-mean toast renders suggestion + single quotes (#3368 review)

Live-render review of the approved #3437 caught two toast-assembly bugs in cmdModel:
(1) t('model_did_you_mean') was called WITHOUT the suggestion arg — model_did_you_mean
is a (m)=>... template that t() invokes, so it rendered 'did you mean "undefined"?';
fixed to t('model_did_you_mean', suggestion). (2) no_model_match already ends with an
opening quote, so '"${args}"' doubled it ('No model matching ""deepseek-v4""'); fixed
to '${args}"'. +4 source-assertion regression tests. Verified live: toast now reads
'No model matching "deepseek-v4" — did you mean "deepseek/deepseek-v4-pro"?'.

* fix(commands): slash-qualified versioned no-snap falls through to suggestion (#3368 Codex CORE)

Codex review found a 2nd no-snap layer the version guard missed: for a slash-qualified
versioned query (e.g. 'xiaomi/mimo-v2.5') whose only near catalog entry is a rejected
tier variant ('xiaomi/mimo-v2.5-pro'), cmdModel's cross-provider /api/session/update
fallback would silently persist the invalid model + 'Switched to...'. Now gated on
!versionedNoSnap (_looksLikeVersionedModel(bare) && a near suggestion exists) so it falls
through to the 'did you mean?' toast; genuinely off-catalog providers (no near variant)
still direct-update. Verified live: '/model deepseek/deepseek-v4' no longer switches, shows
suggestion toast. +1 regression test.

---------

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

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

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

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

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

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

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

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-02 19:48:01 -07:00
nesquena-hermes
264863cc37 fix(config): extend #3429 URI-scheme fix to backend normalization + model-key matching (#3436, @b3nw)
v0.51.218 fixed getModelLabel() (the visible chip) for URI-scheme model IDs but
left the same first-segment-slash-strip bug in the matching/dedup paths:
api/config.py _norm_model_id + _get_label_for_model, and static/ui.js
_normalizeConfiguredModelKey. For gpt://folder/model/latest those treat the
path slashes as provider delimiters, mis-normalizing the model-identity key
(the #3360 collision class, for URI ids). Adds a URI-scheme guard to all three
so the scheme is not stripped. Kept v0.51.218's getModelLabel (more thorough
than #3436's variant — it extracts the model name vs returning the whole id);
took #3436's backend + _normalizeConfiguredModelKey halves with backend/frontend
parity tests.

Co-authored-by: b3nw <b3nw@duck.com>
2026-06-02 21:04:02 +00:00
nesquena-hermes
25e95a7366 fix(ui): #3429 round-2 — never fall back to authority or placeholder (Codex MUST-FIX)
Codex re-check: degenerate URIs still leaked — gpt://folder123 returned the
authority, gpt://folder123/${MODEL} returned the placeholder. Removed the _all[0]
authority fallback and guarded the literal-last-path fallback against placeholders;
a URI with no usable model segment now falls back to the raw id. Added regression
cases for gpt://folder123 and gpt://folder123/${MODEL}.
2026-06-02 20:49:29 +00:00
nesquena-hermes
c223b418a5 fix(ui): #3429 follow-up — drop authority, keep digit-leading model names (Codex MUST-FIX)
Codex gate found two edges in the first cut: (1) the candidate segment list
included the URI authority, so gpt://folder123/v4 and .../latest returned the
folder; (2) _isVersionTail matched any digit-leading segment, dropping a real
model named 2026-model. Fix: build path segments from AFTER the authority only;
tighten the version-tail regex to pure version tokens (latest/stable/v4/1.2),
not mixed names; fall back to last-usable (non-placeholder) path segment so the
authority is never returned. Added edge-case regression tests.
2026-06-02 20:44:59 +00:00
nesquena-hermes
c81c9649dd fix(ui): getModelLabel must not mangle URI-scheme model IDs (#3429 regression from #3366)
#3366 changed getModelLabel() to strip only the first /-segment (fixing #3360
multi-slash proxy IDs). That regressed URI-scheme IDs like Yandex
gpt://${FOLDER}/deepseek-v4-flash/latest — indexOf('/') lands inside the ://
and leaves /${FOLDER}/... path junk in the composer model chip. Detect a
scheme:// id, drop scheme+authority, and take the last meaningful path segment
(skipping ${...} env-var placeholders and bare version tails like latest).
Non-URI multi-slash IDs keep the #3360 first-segment-strip behavior unchanged.
Node-driven regression test covers the URI case + the #3360 non-regression.
2026-06-02 20:36:59 +00: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
b3nw
d06776a4c8 fix: model picker snaps to wrong model with multi-slash IDs (#3360)
When a custom/proxy provider serves models whose IDs share the same base
name across vendor prefixes (e.g. vendor_a/deepseek/deepseek-v4-pro vs
vendor_b/deepseek/deepseek-v4-pro), several normalization functions use
split('/').pop() (or split('/')[-1]) which discards all segments except
the last.  This causes three user-facing symptoms: (1) clicking one
model selects a different colliding model, (2) configured-model badges
attach to the wrong dropdown entry, and (3) the model-chip label in the
composer bar is truncated to just the base model name.

Root cause: all three callers take only the last slash-segment instead
of stripping only the first (provider) segment and preserving the
remaining vendor hierarchy.

Fix 1 — _findModelInDropdown (static/ui.js): Move the exact string match
before the provider-aware normalized match.  Previously, when all models
share the same provider ID (common with LLM proxy setups), the normalized
match returned whichever colliding option appeared first in DOM order,
even though an exact match existed.

Fix 2 — _normalizeConfiguredModelKey (static/ui.js) and _norm_model_id
(api/config.py): Replace split('/').pop() / split('/')[-1] with a first-
segment-only strip (regex on frontend, split('/',1) on backend), matching
the strategy already used by _findModelInDropdown's norm lambda.  This
prevents multi-slash IDs from colliding in badge assignment and the
configured-entry dedup set.  Additionally, strip colon-qualified provider
prefixes (e.g. custom:name/) before the slash strip so badge-key variants
like 'custom:llm-proxy/opencode_go/model' merge correctly with the bare
'opencode_go/model' in the configured section dedup.

Fix 3 — getModelLabel (static/ui.js) and _get_label_for_model
(api/config.py): Same split('/').pop() to first-segment-strip change so
the composer-bar model chip and backend label preserve vendor context
(e.g. shows 'opencode_go/deepseek-v4-pro' instead of 'deepseek-v4-pro').

Verification: 9 new regression tests (test_issue3360) covering exact-
match priority, multi-slash normalization, and backend/frontend parity.
Updated 1 existing test (test_norm_model_id_trailing_empty_guard) that
asserted the old split('/').pop() pattern.  All 25 related tests pass.

AI Usage: Gemini (gemini-2.5-pro), via Antigravity IDE, pair-programmed.

(cherry picked from commit a454fecd2b3a83f7da34473c883be069171aeac9)
2026-06-02 05:08:30 +00:00
nesquena-hermes
08fe4f51f0 fix(#2931): Edge TTS playback path (GET->POST), stoppable hands-free audio, test isolation
Both advisors (Opus MUST-FIX + Codex SHIP-ONLY-WITH-FIXES) caught that the Edge
TTS playback was broken despite the settings selector working:

1. ui.js _playEdgeTts used new Audio('/api/tts?text=...') — a GET — but /api/tts
   is POST-only (405) and registered only in handle_post. The per-message speaker
   button + auto-read silently failed in edge mode, and the GET leaked message
   text into the query string/access log. Rewritten to POST JSON + blob object
   URL (mirrors the working boot.js path), and now surfaces server errors
   (503 not-installed, 429 rate-limit) via toast instead of silent dead air.
2. boot.js hands-free Edge audio was a local var never assigned to the shared
   _playingEdgeAudio handle, so stopTTS() (from _deactivate) couldn't stop it.
   Now registered + cleared on end/error.
3. Test isolation: the _tts_limiter function-attribute singleton persisted across
   the whole suite, flaking 2 of my endpoint tests in the full run. Converted the
   reset to an autouse fixture (before+after each) + unique per-test client IPs.
2026-06-02 01:58:13 +00:00
nesquena-hermes
1c29d6ac9c feat: add Edge TTS as alternative speech engine (#2931, @liuqiangweb-svg) 2026-06-02 01:21:39 +00:00