Release v0.51.561 — context-window indicator stays correct after model switch (#4618) (#4628)

* stage #4618 (allenliang2022): broaden streaming stale-compressor guard to any model-window mismatch + #4618 regression tests + CHANGELOG

Broadens #3256's default-only live-usage guard: the streaming SSE snapshot now
always resolves the real per-model window via the same helper GET /api/session
hydration uses (_context_length_lookup_inputs_for_model + get_model_context_length)
and corrects whenever it differs from the compressor's cached value, with a
TypeError fallback to the legacy 2-arg form. Fixes 'refresh shows 1M, send reverts
to stale 168k + early auto-compress' on model-switched sessions. Per-stream cache
preserved (one lookup/stream). Code byte-identical to PR head 3beb18e92d73.

Adds 4 source-structure regression tests (RED-proven on master).

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

* fix #4618 gate findings: profile-scoped config (Codex cross-profile) + #4248 256k acceptance gate (Opus downward-clobber)

Codex SHIP-WITH-FIXES: live-snapshot used ambient get_config() which in the
detached streaming worker resolves the process-global/default profile (#3294) ->
for a non-default profile pinning a different per-model context_length it would
surface the WRONG profile's window. Now resolves via get_config_for_profile_home
on the session's own profile home (mirrors the worker's _cfg resolution).

Opus SHIP-WITH-FIXES: broadened guard aligned resolution w/ hydration but not its
#4248 acceptance gate -> a transient low-confidence 256k metadata probe could
clobber a LARGER cached window mid-stream. Now reuses the exact hydration helper
_should_accept_session_context_length_refresh on both modern + legacy paths.

+ regression tests for both. Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>

* fix #4618 Codex re-gate findings: broaden save + SSE-done stale-compressor guards too

Codex re-gate found the broadened live-snapshot guard fixed metering but the two
SIBLING paths still used the old default-only exact-cap test:
  - api/streaming.py final session-save: persisted stale other-model window (168k)
    to s.context_length -> wrong window on reload.
  - api/streaming.py terminal  SSE: emitted stale window -> indicator REVERTS
    on stream end (messages.js overwrites S.lastUsage) = the exact 'send reverts to
    168k' symptom.
Both now resolve the real per-model window via the same hydration helper and honor
the #4248 acceptance gate (no 256k downward-clobber), with legacy 2-arg fallback.
This is the root-cause completion across all 3 paths (live/save/SSE-done).

+ 2 regression tests. Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>

* docs(#4618): note model_changed-omission rationale (Opus) + broaden CHANGELOG to all-3-paths

* Release v0.51.561 — Release TT (context-window indicator stays correct after model switch; #4618)

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-06-21 11:36:26 -07:00
1 parent 2621df98eb
commit ee0b476f62
3 files changed
+315 -76

No files matched your search

+3
View File
@@ -3,8 +3,11 @@
## [Unreleased]
## [v0.51.561] — 2026-06-21 — Release TT (context-window indicator stays correct after model switch)
### Fixed
- **The live context-window indicator no longer reverts to a stale value mid-conversation after a model switch.** On a session whose model was switched in place (e.g. to `claude-opus-4.8` with a 1M window), the streaming context snapshot could surface a *previous* model's cached context window (e.g. `claude-opus-4.5`'s 168k) — so a refresh showed the correct 1M but sending a message reverted the indicator to 168k and tripped auto-compress early. The correction now applies consistently across all three paths that surface the window (the live metering snapshot, the final session save, and the terminal `done` SSE payload): each resolves the real per-model window through the same helper `GET /api/session` hydration uses (honoring nested per-model config overrides and custom providers) and corrects whenever it differs from the compressor's cached value, while never letting a low-confidence 256k metadata fallback clobber a larger cached window. The lookup still runs at most once per stream (no per-tick cost). Thanks @allenliang2022. (#4618)
- **`bootstrap.py` now finds the Hermes Agent when it was installed as root (FHS layout) or behind the current shell-wrapper `hermes` launcher (#root-install).** A fresh root install on Linux places the agent at `/usr/local/lib/hermes-agent` (CLI linked into `/usr/local/bin`), and the modern `hermes` launcher is a `#!/usr/bin/env bash` wrapper that `exec`s the venv entrypoint. The old discovery only checked `~/.hermes/hermes-agent`-style paths and parsed the launcher's shebang as a Python interpreter, so on a root VM it found nothing, built a WebUI-only `.venv`, and aborted with "Python environment cannot import both WebUI dependencies and Hermes Agent." Discovery now also probes `/usr/local/lib/hermes-agent` and follows the launcher's `exec` target (any absolute path inside the script) up to `run_agent.py`, so `python3 bootstrap.py` works out of the box for root/Proxmox/container installs with no `HERMES_WEBUI_PYTHON` override needed.
## [v0.51.560] — 2026-06-21 — Release TS (recovered run-journal output reaches the model)
+190 -76
View File
@@ -5721,50 +5721,126 @@ def _run_agent_streaming(
_cc = getattr(_agent, 'context_compressor', None)
if _cc:
_cc_cl_u = getattr(_cc, 'context_length', 0) or 0
# Default-only guard (#3256): the agent-side compressor is
# built in agent_init with the global model.context_length
# applied unconditionally — for non-default models that
# value is the stale global cap (e.g. 232K). Drop it here
# so the live usage payload doesn't surface the wrong cap.
# PERF: resolve the real per-model cap at most once per
# stream (cached in _real_ctx_cache). This snapshot runs on
# every metering tick; doing the config read + metadata
# lookup per tick froze non-default-model streams.
# Stale-compressor self-heal (#3256, broadened): the
# agent-side compressor caches a context_length from the
# model it was *built/last-updated* with. After an in-place
# model switch (or when agent_init seeded it with the global
# model.context_length cap), that cached value can be the
# WRONG model's window — e.g. a session on claude-opus-4.8
# (1M / 936k prompt on Copilot) whose compressor still holds
# claude-opus-4.5's 168k. The original guard only corrected
# the narrow case where the cached value equalled the config
# cap exactly; a leftover *other-model* value (168k) slipped
# straight through to the live usage payload. Broaden it:
# ALWAYS resolve the real per-model window for the agent's
# CURRENT model and, when that differs from the cached value,
# surface the real one. Frontend hydration (GET /api/session)
# already does this; this aligns the streaming path with it
# so "refresh shows 1M, send-a-message drops to 168k" can't
# happen.
# PERF: resolve at most once per stream (cached in
# _real_ctx_cache). This snapshot runs on every metering
# tick; doing the config read + metadata lookup per tick
# froze non-default-model streams.
if _real_ctx_cache[0] is None:
_resolved_real = 0 # 0 = guard not applicable / failed
_resolved_real = 0 # 0 = no correction / lookup failed
try:
from api.config import get_config as _gc_u
_cfg_u = _gc_u()
_mcfg_u = _cfg_u.get('model', {}) if isinstance(_cfg_u, dict) else {}
if isinstance(_mcfg_u, dict):
_def_u = str(_mcfg_u.get('default') or '').strip()
_raw_u = _mcfg_u.get('context_length')
_sm_u = str(getattr(_agent, 'model', '') or '').strip()
_prov_u = str(getattr(_agent, 'provider', '') or '').strip()
_base_u = str(getattr(_agent, 'base_url', '') or '').strip()
_key_u = getattr(_agent, 'api_key', '') or ''
if _sm_u:
# Resolve the real window through the SAME helper
# hydration uses (routes._context_length_lookup_inputs_for_model
# + get_model_context_length). This honors the
# nested per-model config override
# (model.<provider>.models.<model>.context_length,
# e.g. claude-opus-4.8 -> 1,000,000) and custom-
# provider keys, so the streaming/SSE path and the
# GET /api/session path land on the IDENTICAL value.
# Reusing the helper (instead of hand-reading the
# flat top-level model.context_length, which is
# None here) is what prevents a new mismatch like
# "refresh shows 1M, send-a-message shows 936k".
try:
_cl_u = int(_raw_u) if _raw_u is not None else 0
except (TypeError, ValueError):
_cl_u = 0
_sm_u = str(getattr(_agent, 'model', '') or '').strip()
from api.routes import _model_matches_configured_default as _mmcd_u
if (
_cl_u > 0
and _cc_cl_u == _cl_u
and _def_u
and _sm_u
and not _mmcd_u(_sm_u, _def_u, getattr(_agent, 'provider', '') or '')
):
# Recompute from real per-model metadata.
from api.routes import (
_context_length_lookup_inputs_for_model as _cli_u,
_should_accept_session_context_length_refresh as _accept_u,
)
from agent.model_metadata import get_model_context_length as _g_u
# Resolve the SESSION's own profile config, not
# the ambient one. This worker is a detached
# thread that does NOT inherit the per-request
# thread-local profile context, so a bare
# get_config() resolves the process-global
# (default) profile (#3294) — for a non-default
# profile that pins a different per-model
# context_length, that would surface the WRONG
# profile's window in the live payload. Read the
# session's profile home explicitly, mirroring
# the worker's own _cfg resolution below.
try:
from agent.model_metadata import get_model_context_length as _g_u
_real_u = _g_u(
_sm_u,
getattr(_agent, 'base_url', '') or '',
config_context_length=None,
provider=getattr(_agent, 'provider', '') or '',
) or 0
if _real_u:
from api.config import get_config_for_profile_home as _gch_u
from api.profiles import get_hermes_home_for_profile as _ghp_u
_ph_u = _ghp_u(getattr(_session_obj, 'profile', None))
_cfg_u = _gch_u(_ph_u)
except Exception:
from api.config import get_config as _gc_u
_cfg_u = _gc_u()
_lk_u = _cli_u(
_sm_u,
_prov_u,
base_url=_base_u,
api_key=_key_u,
cfg=_cfg_u if isinstance(_cfg_u, dict) else {},
)
_real_u = _g_u(
_sm_u,
_lk_u.base_url,
api_key=_lk_u.api_key,
config_context_length=_lk_u.config_context_length,
provider=_lk_u.provider or _prov_u or '',
custom_providers=_lk_u.custom_providers,
) or 0
# Only treat it as a correction when the real
# window is valid AND disagrees with the
# compressor's cached value. Equal => nothing
# to fix, leave the fast path untouched.
# #4248: never let a low-confidence 256k metadata
# fallback clobber a LARGER cached window — that
# would reintroduce the very "drops to a smaller
# window mid-stream" regression this guard fixes.
# Reuse the exact acceptance gate hydration uses.
# NOTE: we deliberately omit model_changed (=False
# default) here, unlike hydration. The streaming
# path can't cheaply know if the model changed
# since the compressor was seeded, so we err
# toward the LARGER window (auto-compress fires
# late, not early — the safe direction), and the
# next GET /api/session hydration self-heals any
# genuine downward 256k case via model_changed.
if (
_real_u and _real_u != _cc_cl_u
and _accept_u(_cc_cl_u, _real_u)
):
_resolved_real = _real_u
except TypeError:
# Older hermes-agent: legacy 2-arg form.
try:
from api.routes import (
_should_accept_session_context_length_refresh as _accept2_u,
)
from agent.model_metadata import get_model_context_length as _g2_u
_real_u = _g2_u(_sm_u, _base_u) or 0
if (
_real_u and _real_u != _cc_cl_u
and _accept2_u(_cc_cl_u, _real_u)
):
_resolved_real = _real_u
except Exception:
pass
except Exception:
pass
except Exception:
_resolved_real = 0
_real_ctx_cache[0] = _resolved_real
@@ -7910,25 +7986,48 @@ def _run_agent_streaming(
# cap (e.g. 232K) that would clobber the real 1M metadata
# on every stream end. In that case skip the compressor
# value and let the fallback resolver below recompute.
# #4618: broaden the stale-compressor guard the same way the
# live-usage snapshot does. The OLD test only skipped the
# compressor value when it equalled the config cap EXACTLY
# (a non-default model carrying the global cap). But a
# compressor can hold a DIFFERENT model's window after an
# in-place model switch (e.g. opus-4.5's 168k lingering on an
# opus-4.8 1M session) — that value != the config cap, so the
# old guard let it persist to s.context_length and the SSE
# payload, snapping the indicator back to 168k at turn-end.
# Resolve the real per-model window via the SAME helper the
# live path + hydration use and skip the compressor value
# whenever the real window differs, honoring the #4248
# acceptance gate (never let a low-confidence 256k fallback
# clobber a larger cached window).
_skip_cc_cl = False
try:
_model_cfg_cc = _cfg.get('model', {}) if isinstance(_cfg, dict) else {}
if isinstance(_model_cfg_cc, dict):
_cfg_default_cc = str(_model_cfg_cc.get('default') or '').strip()
_raw_cfg_cl_cc = _model_cfg_cc.get('context_length')
from api.routes import (
_context_length_lookup_inputs_for_model as _cli_cc,
_should_accept_session_context_length_refresh as _accept_cc,
)
from agent.model_metadata import get_model_context_length as _g_cc
_sess_model_cc = str(getattr(agent, 'model', resolved_model or '') or '').strip()
if _sess_model_cc and _cc_cl > 0:
_lk_cc = _cli_cc(
_sess_model_cc,
resolved_provider or '',
base_url=getattr(agent, 'base_url', '') or resolved_base_url or '',
api_key=getattr(agent, 'api_key', '') or resolved_api_key or '',
cfg=_cfg if isinstance(_cfg, dict) else {},
)
try:
_cfg_cl_cc = int(_raw_cfg_cl_cc) if _raw_cfg_cl_cc is not None else 0
except (TypeError, ValueError):
_cfg_cl_cc = 0
_sess_model_cc = str(getattr(agent, 'model', resolved_model or '') or '').strip()
from api.routes import _model_matches_configured_default as _mmcd_cc
if (
_cfg_cl_cc > 0
and _cc_cl == _cfg_cl_cc
and _cfg_default_cc
and _sess_model_cc
and not _mmcd_cc(_sess_model_cc, _cfg_default_cc, resolved_provider or '')
):
_real_cc = _g_cc(
_sess_model_cc,
_lk_cc.base_url,
api_key=_lk_cc.api_key,
config_context_length=_lk_cc.config_context_length,
provider=_lk_cc.provider or resolved_provider or '',
custom_providers=_lk_cc.custom_providers,
) or 0
except TypeError:
_real_cc = _g_cc(_sess_model_cc, _lk_cc.base_url) or 0
if _real_cc and _real_cc != _cc_cl and _accept_cc(_cc_cl, _real_cc):
_skip_cc_cl = True
except Exception:
pass
@@ -8176,31 +8275,46 @@ def _run_agent_streaming(
_orig_cc_cl_sse = _cc_cl_sse
_orig_cc_thresh_sse = getattr(_cc, 'threshold_tokens', 0) or 0
_dropped_stale_cap_sse = False
# Default-only guard (#3256): the agent-side context_compressor
# is constructed in agent_init with the global model.context_length
# applied unconditionally, so for non-default models its
# context_length is the stale global cap (e.g. 232K) — surfacing
# it via SSE makes the indicator show the wrong window even
# after the session was correctly resized. Drop the compressor
# value in that case and let the fallback resolver below recompute.
# Default-only guard (#3256), broadened (#4618): the agent-side
# context_compressor caches a context_length from the model it
# was built/last-updated with. For a non-default model it may be
# the stale global cap (e.g. 232K); after an in-place model switch
# it may be a DIFFERENT model's window (e.g. opus-4.5's 168k on an
# opus-4.8 1M session). Either way, surfacing it via the terminal
# `done` SSE makes the indicator REVERT to the wrong window on
# stream end (messages.js overwrites S.lastUsage with this payload)
# — the exact "send a message reverts to 168k" symptom. Resolve
# the real per-model window via the SAME helper the live path +
# hydration use; drop the compressor value whenever the real
# window differs, honoring the #4248 acceptance gate (never let a
# low-confidence 256k fallback clobber a larger cached window).
try:
_model_cfg_sse = _cfg.get('model', {}) if isinstance(_cfg, dict) else {}
if isinstance(_model_cfg_sse, dict):
_cfg_default_sse = str(_model_cfg_sse.get('default') or '').strip()
_raw_cfg_cl_sse = _model_cfg_sse.get('context_length')
from api.routes import (
_context_length_lookup_inputs_for_model as _cli_sse,
_should_accept_session_context_length_refresh as _accept_sse,
)
from agent.model_metadata import get_model_context_length as _g_sse
_sess_model_sse = str(getattr(agent, 'model', resolved_model or '') or '').strip()
if _sess_model_sse and _cc_cl_sse > 0:
_lk_sse = _cli_sse(
_sess_model_sse,
resolved_provider or '',
base_url=getattr(agent, 'base_url', '') or resolved_base_url or '',
api_key=getattr(agent, 'api_key', '') or resolved_api_key or '',
cfg=_cfg if isinstance(_cfg, dict) else {},
)
try:
_cfg_cl_sse = int(_raw_cfg_cl_sse) if _raw_cfg_cl_sse is not None else 0
except (TypeError, ValueError):
_cfg_cl_sse = 0
_sess_model_sse = str(getattr(agent, 'model', resolved_model or '') or '').strip()
from api.routes import _model_matches_configured_default as _mmcd_sse
if (
_cfg_cl_sse > 0
and _cc_cl_sse == _cfg_cl_sse
and _cfg_default_sse
and _sess_model_sse
and not _mmcd_sse(_sess_model_sse, _cfg_default_sse, resolved_provider or '')
):
_real_sse = _g_sse(
_sess_model_sse,
_lk_sse.base_url,
api_key=_lk_sse.api_key,
config_context_length=_lk_sse.config_context_length,
provider=_lk_sse.provider or resolved_provider or '',
custom_providers=_lk_sse.custom_providers,
) or 0
except TypeError:
_real_sse = _g_sse(_sess_model_sse, _lk_sse.base_url) or 0
if _real_sse and _real_sse != _cc_cl_sse and _accept_sse(_cc_cl_sse, _real_sse):
_cc_cl_sse = 0
_dropped_stale_cap_sse = True
except Exception:
@@ -509,3 +509,125 @@ def test_sse_done_payload_rescales_threshold_when_cap_dropped():
assert "usage['threshold_tokens'] = int(_orig_cc_thresh_sse * _fb_cl / _orig_cc_cl_sse)" in _STREAMING_SRC, (
"SSE done payload must rescale threshold_tokens to the resolved window (#3263 MUST-FIX 3)"
)
def test_live_snapshot_guard_broadened_to_any_model_window_mismatch():
"""#4618: the live-usage snapshot stale-compressor guard must correct the
cached window whenever the resolved real per-model window DIFFERS from the
compressor's cached value — not only when it exactly equals the global cap.
A leftover *other-model* cached value (e.g. opus-4.5's 168k lingering on an
opus-4.8 1M session after an in-place model switch) is != the config cap, so
the old `_cc_cl_u == _cl_u` condition let it slip through to the live payload
('refresh shows 1M, send-a-message reverts to 168k'). Pin the broadened
condition so it cannot silently revert to the narrow default-only form."""
assert "_real_u and _real_u != _cc_cl_u" in _STREAMING_SRC, (
"live-snapshot guard must correct on ANY real-window != cached-window "
"mismatch (#4618), not only the default-model exact-cap case"
)
# The narrow default-only signals must be GONE from the live-snapshot block.
assert "_model_matches_configured_default as _mmcd_u" not in _STREAMING_SRC, (
"the old default-only guard (_mmcd_u) must be removed from the "
"live-snapshot path (#4618 broadened it)"
)
def test_live_snapshot_reuses_hydration_context_length_helper():
"""#4618: the broadened guard must resolve the real window through the SAME
helper GET /api/session hydration uses (_context_length_lookup_inputs_for_model
+ get_model_context_length) so the streaming/SSE path and the hydration path
land on an IDENTICAL value (honors nested per-model config overrides + custom
providers). Pin the helper reuse against a hand-rolled flat-cap regression."""
assert "_context_length_lookup_inputs_for_model as _cli_u" in _STREAMING_SRC, (
"live-snapshot guard must reuse the hydration lookup-inputs helper (#4618)"
)
assert "from agent.model_metadata import get_model_context_length as _g_u" in _STREAMING_SRC
def test_live_snapshot_guard_has_legacy_two_arg_fallback():
"""#4618: a TypeError from the modern 6-arg get_model_context_length (older
hermes-agent builds) must fall back to the legacy 2-arg form rather than
raising, and the fallback must apply the SAME mismatch condition."""
assert "from agent.model_metadata import get_model_context_length as _g2_u" in _STREAMING_SRC
assert "_real_u = _g2_u(_sm_u, _base_u) or 0" in _STREAMING_SRC, (
"live-snapshot guard must keep a legacy 2-arg get_model_context_length "
"fallback for older hermes-agent builds (#4618)"
)
def test_live_snapshot_resolves_session_profile_config_not_ambient():
"""#4618 gate fix (Codex): the streaming worker is a detached thread that
does NOT inherit the per-request thread-local profile context, so a bare
get_config() resolves the process-global (default) profile (#3294). For a
non-default profile pinning a different per-model context_length, that would
surface the WRONG profile's window in the live payload. The guard must
resolve the SESSION's own profile config via get_config_for_profile_home."""
assert "from api.config import get_config_for_profile_home as _gch_u" in _STREAMING_SRC, (
"live-snapshot guard must read the session's profile config, not ambient "
"get_config() (#3294 cross-profile leak class)"
)
assert "get_hermes_home_for_profile as _ghp_u" in _STREAMING_SRC
assert "_ghp_u(getattr(_session_obj, 'profile', None))" in _STREAMING_SRC, (
"profile home must derive from the live session object's profile"
)
def test_live_snapshot_guard_honors_256k_clobber_acceptance_gate():
"""#4618 gate fix (Opus): the broadened guard aligned resolution with
hydration but must ALSO honor hydration's #4248 acceptance gate — a transient
low-confidence 256k metadata fallback must NOT clobber a LARGER cached window
mid-stream (which would reintroduce the 'drops to a smaller window' regression
this guard exists to fix). Reuse the exact hydration acceptance helper on both
the modern and legacy paths."""
assert "_should_accept_session_context_length_refresh as _accept_u" in _STREAMING_SRC, (
"live-snapshot guard must reuse the #4248 acceptance gate "
"(_should_accept_session_context_length_refresh)"
)
assert "and _accept_u(_cc_cl_u, _real_u)" in _STREAMING_SRC, (
"the correction must be gated by the 256k-clobber acceptance check (#4248)"
)
# Legacy 2-arg fallback must apply the same gate.
assert "_should_accept_session_context_length_refresh as _accept2_u" in _STREAMING_SRC
assert "and _accept2_u(_cc_cl_u, _real_u)" in _STREAMING_SRC, (
"the legacy 2-arg fallback must also honor the 256k acceptance gate"
)
def test_session_save_path_broadened_to_any_model_window_mismatch():
"""#4618 (Codex re-gate finding 1): the FINAL session-save stale-compressor
guard must also broaden beyond the default-only exact-cap test — otherwise a
leftover other-model window (168k) is PERSISTED to s.context_length and a
reload shows the wrong window. It must resolve the real window via the same
helper and honor the #4248 acceptance gate before skipping the compressor
value."""
assert "_context_length_lookup_inputs_for_model as _cli_cc" in _STREAMING_SRC, (
"save path must resolve the real per-model window via the hydration helper (#4618)"
)
assert "_should_accept_session_context_length_refresh as _accept_cc" in _STREAMING_SRC
assert "if _real_cc and _real_cc != _cc_cl and _accept_cc(_cc_cl, _real_cc):" in _STREAMING_SRC, (
"save path must skip the compressor value on ANY accepted real-window "
"mismatch, not only the default-model exact-cap case (#4618)"
)
def test_sse_done_path_broadened_to_any_model_window_mismatch():
"""#4618 (Codex re-gate finding 2): the terminal `done` SSE usage path must
also broaden — otherwise the indicator REVERTS to the stale window on stream
end (messages.js overwrites S.lastUsage with the done payload). It must
resolve the real window via the same helper and honor the #4248 gate before
dropping the compressor value."""
assert "_context_length_lookup_inputs_for_model as _cli_sse" in _STREAMING_SRC, (
"SSE done path must resolve the real per-model window via the hydration helper (#4618)"
)
assert "_should_accept_session_context_length_refresh as _accept_sse" in _STREAMING_SRC
assert "if _real_sse and _real_sse != _cc_cl_sse and _accept_sse(_cc_cl_sse, _real_sse):" in _STREAMING_SRC, (
"SSE done path must drop the compressor value on ANY accepted real-window "
"mismatch so the indicator can't snap back to a stale window at turn-end (#4618)"
)
# The old narrow default-only matcher must be gone from BOTH sibling paths.
assert "_model_matches_configured_default as _mmcd_sse" not in _STREAMING_SRC, (
"the old default-only SSE matcher (_mmcd_sse) must be removed (#4618)"
)
assert "_model_matches_configured_default as _mmcd_cc" not in _STREAMING_SRC, (
"the old default-only save matcher (_mmcd_cc) must be removed (#4618)"
)