perf: preserve conversation prefixes across agent turns

This commit is contained in:
chenmingxuan committed 2026-10-17 17:25:15 +08:00
1 parent 1440a1f508
commit f1bb5d02aa
19 files changed
+552 -205

No files matched your search

+118 -42
View File
@@ -192,7 +192,17 @@ def _make(
capsule_service=capsule,
# No meter: the base comes from the ledger's operator-recorded prompt
# size (None until recorded), exactly like production wiring.
meter_getter=meter,
meter_getter=meter
or (
lambda: (
75_000
if chunker.awaiting_chunks
and all(
capsule.get_summary(c.capsule_key) is not None for c in chunker.awaiting_chunks
)
else 40_000
)
),
goal="TEST GOAL",
)
ledger.attach_chunker(chunker)
@@ -443,7 +453,8 @@ def test_segment_size_threshold_triggers_without_milestone():
assert len(chunker.chunks) >= 1
assert chunker.chunks[0].start_step_number == 1
total = sum(c.end_step_number - c.start_step_number + 1 for c in chunker.chunks)
assert total >= 4
assert total >= 3
assert chunker.awaiting_chunks # the next prepared batch has not swapped yet
assert len(ledger.unchunked_turns()) >= 2 # floor respected
@@ -458,7 +469,7 @@ def test_source_token_threshold_triggers_chunk():
def test_soft_threshold_compresses_oldest_open_segment():
steps = [_step(i) for i in range(1, 9)]
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, _, _ = _make(
steps, min_active=2, max_steps=100, meter=lambda: meter["value"], auto_capsule=True
)
@@ -843,7 +854,7 @@ def test_hard_threshold_folds_frozen_region_into_recall_only_era_with_step_index
merged) plus the per-step minimal index of every folded chunk — step,
session offset and action phrase, results only via search_history."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 8, _hashes(4))
capsule.resolve("chunk:1-4", _capsule(1, 4))
@@ -1083,7 +1094,7 @@ def test_failed_capsule_redispatches_and_retains_original():
"""Failure rung of the ladder: an exhausted capsule job is re-dispatched at
the next trigger/pressure render while the original turns stay live."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 8, _hashes(4))
key = chunker.awaiting_chunks[0].capsule_key
@@ -1106,7 +1117,7 @@ def test_hard_threshold_force_swaps_pending_chunks_into_recall_only_era():
paragraph degrades to the milestone/step-range list, and ③'s minimal
index survives for every folded step."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, _, _ = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 8, _hashes(4))
assert chunker.awaiting_chunks[0].status == "pending"
@@ -1150,7 +1161,9 @@ def test_pressure_close_waits_for_min_steps_instead_of_one_turn_chunks():
_run_turns(ledger, chunker, 7, 8) # eligible beyond the claim: 1, then 2
assert [c.capsule_key for c in chunker.awaiting_chunks] == ["chunk:1-3"]
_run_turns(ledger, chunker, 9, 9)
assert [c.capsule_key for c in chunker.awaiting_chunks] == ["chunk:1-3", "chunk:4-6"]
assert [c.capsule_key for c in chunker.awaiting_chunks] == [
"chunk:1-3"
] # finish this batch first
def test_size_trigger_waits_for_min_steps_even_when_one_turn_exceeds_the_tokens():
@@ -1193,7 +1206,7 @@ def test_start_gate_holds_ready_chunk_below_start_ratio_then_swaps():
replaced only once the operator's measured context reaches
budget*start_ratio; until then the ready chunk waits in the queue."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 11)]
meter = {"value": 20_000} # < 0.35 * 100k
meter = {"value": 40_000} # < 0.35 * 100k
ledger, chunker, engine, _ = _make(
steps, min_active=2, meter=lambda: meter["value"], auto_capsule=True
)
@@ -1212,13 +1225,13 @@ def test_start_gate_holds_ready_chunk_below_start_ratio_then_swaps():
args = held[0]["payload"]["args"]
assert args["note"] == "held"
assert (args["context_tokens"], args["context_budget"], args["swap_at_tokens"]) == (
20_000,
40_000,
100_000,
35_000,
70_000,
)
# Same render cadence, context now past the gate → the ready prefix swaps.
meter["value"] = 40_000
meter["value"] = 75_000
_run_turns(ledger, chunker, 9, 9, _hashes(4))
assert len(chunker.chunks) == 1
assert chunker.chunks[0].start_step_number == 1
@@ -1226,9 +1239,7 @@ def test_start_gate_holds_ready_chunk_below_start_ratio_then_swaps():
assert ledger.frozen_blocks != ()
done = engine.traces[-1]
assert done["status"] == "success"
assert done["payload"]["args"]["context_tokens"] == 40_000 - (
chunker.chunks[0].source_chars // 4
) + (chunker.chunks[0].summary_chars // 4)
assert done["payload"]["args"]["context_tokens"] < 75_000
def test_leading_stampless_turn_joins_the_next_segment_instead_of_pinning_the_queue():
@@ -1238,13 +1249,14 @@ def test_leading_stampless_turn_joins_the_next_segment_instead_of_pinning_the_qu
the head of the awaiting queue, so no L2 swap ever happened and the hard
line jumped straight to the fold. It joins the following segment."""
steps = [_step(i) for i in range(1, 13)]
meter = {"value": 50_000} # above the start gate
meter = {"value": 40_000} # above the start gate
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
ledger.stage_turn([_observation(0), AIMessage(content="no tool call this turn")])
_run_turns(ledger, chunker, 1, 7, _hashes(4)) # commits turn 0 without a step key
assert [c.capsule_key for c in chunker.awaiting_chunks] == ["chunk:1-4"]
assert all(entry["chunk"] is not None for entry in chunker._awaiting)
meter["value"] = 75_000
capsule.resolve("chunk:1-4", _capsule(1, 4))
_run_turns(ledger, chunker, 8, 8, _hashes(4))
@@ -1278,24 +1290,79 @@ def test_partition_folds_leading_unstamped_turns_into_the_first_segment():
]
def test_start_gate_zero_restores_swap_on_ready():
def test_start_gate_zero_only_prepares_and_does_not_rewrite_prefix():
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
ledger, chunker, _, _ = _make(
steps, min_active=2, meter=lambda: 1_000, auto_capsule=True, start_ratio=0.0
)
_run_turns(ledger, chunker, 1, 8, _hashes(4))
assert len(chunker.chunks) == 1
assert chunker.awaiting_chunks == ()
assert chunker.chunks == ()
assert chunker.awaiting_chunks[0].status == "ready"
def test_start_gate_unknown_base_falls_through_to_swap_on_ready():
"""No measured operator call (meter None, nothing recorded on the ledger):
the gate cannot decide and the legacy swap-on-ready behavior stands."""
def test_unknown_usage_uses_current_prompt_estimate_and_preserves_small_prefix():
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
ledger, chunker, _, _ = _make(steps, min_active=2, auto_capsule=True)
assert ledger.last_prompt_tokens is None
chunker._meter_getter = None
_run_turns(ledger, chunker, 1, 8, _hashes(4))
assert len(chunker.chunks) == 1
assert ledger.last_prompt_tokens is None
assert ledger.projected_prompt_tokens > 0
assert chunker.chunks == ()
assert ledger.context_phase == 0
def test_phase_batches_async_summaries_then_preserves_the_new_prefix():
steps = [_step(i) for i in range(1, 20)]
meter = {"value": 10_000}
ledger, chunker, _, capsule = _make(steps, meter=lambda: meter["value"])
ledger.set_static_prefix([HumanMessage(content="Stable rules")])
_run_turns(ledger, chunker, 1, 10, lambda i: "a" if i <= 4 else "b" if i <= 7 else "c")
assert capsule.submitted == [] # no background LLM work below preparation gate
tail = [_observation(11)]
original = ledger.render(tail)
fingerprint = [message.model_dump() for message in original]
meter["value"] = 40_000
ledger.render(tail)
assert len(chunker.awaiting_chunks) == 2
first, second = chunker.awaiting_chunks
capsule.resolve(first.capsule_key, _capsule(first.start_step_number, first.end_step_number))
meter["value"] = 75_000
assert [m.model_dump() for m in ledger.render(tail)] == fingerprint
assert ledger.context_phase == 0 # partial completion must not break the prefix
capsule.resolve(second.capsule_key, _capsule(second.start_step_number, second.end_step_number))
compacted = ledger.render(tail)
assert ledger.context_phase == 1
assert ledger.last_compaction["after_tokens"] < ledger.last_compaction["before_tokens"]
assert [m.model_dump() for m in original] == fingerprint # prior input objects never mutated
assert len(chunker.chunks) == 2 and not chunker.awaiting_chunks
meter["value"] = 10_000 # provider confirms lower occupancy after compaction
# Commit the previous turn, then append a newly observed turn.
ledger.commit_staged(step_key="s10")
next_input = ledger.render(tail)
snapshot = [m.model_dump() for m in next_input]
turn = _turn(11)
turn[0] = tail[0]
ledger.stage_turn(turn)
ledger.commit_staged(step_key="s11")
following = ledger.render([_observation(12)])
assert [m.model_dump() for m in following[: len(snapshot)]] == snapshot
assert ledger.context_phase == 1
assert compacted[0].content == "Stable rules"
def test_new_large_tail_triggers_pressure_without_waiting_for_next_usage():
steps = [_step(i) for i in range(1, 12)]
ledger, chunker, _, _ = _make(steps, auto_capsule=True)
chunker._meter_getter = None
previous = _run_turns(ledger, chunker, 1, 9, _hashes(4))
ledger.record_prompt_tokens(10_000, previous)
assert ledger.context_phase == 0
# New evidence already exceeds the hard line, despite a small last call.
ledger.render([HumanMessage(content="新证据" * 40_000)])
assert ledger.projected_prompt_tokens > 90_000
assert ledger.context_phase == 1
assert ledger.frozen_blocks
def _big_turn(i: int, chars: int = 4000) -> list:
@@ -1323,7 +1390,7 @@ def test_hard_threshold_swaps_ready_prefix_as_l2_when_that_is_enough():
estimated context back under the hard line, the swap is an ordinary L2
event — no fold, and the younger pending segment stays awaiting."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
# Steps 1–4 (hash-a) close at the switch; steps 5–8 (hash-b) close at the
# next switch. Turns are ~1k tokens each so the L2 swap is worth ~4k.
@@ -1352,7 +1419,7 @@ def test_hard_threshold_escalates_to_fold_when_l2_swap_is_not_enough():
and the whole frozen region folds into one recall-only era whose index
lists every folded step — the pending chunk's steps too."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 10, lambda i: "a" if i <= 4 else ("b" if i <= 8 else "c"))
capsule.resolve("chunk:1-4", _capsule(1, 4))
@@ -1394,9 +1461,10 @@ def test_hard_fold_is_monotonic_later_l2_swaps_append_after_the_folded_era():
flips back to full chunk blocks."""
steps = [_step(i) for i in range(1, 17)]
hash_for = lambda i: "a" if i <= 4 else ("b" if i <= 8 else ("c" if i <= 12 else "d")) # noqa: E731
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, _, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 10, hash_for)
meter["value"] = 75_000
capsule.resolve("chunk:1-4", _capsule(1, 4))
meter["value"] = 99_000
_run_turns(ledger, chunker, 11, 11, hash_for) # hard: folds 1–4 and 5–8
@@ -1405,10 +1473,11 @@ def test_hard_fold_is_monotonic_later_l2_swaps_append_after_the_folded_era():
# Pressure gone (above the start gate, below soft): the next milestone
# switch closes 9–12; once ready it swaps as an ordinary L2 chunk.
meter["value"] = 50_000
meter["value"] = 40_000
_run_turns(ledger, chunker, 12, 15, hash_for)
pending = [c for c in chunker.awaiting_chunks if c.status == "pending"]
assert [c.capsule_key for c in pending] == ["chunk:9-12"]
meter["value"] = 75_000
capsule.resolve("chunk:9-12", _capsule(9, 12))
_run_turns(ledger, chunker, 16, 16, hash_for)
@@ -1430,11 +1499,13 @@ def test_hard_threshold_with_nothing_awaiting_folds_frozen_region_once():
into a recall-only era without consuming turns, and repeated hard renders
leave the frozen blocks byte-identical."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 13)]
meter = {"value": 50_000}
meter = {"value": 40_000}
ledger, chunker, _, _ = _make(
steps, min_active=2, meter=lambda: meter["value"], auto_capsule=True
)
_run_turns(ledger, chunker, 1, 7, _hashes(4))
_run_turns(ledger, chunker, 1, 6, _hashes(4))
meter["value"] = 75_000
_run_turns(ledger, chunker, 7, 7, _hashes(4))
# Chunk 1–4 swapped as L2 at the step-7 render; s5..s6 remain = the floor.
assert [c.capsule_key for c in chunker.chunks] == ["chunk:1-4"]
assert chunker.awaiting_chunks == ()
@@ -1479,14 +1550,15 @@ def test_context_base_reads_the_ledger_operator_measurement_not_the_session_mete
# Some other agent's small call lands on the session meter.
get_meter("session-1").record({"prompt_tokens": 2_700, "completion_tokens": 10})
# The operator's own measured prompt is well below the gate.
ledger.record_prompt_tokens(20_000)
assert chunker._context_base_tokens(ledger) == 20_000
chunker._meter_getter = None
ledger.record_prompt_tokens(40_000)
assert chunker._context_base_tokens(ledger) == 40_000
_run_turns(ledger, chunker, 1, 8, _hashes(4))
assert chunker.chunks == () # held: 20k < 35k
assert [c.status for c in chunker.awaiting_chunks] == ["ready"]
# Operator prompt grows past the gate → swap at the next render.
ledger.record_prompt_tokens(36_000)
ledger.record_prompt_tokens(75_000)
_run_turns(ledger, chunker, 9, 9, _hashes(4))
assert len(chunker.chunks) == 1
@@ -1507,7 +1579,7 @@ def test_held_note_is_fresh_after_a_retry():
then readied while the start gate still holds it gets a fresh ``held``
note: the re-dispatch resets the once-per-chunk held marker."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 14)]
meter = {"value": 20_000} # < 0.35 * 100k: the gate holds ready chunks
meter = {"value": 40_000} # < 0.35 * 100k: the gate holds ready chunks
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 8, _hashes(4))
entry = chunker._awaiting[0]
@@ -1797,7 +1869,7 @@ def test_calibrated_ratio_scales_the_size_trigger():
# Default ratio: the eligible text (two ~4k-char turns once turn 5 is
# staged) is worth ~chars/4 tokens, under the 5k target → nothing closes.
ledger, chunker, _, _ = _make(
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000, meter=lambda: 40_000
)
ledger.record_prompt_tokens(1000, messages=[HumanMessage(content="a" * 4000)])
_run_big_turns(ledger, chunker, 1, 5, lambda i: "hash-a")
@@ -1807,7 +1879,7 @@ def test_calibrated_ratio_scales_the_size_trigger():
# Calibrated to 1 char/token: the same text is worth ~chars tokens → closes.
ledger, chunker, _, _ = _make(
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000
steps, min_active=2, min_steps=2, max_steps=100, target_tokens=5000, meter=lambda: 40_000
)
ledger.record_prompt_tokens(4000, messages=[HumanMessage(content="中" * 4000)])
assert ledger.chars_per_token == pytest.approx(1.0)
@@ -1840,6 +1912,7 @@ def test_compression_announces_one_tool_line_from_dispatch_to_swap():
assert args["source_tokens"] > 0
assert "context_tokens" not in args
meter["value"] = 75_000
capsule.resolve("chunk:1-4", _capsule(1, 4))
ledger.commit_staged(step_key="s8", validator_result={"status": "dispatched"})
ledger.render([_observation(9)])
@@ -1853,16 +1926,14 @@ def test_compression_announces_one_tool_line_from_dispatch_to_swap():
assert done_args["summary_tokens"] > 0
assert done_args["context_budget"] == 100_000
assert done_args["context_estimated"] is True
assert done_args["context_tokens"] == (
60_000 - done_args["source_tokens"] + done_args["summary_tokens"]
)
assert 0 < done_args["context_tokens"] < 75_000
assert "forced" not in done_args
def test_forced_swap_announces_fold_without_a_ratio():
"""Forced folds report estimated context usage without a summary size."""
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 9)]
meter = {"value": None}
meter = {"value": 40_000}
ledger, chunker, engine, _ = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 8, _hashes(4))
assert any(t["status"] == "running" for t in engine.traces)
@@ -1896,7 +1967,7 @@ def test_compression_trace_carries_a_plain_language_phase_on_every_path():
assert COMPRESSION_PHASES == ("summarizing", "ready", "applied", "failed")
steps = [_step(i, "hash-a" if i <= 4 else "hash-b") for i in range(1, 15)]
meter = {"value": 20_000} # < 0.35 * 100k: the start gate holds ready chunks
meter = {"value": 40_000} # < 0.35 * 100k: the start gate holds ready chunks
ledger, chunker, engine, capsule = _make(steps, min_active=2, meter=lambda: meter["value"])
_run_turns(ledger, chunker, 1, 8, _hashes(4))
chunk = chunker.awaiting_chunks[0]
@@ -1933,9 +2004,14 @@ def test_compression_trace_carries_a_plain_language_phase_on_every_path():
assert held["payload"]["args"]["note"] == "held"
# Context past the gate → the swap applies the summary.
meter["value"] = 40_000
for pending in chunker.awaiting_chunks:
capsule.resolve(
pending.capsule_key, _capsule(pending.start_step_number, pending.end_step_number)
)
meter["value"] = 75_000
_run_turns(ledger, chunker, 13, 13, lambda i: "hash-c")
assert [c.start_step_number for c in chunker.chunks] == [1]
assert chunker.chunks[0].start_step_number == 1
assert not chunker.awaiting_chunks
assert mine()[-1] == ("success", "applied")
# Every announcement on every trace carries a known phase.
+39 -42
View File
@@ -71,9 +71,19 @@ def _turn(i: int, tool_call_count: int = 1) -> list:
return messages
def _play_turn(ledger: TranscriptLedger, i: int, *, prev_key=None, prev_result=None, **turn_kwargs):
def _play_turn(
ledger: TranscriptLedger,
i: int,
*,
prev_key=None,
prev_result=None,
compact=False,
**turn_kwargs,
):
"""Mimic the operator's real ordering: commit previous, render, stage."""
ledger.commit_staged(step_key=prev_key, validator_result=prev_result)
if compact:
ledger.compact_active()
turn = _turn(i, **turn_kwargs)
rendered = ledger.render([turn[0]])
ledger.stage_turn(turn)
@@ -159,8 +169,8 @@ def test_recitation_and_ui_list_stripped_at_the_text_edge_and_screenshot_at_k():
until depth K and is then resolved in place; only the live tail ever
carries an indexed element list."""
ledger = TranscriptLedger(step_memory=_service(), image_scrub_depth=3)
_play_turn(ledger, 1)
rendered = _play_turn(ledger, 2, prev_key="step-1")
_play_turn(ledger, 1, compact=True)
rendered = _play_turn(ledger, 2, compact=True, prev_key="step-1")
obs1 = ledger.active_messages[0]
# Turn 1 is now depth 2: text edge passed, screenshot kept.
assert PLAN_RECITATION_MARKER not in str(obs1.content)
@@ -168,10 +178,10 @@ def test_recitation_and_ui_list_stripped_at_the_text_edge_and_screenshot_at_k():
assert _has_image(obs1)
original_obs1 = _fingerprint(obs1)
rendered = _play_turn(ledger, 3, prev_key="step-2")
rendered = _play_turn(ledger, 3, compact=True, prev_key="step-2")
# Turn 1 reached depth 3: the screenshot is resolved.
assert not _has_image(obs1)
assert _fingerprint(obs1) != original_obs1
assert not _has_image(ledger.active_messages[0])
assert _fingerprint(ledger.active_messages[0]) != original_obs1
# Turn 2 (depth 2): text edge passed, screenshot still there; the live tail keeps both.
obs2 = next(m for m in ledger.active_messages if "OBSERVATION [T+00:02]" in str(m.content))
assert PLAN_RECITATION_MARKER not in str(obs2.content)
@@ -189,7 +199,7 @@ def test_depth_k_image_resolved_to_ready_visual_summary():
ledger = TranscriptLedger(step_memory=service, image_scrub_depth=3)
for i in range(1, 5):
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
first_obs = ledger.active_messages[0]
assert not _has_image(first_obs)
@@ -212,7 +222,7 @@ def test_pending_grace_then_placeholder_never_backfilled():
ledger = TranscriptLedger(step_memory=service, image_scrub_depth=2, pending_grace_steps=1)
# Enough turns to push turn 1 past K + grace.
for i in range(1, 7):
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
first_obs = ledger.active_messages[0]
assert not _has_image(first_obs)
@@ -221,7 +231,7 @@ def test_pending_grace_then_placeholder_never_backfilled():
# A late summary must never mutate the frozen message.
service._summaries["step-1"] = "Too late."
_play_turn(ledger, 7, prev_key="step-6")
_play_turn(ledger, 7, compact=True, prev_key="step-6")
assert _fingerprint(ledger.active_messages[0]) == frozen
@@ -232,7 +242,7 @@ def test_failed_summary_becomes_unavailable_placeholder():
ledger = TranscriptLedger(step_memory=service, image_scrub_depth=2, pending_grace_steps=5)
for i in range(1, 4):
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
first_obs = ledger.active_messages[0]
assert not _has_image(first_obs)
@@ -473,7 +483,7 @@ def test_relaxed_depth_below_start_gate_keeps_more_screenshots():
assert ledger.occupancy is None
assert ledger.effective_image_scrub_depth == 6
for i in range(1, 10):
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
# At the render of turn 9 its observation is the live tail (depth 1);
# committed turns 8..5 are depths 2..5 and turn 4 (depth 6) is scrubbed.
ledger.record_prompt_tokens(20_000) # 20% < 35%
@@ -482,34 +492,34 @@ def test_relaxed_depth_below_start_gate_keeps_more_screenshots():
tight = TranscriptLedger(step_memory=_service(), image_scrub_depth=3)
for i in range(1, 10):
_play_turn(tight, i, prev_key=f"step-{i - 1}" if i > 1 else None)
_play_turn(tight, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
assert _image_turns(tight) == [8]
def test_crossing_the_start_gate_tightens_once_and_relaxing_never_backfills():
ledger = _banded_ledger()
for i in range(1, 10):
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
_play_turn(ledger, i, compact=True, prev_key=f"step-{i - 1}" if i > 1 else None)
assert _image_turns(ledger) == [5, 6, 7, 8]
# Past the gate: the next render scrubs the extra depths in one pass
# (turns 5–8 were depths 3–6 at that render; only turn 9 at depth 2 stays).
ledger.record_prompt_tokens(40_000) # 40% >= 35%
assert ledger.effective_image_scrub_depth == 3
_play_turn(ledger, 10, prev_key="step-9")
_play_turn(ledger, 10, compact=True, prev_key="step-9")
assert _image_turns(ledger) == [9]
# Back under the gate: nothing is restored; new turns accumulate again
# until four historical screenshots are in view.
ledger.record_prompt_tokens(20_000)
assert ledger.effective_image_scrub_depth == 6
_play_turn(ledger, 11, prev_key="step-10")
_play_turn(ledger, 11, compact=True, prev_key="step-10")
assert _image_turns(ledger) == [9, 10]
_play_turn(ledger, 12, prev_key="step-11")
_play_turn(ledger, 12, compact=True, prev_key="step-11")
assert _image_turns(ledger) == [9, 10, 11]
_play_turn(ledger, 13, prev_key="step-12")
_play_turn(ledger, 13, compact=True, prev_key="step-12")
assert _image_turns(ledger) == [9, 10, 11, 12]
_play_turn(ledger, 14, prev_key="step-13") # turn 9 reaches depth 6
_play_turn(ledger, 14, compact=True, prev_key="step-13") # turn 9 reaches depth 6
assert _image_turns(ledger) == [10, 11, 12, 13]
@@ -523,10 +533,8 @@ def test_no_band_configuration_keeps_the_fixed_depth():
assert relaxed_only.effective_image_scrub_depth == 3 # no gate → never relaxed
def test_scrub_edge_runs_before_the_chunker_at_render():
"""The chunker sees the active window after the scrub edge advanced, so a
chunk closing over turns older than the floor never captures a live
screenshot in its source transcript."""
def test_chunker_sees_immutable_raw_history_before_a_phase_boundary():
"""Preparing capsules must not scrub any previously sent observation."""
ledger = _banded_ledger()
seen: list[list[int]] = []
@@ -537,9 +545,9 @@ def test_scrub_edge_runs_before_the_chunker_at_render():
ledger.attach_chunker(Probe())
for i in range(1, 9):
_play_turn(ledger, i, prev_key=f"step-{i - 1}" if i > 1 else None)
# At the render of turn 8 the scrub edge (depth 6) had already resolved
# turn 3 (depth 6); the probe saw turns 4–7 with images, never turn 3.
assert seen[-1] == [4, 5, 6, 7]
# A render below pressure preserves every historical image.
assert seen[-1] == list(range(1, 8))
assert ledger.context_phase == 0
# ---------------------------------------------------------------------------
@@ -580,6 +588,7 @@ def test_ephemeral_blocks_vanish_at_the_text_edge_and_not_before():
ledger.stage_turn(turn_with_notice(1))
for i in range(2, 4):
ledger.commit_staged(step_key=f"step-{i - 1}")
ledger.compact_active()
ledger.render([_observation(i)])
ledger.stage_turn(turn_with_notice(i))
@@ -600,15 +609,12 @@ def test_ephemeral_blocks_vanish_at_the_text_edge_and_not_before():
def test_rendering_past_the_edge_is_byte_identical_and_prefix_stable():
"""(iii) A render with nothing new is byte-identical to the previous one,
and across turns exactly one message — the observation reaching depth K —
differs from the previous render's prefix, so the request prefix before
it is what the provider's prompt cache already holds."""
"""Every full previous prompt, including its live tail, survives unchanged."""
ledger = TranscriptLedger(step_memory=_ready_service(12), image_scrub_depth=3)
ledger.set_static_prefix([SystemMessage(content="STATIC")])
def snapshot(rendered: list) -> list[str]:
return [_fingerprint(m) for m in rendered[:-1]] # everything but the live tail
return [_fingerprint(m) for m in rendered] # includes the previously live observation
previous: list[str] = []
for i in range(1, 11):
@@ -620,24 +626,15 @@ def test_rendering_past_the_edge_is_byte_identical_and_prefix_stable():
)
current = snapshot(rendered)
changed = [idx for idx, fp in enumerate(previous) if current[idx] != fp]
if i >= 3:
# The live tail is depth 1 and committed turn i-1 depth 2, so the
# turn reaching depth 3 is i-2; its observation heads its span.
expected = next(
idx
for idx, m in enumerate(rendered[:-1])
if f"OBSERVATION [T+00:0{(i - 2) % 10}]" in str(m.content)
)
assert changed == [expected], (i, changed)
else:
assert changed == [], (i, changed)
assert changed == [], (i, changed)
previous = current
# Same state, rendered again: byte-identical, including the tail.
ledger.commit_staged(step_key="step-10", validator_result={"status": "dispatched"})
tail = [_observation(11)]
again = [_fingerprint(m) for m in ledger.render(tail)]
assert again == [_fingerprint(m) for m in ledger.render(tail)]
assert again[:-1] == previous
assert again[: len(previous)] == previous
# ---------------------------------------------------------------------------