mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
The supervisor asked a light model whether every new `schedule_subagent` child was "materially the same" as any active task and rejected it as `rejected_duplicate` on a positive answer. The judge never saw the selected subagent, model, lane or executor, so it rejected an intentional cross-model check (one objective sent to two configured subagents). In 46 days of live logs it adjudicated 147 spawns, rejected one (that false positive) and never caught a real duplicate; the only real duplicate in the window was a root task the gate cannot see. Each call ran synchronously on the supervisor main loop (2.4 s median, 20 s max), and a BudgetExceeded inside it dropped the whole schedule event. Capability change, owner-accepted 2026-09-14: admission no longer vetoes a child for resembling another task. Exact task-id fencing, the per-root active-child cap, depth caps and cost ceilings remain the floor; the parent decides what to spawn and already receives task_id, objective and subagent_id for every child. Residual risk, disclosed: a parent re-queued after a mid-wave crash may pay a wave twice, bounded by the cap and the per-tree ceiling. - supervisor/events_schedule_task.py: drop `_find_duplicate_task`, `_format_task_for_dedup` and the gate call; keep the cap logic. - supervisor/events.py: drop the re-export. - tests: drop the judge's unit tests and the 26 monkeypatch stubs that silenced it; add a regression test that identical siblings with different (and with identical) configured subagents are both admitted. - The `rejected_duplicate` status constant and its projections stay so old task records and history replay keep reading correctly. - docs/ARCHITECTURE.md: the module row no longer claims a duplicate gate; docs/CHECKLISTS_ARCHIVE.md: standing disclosure for item 21. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
106 lines
4.7 KiB
Python
106 lines
4.7 KiB
Python
"""Only host allocation establishes a task birth fact for archive read floors."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from ouroboros.task_results import load_task_result, write_task_result
|
|
from tests.test_headless_task_title import admission as admission # shared API fixture
|
|
|
|
pytestmark = pytest.mark.serial
|
|
CREATED = "2026-01-01T00:00:01+00:00"
|
|
|
|
|
|
def assert_lifecycle_keeps_creation(data, task_id):
|
|
from ouroboros.headless import copy_child_task_result
|
|
|
|
for status in ("running", "completed"):
|
|
result = write_task_result(data, task_id, status, result=status)
|
|
assert result["created_at"] == CREATED
|
|
child = data.parent / (task_id + "-child")
|
|
write_task_result(child, task_id, "completed", result="child result")
|
|
mirrored = copy_child_task_result(data, {"id": task_id, "child_drive_root": str(child)})
|
|
assert mirrored["created_at"] == CREATED
|
|
|
|
|
|
@pytest.mark.parametrize("supplied_id", [False, True])
|
|
def test_api_only_stamps_its_own_new_uuid_before_preparation(admission, monkeypatch, supplied_id):
|
|
from ouroboros.gateway import tasks
|
|
|
|
client, data, captured, _broadcasts = admission
|
|
clock_calls = []
|
|
monkeypatch.setattr(tasks, "utc_now_iso", lambda: clock_calls.append("birth") or CREATED)
|
|
prepare = tasks.prepare_task_drive
|
|
def prepare_after_allocation(*args, **kwargs):
|
|
assert clock_calls == ([] if supplied_id else ["birth"])
|
|
return prepare(*args, **kwargs)
|
|
monkeypatch.setattr(tasks, "prepare_task_drive", prepare_after_allocation)
|
|
body = {"description": "Inspect a local task"}
|
|
if supplied_id:
|
|
body["task_id"] = "supplied-task"
|
|
response = client.post("/api/tasks", json=body)
|
|
assert response.status_code == 200, response.text
|
|
task_id = response.json()["task_id"]
|
|
initial = load_task_result(data, task_id)
|
|
assert initial["status"] == "scheduled"
|
|
if supplied_id:
|
|
assert "created_at" not in initial and "created_at" not in captured[0]
|
|
else:
|
|
assert initial["created_at"] == captured[0]["created_at"] == CREATED
|
|
assert_lifecycle_keeps_creation(data, task_id)
|
|
|
|
|
|
def test_tool_subagent_birth_precedes_drive_preparation(tmp_path, monkeypatch):
|
|
from ouroboros.tools import control_scheduling
|
|
from ouroboros.tools.registry import ToolContext
|
|
from tests._shared import configure_test_subagent
|
|
|
|
subagent_id = configure_test_subagent(monkeypatch)
|
|
monkeypatch.setenv("OUROBOROS_MAX_SUBAGENT_DEPTH", "3")
|
|
repo, data = tmp_path / "repo", tmp_path / "data"
|
|
repo.mkdir()
|
|
data.mkdir()
|
|
clock_calls = []
|
|
monkeypatch.setattr(control_scheduling, "utc_now_iso", lambda: clock_calls.append("stamp") or CREATED)
|
|
prepare = control_scheduling._prepare_child_drive
|
|
def prepare_after_allocation(*args, **kwargs):
|
|
assert clock_calls == ["stamp"]
|
|
return prepare(*args, **kwargs)
|
|
monkeypatch.setattr(control_scheduling, "_prepare_child_drive", prepare_after_allocation)
|
|
ctx = ToolContext(repo_dir=repo, drive_root=data, task_id="parent")
|
|
result = control_scheduling._schedule_task(
|
|
ctx, subagent_id=subagent_id, objective="Inspect files", expected_output="findings", memory_mode="empty",
|
|
)
|
|
assert ctx.pending_events, result
|
|
task_id = ctx.pending_events[0]["task_id"]
|
|
initial = load_task_result(data, task_id)
|
|
assert initial["status"] == "requested" and initial["created_at"] == CREATED
|
|
assert_lifecycle_keeps_creation(data, task_id)
|
|
|
|
|
|
@pytest.mark.parametrize("supplied_id", [False, True])
|
|
def test_supervisor_stamps_only_a_locally_allocated_id(tmp_path, monkeypatch, supplied_id):
|
|
from supervisor import events, events_schedule_task
|
|
from tests.test_nested_rights_depth import _fake_ctx, _schedule_event
|
|
|
|
monkeypatch.setattr(events_schedule_task, "utc_now_iso", lambda: CREATED)
|
|
event = _schedule_event("supplied-task" if supplied_id else "", "", depth=0, drive_root=tmp_path)
|
|
event["delegation_role"] = "root"
|
|
enqueued = []
|
|
events._handle_schedule_task(event, _fake_ctx(tmp_path, enqueued))
|
|
assert enqueued
|
|
task_id = enqueued[0]["id"]
|
|
initial = load_task_result(tmp_path, task_id)
|
|
assert initial["status"] == "scheduled"
|
|
if supplied_id:
|
|
assert "created_at" not in initial
|
|
else:
|
|
assert initial["created_at"] == CREATED
|
|
assert_lifecycle_keeps_creation(tmp_path, task_id)
|
|
|
|
|
|
def test_final_only_and_legacy_updates_do_not_invent_creation(tmp_path):
|
|
result = write_task_result(tmp_path, "final-only", "failed", ts="2026-01-01T01:00:00Z")
|
|
assert "created_at" not in result
|
|
write_task_result(tmp_path, "legacy", "running", ts="2025-01-01T00:00:00Z")
|
|
result = write_task_result(tmp_path, "legacy", "completed")
|
|
assert "created_at" not in result
|