ouroboros/tests/test_loop_misc.py
Ouroboros c5cd1e4479 P1-fix5b: Keep a relayed sibling message out of the principal's own words
Owner-forwarded Codex audit, finding 2. The P1-6 capture branch recorded every
KIND_TASK_MESSAGE whose provenance is not system or descendant_task as
principal_task_message, so a sibling's message relayed by the parent
(provenance peer_via_ancestor, produced by forward_to_worker with
relayed_from_task_id) entered the shared plan and acceptance corpus as the
principal's directive, with no provenance and no relay ids, while the dialogue
renderer does distinguish it.

The typed provenance the host already resolved now rides the row. The capture
branch derives the source from it (principal_task_message for the parent's own
words, relayed_peer_message for a relay) and passes the typed origin ids
through _record_owner_directive, the one provenance-preserving list;
_accept_owner_directives, the single producer both review surfaces read, keeps
those ids on the row it projects, and _render_directives prints them, so a
relayed row reads [relayed_peer_message . from task X relayed by Y . msg] while
a parent's own message keeps its plain label. No classifier, no keyword rule,
no second list, and no message is dropped from the corpus: a relayed proposal
stays context a reviewer should see, with its real source.

Tests: tests/test_loop_misc.py (the capture test gains a peer_via_ancestor
frame) and tests/test_plan_review_w3.py
test_a_relayed_sibling_message_names_its_real_source_in_the_corpus_and_the_packet,
both failing before this commit. Docs: the corpus clause in
docs/ARCHITECTURE.md names the relayed row.

(cherry picked from commit 0d3b733b6b0ce4d04e4336afe5ed18e530cbfdce)
2026-09-12 18:53:51 +03:00

560 lines
24 KiB
Python

