perf: preserve conversation prefixes across agent turns
This commit is contained in:
1 parent
1440a1f508
commit
f1bb5d02aa
19 files changed
+552
-205
No files matched your search
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user