fix(agent): harden the finalize-turn micro-compaction gate against duck-typed compressors

tests/run_agent/test_proactive_prune_loop_wiring.py builds agents with a
MagicMock compressor; getattr(mock, '_micro_compact_enabled', False)
returns a truthy auto-attribute, so the hook called _micro_compact on the
mock and spliced its (empty-iterating) return over the transcript —
wiping all messages before persist (CI slice 7/8 failure).

Gate now requires _micro_compact_enabled is True, a callable
_micro_compact, and a non-empty list result before touching messages.
Same hardening protects production plugin context engines that don't
subclass ContextCompressor.
This commit is contained in:
kshitijk4poor 2026-07-31 16:10:12 +05:30 committed by kshitij
parent b8bfd68af1
commit c696a5fd9c

View file

@ -358,13 +358,21 @@ def finalize_turn(
if not interrupted and not failed:
try:
_compressor = getattr(agent, "context_compressor", None)
# Strict `is True` + isinstance gates: plugin context engines
# (and MagicMock compressors in tests) satisfy getattr/duck
# checks with truthy auto-attributes — a bare truthiness check
# here called _micro_compact on a mock and spliced its (empty-
# iterating) return value over the transcript, wiping it.
if (
_compressor
and getattr(_compressor, '_micro_compact_enabled', False)
and getattr(_compressor, '_micro_compact_enabled', False) is True
and callable(getattr(_compressor, '_micro_compact', None))
and final_response
):
_before = len(messages)
messages[:] = _compressor._micro_compact(messages) or messages
_compacted = _compressor._micro_compact(messages)
if isinstance(_compacted, list) and _compacted:
messages[:] = _compacted
_after = len(messages)
if _before != _after:
logger.info(