* 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:
3 files changed
+315
-76
No files matched your search
@@ -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
@@ -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)"
|
||||
)
|
||||
Reference in new issue
Block a user