Codex+Opus pre-release gate both flagged: TimeoutError is now in the
consolidated _CLIENT_DISCONNECT_ERRORS dispatch set, so a bare socket-connect
TimeoutError from Joplins urlopen(timeout=8) — which is NOT always URLError-
wrapped — would escape _handle_notes_search and be swallowed by the dispatch
disconnect handler as a fake client disconnect (silent empty response, no log).
Catch (URLError, TimeoutError) at the route so it surfaces as a clean
"not reachable" ValueError -> JSON error. Adds a regression test.
Co-authored-by: someaka <someaka@users.noreply.github.com>
OSError is too broad — it masks real errors like file-not-found.
ssl.SSLError specifically catches SSL-level disconnects without
swallowing unrelated OSError subtypes.
Closes the test_excludes_broad_oserror CI failure.
Address review feedback from @nesquena-hermes on PR #3210:
1. Deduplicate _CLIENT_DISCONNECT_ERRORS:
- Single authoritative definition in api/helpers.py
- api/routes.py now imports from api.helpers instead of defining
its own copy with different membership
- Unified tuple uses OSError (covers ssl.SSLError since it
subclasses OSError) — broad socket-level disconnect coverage
2. Remove github-search-report.md:
- Research scratch output that doesn't belong in the repo root
- Content belongs in PR description or a gist
3. Docstring improvement:
- Added comment explaining why OSError covers ssl.SSLError
- Documents the errno-level socket errors caught by OSError
- api/helpers.py: _safe_write() now logs disconnects at debug level
instead of silently passing. No more invisible errors.
- server.py: Restructure exception handlers to catch
_CLIENT_DISCONNECT_ERRORS first, then Exception. Remove the
isinstance() filter inside except Exception (LBYL anti-pattern).
The 500-response fallback now catches _CLIENT_DISCONNECT_ERRORS
separately (expected) and logs unexpected failures via
traceback.print_exc() instead of bare except Exception: pass.
- tests/test_broken_pipe_cascade.py: Add coverage for SSL/Timeout
disconnect routing and 500-response safety (both disconnect
survival and unexpected error logging).
Extract _safe_write() helper that wraps end_headers() + wfile.write()
in try/except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError,
TimeoutError, ssl.SSLError). Both j() and t() now use _safe_write()
instead of raw wfile calls.
Fixes cascading BrokenPipeError + SSL BAD_LENGTH crash when a client
disconnects mid-response and the error handler tries to write a 500
status through the same broken socket.
Boots the real server.py agent-free and loads the key pages in headless
Chromium, failing on any console error or uncaught JS exception. Catches the
runtime-JS brick class (const-reassign #3162, function/window collision
#2715/#2771) that node --check, ESLint, and the mocked pytest suite cannot see
because they only manifest when a real browser executes the page.
Credential-free: strips *_API_KEY from the env, drives no real model, needs no
secrets. Runs on every PR + push via .github/workflows/browser-smoke.yml.
Co-authored-by: nesquena-hermes <[email protected]>
Opus advisor noted the static guard covered 4 of the 5 identity-mismatch
eviction sites; add the credential-self-heal pop/close pair so a future
re-lock of that path is caught by the gate too.
Keeps the messages.js fix from @mysoul12138 (set _streamFinalized=true right
after the early-return guard, before the fade window, so a stream_end arriving
mid-fade can't call _restoreSettledSession and overwrite live messages with a
stale server snapshot). Drops the contributor's dead empty `else {}` block in
ui.js (no behavior). Adds a regression test pinning the immediate-finalize order.
Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com>
Opus advisor SHOULD-FIX: the tooltip rework dropped t('forked_from') in favor
of a hardcoded English string, a real i18n regression (the catalog key exists
in all locales). Restore the localized base while keeping the clearer
'<base>: <parent>' format. Adds a regression test.
The new lineage/child explanatory suffixes (additive English) and the
read-only title hint are deferred to a small follow-up — they need new locale
keys and the read-only state is still surfaced by the existing meta chip.
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
The merged hunk set state.title=_sessionStateTooltip(...) unconditionally,
two lines after assigning the localized attention.title (pending approval/
clarify, from #3190). That clobbered the attention tooltip and, for a
needs-attention session not currently streaming, blanked it to '' since
_sessionStateTooltip returns '' when neither streaming nor unread.
Make the attention title win, and apply the state tooltip only when non-empty.
Adds a regression test pinning the precedence.
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Completes the test-sharding half of #3197 (Docker-cache half shipped v0.51.177).
Adds pytest-shard 3-way split to tests.yml (3 shards x 3 Python = 9 jobs,
fail-fast: false). pytest-shard is 0-indexed so the matrix uses [0,1,2] — the
original #3197 used [1,2,3] which would have crashed the out-of-range job and
silently skipped shard 0's tests.
Made the suite shard-safe by fixing 4 cross-test state-pollution bugs that
passed sequentially but failed when sharded:
- test_onboarding_mvp: reset onboarding_completed flag (settings.json) in the
autouse fixture; the config-cleanup only cleared config.yaml/.env.
- test_issue693_system_health_panel: invalidate the process-wide password-hash
cache before/after so a prior test's "no password" cache doesn't defeat the
auth-gate assertion.
- test_auth_session_persistence: assert against auth._SESSIONS_FILE (where auth
actually writes) instead of a local _TEST_STATE path that only matched under a
lucky import order.
- test_profile_env_isolation (root cause of the worst leak): stop deleting +
re-importing api.profiles under a temp HERMES_BASE_HOME — that swapped the
module object and poisoned the cached _DEFAULT_HERMES_HOME for every later
test (broke test_title_aux_routing's load_config). Now points the cached path
via monkeypatch.setattr (auto-restored, no module swap).
- conftest: autouse fixture restores HERMES_HOME/HERMES_BASE_HOME after each
test as defense-in-depth against future switch_profile leaks.
Verified: all 3 shards green (6912 passed, 0 failed); full sequential run still
green (6957 passed, 0 failed). Slowest shard ~70s vs ~180s sequential.
Build the Docker image once in a build-image job, cache layers via type=gha,
restore from cache in each smoke variant instead of rebuilding. Variant restore
uses cache-from only (build-image is the sole cache writer).
Co-authored-by: hayriodabas <hayriodabas@users.noreply.github.com>
Per maintainer UX direction: drop the text badge pill ("APPROVAL" /
"N QUESTIONS") in favor of color-coding the existing right-side status dot
plus the colored left rail. Red dot (--error) for pending approvals, amber
dot (--warning) for pending clarifies; theme-driven, visible even when the
session is not streaming/unread. Approval dot gently pulses (disabled under
prefers-reduced-motion). Quieter and consistent with the existing pin/unread
dot family — the row no longer needs to truncate its title to fit a pill.
Updated test_session_attention_badges to assert the dot classes
(is-attention-approval/clarify) instead of the removed badge element/styles.
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
Adds a distinct two-tone attention sound (880->660Hz) for approval and
clarify prompts so they are not confused with the existing completion sound,
plus sidebar attention badges + colored rails driven by `attention` metadata
on /api/sessions. Includes lock-safety note: the in-lock
publish_session_list_changed() calls in clarify.py are safe because publish()
only takes the leaf _SESSION_EVENTS_LOCK and never re-acquires clarify._lock
(verified by Opus advisor review).
Co-authored-by: ai-ag2026 <261867348+ai-ag2026@users.noreply.github.com>
The new CLI/gateway insights pass counted ALL state.db sessions, but
WebUI-native sessions are persisted to state.db with source='webui' AND
already counted from the sidecar _index.json first pass — double-counting
them in totals, model breakdown, and daily charts. Add
`AND COALESCE(source, '') != 'webui'` so only CLI/gateway/cron/tui rows are
added by the second pass.
Adds regression tests proving (a) CLI + Telegram sessions appear in totals
and (b) a webui-source state.db row is not double-counted against its
_index.json entry.
Co-authored-by: wind-chant <wind-chant@users.noreply.github.com>
The Insights page () previously only counted WebUI-native
sessions from its own session index. This adds a query to the Hermes
state.db so CLI and gateway sessions (Telegram, Discord, etc.) are also
included in the aggregated token counts, costs, model breakdown, and
daily activity charts.
The state.db query is best-effort: if the file is missing or
unreadable, the existing WebUI-only data is returned as before.
Custom API aggregators (New API, One API, etc.) route requests using their
own naming conventions — bare names like ``deepseek-v4-flash`` or dot-separated
names like ``moonshotai.kimi-k2.5`` — rather than the OpenRouter-style
``vendor/model`` slash format that ``_heuristic_reasoning_efforts``'s prefix
list was written for.
Because none of these names match the slash-prefixed patterns, the function
returned ``[]``, hiding the reasoning effort selector in the WebUI even for
models that fully support thinking/reasoning.
Fix: add a secondary check in ``_heuristic_reasoning_efforts`` that strips an
optional dot-vendor prefix and matches the remaining model name against a list
of known thinking-capable bare-name prefixes (``deepseek-v4``, ``deepseek-r1``,
``kimi-k2``, ``qwen3``, ``claude-3/4``, ``o1/o3/o4``), plus a keyword catch
for model names containing ``thinking`` or ``reasoning``.
The slash-prefix path and all provider-specific fast paths (copilot, lmstudio,
models.dev metadata) are unchanged; this block only runs as a final fallback.
Regression tests added in ``tests/test_custom_provider_bare_model_reasoning.py``.