mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
Stop naming direct conversation turns; delete the proactive namer
A direct turn renders as an activity block without a title placeholder and is never offered Turn into project (owner decision 14=A), so the light-model title the direct lane coined for every fresh Main turn — one paid call per message, a daemon thread with its own wall-clock bound and a late cost-refresh path — has no reader left. The lane no longer spawns it; spawn_proactive_namer and _refresh_root_cost_after_naming are deleted with the threading/contextvars /pathlib imports only they used, the two tests that exercised them, the terminal-writer inventory row and five test stubs that silenced the thread. Managed promotes and headless runs keep their admission names (admission_names, _admitted_suggested_name) and the task_named frame, so applySuggestedName still titles managed cards. The post-task checkpoint's same-terminal refresh stays (its own test covers it); its comments no longer attribute it to the retired namer. Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
parent
fc40ea335c
commit
56be6556fd
14 changed files with 25 additions and 271 deletions
|
|
@ -117,7 +117,7 @@ server.py (Starlette+uvicorn) ← HTTP + WebSocket on configurable host:port (de
|
|||
├── vision_routing.py ← Send-time image routing SSOT: inline vision vs generic captions vs placeholders on a per-send message copy (`OUROBOROS_IMAGE_INPUT_MODE`, `OUROBOROS_MODEL_VISION`)
|
||||
├── fallback_cooldown.py ← Per-process 429-aware cooldown for the `OUROBOROS_MODEL_FALLBACKS` chain: a transiently-failed model is parked for a short window so fallback walks and repeated rounds skip it; advisory, default-on, fail-soft, passive heal; per-process only — honestly not a swarm-wide governor
|
||||
├── model_concurrency.py ← Per-(model,use_local) `BoundedSemaphore` capping concurrent provider calls (`OUROBOROS_MODEL_MAX_CONCURRENCY`, default 3) so a task's own loop + subagent threads + status pings cannot self-DoS one model's rate limit; excess threads WAIT deadline-bounded; wraps only the provider call in `loop_llm_call.call_llm_with_retry`; per-process only
|
||||
├── project_naming.py ← SSOT for LLM-first project naming: bounded light-model title with deterministic fallback, shared by the proactive card namer (supervisor/worker_chat_lane.py), turn-into-project conversion (gateway/projects.py), and `ensure_project_scope`; the provider call goes through the model_concurrency slot
|
||||
├── project_naming.py ← SSOT for LLM-first project naming: bounded light-model title with deterministic fallback, shared by admission naming (`admission_names`: headless runs and chat promotion, no model call), turn-into-project conversion (gateway/projects.py), and `ensure_project_scope` — a direct conversation turn is never named; the provider call goes through the model_concurrency slot
|
||||
├── loop_tool_execution.py ← Tool dispatch and tool-result handling
|
||||
├── deadline_utils.py ← Shared deadline parsing/remaining-time helpers + the transport-vs-logical wait seam for loop milestones and process-tool/review timeouts
|
||||
├── observability.py ← Private forensic execution ledger: redaction, gzip CAS blobs, call manifests, trace refs
|
||||
|
|
|
|||
|
|
@ -232,8 +232,8 @@ def project_root_post_task_checkpoint_fields(
|
|||
The root writer owns only post-task synthesis and its accounting snapshot;
|
||||
acceptance remains whatever the current record says. Once post-task state
|
||||
is terminal, an open or different-terminal stale patch cannot replace that
|
||||
state or its accounting. A same-terminal patch remains valid so the
|
||||
proactive namer's explicit ``refresh`` can update the final cost snapshot.
|
||||
state or its accounting. A same-terminal patch remains valid so an explicit
|
||||
``refresh`` can update the final cost snapshot.
|
||||
"""
|
||||
overlay = dict(patch_fields)
|
||||
if canonical_fields.get("status"):
|
||||
|
|
@ -316,8 +316,8 @@ def set_root_post_task_checkpoint(
|
|||
return
|
||||
authority_root = roots[0]
|
||||
finalized_event: Dict[str, Any] | None = None
|
||||
# The proactive namer can settle concurrently with post-task synthesis. A
|
||||
# shared critical section makes its refresh and the final snapshot linear.
|
||||
# A late cost refresh can settle concurrently with post-task synthesis. A
|
||||
# shared critical section makes that refresh and the final snapshot linear.
|
||||
with POST_TASK_SYNTHESIS_LOCK:
|
||||
existing = load_task_result(authority_root, task_id) or {}
|
||||
checkpoint = existing.get("root_phase_checkpoint")
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
An LLM-first short human title for a project card, with a deterministic heuristic
|
||||
fallback. Shared by every path that names a project so the UI conversion and the
|
||||
agent never drift:
|
||||
- the proactive card namer (names ANY task card up front, supervisor side);
|
||||
- ``gateway/projects.py`` turn-into-project conversion (reuses the up-front name,
|
||||
- ``gateway/projects.py`` turn-into-project conversion (reuses an admission name,
|
||||
or names inline as a race fallback);
|
||||
- ``ensure_project_scope`` (the agent self-creates + names a project).
|
||||
- ``ensure_project_scope`` (the agent self-creates + names a project);
|
||||
- ``admission_names`` (headless runs and chat promotion, no model call).
|
||||
A direct conversation turn is never named: it renders as an activity block.
|
||||
|
||||
Doctrine:
|
||||
- P5 LLM-first: the model COINS the name; post-processing is purely lexical
|
||||
|
|
@ -19,11 +20,8 @@ Doctrine:
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
import threading
|
||||
from dataclasses import replace
|
||||
from typing import Any, Callable, Dict, Optional, Sequence
|
||||
import contextvars
|
||||
from typing import Any, Dict, Optional, Sequence
|
||||
|
||||
log = logging.getLogger("ouroboros.project_naming")
|
||||
|
||||
|
|
@ -271,134 +269,6 @@ async def llm_project_name_async(
|
|||
return fb
|
||||
|
||||
|
||||
def _refresh_root_cost_after_naming(drive_root: Any, task_id: str) -> None:
|
||||
"""Refresh a terminal root projection after the naming attempt settles."""
|
||||
try:
|
||||
from types import SimpleNamespace
|
||||
|
||||
from ouroboros.agent_task_pipeline import _set_root_post_task_checkpoint
|
||||
from ouroboros.task_results import load_task_result
|
||||
|
||||
current = load_task_result(drive_root, task_id) or {}
|
||||
refreshed = {**current, "id": task_id, "budget_drive_root": str(drive_root)}
|
||||
_set_root_post_task_checkpoint(
|
||||
SimpleNamespace(drive_root=pathlib.Path(drive_root)), refreshed, "refresh",
|
||||
)
|
||||
except Exception:
|
||||
log.debug("project naming cost refresh failed for %s", task_id, exc_info=True)
|
||||
|
||||
|
||||
def spawn_proactive_namer(
|
||||
drive_root: Any, task_id: str, text: str, *, broadcast: Optional[Callable[[dict], None]] = None,
|
||||
) -> None:
|
||||
"""Proactively coin an LLM project name for a fresh card in a DAEMON thread (Cluster B).
|
||||
|
||||
Writes the coined ``suggested_name`` onto the task result (turn-into-project then reuses
|
||||
it with zero extra call) and, via ``broadcast``, emits a ``task_named`` event so the live
|
||||
card shows a human title up front. NEVER blocks the task. ``drive_root`` is captured at
|
||||
CALL time — NOT read from a mutable module global at thread-execution time — so a later
|
||||
context switch (or a test that swaps the supervisor drive) can't redirect this thread's
|
||||
write. Skips cleanly unless ``drive_root`` is a real directory (test safety: a stub /
|
||||
MagicMock drive must never materialise a stray path — chat_observed persists BEFORE the
|
||||
LLM call). Fail-soft."""
|
||||
from ouroboros.settings_integrity import copy_task_settings_context
|
||||
|
||||
body = " ".join(str(text or "").split())
|
||||
if not body:
|
||||
return
|
||||
try:
|
||||
if not pathlib.Path(str(drive_root)).is_dir():
|
||||
return
|
||||
except (OSError, TypeError, ValueError):
|
||||
return
|
||||
|
||||
def _work() -> None:
|
||||
try:
|
||||
# v6.58.0 (§3.4b): HARD total wall-clock bound. The transport timeout bounds
|
||||
# ONE attempt, but llm.chat's retry/fallback chain under a degraded provider
|
||||
# could stretch the whole call to tens of minutes (the incident where the
|
||||
# card was named 24 minutes late). A title is cosmetic: if it hasn't landed
|
||||
# within the transport budget + slack, drop it — the id/title heuristics and
|
||||
# the convert path's own bounded inline call (8s) already cover naming.
|
||||
_result: list[str] = []
|
||||
_detached = threading.Event()
|
||||
_finished = threading.Event()
|
||||
_refresh_lock = threading.Lock()
|
||||
_refreshed = False
|
||||
|
||||
def _refresh_detached_once() -> None:
|
||||
nonlocal _refreshed
|
||||
with _refresh_lock:
|
||||
if _refreshed:
|
||||
return
|
||||
_refreshed = True
|
||||
_refresh_root_cost_after_naming(drive_root, task_id)
|
||||
|
||||
def _call() -> None:
|
||||
try:
|
||||
_result.append(llm_project_name(body, drive_root=drive_root, task_id=task_id))
|
||||
except Exception:
|
||||
log.debug("proactive namer inner call failed for %s", task_id, exc_info=True)
|
||||
finally:
|
||||
_finished.set()
|
||||
if _detached.is_set():
|
||||
_refresh_detached_once()
|
||||
|
||||
settings_context = contextvars.Context()
|
||||
copy_task_settings_context(settings_context)
|
||||
inner = threading.Thread(target=settings_context.run, args=(_call,), name=f"namer-call-{task_id}", daemon=True)
|
||||
inner.start()
|
||||
if not _finished.wait(timeout=max(0.0, _naming_timeout_sec() + 30.0)):
|
||||
_detached.set()
|
||||
# Close the race where settlement lands between wait() and
|
||||
# the detached marker. The once-guard covers both interleavings.
|
||||
if _finished.is_set():
|
||||
_refresh_detached_once()
|
||||
log.debug("proactive namer exceeded its wall-clock bound for %s; skipped", task_id)
|
||||
return
|
||||
inner.join()
|
||||
if not _result:
|
||||
log.debug("proactive namer exceeded its wall-clock bound for %s; skipped", task_id)
|
||||
return
|
||||
name = _result[0]
|
||||
if not name:
|
||||
return
|
||||
from ouroboros.task_results import (
|
||||
STATUS_RUNNING,
|
||||
load_task_result,
|
||||
write_task_result,
|
||||
)
|
||||
|
||||
# Persist suggested_name as same-status ENRICHMENT, not a RUNNING transition: a
|
||||
# fast task may already be terminal (completed/failed/cancelled) by the time this
|
||||
# daemon finishes, and write_task_result's monotonic guard DROPS a regressing
|
||||
# RUNNING write — which would silently lose the name the convert path reuses.
|
||||
# Writing under the current on-disk status lets the monotonic guard's same-status
|
||||
# enrichment carry the field through (and a benign drop only in the rare race where
|
||||
# the status advanced past our read — acceptable for a best-effort title).
|
||||
current = load_task_result(drive_root, task_id) or {}
|
||||
status = str(current.get("status") or "") or STATUS_RUNNING
|
||||
write_task_result(drive_root, task_id, status, suggested_name=name)
|
||||
# A cosmetic namer may settle concurrently with or after the ordinary
|
||||
# post-task worker. The shared refresh/checkpoint critical section
|
||||
# linearizes both cases without marking an unfinished phase complete.
|
||||
_refresh_root_cost_after_naming(drive_root, task_id)
|
||||
if broadcast is not None:
|
||||
try:
|
||||
broadcast({"type": "task_named", "task_id": task_id, "suggested_name": name})
|
||||
except Exception:
|
||||
log.debug("task_named broadcast failed for %s", task_id, exc_info=True)
|
||||
except Exception:
|
||||
log.debug("proactive namer failed for %s", task_id, exc_info=True)
|
||||
|
||||
try:
|
||||
settings_context = contextvars.Context()
|
||||
copy_task_settings_context(settings_context)
|
||||
threading.Thread(target=settings_context.run, args=(_work,), name=f"namer-{task_id}", daemon=True).start()
|
||||
except Exception:
|
||||
log.debug("proactive namer thread spawn failed for %s", task_id, exc_info=True)
|
||||
|
||||
|
||||
def admission_names(body: Dict[str, Any], description: str) -> tuple:
|
||||
"""The run's owner-facing name at admission: ``(title, suggested_name)``.
|
||||
|
||||
|
|
|
|||
|
|
@ -868,7 +868,7 @@ def _ensure_project_scope(ctx: ToolContext, project_name: str = "", project_id:
|
|||
)
|
||||
from ouroboros.project_naming import clean_model_title
|
||||
|
||||
# Run the agent-supplied name through the SAME lexical cleaner the proactive namer and
|
||||
# Run the agent-supplied name through the SAME lexical cleaner admission naming and
|
||||
# turn-into-project conversion use (project_naming SSOT) so every project-naming path
|
||||
# produces consistent titles (quote/emoji strip, length cap); fall back to the raw value.
|
||||
display_name = clean_model_title(project_name) or str(project_name or "").strip()
|
||||
|
|
|
|||
|
|
@ -187,7 +187,7 @@ def _host_operation_failure(metadata: Optional[dict]) -> dict:
|
|||
|
||||
|
||||
def _broadcast_task_named(msg: dict) -> None:
|
||||
"""Bridge broadcast callback for the proactive namer (kept tiny + fail-soft)."""
|
||||
"""Bridge broadcast callback for admission naming (kept tiny + fail-soft)."""
|
||||
try:
|
||||
from supervisor.message_bus import get_bridge
|
||||
|
||||
|
|
@ -345,15 +345,10 @@ def _run_chat_task(
|
|||
_pool()._report_binding_failure(task["id"], pid, exc, path="direct_project_turn")
|
||||
if not task["text"]:
|
||||
task["text"] = "(image attached)" if image_data else ""
|
||||
# Cluster B: proactively coin a project name for a fresh MAIN-CHAT direct card
|
||||
# (not an already-bound project-thread task) so
|
||||
# the card shows a human title up front and turn-into-project reuses it.
|
||||
if not task.get("project_id"):
|
||||
from ouroboros.project_naming import spawn_proactive_namer
|
||||
|
||||
spawn_proactive_namer(
|
||||
_pool().DRIVE_ROOT, str(task["id"]), task["text"], broadcast=_broadcast_task_named
|
||||
)
|
||||
# A direct turn is not named: it renders as an activity block, never as
|
||||
# a titled task card, and joins a Project only through the model's own
|
||||
# scope tools (owner decision 14=A). Managed promotes keep their
|
||||
# admission names (worker_promotion._admitted_suggested_name).
|
||||
attach_task_contract(task)
|
||||
|
||||
# Announce the authoritative start immediately (owner decision 2A):
|
||||
|
|
|
|||
|
|
@ -529,10 +529,6 @@ class TestDesktopChatFullSetStaging:
|
|||
# isolation precedent as tests/test_inflight_indicator_seams.py::
|
||||
# _patch_workers).
|
||||
monkeypatch.setattr(workers, "get_event_q", lambda: queue.Queue())
|
||||
# Avoid the proactive namer spinning a real thread/LLM in this unit test
|
||||
# (it is a local `from ouroboros.project_naming import ...`, so patch source).
|
||||
import ouroboros.project_naming as project_naming
|
||||
monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **k: None)
|
||||
|
||||
# Two uploads on disk: an image and a non-image PDF.
|
||||
img_src = tmp_path / "photo.png"
|
||||
|
|
@ -586,8 +582,6 @@ class TestDesktopChatFullSetStaging:
|
|||
# lifespan in this xdist worker must not abort the turn before
|
||||
# handle_task.
|
||||
monkeypatch.setattr(workers, "get_event_q", lambda: queue.Queue())
|
||||
import ouroboros.project_naming as project_naming
|
||||
monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **k: None)
|
||||
|
||||
b64 = base64.b64encode(_PNG_BYTES).decode("ascii")
|
||||
agent = _FakeChatAgent()
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ TERMINAL_WRITERS = {
|
|||
('ouroboros/post_task_checkpoint.py::set_root_post_task_checkpoint', 'str(existing.get("status") or task.get("status") or STATUS_COMPLETED)'): 'terminal',
|
||||
('ouroboros/project_dialogue.py::_append_terminal_task_projection', 'status'): 'dynamic',
|
||||
('ouroboros/project_dialogue.py::persist_continuation_narrative', 'requested_status'): 'dynamic',
|
||||
('ouroboros/project_naming.py::spawn_proactive_namer._work', 'status'): 'dynamic',
|
||||
# The locked field projector preserves the existing status, including a
|
||||
# terminal one; publishing review evidence never completes the task itself.
|
||||
('ouroboros/review_projection.py::publish_acceptance_checkpoint', '"running"'): 'dynamic',
|
||||
|
|
|
|||
|
|
@ -125,10 +125,10 @@ def test_a_title_hidden_in_metadata_is_refused_like_a_project_id(admission):
|
|||
def test_a_derived_name_is_not_reported_as_model_coined():
|
||||
"""Turn-into-project reuses the name slot; it must not claim authorship.
|
||||
|
||||
Two producers fill `suggested_name`: the proactive namer coins one with a
|
||||
model, and headless admission derives one from the request's first line.
|
||||
The conversion cannot tell them apart, so its naming reason names the SLOT
|
||||
it read rather than a coiner that may not exist.
|
||||
`suggested_name` is filled by admission naming (a caller title, or the
|
||||
request's first line) and by the agent's own scope tools; the conversion
|
||||
cannot tell them apart, so its naming reason names the SLOT it read rather
|
||||
than a coiner that may not exist.
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
|
|
|
|||
|
|
@ -126,7 +126,6 @@ def test_ordinary_main_and_transport_refusals_keep_their_original_envelope(monke
|
|||
@pytest.mark.parametrize("host_operation", [False, True])
|
||||
def test_chat_crash_preserves_only_the_host_accepted_operation(tmp_path, monkeypatch, host_operation):
|
||||
from queue import SimpleQueue
|
||||
from ouroboros import project_naming
|
||||
from supervisor import worker_chat_lane, workers
|
||||
|
||||
class CrashingAgent:
|
||||
|
|
@ -140,7 +139,6 @@ def test_chat_crash_preserves_only_the_host_accepted_operation(tmp_path, monkeyp
|
|||
monkeypatch.setattr(workers, "DRIVE_ROOT", tmp_path)
|
||||
monkeypatch.setattr(workers, "get_event_q", SimpleQueue)
|
||||
monkeypatch.setattr(workers, "send_with_budget", message_bus.send_with_budget)
|
||||
monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **kw: None)
|
||||
client = _client(tmp_path, bridge)
|
||||
assert client.post("/chat/inject", headers=_headers(), json={
|
||||
"chat_id": CHAT, "client_message_id": MSG, "text": "do work",
|
||||
|
|
|
|||
|
|
@ -64,9 +64,6 @@ def _patch_workers(monkeypatch, tmp_path):
|
|||
# Windows CI shard; same isolation precedent as
|
||||
# tests/test_promote_event_transport.py::_isolate_event_bus_shutdown_latch).
|
||||
monkeypatch.setattr(workers, "get_event_q", lambda: queue.Queue())
|
||||
import ouroboros.project_naming as project_naming
|
||||
|
||||
monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **k: None)
|
||||
# Capture the turn's start announce (bridge typing frame) instead of
|
||||
# touching the real singleton bridge.
|
||||
bridge_probe = _BridgeProbe()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from supervisor.active_activity import get_direct_activity_registry
|
|||
|
||||
|
||||
def _lane(monkeypatch, tmp_path):
|
||||
from ouroboros import project_naming
|
||||
from supervisor import message_bus, state
|
||||
|
||||
monkeypatch.setattr(workers, "DRIVE_ROOT", tmp_path)
|
||||
|
|
@ -19,7 +18,6 @@ def _lane(monkeypatch, tmp_path):
|
|||
monkeypatch.setattr(workers, "send_with_budget", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(state, "load_state", lambda: {})
|
||||
monkeypatch.setattr(state, "budget_remaining", lambda *a, **kw: 100)
|
||||
monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(message_bus, "get_bridge", lambda: SimpleNamespace(send_chat_action=lambda *a, **kw: None))
|
||||
workers.open_repo_writer_admission()
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ def test_native_wait_controls_and_manual_restart_address_all_registered_actors(t
|
|||
@pytest.mark.parametrize("project_id", ["", "room"])
|
||||
@pytest.mark.parametrize("action", ["switch", "stop", "stop_settled"])
|
||||
def test_native_post_task_wait_remains_addressable_after_dialogue_closes(phase, monkeypatch, project_id, action):
|
||||
from ouroboros import agent_task_pipeline as pipeline, project_naming
|
||||
from ouroboros import agent_task_pipeline as pipeline
|
||||
from ouroboros.gateway.state import _chat_activities_snapshot_safe
|
||||
from ouroboros.post_task_checkpoint import post_task_model_wait
|
||||
from ouroboros.task_results import load_task_result, write_task_result
|
||||
|
|
@ -97,7 +97,6 @@ def test_native_post_task_wait_remains_addressable_after_dialogue_closes(phase,
|
|||
monkeypatch.setattr(workers, "DRIVE_ROOT", f.root)
|
||||
monkeypatch.setattr(workers, "WORKERS", {})
|
||||
monkeypatch.setattr(workers, "get_event_q", lambda: f.events)
|
||||
monkeypatch.setattr(project_naming, "spawn_proactive_namer", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(message_bus, "get_bridge", lambda: SimpleNamespace(send_chat_action=lambda *a, **kw: None))
|
||||
monkeypatch.setattr(task_queue, "DRIVE_ROOT", f.root)
|
||||
monkeypatch.setattr(task_queue, "RUNNING", {})
|
||||
|
|
|
|||
|
|
@ -217,102 +217,6 @@ def test_read_paths_do_not_create_task_results_dir(tmp_path):
|
|||
assert (root / "task_results").is_dir()
|
||||
|
||||
|
||||
def test_proactive_namer_persists_name_on_already_terminal_task(tmp_path, monkeypatch):
|
||||
"""v6.40.0 #1: the proactive namer must persist ``suggested_name`` even when the task
|
||||
already raced to a terminal status — it enriches under the CURRENT status instead of a
|
||||
regressing RUNNING write (which the monotonic guard would drop, losing the convert-reuse
|
||||
name)."""
|
||||
import threading
|
||||
import time
|
||||
|
||||
from ouroboros import project_naming
|
||||
|
||||
tr.write_task_result(tmp_path, "t", tr.STATUS_COMPLETED, result="fast done")
|
||||
monkeypatch.setattr(project_naming, "llm_project_name", lambda *a, **k: "Nice Title")
|
||||
project_naming.spawn_proactive_namer(tmp_path, "t", "build me a thing")
|
||||
for _ in range(100): # join the daemon namer thread (best-effort, bounded)
|
||||
if not any(th.name == "namer-t" for th in threading.enumerate()):
|
||||
break
|
||||
time.sleep(0.02)
|
||||
r = tr.load_task_result(tmp_path, "t")
|
||||
assert r["status"] == tr.STATUS_COMPLETED, "namer must NOT regress a terminal task to running"
|
||||
assert r.get("suggested_name") == "Nice Title", "suggested_name must survive on a terminal task"
|
||||
|
||||
|
||||
def test_proactive_namer_late_settlement_refreshes_cost_without_late_name(tmp_path, monkeypatch):
|
||||
"""A provider thread outliving the cosmetic deadline still closes accounting only."""
|
||||
import threading
|
||||
import time
|
||||
|
||||
from ouroboros import agent_task_pipeline, project_naming, usage_accounting
|
||||
|
||||
tr.write_task_result(
|
||||
tmp_path,
|
||||
"late-root",
|
||||
tr.STATUS_COMPLETED,
|
||||
root_task_id="late-root",
|
||||
accounted_upper_bound_usd=0.0,
|
||||
cost_final=True,
|
||||
root_phase_checkpoint={"post_task_synthesis": "completed"},
|
||||
)
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
def late_paid_name(*_args, **_kwargs):
|
||||
entered.set()
|
||||
assert release.wait(2)
|
||||
attempt = usage_accounting.reserve_attempt(usage_accounting.AttemptRequest(
|
||||
model="openai/gpt-5.2",
|
||||
provider="openai",
|
||||
reservation_usd=0.25,
|
||||
drive_root=tmp_path,
|
||||
task_id="late-root",
|
||||
root_task_id="late-root",
|
||||
global_limit_usd=5.0,
|
||||
root_limit_usd=5.0,
|
||||
))
|
||||
usage_accounting.mark_dispatched(attempt)
|
||||
usage_accounting.settle_attempt(attempt, {}, cost_usd=0.25, cost_final=True)
|
||||
return "Too Late"
|
||||
|
||||
monkeypatch.setattr(project_naming, "llm_project_name", late_paid_name)
|
||||
monkeypatch.setattr(project_naming, "_naming_timeout_sec", lambda: -29.98)
|
||||
broadcasts = []
|
||||
project_naming.spawn_proactive_namer(
|
||||
tmp_path, "late-root", "build a thing", broadcast=broadcasts.append,
|
||||
)
|
||||
assert entered.wait(1)
|
||||
for _ in range(100):
|
||||
if not any(th.name == "namer-late-root" for th in threading.enumerate()):
|
||||
break
|
||||
time.sleep(0.01)
|
||||
assert not any(th.name == "namer-late-root" for th in threading.enumerate())
|
||||
|
||||
release.set()
|
||||
# The subject is that the late settlement's refresh LANDS, not how fast: the
|
||||
# detached thread runs reserve → dispatch → settle → cost reconstruction →
|
||||
# locked task-result write, each a real lock cycle with fsyncs, and the
|
||||
# Windows full-test leg took longer than a 2 s poll twice out of five runs
|
||||
# (d0bb839e, 5fbdabd3) while green on the others. A refresh that never lands
|
||||
# still fails here — after a bound wide enough for a loaded runner.
|
||||
started = time.monotonic()
|
||||
for _ in range(2000):
|
||||
stored = tr.load_task_result(tmp_path, "late-root")
|
||||
if stored.get("accounted_upper_bound_usd") == 0.25:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
stored = tr.load_task_result(tmp_path, "late-root")
|
||||
assert stored["accounted_upper_bound_usd"] == 0.25, f"no refresh after {time.monotonic() - started:.1f}s"
|
||||
assert stored["accounted_upper_bound_usd_with_children"] == 0.25
|
||||
assert stored["cost_final"] is True
|
||||
assert stored.get("suggested_name") is None
|
||||
assert broadcasts == []
|
||||
assert agent_task_pipeline._root_post_task_already_completed(
|
||||
type("Env", (), {"drive_root": tmp_path})(),
|
||||
{"id": "late-root", "root_task_id": "late-root"},
|
||||
)
|
||||
|
||||
|
||||
def test_read_with_stub_root_leaks_no_cwd_dir(tmp_path, monkeypatch):
|
||||
"""The exact pollution repro: a MagicMock-derived root (``MagicMock/mock``) reaching a
|
||||
READ scan must not create a ``MagicMock`` tree in the cwd."""
|
||||
|
|
|
|||
|
|
@ -3745,10 +3745,10 @@ export function createChatInstance({
|
|||
withRemoteActivity(() => updateLiveCardFromLogEvent(msg.data));
|
||||
});
|
||||
|
||||
// the proactive namer coined a project name for a fresh card — show it
|
||||
// as the card title up front (turn-into-project then reuses the same name). Not
|
||||
// thread-gated on chat_id: the broadcast carries only task_id, and applySuggestedName
|
||||
// no-ops unless THIS thread already holds that card.
|
||||
// Admission naming (a promoted root, a headless run) coined a name for a
|
||||
// managed card — show it as the card title up front (turn-into-project then
|
||||
// reuses the same name). Not thread-gated on chat_id: the broadcast carries
|
||||
// only task_id, and applySuggestedName no-ops unless THIS thread holds that card.
|
||||
onWs('task_named', (msg) => {
|
||||
withRemoteActivity(
|
||||
() => applySuggestedName(msg?.task_id || '', msg?.suggested_name || ''),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue