diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 354de4116010..bb0cac508316 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -410,8 +410,13 @@ def note_turn_start(agent, turn_id: str): # Cross-agent leg: same session_id in flight under a different agent # object means two routing keys resolve to one durable session — the # busy guard (keyed by routing key) cannot see this overlap at all. + # Persist-disabled agents (background-review forks) deliberately share + # the live parent's session_id for prompt-cache warmth but can never + # write to the transcript — they must not register here (would warn a + # false overlap against the parent's real turn) nor pop the parent's + # slot at their persist (note_turn_persisted skips them symmetrically). session_id = getattr(agent, "session_id", None) - if session_id: + if session_id and not getattr(agent, "_persist_disabled", False): now = time.time() with _INFLIGHT_TURNS_LOCK: entry = _INFLIGHT_TURNS_BY_SESSION.get(session_id) @@ -443,12 +448,17 @@ def note_turn_persisted(agent): and the tripwire under-reports instead of double-reporting. A diagnostic must never be noisier than the defect it hunts.""" agent._inflight_turn_id = None - session_id = getattr(agent, "_inflight_turn_session_id", None) or getattr( - agent, "session_id", None - ) - if session_id: - with _INFLIGHT_TURNS_LOCK: - _INFLIGHT_TURNS_BY_SESSION.pop(session_id, None) + # Symmetric with note_turn_start's cross-agent leg: persist-disabled + # forks never registered a session slot, and their persist funnel still + # runs — popping here would steal the live parent turn's slot and make + # the tripwire under-report the real overlap it exists to catch. + if not getattr(agent, "_persist_disabled", False): + session_id = getattr(agent, "_inflight_turn_session_id", None) or getattr( + agent, "session_id", None + ) + if session_id: + with _INFLIGHT_TURNS_LOCK: + _INFLIGHT_TURNS_BY_SESSION.pop(session_id, None) agent._inflight_turn_session_id = None diff --git a/tests/agent/test_turn_overlap_tripwire.py b/tests/agent/test_turn_overlap_tripwire.py index 314c8fffa81a..736983cce9eb 100644 --- a/tests/agent/test_turn_overlap_tripwire.py +++ b/tests/agent/test_turn_overlap_tripwire.py @@ -144,3 +144,36 @@ def test_crashed_cross_agent_turn_warns_once_then_recovers(caplog): note_turn_persisted(agent_b) note_turn_start(agent_c, "s1:t3:cccc") # clean again assert len(caplog.records) == 1 + + +def test_persist_disabled_fork_neither_registers_nor_warns(caplog): + """Background-review forks share the live parent's session_id for + prompt-cache warmth but are _persist_disabled — they can never write + to the transcript, so they must not trip the cross-agent warning + against the parent's real in-flight turn (in either direction).""" + parent, fork = _FakeAgent(), _FakeAgent() + fork._persist_disabled = True + with caplog.at_level(logging.WARNING, logger="agent.agent_runtime_helpers"): + note_turn_start(parent, "s1:t1:aaaa") # real turn in flight + assert note_turn_start(fork, "s1:tr:ffff") is None # fork: silent + note_turn_persisted(fork) # fork's funnel still runs + # Reverse order on the next cycle: fork in flight, then real turn. + note_turn_persisted(parent) + note_turn_start(fork, "s1:tr:gggg") + assert note_turn_start(parent, "s1:t2:bbbb") is None + assert not caplog.records + + +def test_persist_disabled_fork_persist_does_not_steal_parent_slot(caplog): + """The fork's persist funnel still runs; it must not pop the parent's + session slot, or a real cross-agent overlap right after a review fork + would go unreported.""" + parent, fork, intruder = _FakeAgent(), _FakeAgent(), _FakeAgent() + fork._persist_disabled = True + with caplog.at_level(logging.WARNING, logger="agent.agent_runtime_helpers"): + note_turn_start(parent, "s1:t1:aaaa") # real turn holds the slot + note_turn_start(fork, "s1:tr:ffff") + note_turn_persisted(fork) # must NOT release s1 + prev = note_turn_start(intruder, "s1:t2:bbbb") + assert prev == "s1:t1:aaaa" + assert len(caplog.records) == 1