Commit Graph
100 Commits
Author SHA1 Message Date
nesquena-hermes 8a6937978c fix(#5570): skip clear sentinel backups during recovery
Closes the crash-window resurrection corner in the #5532 clear-conversation
data-loss fix. /api/session/clear writes a .json.bak before truncating, and
because the cleared sidecar's truncation_watermark is 0.0, startup recovery saw
bak_count > live_count and could restore the backup — resurrecting the cleared
transcript after a crash/restart in the clear window.

Fix: stamp a unique per-clear `clear_generation` UUID on the sidecar when
clearing a session that had messages (persisted via Session model). Recovery's
_session_records_clear_sentinel() returns no_action when the live sidecar carries
a clear_generation the backup doesn't share AND the live sidecar matches the
exact cleared-empty shape (messages/context empty, watermark/boundary 0.0,
pending fields cleared). Same-generation backups stay recoverable; unreadable or
partial matches FAIL OPEN (normal recovery), so a real crash-loss is never
suppressed.

Contributor stage; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 02:10:30 +00:00
nesquena-hermes 20ca99bc77 release #5573: guard viewport anchor key against lone surrogates + CHANGELOG 2026-07-05 02:06:09 +00:00
nesquena-hermes ef33ad44f2 fix(#5552): guard _messageViewportAnchorKeyForMessage against lone surrogates
A message whose text contained a lone UTF-16 surrogate (e.g. a truncated emoji
from a partial paste/stream) made encodeURIComponent throw URIError inside
_messageViewportAnchorKeyForMessage, crashing the viewport-anchor computation and
breaking scroll-anchor restore for the whole transcript. New _safeEncodeURIComponent
catches the URIError and rebuilds the string via a UTF-16 code-unit walk that keeps
valid high+low surrogate PAIRS intact (emoji survive) and drops only LONE
surrogates. Uses no regex lookbehind/lookahead so it parses on every browser
engine (older WebViews / Safari <16.4 lack lookbehind and would brick ui.js at
parse time).

Contributor stage; gate-pass. Co-authored-by: rumotoshino <rumotoshino@users.noreply.github.com>
2026-07-05 01:49:08 +00:00
nesquena-hermes b2446a54ec release #5593: keep active CLI sessions visible before final output + CHANGELOG 2026-07-05 01:26:23 +00:00
nesquena-hermes b9e8ade4b6 fix(#5587): keep active CLI sessions visible before final output
Self-built stage of contributor PR; gate-pass.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 01:25:50 +00:00
nesquena-hermes 2694ba9e03 release #5592: preserve completed assistant replies during settle + CHANGELOG 2026-07-05 01:15:43 +00:00
nesquena-hermes 73d4b2b227 fix(#5575): preserve completed assistant replies during settle
The streaming settlement path could show a misleading "No response from provider"
error card AFTER a completed answer, because a stale terminal/partial result flag
still forced the generic no_response error path even when the merged current turn
already had a final assistant answer. Now the generic no_response terminal error
is suppressed ONLY when the already-merged current turn has a completed assistant
reply (scoped to soft `partial` results per the issue-thread recommendation),
letting normal `done` settlement continue. Real silent failures, partial
failures, auth/quota errors, cancellation, replayed rows, and terminal
tool/compression cases all keep their existing error behavior.

Self-built stage of contributor PR; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 01:15:09 +00:00
nesquena-hermes a646a23e5b release #5565: distinct Mermaid fullscreen toolbar icon + CHANGELOG 2026-07-05 01:07:35 +00:00
nesquena-hermes 7ee9f78a2e fix(#5525): distinct Mermaid fullscreen toolbar icon
The Mermaid toolbar's "fit" and "fullscreen" icons rendered byte-identical (the
fullscreen path was fit's corner-brackets drawn twice). Replaced the fullscreen
glyph with a distinct corner-frame + diagonal-expand-arrows icon so the two
actions are visually distinguishable at 14px. Still wired to openLightbox; no
dependency on the old path. (The other half of #5525 — mermaid height:0 on
mobile — does not reproduce on current master; verified live at 390x844.)

Self-built (nesquena-hermes); nesquena independent review APPROVED.
2026-07-05 00:53:42 +00:00
nesquena-hermes 7f869b8e39 release #5559: classify credential-pool exhaustion with an actionable hint + CHANGELOG 2026-07-05 00:35:59 +00:00
nesquena-hermes 93f447c7af fix(#3929 partial): classify credential-pool exhaustion with an actionable hint
'All 0 credential(s) exhausted for <provider>' did not match _is_quota_error_text
so it fell through to a generic 'Error' with no hint (the exception path's else
branch discarded the hint entirely). Adds a distinct credential_pool_empty
classification (ordered before the quota check) + an explicit exception-path
branch + an actionable pool-specific hint, so the user sees why the turn failed
and what to do. Does not over-match (doesn't swallow real quota errors) and
doesn't block partial-harvest; downstream tolerates the new type.

NOTE: this is the CLASSIFICATION half of #3929 only — the data-loss half
(reasoning/partial-work restore on reload-reconcile) is a scoped follow-up that
needs the reporter's server logs.

Self-built (nesquena-hermes); nesquena independent review APPROVED.
2026-07-05 00:35:26 +00:00
nesquena-hermes 60c748511f release #5578: stop login next= param nesting/URL-explosion + CHANGELOG 2026-07-05 00:23:06 +00:00
nesquena-hermes e09e819f08 fix(#5578): stop login next= param from nesting into itself and exploding the URL
An expired-auth 401 on the login page fed the login redirect its own address:
each of the three redirect guards (_safeNextPath in login.js,
_safe_login_redirect_path in routes.py, the api() 401 redirect in workspace.js,
_safe_login_inner_next in auth.py) validated against open-redirect but none
rejected a `next` pointing back at the login page, so every auth bounce wrapped +
re-encoded the previous login URL one level deeper — growing the URL exponentially
until the browser's length limit broke the tab. All guards now collapse/reject a
`next` whose decoded leading PATH resolves to the login route (bounded
percent-decoding up to 8 levels, fail-closed at the cap), while preserving the
existing open-redirect protections AND legitimate non-login paths that merely
carry their own `next=` query key. Length cap as belt-and-suspenders.

Self-built (nesquena-hermes); nesquena independent review APPROVED. Gate (Codex)
found + fixed an over-broad nested-next reject (regressed next-carrying paths) and
a deep-encode decode off-by-one; both root-fixed, login guards verified clean.
2026-07-05 00:12:19 +00:00
nesquena-hermes 3d11a6ad54 release #5567: context-local HERMES_HOME override for background workers + CHANGELOG 2026-07-04 23:50:54 +00:00
nesquena-hermes 019a4814b7 fix(#5567): pin worker profile via hermes-agent v0.18.0 context-local HERMES_HOME override
profile_env_for_background_worker mirrored the profile home into process-global
os.environ["HERMES_HOME"] and yielded the worker body OUTSIDE the setup lock, so a
concurrent cross-profile worker (e.g. background title generation on another
profile) could clobber os.environ mid-body — the agent config reader then resolved
the WRONG profile's config (intermittent turn-init failures citing an unused
provider; b3nw's v0.51.849 repro). Now installs hermes-agent v0.18.0's
context-local home override (set_hermes_home_override, a ContextVar that
get_hermes_home() consults before os.environ) for the resolved non-default profile
home — set before yield, reset in finally — so a config read in the worker body
resolves THIS profile's home from task-local state, immune to a concurrent
os.environ clobber. No worker serialization, no os.environ mutation, no detection
heuristic (both earlier WebUI-only mitigations were rejected as regression-unsafe).

Graceful degradation: the override module is resolved lazily+optionally
(_resolve_hermes_home_override, mirroring _resolve_secret_scope_module). On agents
< v0.18.0 the symbol is absent -> returns None -> pre-existing os.environ-mirror
behavior unchanged (no regression, no new failure mode).

Self-built (nesquena-hermes); nesquena independent review APPROVED.
2026-07-04 23:40:10 +00:00
nesquena-hermes 9b293f660c release #5564: mint stable per-message ids for large-session fork alignment + CHANGELOG 2026-07-04 23:24:58 +00:00
nesquena-hermes 8e954a423d fix(#5564): mint stable ids BEFORE dedupe at all commit sites (stale-boundary path)
Codex round-3 gate finding: _dedupe_replayed_context_messages deep-copies the
stale-user repaired boundary row (streaming.py:4501) into the context array. When
the mint ran AFTER dedupe, that deep-copied context row was no longer the shared
result dict, so it stayed id-less while the display copy got the minted id —
re-opening the cross-array id divergence for the stale-repair path. Fix: move
_assign_stable_message_ids() to run immediately after _restore_reasoning_metadata
and BEFORE _dedupe_replayed_context_messages at all four commit sites (streaming
main / in-band self-heal / except-path self-heal + routes runs/MoA), so the shared
result rows are stamped before any deep-copy — both arrays inherit the id.
2026-07-04 23:15:41 +00:00
nesquena-hermes a8568749a1 fix(#5564): carry minted id onto eager-save checkpoint row in display merge
Codex/Opus gate finding: in eager session-save mode the current user turn is
checkpointed into s.messages before the agent runs (no id yet); after
_assign_stable_message_ids stamps the returned result row, the display merge
keeps the durable eager checkpoint and skips the stamped result — leaving the
display row id-less while its context twin carries the minted id, silently
defeating id-based fork/truncate alignment for eager-mode users. The merge now
copies the minted id onto the kept checkpoint when it lacks one (guarded so it
can't overwrite or duplicate). Opus verified cross-array id pairing holds across
the real 3-turn transform pipeline.
2026-07-04 23:06:21 +00:00
nesquena-hermesandb3nw ee93425b26 fix(#5564): mint stable per-message ids so large-session forks align
WebUI keeps two parallel arrays per session: messages (display transcript) and
context_messages (what's sent to the model). In large/compacted sessions they
diverge. Forking / in-place truncation copies a prefix of both and relies on
truncate_context_for_display_keep() to translate a display keep-index into the
matching context index — an aligner that prefers a stable per-row `id`. But the
model-context rows carried neither id nor timestamp (a 989-session live scan
found 0 with any id), so alignment fell back to fragile content-signature
matching that goes ambiguous on repeated tool calls / empty assistant turns.

This closes the DATA gap (the alignment logic itself already prefers `id` and
handles the compacted case via the matcher, shipped in #5563): mint a monotonic,
session-unique integer `id` on the per-turn result rows AFTER the context restore
and BEFORE both arrays are built, so the display and model-context copies of a
logical row share the same id.

- api/streaming.py: new _assign_stable_message_ids(); _restore_reasoning_metadata
  carries `id` forward across turns (as it already does timestamp); wired into all
  three streaming commit sites (main turn, retry, self-heal).
- api/routes.py: same mint on the runs/MoA _handle_chat_sync commit path.
- api/gateway_chat.py: mints ids on the two new rows of a gateway turn.
- The `id` is stripped before the provider API call (not in _API_SAFE_MSG_KEYS),
  same treatment as timestamp — nothing new reaches the provider.

session_ops.py alignment was already delivered by #5563 (id-preferring matcher +
compacted-case fall-through), so no change is needed there now; tests updated to
assert the post-#5563 behavior (id-bearing = exact cut; id-less = errs toward
under-keeping, never the old raw-index mis-cut).

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-04 22:46:56 +00:00
nesquena-hermes 7ce5c83d69 release #5562: refresh sidebar recency after missed session events + CHANGELOG 2026-07-04 22:34:54 +00:00
nesquena-hermesandrodboev b36bd789ce fix(#5551): refresh sidebar recency after missed session events
A reactivated older conversation wasn't bumped to the top / into Today until a
manual refresh when the viewing tab was backgrounded/unfocused: refreshSessionList
early-returns on document.hidden unless force=true, and the sidebar SSE closes on
blur — but the in-flight/pending coalescing only preserved the refresh REASON
string, dropping the opts (force/refreshActive). So a coalesced 'sessions_changed'
refresh lost its force flag and got skipped on a hidden tab. Now the pending
request preserves merged opts (force/refreshActive OR-merged), and a dedicated
resume-refresh path forces a catch-up when the sidebar regains focus, so
cross-device recency re-sorts land without a manual refresh.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 22:34:28 +00:00
nesquena-hermes a7527bef13 release #5558: keep Transparent Stream final replies visible after settle + CHANGELOG 2026-07-04 22:19:25 +00:00
nesquena-hermesandrodboev ae43091993 fix(#5550): keep Transparent Stream final replies visible after settle
The live-anchor scene renderer hardcoded mode:'compact_worklog' in
_renderAnchorLiveScene / _projectLiveAnchorActivityScene / the settle projection,
regardless of the user's actual active display mode. In Transparent Stream mode
the final reply was therefore projected with compact-worklog display hints, so on
turn-settle it folded into the collapsed worklog disclosure group (a refresh fixed
it because the stored transcript was correct). Now the renderer reads the real
active mode via _anchorSceneActiveMode() (window.chatActivityMode() ->
_chatActivityDisplayMode -> _transparentStream, default compact_worklog) and
applies per-mode display hints via _anchorSceneRowDisplayHintForMode(), so the
final answer stays a visible chronological reply in Transparent Stream.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 22:09:50 +00:00
nesquena-hermesandluperrypf cebfdf41f9 fix(#5541): iOS PWA streaming freeze + SSE reconnect reliability
Two mobile reliability fixes on the crown-jewel chat streaming path:
(1) content-visibility:auto on off-screen .msg-row under @media(pointer:coarse)
so WKWebView skips layout/paint for off-screen rows during streaming (kills the
long-chat freeze). The LIVE turn is kept content-visibility:visible via the
STABLE #liveAssistantTurn id (covers ALL render modes — Compact Worklog,
Transparent Stream, restored-live — not just the Transparent-Stream path that
stamps data-live-assistant-turn), so a normal live turn on touch never blanks
mid-stream and its height keeps growing so the new-message cue still fires.
contain-intrinsic-size:auto 1px preserves flick-scroll momentum. Scoped to
touch — desktop find-in-page untouched. Deliberately does NOT set
overflow-anchor:none (inert on iOS WebKit; re-opens #4856/#5338).
(2) SSE reconnect ladder extended 4->6 steps + a last-ditch
_restoreSettledSession full-session poll (8s watchdog) after retries exhaust, so
a response completed during an iOS Tailscale/VPN reconnect is recovered without
an error banner.

Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-04 21:51:38 +00:00
nesquena-hermes e974f03829 release #5543: fire-and-forget memory commit + drain-safe managed stop + CHANGELOG 2026-07-04 21:41:05 +00:00
nesquena-hermesandluperrypf 19d00e3586 fix(#5543): fire-and-forget commit_session_memory + drain-safe managed stop
Move the 1-5s commit_session_memory() extraction off the POST /api/session/new
request thread (was blocking + New Chat) into a fire-and-forget daemon thread,
guarded by the existing per-session in-flight serialization.

Data-loss prevention on managed stop:
- server.py installs a SIGTERM handler that requests an orderly stop so
  serve_forever()'s finally block runs drain_all_on_shutdown() (the default
  SIGTERM disposition terminated without unwinding, losing in-flight commits).
- session_lifecycle: bounded self-unregistering background-commit registry;
  a _draining flag so no NEW worker starts once the drain begins (the inline
  generation-drain commits any late arrival instead); a 30s overall drain
  deadline so a stuck worker cannot hang the stop path forever.

Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-04 21:40:21 +00:00
nesquena-hermes 3b541b745f release #5544: re-pin auto-scroll at true bottom + CHANGELOG 2026-07-04 21:24:39 +00:00
nesquena-hermesandluperrypf 4dc87e9069 fix(#5544): re-pin auto-scroll only at true bottom after scroll-to-bottom
A single scroll-up during streaming set _messageUserUnpinned=true and
scrollIfPinned() then permanently stopped auto-follow (only scrollToBottom()
cleared it) — a permanent auto-follow lockout even after the user returned to
the bottom. scrollIfPinned() now re-pins, but ONLY when the reader has genuinely
reached the true-bottom tail (<=80px) AND shows no active scroll intent
(wheel/key/touch/non-message), reusing the listener's _nearBottomCount debounce.
Proximity alone (the ~250px nearBottom band) must never re-pin — that is the
#4295 invariant. Restores the listener's <=80px true-bottom gate too.

Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-04 21:24:17 +00:00
nesquena-hermes e588901f2f release #5563: align forked context to display for large/compacted sessions + CHANGELOG 2026-07-04 21:07:49 +00:00
nesquena-hermesandb3nw 322c9fdb62 fix(#5563): align forked context to display for large (compacted) sessions
Fork ("Fork from here") on a large/compacted session could cut context off a
real turn boundary — slicing mid-turn or leaving a dangling tool call, feeding
a malformed context to the model. Align the fork cut to a resolved turn boundary
via a signature matcher handling both compacted and non-compacted display
coordinate spaces; errs toward under-keeping with send-time sanitization backstop.

Co-authored-by: b3nw <b3nw@users.noreply.github.com>
2026-07-04 20:55:06 +00:00
nesquena-hermes 3f599ead04 fix(test): drop the one genuinely-unused _seed_session_dir binding (lint)
CI lint (diff-scoped ruff F) flagged 'models =' unused at line 92 (that test
uses only the seed side-effect). Removed the assignment there; kept it at line
320 where test_clear_survives_startup_recovery legitimately reads models.SESSION_DIR.
Ruff clean, 11 #5532 tests pass.
2026-07-04 20:15:20 +00:00
nesquena-hermesandrodboev 81e89ec7b2 release #5532: clear-conversation P0 data-loss (combined, rebuilt on de-brittled base)
Rebuilt the combined #5556+#5553 fix on v0.51.859 (now has the flake fix + the
#5542 RFC de-brittle, so no anchor-test collateral). Nathan's call: rodboev's
#5556 primary + fold self-built #5553 improvements, credit both.

Clear handler now:
- routes through shared truncate_session_at_keep(s,0) (single source of truth,
  sets watermark=_truncation_watermark_for([])==0.0, the #2914 sentinel that
  blocks state.db append-merge replay)
- detaches compression lineage ONLY when the parent is a pre_compression_snapshot
  (Codex-caught: preserve genuine fork parent links for nesting + "Forked from")
- rodboev's persisted-clear read-back verification + stale .bak removal

RFC conflict resolved in favor of the de-brittled (symbol-anchor) master version.
Ships both PRs' test files (state_db_replay + clear_truncation_watermark incl the
fork-preservation regression). Opus (on the amended tree): both resurrection paths
closed, detach lineage-safe, verification sound — SHIP. 3 non-blocking follow-ups
filed (#5570 .bak crash-window, #5571 fork-stitch corner, #5572 messaging clear).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 19:58:38 +00:00
nesquena-hermes 5e26abca89 Merge commit 'ea2dc31c09bc' into release/stage-5532b
# Conflicts:
#	docs/rfcs/session-sse-contract-v1.md
2026-07-04 19:56:02 +00:00
nesquena-hermes 4c1cdcba3e Merge remote-tracking branch 'origin/master' into fix/5542-debrittle-rfc-anchors 2026-07-04 19:44:59 +00:00
nesquena-hermes d03c6caa61 Merge remote-tracking branch 'origin/master' into release/stage-5547 2026-07-04 19:12:25 +00:00
nesquena-hermesandrodboev 3dee3d299e release #5547: Transparent Stream running-tool status fix + CHANGELOG
rodboev's one-arg fix (#5523): _anchorSceneTransparentNodeForRow passed a
hardcoded settled=true into _transparentToolStatus even when the caller supplied
settled:false, so a running tool rendered as done in Transparent Stream. Now
passes the caller's actual  value. +19/3f, 93 tests green.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 19:03:58 +00:00
nesquena-hermes 3550af894c Merge commit '6cf5706f78ef' into release/stage-5547 2026-07-04 18:57:58 +00:00
nesquena-hermes d025b37186 fix(tests): service_tier flake TRUE root cause is stale global cfg, not yaml cache
Two full-suite iterations pinned the real vector: set_hermes_default_model() calls
resolve_model_provider() which reads the module-global config.cfg (config.py:2488).
A prior lmstudio test leaves cfg={model:{provider:lmstudio,base_url:1234}} in
_cfg_cache; resolve_model_provider then returns lmstudio's provider+base_url, which
set_hermes_default_model persists to the test's OWN config file -> the on-disk
assertion sees 'provider: lmstudio' instead of the written 'provider: openai'.
The autouse fixture now _cfg_cache.CLEAR()s its CONTENTS (not just re-aliases cfg),
so resolve_model_provider reads an empty model cfg and can't inject a stale
provider/base_url. Deterministically proven: reproduced the exact lmstudio leak with
a probe test (fixture on=PASS; _cfg_cache.clear() line removed=the exact full-suite
failure reproduces). Also clears _yaml_file_cache + fingerprint/mtime for defense.
2026-07-04 18:56:58 +00:00
nesquena-hermes 3a70ab5871 fix(tests): service_tier flake root cause is _yaml_file_cache poison, not just cfg rebind
The full-suite run revealed the TRUE mechanism: the failure is at the DISK
assertion (before get_auxiliary_models) — set_hermes_default_model() read a
STALE lmstudio config and wrote it back. Root cause: the module-global
_yaml_file_cache is keyed on (str(config_path), st_mtime_ns, st_size); pytest
reuses tmp_path base dirs across the session, so a prior lmstudio test's
config.yaml shares both the path string AND (mtime_ns,size) with this test's tiny
file (same coarse mtime tick) → stale cache hit serves the wrong provider's
config. The autouse fixture now ALSO clears _yaml_file_cache (proven: 2->0), in
addition to the cfg/_cfg_fingerprint reset for the get_auxiliary_models read path.
Fixed the duplicate decorator introduced in the prior commit.
2026-07-04 18:43:45 +00:00
nesquena-hermes ea8ee80a9c Debrittle #5513 session-SSE-contract RFC anchors to symbols (#5542)
The RFC (docs/rfcs/session-sse-contract-v1.md) cited hardcoded absolute
line numbers like `api/routes.py:12345-12346` and `api/routes.py:16177`,
and tests/test_issue4812_session_sse_contract_rfc.py validated those exact
lines against source. Any PR that shifted routes.py lines (e.g. #5543,
#5534) broke the test — a chronic brittle-failure class (#5542).

Root fix: anchor by SYMBOL, never by line number.

RFC:
- Strip every `api/routes.py:<NNNN>` / `<NNNN>-<NNNN>` and
  `api/streaming.py:<NNNN>` line-number suffix; keep the symbol name
  (route string, function/constant names) + the file, e.g.
  "_handle_session_events_stream() in api/routes.py".
- Update the inventory note to state names are the stable anchors and the
  RFC deliberately avoids line numbers so a source-layout shift can't
  invalidate the doc or its contract test.

Test:
- Rewrite the two line-anchor tests to assert (a) each cited symbol still
  exists in api/routes.py and (b) the RFC names that symbol — dropping the
  `api/routes.py:<line>` regex bounds-checks entirely. Invariant preserved:
  "the RFC's cited symbols are real and named in the doc", NOT "the cited
  line numbers are exact".
- Add test_rfc_uses_no_hardcoded_routes_line_numbers as a regression guard
  that fails if any `*.py:<line>` anchor is reintroduced into the RFC.

Proven de-brittled: prepending 31 lines to api/routes.py (scratch, reverted)
leaves all 34 tests green.

Refs #5542, #5513
2026-07-04 18:42:22 +00:00
nesquena-hermes f3f328df3d fix(tests): eliminate the two chronic full-suite flakes (nous_picker + service_tier)
Both fail ONLY in the full suite, not in isolation — they've been noise on every
release gate. Root-caused and fixed at the source (no retries/skips):

1. test_issue1567 nous_picker (test_hermes_cli_unavailable_falls_back_to_static_4):
   NOT actually a flake — a real assertion that silently broke as the curated list
   grew. It asserted every curated @nous: id (now 28) appears in the picker group's
   visible 'models', but when the list exceeds _NOUS_FEATURED_THRESHOLD (25) the
   dropdown intentionally shows only the featured subset and stashes the rest under
   'extra_models'. Fixed to assert the invariant that matters ('no curated entry is
   DROPPED') against the visible+extra_models union, so it tracks the real behavior
   instead of drifting to failure every time the catalog grows.

2. test_issue4536 service_tier (test_main_service_tier_roundtrip_via_auxiliary_endpoint):
   Classic test-isolation leak. get_auxiliary_models() reads the module-global
   config.cfg after reload_config(); if an EARLIER full-suite test rebinds
   config.cfg via monkeypatch and leaves 'cfg is not _cfg_cache',
   _cfg_has_in_memory_overrides() stays True and get_auxiliary_models() reads that
   stale dict instead of the freshly-reloaded temp config. Added an autouse fixture
   that re-aliases cfg to _cfg_cache and clears the override fingerprint before AND
   after each test in the class. PROVEN: reproduced the failure deterministically
   with a leak-probe test, confirmed the fixture neutralizes it (fixture off = FAIL,
   fixture on = PASS).
2026-07-04 18:32:43 +00:00
nesquena-hermesandruizanthony 6baf501dce harden #5471: dual-signature suppression (Opus root fix) — covers stale prefix + attachment
Opus found the exact-match signature fails open in the COMMON case: the server's
last persisted draft is often a PREFIX of the submitted text (Enter-to-send within
400ms cancels the pending debounced save), so matching only the submitted text
misses it and the prefix restores (the literal 'tail of the already-submitted
message' symptom). Root fix (Opus-recommended): capture BOTH signatures at send —
the remembered SERVER-draft signature (_composerDraftPayloadSignatureForSid, read
before the reset) AND the submitted-payload signature — and suppress a restore
matching EITHER. The prefix IS the remembered server draft; the remembered files
went through the same lossy serialization so they self-match; a genuinely new
cross-tab draft matches neither and restores. Subsumes the file-canonicalization
fix (kept as belt-and-suspenders). Suppression stores signatures[] not signature.

Regression tests: stale-prefix suppressed (Opus Finding 1), file round-trip
self-matches (Codex Finding 2), cross-tab-different still restores. Test harness
updated to model send-time server draft vs later restore draft as distinct.

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-04 08:51:13 +00:00
nesquena-hermesandruizanthony 5a8dc91bbb harden #5471: canonicalize draft files before persist+sign (attachment round-trip)
Codex found a SILENT edge in the payload-signature fix: a live browser File
JSON-serializes to {} when the draft is POSTed to the server, so a text+attachment
send's suppression signature (computed from the rich File) never matched the
restored server draft (files:[{}]) — suppression cleared and the just-sent tail
could repopulate for the attachment case. Fix: _composerDraftFilesForPersist()
canonicalizes files to a stable serializable shape (name/path/size/type/lastModified)
used at BOTH _saveComposerDraft/_saveComposerDraftNow (persist) AND
_composerDraftPayloadSignature (sign), so the sent signature matches the
round-tripped server draft. Regression test self-demonstrates lossy-mismatch vs
canon-match.

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-04 08:37:15 +00:00
nesquena-hermes 1d4d224f67 release #5471: payload-scoped composer draft-restore suppression + CHANGELOG
ruizanthony's re-push answering the gate-cert SILENT finding: the 30s post-send
draft-restore suppression discriminated only by sid+TTL, so a legitimately NEW
cross-tab draft for the same session was silently swallowed for up to 30s. Fix
adds a payload signature (_composerDraftPayloadSignature): suppression now only
blocks a restore whose payload matches the just-sent one; a different-signature
draft restores immediately. Verified the fix-spec is implemented (sessions.js:64-104).
2026-07-04 08:27:39 +00:00
nesquena-hermes 85898453dd Merge commit '68a692a8e07b' into release/stage-5471 2026-07-04 08:26:49 +00:00
nesquena-hermes 658624af7f release #5522: Windows git.exe resolution for version/cache-bust + CHANGELOG
allenliang2022's fix: WEBUI_VERSION degraded to 'unknown' on Windows when git
wasn't on the launching process PATH (venv/service/non-interactive launcher),
freezing the ?v= static-asset cache stamp so browsers served stale JS/CSS after
upgrade. Extends the pre-existing _resolve_git_executable() with a win32-only
Git-for-Windows registry lookup (WOW64 64/32 + HKCU) + common install-path
fallbacks, only when shutil.which('git') fails. Non-win32 paths untouched.
5 tests incl non-windows-never-touches-registry guard + e2e version recovery.
2026-07-04 08:07:29 +00:00
nesquena-hermes f65598113d Merge commit '2c03277a9738' into release/stage-5522 2026-07-04 08:06:47 +00:00
nesquena-hermes 2f97625323 release #5456: transparent-stream churn reduction + CHANGELOG
Stacey2911's follow-up to the identity-preserving live-row reconcile: skip
redundant innerHTML rewrite when unchanged, skip reinsert when already in
position, stop thinking-block scrollbar flicker. Deep-review subagent + gate:
no identity-invariant break (attrs/className/_tcData/interactive-state all still
synced unconditionally; only redundant DOM writes gated). Staged on current
master (resolves the stale-base #4295 test artifact — 114 transparent/anchor
tests green on merge).
2026-07-04 07:46:18 +00:00
nesquena-hermes 7a8b5ef7cf Merge commit '6771fc2cdc34' into release/stage-5456 2026-07-04 07:45:19 +00:00
nesquena-hermesandakay64 d27ef5d196 harden #5511: active provider owns its OWN providers: allowlist (complete ownership model)
Codex 5th CORE: the ownership set only covered static _PROVIDER_MODELS, not the
active provider's own config.yaml providers.<active>.models allowlist. So an
active provider defined purely via providers: (no static catalog) could be
hijacked by another slug's overlapping entry earlier in config order. Fix folds
the active provider's own providers: allowlist (list + dict shapes, copilot
excluded) into _provider_models_set — completing the ownership model so the
active provider owns its declared models from EITHER the static catalog OR its
own providers: entry. Regression test (fail-without-fix verified). 40 model_resolver
tests green.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:28:44 +00:00
nesquena-hermesandakay64 f2604a22bd harden #5511: _canonicalise_provider_id accepts alias targets in _PROVIDER_MODELS (Gemini class)
Codex 4th CORE (root latent bug in the shared canonicalizer): _canonicalise_provider_id
only accepted an alias resolution if the target was in _PROVIDER_DISPLAY. But some
canonical targets (e.g. 'gemini') are indexed in _PROVIDER_MODELS and NOT
_PROVIDER_DISPLAY, so 'google-gemini'->'gemini' was rejected and returned raw
'google-gemini' — leaving the ownership guard empty and letting providers.openai.models
hijack an active Gemini-owned model. Fix mirrors the direct-hit check (line 1445):
accept alias targets in EITHER _PROVIDER_DISPLAY OR _PROVIDER_MODELS. This is the
root fix for the whole alias-canonicalization class (gemini/google-gemini/
google-ai-studio etc.), not just the reported site. Blast-radius verified: 81
canonicalise/provider-alias tests across 5 files still green; x-ai still stays x-ai,
opencode_go still folds. Gemini regression test added (fail-without-fix verified).

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:23:45 +00:00
nesquena-hermesandakay64 4399ff02e0 harden #5511: canonicalise active provider for ownership set (latent alias bug)
Codex 3rd CORE (pre-existing latent bug the providers: scan exposes): the
_provider_models_set ownership lookup used the RAW config_provider (e.g. 'z-ai')
but _PROVIDER_MODELS is keyed by canonical slug ('zai'), so an aliased active
provider got an empty ownership set -> _skip_custom_providers guard-2 silently
failed -> another providers.<slug>.models entry could hijack an active-owned
model (verified: active z-ai + glm-5 + providers.openai.models.glm-5 -> openai).
Fix: canonicalise config_provider before the _PROVIDER_MODELS lookup (and reuse
the canonical slug for the scan's ownership guard). Regression test with an
aliased active provider (verified fail-without-fix).

This is the root-cause fix for the alias class — the ownership guard now holds
for every aliased provider (zai/kilo/etc.), not just the reported site.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:17:05 +00:00
nesquena-hermesandakay64 f8c9f9e08c harden #5511: providers: scan honors active-provider ownership guard
Codex 2nd CORE: the new providers: scan didn't honor the _skip_custom_providers
active/default ownership guard (config.py:2535) that the custom_providers scan
uses. So with an active provider (e.g. ai-gateway) + model.default=gpt-5 AND a
providers.openai.models.gpt-5 entry, the scan hijacked gpt-5 to openai instead of
the active ai-gateway path. Fix: when _skip_custom_providers is true, the
providers: scan only considers the active provider's own canonical slug (skip all
other slugs). Regression tests: active-provider-owns-default not hijacked (fails
without fix); active provider's OWN providers: entry still matches.

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:10:47 +00:00
nesquena-hermesandakay64 cca3b6c279 release #5511: exclude Copilot from providers: routing scan + regression tests + CHANGELOG
Gate-cert (nesquena-hermes) + Codex found 1 CORE: akay64's new providers: scan in
resolve_model_provider() iterated ALL providers.<slug>.models INCLUDING
providers.copilot.models — but that key is a documented per-model SETTINGS map
(reasoning_effort/limits), NOT a routing allowlist (config.py exception). So a
Copilot per-model settings entry hijacked that model's routing to Copilot
(Codex reproduced gpt-5 -> copilot). Fix (Codex-exact): skip copilot in the scan.

Added the regression tests the PR shipped 0 of: user-defined allowlist routes;
active-openai + providers.copilot.models (dict + list shapes) stays openai
(both verified to fail without the exclusion).

Co-authored-by: akay64 <akay64@users.noreply.github.com>
2026-07-04 07:06:05 +00:00
nesquena-hermes ec9f77c319 Merge commit '87404d76f036' into release/stage-5511 2026-07-04 06:57:54 +00:00
nesquena-hermesandrodboev a94dacb679 release #5513: fix live-emission anchor + source-validate all run-journal anchors
Codex re-gate finding: line 83 cited api/routes.py:15703-15762 for live SSE id:
emission, but that range is replay/helper code — the actual _sse_with_id() call
sites are 15918 (live /api/chat/stream), 15811 (runner-observe), 15721/15734
(journal replay). Corrected the bullet with the real, distinct anchors.

Also extended the test (test_rfc_run_journal_anchors_land_on_real_source) to
validate each named-symbol anchor (_parse_run_journal_event_id/_after_seq,
_runner_event_id, _replay_run_journal) + the live-emission anchor against the
actual source token, so no stale line number can silently pass.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 06:48:35 +00:00
nesquena-hermesandrodboev 6182ae044b release #5513: correct RFC source anchors + self-validating test
Codex Phase-1 gate found the RFC cited stale/wrong source anchors (docs-only, but
actively misleading to future implementers): /api/sessions/events is routed at
routes.py:12345-12346 (RFC said 12320-12321 = /api/chat/cancel);
_handle_session_events_stream at 16177 (RFC said 16142 = a gateway-sse comment);
replay reads after_event_id/after_seq QUERY PARAMS (RFC said Last-Event-ID header
— that's the *proposed* new-endpoint contract, not current). Corrected all six
routes.py anchors + the replay-mechanism description, verified each against master.

The accompanying test asserted the WRONG anchors and passed (codifying the error).
Rewrote test_rfc_cites_current_global_endpoint_source to parse every routes.py
anchor from the RFC and verify it lands on the real /api/sessions/events route +
_handle_session_events_stream definition — so it now catches drift instead of
locking in a stale number.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 06:37:18 +00:00
nesquena-hermes 174dec88eb Merge commit 'dcaf5a69aa84' into release/stage-p1 2026-07-04 06:25:00 +00:00
nesquena-hermes 947fa2b92d release #5516: fix stale test docstring (#composerWrap not textarea) 2026-07-04 06:15:42 +00:00
nesquena-hermes c77f4cf014 release #5516: composer-grow re-pin — hot-path optimization + CHANGELOG
Addresses Greptile P2 nit: autoResize() now only calls the re-pin when the
composer actually grew (offsetHeight before/after compare), skipping the DOM
read on a no-height-change keystroke. The #composerWrap ResizeObserver remains
the safety net for growth paths that bypass autoResize. Test tightened to assert
the grow-guard. CHANGELOG entry added (self-built, release owns changelog).
2026-07-04 06:06:05 +00:00
nesquena-hermes 042d7f3ec7 Merge commit '7e7e71f6953f' into release/stage-5516 2026-07-04 05:59:19 +00:00
nesquena-hermesandrodboev 6c88c0360c harden #5504: dedup audit tombstone item when .bak + state.db row both survive
Codex v4 SILENT finding: my audit Finding-2 fix made the orphan-.bak branch emit
a state_db_deleted_webui_tombstone item, but the state_db_missing_rows loop still
emitted the same sid/kind again when both a surviving .json.bak AND a state.db row
exist for one deleted session — double-counting unsafe_to_repair, violating the
no-double-classification invariant. Fix: track _bak_tombstoned_ids from the .bak
branch and skip them in the state.db loop. Regression test
test_audit_no_double_count_when_bak_and_state_db_row_both_survive (verified fails
without the dedup).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 05:38:49 +00:00
nesquena-hermesandrodboev 63d0c00f8d harden #5504 on rodboev's latest head: complete the tombstone wiring + CHANGELOG
@rodboev independently pushed fixes for messaging-tombstone (matches ours) and an
audit orphan-.bak skip — but the audit skip used _marks_deleted_webui_session
(index heuristic), which re-introduces the crash-suppression bug, and the other 4
resurrection/recovery paths were still open. Rebuilt the stage on rodboev's new
head (fc21c3f3, attribution preserved) and applied the complete superset:

- routes.py claim path: tombstone 404 gated on _state_db_session_source in
  ('','webui','fork') so foreign-source rows always materialize (finding B)
- models.py sidebar projection: skip tombstoned source='webui' rows w/ no live
  sidecar so a deleted session can't resurface in /api/sessions (finding C)
- session_recovery.py orphan-.bak startup: skip restore when durable tombstone
  present, durable-only (finding D)
- session_recovery.py audit orphan-.bak: durable-only classify as
  deleted_session_skipped (corrects rodboev's index-heuristic version)
- session_recovery.py _read_state_db_missing_sidecar_rows: durable-tombstone-only
  so a genuine crash (index intact, no durable tombstone) still repairs (finding F)

Regression tests (all verified fail-without-fix): sidebar-projection, orphan-.bak
startup skip, messaging-not-tombstoned, index-only-crash-repairable. Both advisors
(Codex SAFE TO SHIP + Opus ship-with-fixes) cleared the prior stage; this rebuild
carries the same fixes onto the contributor's latest work.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 05:32:51 +00:00
nesquena-hermes 06217c93b8 Merge commit 'fc21c3f38e56' into release/stage-5504 2026-07-04 05:25:04 +00:00
nesquena-hermes 4f8bd1ef38 docs(changelog): #5487 stray-pointerup session-switch guard + #5458 IPv4-first gallery 2026-07-04 03:54:11 +00:00
nesquena-hermes fe30f1ff00 Merge PR #5458 from rumotoshino: prefer IPv4 for extension gallery downloads 2026-07-04 03:39:19 +00:00
nesquena-hermes fcd3de7d8f Merge PR #5487 from webtecnica: prevent unintended session switch on stray pointerup (#5462) 2026-07-04 03:39:18 +00:00
nesquena-hermes e874fc4149 docs(changelog): #5460 replace cross-profile empty sessions on profile switch 2026-07-04 03:33:06 +00:00
nesquena-hermes dbf3c9d3c0 Merge PR #5460 from ruizanthony: replace cross-profile empty sessions on profile switch 2026-07-04 03:05:45 +00:00
nesquena-hermes 8b6ababf8c docs(changelog): #5496 refresh dictation recognition.lang after locale change (#5483) 2026-07-04 02:42:56 +00:00
nesquena-hermes 091103cf6d Merge PR #5496 from webtecnica: refresh recognition.lang before dictation start (#5483) 2026-07-04 02:41:01 +00:00
nesquena-hermes 378548b566 docs(changelog): #5480 stale WebUI client recovery banner (#5428) 2026-07-04 01:28:02 +00:00
nesquena-hermes 568d5c4b0e gate-fix #5480: suppress 'unknown' server version (case-insensitive) to prevent false banner
Codex gate: _normalizeWebUIVersion suppressed empty/__WEBUI_VERSION__/'not detected'
but NOT 'unknown' — api/updates.py emits WEBUI_VERSION='unknown' on git-describe
failure (Docker/CI images), so a real client version would falsely fire the
stale-client banner against server='unknown'. Suppress 'unknown' (+ case-insensitive
for all sentinels). + regression test asserting unknown/UNKNOWN/Unknown don't fire.
2026-07-04 01:15:26 +00:00
nesquena-hermes b04f0387c4 Merge PR #5480 from rodboev: surface stale WebUI client recovery (#5428) 2026-07-04 01:09:14 +00:00
nesquena-hermes 8b1109a079 docs(changelog): #5452 fit fullscreen Mermaid to lightbox (#5413) 2026-07-04 00:17:07 +00:00
nesquena-hermes 0eb80f6197 Merge PR #5452 from rodboev: fit fullscreen Mermaid diagrams to the lightbox (#5413) 2026-07-04 00:07:04 +00:00
nesquena-hermes 8d9d37c118 Merge remote-tracking branch 'origin/master' into release/stage-5467
# Conflicts:
#	CHANGELOG.md
2026-07-03 23:44:49 +00:00
nesquena-hermes ed508ff6ef Merge remote-tracking branch 'origin/master' into release/stage-5467
# Conflicts:
#	CHANGELOG.md
2026-07-03 23:39:31 +00:00
nesquena-hermesandwebtecnica 329e911327 fix: prevent unintended session switch on stray pointerup (#5462)
Clean rebase of webtecnica's #5487 (rebase-first).

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-07-03 23:34:37 +00:00
nesquena-hermes f66944d006 docs(changelog): #5467 sidebar render-skip on idle polls (#5455) 2026-07-03 23:23:20 +00:00
nesquena-hermes a53c183dd5 fix(test): widen _applySessionListPayload slice to reach _mustForceRender assertion 2026-07-03 23:05:03 +00:00
nesquena-hermes b438976507 gate-fix #5467 r2: force render when recovering from skeleton/error-banner DOM
Codex re-gate found 2 more false-skips (DOM state OUTSIDE the signature path):
- CORE: profile-switch skeleton stays visible if the new payload signature
  matches the last one (empty/same-shaped profile) — skip fires over the skeleton.
- SILENT: stale 'Could not load conversations' banner persists when a transient
  fetch failure heals with identical rows.
Fix: capture _hadSessionListSkeleton + _hadSessionListLoadError BEFORE clearing
them, and disallow the identical-signature skip when either was set. + regression
test. (messages.js:1462 finding is a base-comparison artifact for #5472/#5479/#5484,
not in #5467's diff.)
2026-07-03 23:04:41 +00:00
nesquena-hermes 3999418b0e gate-fix #5467: serialize full rows in render-skip signature (close false-skips)
Codex gate found the render-skip signature omitted fields the sidebar renders:
- CORE: pending/running (active_stream_id/has_pending_user_message/pending_started_at)
- SILENT: attention dots
- SILENT: source/read-only/worktree/lineage/child/model/profile + _sidebarReferenceSessions
Any omitted field = a false-skip = stale sidebar.

Root-cause fix (most reliable): serialize the FULL applied row objects +
_sidebarReferenceSessions instead of a hand-picked allowlist — inherently
complete, can't rot when a new rendered field is added. Rewrote the
'ignores non-display fields' test into positive full-serialization + no-strip
regression assertions.
2026-07-03 22:58:21 +00:00
nesquena-hermes 10e5e941d3 Merge PR #5467 from ai-ag2026: skip full session-list rebuild when payload unchanged (#5455) 2026-07-03 22:47:40 +00:00
nesquena-hermes 8db6ec1536 Merge remote-tracking branch 'origin/master' into release/stage-5469 2026-07-03 22:32:54 +00:00
nesquena-hermes f18b384a2f docs(changelog): #5469 desktop scroll-anchor recover via sessionIdx 2026-07-03 22:32:53 +00:00
nesquena-hermes 3053b6522b docs(changelog): #5438 preset cron schedule builder (#5427) 2026-07-03 22:25:50 +00:00
nesquena-hermes e81711d3a9 Merge PR #5469 from allenliang2022: recover viewport anchor via sessionIdx (desktop scroll jump-back) 2026-07-03 22:18:17 +00:00
nesquena-hermes 5cac66b4e5 Merge PR #5438 from rodboev: add preset cron schedule builder (#5427) 2026-07-03 22:16:37 +00:00
nesquena-hermes 8269de961e fix(test): stub _cancelThrottledSnapshotTimer + _clearAnchorProseIncrementalNode in #5224 node harness
The #5466 fix added these two cleanup calls to _restoreSettledSession; the
test_issue5224 node harness extracts+runs that function with every helper
stubbed on globalThis, but hadn't stubbed the two new ones — so the calls threw,
the function's catch returned 'error' instead of 'restored', failing 8 CI shards.
Add the stubs. (Local full-suite missed it on first run; CI sharding surfaced it.)
2026-07-03 22:04:54 +00:00
nesquena-hermes f3bf04c69a docs(changelog): #5466 incremental live streaming render (O(n^2)->O(n)) (#5455) 2026-07-03 21:44:10 +00:00
nesquena-hermes eaf7d1e9ab gate-fix #5466: stamp anchor attrs on incremental node + teardown on all terminal paths
Codex gate findings (2 SILENT, both fixed):
- ui.js: incremental prose node now flows through the shared row-decoration
  block (data-anchor-scene-row/-row-id/-row-role/-source-event-type) instead of
  returning early, so live incremental rows keep the identity attrs the scene
  reconciler matches on.
- messages.js: _cancelThrottledSnapshotTimer() + _clearAnchorProseIncrementalNode()
  now run on apperror, cancel, _handleStreamError, and _restoreSettledSession
  terminal paths (previously only fallback/done/stream_end) — closes the
  snapshot-timer/anchor-cache/window-global teardown gap.
- test: extend test_messages_js_stream_perf_cleanup_lifecycle to pin teardown on
  all four additional terminal paths.

CORE equivalence (Codex flagged smd != renderMd): reconciled — Opus's 1498-frame
harness against real smd.min.js proves incremental-smd == whole-smd byte-identical;
live-smd-vs-renderMd difference is the already-shipped fidelity model (main live
body streams smd, settles renderMd), and the incremental path is gated on !settled
so settled DOM still comes from renderMd. Not a regression.
2026-07-03 21:24:08 +00:00
nesquena-hermes 819f1a1e7b Merge PR #5466 from ai-ag2026: reduce O(n^2) streaming render to O(n) (#5455) 2026-07-03 21:04:16 +00:00
nesquena-hermesandruizanthony 5e5b44e733 fix(webui): replace cross-profile empty sessions on profile switch + open all-profile rows under owning profile
Clean rebase of ruizanthony's #5460 (rebase-first).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-03 20:49:51 +00:00
nesquena-hermes 8fe2d63ec0 Merge remote-tracking branch 'origin/master' into release/stage-p1c
# Conflicts:
#	CHANGELOG.md
2026-07-03 20:44:31 +00:00
nesquena-hermes bc0a3b299a docs(changelog): note hidden-tab attention-chime timing delta (Opus advisor) 2026-07-03 20:43:42 +00:00
nesquena-hermes 2fd2d0951f docs(changelog): Batch C — #5464 read-only session-listing projection + #5465 pause hidden-tab polling (#5455) 2026-07-03 20:32:58 +00:00
nesquena-hermesandrumotoshino 5ea8b09b83 fix: prefer IPv4 for extension gallery downloads to prevent timeout on broken IPv6
Clean rebase of rumotoshino's #5458 (rebase-first).

Co-authored-by: rumotoshino <rumotoshino@users.noreply.github.com>
2026-07-03 20:26:18 +00:00
nesquena-hermes 6edcf14bbc Merge PR #5465 from ai-ag2026: pause background session polling while tab hidden (#5455) 2026-07-03 20:16:35 +00:00
nesquena-hermes 797eb4a1b9 Merge PR #5464 from ai-ag2026: open session-listing projection read-only (#5455) 2026-07-03 20:16:35 +00:00