Commit Graph
515 Commits
Author SHA1 Message Date
Rod Boev 4329768288 feat(topbar): badge pending prompts in document title (#4121) 2026-06-13 15:04:20 -04:00
Rod Boev 707a19e34c Keep busy-slash regressions aligned with /yolo support 2026-06-13 14:15:35 -04:00
Rod Boev 6c7d551b41 fix(commands): keep slash command ownership ahead of bundles (#4087) 2026-06-13 14:06:35 -04:00
Rod Boev 5a3c6c6f05 fix(commands): surface skill bundles in WebUI slash commands (#4087) 2026-06-13 14:06:35 -04:00
Rod Boev 61ddf4d21e fix(commands): run /yolo from busy slash intercept (#4117) 2026-06-13 13:59:04 -04:00
nesquena-hermes 310edac58e fix(#4108): add creation-time backstop cleanup to bound the registry-leak class
Codex found leak paths across 3 rounds (settled-restore, stream-end-fallback, reconnect-
preflight-inactive, stale-bailout, non-current-pane error, external cancelSessionStream).
Rather than chase every current+future teardown route, schedule an identity-guarded cleanup
at registry CREATION (600s) so window._liveAnchorRegistries self-expires no matter how the
stream ends — including external paths (boot.js cancelSessionStream) that bypass the SSE
handlers. Explicit terminal-path calls just expire it sooner. Shadow-only + identity-guarded,
so a rare >10min active stream only loses reconnect-continuity of an unconsumed registry.
2026-06-13 16:19:18 +00:00
nesquena-hermes 3cf96b1293 fix(#4108): cleanup anchor registry on reconnect-preflight-inactive + stale-stream-bailout paths (Codex round 2)
Two more early-return paths created the registry (early in attachLiveStream) then
returned without scheduling cleanup: (1) reconnect preflight when /api/chat/stream/status
is inactive/no-replay (~3966); (2) _bailOutOfTerminalEventsFromStaleStream (~1490) when a
stale stream's terminal event arrives after a newer stream owns the session. Both now
schedule identity-guarded cleanup (map.get(streamId)===registry, keyed on the stale
closure's own streamId so the newer registry is untouched).
2026-06-13 16:13:55 +00:00
nesquena-hermes 9594b97223 fix(#4108): schedule anchor registry cleanup on settled-restore + stream-end-fallback terminal paths (Codex leak catch)
Codex found window._liveAnchorRegistries retained registries for streams ending via
_restoreSettledSession() and _finalizeStreamEndFallback() (both terminal-close without
cleanup). Added _flushReasoningToAnchor()+_scheduleAnchorRegistryCleanup() to both —
identity-guarded delete already prevents clobbering a newer registry. All anchor helpers
are nested in attachLiveStream's closure (hoisted), ESLint runtime gate clean.
2026-06-13 16:09:20 +00:00
Rod Boev 59562e416a fix(#4071): wire /credits through WebUI command dispatch 2026-06-13 07:50:16 -04:00
Frank Song aa96bc52bc Add assistant turn anchor live shadow feed 2026-06-13 16:26:52 +08:00
nesquena-hermes e4bdcbfeae Merge #3823 (tool iteration limit terminal state) onto master
# Conflicts:
#	CHANGELOG.md
2026-06-13 05:06:46 +00:00
nesquena-hermes c02c31c837 Merge #4006 (scroll auto-follow setting + pin guards) onto master
# Conflicts:
#	api/config.py
#	static/panels.js
2026-06-13 03:54:11 +00:00
nesquena-hermes 92f7a24448 Merge #4024 Transparent Stream onto master (rebase)
# Conflicts:
#	CHANGELOG.md
2026-06-13 00:40:17 +00:00
nesquena-hermes 1e5a741f50 Merge #4054 into stage-mj 2026-06-12 20:14:42 +00:00
nesquena-hermes 10481d4f34 Merge #4051 into stage-mj 2026-06-12 20:14:42 +00:00
nesquena-hermes 99ee11a733 Merge #4046 into stage-mj 2026-06-12 20:14:42 +00:00
Rod Boev 65c0864e34 fix(approval): clear stale approval cards while busy 2026-06-12 11:55:43 -04:00
Rod Boev 80dab6f3eb fix(commands): inject forced skill content for /use (#4044) 2026-06-12 08:34:27 -04:00
Rod Boev 1529dd0841 fix(commands): route /reload-skills through the WebUI allowlists (#4043) 2026-06-12 08:31:11 -04:00
Rod Boev 7795f29946 fix(composer): preserve send-button sync under scheduled resize (#4042) 2026-06-12 08:18:04 -04:00
Rod Boev aab43cdaff fix(approval): keep approval polling alive across busy dips (#4041) 2026-06-12 08:12:57 -04:00
Rod Boev 768ea5ee87 fix(composer): coalesce composer auto-resize reflows (#4042) 2026-06-12 07:54:48 -04:00
Mohamed Habib 306a148fbf merge: sync transparent stream branch with master 2026-06-12 05:09:01 +03:00
Frank Song ec23dac922 Handle tool limit terminal state 2026-06-12 10:03:31 +08:00
Mohamed Habib fcfaa4686a feat: add transparent stream activity display 2026-06-12 04:45:41 +03:00
Ming e05b99b7fd fix(scroll): guard _scrollAfterMessageRender and _finishDone against pin hysteresis snap-back
Two scroll-state bugs cause the viewport to snap to the bottom after
streaming completes, even when the user has scrolled up to read:

1. _scrollAfterMessageRender (ui.js): when preserveScroll=true and
   _scrollPinned=true (re-set by the 250px near-bottom hysteresis),
   _followMessagesAfterDomReplace() calls scrollToBottom(), overriding
   the user's position. Added !_messageUserUnpinned guard so explicitly
   scrolled-up users get their snapshot restored instead.

2. _finishDone (messages.js): the explicit scrollToBottom() at L3302
   fires when shouldFollowOnDone is true, but that flag only checks a
   120px near-bottom threshold. Added _isMessagePaneNearBottom(250)
   gate so users reading 250px+ above bottom keep their position.

Also adds a new 'auto_scroll_follow' setting (default: off) that gates
scrollIfPinned() and _shouldFollowMessagesOnDomReplace(). When disabled,
the viewport never auto-scrolls during streaming — the user controls
scroll position manually and uses the ↓ button to jump to bottom.

7-file setting-add pattern applied:
- api/config.py: _SETTINGS_DEFAULTS + _SETTINGS_BOOL_KEYS
- static/boot.js: both init paths
- static/ui.js: scrollIfPinned() + _shouldFollowMessagesOnDomReplace()
- static/index.html: checkbox in appearance settings
- static/i18n.js: en + zh-CN translations
- static/panels.js: payload, autosave, populate, full save
- messages.js: no additional changes (gated via _shouldFollowMessagesOnDomReplace)

Tests updated: test_tars_scroll_reset_regressions, test_issue1690,
test_issue3545. All 116 scroll-related tests pass.
2026-06-11 21:48:14 +02:00
nesquena-hermes 2cbae63a8d fix(#3996): preserve pending sid on hidden-tab skip so a bg-tab session reopens (Codex CORE)
If startSessionStream(sid) runs while the tab is ALREADY hidden (session loaded/
restored in a background tab), it set _sessionStreamSessionId then returned at the
hidden-skip WITHOUT recording _sessionStreamHiddenSid — so the visibility handler
(which reopens only from the holder) never reattached, silently dropping
bg_task_complete/server_turn_started until the next explicit startSessionStream.
Set _sessionStreamHiddenSid = sid on the hidden-skip return. Adds a regression test.
2026-06-11 19:17:11 +00:00
nesquena-hermes 5fdfb70bce fix(#3996): reopen per-session SSE on tab re-show (reopen-bug fix)
#3996 closes the gateway + per-session SSE on hidden tabs to free HTTP/1.1
connection-pool slots (#3992). But the per-session-stream visibility hook
guarded its reopen on _sessionStreamSessionId, which stopSessionStream() nulls —
so the per-session SSE closed on hide but never reopened on re-show (regressing
live-stream reattach). Capture the id into a dedicated _sessionStreamHiddenSid
before the close and reopen from it on visibility return. Gateway hook (sessions.js)
was already correct (reopens unconditionally + re-guards internally).
2026-06-11 19:10:38 +00: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
nesquena-hermesandt3chn0pr13st 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-hermesandnesquena-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-hermesandnesquena-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
aa5cd6e8ff Release v0.51.346 — Release LJ (PWA notification controls #3229) (#3897)
* Add PWA notification controls (#3229, #3196)

Rebased onto fresh master (#3229 was 281 commits behind). Routes browser
notifications through the service worker (reg.showNotification) with a direct
Notification fallback, threads the originating session deep link/tag into the
payload, and adds Enable/Send-test/permission-status controls in Settings.
notificationclick prefers an already-open target-session tab before opening a
new window. Added the 6 new i18n keys to the Polish (pl) locale block, which
the original PR missed.

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

* Apply Opus review fixes to #3229 PWA notifications

- MUST-FIX: _showPwaNotification used navigator.serviceWorker.ready, which
  never settles when no SW registration ever activates (reverse-proxy MIME
  mishap, SW disabled) — every notification was silently dropped, a regression
  vs master. Switch to getRegistration() raced against a 2s timeout, falling
  back to new Notification().
- SHOULD-FIX: notificationclick matched client.url === targetUrl exactly, but
  _sessionUrlForSid copies the current query/hash into the deep link, so an
  already-open session tab missed the match and spawned a duplicate window.
  Compare pathnames instead.
- SHOULD-FIX: "Send test" with permission denied was a silent no-op — now
  surfaces the notifications_denied toast on the force path.
- SHOULD-FIX: permission-status span went stale after granting via the button
  — refresh it in requestNotificationPermission().then.
- Consistency: the request-permission grant branch now has the same
  new Notification fallback as the granted branch.
- Update test_pwa_notification_controls.py to assert the pathname match.

* Stamp v0.51.346 — Release LJ (PWA notification controls #3229/#3196)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: AJV20 <AJV20@users.noreply.github.com>
2026-06-09 15:54:21 -07:00
nesquena-hermes 26e133e3e8 [HELD — independent review pending] Release v0.51.340 — bg_task agent wakeup (trio #2968+#2971+#2979) (#3867)
* 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
8d4ed1f90d Release v0.51.338 — Release LB (saved prompts library, #3571) (#3860)
* feat(composer): add saved prompts library with per-profile storage (#2732)

* fix(composer): move saved-prompts popup out of .composer-left to preserve DOM test (#2732)

* fix(composer): correct ARIA roles, add server-side prompt limits (#2732)

* fix(composer): surface save-prompt errors instead of silent success toast (#2732)

* Release v0.51.338 — Release LB (saved prompts library, #3571)

Composer saved-prompts library (@rodboev): bookmark button → popup of saved
prompts; click to insert, save current input, delete. Persists to
$HERMES_HOME/webui/saved_prompts.json with server-side caps (8000 chars / 200).

Maintainer work (per Nathan): conditions were (a) verify it actually works and
(b) hide on mobile. Both met:
- Live-verified load/save/delete all persist through the UI.
- Added mobile-hide (#btnSavedPrompts,.saved-prompts-popup display:none in the
  @media max-width:640px + 900px composer blocks). DOM-verified visible at 1280px,
  vision-confirmed absent from the composer at 390px.
- Added missing Polish (pl) i18n for the 5 saved_prompts_* keys (PR had en+others
  but not pl — failed locale-parity).
- Added tests/test_issue3571_saved_prompts.py (mobile-hide + caps + wiring guards).

Full suite green, ESLint/scope-undef CLEAN, Opus SHIP-safe (auth-gated, CSRF,
XSS-safe, sane caps), 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: rodboev <rodboev@users.noreply.github.com>
2026-06-08 19:32:12 -07:00
3063c4930f Release v0.51.336 — Release KZ (fix per-token inline-thinking perf regression, #3633 follow-up) (#3854)
* fix(streaming): make per-token inline-thinking extraction linear (#3633 follow-up)

Codex post-merge perf catch on #3633: _parseStreamState() and
syncInflightAssistantMessage() call _extractInlineThinkingFromContent on the
FULL accumulated assistantText on EVERY streamed token. The #3633 rewrite made
that a full char-by-char walk, so cost was O(n^2) over a stream — a Node harness
measured ~88s (no-tag) / ~103s (leading <think> block) for 2000x100-char tokens,
which would freeze the main thread on long reasoning-model responses.

Two fixes (Python api/streaming.py + JS static/messages.js twin, line-for-line parity):
1. Fast path: if the text contains no complete thinking opener AND (when
   streaming) its tail is not a prefix of an opener, return unchanged without the
   char walk — two cheap substring scans. Handles the common no-tag case.
2. Bulk-skip plain trailing content: track the next complete opener via
   str.find/indexOf (_next_inline_thinking_opener / _nextThinkingOpener); once no
   opener remains ahead, append the remainder and stop instead of walking it
   (streaming still suppresses a trailing partial-opener prefix). Handles the
   leading-block-then-long-answer case.

Result: ~88s/103s → ~0.5s/0.9s (Python), ~0.18s/0.21s (JS). All behavioral cases
(persist + streaming, code-awareness, position-aware unclosed, leading whitespace)
verified unchanged in both twins. Added a per-token streaming perf regression test
and wired _nextThinkingOpener into the node driver harness.

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

* fix(streaming): perf bulk-skip must respect code context for partial-opener tails

Codex catch on the perf fix: the no-complete-opener bulk-skip suppressed a
trailing partial opener (e.g. '<thi') unconditionally during streaming, but a
partial opener INSIDE inline-backtick / fenced / indented code must stay visible
(master parity). Now, when streaming and the tail is a partial opener, fall
through to the code-aware char walk (bounded — a partial tail is a transient
single token) instead of bulk-skipping; only a PLAIN-text partial opener is
suppressed as a forming block. Added _text_tail_is_partial_opener /
_textTailIsPartialOpener (Python + JS parity) + regression tests for the
inside-code vs plain partial-tail cases.

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

* docs(changelog): v0.51.336 KZ — inline-thinking streaming perf fix

---------

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes-agent@nesquena-hermes.local>
2026-06-08 18:16:52 -07:00
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
52993af88a Release v0.51.333 — Release KW (collapse old interim progress notes, #3574) (#3848)
* feat(streaming): collapse old interim progress notes after 3 visible (#2403)

* fix(streaming): delegated handler for interim-collapse toggle survives live-turn restore

The interim-collapse toggle attached its click listener via per-element
addEventListener at creation time. snapshotLiveTurnHtmlForSession /
restoreLiveTurnHtmlForSession rebuild the live turn via outerHTML/innerHTML
on session switch, which strips JS listeners — so a restored toggle was
visible but inert and the collapsed interim notes became permanently
unreachable for the rest of the turn.

Replace with a stateless document-level delegated click handler
(_interimCollapseDelegatedClick) that resolves the toggle via closest(),
reads state from the DOM (.interim-collapsed) + data-threshold, and works
on both freshly-created and innerHTML-restored toggles. Add 4 regression
tests pinning the delegated-handler contract.

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

* Release v0.51.333 — Release KW (collapse old interim progress notes, #3574)

Collapse old interim progress notes after 3 visible during a live turn
(#3574, @rodboev). Maintainer fix during re-gate: replaced the per-element
toggle listener with a stateless document-level delegated handler so the
toggle survives the live-turn DOM restore (Codex caught: innerHTML rebuild
dropped the listener → collapsed notes unreachable). Full suite 8303,
ESLint/scope-undef CLEAN, Opus SHIP-safe, Codex SAFE-TO-SHIP after fix,
collapse + manual-expand-guard + restore-path delegated handler all live-verified.

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:36:39 -07:00
nesquena-hermes ee982a7581 Release v0.51.322 — Release KL (sortable + filterable markdown tables, #3728) (#3803)
#3728 (@rodboev): markdown tables sortable+filterable on desktop, suppressed <=640px so mobile is unchanged. UX-approved by Nathan via Telegram. Full suite 8234, CI 11/11, live-driven. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-07 16:35:28 -07:00
65c4bc9fa2 Release v0.51.295 — stage-3739/3742 (model-pick revert fix #3739 + session-status revert #3742) (#3743)
* fix: honor explicit model pick, suppress silent revert on cross-family selection (#3737)

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

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

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

Closes #3737

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

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

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

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

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

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

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

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

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

reverts #3570

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

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

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

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

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

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

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

---------

Co-authored-by: John Doe <johndoe@example.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 13:39:14 -07:00
e3a7c93dc6 [HELD — independent review pending] Release v0.51.294 — stage-3401 (live-to-final redesign #3401 + 4 deep-review fixes) (#3741)
* Harden interrupted recovery control filtering

* Redesign live-to-final assistant replies

* Fix live activity anchor test fixture

* Fix CI lint issues for live reply tests

* Strengthen live progress prompt contract

* Recover PR #3401 refresh on origin/master

* Repair live-to-final refresh regressions

* Fix live worklog refresh regressions

* Show live footer timer on initial stream start

* Restore live stream shell after reload

* Preserve per-frame live SSE replay cursors

* Preserve reasoning as Worklog Thinking cards

* Quiet Worklog Thinking card styling

* Align Worklog Thinking card styling

* Scope live Worklog Thinking cards by segment

* Suppress exact duplicate settled Thinking

* Close #3401 merge review test gaps

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

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

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

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

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

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

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

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: Nathan-Hermes <nesquena-hermes@users.noreply.github.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-06 12:12:37 -07:00
9720e0b03d Release v0.51.292 — Release JH (stage-s4 — compression-exhausted turns surface as errors #3316 fixes #3315) (#3705)
* fix(#3315): surface compression-exhausted/no-final-answer turns as errors (#3316)

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

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

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

---------

Co-authored-by: Frank Song <franksong2702@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 22:45:09 -07:00
nesquena-hermesandnesquena-hermes 534280e058 Release v0.51.291 — Release JG (stage-s2 — preserve live turn content on switch-away #3668) (#3704)
* fix(#3668): snapshot live turn before stream teardown on session switch

The 'stays gone' variant: switching away from a streaming session during a quiet
window (mid tool-exec / silent thinking, between content SSE events) left a
stale/absent live-turn snapshot, so restoreLiveTurnHtmlForSession() failed on
switch-back and loadSession()'s fallback rebuilt with an empty appendThinking(),
permanently losing streamed thinking/tool content (only the elapsed clock
survived). closeLiveStream() now snapshots the live-turn DOM via
snapshotLiveTurnHtmlForSession(sessionId) BEFORE closing the source + tearing
down LIVE_STREAMS, so switch-back always restores the exact state shown at
switch-away. + regression test asserting snapshot precedes teardown.

* docs(changelog): v0.51.291 — Release JG (stage-s2, #3668)

---------

Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 22:26:43 -07:00
nesquena-hermesandnesquena-hermes da5bf69aee fix(sidebar): hoist _sessionAttentionState to fix ReferenceError crash (#3696) + scope-undef prevention gate (#3698)
* 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
d4f26f5156 Release v0.51.288 — Release JD (stage-r24 — collapsible approval card #3515) (#3697)
* feat(approval): make the approval card collapsible (#3515)

Adds a collapse toggle to the approval card header so users can shrink it
to a thin header strip and keep the tool-call rationale/transcript above
readable. Full ARIA (aria-expanded/controls/label), chevron swap, and
transcript reflow that preserves near-bottom scroll. Closes #3007.

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

* docs(changelog): v0.51.288 — Release JD (stage-r24)

* fix(approval): clear collapsed state for a distinct queued approval (#3515)

Codex regression-gate finding: showApprovalCard's sameApproval check didn't
include approval_id and didn't clear .collapsed in the !sameApproval branch, so
a NEW/parallel approval arriving while the card was already collapsed could
render collapsed with its command + action buttons hidden. Add approval_id to
the signature; clear .collapsed for a distinct approval before syncing. +2 regression tests.

---------

Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: nesquena-hermes <[email protected]>
2026-06-05 19:52:52 -07:00
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
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
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
58528a4d88 Release v0.51.270 — Release IL (stage-u1 — un-hold batch: #3517 #3624 #3613) (#3674)
* feat(commands): add /use to force a skill for the next turn (#3517, #2977)

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

* fix(auth): cap pending passkey challenges by evicting oldest, not rejecting (#3624)

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

* fix(providers): expose model-provider plugins in WebUI (#3613)

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

* docs(changelog): v0.51.270 — Release IL (stage-u1, 3-PR un-hold batch; #3448 + #3618 dropped)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: Hinotobi <paperlantern.agent@gmail.com>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
2026-06-05 11:35:26 -07:00
442b033e67 Release v0.51.268 — Release IJ (stage-b1 — low-risk perf + provider/clarify fixes) (#3671)
* perf(providers): O(1) codex cache merge membership checks (#3656)

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

* fix(models): add MiniMax-M3 to WebUI MiniMax fallback catalog test (#3627)

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

* fix(config): make DeepSeek reasoning-effort heuristic position-independent (#3650)

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

* fix(clarify): don't stash clarify draft while submission is in flight (#3651)

Co-authored-by: carryzuo00 <carryzuo00@gmail.com>

* perf(sessions): batch lineage report child fetch by parent id (#3659)

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

* perf(sessions): batch orphan sidecar state.db existence probes (#3657)

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

* test(streaming): pin DOM-INFLIGHT reattach invariant (#3572)

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

* docs(changelog): v0.51.268 — Release IJ (stage-b1)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: Pamnard <pamnard@users.noreply.github.com>
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Co-authored-by: happy5318 <happy5318@users.noreply.github.com>
Co-authored-by: carryzuo00 <carryzuo00@gmail.com>
2026-06-05 10:44:02 -07:00