fix: surface auto-compression handoff

This commit is contained in:
Dennis Soong
2026-05-19 10:45:43 +08:00
parent 718a4c7615
commit ea978a1989
5 changed files with 70 additions and 5 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Surface automatic-compression handoff metadata through the `compressed` SSE event so the active browser stream keeps the completion card even after the backend rotates to a compressed continuation session. The event now carries both the origin session id and continuation id, and the automatic-compression detail line names the compressed session instead of silently dropping the done state.
## [v0.51.91] — 2026-05-18 — Release BO (stage-384 — 5-PR full sweep batch — reasoning-replay history fix + archive-extract per-session inbox + fallback streaming warnings + sanitized custom-provider env hints + Slice 3c queue/goal adapter routing)
### Fixed
+10 -1
View File
@@ -4345,11 +4345,15 @@ def _run_agent_streaming(
# reference stays alive even after the dict entry is removed.
# Concurrent readers that already looked up the old ID will still
# see the same Lock object until they release it.
_compression_origin_session_id = session_id
_compression_continuation_session_id = None
_agent_sid = getattr(agent, 'session_id', None)
_compressed = False
if _agent_sid and _agent_sid != session_id:
old_sid = session_id
new_sid = _agent_sid
_compression_origin_session_id = old_sid
_compression_continuation_session_id = new_sid
s.session_id = new_sid
# Carry profile identity across the compression boundary.
# Without this, s.profile stays None on the continuation
@@ -4426,8 +4430,13 @@ def _run_agent_streaming(
_compression_summary_from_messages(s.messages)
or _compression_summary_from_messages(s.context_messages)
)
if _compression_continuation_session_id is None:
_compression_continuation_session_id = s.session_id
put('compressed', {
'session_id': s.session_id,
'session_id': _compression_origin_session_id,
'old_session_id': _compression_origin_session_id,
'new_session_id': _compression_continuation_session_id,
'continuation_session_id': _compression_continuation_session_id,
'message': 'Context auto-compressed to continue the conversation',
'usage': _live_usage_snapshot(),
})
+4 -1
View File
@@ -1809,7 +1809,9 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
if(!S.session||S.session.session_id!==activeSid) return;
let d={};
try{ d=JSON.parse(e.data||'{}')||{}; }catch(_){ d={}; }
if(d.session_id&&d.session_id!==activeSid) return;
const eventSid=d.old_session_id||d.session_id||activeSid;
if(eventSid!==activeSid) return;
const continuationSid=d.new_session_id||d.continuation_session_id||'';
const message=String(d.message||'Context auto-compressed to continue the conversation').trim();
if(d.usage&&typeof _syncCtxIndicator==='function'){
S.lastUsage={...(S.lastUsage||{}),...d.usage};
@@ -1822,6 +1824,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
automatic:true,
message,
summary:{headline:message},
continuationSessionId:continuationSid,
};
setCompressionUi(state);
const appended=typeof appendLiveCompressionCard==='function'&&appendLiveCompressionCard(state);
+4 -1
View File
@@ -5015,7 +5015,10 @@ function _autoCompressionDetailText(state){
const running=state&&state.phase==='running';
const base=_autoCompressionBaseDetail(state);
const elapsedLabel=running?_compressionElapsedLabel(state):'';
return elapsedLabel?`Elapsed: ${elapsedLabel}`:base;
if(running)return elapsedLabel?`Elapsed: ${elapsedLabel}`:base;
const continuation=String(state&&state.continuationSessionId||'').trim();
const handoff=continuation?`Continued in compressed session: ${continuation}`:'';
return [base,handoff].filter(Boolean).join('\n');
}
function _autoCompressionCardsHtml(state){
const running=state&&state.phase==='running';
+48 -2
View File
@@ -160,6 +160,19 @@ def test_auto_compression_running_detail_avoids_duplicate_message_text():
assert "${base}\\nElapsed:" not in helper
def test_auto_compression_done_detail_surfaces_continuation_handoff():
src = _read("static/ui.js")
start = src.find("function _autoCompressionDetailText")
assert start != -1, "auto compression detail helper not found"
end = src.find("function _autoCompressionCardsHtml", start)
assert end != -1, "auto compression card helper not found after detail helper"
helper = src[start:end]
assert "continuationSessionId" in helper
assert "Continued in compressed session" in helper
assert "return [base,handoff].filter(Boolean).join('\\n');" in helper
def test_auto_compression_live_card_keeps_elapsed_state_for_timer_refresh():
src = _read("static/ui.js")
start = src.find("function appendLiveCompressionCard")
@@ -208,7 +221,22 @@ def test_auto_compression_sse_keeps_inactive_and_malformed_paths_safe():
assert guard in block
assert block.index(guard) < block.index("setCompressionUi")
assert "try{ d=JSON.parse(e.data||'{}')||{}; }catch(_){ d={}; }" in block
assert "if(d.session_id&&d.session_id!==activeSid) return;" in block
assert "const eventSid=d.old_session_id||d.session_id||activeSid;" in block
assert "if(eventSid!==activeSid) return;" in block
def test_auto_compression_done_accepts_rotated_continuation_session_event():
block = _compressed_listener_block()
# Auto-compression can rotate the backend session id before the 'compressed'
# event is emitted. The browser stream still belongs to the pre-compression
# activeSid, so the listener must correlate on old_session_id and keep the
# continuation id as display metadata instead of dropping the event.
assert "const eventSid=d.old_session_id||d.session_id||activeSid;" in block
assert "const continuationSid=d.new_session_id||d.continuation_session_id||'';" in block
assert "if(eventSid!==activeSid) return;" in block
assert block.index("const eventSid=") < block.index("if(eventSid!==activeSid) return;")
assert "continuationSessionId:continuationSid" in block
def test_auto_compression_done_sse_refreshes_context_indicator_usage():
@@ -228,10 +256,28 @@ def test_auto_compression_done_payload_includes_live_usage_snapshot():
assert end != -1, "compressed SSE payload end not found"
block = src[start:end]
assert "'session_id': s.session_id" in block
assert "'session_id': _compression_origin_session_id" in block
assert "'old_session_id': _compression_origin_session_id" in block
assert "'new_session_id': _compression_continuation_session_id" in block
assert "'continuation_session_id': _compression_continuation_session_id" in block
assert "'usage': _live_usage_snapshot()" in block
def test_auto_compression_rotation_tracks_origin_and_continuation_ids_for_sse():
src = _read("api/streaming.py")
rotate_start = src.find("# ── Handle context compression side effects ──")
assert rotate_start != -1, "compression side-effect block not found"
rotate_end = src.find("# Stamp 'timestamp'", rotate_start)
assert rotate_end != -1, "compression side-effect block end not found"
block = src[rotate_start:rotate_end]
assert "_compression_origin_session_id = session_id" in block
assert "_compression_continuation_session_id = None" in block
assert "_compression_origin_session_id = old_sid" in block
assert "_compression_continuation_session_id = new_sid" in block
assert "'new_session_id': _compression_continuation_session_id" in block
def test_auto_compression_card_reuses_compression_card_renderer():
src = _read("static/ui.js")
start = src.find("function _autoCompressionCardsHtml")