fix: clear runtime fields on compression snapshots

This commit is contained in:
ai-ag2026
2026-05-15 09:20:19 +02:00
parent 5e518b1c10
commit 3a4259476d
3 changed files with 83 additions and 5 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Context-compression snapshot preservation now clears archived parent runtime fields (`active_stream_id`, `pending_user_message`, attachments, and start timestamp) before saving the old session id. This prevents a completed continuation session from leaving its parent looking permanently active/running after compression.
### Added
- **PR #2099** by @dobby-d-elf — Adds an opt-in `Settings → Preferences → Fade text effect` toggle (off by default). When enabled, newly streamed output tokens are revealed through an adaptive playout buffer and animated with an opacity-only fade similar to ChatGPT and other frontier LLM apps. Implementation details: fade locked per stream to avoid mid-stream toggle rewind; reduced-motion users get non-animated text; live cursor hidden while fade is active; custom renderer on `streaming-markdown` parser wraps only newly-appended words; animated spans replace themselves with plain text on `animationend` (no long-lived wrapper buildup in long responses); unsafe streamed `href`/`src` values blocked in fade renderer `set_attr` path. Performance tuning: 200ms base fade duration scaling to 350ms for fast output, 16ms word stagger, 320ms done-drain wait cap, 160 wps visual cap, max 2-3 words/frame, brief pauses after sentence punctuation. Default-off means existing users see no change. 293-line regression test pinning the contract.
+32 -5
View File
@@ -1862,6 +1862,37 @@ def _drop_checkpointed_current_user_from_context(messages, msg_text):
return history
def _save_pre_compression_snapshot(session, old_session_id):
"""Persist the archived pre-compression session without live turn state.
During context compression the same ``Session`` object is reused for the new
continuation id. Before the final continuation save clears
``active_stream_id`` and ``pending_*``, we also preserve an old-id snapshot so
the full pre-compression transcript remains recoverable. That archived
parent must not keep the current stream bookkeeping, otherwise the sidebar can
reopen the parent as a permanently running session while the child already
contains the completed answer.
"""
saved_sid = session.session_id
saved_active_stream_id = getattr(session, 'active_stream_id', None)
saved_pending_user_message = getattr(session, 'pending_user_message', None)
saved_pending_attachments = list(getattr(session, 'pending_attachments', []) or [])
saved_pending_started_at = getattr(session, 'pending_started_at', None)
session.session_id = old_session_id
session.active_stream_id = None
session.pending_user_message = None
session.pending_attachments = []
session.pending_started_at = None
try:
session.save(touch_updated_at=False, skip_index=True)
finally:
session.session_id = saved_sid
session.active_stream_id = saved_active_stream_id
session.pending_user_message = saved_pending_user_message
session.pending_attachments = saved_pending_attachments
session.pending_started_at = saved_pending_started_at
def _stream_writeback_is_current(session, stream_id):
"""Return True only while a worker still owns the session writeback.
@@ -3651,18 +3682,14 @@ def _run_agent_streaming(
# before save then restored after — but the on-disk
# copy persisted with parent=None, breaking
# fork-of-fork lineage traversal. (#2227 + #2223)
saved_sid = s.session_id
s.session_id = old_sid
try:
s.save(touch_updated_at=False, skip_index=True)
_save_pre_compression_snapshot(s, old_sid)
logger.info(
"Preserved pre-compression session %s (%d messages) to disk",
old_sid, len(s.messages),
)
except Exception:
logger.debug("Failed to preserve pre-compression session file", exc_info=True)
finally:
s.session_id = saved_sid
except OSError:
logger.debug("Could not read old session file before preservation")
# Always link the continuation session to its immediate predecessor
@@ -0,0 +1,47 @@
from api import streaming
class FakeSession:
def __init__(self):
self.session_id = "new_session"
self.parent_session_id = "original_parent"
self.active_stream_id = "live-stream"
self.pending_user_message = "current prompt"
self.pending_attachments = [{"name": "file.txt"}]
self.pending_started_at = 123.0
self.messages = [{"role": "user", "content": "current prompt"}]
self.saved_payload = None
def save(self, *, touch_updated_at=True, skip_index=False):
self.saved_payload = {
"session_id": self.session_id,
"parent_session_id": self.parent_session_id,
"active_stream_id": self.active_stream_id,
"pending_user_message": self.pending_user_message,
"pending_attachments": list(self.pending_attachments),
"pending_started_at": self.pending_started_at,
"touch_updated_at": touch_updated_at,
"skip_index": skip_index,
}
def test_pre_compression_snapshot_clears_runtime_fields_while_restoring_continuation_state():
session = FakeSession()
streaming._save_pre_compression_snapshot(session, "old_session")
assert session.saved_payload == {
"session_id": "old_session",
"parent_session_id": "original_parent",
"active_stream_id": None,
"pending_user_message": None,
"pending_attachments": [],
"pending_started_at": None,
"touch_updated_at": False,
"skip_index": True,
}
assert session.session_id == "new_session"
assert session.active_stream_id == "live-stream"
assert session.pending_user_message == "current prompt"
assert session.pending_attachments == [{"name": "file.txt"}]
assert session.pending_started_at == 123.0