From 97308707e91a34310531661cec89a23b11b14f2c Mon Sep 17 00:00:00 2001 From: Teknium Date: Thu, 9 Apr 2026 14:28:49 -0700 Subject: [PATCH] fix: insert static fallback when compression summary fails When _generate_summary() failed (no provider, timeout, model error), the compressor silently dropped all middle turns with just a debug log. The agent would then see head + tail with no explanation of the gap, causing total context amnesia (generic greetings instead of continuing the conversation). Now generates a static fallback marker that tells the model context was lost and to continue from the recent tail messages. The fallback flows through the same role-alternation logic as a real summary so message structure stays valid. --- agent/context_compressor.py | 62 +++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index c61cf2c5a..eba2de3f3 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -691,33 +691,43 @@ Write only the summary body. Do not include any preamble or prefix.""" ) compressed.append(msg) - _merge_summary_into_tail = False - if summary: - last_head_role = messages[compress_start - 1].get("role", "user") if compress_start > 0 else "user" - first_tail_role = messages[compress_end].get("role", "user") if compress_end < n_messages else "user" - # Pick a role that avoids consecutive same-role with both neighbors. - # Priority: avoid colliding with head (already committed), then tail. - if last_head_role in ("assistant", "tool"): - summary_role = "user" - else: - summary_role = "assistant" - # If the chosen role collides with the tail AND flipping wouldn't - # collide with the head, flip it. - if summary_role == first_tail_role: - flipped = "assistant" if summary_role == "user" else "user" - if flipped != last_head_role: - summary_role = flipped - else: - # Both roles would create consecutive same-role messages - # (e.g. head=assistant, tail=user — neither role works). - # Merge the summary into the first tail message instead - # of inserting a standalone message that breaks alternation. - _merge_summary_into_tail = True - if not _merge_summary_into_tail: - compressed.append({"role": summary_role, "content": summary}) - else: + # If LLM summary failed, insert a static fallback so the model + # knows context was lost rather than silently dropping everything. + if not summary: if not self.quiet_mode: - logger.debug("No summary model available — middle turns dropped without summary") + logger.warning("Summary generation failed — inserting static fallback context marker") + n_dropped = compress_end - compress_start + summary = ( + f"{SUMMARY_PREFIX}\n" + f"Summary generation was unavailable. {n_dropped} conversation turns were " + f"removed to free context space but could not be summarized. The removed " + f"turns contained earlier work in this session. Continue based on the " + f"recent messages below and the current state of any files or resources." + ) + + _merge_summary_into_tail = False + last_head_role = messages[compress_start - 1].get("role", "user") if compress_start > 0 else "user" + first_tail_role = messages[compress_end].get("role", "user") if compress_end < n_messages else "user" + # Pick a role that avoids consecutive same-role with both neighbors. + # Priority: avoid colliding with head (already committed), then tail. + if last_head_role in ("assistant", "tool"): + summary_role = "user" + else: + summary_role = "assistant" + # If the chosen role collides with the tail AND flipping wouldn't + # collide with the head, flip it. + if summary_role == first_tail_role: + flipped = "assistant" if summary_role == "user" else "user" + if flipped != last_head_role: + summary_role = flipped + else: + # Both roles would create consecutive same-role messages + # (e.g. head=assistant, tail=user — neither role works). + # Merge the summary into the first tail message instead + # of inserting a standalone message that breaks alternation. + _merge_summary_into_tail = True + if not _merge_summary_into_tail: + compressed.append({"role": summary_role, "content": summary}) for i in range(compress_end, n_messages): msg = messages[i].copy()