mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 12:47:42 +02:00
[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:
co-authored by
Claude Opus 5
parent
4fd0c1a10e
commit
b133ef7a1a
@@ -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"
|
||||
Reference in New Issue
Block a user