fix(compression): detect semantic no-op results
This commit is contained in:
parent
75af6dc57c
commit
766c617e83
2 changed files with 45 additions and 4 deletions
|
|
@ -28,6 +28,7 @@ these paths see no behavioural change.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -1055,6 +1056,7 @@ def compress_context(
|
|||
engine_name,
|
||||
)
|
||||
|
||||
messages_before_compression = copy.deepcopy(messages)
|
||||
compressed = compress_fn(messages, **compress_kwargs)
|
||||
except BaseException:
|
||||
# ANY exception after lock acquisition — memory hook, capability
|
||||
|
|
@ -1097,10 +1099,13 @@ def compress_context(
|
|||
finally:
|
||||
_release_lock()
|
||||
|
||||
# A compressor that returns the exact input object made no structural
|
||||
# progress. Do not rotate/rewrite the session or arm post-compression
|
||||
# deferral in that case; its own anti-thrash counter records the no-op.
|
||||
if compressed is messages:
|
||||
# Compare against the pre-dispatch semantic state, not object identity:
|
||||
# legacy/plugin engines may return an equal copy for a no-op, or mutate
|
||||
# the live list while returning an unchanged snapshot. Neither case may
|
||||
# rotate or rewrite the session.
|
||||
if compressed == messages_before_compression:
|
||||
if messages != messages_before_compression:
|
||||
messages[:] = copy.deepcopy(messages_before_compression)
|
||||
logger.info(
|
||||
"Compression made no progress (session=%s) — skipping boundary rewrite.",
|
||||
agent.session_id or "none",
|
||||
|
|
|
|||
|
|
@ -371,6 +371,42 @@ def test_empty_compression_result_does_not_rotate_session(tmp_path: Path) -> Non
|
|||
assert db.get_session(parent_sid)["end_reason"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("in_place", [False, True])
|
||||
def test_equal_copy_compression_result_does_not_rewrite_session(
|
||||
tmp_path: Path,
|
||||
in_place: bool,
|
||||
) -> None:
|
||||
db = SessionDB(db_path=tmp_path / "state.db")
|
||||
parent_sid = f"EQUAL_COPY_NOOP_{in_place}"
|
||||
db.create_session(parent_sid, source="cli")
|
||||
|
||||
agent = _build_agent_with_db(db, parent_sid)
|
||||
setattr(agent, "compression_in_place", in_place)
|
||||
messages = [{"role": "user", "content": f"m{i}"} for i in range(20)]
|
||||
compressor = getattr(agent, "context_compressor")
|
||||
compressor.compress.side_effect = lambda incoming, **_kw: list(incoming)
|
||||
|
||||
with patch.object(
|
||||
db,
|
||||
"archive_and_compact",
|
||||
wraps=db.archive_and_compact,
|
||||
) as archive_and_compact:
|
||||
returned, _sp = agent._compress_context(
|
||||
messages,
|
||||
"sys",
|
||||
approx_tokens=120_000,
|
||||
)
|
||||
|
||||
assert returned is messages
|
||||
assert getattr(agent, "session_id") == parent_sid
|
||||
assert _count_children(db, parent_sid) == 0
|
||||
parent = db.get_session(parent_sid)
|
||||
assert parent is not None
|
||||
assert parent["end_reason"] is None
|
||||
assert db.get_compression_lock_holder(parent_sid) is None
|
||||
archive_and_compact.assert_not_called()
|
||||
|
||||
|
||||
def test_lock_refresh_keeps_owner_live_past_initial_ttl(tmp_path: Path, monkeypatch) -> None:
|
||||
"""The owning compression call must keep its lease alive while it runs."""
|
||||
real_try_acquire = SessionDB.try_acquire_compression_lock
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue