[eric] agents: a pruned recap says it is partial, and the prune that reclaimed the history is counted

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U6zrBsUCNzpMBnov3rTVYV
This commit is contained in:
ciregenz
2026-08-29 11:17:38 -07:00
co-authored by Claude Opus 5
parent 4fd0c1a10e
commit b133ef7a1a
6 changed files with 145 additions and 2 deletions
+3
View File
@@ -209,6 +209,9 @@ class AgentSession(BaseModel):
# first? Per-turn counters die with the turn, so a block envelope carried nothing.
cli_compactions: int = 0
midturn_breaks: int = 0
# The third context event, and the one that actually fires: a proactive prune reclaimed 40K on a
# real session while BOTH counters above read 0, so the fleet still could not see it (ENG-418).
proactive_prunes: int = 0
# Aux-LLM distilled summary of the turns dropped by compaction, cached against the cutoff id it was built for; keeps the gist of old history on a rebuild instead of a hard drop.
compacted_summary: Optional[str] = None
compacted_summary_through: Optional[str] = None
@@ -108,6 +108,7 @@ def p_report_model_error(subkind: str, session_id: str, session: AgentSession, t
# many times the CLI compacted this chat, and how many times we broke the turn first.
"cli_compactions": int(getattr(session, "cli_compactions", 0) or 0),
"midturn_breaks": int(getattr(session, "midturn_breaks", 0) or 0),
"proactive_prunes": int(getattr(session, "proactive_prunes", 0) or 0),
"history_prefix_sent": session.history_prefix_sent,
"delegated": p_used_delegation(session),
})
@@ -212,9 +212,16 @@ def build_history_prefix(messages, cutoff_msg_id: Optional[str] = None) -> str:
if not lines:
return ""
# Framing lifted from hermes-agent's compaction handoff (context_compressor.py, MIT): reference only, never active instructions, the message after it is the single source of truth, and an explicit end marker so a weak model cannot read the last line as fresh input.
# Say it is PARTIAL when it is. Measured 2026-08-29 on the packaged build: after a proactive
# prune the model was asked what the user's FIRST message had been and confidently quoted a much
# later one, because nothing in the recap distinguishes "this is the start of the conversation"
# from "this is what survived". A recap that hides its own gap turns lost context into a
# confident wrong answer, which is worse than the loss.
p_partial = " Earlier turns have been dropped to save space, so this does NOT begin at the start of the conversation; if asked about something not in it, say so rather than guessing." if cutoff_msg_id else ""
p_recap_frame = ("Recap of YOUR OWN earlier turns in this same conversation (what was asked and which tools "
"you ran), kept locally by the OpenSwarm app so you can continue where you left off. "
"Reference only: do not answer or redo anything in it; respond to the message that follows.")
"you ran), kept locally by the OpenSwarm app so you can continue where you left off."
+ p_partial +
" Reference only: do not answer or redo anything in it; respond to the message that follows.")
return (f"{SESSION_RECAP_OPEN}\n{PLATFORM_NOTE_PREAMBLE}\n{p_recap_frame}\n" + "\n".join(lines)
+ f"\n--- end of recap; respond to the message below, not the recap above ---\n{SESSION_RECAP_CLOSE}")
@@ -128,6 +128,7 @@ def arm_proactive_prune(session: AgentSession) -> None:
"""Commit the prune: mark history aged and force the rebuild that actually applies it."""
from backend.apps.agents.manager.context_budget import maybe_compact
maybe_compact(session, force=True)
session.proactive_prunes += 1
session.needs_fresh_session = True
session.proactive_prune_rearm_tokens = (
estimate_aged_rebuild_tokens(session) + REARM_GROWTH_TOKENS
@@ -224,3 +224,80 @@ def test_a_terms_summary_is_not_mistaken_for_the_filter_refusing():
):
assert neutralize_provider_refusal(innocent) == innocent
assert classify_provider_error(innocent) is None
# ------------------------------------------------- the mid-turn valve's growth rule (ENG-418)
def test_a_turn_that_starts_high_and_sits_still_is_still_left_alone(monkeypatch):
"""Growth made more turns breakable, and every break costs a transcript REBUILD -- the frequency
ENG-382 exists to reduce. The innocent case is a rebuild that failed to shrink: it starts high
and does not grow, and it must RUN rather than rebuild forever."""
from backend.apps.agents.manager.context_budget import maybe_break_midturn
from backend.apps.agents.manager.streaming.state import TurnState
s = AgentSession(name="t", model="opus-5"); s.context_window = 1_000_000
t = TurnState()
assert maybe_break_midturn(s, t, {"input_tokens": 500_000}) is False
assert maybe_break_midturn(s, t, {"input_tokens": 505_000}) is False, "5K is not material growth"
assert s.midturn_breaks == 0
def test_a_grown_turn_breaks_at_most_once_so_rebuilds_cannot_multiply(monkeypatch):
from backend.apps.agents.manager.context_budget import maybe_break_midturn
from backend.apps.agents.manager.streaming.state import TurnState
s = AgentSession(name="t", model="opus-5"); s.context_window = 1_000_000
t = TurnState()
maybe_break_midturn(s, t, {"input_tokens": 100_000})
assert maybe_break_midturn(s, t, {"input_tokens": 400_000}) is True
for n in (500_000, 600_000, 900_000):
assert maybe_break_midturn(s, t, {"input_tokens": n}) is False
assert s.midturn_breaks == 1
# --------------------------------------------------- Close vs Stop on a workflow card (ENG-421)
def test_a_workflow_run_that_ended_on_its_own_still_despawns():
"""The card is kept for a HUMAN stop. The innocent case is the nightly workflow that ends by
itself: keeping its card would leave litter on the canvas every single night, which is the leak
the original rule was written to prevent."""
a = "frontend/src/shared/state/isUserLaunchedSession.ts"
src = open(a, encoding="utf-8").read()
assert "if (LIVE_STATUSES.has" in src and "ended_by_user" in src
assert "!session.dismissed_by_user" in src, "Close must still be able to dismiss"
assert "!session.closed_at" not in src, \
"closed_at is stamped by the executor on every run; using it made the fix dead code"
def test_the_dismissal_flag_is_at_the_close_door_not_in_the_shared_helper():
"""agent_manager.close_session is called by the workflow executor for bookkeeping. A flag there
would mark every finished run as user-dismissed and delete the stopped-run cards again."""
helper = open("backend/apps/agents/manager/SessionControl.py", encoding="utf-8").read()
assert "dismissed_by_user" not in helper
# ------------------------------------------------------ the PTC fan-out width cap (ENG-417)
def test_one_scripts_fan_out_cannot_starve_the_rest_of_the_chat():
"""The cap is module-global on purpose: two scripts in one sidecar SHARE the width rather than
each taking a full one. The innocent case is the chat's other builtin tools, which ride the same
process and must still get scheduled while a 25-call batch runs."""
from backend.apps.agents import ptc_mcp_server as ptc
src = open("backend/apps/agents/ptc_mcp_server.py", encoding="utf-8").read()
assert "P_FANOUT_SLOTS = threading.Semaphore(SCRIPT_FANOUT_WIDTH)" in src
i = src.index("def p_dispatch_batch")
body = src[i:src.index("def p_elide", i)]
assert "P_FANOUT_SLOTS.acquire(timeout=" in body, "an unbounded acquire can lose a slot forever"
assert "finally:" in body and "P_FANOUT_SLOTS.release()" in body
assert ptc.SCRIPT_FANOUT_WIDTH <= 8
# ------------------------------------------------- the canvas wheel owner (ENG-420)
def test_a_gesture_that_starts_on_a_panel_still_belongs_to_the_panel():
"""Canvas ownership was added so a pan is not swallowed mid-drift. The innocent case is the
original rule it must not break: a scroll that starts inside Settings has to stay there past the
panel's end, or reaching the bottom of a list drags the whole world."""
src = open("frontend/src/app/pages/Dashboard/hooks/interaction/wheelGestureOwner.ts",
encoding="utf-8").read()
assert "owner === CANVAS_OWNER" in src
assert "owner.contains(target" in src, "an element owner must still hold its own gesture"
assert "isZoom" in src, "zoom must stay reachable on every surface"
@@ -0,0 +1,54 @@
"""A recap that hides its own gap turns lost context into a confident wrong answer.
Found on the packaged build 2026-08-29, simulating the heaviest real install's shape (123 tool calls,
132K input, serial read->run->read work). A proactive prune reclaimed ~40K, which is by design. Then
the chat was asked what the user's FIRST message had been, and it quoted a much later one as fact
instead of saying it did not know. Nothing in the recap distinguishes "this is the start of the
conversation" from "this is what survived the prune", so the model has no way to tell.
Row 5 on the ladder (a lying status), reached from row 1 (context silently gone). The loss itself is
the intended trade; presenting the remainder as the whole is not."""
from backend.apps.agents.core.models import AgentSession, Message
from backend.apps.agents.manager.session.history_compaction import build_history_prefix
def p_msgs():
return [
Message(role="user", content="Work in /tmp/osw-heavy, there are 14 modules", branch_id="main"),
Message(role="tool_call", content={"tool": "Read", "input": {"file": "mod_01.py"}}, branch_id="main"),
Message(role="tool_result", content={"tool_name": "Read", "text": "ok"}, branch_id="main"),
Message(role="user", content="Now re-read them all", branch_id="main"),
Message(role="tool_call", content={"tool": "Read", "input": {"file": "mod_02.py"}}, branch_id="main"),
Message(role="tool_result", content={"tool_name": "Read", "text": "ok"}, branch_id="main"),
Message(role="user", content="And once more", branch_id="main"),
]
def test_a_pruned_recap_says_it_does_not_start_at_the_beginning():
msgs = p_msgs()
out = build_history_prefix(msgs, cutoff_msg_id=msgs[2].id)
low = out.lower()
assert "dropped" in low, "the recap must admit earlier turns are gone"
assert "does not begin at the start" in low or "not begin at the start" in low
assert "say so rather than guessing" in low, "and tell it what to do instead of inventing one"
def test_an_UNPRUNED_recap_makes_no_such_claim():
"""The innocent case: a full recap really does start at the beginning, and telling the model
otherwise would make it refuse to answer things it genuinely has."""
out = build_history_prefix(p_msgs(), cutoff_msg_id=None)
assert "dropped" not in out.lower()
assert "The user asked:" in out, "a full recap still carries the asks"
def test_the_prune_is_counted_where_the_fleet_can_see_it():
"""ENG-418 added counters for the CLI's compactions and our mid-turn breaks. On a real heavy
session BOTH read 0 while a proactive prune reclaimed ~40K, so the fleet still could not answer
'how often does a chat lose history'."""
s = AgentSession(name="t", model="opus-5")
assert s.proactive_prunes == 0
src = open("backend/apps/agents/manager/session/proactive_prune.py", encoding="utf-8").read()
assert "session.proactive_prunes += 1" in src
env = open("backend/apps/agents/manager/run/handle_run_error.py", encoding="utf-8").read()
assert '"proactive_prunes"' in env, "counted but never sent is the same blind spot"