Merge remote-tracking branch 'origin/master' into paperclip/pr-6222
This commit is contained in:
commit
486d2038f2
4 files changed
+153
-17
No files matched your search
@@ -9,6 +9,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The "Compressing context" card no longer flashes on turns that aren't actually compressing.** The card was driven by a broad status match (anything containing `compressing`), so skip/cooldown/defer notices and post-compression summaries (`Compressed X → Y`) could paint a phantom compression card mid-turn. A dedicated `_is_agent_compression_start_status()` now recognizes only the five genuine start-of-compression markers the Hermes agent emits, and the transcript renderer no longer lets an unrecognized `terminal_status` row paint a compression anchor. Real compression turns still show the card unchanged; verified against every current agent compression emitter. Thanks @carlotestor. (#6184)
|
||||
|
||||
- **Transparent Stream no longer shows the start of the final answer twice after a multi-segment turn settles.** In a turn with interim assistant prose interleaved with tool calls, the beginning of the final answer could render twice after the live→settled transition — once as a stale live-token accumulator snapshot, once as the real assistant segment — persisting until reload. The earlier suppression gate (#5758) computed its "after the last tool row" boundary over the combined row list, where settled per-message tool rows re-list tools that ran earlier in the turn, pushing the boundary past the final segment's accumulator so the dedupe never fired. Final-segment eligibility is now judged against the live projection's own chronology (a live-prose row belongs to the final segment only when no *projected* tool row follows it), so the stale prefix is dropped while pre-tool narration and every tool/thinking row are preserved. Thanks @ai-ag2026. (#6189, #5749)
|
||||
|
||||
- **The frontend now reads the `msg_limit` ceiling from `/api/session` metadata instead of hand-mirroring the constant.** Following the ceiling clamp shipped in exp-v0.52.98, the backend advertises its `_MAX_MSG_LIMIT` as `_msg_limit_max` in every `/api/session` response, and the frontend reads it dynamically (`_msgLimitMax`, a module-scope value defaulting to the static fallback), so the two can no longer drift and the mirrored constant's drift-guard test is no longer needed. Older backends that omit the field fall back to the built-in default, so mixed-version deployments are unaffected. Thanks @webtecnica. (#6214, #6177)
|
||||
|
||||
+43
-10
@@ -542,6 +542,48 @@ def _is_fallback_lifecycle_message(kind: str, message: str) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _is_agent_compression_start_status(kind: str, message: str) -> bool:
|
||||
"""Return True only for real Hermes context-compression start notices.
|
||||
|
||||
WebUI bridges matching lifecycle statuses into an SSE ``compressing`` event
|
||||
and paints the live "Compressing context" worklog divider. The previous
|
||||
matcher used broad substrings such as ``'compressing' in message`` and
|
||||
``'preflight compression' in message``, which can false-positive on skip /
|
||||
cooldown / unrelated notices and make brand-new low-token turns look like
|
||||
auto-compression.
|
||||
|
||||
Positive markers below match the agent emitters in hermes-agent
|
||||
(``turn_context`` preflight, ``conversation_loop`` pre-API / 413 / too-large,
|
||||
``conversation_compression`` compaction status). Explicitly reject skip /
|
||||
defer notices so "Skipping preflight compression…" never surfaces as a
|
||||
running compress divider.
|
||||
"""
|
||||
k = str(kind or '').strip().lower()
|
||||
m = str(message or '').strip().lower()
|
||||
if k != 'lifecycle' or not m:
|
||||
return False
|
||||
# Skip / cooldown / defer logs must never look like a live compression start.
|
||||
if (
|
||||
'skipping' in m
|
||||
or 'defer' in m
|
||||
or 'cooldown' in m
|
||||
or 'will not start' in m
|
||||
):
|
||||
return False
|
||||
# Post-compress retry chatter is not a start event.
|
||||
if 'compressed' in m and 'compressing' not in m and 'compression attempt' not in m:
|
||||
return False
|
||||
return (
|
||||
'preflight compression:' in m
|
||||
or 'pre-api compression:' in m
|
||||
or 'compacting context' in m
|
||||
or 'context too large' in m
|
||||
or '— compressing (' in m
|
||||
or '- compressing (' in m
|
||||
or 'compression attempt' in m
|
||||
)
|
||||
|
||||
|
||||
def _prewarm_skill_tool_modules():
|
||||
"""Import tools.skills_tool and tools.skill_manager_tool outside any lock.
|
||||
|
||||
@@ -7148,16 +7190,7 @@ def _run_agent_streaming(
|
||||
and 'http' in _lower
|
||||
):
|
||||
_captured_terminal_error[0] = _message
|
||||
_is_compression_start = (
|
||||
_kind == 'lifecycle'
|
||||
and (
|
||||
'preflight compression' in _lower
|
||||
or 'compressing' in _lower
|
||||
or 'compacting context' in _lower
|
||||
or 'context too large' in _lower
|
||||
)
|
||||
)
|
||||
if _is_compression_start:
|
||||
if _is_agent_compression_start_status(_kind, _message):
|
||||
put('compressing', {
|
||||
'session_id': session_id,
|
||||
'message': 'Compressing context',
|
||||
|
||||
+32
-2
@@ -3936,8 +3936,38 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(role==='prose'||kind==='process_prose') return 'token';
|
||||
if(role==='thinking'||kind==='reasoning') return 'reasoning';
|
||||
if(role==='tool') return row&&row.status==='running'?'tool':'tool_complete';
|
||||
if(role==='terminal'||kind==='terminal_status') return row&&row.status==='running'?'compressing':'done';
|
||||
if(role==='lifecycle'||kind==='lifecycle_status') return 'compressing';
|
||||
// Terminal statuses are done/cancel/error/apperror — never invent a
|
||||
// compression start from a running terminal row (false "Compressing context").
|
||||
if(role==='terminal'||kind==='terminal_status'){
|
||||
const termStatus=String(row&&row.status||'').trim().toLowerCase();
|
||||
if(termStatus==='cancelled'||termStatus==='canceled'||termStatus==='interrupted') return 'cancel';
|
||||
if(termStatus==='error'||termStatus==='failed'||termStatus==='errored') return 'error';
|
||||
if(termStatus==='running') return '';
|
||||
return 'done';
|
||||
}
|
||||
// lifecycle_status is shared by compressing + compressed. Prefer explicit
|
||||
// cues; do not default every lifecycle row to a running compress divider.
|
||||
if(role==='lifecycle'||kind==='lifecycle_status'){
|
||||
const phase=String(row&&(row.phase||row.status)||'').trim().toLowerCase();
|
||||
const text=String(row&&(row.text||row.message||row.label)||'').trim().toLowerCase();
|
||||
if(
|
||||
phase==='done'||phase==='completed'||phase==='compressed'
|
||||
|| text.includes('auto-compressed')
|
||||
|| text.includes('compression finished')
|
||||
|| (text.includes('compressed')&&!text.includes('compressing'))
|
||||
) return 'compressed';
|
||||
if(
|
||||
phase==='running'||phase==='compressing'
|
||||
|| text.includes('compressing context')
|
||||
|| text.includes('compacting context')
|
||||
|| text.includes('preflight compression')
|
||||
|| text.includes('pre-api compression')
|
||||
|| text.includes('context too large')
|
||||
|| text.includes('compression attempt')
|
||||
|| (text.includes('compressing')&&!text.includes('skipping'))
|
||||
) return 'compressing';
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
function _hydrateAnchorRegistryFromActivityScene(scene){
|
||||
|
||||
@@ -5,6 +5,7 @@ from api.models import Session
|
||||
from api.streaming import (
|
||||
_POST_COMPRESSION_TOOL_RESULT_SUMMARY_FLAG,
|
||||
_compressed_context_tool_result_summary,
|
||||
_is_agent_compression_start_status,
|
||||
_is_fallback_lifecycle_message,
|
||||
_merge_display_messages_after_agent_result,
|
||||
_message_text,
|
||||
@@ -447,14 +448,13 @@ def test_agent_status_callback_emits_compressing_and_warning_events():
|
||||
assert end != -1, "status callback block end marker not found"
|
||||
block = src[start:end]
|
||||
|
||||
# compressing events for compression lifecycle notices
|
||||
# compressing events only via the narrowed helper (no broad substring matcher)
|
||||
assert "put('compressing'" in block
|
||||
assert "'session_id': session_id" in block
|
||||
assert "'message': 'Compressing context'" in block
|
||||
assert "'preflight compression'" in block
|
||||
assert "'compressing'" in block
|
||||
assert "'compacting context'" in block
|
||||
assert "'context too large'" in block
|
||||
assert "_is_agent_compression_start_status(_kind, _message)" in block
|
||||
assert "or 'compressing' in _lower" not in block
|
||||
assert "or 'preflight compression' in _lower" not in block
|
||||
|
||||
# warning events with type:fallback for rate-limit/fallback lifecycle notices
|
||||
assert "put('warning'" in block
|
||||
@@ -471,6 +471,77 @@ def test_agent_status_callback_emits_compressing_and_warning_events():
|
||||
assert "agent.status_callback = _agent_kwargs.get('status_callback')" in src
|
||||
|
||||
|
||||
def test_agent_compression_start_status_matches_real_emitters_only():
|
||||
# Real start notices from hermes-agent emitters
|
||||
assert _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"📦 Preflight compression: ~101,000 tokens >= 96,000 threshold. This may take a moment.",
|
||||
)
|
||||
assert _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"📦 Pre-API compression: ~521,055 tokens near the context/output limit. Compacting before the next model call.",
|
||||
)
|
||||
assert _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"🗜️ Compacting context — summarizing earlier conversation so I can continue...",
|
||||
)
|
||||
assert _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"🗜️ Context too large (~120,000 tokens) — compressing (1/3)...",
|
||||
)
|
||||
assert _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"⚠️ Request payload too large (413) — compression attempt 1/3...",
|
||||
)
|
||||
|
||||
# False positives that previously (or could) light the divider on low-token turns
|
||||
assert not _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"Skipping preflight compression: rough estimate ~20,000 >= 96,000, but last real provider prompt was 18,000 after compression",
|
||||
)
|
||||
assert not _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"Skipping preflight compression: same-session cooldown active (~30 seconds remaining, session abc)",
|
||||
)
|
||||
assert not _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"Skipping Hermes preflight compression for codex app-server (mode=native); Hermes will not start thread compaction here.",
|
||||
)
|
||||
assert not _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"🗜️ Compressed 924 → 143 messages, retrying...",
|
||||
)
|
||||
assert not _is_agent_compression_start_status(
|
||||
"lifecycle",
|
||||
"Rate limited — switching to fallback provider...",
|
||||
)
|
||||
assert not _is_agent_compression_start_status("tool", "compacting context")
|
||||
assert not _is_agent_compression_start_status("lifecycle", "")
|
||||
assert not _is_agent_compression_start_status("lifecycle", "Working on request")
|
||||
|
||||
|
||||
def test_snapshot_anchor_hydration_does_not_invent_compressing_rows():
|
||||
src = _read("static/messages.js")
|
||||
start = src.find("function _sourceEventTypeForSnapshotAnchorRow")
|
||||
assert start != -1, "snapshot anchor source helper not found"
|
||||
end = src.find("function _hydrateAnchorRegistryFromActivityScene", start)
|
||||
assert end != -1, "hydrate helper after snapshot source helper not found"
|
||||
block = src[start:end]
|
||||
|
||||
# Old false-positive defaults must be gone
|
||||
assert "return row&&row.status==='running'?'compressing':'done'" not in block
|
||||
assert "if(role==='lifecycle'||kind==='lifecycle_status') return 'compressing';" not in block
|
||||
|
||||
# Terminal running no longer maps to compressing
|
||||
assert "if(termStatus==='running') return '';" in block
|
||||
# Lifecycle requires positive compression cues
|
||||
assert "text.includes('compacting context')" in block
|
||||
assert "return 'compressed';" in block
|
||||
assert "return 'compressing';" in block
|
||||
# Unknown lifecycle rows stay empty (no invented divider)
|
||||
assert "return '';" in block
|
||||
|
||||
|
||||
def test_agent_status_callback_wiring():
|
||||
src = _read("api/streaming.py")
|
||||
assert "_agent_status_callback" in src
|
||||
|
||||
Reference in new issue
Block a user