Commit graph

3458 commits

Author SHA1 Message Date
Frank Song
70b019f710 feat: let extension skins declare base scheme 2026-06-30 18:17:40 +08:00
allenliang2022
eabbe9a821 fix(webui): hidden-tab poll keeps retrying when attach defers in multi-pane (#5172 follow-up)
#5172 (render server-initiated turns in a hidden tab, shipped v0.51.743 via
#5218) has a non-blocking multi-pane edge flagged in review: when a
server-initiated turn arrives for a session that is NOT the active pane, the
hidden-tab status poll called _attachServerInitiatedStream() and could not tell
whether the attach actually happened. _attachServerInitiatedStream returned
undefined in all paths, so the poll had no signal — a stop-on-call would cancel
polling while the turn was never rendered, leaving it invisible until the next
user interaction.

Fix: _attachServerInitiatedStream now returns a bool — true when this tab is
responsible for rendering the stream (attached, or already attached to the same
sid/streamId), false when the attach was not consummated (sid not the current
pane, invalid input, or a thrown error). _startHiddenActiveStreamPoll captures
the bool and only calls _stopHiddenActiveStreamPoll() when true; on false it
keeps polling and re-tries on a later tick (e.g. once the user switches the pane
back to sid, or the turn finishes and status returns active_stream_id=null).

Tests in tests/test_hidden_tab_server_initiated_turn.py: assert all return paths
are explicit bool (no bare return), the non-current-pane path returns false, and
the poll gates _stopHiddenActiveStreamPoll on the attach result.
2026-06-30 16:56:45 +08:00
Frank Song
97992e72ad Merge origin/master into PR 5253
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 14:32:18 +08:00
justanotherAIcontributor
91d32c40c5
Merge branch 'master' into fix/large-text-paste-toggle-upstream 2026-06-30 16:13:49 +10:00
Frank Song
7e038aaad3 Merge branch 'master' into franksong2702/fix-transparent-live-snapshot
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 13:54:15 +08:00
Frank Song
3d865e4c1f Merge origin/master into PR #5256
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 13:52:36 +08:00
Frank Song
e01d9ff46e Merge origin/master into PR #5256
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 13:15:47 +08:00
Frank Song
0369e1e9de Merge branch 'master' of https://github.com/nesquena/hermes-webui into franksong2702/fix-transparent-live-snapshot
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 13:12:39 +08:00
Frank Song
3188fe1445 Merge origin/master into PR 5253
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 13:10:55 +08:00
Rod Boev
51a0fca24a fix(#5092): continue WebUI goals after gateway turns 2026-06-30 05:04:32 +00:00
nesquena-hermes
468b61e513 fix(auth): release STREAM_SESSION_OWNERS on worker early-return + stale-run prune
Gate hardening for #5198: the new stream-owner map leaked entries on three
paths that bypass the teardown finally — worker early-return when the stream
was cancelled before startup (streaming.py, gateway_chat.py) and the direct
ACTIVE_RUNS stale-zombie prune (routes.py). Each now calls unregister_stream_owner.
Adds a regression test proving the early-return path no longer leaks.

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-06-30 04:39:00 +00:00
Hermes Agent
fabd461dcd fix(auth): guard profile-owned stream IDs 2026-06-30 04:28:15 +00:00
Hermes Agent
a397b5e078 test(auth): align chat retag fixture with active profile 2026-06-30 04:28:15 +00:00
Hermes Agent
9e861b5651 fix(auth): tolerate query-less route inputs 2026-06-30 04:28:15 +00:00
Hermes Agent
c068dbc39b fix(auth): enforce active-profile session IDs 2026-06-30 04:28:15 +00:00
Frank Song
6ac5b9dbee Fix transparent live anchor scene replay
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 12:27:02 +08:00
Frank Song
2c8f30de35 Merge origin/master into PR 5253
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 12:26:58 +08:00
Frank Song
44d881eede Merge origin/master into PR #5256
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 12:26:28 +08:00
Stacey2911
6b0d8eaadf fix(webui): make large text paste-as-attachment toggleable via settings
Add a user setting 'large_text_paste_as_attachment' (default: on) that lets
users paste large text inline instead of having it auto-converted to .md file
attachments. The toggle lives in Control Center → Chat tab and persists via
settings.json, following the established upstream pattern for boolean settings.
2026-06-30 14:25:53 +10:00
hinotoi-agent
41ec8238ba chore: log file manager profile denials 2026-06-30 04:12:27 +00:00
hinotoi-agent
40679f2af8 test: isolate file manager profile checks 2026-06-30 04:12:27 +00:00
hinotoi-agent
beb39f1ef2 fix: scope file manager sessions to active profile 2026-06-30 04:12:27 +00:00
Rod Boev
eb3c5a26bd Clarify terminal marker replacement semantics 2026-06-30 03:33:55 +00:00
Rod Boev
ae67cd3428 test(chat): refresh restore guard source-shape assertions (#5224) 2026-06-30 03:33:55 +00:00
Rod Boev
494d137082 fix(chat): scope transcript preserve to the current terminal tail (#5224) 2026-06-30 03:33:55 +00:00
Rod Boev
fd35d8c9ab fix(chat): narrow transcript preservation to stale-prefix restores (#5224) 2026-06-30 03:33:55 +00:00
Rod Boev
42cc02634a test(chat): refresh terminal recovery source-shape guards (#5224) 2026-06-30 03:33:55 +00:00
Rod Boev
c17f222427 fix(chat): preserve visible transcript on terminal stream failure (#5224) 2026-06-30 03:33:55 +00:00
Frank Song
670105b121 test(sidebar): relax menu delete hook assertion
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 11:31:41 +08:00
Frank Song
bb6487a70a fix(sidebar): remove menu-deleted sessions optimistically 2026-06-30 10:56:48 +08:00
Rod Boev
c2a0472514 test(#5250): harden settings menu selector contract 2026-06-29 22:56:07 -04:00
Frank Song
71a458784a fix(chat): guard live worklog manual scroll intent
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-30 10:52:11 +08:00
Rod Boev
600f1d3a61 test(#3825): fix OIDC regression checks 2026-06-29 22:31:45 -04:00
Rod Boev
39fbcb493b fix(auth): harden OIDC issuer and EC token checks (#3825) 2026-06-29 22:31:45 -04:00
Rod Boev
3b26cab859 fix(auth): keep OIDC startup warnings off the optional crypto path (#3825) 2026-06-29 22:31:45 -04:00
Rod Boev
f7a9c98810 fix(auth): harden OIDC key selection and startup guidance (#3825) 2026-06-29 22:28:53 -04:00
Rod Boev
7d0e86ecc5 fix(auth): require an explicit OIDC allowlist (#3825) 2026-06-29 22:28:53 -04:00
Rod Boev
1167617e74 fix(auth): harden OIDC login links and token handling (#3825) 2026-06-29 22:28:53 -04:00
Rod Boev
e0e10cb106 feat(auth): add native OIDC login for WebUI sessions (#3825) 2026-06-29 22:28:53 -04:00
Frank Song
448e28f442 fix(chat): preserve manual live worklog scroll snapshots 2026-06-30 10:28:36 +08:00
Rod Boev
761ffc5d96 fix(#5250): let settings search results escape the menu clip 2026-06-29 22:23:00 -04:00
allenliang2022
b117c5aa61 fix(webui): keep a following reader pinned through streaming growth and SSE recovery
Two related 'jump back' classes where a reader who is following the live stream
gets stranded mid-transcript, both verified live (OLD stranded the viewport
470-580px from bottom; FIX lands at 1px).

1. Content-grew-beneath-a-pinned-viewport (static/ui.js scroll listener).
   While streaming on a tall transcript (especially on mobile, where chunks land
   fast), new content increases scrollHeight under a stationary viewport. The
   reader never scrolled (top did not move up, _messageUserUnpinned is false), but
   bottomDistance crosses the nearBottom threshold, so the code fell through to
   _scrollPinned=false, killing auto-follow mid-stream. The follow writer and the
   scroll listener then fought frame-by-frame; the viewport stalled while content
   kept growing and was progressively stranded. Fix: in the !_messageUserUnpinned
   branch, when the viewport did NOT move up and auto-follow is on, keep the pin
   and re-snap to the true bottom via _setMessageScrollToBottom() instead of
   unpinning. The genuine scroll-away fallthrough is unchanged.

2. SSE-recovery follow-restore (static/messages.js). _handleStreamError (SSE
   drop), the Task-cancelled apply + fallback paths, and the reconnect-stream-dead
   cleanup all push/replace S.messages then renderMessages({preserveScroll:true}).
   preserveScroll's restore path keys on the pre-render snapshot's bottom-distance,
   which during a live stream can read large (content grew under a followed
   viewport), so it yanked a following reader up to a stale historical position on
   a process restart / SSE drop / cancel. Fix: capture follow-intent
   (_isMessagePaneNearBottom) BEFORE mutating S.messages, and after the recovery
   render, scrollToBottom() if the reader was following — so they see the
   interruption/cancellation notice in place. Readers who scrolled up to read
   history are left where they were.

Structural source-lock tests in tests/test_sse_recovery_scroll_stranding.py;
behavioral A/B verified via Playwright on a local source build.
2026-06-30 10:16:06 +08:00
Rod Boev
07a8c7dc6f fix(session): preserve kept tool rows on ambiguous truncate (#5134) 2026-06-30 01:40:07 +00:00
Rod Boev
e9387f8aa5 fix(session): fail closed on ambiguous truncate context matches (#5134) 2026-06-30 01:40:07 +00:00
Rod Boev
02a3750488 fix(session): keep truncate rewind on complete turns (#5134) 2026-06-30 01:40:07 +00:00
Rod Boev
3627c9e5d8 fix(session): drop interleaved tool rows after mid-history truncate (#5134) 2026-06-30 01:40:07 +00:00
santastabber
0bc26c9a8a fix(sidebar): stack subagent sessions under parent 2026-06-30 11:01:18 +10:00
b3nw
da43de72b8 fix(model): repair bare custom-provider suffix on resolver slow path (#5225)
Mirror the #5128 fast-path suffix repair when profile context is present
but requested_provider is omitted, which async delegation completion can
trigger via process_wakeup.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-30 01:01:17 +00:00
Frank Song
eff01bf322 Gate settled anchor fallbacks by raw index 2026-06-30 08:00:54 +08:00
Rod Boev
e2a1a54b37 fix(#4843): preserve falsy /pet hook messages 2026-06-29 18:26:40 -04:00
Jamie (Hermes)
0674a0198c perf(sessions): page archived sidebar rows 2026-06-29 22:24:35 +00:00
Rod Boev
db5d406d88 fix(boot): restore voice-mode runtime prefs during prefill rework (#4961) 2026-06-29 17:36:51 -04:00
Rod Boev
affdb40a1c fix(config): route stale model reads through shared reload gate (#5220) 2026-06-29 20:50:34 +00:00
Rod Boev
6653b7bd2f fix(config): collapse post-restart config reload stampede (#5220) 2026-06-29 20:50:34 +00:00
Brett Yelverton
6c73e417e0 test: add regression coverage for neuralwatt provider env-var mapping
Mirrors test_issue2025_xiaomi_env_key.py pattern:
- _PROVIDER_ENV_VAR['neuralwatt'] == 'NEURALWATT_API_KEY'
- _provider_has_key('neuralwatt') with env var set
- _provider_has_key('neuralwatt') without env var
- model group appears in get_available_models() when config lists models
2026-06-29 20:41:16 +00:00
nesquena-hermes
5fd6051743 test(#5176): update sprint20 continuous-recognition assertion for the new opt-in default-false form 2026-06-29 20:28:26 +00:00
nesquena-hermes
0aea4c7e18 harden(#5176): clamp configurable voice silence timeout (maintainer hardening)
Apply the warm-review polish: parseInt radix 10, fall back to 1800 for
missing/NaN/non-positive values, and floor at 200ms so a mistyped tiny/negative
hermes-voice-silence-ms can't make the recognizer auto-send instantly. De-brittle
the source-string test to assert behavior (reads key / 1800 fallback / guards
non-positive) instead of one literal expression. Default behavior unchanged.
2026-06-29 20:21:22 +00:00
Sean
5bfa9a8ec0 feat(voice): make silence timeout and continuous recognition configurable via localStorage
The voice-mode loop hardcoded SILENCE_MS=1800 and
_recognition.continuous=false, giving users no way to tune the
auto-send timing or keep the mic open across natural pauses.

Changes:
- SILENCE_MS now reads from localStorage key 'hermes-voice-silence-ms'
  with 1800ms fallback — users set it from the dev console or a future
  settings toggle
- _recognition.continuous reads from 'hermes-voice-continuous' (default
  false) — set to 'true' to keep the mic open across pauses within an
  utterance, with the silence timer remaining the sole send decision

New test file (tests/test_issue4761_voice_mode_config.py, 8 tests)
pins both localStorage reads and verifies the surrounding voice-mode
infrastructure is intact.

Refs #4761
2026-06-29 20:19:40 +00:00
nesquena-hermes
62fde9c32b test(#5201): widen hidden-tab skip-block window to 2900 after rebase (maintainer rebase-fix)
#5201 grows startSessionStream, pushing the hidden early-return skip path to
offset ~2247; #5172's test sliced an outer 2400 window that truncated the
skip_block before _startHiddenActiveStreamPoll(sid). Widen to 2900 so the
assertion reaches it. Both production hidden-poll paths are intact (Codex
verified); test-window only.
2026-06-29 20:06:18 +00:00
nesquena-hermes
ac9d9b8f8c test(#5201): widen sse-visibility reopen window to 2200 after master-rebase (maintainer rebase-fix)
The rebase onto current master (which carries #5172's window bumps) left the
reopen-path test window at 1600, but #5201 grows startSessionStream so the
'else if (_sessionStreamHiddenSid)' reopen assertion now lands at offset ~1741.
Widen to 2200 (matching #5201's sibling windows) so the assertion reaches the
code. Test-window only; production unchanged.
2026-06-29 19:53:25 +00:00
Frank Song
afbb3e5010 Fix session stream resume after live stream end
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 19:52:02 +00:00
Frank Song
47d8ac94d3 Harden connection flapping aftercare
Keep sidebar retries boot-scoped, preserve immediate source-change cache rebuilds, and tighten session stream ownership before closing duplicate SSE sockets.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 19:52:02 +00:00
Frank Song
cbb644da69 Reduce connection lost flapping under large session state 2026-06-29 19:52:02 +00:00
Frank Song
7f98952db0 test: harden anchor fallback harness 2026-06-29 19:37:37 +00:00
Frank Song
7cdcadcbc4 test: lock anchor fallback render ownership 2026-06-29 19:37:37 +00:00
Rod Boev
efa3ebaac4 fix(#4470): reject ambiguous kanban runtime input 2026-06-29 15:27:17 -04:00
Rod Boev
7b96b93459 test(#4470): add kanban_priority_hint to i18n completeness check 2026-06-29 15:27:16 -04:00
Rod Boev
396dd37891 test(#4470): update workspace-disabled assertion to match createOnlyIds pattern 2026-06-29 15:27:16 -04:00
Rod Boev
aaabd32c92 feat(#4470): expose skills, max_runtime_seconds, and parents in Kanban create modal 2026-06-29 15:27:16 -04:00
Frank Song
fbb1e9567c test: align new chat guard static coverage
Update the older #1432 static regression to assert the extracted reusable-empty helper owns the message-count and in-flight checks, while both New Chat entry points call the helper. This keeps the test aligned with the shared guard introduced in PR #5206.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 18:38:25 +00:00
Frank Song
28c2c37c1c Fix new chat empty-session reuse guard 2026-06-29 18:38:25 +00:00
Rod Boev
30fd052efa fix(boot): preserve native Ctrl+K text editing during URL prefill rework (#4961) 2026-06-29 14:29:47 -04:00
nesquena-hermes
dc38e42a26 test(#5204): lock redactor memoization contract (cached==uncached + oversize bypass) (auto-fix)
Adds the optional regression test the gate recommended: pins that
_redact_fn_cached stays byte-identical to _redact_fn_uncached for sensitive
input, is idempotent, and that >16KB inputs which bypass the lru_cache are
still redacted identically (no cache-skip secret leak). Production code
unchanged.
2026-06-29 18:23:41 +00:00
Rod Boev
ca5b20d2e6 Keep overflow cleanup off the accept loop 2026-06-29 18:10:20 +00:00
Rod Boev
55ad113ee7 fix(#5210): bound HTTP worker dispatch under reconnect storms 2026-06-29 18:10:20 +00:00
nesquena-hermes
683a96acda test(#5196): add _safe_replace retry/backoff coverage (auto-fix)
Adds the test the project's contribution rules require for non-trivial logic
(AGENTS.md). Covers: posix passthrough (1 call, no sleep), Windows
retry-then-succeed, reraise after exhausting 5 attempts (no silent success on a
save path), no-retry on non-PermissionError, and the 50/100/200/400ms backoff
sequence. Production _safe_replace unchanged.
2026-06-29 17:54:37 +00:00
nesquena-hermes
9dcd31d501 test(#5099): widen ctrl-k branch windows so guard assertions reach the code (auto-fix)
The source-string assertion windows were too short: the Ctrl+K branch window
(900) cut off before newSession()/closeMobileSidebar() (@992/@1155) and the
Ctrl+B forward window (+120) cut off isContentEditable (@119+). Widen to
1500 / +300 so the guards are actually verified (non-vacuous). Codex-recommended
headroom. Production guard unchanged.
2026-06-29 17:46:18 +00:00
nankingjing
0a94e55384 fix(ux): skip Ctrl+K new-chat shortcut in text inputs (#5099)
Let Emacs-adjacent users keep kill-line in the composer and other
editable fields while preserving global Cmd/Ctrl+K elsewhere.

Fixes #5099
2026-06-29 17:36:47 +00:00
allenliang2022
955e31b9a1 fix(chat): do not collapse a pure-prose turn into a worklog (stream-done jump)
A turn that streamed only prose (a long plain-text answer, or a degeneration
burst that floods the body with repeated tokens) still projected an anchor
activity scene whose activity_rows were all prose/terminal — zero tool/thinking
rows. The settle path promoted it to a collapsed worklog anyway (the gate only
checked activity_rows.length), hiding the whole answer and shrinking the
transcript by the full streamed height at STREAM_DONE, so the browser clamped a
bottom-pinned viewport back to the top (the 'jump back' report).

Add a worklog-worthiness predicate at both gates: the generation gate
(_anchorSceneHasWorklogWorthyRows in messages.js, decides whether to attach a
scene at all) and the render gate (_anchorSceneSceneHasWorklogWorthyRows in
ui.js, defense-in-depth for already-persisted all-prose scenes). A scene is
worklog-worthy only if it has >=1 tool/thinking row or a compression lifecycle
card; pure prose is not, so the turn renders as normal visible prose and the
viewport stays pinned at the bottom.

Behavioral + structural tests in tests/test_pure_prose_turn_not_worklog.py.
2026-06-29 17:06:45 +00:00
allenliang2022
2aa1d168b0 fix(chat): scope STREAM_DONE keep-open to just-settled turn via one-shot token
Address maintainer must-fix: _shouldKeepSettledWorklogOpenForPinnedFollow()
keyed only on global pin state, so every historical settled worklog re-expanded
on any pinned re-render — defeating the compact default. Gate the keep-open
exception on a one-shot token = the just-settled stream id (armed at STREAM_DONE
renderMessages, disarmed right after), threaded as _shouldKeep...(streamId).
Unpinned path unchanged. Replace source-string test with a behavioral Node test
that drives a second pinned turn and asserts it collapses.
2026-06-29 17:06:44 +00:00
allenliang2022
0d198d677d test(chat): document keep-open precedence over live collapse state
Follow-up to maintainer review on #5058: make the pinned-follower helper comment
explicit that avoiding the visible STREAM_DONE shrink jump intentionally takes
precedence over a transient user-collapsed live worklog while the reader remains
pinned. Add a source lock so that intent stays documented.

Focused scroll locks remain green (22 passed), node --check clean.
2026-06-29 17:06:44 +00:00
allenliang2022
5fd3b1c9d7 fix(chat): prevent stream-end worklog collapse jump for pinned readers
A new post-#4970 scroll-jump class surfaced after Release YH: when a streamed
assistant turn with a large live worklog/tool trace settles, the settled compact
worklog collapses hundreds of pixels at STREAM_DONE. The reader is still pinned
and scroll state is correct, but the net scrollHeight shrink makes the browser
clamp scrollTop down by the same amount, which is visible as a large backward
jump.

Fix by keeping the just-settled activity worklog open for pinned followers
(_scrollPinned && !_messageUserUnpinned), so the live->settled DOM swap is
height-stable. Unpinned readers still get compact settled worklogs and keep their
viewport normally. Also make _anchorSceneWorklogGroup respect opts.collapsed;
previously it hard-coded collapsed: !live so the caller could not request an open
settled group.

Regression locks assert the pinned-follow helper, that settled rendering passes
collapsed:!keepSettledWorklogOpen, and that _anchorSceneWorklogGroup honors an
explicit opts.collapsed. Verified locally with a Playwright repro: before the
fix a large tool-worklog answer produced scrollHeight -367/-422px and scrollTop
-367/-422px at STREAM_DONE; after the fix nShrinks=0 and nBack=0. Local focused
scroll locks pass: 22 passed.
2026-06-29 17:06:44 +00:00
allenliang2022
860a3c2458 fix(webui): keep stale transcript mounted while hidden-tab return reloads new messages (#5177)
After a hidden interval that added persisted messages (post-turn bg-review
writes, sibling-tab writes, the just-finished main turn flushing), switching
the tab back caused refreshActiveSessionIfExternallyUpdated('visible'|'focus')
to hit remoteCount !== localCount and call loadSession(sid, {force:true}),
which synchronously did S.messages = [] before awaiting the metadata +
messages fetches. The visible result was the entire conversation transcript
blanking for the round-trip and reappearing — '对话突然消失,重刷才回来'.

sessions.js itself warns about this 'disappear and reappear' tradeoff at the
top of the remoteCount/remoteLast guard, but only short-circuits the
metadata-only branch (#5061). #5122's 4-probe staged reconnect covers a
different path (SSE error with ready_state=2 on a visible tab) and does not
apply when the SSE error arrives while visibility_state='hidden' — that
bottoms out through _deferStreamErrorIfPageHidden before the reconnect block
ever runs.

Fix:

- refreshActiveSessionIfExternallyUpdated maps the visibility/focus recovery
  reasons ({visible, focus}) to a new keepStaleUntilLoaded option, forwarded
  to loadSession. The post-stream idle reconcile and the poll/external paths
  keep their existing behaviour.
- loadSession ANDs opts.keepStaleUntilLoaded with sameSessionForceReload (so
  cross-session switches still clear synchronously — leaving a prior
  session's transcript on screen during a navigation is the original bug
  the clear was written for) and, on that path, skips the synchronous
  S.messages/S.toolCalls/_messagesTruncated/_oldestIdx clear. The new
  transcript is SWAPPED into S.messages by _ensureMessagesLoaded(sid,
  {force:true}), so the user sees old DOM directly replaced by new DOM in a
  single render frame.
- _ensureMessagesLoaded grows an opts.force escape hatch so its 'messages
  already populated' early-return cannot skip the swap when stale messages
  are still in place.

Verified live on a source 8701 build:
- before: minHtmlObserved 65874, minKidsObserved 11 — 372 samples at 16ms,
  DOM html length NEVER dipped below the pre-call value during the reload.
- loadSession was called with force:true keepStale:true reason:'visible',
  outcome 'reloaded', sMsgs 21 → 1037, after kids 11 → 134 (single-frame
  swap).

Tests:

- tests/test_issue5177_hidden_tab_blank_gap.py (new, 7 cases): source-locks
  the keep-stale guard shape, the recovery-reason map, the
  _ensureMessagesLoaded opts.force escape hatch, and that the synchronous
  clear is wrapped in if(!_keepStaleUntilLoaded).
- Updated the two existing source-lock tests whose anchor strings included
  the loadSession call signature
  (tests/test_webui_external_refresh_frontend.py +
  tests/test_tars_scroll_reset_regressions.py) to match the new
  loadSession(... keepStaleUntilLoaded:_keepStaleUntilLoaded) form. No
  behavioural assertions changed.

Local: node --check static/sessions.js OK. 122 passed across
test_issue5177_hidden_tab_blank_gap.py + the full adjacent set
(external-refresh / journal-frontend / scroll-reset / reconnect-chronology /
inflight-stream-reuse / sse-error-multi-probe / issue4295 / issue4856).
2026-06-29 17:02:09 +00:00
allenliang2022
ada8c262ca fix(webui): render server-initiated turns in a hidden tab without a refresh
A turn started SERVER-SIDE (self-wake, cron, restart hook) fans a
`server_turn_started` frame onto the per-session live-view SSE channel so an
open tab renders it live. But while a tab is HIDDEN the WebUI deliberately does
NOT hold that persistent SSE open (connection-pool budget — #3992 / #4151), so
`get_session_channel()` returns None at fan-out time and the turn is dropped for
that tab. The user only saw the turn after a manual interaction (refresh / send)
re-subscribed the channel and replayed it.

Bridge the gap with a lightweight poll of `/api/session/status` (one short GET
per ~6s tick — NOT a held connection, so the connection-pool budget the hidden
guard protects is preserved) that attaches the existing live renderer when it
sees a *live* active stream.

Backend (`api/session_ops.py`):
- `session_status()` now exposes `active_stream_id`, derived through a new
  `_live_active_stream_id()` helper that only returns the id when the stream is
  genuinely live (present in STREAMS / ACTIVE_RUNS). A stale id left by a
  crashed/restarted run surfaces as None, so the poller never attaches a
  renderer to a dead stream. Additive field — existing consumers ignore it.

Frontend (`static/messages.js`):
- `_startHiddenActiveStreamPoll` / `_stopHiddenActiveStreamPoll` /
  `_attachServerInitiatedStream` implement the poll lifecycle. The poll fires
  one immediate tick on hide (so an in-flight turn is caught without waiting a
  full interval), attaches mid-flight turns via the reconnecting/replay path,
  and stops on re-show (the real SSE takes over), session switch, or once a
  stream is rendering.
- Started on BOTH hidden paths: the `visibilitychange` hook's hidden branch (a
  visible tab going to background) and `startSessionStream`'s hidden
  early-return skip (a session opened while already hidden). The visible path
  and `stopSessionStream` clear it.

Tests:
- `tests/test_hidden_tab_server_initiated_turn.py` — backend live-validation
  (stale id → None; STREAMS/ACTIVE_RUNS id returned) and frontend lifecycle /
  both-hidden-paths / replay-attach source locks.
- Widened the brittle fixed-width source-window slices in
  `tests/test_issue3996_sse_visibility.py` (1600/1700 → 2400) so the existing
  `visibilitychange` / hidden-skip assertions still find their markers after the
  poll-start lines were inserted into `startSessionStream`.
2026-06-29 16:28:30 +00:00
Rod Boev
b5dc306723 test(settings): decouple ranking proofs from insertion order (#5149) 2026-06-29 11:18:07 -04:00
Rod Boev
533eaf7c26 test(settings): close ranking proof gaps (#5149) 2026-06-29 11:04:59 -04:00
Rod Boev
c802c18242 test(settings): harden ranking proof coverage (#5149) 2026-06-29 10:50:05 -04:00
Rod Boev
782335b62a feat(settings): rank settings search results by match source (#5149) 2026-06-29 10:39:08 -04:00
Rod Boev
faed296819 fix(#4961): keep URL boot prefill draft-only 2026-06-29 10:03:36 -04:00
Rod Boev
71cd69cfbf fix(#4961): honor send query prefill flag 2026-06-29 09:52:43 -04:00
Rod Boev
1bd9e52e5f test(#4961): lock the guard ordering to the rebased boot flow 2026-06-29 09:52:43 -04:00
Rod Boev
a527ca734f test(frontend): keep saved-session boot coverage aligned (#4961) 2026-06-29 09:52:43 -04:00
Rod Boev
93d356c0d4 fix(#4961): preserve prefill on zero-message session restore 2026-06-29 09:52:43 -04:00
Rod Boev
ac5a849301 feat(#4961): support URL query prefill for browser search 2026-06-29 09:52:43 -04:00
Ryan via Axis
d52c5898ca Update mobile edge guard test expectation 2026-06-29 07:43:22 -04:00
Rod Boev
06cd321cec fix(tts): reject credentialed OpenAI base URLs (#4982) 2026-06-29 02:41:56 -04:00
nesquena-hermes
359eae93de test: de-flake test_skills_stats_cache mtime race
test_skills_stats_cache asserted that adding a skill bumps the skills-dir
mtime so the cheap per-call probe recomputes counts on the next call. Two
_write_skill() calls inside the same coarse filesystem mtime tick could leave
the skills-dir mtime byte-identical to the cached probe value, so the probe
saw no change and served the stale count of 1 (assert enabled==2 failed). This
is an order-dependent flake: passes in isolation, fails intermittently under
full-suite ordering (observed across multiple unrelated PRs).

Force the skills-dir mtime strictly forward after the second write so the
probe is guaranteed to detect a genuine mtime change. The assertion stays
honest (it still proves recompute-on-mtime-change); only the race is removed.
2026-06-29 06:07:44 +00:00
Rod Boev
04d0cde843 fix(#692): keep budget validation consistent across save and read 2026-06-29 01:59:51 -04:00
nesquena-hermes
72f7530c45 ci: run tests across 5 shards (up from 3) + fix 2 latent test-isolation leaks
Two cross-file isolation leaks that a finer shard partition exposes are fixed at
root so every shard passes regardless of execution order:
- test_auth_session_persistence.py: bind api.auth STATE_DIR/_SESSIONS_FILE/
  _LOGIN_ATTEMPTS_FILE in setUp + restore in tearDown (was relying on a sibling
  reload test to rebind them).
- test_request_diagnostics_cache.py: pin the session-list cache source stamp via
  an autouse fixture (matches test_session_sidebar_cache.py) so a leaked state.db
  WAL sidecar can't cause a spurious cache-miss.

Verified all shards green under 3-, 4-, and 5-way partitions. CI infra + test
harness only; no application behavior change.
2026-06-29 05:55:16 +00:00
Frank Song
058372c15f Preserve empty anchor tool args in parity helper
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 05:35:03 +00:00
Frank Song
e670320337 Tighten anchor runtime journal parity test
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-29 05:35:03 +00:00
Frank Song
e1bf1122a7 Test anchor runtime journal parity 2026-06-29 05:35:03 +00:00
santastabber
178db13512 fix: authorize session html media artifacts 2026-06-29 05:17:33 +00:00
Stacey2911
dd658d7a04 fix(workspace): align Workspace Todos rendering with sidebar 2026-06-29 04:44:16 +00:00
Rod Boev
a98b0551c9 test(updates): keep launchd regression proofs faithful (#5175) 2026-06-29 04:22:51 +00:00
Rod Boev
a052f390f8 fix(updates): resolve git under macOS launchd (#5175) 2026-06-29 04:22:51 +00:00
nesquena-hermes
fae5c45395 Merge #5163 into stage/p1a 2026-06-29 04:07:21 +00:00
nesquena-hermes
65b27886d0 Merge #5166 into stage/p1a 2026-06-29 04:07:21 +00:00
Rod Boev
31a443ff88 fix(health): preserve restart failure contract and proofs (#5156) 2026-06-29 03:44:06 +00:00
Rod Boev
8b22c20061 fix(tests): tighten stash reset proof for agent update (#5156) 2026-06-29 03:44:06 +00:00
Rod Boev
6d80dd41d6 fix(updates): restart gateway after in-app agent update (#5156) 2026-06-29 03:44:06 +00:00
nesquena-hermes
16e280804b
Merge branch 'master' into fix/4657-fit-engine-rebased 2026-06-28 19:24:13 -07:00
Stacey2911
43a0a39efa fix: gate reasoning_content replay by provider policy
Prevent historical assistant reasoning_content from being replayed to
generic/local providers by default while preserving current behavior for
known reasoning-capable provider protocols.

Adds a webui.reasoning_content_replay policy read by the sanitizer:
auto, strip, and preserve. Auto strips for local/generic backends such as
LM Studio/Ollama/llama.cpp and preserves for explicitly known provider
protocols that may require reasoning replay.

This only affects provider-facing sanitized history. It does not modify
stored session/display reasoning metadata.
2026-06-29 11:47:02 +10:00
claw-io
159298d319 fix(profiles): use profile-scoped resolver for /api/profile/active default_workspace (Codex gate #5169)
Codex flagged that get_last_workspace() falls back to the GLOBAL last_workspace.txt
before the profile config.yaml — so a named profile without its own
last_workspace.txt would leak the global path, re-introducing the #5169 bug.
Add get_profile_default_workspace() (profile last_workspace.txt -> config.yaml ->
terminal.cwd -> default, NO global fallback) and call it from /api/profile/active.
Adds a regression test: global last_workspace.txt present + named profile with
only config.yaml workspace must return the profile path, never the global.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-29 01:45:17 +00:00
nesquena-hermes
628d378857 chore(composer): drop #4657's quota-chip default-flip — keep just the fit-engine
Per maintainer decision: #4657 bundled a scope-creep change flipping
show_quota_chip default off->on plus a migration force-resetting stored false->true
(stomping explicit opt-outs). Master deliberately ships quota-chip default-off.
Reverted config.py + the quota-chip wiring in panels.js/boot.js + the quota test
to master, keeping ONLY the footer fit-by-overflow engine. The mobile quota-action
selector addition (hide_composer_quota_chip selectors) is kept (fit-related, not a
default change).

Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:38:34 +00:00
claw-io
ec0ef4a0d3 fix(profiles): show active profile workspace on blank new-chat (#5169)
Expose a profile-scoped default_workspace on GET /api/profile/active (resolved
by the profile-aware get_last_workspace(), fail-open) and hydrate boot.js from
it, overriding the global GET /api/settings value. GET /api/settings stays
global-only so the settings UI can't clobber the shared default. Combines
claw-io's #5173 (primary) with the fail-open backend hardening + regression test
from #5171.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-06-29 01:25:58 +00:00
nesquena-hermes
c6370235be feat(composer): fit footer controls by overflow (#4657, rebased onto master)
Rebased Paladin173's #4657 onto current master. The per-control visibility
toggles half already shipped via his own #4598, so this rebase carries only the
net-new JS fit-by-overflow engine (_fitComposerFooter: ResizeObserver +
MutationObserver measure .composer-left overflow, step full -> cf-icons ->
cf-burger). Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
2026-06-29 01:19:02 +00:00
nesquena-hermes
c344844766 fix(#5132): split state.db sidebar override — source uncapped, count capped
Codex deep-gate caught a CORE regression in the original top-N cap: capping
_apply_sidebar_state_db_overrides also capped the SOURCE classification that
/api/sessions filters on BEFORE the lazy lineage correction, so a stale-CLI JSON
row whose state.db source is webui could be silently filtered out of a WebUI-only
sidebar once it fell beyond the cap. Split the read: the indexed sessions-table
source/title lookup runs for ALL rows; only the expensive messages COUNT/MAX
aggregation (the actual 5-18s bottleneck) is capped to top-N. Added a 500-row
end-to-end regression with a real state.db proving the beyond-cap stale-CLI row
stays webui.
2026-06-29 00:54:15 +00:00
nesquena-hermes
c646d92206 perf(#5132): cap /api/sessions state.db source/title overrides to top-N
On power-user instances with thousands of sessions, GET /api/sessions
blocked 5-18s in the all_sessions.state_db_overrides stage:
_apply_sidebar_state_db_overrides read state.db for every row (2400+ ids)
on every concurrent poll. With the gateway-watcher reading and the agent
writing the same state.db, this became lock/IO contention that piled up
concurrent polls and flapped the UI to 'Connection lost'.

Cap the override lookup to the top-N most-recent (paint-priority) rows
that are actually visible, mirroring the existing lineage-enrichment cap
(#4638). The caller passes an already pinned-first/newest-first sorted
list, so the top-N covers the visible window; rows beyond the cap keep
their JSON source/title and are corrected lazily on history-panel open.
Cap defaults to 300, env-configurable via
HERMES_WEBUI_STATE_DB_OVERRIDE_TOP_N (0 disables). Fails open.

Reported by @latipun, reproduced by @b3nw.

Co-authored-by: latipun <latipun@users.noreply.github.com>
2026-06-29 00:40:10 +00:00
Rod Boev
ec7ecf9e46 Avoid misleading pet recovery guidance 2026-06-28 20:24:29 -04:00
nesquena-hermes
a8e921d13a fix(sidebar): scope hidden archived-parent references to active project + source
The sidebar_reference_sessions payload arrives project-unscoped from
/api/sessions. _renderSidebarRowsFromRawSessions appended it to EVERY
render unconditionally, so an archived parent in project B entered a
project-A-filtered render's suppression context and silently hid a
visible project-A child/fork (rendered as []). Add
_scopedSidebarReferenceRows() to filter references to the same project +
webui/cli source bucket as the render they feed, mirroring
_partitionSidebarSessionRows. Regression test locks the cross-project
case; the existing suppression test now exercises the scoped helper.

Co-authored-by: santastabber <santastabber@users.noreply.github.com>
2026-06-29 00:21:04 +00:00
santastabber
5ed0ba6ef6 fix(sidebar): keep hidden archived parents as child references 2026-06-29 00:13:32 +00:00
Rod Boev
6f333984f6 fix(#4843): respect truthy pet hook handoffs 2026-06-28 19:56:05 -04:00
Rod Boev
5471e34893 test(#4843): prove the /pet webui guard path 2026-06-28 19:49:12 -04:00
Rod Boev
e0f8f61e03 test(#5152): anchor resolver assertion to the new helper 2026-06-28 19:44:38 -04:00
Rod Boev
25d213cfe9 feat(#4843): add WebUI pet extension handoff 2026-06-28 19:32:42 -04:00
nesquena-hermes
4d79de266c Merge master into stage-5155 (catch up to v0.51.728) 2026-06-28 23:25:57 +00:00
Rod Boev
4eb77da80e Keep settings-search coverage anchored 2026-06-28 19:24:51 -04:00
Rod Boev
552a13fa9e Avoid duplicate busy-placeholder refresh 2026-06-28 19:24:44 -04:00
Rod Boev
dfeb21da98 fix(tests): accept Windows venv layout in scripts/test.sh (#5152) 2026-06-28 19:17:19 -04:00
nesquena-hermes
f968de8d35 merge #5155 extension settings surface 2026-06-28 23:07:00 +00:00
nesquena-hermes
9736e943bb merge #5151 distinguish offline gateway approval warnings 2026-06-28 22:54:54 +00:00
nesquena-hermes
751ba20fec merge #5129 surface provider auth failures in chat 2026-06-28 22:54:54 +00:00
nesquena-hermes
1a4dcd4fb5 merge #5128 custom-provider bare-model wakeup repair 2026-06-28 22:54:54 +00:00
Rod Boev
ec00e037f3 fix(settings): widen busy-input settings search coverage (#5148) 2026-06-28 18:47:42 -04:00
Rod Boev
9a1f27a48b feat(settings): add opt-in busy placeholder hint (#5144) 2026-06-28 18:47:42 -04:00
Rod Boev
7153e8d78f fix(extensions): prune malformed browser overrides (#5094) 2026-06-28 18:31:43 -04:00
Rod Boev
234fefe6ee fix(#5139): keep timeout capability probes on the unsupported path 2026-06-28 18:29:37 -04:00
Rod Boev
aef8e524fd fix(extensions): keep settings-only installs canonical (#5094) 2026-06-28 18:25:47 -04:00
nesquena-hermes
fc02953ea6 Merge branch 'pr-5117-head' into stage/5117-defer 2026-06-28 22:25:26 +00:00
Rod Boev
269d2e4d93 test(#5139): isolate the 404 warning regression from shared cache state 2026-06-28 18:23:20 -04:00
nesquena-hermes
7b12dff3e6 test(#5121): make auth-failure tests host-independent
The 4 terminal-auth-failure tests assert an unrecoverable 401 surfaces an
apperror / persisted _error turn. The settlement path first attempts the
#1401 credential self-heal, which reads ~/.hermes/auth.json. On a host with
populated credentials, self-heal can succeed and retry the mock agent,
swallowing the error the test expects -- so the result depended on host
credentials (green on CI / Windows with no creds, red on a live agent box).

Add an autouse fixture that forces _attempt_credential_self_heal to a no-op
so every host exercises the unrecoverable-failure path. The one test that
verifies a successful retry already patches the symbol in its own body, so it
is unaffected.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-06-28 22:20:28 +00:00
Rod Boev
23b7a7935a fix(#5139): keep older gateways on the unsupported warning path 2026-06-28 18:18:47 -04:00
Rod Boev
46d91a7c61 fix(extensions): render installed controls from trusted state (#5094) 2026-06-28 18:15:36 -04:00
Rod Boev
938f721b16 fix(extensions): reload before editing untrusted installs (#5094) 2026-06-28 18:07:52 -04:00
Rod Boev
615b023eb0 fix(extensions): pin schemas to trusted bootstrap state (#5094) 2026-06-28 17:59:36 -04:00
Rod Boev
343a8b50bf fix(#5139): distinguish offline gateway approval warnings 2026-06-28 17:53:37 -04:00
b3nw
7a04b7e9aa fix: gate bare-model repair on profile provider match (#5128 review)
When session model_provider differs from profile model.provider, do not
expose profile_default_model for suffix repair. Fast path also requires
profile_provider to match requested_provider when profile_provider is set.

Addresses cross-provider mis-resolution (custom:other-proxy + bare suffix).

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 21:52:06 +00:00
Rod Boev
cb96a5cf39 fix(extensions): trust only bootstrapped extension ids (#5094) 2026-06-28 17:51:42 -04:00
nesquena-hermes
c556262159 Merge branch 'pr-5147-head' into stage/5147-approval 2026-06-28 21:48:51 +00:00
Rod Boev
07ed67a793 fix(extensions): keep runtime settings gated on owned storage (#5094) 2026-06-28 17:44:02 -04:00
Rod Boev
62be5b02ea fix(#5135): clear stale gateway approval mirrors on stream teardown 2026-06-28 17:32:55 -04:00
Rod Boev
a76c794d6d feat(extensions): add browser-local extension settings surface (#5094) 2026-06-28 17:28:20 -04:00
nesquena-hermes
fc087c067f Merge branch 'pr-5118-head' into stage/5118-scroll 2026-06-28 21:22:53 +00:00
nesquena-hermes
fb7b16e723 Merge branch 'pr-5131-head' into stage/5131-cron 2026-06-28 20:51:44 +00:00
nesquena-hermes
a17080d61f Merge branch 'pr-5113-stage' into stage/5113-brand 2026-06-28 20:36:46 +00:00
nesquena-hermes
9b17c1c80a Merge branch 'pr-5090-head' into stage/p1-lowrisk-a 2026-06-28 20:22:22 +00:00
nesquena-hermes
822effe0d5 test(graphite-skin): drop stale .app-titlebar-icon rect assertion
PR #5113 (new cyan caduceus brand mark) intentionally removed the per-skin
.app-titlebar-icon rect/path/circle recolor overrides — the old gold mark was
forced to the skin accent, but the new brand badge is self-colored and constant
across themes. test_graphite_skin_tunes_workbench_chrome still asserted that
removed rule. Dropped the obsolete assertion; the rest of the graphite chrome
coverage is unchanged.
2026-06-28 20:11:22 +00:00
Rod Boev
2eeb0f0f53 fix(#5130): serialize profile-scoped snapshot repair 2026-06-28 16:03:25 -04:00
Rod Boev
fd139132b6 fix(#5130): harden cron snapshot repair follow-up 2026-06-28 15:56:50 -04:00
nesquena-hermes
2d249089fc fix(session): guard _evict_session_agent against closing a live session's _session_db
Mirror the worker's LRU-eviction ACTIVE_RUNS liveness check in
_evict_session_agent so truncate/clear/model-switch can't close the SessionDB
out from under an in-flight turn on the same session (#5096 Bug D follow-up).
The cache handle still drops (harmless — worker holds a local ref); only the
lifecycle commit + _session_db.close() are skipped while a run is active.

Co-authored-by: claw-io <claw-io@users.noreply.github.com>
2026-06-28 19:36:59 +00:00
Rod Boev
7e806d7194 fix(#5130): recompute cron snapshots in selected profile 2026-06-28 15:29:40 -04:00
Rod Boev
4763c2a9a4 Keep null error fields from tripping replay failure 2026-06-28 15:22:33 -04:00
Rod Boev
8fbc3dcc9f Preserve canonical provider error probes in auth terminal turns 2026-06-28 15:11:01 -04:00
b3nw
159d8a96f4 fix: repair bare custom-provider model on process-wakeup fast path
Refs #5127.

When _resolve_compatible_session_model_state takes the #1855 fast path with
a bare runtime model suffix and a slash-qualified profile default, repair to
the full qualified ID for custom/custom:* providers.

Also load profile model.default when the session already has model_provider
so wakeup turns can apply that repair.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 18:34:10 +00:00
nesquena-hermes
2ebcaeac64 Merge branch 'pr-5122-head' into stage/brick-5096 2026-06-28 18:26:34 +00:00
Rod Boev
3e27a8bafe fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 14:23:08 -04:00
nesquena-hermes
aae7cc58d2 Merge branch 'pr-5126-head' into stage/brick-5096
# Conflicts:
#	api/session_ops.py
2026-06-28 18:14:03 +00:00
nesquena-hermes
57061d0b19 Merge branch 'pr-5125-head' into stage/brick-5096 2026-06-28 18:13:29 +00:00
b3nw
6e499eb1dd fix(session): semantic context truncate on rewind + evict agent
Refs nesquena/hermes-webui#5096

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 18:01:56 +00:00
b3nw
bf1f8339b8 fix(ui): use absolute keep_count for edit and regenerate truncate
Refs nesquena/hermes-webui#5096

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 18:01:54 +00:00
Rod Boev
3fafa3efc6 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 14:00:40 -04:00
b3nw
aacf860ccc fix(session): truncate context_messages when branching from fork point
Refs nesquena/hermes-webui#5096

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-06-28 17:59:46 +00:00
Rod Boev
eb886836f5 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 13:37:18 -04:00
Rod Boev
ca4fe4e54b fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 13:26:57 -04:00
allenliang2022
b0d01e492a fix(test): update run-journal reconnect anchor for the (1/N) template literal
test_error_reconnect_path_can_restore_from_journal anchored on the exact string
setComposerStatus('Reconnecting — but the first status was changed to a template
literal `Reconnecting… (1/${_retryDelays.length})` for the staged-probe counter,
so that single-quoted anchor no longer exists (ValueError: substring not found in
CI shard 2). Re-anchor on the block's stable entry point _reconnectAttempted=true;
(unique in source) and widen the window to 1100 chars; the st.active /
st.replay_available / Restoring stream / _runJournalReplayParams() assertions are
unchanged.
2026-06-29 01:22:40 +08:00
Rod Boev
5f02189939 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 13:18:32 -04:00
allenliang2022
265e176f6c address greptile P2s: stage counter starts at (1/N); brace-match test window
- messages.js: declare _retryDelays before the first setComposerStatus and show
  'Reconnecting… (1/4)' on the initial probe so the counter progresses
  (1/4)->(2/4)->(3/4)->(4/4) instead of jumping from a bare 'Reconnecting…' to
  (2/4) (looked like it started at 2).
- test: replace the fixed block_start+1600 char window with a brace-matched
  _reconnect_block() helper so the ordering / no-early-clear assertions cannot
  silently under-assert if the reconnect block grows. Add a source-lock for the
  (1/N) stage-counter start.
2026-06-29 01:16:53 +08:00
Rod Boev
6681bef796 fix(streaming): surface provider auth failures in chat (#5121) 2026-06-28 13:04:27 -04:00
allenliang2022
8edbf48f32 fix(webui): stage SSE-error reconnect probes so the live turn doesn't blank out
After the chat EventSource fires 'error', attachLiveStream made a single 1.5s
reconnect probe and, if /api/chat/stream/status did not yet report active or
replay_available, fell straight through to _handleStreamError(): clearing the
owner INFLIGHT state, nulling S.activeStreamId, pushing a 'Connection
interrupted' message and re-rendering. That wiped the live message DOM even
when the backend was still producing tokens or the run-journal replay file was
a beat away from becoming visible, so the settled response disappeared and then
reappeared from sidecar/replay (or only on refresh).

Replace the single probe with a short staged retry window
(_retryDelays=[1500,3000,5000,8000] ms) driven by a recursive _probeReconnect.
Each stage re-queries stream status and reconnects/replays when the backend is
reachable; the live DOM and S.activeStreamId/INFLIGHT state are kept intact
across the whole window, and _handleStreamError is only reached after every
stage has failed.

Adds tests/test_sse_error_multi_probe_reconnect.py source-locking the staged
shape and the ordering invariant (terminal error only after the retry window is
exhausted; no inflight/activeStreamId clear mid-window).
2026-06-29 01:04:26 +08:00
santastabber
93d8907a3e Defer non-critical session switch refreshes 2026-06-29 00:53:41 +10:00
santastabber
b6b7f1df1d Fix pinned stream scroll restoration 2026-06-29 00:52:36 +10:00
Rod Boev
4584f02550 fix(lint): remove unused os import from budget tests 2026-06-28 10:37:36 -04:00
Rod Boev
664b29d403 fix: address adversarial review findings for budget feature 2026-06-28 10:24:30 -04:00
Rod Boev
62e25ad9c1 feat(#692): add configurable budget limit with percentage-used tracking 2026-06-28 10:10:35 -04:00
Rod Boev
7702acfca6 fix(tts): close rework blockers for OpenAI TTS (#4982) 2026-06-28 09:55:46 -04:00
Rod Boev
21e229cb0f feat(#4982): support OpenAI-compatible TTS backend in WebUI (#4982) 2026-06-28 09:50:51 -04:00
Frank Song
2ef838be8c test(anchor): widen JS regex extraction guard
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 17:05:16 +08:00
Frank Song
767285f21f Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 17:02:52 +08:00
nesquena-hermes
94d952c3bb fix: harden None providers config across onboarding/providers/routes (salvage of #3967)
A config.yaml with an explicit empty/null `providers:` key parses to None in
PyYAML. `cfg.get("providers", {})` only returns the {} default when the key is
*absent*, so an explicit null yields None instead of an empty mapping. master
already guards each read with `isinstance(providers_cfg, dict)`, but the value
fed to that guard is still a footgun: the natural chained form
`cfg.get("providers", {}).get(...)` (already fixed once in api/config.py)
crashes with AttributeError on None.

Harden every providers-key read in the three remaining files to
`cfg.get("providers") or {}` so an explicit null degrades to an empty dict at
the source, matching the existing api/config.py convention. Sites hardened:

  api/onboarding.py:578  (_provider_api_key_present)
  api/providers.py:1051  (_provider_has_shadowed_codex_oauth_value)
  api/providers.py:1219  (_provider_has_key)
  api/providers.py:1265  (_get_provider_api_key)
  api/providers.py:2348  (get_providers catalog)
  api/providers.py:2781  (_clean_provider_key_from_config)
  api/routes.py:5045     (_context_length_config_api_key_for_provider)
  api/routes.py:5110     (_context_length_lookup_inputs_for_model)
  api/routes.py:15916    (_handle_live_models)

Adds tests/test_none_providers_config_guard.py: per-file source-form pins (fail
on master / on any single-file revert) plus behavioural checks driving the real
functions with `providers: None` to prove no crash and parity with the
empty-mapping config. Updates the existing test_issue3717 routes-string pin to
match the hardened form.

This is a focused salvage of grab-bag PR #3967 -- only the None-providers
hardening is taken; the PR's unrelated changes (account-usage worker-pool
refactor, function removals) are intentionally dropped.

Co-authored-by: lidi1011 <lidi1011@users.noreply.github.com>
2026-06-28 08:57:39 +00:00
nesquena-hermes
af0835a083 fix(streaming): delegate image routing to agent canonical, preserve unknown-model multimodal (salvage of #4113)
WebUI's api/streaming.py:_resolve_image_input_mode was a LOCAL
re-implementation of agent/image_routing.py:decide_image_input_mode that
had DIVERGED from the canonical function. The local copy never consulted
the active model's vision capability — it only checked a handful of config
heuristics — so it returned "native" (embedding the image as an image_url
part) even for models KNOWN to lack vision. That silently sends pixels to a
blind model (#21160-class failure) instead of routing through the text
(vision_analyze) pipeline.

Fix: delegate to the canonical agent.image_routing.decide_image_input_mode
(single source of truth), with a try/except fallback that preserves the
historical WebUI behaviour when the agent package is not importable (e.g.
the WebUI standalone test environment).

Carve-out preserved: the canonical router conservatively returns "text" for
UNKNOWN/custom models (no models.dev capability data). WebUI historically
forwards those NATIVELY and relies on the agent's strip-and-retry guard
(_try_shrink_image_parts_in_messages / _strip_images_from_messages) to
downgrade on a provider rejection. We keep that: a canonical "text" verdict
is only honoured when there's a real signal — an explicit user choice
(image_input_mode: text or a configured auxiliary.vision backend) or a model
KNOWN to lack vision. Otherwise we forward native.

Tests:
- Existing gateway test (forwards_image_attachments_as_multimodal_parts)
  stays green — unknown "test-model" still forwards native.
- New non-vacuous regression: a model KNOWN to be text-only
  (supports_vision == False) now routes "text" — this FAILS against master,
  where the old local copy returned "native" for the same config.
- Added coverage for the unknown-model carve-out, known-vision native path,
  explicit-text-signal precedence, and the agent-unavailable fallback.

Salvage of #4113 by @gkd2323c. The original PR delegated fully to canonical
but did not add the unknown-model carve-out, which would have regressed the
WebUI native-forwarding behaviour the gateway test encodes.

Co-authored-by: gkd2323c <gkd2323c@users.noreply.github.com>
2026-06-28 08:48:06 +00:00
Frank Song
f786a70887 Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 16:41:42 +08:00
nesquena-hermes
04066b4b43 feat(insights): surface prompt-cache hit rate (salvage of #3912)
Surfaces prompt-cache hit rate on the Insights page so cache-efficiency
(a key cost-optimisation signal) is visible at a glance.

Backend (api/routes.py, _handle_insights):
- Aggregate cache_read_tokens per model and per day, plus a total.
- Graceful fallback for older agent state.db schemas lacking the
  cache_read_tokens column (COALESCE + OperationalError second query).
- New response fields: total_cache_read_tokens, total_cache_hit_percent,
  per-model cache_read_tokens + cache_hit_percent, per-day cache_read_tokens.

Frontend:
- Models table gains a Cache column (panels.js + style.css 6-col grid).
- Daily bar tooltip includes the bounded cache hit rate.
- New i18n keys insights_model_cache / insights_cache_hit in all 13 locales.

Correctness fix (maintainer-flagged on #3912): the original computed
hit-rate as cache_read / input_tokens (reads-over-misses), which EXCEEDS
100% on cache-heavy sessions. This uses the FULL prompt total as the
denominator -- cache_read / (input_tokens + cache_read) -- via the shared
api.usage.prompt_cache_hit_percent helper, so it is bounded 0-100% and
means "% of the prompt served from cache". Returns None (rendered as a
muted dash) when there are no cache reads.

Tests (tests/test_insights.py):
- Updated existing assertions for the new response shape.
- Added a non-vacuous <=100% boundary test: a cache-heavy case
  (cache_read=9900, input=100) where the OLD formula reports 9900% but the
  bounded denominator yields 99%. Verified the test FAILS under the old
  formula and PASSES under the fix.

Salvage note: PR #3912's diff was ~99 releases stale (mostly re-derived old
code, illusory +5405). This ports ONLY the real ~190-line feature fresh
onto current master.

Closes #3911

Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
2026-06-28 08:11:22 +00:00
Frank Song
5b96da8c1d test(anchor): locate JS body after params
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 16:11:21 +08:00
Frank Song
a86d2d2060 Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 16:01:58 +08:00
nesquena-hermes
31a01e8adf fix(routes): claim CLI/TUI/Desktop sessions on POST, not just GET (salvage of #4153)
CLI/TUI/Desktop sessions were *recreated* on every POST to /api/chat/start
instead of *claimed*, because the claim/synthesis logic only ran on the GET
stub path. GET /api/session already synthesized a read-only stub from
state.db, but POST /api/chat/start unconditionally 404'd on any session_id
without a WebUI JSON sidecar — and the frontend 404 handler then ran its
empty-state self-heal, stripping the URL and silently discarding the user's
typed message.

This closes the GET/POST asymmetry by routing both endpoints through a new
shared helper, _claim_or_synthesize_cli_session, that materialises a
WebUI-owned Session on first write (via synth.save()).

Preserved guards:
- Denylist of foreign-owned sources via _is_claimable_cli_source:
  {claude_code, cron, external_agent, gateway, messaging, unknown}; cli/tui/
  desktop fall through to CLAIMABLE.
- The not_claimable arm returns 403 (NOT 404) and never reaches synth.save()
  (ownership guard). 403-vs-404 matters: the frontend 404 handler runs the
  empty-state self-heal that strips the URL, so a legitimately-listed
  read-only session must surface a refusal instead of vanishing.
- created_at mapping from state.db is load-bearing (Session.save() doesn't
  touch created_at), so claimed sidecars no longer sort as "Jan 1 1970".
- The 500 path uses _sanitize_error so a raw OSError can't leak the sidecar
  filesystem path.
- The #2782 deleted-WebUI-session 404 self-heal contract is preserved via
  _session_index_marks_was_webui.
- The GET path's _session_visible_to_active_profile gate is preserved.

Polish over the original PR:
- Test docstring rationale corrected re: updated_at — Session.save() defaults
  touch_updated_at=True so the claimed sidecar's updated_at is stamped to
  "now" regardless; only created_at is preserved. Assertions unchanged.
- touch_updated_at=True default kept (a claimed session jumps to "just now",
  which is the desired UX); no test pins persisted updated_at to the state.db
  value.
- Added a path-leak assertion to the 500-sanitisation test: the sanitized
  string must not contain a '/' segment from the session-store root.

Salvage of #4153.

Co-authored-by: merodahero <merodahero@users.noreply.github.com>
2026-06-28 07:43:46 +00:00
Frank Song
51a0bee188 Merge origin/master into anchor fallback audit
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 15:40:48 +08:00
Frank Song
770d877467 test(anchor): harden JS helper extraction
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 15:32:45 +08:00
Stacey2911
b39c147b31 fix(workspace): live-sync active Workspace Todos tab 2026-06-28 07:28:30 +00:00
Frank Song
9c0eecfbe9 test(anchor): include node stderr in behavior guard failures
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-28 15:03:19 +08:00