ouroboros/supervisor/workers.py

2208 lines
92 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Worker lifecycle, health, and direct-chat handling for the supervisor."""
from __future__ import annotations
import logging
log = logging.getLogger(__name__)
import json # noqa: F401
import multiprocessing as mp
import os
import pathlib
import sys
import threading
import time
import uuid
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple, Union # noqa: F401
from supervisor.state import load_state, append_jsonl, reconstruct_task_cost
from supervisor.message_bus import send_with_budget
from supervisor.message_bus import coerce_chat_identity # noqa: F401 -- worker_health leaf reads it via the _pool() handle
from ouroboros.config import DATA_DIR, REPO_DIR as CONFIG_REPO_DIR, WORKER_SPAWN_GRACE_SEC
from ouroboros.depth_evidence import parse_task_depth
from ouroboros.review_owner_custody import (
reconcile_confirmed_dead_review_owners as _reconcile_review_owners,
)
from ouroboros.utils import utc_now_iso
REPO_DIR: pathlib.Path = pathlib.Path(CONFIG_REPO_DIR)
DRIVE_ROOT: pathlib.Path = pathlib.Path(DATA_DIR)
MAX_WORKERS: int = 10
HEARTBEAT_STALE_SEC: int = 120
QUEUE_MAX_RETRIES: int = 1
BRANCH_DEV: str = "ouroboros"
BRANCH_STABLE: str = "ouroboros-stable"
_CTX = None
_LAST_SPAWN_TIME: float = 0.0 # grace period: don't count dead workers right after spawn
_SPAWN_GRACE_SEC: float = WORKER_SPAWN_GRACE_SEC # config SSOT; leaves read it through _pool()
# Defaults: spawn on macOS + Windows, forkserver on Linux (G13); the env key
# OUROBOROS_WORKER_START_METHOD overrides either. fork() from the long-lived,
# multi-threaded supervisor is unsafe on every OS: a child forked while another
# server thread holds a per-module import lock keeps that lock with no thread
# left to release it and wedges on its first import of that module (the
# deadlock colab_bootstrap.py names; macOS adds dead Mach ports and a _scproxy
# SIGSEGV pre-exec). forkserver forks every worker from ONE bare single-threaded
# server process, so no lock is inherited, and a warm child still confirms in
# seconds (mock lane, fork -> forkserver: startup 2.4-3.3 -> 3.5-4.9s, respawn
# 2.3 -> 2.5-3.2s; window 90s). worker_main is module-level and re-derives state
# from argv (its lifeline: spawner sentinel, not ppid), so spawn/forkserver/frozen re-exec are safe.
_DEFAULT_WORKER_START_METHOD = "forkserver" if sys.platform.startswith("linux") else "spawn"
_WORKER_START_METHOD = str(os.environ.get("OUROBOROS_WORKER_START_METHOD", _DEFAULT_WORKER_START_METHOD) or _DEFAULT_WORKER_START_METHOD).strip().lower()
if _WORKER_START_METHOD not in {"fork", "spawn", "forkserver"}:
_WORKER_START_METHOD = _DEFAULT_WORKER_START_METHOD
def _get_ctx():
"""Return the multiprocessing context for workers."""
global _CTX
if _CTX is None:
_CTX = mp.get_context(_WORKER_START_METHOD)
return _CTX
def init(repo_dir: pathlib.Path, drive_root: pathlib.Path, max_workers: int,
branch_dev: str = "ouroboros", branch_stable: str = "ouroboros-stable") -> None:
global REPO_DIR, DRIVE_ROOT, MAX_WORKERS, BRANCH_DEV, BRANCH_STABLE
REPO_DIR = repo_dir
DRIVE_ROOT = drive_root
MAX_WORKERS = max_workers
BRANCH_DEV = branch_dev
BRANCH_STABLE = branch_stable
from supervisor import queue
queue.init(drive_root)
queue.init_queue_refs(PENDING, RUNNING, QUEUE_SEQ_COUNTER_REF)
@dataclass
class Worker:
wid: int
proc: mp.Process
in_q: Any
busy_task_id: Optional[str] = None
# Variant A (off-loop reaping): set under _queue_lock when a timed-out task's heavy
# teardown (kill/join/archive/respawn) is handed to the background reaper. The slot
# is unavailable for assignment until respawn_worker() installs a fresh Worker.
reaping: bool = False
# A required owner wait keeps this process and task, lending only dispatch capacity.
active_capacity: bool = True
# Unlike temporary reaping, the readiness owner exhausted its bounded attempts.
readiness_exhausted: bool = False
_EVENT_Q = None
_EVENT_Q_MANAGER = None
_EVENT_Q_GENERATION = ""
_EVENT_Q_LOCK = threading.Lock()
_EVENT_Q_SHUTDOWN = False
_WORKER_POOL_DISABLED_REASON = ""
def get_event_q():
"""Return the process-lifetime supervisor event bus, creating it lazily.
Worker-pool generations are replaceable; the producers that publish onto
this bus (direct chat, active turns, and workers) are not.
Rotating the queue during a pool respawn strands those producers on an
undrained queue, so only a new server process creates a new bus.
"""
global _EVENT_Q, _EVENT_Q_MANAGER, _EVENT_Q_GENERATION
with _EVENT_Q_LOCK:
if _EVENT_Q_SHUTDOWN:
raise RuntimeError("supervisor event bus is shutting down")
if _EVENT_Q is None:
# A raw multiprocessing.Queue has an asynchronous feeder and its
# pipe can be corrupted when a worker is force-killed mid-frame.
# A manager-backed queue serializes synchronously in the producer
# and isolates each producer connection, so replacing/killing a
# worker generation cannot wedge the process-lifetime bus.
_EVENT_Q_MANAGER = _get_ctx().Manager()
_EVENT_Q = _EVENT_Q_MANAGER.Queue()
_EVENT_Q_GENERATION = f"{os.getpid()}:{uuid.uuid4().hex[:12]}"
try:
from ouroboros.process_custody import record_process
manager_proc = getattr(_EVENT_Q_MANAGER, "_process", None)
manager_pid = int(getattr(manager_proc, "pid", 0) or 0)
if manager_pid:
record_process(
DRIVE_ROOT,
pid=manager_pid,
cmd="multiprocessing SyncManager",
purpose="supervisor_event_queue_manager",
scope="session",
reap_process_group=False,
)
except Exception:
log.warning("Failed to custody-track event queue manager", exc_info=True)
try:
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{
"ts": utc_now_iso(),
"type": "event_queue_generation_started",
"generation": _EVENT_Q_GENERATION,
"server_pid": os.getpid(),
"start_method": _WORKER_START_METHOD,
},
)
except Exception:
log.debug("Failed to record event queue generation", exc_info=True)
return _EVENT_Q
def shutdown_event_q() -> None:
"""Stop the manager on graceful exit; custody reaps it after a hard exit."""
global _EVENT_Q, _EVENT_Q_MANAGER, _EVENT_Q_GENERATION, _EVENT_Q_SHUTDOWN
with _EVENT_Q_LOCK:
_EVENT_Q_SHUTDOWN = True
manager = _EVENT_Q_MANAGER
_EVENT_Q = None
_EVENT_Q_MANAGER = None
_EVENT_Q_GENERATION = ""
if manager is not None:
try:
manager.shutdown()
except Exception:
log.debug("Event queue manager shutdown failed", exc_info=True)
def event_queue_generation() -> str:
"""Stable diagnostic identity for the current server-process event bus."""
get_event_q()
return _EVENT_Q_GENERATION
WORKERS: Dict[int, Worker] = {}
PENDING: List[Dict[str, Any]] = []
RUNNING: Dict[str, Dict[str, Any]] = {}
CRASH_TS: List[float] = []
QUEUE_SEQ_COUNTER_REF: Dict[str, int] = {"value": 0}
# Shared queue lock; queue.py owns the canonical definition.
from supervisor.queue import _queue_lock
def worker_pool_admission_state(ctx: Any = None) -> Dict[str, Any]:
"""Busy/booting pools and live owner waits can queue work; exhausted pools cannot."""
state = _worker_pool_execution_state(
getattr(ctx, "WORKERS", None), running=getattr(ctx, "RUNNING", None),
)
update_reason = repo_writer_admission_closed()
available = state["available"] and not update_reason
return {
**state, "available": available,
"reason_code": "" if available else "worker_pool_unavailable",
"disabled_reason": state["disabled_reason"] or update_reason,
}
def ensure_worker_pool_started(n: int = 0, *, allow_disabled_restart: bool = False) -> bool:
"""Start an absent pool; only explicit internal recovery may clear disablement."""
with _queue_lock:
# Update admission can be closed while the one authorized assisted
# resolver is already running. That does not make an existing healthy
# pool absent and must never trigger a second full-pool spawn.
if _worker_pool_execution_state()["available"]:
return True
state = worker_pool_admission_state()
if state["available"]:
return True
if state["disabled_reason"] not in {"", "no_workers"} and not allow_disabled_restart:
return False
spawn_workers(n)
return True
import threading as _threading
# Admission and activity registration share this short lock; execution never holds it.
_repo_writer_gate_lock = _threading.RLock()
_repo_writer_gate_reason = ""
def close_repo_writer_admission(reason: str) -> None:
"""Stop new in-process chat turns from entering the managed checkout."""
global _repo_writer_gate_reason
with _repo_writer_gate_lock:
_repo_writer_gate_reason = str(reason or "managed update")
def open_repo_writer_admission(expected_reason: str = "") -> bool:
"""Open the process-local gate, optionally only for the exact current owner."""
global _repo_writer_gate_reason
with _repo_writer_gate_lock:
if expected_reason and _repo_writer_gate_reason != expected_reason:
return False
_repo_writer_gate_reason = ""
return True
def repo_writer_admission_closed() -> str:
with _repo_writer_gate_lock:
reason = _repo_writer_gate_reason
if reason:
return reason
# The in-memory latch disappears on restart; the transaction marker does
# not. Let that durable state close the same gate during boot recovery.
try:
from supervisor.update_merge import active_update_tx
tx = active_update_tx()
if tx:
return f"managed_update_tx:{tx.get('phase') or 'unknown'}"
except Exception:
log.warning("Could not read durable managed-update admission state", exc_info=True)
return "managed_update_tx:unreadable"
return ""
def repo_writer_task_allowed(task: Dict[str, Any]) -> bool:
"""Only the tx-authorized resolver may dispatch while the gate is closed."""
if not repo_writer_admission_closed():
return True
try:
from supervisor.update_merge import authorized_assisted_task
return bool(authorized_assisted_task(
str(task.get("id") or ""),
task.get("metadata") if isinstance(task.get("metadata"), dict) else None,
))
except Exception:
return False
def drain_repo_writers(timeout: float = 30.0) -> List[str]:
"""Wait for every registered execution after closing writer admission."""
from supervisor.active_activity import get_direct_activity_registry
return get_direct_activity_registry().wait_until_empty(float(timeout))
def _repo_writer_turn_allowed(chat_id: int) -> bool:
reason = repo_writer_admission_closed()
if not reason:
return True
try:
send_with_budget(
chat_id,
"🔒 An update is using the repository. Try this message again when it finishes.",
role="system", system_type="managed_update_notice")
except Exception:
log.debug("Could not report managed-update writer gate", exc_info=True)
return False
def _get_chat_agent():
"""Construct a fresh native actor; each turn owns its mutable state."""
if not getattr(sys, 'frozen', False) and str(REPO_DIR) not in sys.path:
sys.path.insert(0, str(REPO_DIR))
from ouroboros.agent import make_agent
from ouroboros.owner_wait import direct_owner_wait
from supervisor.queue import transition_acceptance_fence
agent = make_agent(repo_dir=str(REPO_DIR), drive_root=str(DRIVE_ROOT), event_queue=get_event_q())
agent.owner_wait_callback = direct_owner_wait
agent.fence_transition = transition_acceptance_fence # in-process fence: no event, no ack file, no wait
return agent
def get_direct_chat_agent(task_id: str):
from supervisor.active_activity import get_direct_activity_registry
entry = get_direct_activity_registry().get(task_id)
return entry.actor if entry is not None else None
def chat_turn_liveness():
"""All in-process actors, read without taking any execution/admission lock."""
from supervisor.active_activity import get_direct_activity_registry
return [
(str(entry.activity_id), getattr(entry.actor, "_last_activity_ts", None))
for entry in get_direct_activity_registry().actors()
if getattr(entry.actor, "_busy", False)
]
def direct_chat_turns() -> List[Dict[str, Any]]:
from supervisor.active_activity import get_direct_activity_registry
return [turn for entry in get_direct_activity_registry().actors()
if (turn := direct_chat_turn(entry.activity_id)) is not None]
def direct_chat_turn(task_id: str = "") -> Optional[Dict[str, Any]]:
"""Read one addressable actor through the same owner used by routing/controls."""
if not task_id:
turns = direct_chat_turns()
return turns[0] if len(turns) == 1 else None
agent = get_direct_chat_agent(task_id)
if agent is None or not getattr(agent, "_busy", False):
return None
current = str(getattr(agent, "_current_task_id", "") or "")
if not current or (task_id and current != str(task_id)):
return None
if not getattr(agent, "_accepting_owner_messages", False):
return None
metadata = getattr(agent, "_current_task_metadata", {})
metadata = metadata if isinstance(metadata, dict) else {}
text = str(getattr(agent, "_current_task_text", "") or "")
record = {
"id": current,
"type": "task",
"chat_id": int(getattr(agent, "_current_chat_id", 0) or 0),
"project_id": str(metadata.get("project_id") or ""),
"title": str(metadata.get("title") or ""),
"suggested_name": str(metadata.get("suggested_name") or ""),
"objective": text,
"text": text,
"metadata": dict(metadata),
"_is_direct_chat": True,
"_started_at": float(getattr(agent, "_task_started_ts", 0.0) or 0.0),
}
current_focus = metadata.get("focus")
if current_focus is not None:
record["focus"] = current_focus
stamps = getattr(agent, "_direct_turn_stamps", None)
if isinstance(stamps, dict) and str(stamps.get("_task_id") or "") == current:
record.update({key: value for key, value in stamps.items() if key != "_task_id"})
return record
def arm_direct_chat_turn(
task_id: str, arm: Any, *, latch_key: str = "stop_control_msg_id", **extra_stamps: Any,
) -> Optional[Dict[str, Any]]:
"""Atomically arm an owner control against the live direct turn.
The turn's completion path flips ``_busy``/``_accepting_owner_messages``
under the agent's owner-message admission lock; a liveness read followed
by an unlocked control write could therefore arm a turn that ended in
between — a control and an owner toast over an answer that already
landed. So the re-read, the write (``arm(turn)`` returns the control's
msg_id, or "" when nothing was written) and the stamp all happen under
that same lock: either the turn is live for the whole arm and ends only
after it, or it was already gone and NOTHING is written. Returns the
armed record, or None when the turn was gone. ``latch_key`` names the
stamp the control's msg_id lands under (the immediate stop and the
graceful owner-stop episode keep separate latches, so one never hides
the other); ``extra_stamps`` ride along under the same lock."""
agent = get_direct_chat_agent(task_id)
if agent is None:
return None
lock = getattr(agent, "_owner_message_admission_lock", None)
with (lock if lock is not None else _threading.Lock()):
turn = direct_chat_turn(task_id)
if turn is None:
return None
written = arm(turn)
if written:
stamp_direct_chat_turn(task_id, **{latch_key: written, **extra_stamps})
turn.update({latch_key: written, **extra_stamps})
return turn
def stamp_direct_chat_turn(task_id: str, **fields: Any) -> bool:
"""Record owner-control state on the live direct turn — the direct-chat
twin of the latch a pooled task keeps on its RUNNING row (for example the
armed owner-stop control id, so a sweep tick re-arms idempotently instead
of re-toasting). Stamps belong to ONE turn id and vanish with it. Returns
False when that turn is not live (nothing to stamp)."""
agent = get_direct_chat_agent(task_id)
if agent is None or direct_chat_turn(task_id) is None:
return False
stamps = getattr(agent, "_direct_turn_stamps", None)
if not isinstance(stamps, dict) or str(stamps.get("_task_id") or "") != str(task_id):
stamps = {"_task_id": str(task_id)}
agent._direct_turn_stamps = stamps
stamps.update(fields)
return True
def _stage_promoted_initial_attachments(
evt: dict, task: dict, tid: str, *, inherited_manifest: Any = None,
) -> tuple[list[dict], Optional[dict]]:
"""Stage before any durable project/workspace/task admission side effect."""
uploads = evt.get("attachment_uploads")
uploads = uploads if isinstance(uploads, list) else []
inherited_authority = inherited_manifest if isinstance(inherited_manifest, dict) else {}
inherited = inherited_authority.get("attachment_manifest") if inherited_authority else inherited_manifest
inherited = inherited if isinstance(inherited, list) else []
if not uploads and not inherited and "attachment_manifest_ref" not in inherited_authority:
return [], None
manifest: list[dict] = []
try:
from ouroboros.artifacts import (
attachment_manifest_all_rejected,
materialize_inherited_attachment_manifest,
remove_staged_attachments,
stage_task_attachments,
)
from ouroboros.gateway.tasks import _render_attachment_lines
if "attachment_manifest_ref" in inherited_authority:
from ouroboros.artifacts import resolve_attachment_manifest
source_task_id = str((inherited_authority.get("lineage") or {}).get("root_task_id") or "")
inherited = resolve_attachment_manifest(DRIVE_ROOT, source_task_id, inherited_authority)
inherited_rows, inherited_error = materialize_inherited_attachment_manifest(
inherited, DRIVE_ROOT, tid,
)
if inherited_error:
failed_inherited = [
{
**{
key: row[key] for key in ("ordinal", "label")
if isinstance(row, dict) and key in row
},
"status": "rejected",
"reason": "inherited_source_unavailable",
}
for row in inherited
]
return failed_inherited, {
"status": "needs_manual_target",
"reason": "attachment_admission_rejected",
"detail": f"Inherited attachment materialization failed: {inherited_error}",
"attachment_manifest": failed_inherited,
"task_id": tid,
}
upload_rows = stage_task_attachments(DRIVE_ROOT, tid, uploads) if uploads else []
# Preserve the private cleanup ownership carried by the staging helper so
# every later admission refusal remains atomic even after composing
# inherited and newly-uploaded inputs.
manifest = inherited_rows if inherited_rows else upload_rows
if inherited_rows and upload_rows:
inherited_rows.extend(upload_rows)
inherited_owned = getattr(inherited_rows, "_cleanup_owned_paths", None)
upload_owned = getattr(upload_rows, "_cleanup_owned_paths", None)
if isinstance(inherited_owned, set) and isinstance(upload_owned, set):
inherited_owned.update(upload_owned)
for ordinal, row in enumerate(manifest):
if isinstance(row, dict):
row["ordinal"] = ordinal
rendered = _render_attachment_lines(manifest)
# В25c partial-default; a FULLY-rejected set stays atomic (the task
# would start with none of its declared material).
if attachment_manifest_all_rejected(manifest):
remove_staged_attachments(manifest)
from ouroboros.headless import remove_subagent_task_drive
from supervisor.queue import task_settlement_interlock, task_settlement_liveness
remove_subagent_task_drive(DRIVE_ROOT, tid, live=task_settlement_liveness,
guard=task_settlement_interlock, admission_rollback=True)
return manifest, {
"status": "needs_manual_target",
"reason": "attachment_admission_rejected",
"detail": rendered,
"attachment_manifest": manifest,
"task_id": tid,
}
if rendered:
task["attachments"] = manifest
task["attachment_images"] = [
item for item in manifest
if str(item.get("status") or "staged") == "staged" and item.get("is_image")
]
return manifest, None
except Exception:
log.warning("promote: attachment staging failed for %s", tid, exc_info=True)
if manifest:
try:
from ouroboros.artifacts import remove_staged_attachments
remove_staged_attachments(manifest)
except Exception:
log.debug("promote: partial composed attachment cleanup failed", exc_info=True)
declared = [*inherited, *uploads]
manifest = [
{
"ordinal": index,
"status": "rejected",
"reason": "staging_unavailable",
"label": str(item.get("label") or item.get("display_name") or f"attachment {index + 1}")
if isinstance(item, dict) else f"attachment {index + 1}",
}
for index, item in enumerate(declared)
]
return manifest, {
"status": "needs_manual_target",
"reason": "attachment_admission_rejected",
"detail": "\n".join(
f"- {row['label']}: rejected (reason=staging_unavailable, ordinal={row['ordinal']})"
for row in manifest
),
"attachment_manifest": manifest,
"task_id": tid,
}
def _reject_promoted_after_attachment_stage(
outcome: dict, manifest: list[dict],
) -> dict:
"""Central idempotent cleanup for every non-scheduled post-stage exit."""
if manifest:
try:
from ouroboros.artifacts import remove_staged_attachments
remove_staged_attachments(manifest)
except Exception:
log.debug("promote: staged attachment cleanup failed", exc_info=True)
return outcome
def _apply_presence_promotion_authority(
evt: dict, task: dict, *, objective: str, expected_output: str,
) -> list[dict] | dict:
"""Preserve inherited Presence authority while rebinding the new root.
A speaker's promote makes the root answer its conversation; a delegated
descendant's carries only the binding it acts for, so its root is that
binding's related work under the same ceiling and never a speaker.
"""
from ouroboros.dialogue_provenance import presence_root_carrier
contract = evt.get("task_contract") if isinstance(evt.get("task_contract"), dict) else {}
carrier = presence_root_carrier(evt, task_contract=contract)
if not carrier:
return []
if "presence" in carrier:
task["_presence_origin"] = True
task["source"] = "presence_promote"
task.setdefault("metadata", {}).update(carrier)
inherited_manifest = [
dict(row) for row in (contract.get("attachment_manifest") or [])
if isinstance(row, dict)
]
promoted_contract = dict(contract)
promoted_contract.update({
"task_type": "task",
"objective": objective,
"expected_output": expected_output,
"attachment_manifest": [],
# The new root owns its objective: a delegated promoter's claims are not its premise.
"acceptance_claims": [],
"success_criteria": [],
})
promoted_contract.pop("lineage", None)
promoted_contract.pop("attachment_manifest_ref", None)
task["task_contract"] = promoted_contract
return contract if "attachment_manifest_ref" in contract else inherited_manifest
def _relocate_promoted_attachments(task: dict, tid: str, manifest: list[dict]) -> bool:
"""Move a pre-admitted manifest into the selected child drive, if any."""
staged = [row for row in manifest if row.get("status") == "staged" and row.get("abs_path")]
if not staged:
return True
target_root = pathlib.Path(str(task.get("drive_root") or DRIVE_ROOT))
source_dir = pathlib.Path(str(staged[0]["abs_path"])).parent
try:
from ouroboros.artifacts import task_artifact_dir_path
target_dir = task_artifact_dir_path(target_root, tid, create=False) / "attachments"
if source_dir.resolve(strict=False) == target_dir.resolve(strict=False):
return True
target_dir.parent.mkdir(parents=True, exist_ok=True)
source_dir.replace(target_dir)
from ouroboros.artifacts import rebase_staged_attachment_manifest
rebase_staged_attachment_manifest(manifest, source_dir, target_dir)
return True
except Exception:
log.warning("promote: attachment relocation failed for %s", tid, exc_info=True)
return False
def _promoted_scheduled_outcome(task: dict, admitted: Any, tid: str) -> dict:
"""Carry the exact admitted contract to the canonical result writer."""
admitted_contract = (
admitted.get("task_contract")
if isinstance(admitted, dict)
and isinstance(admitted.get("task_contract"), dict)
else task.get("task_contract")
)
return {
"status": "scheduled",
"task_id": tid,
"_admitted_task_contract": dict(admitted_contract or {}),
}
def _announce_created_project(project: Any, tid: str, task: Any = None) -> None:
"""Agent-initiated creation (owner 2=A): owe the durable Main "project
started" row, only when THIS call actually created the project
(``created is True`` from ``create_project``; idempotent replays stay
silent). Fail-soft — a cosmetic row must never reject the promotion or
the mid-task scope call."""
if not isinstance(project, dict) or project.get("created") is not True:
return
try:
from ouroboros.project_dialogue import announce_project_started
announce_project_started(DRIVE_ROOT, project, tid, task=task)
except Exception:
log.debug("project started row failed for %s", project.get("id"), exc_info=True)
from supervisor.log_addressing import TurnEventQueue as _TurnEventQueue # noqa: E402, F401
# Log types the worker sink does NOT forward: each already reaches the dashboard
# live via a dedicated EVENT_Q sibling/handler, so forwarding the worker's
# append_jsonl copy too would double-broadcast (and task_checkpoint would also be
# re-persisted to events.jsonl by _handle_log_event, a double file write).
# The second group publishes the SAME type twice at the producer (a durable
# append plus an emit_log_event live sibling of the identical type): the live
# copy is kept and the forwarded append copy is dropped.
# The server process runs the same suppression discipline over its raw
# append_jsonl->sink broadcasts (server.py installs the wrapper): every type
# here has a dedicated ctx.bridge.push_log at its supervisor handler, so the
# sink copy would be the second delivery of the same event. This is the
# exactly-once contract test_log_forwarding pins with the production sink
# installed. The set is a superset of the worker list because the direct-chat
# agent (an owner's turn and a consciousness wake-up alike) runs inside the
# server process and appends the same worker-shaped rows there.
from supervisor.worker_process import WORKER_LOG_SINK_SUPPRESSED_TYPES # noqa: E402 -- moved span, needed at import time below
SERVER_LOG_SINK_SUPPRESSED_TYPES = WORKER_LOG_SINK_SUPPRESSED_TYPES | frozenset({
"budget_scope_paused", "task_metrics_event", "review_late_result",
"task_cost_finalized", "skill_exec_finished", "skill_exec_failed",
"task_cancel_cascade_noop", "task_cancel_cascade_error", "task_model_wait",
})
_TERMINALIZATION_RETRY_FIELD = "_terminalization_retry"
_TERMINALIZATION_RETRY_STATUSES = frozenset({"failed", "cancelled", "interrupted"})
_CANCEL_INTENT_AUTHORITY_HOLD_FIELD = "_cancel_intent_authority_hold"
def _terminalization_retry_spec(task: Any) -> Optional[Dict[str, Any]]:
"""Return the bounded retry contract carried by a non-dispatchable row."""
if not isinstance(task, dict):
return None
raw = task.get(_TERMINALIZATION_RETRY_FIELD)
if not isinstance(raw, dict):
return None
status = str(raw.get("status") or "failed").strip() or "failed"
return {
"reason": str(raw.get("reason") or "Worker shutdown terminalization was not durable.")[:2000],
"status": status if status in _TERMINALIZATION_RETRY_STATUSES else "failed",
"trigger": str(raw.get("trigger") or "terminalization_retry").strip() or "terminalization_retry",
"reconcile_delegate_custody": bool(raw.get("reconcile_delegate_custody", True)),
}
def _terminalization_retry_request(task: Any) -> Optional[Dict[str, Any]]:
"""Return the exact terminalization request, finalizing interrupted custody."""
spec = _terminalization_retry_spec(task)
if spec is None:
return None
if spec["status"] == "interrupted":
spec["status"] = "failed"
spec["reason"] = (
f"{spec['reason']} Original shutdown status was interrupted; "
"the retained terminalization custody was finalized as failed."
)
return spec
def _retry_claim_identity(task: Any, intent: Any) -> bool:
"""Prove that a marker and claimed intent name the same custody request."""
if not isinstance(task, dict) or not isinstance(intent, dict):
return False
raw = task.get(_TERMINALIZATION_RETRY_FIELD)
request_id = str(raw.get("claim_request_id") or "") if isinstance(raw, dict) else ""
owner = str(raw.get("claim_owner") or "") if isinstance(raw, dict) else ""
if not request_id or owner != "pending_drop":
return False
if request_id != str(intent.get("request_id") or ""):
return False
try:
int(raw.get("claim_generation"))
int(raw.get("claim_pid"))
except (TypeError, ValueError, OverflowError):
return False
return str(intent.get("claim_owner") or "") == owner
def _retry_claim_matches(task: Any, intent: Any) -> bool:
"""Prove that a refused claim is this process's exact marker claim."""
if not _retry_claim_identity(task, intent):
return False
raw = task.get(_TERMINALIZATION_RETRY_FIELD)
try:
return (
int(raw.get("claim_generation")) == int(intent.get("generation"))
and int(raw.get("claim_pid")) == int(intent.get("claim_pid")) == os.getpid()
)
except (TypeError, ValueError, OverflowError):
return False
def _prepare_retry_claim(task: Dict[str, Any], task_id: str) -> Optional[Dict[str, Any]]:
"""Hold or safely recover a pending-drop claim before terminal retry.
A live claim owned by another process is never touched. If the marker names
an older claim whose owner is abandoned, ``claim_intent`` performs the
atomic generation-fenced takeover; the marker is updated before any later
failure can retain it. The returned claim stays held until the terminal
result and event are durable; ``None`` means custody cannot be proven.
"""
if not task_id:
return None
spec = _terminalization_retry_spec(task)
if spec is None or not str(spec.get("trigger") or "").startswith("pending_cancel"):
return {}
try:
from ouroboros.cancel_intents import (
active_intents, claim_intent, claim_is_abandoned,
)
intents = active_intents(DRIVE_ROOT, strict=True)
except Exception:
log.debug("Pending-drop claim read failed for retry %s", task_id, exc_info=True)
return None
intent = intents.get(task_id) if isinstance(intents, dict) else None
if not isinstance(intent, dict):
return {}
if intent.get("state") == "requested":
try:
claim = claim_intent(DRIVE_ROOT, task_id, owner="pending_drop") or {}
except Exception:
log.warning("Pending-drop claim acquisition failed for retry %s", task_id, exc_info=True)
return None
if not claim or claim.get("claim_refused"):
try:
current = active_intents(DRIVE_ROOT, strict=True).get(task_id)
except Exception:
return None
return {} if not isinstance(current, dict) else None
task.update(_attach_retry_claim(task, claim))
return dict(claim)
if intent.get("state") != "claimed":
return None
claim = intent
if not _retry_claim_matches(task, intent):
identity = _retry_claim_identity(task, intent)
raw_marker = task.get(_TERMINALIZATION_RETRY_FIELD)
has_claim_metadata = isinstance(raw_marker, dict) and any(
key in raw_marker
for key in ("claim_request_id", "claim_owner", "claim_generation", "claim_pid")
)
if not identity and has_claim_metadata:
return None
try:
abandoned = bool(claim_is_abandoned(intent))
except Exception:
log.debug("Pending-drop claim liveness failed for retry %s", task_id, exc_info=True)
return None
if not abandoned:
return None
try:
claim = claim_intent(DRIVE_ROOT, task_id, owner="pending_drop") or {}
except Exception:
log.warning("Pending-drop claim takeover failed for retry %s", task_id, exc_info=True)
return None
if not claim or claim.get("claim_refused"):
try:
current = active_intents(DRIVE_ROOT, strict=True).get(task_id)
except Exception:
return None
return {} if not (isinstance(current, dict) and current.get("state") == "claimed") else None
task.update(_attach_retry_claim(task, claim))
return dict(claim)
def _attach_retry_claim(task: Dict[str, Any], claim: Any) -> Dict[str, Any]:
"""Copy a retry row and attach only its current fenced claim metadata."""
retained = dict(task)
raw = retained.get(_TERMINALIZATION_RETRY_FIELD)
if not isinstance(raw, dict) or not isinstance(claim, dict):
return retained
request_id = str(claim.get("request_id") or "")
if not request_id:
return retained
marker = dict(raw)
marker["claim_request_id"] = request_id
marker["claim_owner"] = str(claim.get("claim_owner") or "pending_drop")
try:
marker["claim_generation"] = int(claim.get("generation"))
except (TypeError, ValueError, OverflowError):
marker.pop("claim_generation", None)
try:
marker["claim_pid"] = int(claim.get("claim_pid"))
except (TypeError, ValueError, OverflowError):
marker.pop("claim_pid", None)
retained[_TERMINALIZATION_RETRY_FIELD] = marker
return retained
def _attach_retry_event_state(task: Dict[str, Any], event_published: Optional[bool]) -> Dict[str, Any]:
"""Copy a retry row while recording whether its terminal event was sent."""
retained = dict(task)
if event_published is None:
return retained
raw = retained.get(_TERMINALIZATION_RETRY_FIELD)
if not isinstance(raw, dict):
return retained
marker = dict(raw)
if event_published or marker.get("event_published"):
marker["event_published"] = True
retained[_TERMINALIZATION_RETRY_FIELD] = marker
return retained
def _make_terminalization_retry_task(
task: Dict[str, Any], task_id: str, *, reason: str, status: str, trigger: str,
reconcile_delegate_custody: bool = True, claim: Optional[Dict[str, Any]] = None,
event_published: Optional[bool] = None,
) -> Dict[str, Any]:
"""Copy a killed task into durable, non-dispatchable terminalization custody."""
retry = dict(task) if isinstance(task, dict) else {}
retry["id"] = str(task_id)
if retry.get("chat_id") is None or retry.get("chat_id") == "":
# Snapshot restore requires a concrete chat identity; zero is the
# explicit no-chat route used by headless tasks.
retry["chat_id"] = 0
raw_attempt = retry.get("_attempt")
if raw_attempt is not None:
try:
retry["_attempt"] = max(1, int(raw_attempt))
except (TypeError, ValueError, OverflowError):
retry.pop("_attempt", None)
normalized_status = str(status or "failed").strip() or "failed"
retry_spec = {
"reason": str(reason or "Worker shutdown terminalization was not durable.")[:2000],
"status": normalized_status if normalized_status in _TERMINALIZATION_RETRY_STATUSES else "failed",
"trigger": str(trigger or "terminalization_retry").strip() or "terminalization_retry",
"reconcile_delegate_custody": bool(reconcile_delegate_custody),
}
if event_published:
retry_spec["event_published"] = True
retry[_TERMINALIZATION_RETRY_FIELD] = retry_spec
return _attach_retry_event_state(_attach_retry_claim(retry, claim), event_published)
def _retain_terminalization_retry_task(
task: Dict[str, Any], task_id: str, *, reason: str, status: str, trigger: str,
reconcile_delegate_custody: bool = True, claim: Optional[Dict[str, Any]] = None,
event_published: Optional[bool] = None,
) -> Dict[str, Any]:
"""Return retry custody without replacing an existing stronger marker."""
if isinstance(task, dict) and isinstance(task.get(_TERMINALIZATION_RETRY_FIELD), dict):
return _attach_retry_event_state(_attach_retry_claim(task, claim), event_published)
return _make_terminalization_retry_task(
task,
task_id,
reason=reason,
status=status,
trigger=trigger,
reconcile_delegate_custody=reconcile_delegate_custody,
claim=claim,
event_published=event_published,
)
def _audit_delegate_terminal_custody(
task_id: str, trigger: str, *, enabled: bool = True,
) -> None:
"""Best-effort delegated-run reconciliation before terminal publication."""
if not enabled:
return
try:
from ouroboros import delegate_terminal
audit = delegate_terminal.terminal_reconcile_task(
DRIVE_ROOT, task_id, trigger=trigger,
)
delegate_terminal.record_terminal_reconciliation(
DRIVE_ROOT, task_id, audit,
)
except Exception:
log.warning(
"Terminal delegate reconciliation failed for %s", task_id,
exc_info=True,
)
def _terminalization_status_accepted(
persisted: Any, requested: str, *, allow_interrupted: bool,
) -> bool:
"""Accept only a durable terminal status (or the intentional interrupted marker)."""
from ouroboros.task_results import STATUS_INTERRUPTED, _TRULY_TERMINAL_STATUSES
persisted_status = str(persisted or "").strip()
requested_status = str(requested or "failed").strip() or "failed"
return persisted_status in _TRULY_TERMINAL_STATUSES or (
allow_interrupted
and requested_status == STATUS_INTERRUPTED
and persisted_status == STATUS_INTERRUPTED
)
def _settle_terminalization_task(
task: Dict[str, Any], *, reason: str, status: str, trigger: str,
reconcile_delegate_custody: bool = True, allow_interrupted: bool = False,
event_already_published: bool = False,
) -> bool:
"""Write and publish one shutdown outcome, retaining custody on any failure."""
task_id = str(task.get("id") or "").strip()
if not task_id:
return False
requested_status = str(status or "failed").strip() or "failed"
try:
_audit_delegate_terminal_custody(
task_id, trigger, enabled=reconcile_delegate_custody,
)
persisted = _write_failure_result(
task_id, reason=reason, status=requested_status,
)
except Exception:
log.warning(
"Failed to write failure result for task %s", task_id,
exc_info=True,
)
return False
if not _terminalization_status_accepted(
persisted, requested_status, allow_interrupted=allow_interrupted,
):
log.warning(
"Task %s did not reach an acceptable shutdown status during kill: %r",
task_id,
persisted,
)
return False
if event_already_published:
return True
try:
if not _emit_task_done_terminal(task, task_id, str(persisted).strip()):
log.warning("Failed to emit terminal task_done for %s", task_id)
return False
except Exception:
log.warning("Failed to emit terminal task_done for %s", task_id, exc_info=True)
return False
return True
def _settle_terminalization_retry_task(
task: Dict[str, Any], claim: Optional[Dict[str, Any]] = None,
) -> bool:
"""Retry a retained marker using its own immutable terminal contract."""
request = _terminalization_retry_request(task)
if request is None:
return False
if claim and claim.get("request_id"):
try:
from ouroboros.cancel_intents import claim_still_owned
if not claim_still_owned(DRIVE_ROOT, str(task.get("id") or ""), claim):
return False
except Exception:
log.debug("Pending-drop claim ownership check failed", exc_info=True)
return False
settled = _settle_terminalization_task(
task,
reason=request["reason"],
status=request["status"],
trigger=request["trigger"],
reconcile_delegate_custody=request["reconcile_delegate_custody"],
allow_interrupted=False,
event_already_published=bool(
isinstance(task.get(_TERMINALIZATION_RETRY_FIELD), dict)
and task[_TERMINALIZATION_RETRY_FIELD].get("event_published")
),
)
if not settled:
return False
if not claim or not claim.get("request_id"):
return True
task.update(_attach_retry_event_state(task, True))
try:
from ouroboros.cancel_intents import active_intents, release_claim
if release_claim(
DRIVE_ROOT,
str(task.get("id") or ""),
error="terminalization retry completed",
expected_generation=claim.get("generation"),
request_id=str(claim.get("request_id") or ""),
):
return True
current = active_intents(DRIVE_ROOT, strict=True).get(str(task.get("id") or ""))
except Exception:
log.warning("Pending-drop claim release failed after retry %s", task.get("id"), exc_info=True)
return False
return not (isinstance(current, dict) and current.get("state") == "claimed")
def _retry_terminalization_pending() -> Tuple[List[str], List[str]]:
"""Retry retained shutdown rows before any assignment can inspect them."""
terminalized: List[str] = []
unresolved: List[str] = []
retry_state_changed = False
survivors: List[Dict[str, Any]] = []
for task in list(PENDING):
spec = _terminalization_retry_spec(task)
if spec is None:
survivors.append(task)
continue
raw_marker = task.get(_TERMINALIZATION_RETRY_FIELD) if isinstance(task, dict) else None
marker_before = dict(raw_marker) if isinstance(raw_marker, dict) else None
task_id = str(task.get("id") or "").strip() if isinstance(task, dict) else ""
retry_claim = _prepare_retry_claim(task, task_id)
if retry_claim is None:
unresolved.append(task_id or "<missing-task-id>")
survivors.append(task)
else:
if not task_id or not _settle_terminalization_retry_task(task, retry_claim):
unresolved.append(task_id or "<missing-task-id>")
survivors.append(task)
else:
terminalized.append(task_id)
current_marker = task.get(_TERMINALIZATION_RETRY_FIELD) if isinstance(task, dict) else None
if marker_before != (dict(current_marker) if isinstance(current_marker, dict) else None):
retry_state_changed = True
PENDING[:] = survivors
if retry_state_changed and unresolved:
try:
from supervisor import queue
if queue.persist_queue_snapshot(reason="terminalization_retry_state") is not True:
log.warning("Failed to persist terminalization retry state")
except Exception:
log.warning("Failed to persist terminalization retry state", exc_info=True)
return terminalized, unresolved
def _retry_terminalization_pending_for_assignment(queue: Any) -> None:
"""Settle shutdown custody and persist the changed queue before assignment."""
terminalized, unresolved = _retry_terminalization_pending()
if terminalized:
queue.persist_queue_snapshot(reason="terminalization_retry_settled")
if unresolved:
log.error(
"Shutdown terminalization rows remain deferred; continuing assignment for other tasks: %s",
", ".join(unresolved),
)
_WORKER_PIDS_FILENAME = "worker_pids.json"
def _reconcile_confirmed_dead_review_owner(owner_pid: int) -> None:
_reconcile_review_owners(DRIVE_ROOT, {owner_pid})
from supervisor.worker_pool_lifecycle import _serialized_worker_lifecycle # noqa: E402 -- moved span, decorator read at import time below
@_serialized_worker_lifecycle
def spawn_workers(n: int = 0) -> None:
global _CTX, _WORKER_POOL_DISABLED_REASON
global _LAST_SPAWN_TIME
with _queue_lock:
if WORKERS:
raise RuntimeError(
"spawn_workers requires an empty pool; stop the current workers "
"or use respawn_worker for one slot"
)
# Never hold the queue's process-local threading.RLock across fork: a child
# would inherit it owned by a vanished thread. The dedicated lifecycle lock
# is not used by worker code and serializes competing full-pool starts/kills.
reap_orphaned_workers()
_CTX = mp.get_context(_WORKER_START_METHOD)
event_q = get_event_q()
events_cursor, spawned_at = events_log_cursor(), time.time()
count = n or MAX_WORKERS
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{
"ts": utc_now_iso(),
"type": "worker_spawn_start",
"start_method": _WORKER_START_METHOD,
"count": count,
"event_queue_generation": event_queue_generation(),
"event_queue_transport": "manager",
},
)
new_workers: Dict[int, Worker] = {}
try:
for i in range(count):
in_q = _CTX.Queue()
from supervisor.worker_process import spawn_worker_process
proc = spawn_worker_process(_CTX, i, in_q, event_q, REPO_DIR, DRIVE_ROOT)
# Unassignable until the readiness seam observes this child's worker_ready row.
new_workers[i] = Worker(wid=i, proc=proc, in_q=in_q, busy_task_id=None, reaping=True)
except Exception:
for worker in new_workers.values():
try:
worker.proc.terminate()
worker.proc.join(timeout=2)
except Exception:
pass
raise
with _queue_lock:
if WORKERS:
for worker in new_workers.values():
try:
worker.proc.terminate()
worker.proc.join(timeout=2)
except Exception:
pass
raise RuntimeError("worker pool appeared during serialized startup")
WORKERS.update(new_workers)
_WORKER_POOL_DISABLED_REASON = ""
_LAST_SPAWN_TIME = time.time()
_record_worker_pids()
# Readiness + SHA verification run off-loop so spawn does not block the supervisor loop.
threading.Thread(target=_verify_worker_sha_after_spawn, args=(dict(new_workers), events_cursor, 1, spawned_at), daemon=True).start()
@_serialized_worker_lifecycle
def kill_workers(
force: bool = True,
*,
result_reason: str = "Worker process crashed (crash storm). Task was not completed.",
terminal_status: str = "",
archive_service_logs: bool = True,
disable_reason: str = "",
preserve_pending: bool = False,
preserve_running_task_ids: Optional[set[str]] = None,
reconcile_delegate_custody: bool = True,
reconcile_review_custody: bool = True,
) -> bool:
global _WORKER_POOL_DISABLED_REASON
from supervisor import queue
from supervisor.queue_snapshot import _exact_pause_row
with _queue_lock:
if disable_reason:
_WORKER_POOL_DISABLED_REASON = str(disable_reason)
# Publish the admission fence before slow process-tree teardown so
# concurrent ingress can refuse without starting project/workspace
# side effects while workers are being joined.
try:
fence_persisted = queue.persist_queue_snapshot(reason="worker_pool_disabling") is not False
except Exception:
fence_persisted = False
log.warning("Failed to persist worker-pool disable fence", exc_info=True)
if not fence_persisted:
# No worker or queue mutation happened yet. Re-open the local
# pool admission so an aborted managed update can recover it.
_WORKER_POOL_DISABLED_REASON = ""
log.error("Worker shutdown blocked: disable fence was not durable")
return False
cleared_running = len(RUNNING)
for w in WORKERS.values():
if w.proc.pid:
kill_worker_tree(w.proc.pid)
elif w.proc.is_alive():
w.proc.terminate()
for w in WORKERS.values():
w.proc.join(timeout=3)
_kill_survivors()
dead_pids: set[int] = set()
for w in WORKERS.values():
try:
if w.proc.pid and not w.proc.is_alive():
dead_pids.add(int(w.proc.pid))
except Exception:
log.debug("Cannot confirm worker %s dead", w.wid, exc_info=True)
WORKERS.clear()
orphaned_ids = []
drained_ids = []
terminalization_retry_ids = []
# #1196: an exact mid-run budget pause survives the physical epoch. Its
# PENDING carrier and its durable ``paused`` row are left exactly as they
# are — never cancelled here, never ``pending_parent_interrupted`` — so the
# next boot's ``restore_pending_from_snapshot`` re-validates the durable
# authority and parks the same task id again (or holds it, typed).
retained_paused_ids = []
cleanup_ok = True
try:
done_status = terminal_status or "failed"
preserve_running = set(preserve_running_task_ids or ())
running_task_ids = set(RUNNING) - preserve_running
interrupted_roots = {
str((meta.get("task") or {}).get("root_task_id") or task_id)
for task_id, meta in RUNNING.items()
if isinstance(meta, dict) and task_id not in preserve_running
}
def _settle_killed_pending(
task: Dict[str, Any], *, reason: str, status: str, trigger: str,
) -> bool:
"""Return true only after pending custody is durably terminal."""
return _settle_terminalization_task(
task,
reason=reason,
status=status,
trigger=trigger,
reconcile_delegate_custody=reconcile_delegate_custody,
)
def _retain_killed_pending(
task: Dict[str, Any], *, reason: str, status: str, trigger: str,
) -> Dict[str, Any]:
"""Keep failed pending settlement in non-dispatchable custody."""
task_id = str(task.get("id") or "").strip()
retry = _retain_terminalization_retry_task(
task,
task_id,
reason=reason,
status=status,
trigger=trigger,
reconcile_delegate_custody=reconcile_delegate_custody,
)
terminalization_retry_ids.append(task_id or "<missing-task-id>")
return retry
def _settle_existing_retry(task: Dict[str, Any]) -> Optional[bool]:
"""Process a marker without allowing the current kill reason to replace it."""
if _terminalization_retry_spec(task) is None:
return None
task_id = str(task.get("id") or "").strip()
retry_claim = _prepare_retry_claim(task, task_id)
if retry_claim is None:
terminalization_retry_ids.append(task_id or "<missing-task-id>")
return False
if _settle_terminalization_retry_task(task, retry_claim):
return True
terminalization_retry_ids.append(task_id or "<missing-task-id>")
return False
for task_id in list(RUNNING):
meta = RUNNING.get(task_id) or {}
task = meta.get("task") if isinstance(meta, dict) and isinstance(meta.get("task"), dict) else {}
existing_retry = next(
(
row for row in PENDING
if isinstance(row, dict) and str(row.get("id") or "") == str(task_id)
and _terminalization_retry_spec(row) is not None
),
None,
)
if existing_retry is not None:
if _settle_existing_retry(existing_retry):
PENDING.remove(existing_retry)
orphaned_ids.append(str(task_id))
RUNNING.pop(str(task_id), None)
continue
if task_id in preserve_running:
successor = dict(task)
continuing_wait = isinstance(successor.get("_owner_wait_resume"), dict)
successor["_attempt"] = int(meta.get("attempt") or task.get("_attempt") or 1) + (0 if continuing_wait else 1)
try:
from ouroboros.owner_hurry import retry_reset
if not continuing_wait:
retry_reset(
queue._task_drive_for_task(task, str(task_id)),
DRIVE_ROOT, str(task_id), reason="planned_restart_requeue",
)
except Exception:
log.debug(
"Planned-restart retry reset failed for %s", task_id,
exc_info=True,
)
PENDING.insert(0, successor)
RUNNING.pop(str(task_id), None)
continue
if _settle_terminalization_task(
task,
reason=result_reason,
status=done_status,
trigger="worker_pool_kill",
reconcile_delegate_custody=reconcile_delegate_custody,
allow_interrupted=True,
):
if archive_service_logs:
try:
from ouroboros.tools.services import archive_task_service_logs
archive_task_service_logs(pathlib.Path(DRIVE_ROOT), str(task_id), task)
except Exception:
log.debug("Failed to archive service logs for task %s", task_id, exc_info=True)
orphaned_ids.append(task_id)
else:
# The worker is already dead, but the durable outcome or its
# terminal event is not proven. Move the row to a
# non-dispatchable retry custody that survives snapshot/boot.
retry_task = _retain_terminalization_retry_task(
task,
str(task_id),
reason=result_reason,
status=done_status,
trigger="worker_pool_kill",
reconcile_delegate_custody=reconcile_delegate_custody,
)
RUNNING.pop(str(task_id), None)
replaced = False
for index, row in enumerate(PENDING):
if isinstance(row, dict) and str(row.get("id") or "") == str(task_id):
merged = dict(row)
merged.update(retry_task)
PENDING[index] = merged
replaced = True
break
if not replaced:
PENDING.append(retry_task)
terminalization_retry_ids.append(str(task_id))
if preserve_pending:
kept = []
for task in PENDING:
retry_outcome = _settle_existing_retry(task)
if retry_outcome is not None:
if retry_outcome:
drained_ids.append(str(task.get("id") or ""))
else:
kept.append(task)
continue
if str(task.get("id") or "") in preserve_running:
kept.append(task)
continue
if _exact_pause_row(task):
retained_paused_ids.append(str(task.get("id") or ""))
kept.append(task)
continue
parent_id = str(task.get("parent_task_id") or "")
root_id = str(task.get("root_task_id") or "")
if parent_id and (parent_id in running_task_ids or root_id in interrupted_roots):
tid = str(task.get("id") or "")
if _settle_killed_pending(
task,
reason="Parent task was interrupted before this child started.",
status="cancelled",
trigger="pending_parent_interrupted",
):
drained_ids.append(tid)
else:
kept.append(_retain_killed_pending(
task,
reason="Parent task was interrupted before this child started.",
status="cancelled",
trigger="pending_parent_interrupted",
))
continue
kept.append(task)
PENDING[:] = kept
else:
# Keep the previous snapshot authoritative until every drained
# row has either durable terminal custody or been requeued.
drained = queue.drain_all_pending(persist=False)
for task in drained:
tid = str(task.get("id") or "").strip()
retry_outcome = _settle_existing_retry(task)
if retry_outcome is not None:
if retry_outcome:
drained_ids.append(tid)
else:
PENDING.append(task)
continue
if _exact_pause_row(task):
retained_paused_ids.append(tid)
PENDING.append(task)
continue
if _settle_killed_pending(
task,
reason=result_reason,
status=done_status,
trigger="pending_pool_kill",
):
drained_ids.append(tid)
else:
# No id, failed durable write, or failed notification:
# retain non-dispatchable custody so a later supervisor
# pass can retry without starting the task.
PENDING.append(_retain_killed_pending(
task,
reason=result_reason,
status=done_status,
trigger="pending_pool_kill",
))
if orphaned_ids or drained_ids or terminalization_retry_ids or retained_paused_ids:
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{
"ts": utc_now_iso(),
"type": "zombie_prevention_cleanup",
"orphaned_running": orphaned_ids,
"drained_pending": drained_ids,
"terminalization_retry": terminalization_retry_ids,
**({"retained_budget_paused": retained_paused_ids} if retained_paused_ids else {}),
},
)
except Exception:
cleanup_ok = False
log.warning("Zombie prevention cleanup failed", exc_info=True)
for terminal_id in orphaned_ids:
RUNNING.pop(str(terminal_id), None)
if reconcile_review_custody:
_reconcile_review_owners(DRIVE_ROOT, dead_pids)
try:
snapshot_ok = queue.persist_queue_snapshot(reason="kill_workers") is not False
except Exception:
snapshot_ok = False
log.warning("Failed to persist queue snapshot after worker shutdown", exc_info=True)
if not snapshot_ok:
log.error("Worker shutdown completed without a durable final queue snapshot")
if cleared_running:
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{
"ts": utc_now_iso(),
"type": "running_cleared_on_kill", "count": cleared_running,
"force": force,
},
)
return bool(cleanup_ok and snapshot_ok)
def _persist_pending_terminalization_retries(task_ids: List[str]) -> None:
"""Record retained pending-terminal rows and make their custody restart-safe."""
if not task_ids:
return
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{"ts": utc_now_iso(), "type": "pending_terminal_event_retry", "task_ids": task_ids},
)
try:
from supervisor import queue
if not queue.persist_queue_snapshot(reason="pending_terminal_event_retry"):
log.warning("Failed to persist pending terminal-event retry custody")
except Exception:
log.warning(
"Failed to persist pending terminal-event retry custody",
exc_info=True,
)
def _read_pending_cancel_intents(
active_intents: Any, assignment: Dict[str, bool],
) -> Optional[Dict[str, Dict[str, Any]]]:
"""Read the live cancel projection, preserving authority failure."""
try:
current = active_intents(DRIVE_ROOT, strict=True)
except Exception:
assignment["safe"] = False
log.error(
"Cancel-intent authority became unreadable during pending cleanup; "
"assignment is blocked",
exc_info=True,
)
return None
if not isinstance(current, dict):
assignment["safe"] = False
log.error(
"Cancel-intent authority returned a non-object projection; "
"assignment is blocked",
)
return None
return current
def _settle_cancelled_pending_row(
task: Dict[str, Any],
task_id: str,
marker: Optional[Dict[str, Any]],
current_intents: Dict[str, Dict[str, Any]],
*,
pending_tail: List[Dict[str, Any]],
survivors: List[Dict[str, Any]],
dropped: List[str],
terminalization_retry_ids: List[str],
assignment: Dict[str, bool],
active_intents: Any,
claim_intent: Any,
release_claim: Any,
settle_intent: Any,
intent_outcome_fields: Any,
write_task_result: Any,
status_cancelled: str,
) -> bool:
"""Settle one pre-assignment cancellation; return True to abort the pass.
The caller holds ``_queue_lock`` and owns the ordered pending partition.
Keeping those containers explicit preserves the existing whole-pass abort
semantics without giving this leaf a second queue owner.
"""
def _release_pending_claim(claim: Dict[str, Any], error: str) -> bool:
"""Prove that this drop's claim is no longer live after a failed step."""
request_id = str(claim.get("request_id") or "")
if not request_id:
return True
try:
if release_claim is not None and release_claim(
DRIVE_ROOT, task_id, error=error,
expected_generation=claim.get("generation"), request_id=request_id,
):
return True
except Exception:
log.warning("pending-drop claim release failed for %s", task_id, exc_info=True)
if active_intents is None:
return False
current = _read_pending_cancel_intents(active_intents, assignment)
if current is None:
return False
current = current.get(task_id)
if not isinstance(current, dict) or current.get("state") != "claimed":
return True
# A changed live claim belongs to another custody owner; it is not proof
# that this drop released its own claim.
return False
def _pending_claim_is_ours(claim: Dict[str, Any]) -> bool:
"""Allow publication only while an unresolved claim is still ours."""
if not claim or not claim.get("request_id") or active_intents is None:
return False
current = _read_pending_cancel_intents(active_intents, assignment)
if current is None:
return False
current = current.get(task_id)
try:
return bool(
isinstance(current, dict)
and current.get("state") == "claimed"
and str(current.get("request_id") or "") == str(claim.get("request_id") or "")
and int(current.get("generation") or 0) == int(claim.get("generation") or 0)
and int(current.get("claim_pid") or 0) == os.getpid()
and str(current.get("claim_owner") or "") == "pending_drop"
)
except (TypeError, ValueError, OverflowError):
return False
def _pending_claim_is_released(claim: Dict[str, Any]) -> bool:
"""Observe that a failed settle already removed the claim."""
if not claim or not claim.get("request_id") or active_intents is None:
return not claim or not claim.get("request_id")
current = _read_pending_cancel_intents(active_intents, assignment)
if current is None:
return False
current = current.get(task_id)
return not (isinstance(current, dict) and current.get("state") == "claimed")
# AR2-2 settle-owner unity: claim before writing. A marker may carry the
# exact claim that this same pending-drop process still owns; recognize
# that fenced refusal instead of dropping its custody.
claim: Dict[str, Any] = {}
if task_id in current_intents:
if claim_intent is None:
survivors.append(task)
return False
try:
claim = claim_intent(DRIVE_ROOT, task_id, owner="pending_drop") or {}
except Exception:
# A raised claim write/read is not evidence that another custody
# owner won. Retain this row and stop the pass so a transiently
# unreadable projection cannot lose queue custody or dispatch it.
assignment["safe"] = False
log.error(
"Pending-drop claim authority is unreadable for %s; "
"assignment is blocked",
task_id,
exc_info=True,
)
survivors.append(task)
survivors.extend(pending_tail)
return True
if not isinstance(claim, dict):
assignment["safe"] = False
log.error(
"Pending-drop claim returned a non-object for %s; "
"assignment is blocked",
task_id,
)
survivors.append(task)
survivors.extend(pending_tail)
return True
if claim.get("claim_refused"):
if marker is not None and _retry_claim_matches(task, claim):
pass
elif marker is not None:
survivors.append(task)
return False
else:
# A different live custody owner is waiting for the queue lock
# this pass holds. Keep the row for that owner and abort dispatch.
assignment["safe"] = False
survivors.append(task)
survivors.extend(pending_tail)
return True
if task_id in current_intents and not claim:
# The projection may have changed after the snapshot. Without an owned
# claim this drop cannot fence its settle, so defer the whole pass.
assignment["safe"] = False
log.info(
"Pending cancellation custody changed during claim for %s; "
"deferring the assignment pass",
task_id,
)
survivors.append(task)
survivors.extend(pending_tail)
return True
intent = claim or current_intents.get(task_id) or {}
try:
cost_fields = reconstruct_task_cost(task_id, fields=True)
except Exception:
cost_fields = {
"cost_accounting_status": "unavailable",
"cost_final": False,
# ABI-3: honest name only — nothing emits the retired alias.
"accounted_upper_bound_usd": None,
}
try:
stored = write_task_result(
DRIVE_ROOT, task_id, status_cancelled,
strict_existing_dict=True,
result="Cancelled before start.", **cost_fields,
**intent_outcome_fields(intent),
) or {}
stored_status = str(stored.get("status") or "").strip()
if not stored_status:
raise ValueError("cancel result writer returned no durable status")
except Exception:
log.debug("Failed to finalize cancelled pending task %s", task_id, exc_info=True)
released = _release_pending_claim(claim, "pending-drop persistence failed")
if released:
dropped.append(task_id)
else:
survivors.append(_retain_terminalization_retry_task(
task, task_id,
reason="Cancelled pending task result is not durable and its claim needs recovery.",
status=status_cancelled, trigger="pending_cancel_result",
reconcile_delegate_custody=False, claim=claim,
))
terminalization_retry_ids.append(task_id)
return False
intent_present = task_id in current_intents
# Presence of an active projection is the custody fact; availability of the
# settle helper cannot turn a live claim into a settled one.
intent_settled = not intent_present
claim_released = intent_settled
if intent_present and settle_intent is not None:
try:
# The durable scope is re-read by settle_intent; cascade refusal may
# auto-release this exact claim and is handled as replayable below.
settled_row = settle_intent(
DRIVE_ROOT, task_id,
outcome=("cancelled" if stored_status == status_cancelled else "already_settled"),
detail=("dropped before assignment" if stored_status == status_cancelled else stored_status),
expected_generation=claim.get("generation"),
request_id=str(claim.get("request_id") or ""),
)
intent_settled = settled_row is not None
except Exception:
intent_settled = False
log.debug("Failed to settle cancel intent for pending %s", task_id, exc_info=True)
if not intent_settled:
claim_released = _pending_claim_is_released(claim)
raw_marker = task.get(_TERMINALIZATION_RETRY_FIELD)
event_already_published = bool(
isinstance(raw_marker, dict) and raw_marker.get("event_published")
)
claim_unresolved = not intent_settled and not claim_released
event_published = event_already_published
can_publish = not claim_unresolved or _pending_claim_is_ours(claim)
if can_publish and not event_already_published:
try:
event_published = bool(
_emit_task_done_terminal(task, task_id, stored_status, cost_fields=cost_fields)
)
except Exception:
event_published = False
log.warning(
"Failed to emit terminal task_done for cancelled pending task %s",
task_id, exc_info=True,
)
if claim_unresolved and can_publish:
claim_released = _release_pending_claim(
claim, "pending-drop intent settlement failed",
)
claim_unresolved = not intent_settled and not claim_released
if claim_unresolved:
survivors.append(_retain_terminalization_retry_task(
task, task_id,
reason="Cancelled pending task has an unresolved intent claim.",
status=status_cancelled, trigger="pending_cancel_intent",
reconcile_delegate_custody=False, claim=claim,
event_published=event_published,
))
terminalization_retry_ids.append(task_id)
elif event_published:
dropped.append(task_id)
else:
survivors.append(_retain_terminalization_retry_task(
task, task_id,
reason="Cancelled pending task has an unpublished terminal event.",
status=status_cancelled, trigger="pending_cancel_event",
reconcile_delegate_custody=False, claim=claim,
event_published=False,
))
terminalization_retry_ids.append(task_id)
return False
def _drop_cancelled_pending() -> bool:
"""Remove pending tasks cancelled/finished between scheduling and assignment
so a cancelled subagent never actually starts. Caller holds _queue_lock.
The pre-assignment consult of the durable cancel-intent projection (phase A):
a task with an active intent (or a legacy ``cancel_requested`` latch file) is
settled as ``cancelled`` with a reconstructed — usually confirmed pre-start
zero — cost, never assigned to a worker. Return ``False`` when the
projection cannot be read authoritatively; the caller must then abort the
assignment pass before selecting a worker.
It settles under the SAME rules ``cancel_task_custody`` follows, because it
cannot call custody (the caller already holds ``_queue_lock``, which custody
takes): the STORED status decides the outcome (a task that completed keeps
its result), a durable write that FAILS leaves the intent active for the
watchdog instead of publishing a cancellation that was never persisted, and
the intent's ``requested_by`` stamps ``parent_decision`` at the OUTCOME — a
parent whose reminder is silenced by that field would otherwise keep nagging
about a child it cancelled itself.
"""
if not PENDING:
return True
try:
from ouroboros.task_results import (
STATUS_CANCEL_REQUESTED, STATUS_CANCELLED, _TRULY_TERMINAL_STATUSES,
load_task_result, write_task_result,
)
except Exception:
log.error(
"Pending task-result authority imports are unavailable; assignment is blocked",
exc_info=True,
)
return False
assignment = {"safe": True}
try:
from ouroboros.cancel_intents import (
active_intents, claim_intent, release_claim, settle_intent,
)
if not isinstance(active_intents(DRIVE_ROOT, strict=True), dict):
raise TypeError("cancel-intent authority returned a non-object projection")
except Exception:
log.error(
"Cancel-intent authority is unreadable; pending assignment is blocked",
exc_info=True,
)
return False
try:
from supervisor.task_lifecycle import _intent_outcome_fields
except Exception:
def _intent_outcome_fields(_intent): # type: ignore[misc]
return {}
survivors: List[Dict[str, Any]] = []
dropped: List[str] = []
terminalization_retry_ids: List[str] = []
hold_state_changed = False
pending_rows = list(PENDING)
for index, t in enumerate(pending_rows):
if not assignment["safe"]:
survivors.extend(pending_rows[index:])
break
tid = str(t.get("id") or "") if isinstance(t, dict) else ""
authority_hold = bool(
isinstance(t, dict)
and isinstance(t.get(_CANCEL_INTENT_AUTHORITY_HOLD_FIELD), dict)
)
marker = _terminalization_retry_spec(t)
marker_trigger = str((t.get(_TERMINALIZATION_RETRY_FIELD) or {}).get("trigger") or "") if isinstance(t, dict) else ""
# Existing shutdown custody owns its outcome. Only the cancellation
# markers below may re-enter this drop path to recover an intent claim.
if marker is not None and not marker_trigger.startswith("pending_cancel"):
survivors.append(t)
continue
status = ""
authority_error = False
if tid:
try:
existing = load_task_result(DRIVE_ROOT, tid, strict=True)
status = str((existing or {}).get("status") or "")
except Exception:
authority_error = True
if authority_error:
if authority_hold:
assignment["safe"] = False
log.error(
"Pending cancel-authority hold remains: task-result authority "
"is unreadable for %s",
tid or "<missing-task-id>",
)
survivors.extend(pending_rows[index:])
break
if marker is not None or not tid:
survivors.append(t)
else:
survivors.append(_make_terminalization_retry_task(
t, tid,
reason="Pending task result authority is unreadable; dispatch is blocked.",
status="failed", trigger="pending_result_authority",
reconcile_delegate_custody=False,
))
terminalization_retry_ids.append(tid)
continue
current_intents = _read_pending_cancel_intents(active_intents, assignment)
if current_intents is None:
# Keep this row and every row not yet examined. The caller will
# abort the assignment pass; no ordinary pending row may cross the
# dispatch boundary while cancellation authority is unknown.
survivors.extend(pending_rows[index:])
break
if authority_hold:
if not tid:
assignment["safe"] = False
survivors.extend(pending_rows[index:])
break
hold_state_changed = True
if status in _TRULY_TERMINAL_STATUSES:
dropped.append(tid)
continue
# The projection and result are now authoritative. Remove only the
# restore-time nonterminal hold; an active intent or legacy latch
# falls through into the ordinary cancellation-custody path below.
t = dict(t)
t.pop(_CANCEL_INTENT_AUTHORITY_HOLD_FIELD, None)
if marker is not None and not (
status == STATUS_CANCEL_REQUESTED
or (current_intents is not None and tid in current_intents)
):
survivors.append(t)
continue
if tid and (
status == STATUS_CANCEL_REQUESTED or tid in current_intents
):
abort_assignment = _settle_cancelled_pending_row(
t,
tid,
marker,
current_intents,
pending_tail=pending_rows[index + 1:],
survivors=survivors,
dropped=dropped,
terminalization_retry_ids=terminalization_retry_ids,
assignment=assignment,
active_intents=active_intents,
claim_intent=claim_intent,
release_claim=release_claim,
settle_intent=settle_intent,
intent_outcome_fields=_intent_outcome_fields,
write_task_result=write_task_result,
status_cancelled=STATUS_CANCELLED,
)
if abort_assignment:
break
continue
if status in _TRULY_TERMINAL_STATUSES and marker is None:
dropped.append(tid)
continue
survivors.append(t)
PENDING[:] = survivors
if hold_state_changed:
try:
from supervisor import queue
persisted = queue.persist_queue_snapshot(
reason="cancel_intent_authority_hold_resolved",
)
except Exception:
persisted = False
log.error(
"Failed to persist resolved cancel-intent authority hold",
exc_info=True,
)
if persisted is not True:
# A resumed ordinary row must not cross dispatch until removal of
# its restart-visible hold is durable. Restore the pre-pass queue;
# any terminal side effects are monotonic and will be observed on
# the next pass.
PENDING[:] = pending_rows
assignment["safe"] = False
if dropped:
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{"ts": utc_now_iso(), "type": "pending_cancelled_dropped", "task_ids": dropped},
)
_persist_pending_terminalization_retries(terminalization_retry_ids)
return bool(assignment["safe"])
def _normalize_pending_task_depth(task: Dict[str, Any]) -> str:
"""Normalize a pending depth, or return the typed-ingress error."""
try:
task["depth"] = parse_task_depth(task.get("depth"), default=0)
except (TypeError, ValueError) as exc:
return str(exc)
return ""
def _terminalize_invalid_pending_depth(task: Dict[str, Any], detail: str) -> bool:
"""Give a bypassed pending row terminal custody before any worker dispatch."""
task_id = str(task.get("id") or "").strip()
if not task_id:
return False
result_root = pathlib.Path(task.get("budget_drive_root") or DRIVE_ROOT)
raw_depth = task.get("depth")
if raw_depth is not None and not isinstance(raw_depth, (str, int, float, bool)):
raw_depth = repr(raw_depth)[:200]
try:
from ouroboros.task_results import STATUS_FAILED, write_task_result
try:
cost_fields = reconstruct_task_cost(task_id, fields=True, drive_root=result_root)
except Exception:
cost_fields = {
"cost_accounting_status": "unavailable",
"cost_final": False,
# ABI-3: honest name only — nothing emits the retired alias.
"accounted_upper_bound_usd": None,
}
stored = write_task_result(
result_root,
task_id,
STATUS_FAILED,
strict_existing_dict=True,
reason_code="invalid_task_depth",
result=f"Task was not dispatched: {str(detail)[:500]}",
# Keep the rejected value as evidence, never as an executable depth.
depth=0,
raw_task_depth=raw_depth,
invalid_task_depth=True,
parent_task_id=task.get("parent_task_id"),
root_task_id=task.get("root_task_id"),
delegation_role=task.get("delegation_role"),
chat_id=task.get("chat_id"),
metadata=task.get("metadata") if isinstance(task.get("metadata"), dict) else {},
**cost_fields,
)
if str((stored or {}).get("status") or "") != STATUS_FAILED:
return False
except Exception:
log.warning("Failed to terminalize invalid pending task %s", task_id, exc_info=True)
return False
# Durable custody is authoritative; notification and diagnostics are
# best-effort and must not make an already-terminal row look uncommitted.
_emit_task_done_terminal(
task, task_id, "failed", reason_code="invalid_task_depth", cost_fields=cost_fields,
)
try:
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{
"ts": utc_now_iso(),
"type": "pending_invalid_task_depth",
"task_id": task_id,
"raw_task_depth": raw_depth,
},
)
except Exception:
log.debug("Failed to record invalid pending depth for %s", task_id, exc_info=True)
return True
def _quarantine_invalid_pending_depths() -> tuple[list[str], list[str]]:
"""Settle malformed pending rows before budget or capacity filters run."""
terminalized: list[str] = []
unresolved: list[str] = []
for index in range(len(PENDING) - 1, -1, -1):
task = PENDING[index]
if not isinstance(task, dict):
continue
if _terminalization_retry_spec(task) is not None:
# A shutdown custody row has an explicit retry contract; depth
# quarantine must not rewrite it before its intended outcome lands.
continue
detail = _normalize_pending_task_depth(task)
if not detail:
continue
task_id = str(task.get("id") or "").strip()
if _terminalize_invalid_pending_depth(task, detail):
PENDING.pop(index)
if task_id:
terminalized.append(task_id)
else:
unresolved.append(task_id or "<missing-task-id>")
if terminalized:
try:
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{
"ts": utc_now_iso(),
"type": "pending_invalid_task_depth_quarantined",
"task_ids": terminalized,
},
)
except Exception:
log.debug("Failed to record invalid pending-depth quarantine", exc_info=True)
return terminalized, unresolved
def _invalid_depth_deferred(task: dict, deferred_ids: set[str]) -> bool:
task_id = str(task.get("id") or "").strip()
return (
_terminalization_retry_spec(task) is not None
or (task_id or "<missing-task-id>") in deferred_ids
)
def _drop_assignable_evolution_tasks(deferred_ids: set[str]) -> list[str]:
"""Remove policy-blocked evolution rows while retaining deferred custody."""
blocked_ids = []
kept = []
for task in PENDING:
if str(task.get("type") or "") == "evolution" and not _invalid_depth_deferred(task, deferred_ids):
blocked_ids.append(str(task.get("id") or ""))
else:
kept.append(task)
PENDING[:] = kept
return blocked_ids
def _running_subagent_count(root_task_id: str) -> int:
if not root_task_id:
return 0
count = 0
for meta in RUNNING.values():
task = meta.get("task") if isinstance(meta, dict) else None
if (
isinstance(task, dict)
and str(task.get("delegation_role") or "") == "subagent"
and str(task.get("root_task_id") or "") == root_task_id
):
count += 1
return count
def _assignment_depth_reservation_admits(candidate: dict) -> bool:
root_task_id = str(candidate.get("root_task_id") or "")
parent_id = str(candidate.get("parent_task_id") or "").strip()
if not root_task_id or not parent_id:
return False
parent_running = any(
str((meta.get("task") if isinstance(meta, dict) else {}).get("id") or "") == parent_id
and str((meta.get("task") if isinstance(meta, dict) else {}).get("root_task_id") or "") == root_task_id
and str((meta.get("task") if isinstance(meta, dict) else {}).get("delegation_role") or "") == "subagent"
for meta in RUNNING.values()
)
if not parent_running:
return False
direct_running_children = sum(
1 for meta in RUNNING.values()
if isinstance(meta, dict)
and isinstance(meta.get("task"), dict)
and str(meta["task"].get("root_task_id") or "") == root_task_id
and str(meta["task"].get("delegation_role") or "") == "subagent"
and str(meta["task"].get("parent_task_id") or "").strip() == parent_id
)
return direct_running_children < 1
def _worker_crash_storm_detected(
*, busy_crashes: int, dead_detections: int, crashed_tasks: List[Dict[str, Any]]
) -> bool:
now = time.time()
alive_now = sum(1 for worker in WORKERS.values() if worker.proc.is_alive())
if dead_detections:
# Only count busy crashes or all-workers-dead as storm signals.
if busy_crashes > 0 or alive_now == 0:
CRASH_TS.extend([now] * max(1, dead_detections))
else:
CRASH_TS.clear()
CRASH_TS[:] = [stamp for stamp in CRASH_TS if (now - stamp) < 60.0]
if len(CRASH_TS) < 3:
return False
# Do not execv on crash storms; keep direct-chat mode alive.
st = load_state()
append_jsonl(
DRIVE_ROOT / "logs" / "supervisor.jsonl",
{
"ts": utc_now_iso(),
"type": "crash_storm_detected",
"crash_count": len(CRASH_TS),
"worker_count": len(WORKERS),
"crashed_tasks": crashed_tasks,
},
)
if st.get("owner_chat_id"):
send_with_budget(
int(st["owner_chat_id"]),
"⚠️ Frequent worker crashes. Multiprocessing workers disabled, "
"continuing in direct-chat mode (threading).",
is_progress=True,
progress_meta={
"task_incident": "worker_crash_storm",
"toast_once": f"worker-crash-storm:{int(min(CRASH_TS) if CRASH_TS else now)}",
},
role="system", system_type="worker_failure")
return True
# v7next F1 (D08): moved spans live in their owner leaves; re-exported here
# so this facade stays the single import surface for callers and tests.
from supervisor.worker_assignment import ( # noqa: E402, F401 -- intentional public re-exports
_cancel_unauthorized_evolution,
_evolution_assignment_error,
assign_tasks,
)
from supervisor.worker_chat_lane import ( # noqa: E402, F401 -- intentional public re-exports
_broadcast_task_named,
_handle_chat_direct_locked,
_run_chat_task,
auto_resume_after_restart,
handle_chat_direct,
handle_wake_direct,
)
from supervisor.worker_health import ( # noqa: E402, F401 -- intentional public re-exports
_emit_task_done_terminal,
_ensure_workers_healthy_locked,
ensure_workers_healthy,
terminal_task_metadata,
)
from supervisor.worker_pool_lifecycle import ( # noqa: E402, F401 -- intentional public re-exports
_WORKER_LIFECYCLE_LOCK,
_worker_pool_execution_state,
disable_exhausted_worker_pool,
_first_worker_event_since,
_kill_survivors,
_record_worker_pids,
_serialized_worker_lifecycle,
_verify_worker_sha_after_spawn,
_worker_pids_path,
_write_failure_result,
events_log_cursor,
kill_worker_tree,
kill_workers_for_update,
reap_orphaned_workers,
respawn_worker,
)
from supervisor.worker_process import ( # noqa: E402, F401 -- intentional public re-exports
WORKER_LOG_SINK_SUPPRESSED_TYPES,
_bind_worker_repo_root,
_current_custody_session_id,
_log_worker_crash,
_prepare_worker_task_runtime,
worker_main,
)
from supervisor.worker_promotion import ( # noqa: E402, F401 -- intentional public re-exports
_admit_promoted_workspace,
_canonical_promoted_repair_constraint,
_origin_from_mapping,
_origin_from_task_record,
_promote_duplicate_reason,
_promoted_force_plan_metadata,
_report_binding_failure,
ensure_project_scope,
promote_chat_to_task,
)