"""Loop miscellaneous regressions: the message stream and pacing seams.
Consolidated from former ``test_loop_incoming_messages.py`` (image payload
preservation) and ``test_loop_skill_finalization.py``, then divided by theme
(the v7 L-B test split): the task-acceptance gate lives in
``test_loop_acceptance_gate.py``, the self-authored skill finalization gate in
``test_loop_skill_finalization.py``, the ``run_llm_loop`` round/finalization
suite in ``test_run_llm_loop.py`` and the image auto-attach seam in
``test_loop_image_attach.py``.
Kept as the home for loop micro-regressions that do not justify a standalone
file: message draining, owner directives, self-check and pacing injections,
the final-answer latch, the provider-death retry wall and the per-task
web gate.
"""
from __future__ import annotations
import json
import queue
from types import SimpleNamespace
import pytest
import ouroboros.loop as loop_mod
from ouroboros.loop_llm_call import RETRY_WALL_EXHAUSTED_KEY
from ouroboros.loop import (
run_llm_loop,
)
from ouroboros.loop_acceptance import _latch_final_answer_marker, _server_web_allowed_by_task
from ouroboros.loop_messages import _initialize_owner_directives
from ouroboros.loop_nudges import _maybe_inject_self_check, _maybe_inject_time_budget_milestone
from ouroboros.loop_round_limits import _drain_incoming_messages
# ---------------------------------------------------------------------------
# _drain_incoming_messages — telegram image payload preservation
# ---------------------------------------------------------------------------
def test_drain_incoming_messages_preserves_image_payload():
messages: list = []
incoming_messages: queue.Queue = queue.Queue()
incoming_messages.put({
"text": "photo from telegram",
"image_base64": "aW1hZ2U=",
"image_mime": "image/png",
"image_caption": "photo from telegram",
})
_drain_incoming_messages(
messages=messages,
incoming_messages=incoming_messages,
drive_root=None,
task_id="",
event_queue=None,
_owner_msg_seen=set(),
)
assert len(messages) == 1
assert messages[0]["role"] == "user"
content = messages[0]["content"]
assert isinstance(content, list)
assert content[0]["type"] == "text"
assert content[0]["text"] == "[Message from my human]: photo from telegram"
assert content[1]["type"] == "image_url"
assert content[1]["image_url"]["url"] == "data:image/png;base64,aW1hZ2U="
def test_owner_directives_survive_compaction_without_control_prose(tmp_path):
from ouroboros import task_pacing
from ouroboros.deadline_utils import parse_deadline_ts
from ouroboros.owner_mailbox import (
KIND_FINALIZE_NOW,
drain_owner_entries,
write_owner_message,
)
ctx = SimpleNamespace()
messages = [
{"role": "system", "content": "policy"},
{"role": "user", "content": "Initial requirement verbatim"},
]
_initialize_owner_directives(ctx, messages)
incoming: queue.Queue = queue.Queue()
incoming.put({"text": "direct follow-up", "client_message_id": "direct-1"})
write_owner_message(tmp_path, "mailbox follow-up", task_id="root", msg_id="mail-1")
write_owner_message(
tmp_path, "deadline control", task_id="root", msg_id="control-1",
kind=KIND_FINALIZE_NOW,
)
control_entry = next(
row for row in drain_owner_entries(tmp_path, "root")
if row["msg_id"] == "control-1"
)
expected_deadline = (
parse_deadline_ts(control_entry["ts"]).timestamp()
+ task_pacing.effective_finalization_reserve_sec(ctx)
)
controls = _drain_incoming_messages(
messages,
incoming,
tmp_path,
"root",
None,
set(),
owner_ctx=ctx,
)
assert set(controls) == {"finalize_now", "finalize_deadline_ts"}
assert controls["finalize_now"] == "deadline control"
assert controls["finalize_deadline_ts"] == expected_deadline
assert [row["source"] for row in ctx._owner_directives] == [
"initial_user", "direct_incoming", "owner_mailbox",
]
assert ctx._owner_directives[0]["content"] == "Initial requirement verbatim"
assert ctx._owner_directives[1]["msg_id"] == "direct-1"
assert ctx._owner_directives[2] == {
"source": "owner_mailbox",
"content": "mailbox follow-up",
"msg_id": "mail-1",
}
assert "deadline control" not in json.dumps(ctx._owner_directives)
def test_maybe_inject_self_check_handles_assistant_none_content():
messages = [
{"role": "user", "content": "inspect"},
{
"role": "assistant",
"content": None,
"tool_calls": [{
"id": "call-1",
"type": "function",
"function": {"name": "read_file", "arguments": "{}"},
}],
},
{"role": "tool", "tool_call_id": "call-1", "content": "done"},
]
progress = []
injected = _maybe_inject_self_check(
15,
30,
messages,
{"cost": 0.0},
progress.append,
)
assert injected is True
assert messages[-1]["role"] == "user"
assert "[CHECKPOINT 1" in messages[-1]["content"]
assert progress
def test_time_budget_milestone_injects_once_per_threshold(monkeypatch):
messages = [{"role": "user", "content": "solve"}]
ctx = SimpleNamespace(
task_metadata={
"created_at": "2026-06-10T00:00:00Z",
"deadline_at": "2026-06-10T10:00:00Z",
},
)
from datetime import datetime, timezone
monkeypatch.setattr("ouroboros.task_pacing.utc_now", lambda: datetime(2026, 6, 10, 5, 1, tzinfo=timezone.utc))
injected = _maybe_inject_time_budget_milestone(
messages,
SimpleNamespace(_ctx=ctx),
event_queue=None,
task_id="task-time",
drive_logs=None,
)
injected_again = _maybe_inject_time_budget_milestone(messages, SimpleNamespace(_ctx=ctx))
assert injected is True
assert injected_again is False
assert "[TIME BUDGET" in messages[-1]["content"]
assert "50% remaining" in messages[-1]["content"]
assert ctx._time_budget_milestones_seen == {"50%"}
def test_intrinsic_pacing_injects_without_deadline(monkeypatch):
"""No deadline_at: surface elapsed/rounds/cost once per interval bucket.
v6.60.0: the FINAL ANSWER phrase appears ONLY when the task contract declares
answer_protocol="final_answer_line" (marker phrases are protocol-gated)."""
messages = [{"role": "user", "content": "solve"}]
ctx = SimpleNamespace(task_metadata={"created_at": "2026-06-10T00:00:00Z"}) # no deadline_at
from datetime import datetime, timezone
monkeypatch.delenv("OUROBOROS_PACING_INTERVAL_SEC", raising=False)
# 20 min elapsed, default interval 600s -> bucket 2.
monkeypatch.setattr("ouroboros.task_pacing.utc_now", lambda: datetime(2026, 6, 10, 0, 20, tzinfo=timezone.utc))
injected = _maybe_inject_time_budget_milestone(
messages, SimpleNamespace(_ctx=ctx), round_idx=7,
accumulated_usage={"cost": 1.25}, task_id="t",
)
injected_again = _maybe_inject_time_budget_milestone(
messages, SimpleNamespace(_ctx=ctx), round_idx=8, accumulated_usage={"cost": 1.4},
)
assert injected is True
assert injected_again is False # same bucket -> not repeated
assert "[PACING" in messages[-1]["content"]
assert "Rounds so far: 7" in messages[-1]["content"]
assert "FINAL ANSWER:" not in messages[-1]["content"] # no protocol declared
# With the protocol declared, the salvage phrase rides the SAME milestone.
proto_ctx = SimpleNamespace(
task_metadata={"created_at": "2026-06-10T00:00:00Z"},
task_contract={"answer_protocol": "final_answer_line"},
)
proto_messages = [{"role": "user", "content": "solve"}]
assert _maybe_inject_time_budget_milestone(
proto_messages, SimpleNamespace(_ctx=proto_ctx), round_idx=7,
accumulated_usage={"cost": 1.25}, task_id="t2",
) is True
assert "FINAL ANSWER:" in proto_messages[-1]["content"]
def test_latch_final_answer_marker_captures_explicit_marker_only():
trace = {"tool_calls": [{"tool": "read_file"}]}
_latch_final_answer_marker(trace, "analysis\nFINAL ANSWER: 123")
assert trace["best_valid_final_answer"] == "123"
assert trace["best_valid_final_answer_tools"] == 1
_latch_final_answer_marker(trace, "answer-ish prose without marker")
assert trace["best_valid_final_answer"] == "123"
def test_latch_final_answer_marker_counts_same_turn_tool_calls():
trace = {"tool_calls": [{"tool": "read_file"}]}
current = [{"function": {"name": "run_command"}}, {"function": {"name": "verify_and_record"}}]
_latch_final_answer_marker(trace, "FINAL ANSWER: draft", current_tool_calls=current)
assert trace["best_valid_final_answer"] == "draft"
# Same-turn tool calls are newer grounding and must invalidate this latch unless
# the model re-emits the marker after those tools complete.
assert trace["best_valid_final_answer_tools"] == 1
def test_server_web_allowed_respects_task_resource_contract():
assert _server_web_allowed_by_task(SimpleNamespace(task_contract={})) is True
assert _server_web_allowed_by_task(SimpleNamespace(task_contract={"allowed_resources": {"web": False}})) is False
assert _server_web_allowed_by_task(SimpleNamespace(task_contract={"allowed_resources": {"network": False}})) is False
assert _server_web_allowed_by_task(SimpleNamespace(task_contract={"disabled_tools": ["web_search"]})) is True
def test_intrinsic_pacing_disabled_when_interval_zero(monkeypatch):
messages = [{"role": "user", "content": "solve"}]
ctx = SimpleNamespace(task_metadata={"created_at": "2026-06-10T00:00:00Z"})
from datetime import datetime, timezone
monkeypatch.setenv("OUROBOROS_PACING_INTERVAL_SEC", "0")
monkeypatch.setattr("ouroboros.task_pacing.utc_now", lambda: datetime(2026, 6, 10, 1, 0, tzinfo=timezone.utc))
assert _maybe_inject_time_budget_milestone(messages, SimpleNamespace(_ctx=ctx), round_idx=3) is False
def test_deadline_local_finalize_gate(monkeypatch):
"""Self-finalize only when a REAL deadline is within the grace window."""
from datetime import datetime, timezone
captured = {}
def _fake_final(ctx, *, prompt, fallback_text, reason_code):
captured["reason_code"] = reason_code
return ("BEST EFFORT", {"reason_code": reason_code}, {})
monkeypatch.setattr(loop_mod, "_forced_final_answer", _fake_final)
# v6.54.4: the gate consults the task_pacing effective reserve SSOT.
monkeypatch.setattr("ouroboros.task_pacing.effective_finalization_reserve_sec", lambda ctx: 120.0)
monkeypatch.setattr(loop_mod, "utc_now", lambda: datetime(2026, 6, 10, 9, 59, 0, tzinfo=timezone.utc))
# Far from deadline (10:30 vs now 09:59 -> ~31 min left > 120s) -> no finalize.
far = SimpleNamespace(_ctx=SimpleNamespace(task_metadata={"deadline_at": "2026-06-10T10:30:00Z"}))
assert loop_mod._maybe_deadline_local_finalize(SimpleNamespace(), far) is None
# Within grace (10:00 vs now 09:59 -> 60s < 120s) -> finalize best-effort.
near = SimpleNamespace(_ctx=SimpleNamespace(task_metadata={"deadline_at": "2026-06-10T10:00:00Z"}))
result = loop_mod._maybe_deadline_local_finalize(SimpleNamespace(), near)
assert result is not None and result[0] == "BEST EFFORT"
assert captured["reason_code"] == "deadline_local"
# No deadline_at at all -> never fires (no synthesized deadline).
none_ctx = SimpleNamespace(_ctx=SimpleNamespace(task_metadata={}))
assert loop_mod._maybe_deadline_local_finalize(SimpleNamespace(), none_ctx) is None
# ---------------------------------------------------------------------------
# Skill finalization gate (self-authored skills must reach ready+enabled
# before the loop accepts a final text response)
# ---------------------------------------------------------------------------
def test_managed_unknown_waits_without_cross_model_fallback_until_deadline(tmp_path, monkeypatch):
from datetime import datetime, timedelta, timezone
from ouroboros import loop_transport
from ouroboros.tools.registry import ToolRegistry
class FakeLLM:
def default_model(self):
return "test-model"
calls = {"primary": 0, "fallback": 0}
def ambiguous(*args, **_kwargs):
calls["primary"] += 1
usage = args[10]
usage["_last_llm_error"] = "provider outcome unknown"
usage["_last_llm_error_kind"] = "provider_outcome_unknown"
usage["_last_llm_retry_same_request"] = False
return None, 0.0
def forbidden_fallback(**_kwargs):
calls["fallback"] += 1
raise AssertionError("unknown physical work must stop the paid chain")
monkeypatch.setattr(loop_mod, "call_llm_with_retry", ambiguous)
monkeypatch.setattr(loop_mod, "_run_cross_model_fallback_chain", forbidden_fallback)
monkeypatch.setenv("OUROBOROS_TASK_REVIEW_MODE", "off")
registry = ToolRegistry(repo_dir=tmp_path, drive_root=tmp_path)
waits = []
def expire_while_waiting(seconds, wake_check):
waits.append(seconds)
registry._ctx.task_metadata["deadline_at"] = (datetime.now(timezone.utc) - timedelta(seconds=1)).isoformat()
return False
monkeypatch.setattr(loop_transport, "interruptible_wait_sleep", expire_while_waiting)
result, usage, _trace = run_llm_loop(
messages=[{"role": "user", "content": "go"}],
tools=registry,
llm=FakeLLM(),
drive_logs=tmp_path,
emit_progress=lambda _text, *, incident=None: None,
incoming_messages=queue.Queue(),
task_id="unknown-provider-task",
drive_root=tmp_path,
)
assert calls == {"primary": 1, "fallback": 0}
assert len(waits) == 1
assert usage["_last_llm_error_kind"] == "provider_outcome_unknown"
assert "no retry or paid fallback" in result
# ---------------------------------------------------------------------------
# OB-01 — provider death does not re-burn the budget on a second forced call
# ---------------------------------------------------------------------------
def _provider_death_ctx(tmp_path, accumulated):
"""A minimal forced-rail context. ``tools=None`` on purpose: every forced
helper then takes its documented tool-less path, so the test observes the
loop's own decisions instead of a registry stub's."""
return loop_mod._RoundLimitContext(
messages=[
{"role": "user", "content": "do the thing"},
{"role": "assistant", "content": "PARTIAL RESULT: step one is done."},
],
llm=SimpleNamespace(),
active_model="openai/gpt-5.5",
active_effort="medium",
max_retries=3,
drive_logs=tmp_path,
task_id="task-provider-death",
round_idx=7,
event_queue=None,
accumulated_usage=accumulated,
task_type="task",
active_use_local=False,
max_rounds=200,
drive_root=tmp_path,
)
def _count_forced_helpers(monkeypatch):
"""Wrap the non-model work of the forced rail with call counters, keeping the
REAL implementations so "it still runs" is observed, not simulated."""
seen = {"model": [], "services": [], "drain": []}
real_services = loop_mod._finalize_forced_services
real_drain = loop_mod._drain_forced_owner_directives
def _model(ctx):
seen["model"].append(1)
return "FRESH FORCED ANSWER"
def _services(ctx, trace):
seen["services"].append(1)
return real_services(ctx, trace)
def _drain(ctx, trace):
seen["drain"].append(1)
return real_drain(ctx, trace)
monkeypatch.setattr(loop_mod, "_call_forced_model_once", _model)
monkeypatch.setattr(loop_mod, "_finalize_forced_services", _services)
monkeypatch.setattr(loop_mod, "_drain_forced_owner_directives", _drain)
return seen
def test_provider_death_skips_the_forced_call_when_the_retry_wall_is_spent(tmp_path, monkeypatch):
"""The whole point of OB-01: the transport already spent the same-model retry
wall, so the forced finalization must NOT re-burn the budget on a request that
cannot land. Everything the forced rail owns besides the call still runs."""
seen = _count_forced_helpers(monkeypatch)
accumulated = { # realistic transport stamps of an exhausted transient wall
RETRY_WALL_EXHAUSTED_KEY: True,
"execution_status": "infra_failed",
"reason_code": "llm_api_error",
"_last_llm_error_kind": "provider_transient",
}
ctx = _provider_death_ctx(tmp_path, accumulated)
messages_before = [dict(m) for m in ctx.messages]
text, usage, trace = loop_mod._handle_provider_unavailable(ctx)
assert seen["model"] == [] # ZERO llm calls from this rail
assert len(seen["services"]) == 1 # services still finalized
assert len(seen["drain"]) == 1 # exactly ONE directive drain
# Salvage still delivered, and the delivery candidate still packaged.
assert "PARTIAL RESULT: step one is done." in text
assert trace["forced_finalization"]["reason_code"] == "provider_unavailable"
# Replay durability: an unsent prompt must never reach the transcript, or a
# resume would read it as a request the model ignored.
assert [dict(m) for m in ctx.messages] == messages_before
assert not any(
"[PROVIDER_UNAVAILABLE]" in str(m.get("content") or "") for m in ctx.messages
)
# False completion: an outage is an INFRA FAILURE, and no model answer was
# extracted, so the best_effort gate must not be handed a typed success fact.
assert usage["reason_code"] == "provider_unavailable"
assert usage["execution_status"] == "infra_failed"
assert "_best_effort_extracted" not in usage
def test_provider_death_still_makes_the_forced_call_when_the_wall_is_unspent(tmp_path, monkeypatch):
"""The skip is not the new default. A PERMANENT refusal (auth/quota/bad
request) fails fast and leaves the wall unspent, so the forced rail keeps the
one chance its class is entitled to."""
seen = _count_forced_helpers(monkeypatch)
accumulated = {} # no marker: the transport never exhausted its retries
ctx = _provider_death_ctx(tmp_path, accumulated)
text, usage, _trace = loop_mod._handle_provider_unavailable(ctx)
assert seen["model"] == [1]
assert "FRESH FORCED ANSWER" in text
assert usage["execution_status"] == "infra_failed"
assert any(
"[PROVIDER_UNAVAILABLE]" in str(m.get("content") or "") for m in ctx.messages
)
@pytest.mark.parametrize(
"marker, expect_call",
[
("unexpected-string", False),
(1, False),
({"nested": "garbage"}, False),
(0, True),
("", True),
(None, True),
],
ids=["truthy_str", "truthy_int", "truthy_dict", "zero", "empty_str", "none"],
)
def test_malformed_retry_wall_marker_reads_as_a_plain_truth_value(
tmp_path, monkeypatch, marker, expect_call,
):
"""A shared usage dict can hold anything, so the read is `bool(...)` and
nothing else: any truthy value means the wall is spent, any falsy value means
it is not. No shape assumption, no crash, no silent third behaviour."""
seen = _count_forced_helpers(monkeypatch)
ctx = _provider_death_ctx(tmp_path, {
RETRY_WALL_EXHAUSTED_KEY: marker,
"execution_status": "infra_failed", "reason_code": "llm_api_error",
})
_text, usage, _trace = loop_mod._handle_provider_unavailable(ctx)
assert seen["model"] == ([1] if expect_call else [])
assert usage["execution_status"] == "infra_failed"
@pytest.mark.parametrize(
"reason_code, marker",
[
("round_limit", "[ROUND_LIMIT] wrap up"),
("finalization_grace", "[FINALIZE_NOW] wrap up"),
("deadline_local", "[DEADLINE] wrap up"),
("owner_requested_finalization", "[OWNER_STOP] wrap up"),
("budget_exhausted", "[BUDGET] wrap up"),
("children_unabsorbed", "[CHILDREN] wrap up"),
],
)
def test_other_forced_rails_never_skip_even_with_the_wall_marker_set(
tmp_path, monkeypatch, reason_code, marker,
):
"""The skip is gated on the provider-death rail SPECIFICALLY. Every OTHER
reason code `loop.py` passes to `_forced_final_answer` — round limit,
finalization grace, deadline, owner stop, budget exhaustion and unabsorbed
children — still makes its one forced call even when a transient wall was
spent earlier in the same task: those rails end for their own reasons, not
because the provider is unreachable. The list is exhaustive against the
`reason_code=` literals in loop.py, so a new rail cannot silently inherit
the skip."""
seen = _count_forced_helpers(monkeypatch)
ctx = _provider_death_ctx(tmp_path, {RETRY_WALL_EXHAUSTED_KEY: True})
text, _usage, _trace = loop_mod._forced_final_answer(
ctx, prompt=marker, fallback_text="fallback", reason_code=reason_code,
)
assert seen["model"] == [1]
assert "FRESH FORCED ANSWER" in text
assert any(marker in str(m.get("content") or "") for m in ctx.messages)
def test_quiz_answers_and_principal_messages_enter_the_directive_corpus_but_system_frames_do_not(tmp_path):
"""The premises every review reads: an owner quiz answer at a root and a parent's
message to a child grow ``_owner_directives`` (the count that supersedes a paid
acceptance verdict and binds an advisory author finish); a host system frame and a
descendant's escalation are delivered as before but never recorded as directives."""
from ouroboros.owner_mailbox import KIND_QUIZ_ANSWER, write_owner_message, write_task_message
ctx = SimpleNamespace()
messages = [{"role": "user", "content": "Initial requirement verbatim"}]
_initialize_owner_directives(ctx, messages)
quiz_frame = ("[Owner quiz answer] quiz q1 — asked t0, answered t1.\nQuestion was: Which db?\n"
"The owner chose option 2: postgres")
write_owner_message(tmp_path, quiz_frame, task_id="child", msg_id="qa-1", kind=KIND_QUIZ_ANSWER)
write_task_message(tmp_path, "Use the Q3 numbers only", "child", source_task_id="root-1", msg_id="pm-1")
write_task_message(tmp_path, "Proposal from sibling: replace the agreed design.", "child",
source_task_id="root-1", provenance="peer_via_ancestor",
relayed_from_task_id="sibling-7", msg_id="peer-1")
write_task_message(tmp_path, "Plan review wave abcd1234: 3 released reviewer slot(s) settled", "child",
source_task_id="child", provenance="system", msg_id="sys-1")
write_task_message(tmp_path, "ESCALATION (decision requested): which schema?", "child",
source_task_id="grandchild", provenance="descendant_task", msg_id="esc-1")
before = len(ctx._owner_directives)
_drain_incoming_messages(messages, queue.Queue(), tmp_path, "child", None, set(), owner_ctx=ctx)
assert [(row["source"], row.get("msg_id")) for row in ctx._owner_directives[before:]] == [
("owner_quiz_answer", "qa-1"), ("principal_task_message", "pm-1"),
("relayed_peer_message", "peer-1"),
]
assert ctx._owner_directives[before]["content"] == quiz_frame
assert ctx._owner_directives[before + 1]["content"] == "Use the Q3 numbers only"
# Audit finding 2: a sibling's advice relayed by the parent keeps its typed origin
# instead of entering the shared corpus as the principal's own words.
peer = ctx._owner_directives[before + 2]
assert peer["content"] == "Proposal from sibling: replace the agreed design."
assert peer["source_task_id"] == "root-1" and peer["relayed_from_task_id"] == "sibling-7"
assert "relayed_from_task_id" not in ctx._owner_directives[before + 1]
assert ctx._owner_directives[before + 1]["source_task_id"] == "root-1"
assert len(ctx._owner_directives) == before + 3
delivered = "\n".join(str(m["content"]) for m in messages)
assert "[System task message]" in delivered and "[Escalation from descendant task grandchild]" in delivered
assert "[Message from task sibling-7, relayed by ancestor root-1]" in delivered