[eric] agents: the rebuild recap's tool trail is bounded, so a 766-call chat restarts at 78K tokens instead of 192K and the CLI stops compacting on its first step

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012G8kyALnPjsA7aJFmMBq3R
This commit is contained in:
ciregenz
2026-09-01 18:14:32 -07:00
co-authored by Claude Fable 5.1
parent 17ebd689f6
commit 383c04b0e2
2 changed files with 136 additions and 3 deletions
@@ -1,6 +1,6 @@
import json
import logging
from typing import List, Optional, Tuple
from typing import Dict, List, Optional, Tuple
from typeguard import typechecked
import os
import re
@@ -22,6 +22,11 @@ SESSION_RECAP_CLOSE = "</openswarm_session_recap>"
# Per-turn caps so the re-grounded recap stays compact (summaries, not replays) and cannot reinflate the context window from one giant tool input/output.
RECAP_TOOL_INPUT_CAP = 200
RECAP_TOOL_RESULT_CAP = 500
# Whole-trail budget: a rebuilt session must start far below the compaction trigger, whatever the chat's length. ~10K tokens.
RECAP_TRAIL_MAX_CHARS = 40_000
# Older calls survive as bare stubs (the command, so it can be re-run) inside this second budget; older still fold to a count.
RECAP_STUB_MAX_CHARS = 24_000
RECAP_STUB_CHARS = 120
# Inline budget for a spilled tool result, split head/tail. Same total as the old head-only 4KB, but a test summary or build verdict lives at the END of the output and head-only threw it away every time.
SPILL_HEAD_CHARS = 2_500
@@ -133,7 +138,7 @@ def get_branch_messages(session) -> List:
@typechecked
def trail_lines(messages, cutoff_msg_id: Optional[str] = None) -> List[str]:
def trail_lines(messages, cutoff_msg_id: Optional[str] = None, max_trail_chars: int = RECAP_TRAIL_MAX_CHARS) -> List[str]:
"""The user's asks and the tool trail, and NEVER a line of model-authored prose.
Extracted so every renderer bound for a model's context shares one definition of what is safe
@@ -163,7 +168,67 @@ def trail_lines(messages, cutoff_msg_id: Optional[str] = None) -> List[str]:
tool_name = m.content.get("tool_name") if isinstance(m.content, dict) else None
label = f"Tool result ({tool_name})" if tool_name else "Tool result"
lines.append(f"{label}: {strip_forged_sentinels(body)}")
return lines
return bound_trail(lines, max_trail_chars)
@typechecked
def bound_trail(lines: List[str], max_trail_chars: int) -> List[str]:
"""Three tiers, newest first: full lines within `max_trail_chars`; then bare re-runnable call
stubs (no result lines, each cut to RECAP_STUB_CHARS) within RECAP_STUB_MAX_CHARS; then ONE
counted line for everything older. Asks are never dropped. Aging alone left one full line per
tool call forever: a real 766-call chat rebuilt with a 98K-token recap, so its first request was
192K tokens against a 180K trigger and the CLI compacted before the second tool call, every time
(the Recall Radar thrash, 2026-09-01)."""
p_tool = [i for i, l in enumerate(lines) if l.startswith("Tool call") or l.startswith("Tool result")]
if not p_tool:
return lines
p_spent = 0
p_full_from = len(p_tool)
for k in range(len(p_tool) - 1, -1, -1):
p_spent += len(lines[p_tool[k]]) + 1
if p_spent > max_trail_chars:
break
p_full_from = k
if p_full_from == 0:
return lines
p_stub: Dict[int, str] = {}
p_stub_spent = 0
p_stub_from = p_full_from
for k in range(p_full_from - 1, -1, -1):
i = p_tool[k]
if not lines[i].startswith("Tool call"):
continue
p_line = lines[i][:RECAP_STUB_CHARS]
if p_stub_spent + len(p_line) + 1 > RECAP_STUB_MAX_CHARS:
break
p_stub_spent += len(p_line) + 1
p_stub[i] = p_line
p_stub_from = k
p_folded_idx = p_tool[:p_stub_from]
p_counts: Dict[str, int] = {}
for i in p_folded_idx:
if lines[i].startswith("Tool call: "):
p_name = lines[i][len("Tool call: "):].split("(", 1)[0].strip() or "tool"
p_counts[p_name] = p_counts.get(p_name, 0) + 1
p_calls = sum(p_counts.values())
p_by_tool = ", ".join(f"{n} {c}" for n, c in sorted(p_counts.items(), key=lambda kv: -kv[1])[:8])
p_fold = f"[{p_calls} earlier tool calls are not shown to save space" + (f": {p_by_tool}" if p_by_tool else "") + "]"
p_folded = set(p_folded_idx)
p_middle = set(p_tool[p_stub_from:p_full_from])
p_out: List[str] = []
p_fold_written = p_calls == 0
for i, l in enumerate(lines):
if i in p_folded:
if not p_fold_written:
p_out.append(p_fold)
p_fold_written = True
continue
if i in p_middle:
if i in p_stub:
p_out.append(p_stub[i])
continue
p_out.append(l)
return p_out
@typechecked
@@ -0,0 +1,68 @@
"""The recap's tool trail must be bounded whatever the chat's length.
Aging kept one line per tool call forever. Eric's real Recall Radar chat (766 tool calls) rebuilt
with a 311,990-char / 97,923-token recap, so its first request was 192,056 tokens against a 180,000
trigger: the CLI compacted before the second tool call, the window refilled, and the chat died of
"Autocompact is thrashing" five times on exp.5 and again, identically, on exp.3 (2026-09-01)."""
from backend.apps.agents.core.models import Message
from backend.apps.agents.manager.session.history_compaction import (
RECAP_STUB_MAX_CHARS, RECAP_TRAIL_MAX_CHARS, bound_trail, build_history_prefix, trail_lines,
)
def p_long_chat(calls: int):
msgs = [Message(role="user", content="Build the recall app, for real.", branch_id="main")]
for i in range(calls):
tool = "Bash" if i % 3 else "Read"
msgs.append(Message(role="tool_call", content={"tool": tool, "input": {"command": f"step {i} " + "x" * 120}}, branch_id="main"))
msgs.append(Message(role="tool_result", content={"tool_name": tool, "text": f"result {i} " + "y" * 300}, branch_id="main"))
if i % 100 == 99:
msgs.append(Message(role="user", content=f"checkpoint ask {i}", branch_id="main"))
msgs.append(Message(role="user", content="what's the progress?", branch_id="main"))
return msgs
def test_an_800_call_chat_rebuilds_under_the_budget_and_says_what_it_dropped():
lines = trail_lines(p_long_chat(800))
p_tool = [l for l in lines if l.startswith("Tool ")]
assert sum(len(l) + 1 for l in p_tool) <= RECAP_TRAIL_MAX_CHARS + RECAP_STUB_MAX_CHARS, "full tier plus stub tier is the whole tool budget"
p_fold = [l for l in lines if "earlier tool calls are not shown" in l]
assert len(p_fold) == 1, "exactly one counted fold line"
assert "Bash" in p_fold[0] and "Read" in p_fold[0], "the fold names what it dropped, by tool"
assert lines.index(p_fold[0]) < lines.index(p_tool[0]), "the fold sits where the dropped span was, before the kept tail"
def test_every_ask_survives_and_the_newest_calls_stay_verbatim():
msgs = p_long_chat(800)
lines = trail_lines(msgs)
asks = [l for l in lines if l.startswith("The user asked")]
assert len(asks) == 1 + 8 + 1, "no ask is ever folded"
assert any("step 799 " in l for l in lines), "the newest call is verbatim"
assert any("result 799 " in l for l in lines), "the newest result is verbatim"
assert not any("step 5 " in l for l in lines), "the oldest call is folded away"
def test_a_short_chat_is_untouched():
msgs = p_long_chat(20)
lines = trail_lines(msgs)
assert not any("not shown" in l for l in lines)
assert sum(1 for l in lines if l.startswith("Tool call")) == 20
def test_the_whole_prefix_is_bounded_for_the_real_shape():
out = build_history_prefix(p_long_chat(766))
assert len(out) < RECAP_TRAIL_MAX_CHARS + RECAP_STUB_MAX_CHARS + 8_000, f"prefix is {len(out)} chars; the frame plus a bounded trail"
def test_the_middle_tier_keeps_bare_rerunnable_stubs_and_no_result_lines():
lines = trail_lines(p_long_chat(800))
p_first_full = next(i for i, l in enumerate(lines) if l.startswith("Tool result"))
p_stubs = [l for l in lines[:p_first_full] if l.startswith("Tool call")]
assert p_stubs, "an 800-call chat has a middle tier of bare stubs"
assert all(len(l) <= 120 for l in p_stubs)
assert not any(l.startswith("Tool result") for l in lines[:p_first_full])
def test_bound_trail_is_a_no_op_under_budget():
lines = ["The user asked: a", "Tool call: Read(x)", "Tool result (Read): ok"]
assert bound_trail(lines, 10_000) == lines