atlas-iac/services/hermes/scripts/cli_lane_runner.py

3083 lines
112 KiB
Python

#!/usr/bin/env python3
"""Run durable Codex and Claude workers from Hermes' authoritative Kanban."""
from __future__ import annotations
import concurrent.futures
import ctypes
import errno
import hashlib
import json
import os
import re
import selectors
import signal
import sqlite3
import stat
import subprocess
import sys
import threading
import time
import uuid
import urllib.error
import urllib.request
from dataclasses import asdict, dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable
import cli_lane_goal
DATA_ROOT = Path(os.environ.get("HERMES_HOME", "/opt/data"))
SWITCHYARD_URL = os.environ.get(
"HERMES_SWITCHYARD_URL",
"http://hermes-switchyard.hermes.svc.cluster.local:9005/v1/chat/completions",
)
STATE_ROOT = DATA_ROOT / "cli-lanes"
CODEX_BIN = DATA_ROOT / "tools/bin/codex"
CLAUDE_BIN = DATA_ROOT / "tools/bin/claude"
CLAUDE_SETTINGS = DATA_ROOT / "home/.claude/settings.json"
RESULT_SCHEMA_PATH = STATE_ROOT / "worker-result.schema.json"
EFFORTS = ("low", "medium", "high", "xhigh")
EXTERNAL_PREFIX = "cli-"
DEFAULT_CLAIM_TTL = 7 * 24 * 60 * 60
DEFAULT_MAX_RUNTIME = 12 * 60 * 60
HEARTBEAT_SECONDS = 20
PROVIDER_HEALTH_MAX_AGE_SECONDS = 5 * 60
PROVIDER_AUTH_FAILURE_MAX_AGE_SECONDS = 12 * 60 * 60
KANBAN_STORAGE_ATTEMPTS = 5
ARTIFACT_GC_INTERVAL_SECONDS = 5 * 60
ARTIFACT_RETENTION_AGE_SECONDS = 30 * 24 * 60 * 60
ARTIFACT_RETENTION_COUNT = 256
ARTIFACT_RETENTION_BYTES = 128 * 1024 * 1024
MAX_TERMINAL_RECORD_BYTES = 1024 * 1024
QUARANTINE_HASH_BYTES = 64 * 1024
PROVIDER_HEALTH_PATHS = {
"codex": DATA_ROOT / "provider-health/codex.json",
"claude": DATA_ROOT / "provider-health/claude.json",
}
WORKTREE_LOCK = threading.Lock()
BOARD_CORRUPTION_ERRORS: dict[str, str] = {}
LAST_ARTIFACT_GC = 0.0
CAPACITY_PATTERN = re.compile(
r"(?:rate.?limit|capacity|overload|usage.?limit|quota|credit|exhaust|429|529|authentication|oauth|token.*expired)",
re.I,
)
NO_CLAUDE_SESSION = "No conversation found with session ID:"
CLAUDE_SESSION_COLLISION = "Session ID already in use"
NO_CODEX_THREAD = "no rollout found for thread id"
RESULT_SCHEMA: dict[str, Any] = {
"type": "object",
"additionalProperties": False,
"required": [
"status",
"summary",
"changed_files",
"tests_run",
"artifacts",
"findings",
"blockers",
],
"properties": {
"status": {
"type": "string",
"enum": sorted(cli_lane_goal.RESULT_STATUSES),
},
"summary": {"type": "string"},
"changed_files": {"type": "array", "items": {"type": "string"}},
"tests_run": {"type": "array", "items": {"type": "string"}},
"artifacts": {"type": "array", "items": {"type": "string"}},
"findings": {
"type": "array",
"items": {"type": "string"},
"description": (
"Defects, risks, or observations discovered by a review or diagnosis. "
"Findings do not prevent the assigned review or diagnosis from completing."
),
},
"blockers": {
"type": "array",
"items": {"type": "string"},
"description": (
"Concrete obstacles that prevent completion of the assigned task itself. "
"This must be empty when status is completed; review findings belong in findings."
),
},
},
}
@dataclass(frozen=True)
class Route:
"""One provider/model/effort decision for an observable worker boundary."""
provider: str
model: str
effort: str
profile: str
classifier: str
reason: str
latency_ms: int
fallback_chain: tuple[str, ...]
@dataclass
class ProcessResult:
"""Captured outcome from one provider CLI invocation."""
returncode: int
output: str
structured: dict[str, Any] | None
capacity_failure: bool
class TerminalFinalizationPending(RuntimeError):
"""An accepted worker result is durable but not committed to Kanban yet."""
@dataclass(frozen=True)
class TerminalIdentity:
"""Replay authority derived only from a journal's directory and filename."""
board: str
task_id: str
run_id: int
state: str
@dataclass
class TerminalSnapshot:
"""One inode-bound, bounded journal read held open across finalization."""
document: dict[str, Any]
file_stat: os.stat_result
descriptor: int
directory_descriptor: int
def close(self) -> None:
"""Release the pinned file and directory descriptors."""
os.close(self.descriptor)
os.close(self.directory_descriptor)
@dataclass
class TerminalRecoverySnapshot:
"""One raw directory entry pinned before recovery classifies its payload."""
document: dict[str, Any] | None
file_stat: os.stat_result
descriptor: int | None
directory_descriptor: int
prefix: bytes
invalid_reason: str | None
def close(self) -> None:
"""Release the descriptors retained across classification/quarantine."""
if self.descriptor is not None:
os.close(self.descriptor)
self.descriptor = None
if self.directory_descriptor >= 0:
os.close(self.directory_descriptor)
self.directory_descriptor = -1
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def atomic_json(path: Path, value: dict[str, Any], mode: int = 0o600) -> None:
"""Durably replace a small state document without following temp symlinks."""
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(path.parent, 0o700, follow_symlinks=False)
temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = None
try:
descriptor = os.open(temporary, flags, mode)
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
descriptor = None
json.dump(value, stream, indent=2, sort_keys=True)
stream.write("\n")
stream.flush()
os.fsync(stream.fileno())
os.chmod(temporary, mode, follow_symlinks=False)
os.replace(temporary, path)
_fsync_directory(path.parent)
finally:
if descriptor is not None:
os.close(descriptor)
try:
temporary.unlink()
except FileNotFoundError:
pass
def _fsync_directory(directory: Path) -> None:
"""Persist directory-entry changes after an atomic rename or quarantine."""
flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(directory, flags)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
def load_json(path: Path) -> dict[str, Any]:
try:
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
with os.fdopen(descriptor, "r", encoding="utf-8") as stream:
value = json.load(stream)
except (OSError, UnicodeError, json.JSONDecodeError):
return {}
return value if isinstance(value, dict) else {}
def parse_assignee(assignee: str) -> tuple[str | None, str | None]:
"""Parse external lane overrides while leaving cli-auto fully automatic."""
value = str(assignee or "").strip().lower()
if value == "cli-auto":
return None, None
match = re.fullmatch(r"cli-(codex|claude)-(low|medium|high|xhigh)", value)
if not match:
raise ValueError(f"unsupported external lane assignee: {assignee}")
return match.group(1), match.group(2)
def fresh_unavailable_provider(now: float | None = None) -> str | None:
"""Return one recently proven-down provider for automatic route exclusion."""
current = time.time() if now is None else now
unavailable: list[str] = []
for provider, path in PROVIDER_HEALTH_PATHS.items():
health = load_json(path)
try:
age = current - path.stat().st_mtime
except OSError:
continue
max_age = (
PROVIDER_AUTH_FAILURE_MAX_AGE_SECONDS
if health.get("authenticated") is False
else PROVIDER_HEALTH_MAX_AGE_SECONDS
)
if (
0 <= age <= max_age
and health.get("state") == "unavailable"
):
unavailable.append(provider)
# If both providers are down, keep the normal attempt/fallback path so the
# card records authoritative current errors instead of trusting snapshots.
return unavailable[0] if len(unavailable) == 1 else None
def _decode_worker_target(value: str) -> tuple[str, str, str]:
"""Decode the selected model header emitted by a worker decision route."""
parts = value.split("/", 3)
if len(parts) != 4 or parts[0] != "worker":
raise RuntimeError(f"invalid Switchyard worker target: {value}")
provider, model, effort = parts[1:]
if provider not in {"codex", "claude"} or effort not in EFFORTS:
raise RuntimeError(f"unsupported Switchyard worker target: {value}")
return provider, model, effort
def select_route(
prompt: str,
assignee: str,
*,
exclude_provider: str | None = None,
exclude_reason: str | None = None,
switchyard_url: str = SWITCHYARD_URL,
open_request: Callable[..., Any] = urllib.request.urlopen,
) -> Route:
"""Ask Switchyard to select one native CLI worker at this boundary."""
started = time.monotonic()
manual_provider, manual_effort = parse_assignee(assignee)
if manual_provider and manual_effort:
route_id = f"atlas/worker/manual/{manual_provider}/{manual_effort}"
source = "switchyard-manual"
else:
route_id = "atlas/worker/auto/maximum"
source = "switchyard-classifier"
context = prompt
if exclude_provider:
reason = exclude_reason or "failed or exhausted capacity at this boundary"
context += (
f"\n\nRouting constraint: the {exclude_provider} provider {reason}. "
"Do not select it."
)
payload = json.dumps(
{
"model": route_id,
"messages": [{"role": "user", "content": context}],
"stream": False,
"max_tokens": 1,
}
).encode("utf-8")
request = urllib.request.Request(
switchyard_url,
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with open_request(request, timeout=60) as response:
selected = str(response.headers.get("x-model-router-selected-model") or "")
rationale = str(response.headers.get("x-model-router-rationale") or "")
response_body = response.read()
except (OSError, urllib.error.URLError) as exc:
raise RuntimeError(f"Switchyard worker routing failed: {exc}") from exc
provider, model, effort = _decode_worker_target(selected)
# Switchyard preserves the stable tier target in the selection header and
# top-level response model. The worker broker's assistant content contains
# the steward-resolved provider model required by the native CLI.
try:
response_document = json.loads(response_body)
resolved_target = str(response_document["choices"][0]["message"]["content"] or "")
resolved_provider, resolved_model, resolved_effort = _decode_worker_target(resolved_target)
required_prefix = "gpt-" if provider == "codex" else "claude-"
if (
resolved_provider == provider
and resolved_effort == effort
and resolved_model.startswith(required_prefix)
):
model = resolved_model
except (AttributeError, IndexError, KeyError, RuntimeError, TypeError, ValueError, json.JSONDecodeError):
pass
if exclude_provider and provider == exclude_provider:
alternate = "claude" if provider == "codex" else "codex"
guarded = select_route(
prompt,
f"cli-{alternate}-{effort}",
switchyard_url=switchyard_url,
open_request=open_request,
)
return Route(
provider=guarded.provider,
model=guarded.model,
effort=guarded.effort,
profile=guarded.profile,
classifier=f"{source}-health-guard",
reason=(
f"Switchyard selected excluded {exclude_provider}; preserved "
f"its {effort} effort on healthy-provider route. {guarded.reason}"
),
latency_ms=int((time.monotonic() - started) * 1000),
fallback_chain=(),
)
return Route(
provider=provider,
model=model,
effort=effort,
profile=f"{provider}-{effort}",
classifier=source,
reason=rationale or f"Switchyard selected {selected}",
latency_ms=int((time.monotonic() - started) * 1000),
fallback_chain=(),
)
def state_path(board: str, task_id: str) -> Path:
safe_board = re.sub(r"[^a-zA-Z0-9_.-]+", "-", board)
safe_task = re.sub(r"[^a-zA-Z0-9_.-]+", "-", task_id)
return STATE_ROOT / safe_board / f"{safe_task}.json"
def _result_path(state_file: Path, run_id: Any, sequence: int) -> Path:
"""Return a unique provider-result path; completed turns are never reused."""
safe_run = re.sub(r"[^a-zA-Z0-9_.-]+", "-", str(run_id or "unknown"))
return state_file.with_name(
f"{state_file.stem}.run-{safe_run}.provider-{sequence}.result.json"
)
def _candidate_path(state_file: Path, run_id: Any, sequence: int) -> Path:
"""Return the durable path for one exact structured worker response."""
safe_run = re.sub(r"[^a-zA-Z0-9_.-]+", "-", str(run_id or "unknown"))
return state_file.with_name(
f"{state_file.stem}.run-{safe_run}.candidate-{sequence}.json"
)
def _terminal_path(
state_file: Path,
run_id: Any,
state: str = "pending",
) -> Path:
"""Return the identity-bound replay path for an accepted terminal response."""
if state not in {"pending", "committed"}:
raise ValueError(f"invalid terminal journal state: {state}")
safe_run = re.sub(r"[^a-zA-Z0-9_.-]+", "-", str(run_id or "unknown"))
return state_file.with_name(
f"{state_file.stem}.run-{safe_run}.terminal.{state}.json"
)
_TERMINAL_NAME = re.compile(
r"^(?P<task>[a-zA-Z0-9_.-]+)\.run-(?P<run>[0-9]+)\."
r"terminal\.(?P<state>pending|committed)\.json$"
)
_TERMINAL_EVIDENCE_NAME = re.compile(
r"^(?P<task>[a-zA-Z0-9_.-]+)\.run-(?P<run>[0-9]+)\."
r"terminal\.(?P<state>prepared|conflict)-(?P<digest>[a-f0-9]{32})\.json$"
)
_RETIRE_NAME = re.compile(
r"^\.retire\.(?P<path_digest>[a-f0-9]{16})\."
r"(?P<source_digest>[a-f0-9]{16})\.(?P<sequence>[0-9]+)$"
)
_LEGACY_RETIRE_NAME = re.compile(r"^\.retire\.[a-f0-9]{32}\.[0-9]+$")
_SAFE_BOARD = re.compile(r"^[a-zA-Z0-9_.-]+$")
def _terminal_identity(path: Path) -> TerminalIdentity | None:
"""Parse replay authority lexically before opening a journal or board."""
try:
relative = path.relative_to(STATE_ROOT)
except ValueError:
return None
if len(relative.parts) != 2:
return None
board, filename = relative.parts
match = _TERMINAL_NAME.fullmatch(filename)
try:
board_stat = path.parent.stat(follow_symlinks=False)
except OSError:
return None
if (
not match
or not stat.S_ISDIR(board_stat.st_mode)
or not _SAFE_BOARD.fullmatch(board)
or board in {".", ".."}
or match.group("task") in {".", ".."}
):
return None
return TerminalIdentity(
board=board,
task_id=match.group("task"),
run_id=int(match.group("run")),
state=match.group("state"),
)
def _terminal_evidence_identity(path: Path) -> TerminalIdentity | None:
"""Parse immutable prepared/conflict evidence authority from its path."""
try:
relative = path.relative_to(STATE_ROOT)
except ValueError:
return None
if len(relative.parts) != 2:
return None
board, filename = relative.parts
match = _TERMINAL_EVIDENCE_NAME.fullmatch(filename)
try:
board_stat = path.parent.stat(follow_symlinks=False)
except OSError:
return None
if (
not match
or not stat.S_ISDIR(board_stat.st_mode)
or not _SAFE_BOARD.fullmatch(board)
or board in {".", ".."}
or match.group("task") in {".", ".."}
):
return None
return TerminalIdentity(
board=board,
task_id=match.group("task"),
run_id=int(match.group("run")),
state=match.group("state"),
)
def _read_bounded(descriptor: int, limit: int) -> bytes:
"""Read at most ``limit`` bytes from a regular file descriptor."""
chunks: list[bytes] = []
remaining = max(0, limit)
while remaining:
chunk = os.read(descriptor, min(64 * 1024, remaining))
if not chunk:
break
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
def _open_small_json_snapshot(path: Path) -> TerminalSnapshot | None:
"""Open and pin one bounded, singly-linked JSON document."""
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
file_flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
directory = None
descriptor = None
try:
directory = os.open(path.parent, directory_flags)
descriptor = os.open(path.name, file_flags, dir_fd=directory)
opened = os.fstat(descriptor)
if (
not stat.S_ISREG(opened.st_mode)
or opened.st_nlink != 1
or opened.st_size > MAX_TERMINAL_RECORD_BYTES
):
return None
payload = _read_bounded(descriptor, MAX_TERMINAL_RECORD_BYTES + 1)
if len(payload) > MAX_TERMINAL_RECORD_BYTES:
return None
current = os.stat(
path.name,
dir_fd=directory,
follow_symlinks=False,
)
if (
current.st_dev != opened.st_dev
or current.st_ino != opened.st_ino
or current.st_size != opened.st_size
or len(payload) != opened.st_size
):
return None
value = json.loads(payload.decode("utf-8"))
if not isinstance(value, dict):
return None
snapshot = TerminalSnapshot(value, opened, descriptor, directory)
descriptor = None
directory = None
return snapshot
except (OSError, UnicodeError, json.JSONDecodeError):
return None
finally:
if descriptor is not None:
os.close(descriptor)
if directory is not None:
os.close(directory)
def _open_terminal_recovery_snapshot(path: Path) -> TerminalRecoverySnapshot | None:
"""Pin the entry recovery inspected, even when its payload is malformed."""
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
file_flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
directory = None
descriptor = None
try:
directory = os.open(path.parent, directory_flags)
observed = os.stat(
path.name,
dir_fd=directory,
follow_symlinks=False,
)
if not stat.S_ISREG(observed.st_mode):
snapshot = TerminalRecoverySnapshot(
None,
observed,
None,
directory,
b"",
"non-regular",
)
directory = None
return snapshot
try:
descriptor = os.open(path.name, file_flags, dir_fd=directory)
except OSError:
snapshot = TerminalRecoverySnapshot(
None,
observed,
None,
directory,
b"",
"open-failed",
)
directory = None
return snapshot
opened = os.fstat(descriptor)
if opened.st_dev != observed.st_dev or opened.st_ino != observed.st_ino:
os.close(descriptor)
descriptor = None
snapshot = TerminalRecoverySnapshot(
None,
observed,
None,
directory,
b"",
"identity-changed-during-open",
)
directory = None
return snapshot
observed = opened
if observed.st_nlink != 1:
snapshot = TerminalRecoverySnapshot(
None,
observed,
descriptor,
directory,
b"",
"hardlinked",
)
descriptor = None
directory = None
return snapshot
if observed.st_size > MAX_TERMINAL_RECORD_BYTES:
prefix = _read_bounded(descriptor, QUARANTINE_HASH_BYTES)
snapshot = TerminalRecoverySnapshot(
None,
observed,
descriptor,
directory,
prefix,
"oversized",
)
descriptor = None
directory = None
return snapshot
payload = _read_bounded(descriptor, MAX_TERMINAL_RECORD_BYTES + 1)
after_read = os.fstat(descriptor)
unchanged = (
after_read.st_dev == observed.st_dev
and after_read.st_ino == observed.st_ino
and after_read.st_size == observed.st_size
and after_read.st_mtime_ns == observed.st_mtime_ns
and after_read.st_ctime_ns == observed.st_ctime_ns
and len(payload) == observed.st_size
)
document = None
invalid_reason = "unstable-payload"
if unchanged:
try:
parsed = json.loads(payload.decode("utf-8"))
if isinstance(parsed, dict):
document = parsed
invalid_reason = None
else:
invalid_reason = "non-object-payload"
except (UnicodeError, json.JSONDecodeError):
invalid_reason = "malformed-payload"
snapshot = TerminalRecoverySnapshot(
document,
after_read,
descriptor,
directory,
payload[:QUARANTINE_HASH_BYTES],
invalid_reason,
)
descriptor = None
directory = None
return snapshot
except OSError:
return None
finally:
if descriptor is not None:
os.close(descriptor)
if directory is not None:
os.close(directory)
def _open_terminal_snapshot(
path: Path,
identity: TerminalIdentity,
) -> TerminalSnapshot | None:
"""Open a terminal journal only after its lexical identity is verified."""
if _terminal_identity(path) != identity:
return None
return _open_small_json_snapshot(path)
def _load_terminal_json(path: Path, identity: TerminalIdentity) -> dict[str, Any]:
"""Read one small, singly-linked journal through its validated directory."""
snapshot = _open_terminal_snapshot(path, identity)
if snapshot is None:
return {}
try:
return snapshot.document
finally:
snapshot.close()
def _load_small_json(path: Path) -> dict[str, Any]:
"""Read one bounded immutable evidence document without following links."""
snapshot = _open_small_json_snapshot(path)
if snapshot is None:
return {}
try:
return snapshot.document
finally:
snapshot.close()
def _persist_candidate(
state: dict[str, Any],
state_file: Path,
structured: dict[str, Any],
*,
route: Route,
returncode: int,
goal_turn: int,
) -> Path:
"""Persist every structured response before any judge can supersede it."""
sequence = max(0, int(state.get("candidate_sequence", 0) or 0)) + 1
path = _candidate_path(state_file, state.get("run_id"), sequence)
while path.exists():
sequence += 1
path = _candidate_path(state_file, state.get("run_id"), sequence)
atomic_json(
path,
{
"board": state.get("board"),
"task_id": state.get("task_id"),
"expected_run_id": state.get("run_id"),
"goal_turn": goal_turn,
"provider": route.provider,
"model": route.model,
"effort": route.effort,
"returncode": returncode,
"structured": structured,
"recorded_at": utc_now(),
},
)
state["candidate_sequence"] = sequence
state["last_candidate_file"] = str(path)
atomic_json(state_file, state)
return path
def _write_terminal_record(
state_file: Path,
*,
board: str,
task_id: str,
run_id: Any,
structured: dict[str, Any],
summary: str,
metadata: dict[str, Any],
) -> tuple[Path, dict[str, Any]]:
"""Journal an accepted result before attempting the Kanban transaction."""
path = _terminal_path(state_file, run_id)
record = {
"board": board,
"task_id": task_id,
"expected_run_id": run_id,
"result": json.dumps(structured, sort_keys=True),
"summary": summary,
"metadata": metadata,
"kanban_state": "pending",
"recorded_at": utc_now(),
}
atomic_json(path, record)
return path, record
def _terminal_record_valid(
record: dict[str, Any],
identity: TerminalIdentity | None = None,
) -> bool:
"""Reject malformed or non-terminal replay journals without side effects."""
if not isinstance(record, dict):
return False
required_strings = ("board", "task_id", "result", "summary")
if not all(
isinstance(record.get(key), str) and record[key]
for key in required_strings
):
return False
if not isinstance(record.get("expected_run_id"), int):
return False
if not isinstance(record.get("metadata"), dict):
return False
try:
structured = json.loads(record["result"])
except (TypeError, json.JSONDecodeError):
return False
if not isinstance(structured, dict):
return False
required = set(RESULT_SCHEMA["required"])
if set(structured) != required:
return False
if (
type(structured["status"]) is not str
or structured["status"] not in cli_lane_goal.RESULT_STATUSES
or type(structured["summary"]) is not str
or not structured["summary"].strip()
):
return False
for key in ("changed_files", "tests_run", "artifacts", "findings", "blockers"):
value = structured[key]
if type(value) is not list or any(type(item) is not str for item in value):
return False
valid = (
structured["status"] == "completed"
and structured["blockers"] == []
and cli_lane_goal.unfinished_result_reason(structured) is None
and record["summary"] == structured["summary"]
and record.get("kanban_state") in {"pending", "prepared", "committed"}
)
if not valid or identity is None:
return valid
return (
record.get("board") == identity.board
and record.get("task_id") == identity.task_id
and record.get("expected_run_id") == identity.run_id
and record.get("kanban_state") == identity.state
)
def build_prompt(context: str, workspace: Path, handoff: str = "") -> str:
"""Create a bounded worker contract with an explicit machine-readable result."""
return f"""You are a durable coding worker managed by Hermes Kanban.
Work only on this objective and its acceptance criteria:
{context}
Workspace: {workspace}
{handoff}
Operate autonomously inside the workspace. Inspect before editing, preserve unrelated user changes, run proportionate tests, and do not claim completion without evidence. You have owner-level Kubernetes access in every namespace. Prefer Flux-tracked manifests for durable changes, but use kubectl, Flux, exec, port-forwarding, rollout operations, and existing Vault workflows when the objective or incident requires them. Persist any desired-state mutation back to Git. Do not force-push, hard-reset, clean untracked files, or expose credentials.
Return a final JSON object matching the supplied schema. Use status=incomplete when required work, tests, commands, commits, pushes, or verification are still running or remain to be done. Use status=blocked only when an obstacle prevents completion of the assigned task itself. Never use status=completed for a progress report. For review or diagnostic tasks, put defects and risks in findings; those findings can make the reviewed change unfit to ship without blocking completion of the review. The blockers array must be empty whenever status is completed. List changed files, tests run, durable artifact paths, findings, and task blockers explicitly.
"""
def git_handoff(workspace: Path, prior_output: str) -> str:
"""Build an explicit cross-provider handoff without transferring hidden state."""
def read_git(*args: str) -> str:
completed = subprocess.run(
["git", "-C", str(workspace), *args],
text=True,
capture_output=True,
timeout=30,
check=False,
)
return (completed.stdout or completed.stderr).strip()[-12000:]
return (
"\nCross-provider handoff from a failed or exhausted worker:\n"
f"Git status:\n{read_git('status', '--short', '--branch')}\n\n"
f"Current diff summary:\n{read_git('diff', '--stat')}\n\n"
f"Prior worker tail:\n{prior_output[-10000:]}\n"
"Reinspect the workspace and verify all inherited claims before continuing."
)
def workspace_artifacts(workspace: Path, values: Any) -> list[str]:
"""Return existing regular artifacts contained by the task worktree."""
if not isinstance(values, list):
return []
root = workspace.resolve()
artifacts: list[str] = []
for value in values:
if not isinstance(value, str) or not value.strip():
continue
candidate = Path(value.strip()).expanduser()
if not candidate.is_absolute():
candidate = root / candidate
try:
resolved = candidate.resolve(strict=True)
resolved.relative_to(root)
except (OSError, RuntimeError, ValueError):
continue
if resolved.is_file():
artifacts.append(str(resolved))
return list(dict.fromkeys(artifacts))
def _extract_json(value: Any) -> dict[str, Any] | None:
if isinstance(value, dict) and value.get("status") in cli_lane_goal.RESULT_STATUSES:
return value
if not isinstance(value, str):
return None
candidates = [value]
match = re.search(r"\{.*\}", value, re.DOTALL)
if match:
candidates.append(match.group(0))
for candidate in candidates:
try:
parsed = json.loads(candidate)
except json.JSONDecodeError:
continue
if isinstance(parsed, dict) and parsed.get("status") in cli_lane_goal.RESULT_STATUSES:
return parsed
return None
def _event_payload(provider: str, line: str, state: dict[str, Any], state_file: Path) -> dict[str, Any] | None:
"""Persist provider session identifiers before interpreting final output."""
try:
event = json.loads(line)
except json.JSONDecodeError:
return None
if not isinstance(event, dict):
return None
if provider == "codex" and event.get("type") == "thread.started":
thread_id = event.get("thread_id") or event.get("thread", {}).get("id")
if thread_id:
state["codex_thread_id"] = str(thread_id)
atomic_json(state_file, state)
if provider == "claude" and event.get("session_id"):
state["claude_session_id"] = str(event["session_id"])
atomic_json(state_file, state)
for key in ("structured_output", "result", "output", "text"):
parsed = _extract_json(event.get(key))
if parsed:
return parsed
item = event.get("item")
if isinstance(item, dict):
for key in ("text", "content"):
parsed = _extract_json(item.get(key))
if parsed:
return parsed
return None
def stream_process(
command: list[str],
*,
provider: str,
cwd: Path,
env: dict[str, str],
log_path: Path,
state: dict[str, Any],
state_file: Path,
heartbeat: Callable[[str], bool],
max_runtime: int,
) -> ProcessResult:
"""Stream JSONL to Kanban logs while maintaining the authoritative lease."""
process = subprocess.Popen(
command,
cwd=cwd,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
start_new_session=True,
)
assert process.stdout is not None
selector = selectors.DefaultSelector()
selector.register(process.stdout, selectors.EVENT_READ)
started = time.monotonic()
last_heartbeat = 0.0
known_descendants: dict[int, tuple[int, int]] = {}
lines: list[str] = []
structured: dict[str, Any] | None = None
log_path.parent.mkdir(parents=True, exist_ok=True)
forced_failure = ""
with log_path.open("a", encoding="utf-8") as log:
log.write(f"\n[{utc_now()}] starting {provider} worker\n")
log.flush()
while process.poll() is None:
known_descendants.update(_descendant_processes(process.pid))
now = time.monotonic()
if now - started > max_runtime:
_terminate_worker_process(process, known_descendants)
forced_failure = "worker exceeded its maximum runtime"
lines.append(forced_failure + "\n")
break
if now - last_heartbeat >= HEARTBEAT_SECONDS:
if not heartbeat(f"{provider} worker active for {round(now - started)}s"):
_terminate_worker_process(process, known_descendants)
forced_failure = "Kanban lease was lost; provider process terminated"
lines.append(forced_failure + "\n")
break
last_heartbeat = now
for key, _ in selector.select(timeout=1.0):
line = key.fileobj.readline()
if not line:
continue
lines.append(line)
if len(lines) > 4000:
lines = lines[-4000:]
log.write(line)
log.flush()
parsed = _event_payload(provider, line, state, state_file)
structured = parsed or structured
_terminate_worker_process(process, known_descendants)
remainder = process.stdout.read()
if remainder:
lines.append(remainder)
log.write(remainder)
for line in remainder.splitlines():
parsed = _event_payload(provider, line, state, state_file)
structured = parsed or structured
if forced_failure:
log.write(forced_failure + "\n")
log.write(f"\n[{utc_now()}] {provider} exit={process.returncode}\n")
selector.close()
output = "".join(lines)[-100000:]
returncode = int(process.returncode if process.returncode is not None else 1)
return ProcessResult(
returncode=returncode,
output=output,
structured=structured,
capacity_failure=returncode != 0 and bool(CAPACITY_PATTERN.search(output)),
)
def _process_record(pid: int) -> tuple[int, int, int] | None:
"""Return one Linux process's parent, group, and start-time identity."""
try:
raw = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8")
fields = raw[raw.rfind(")") + 2 :].split()
return int(fields[1]), int(fields[2]), int(fields[19])
except (IndexError, OSError, ValueError):
return None
def _descendant_processes(root_pid: int) -> dict[int, tuple[int, int]]:
"""Snapshot descendants as pid -> (process group, start-time identity)."""
records: dict[int, tuple[int, int, int]] = {}
for entry in Path("/proc").iterdir():
if not entry.name.isdigit():
continue
pid = int(entry.name)
record = _process_record(pid)
if record is not None:
records[pid] = record
family = {root_pid}
changed = True
while changed:
changed = False
for pid, (parent, _, _) in records.items():
if pid not in family and parent in family:
family.add(pid)
changed = True
return {
pid: (records[pid][1], records[pid][2])
for pid in family
if pid != root_pid and pid in records
}
def _process_identity_matches(pid: int, start_time: int) -> bool:
record = _process_record(pid)
return record is not None and record[2] == start_time
def _signal_worker_tree(
root_pid: int,
descendants: dict[int, tuple[int, int]],
sig: signal.Signals,
) -> None:
"""Signal captured descendants across terminal-created process groups."""
groups = {root_pid}
for pid, (process_group, start_time) in descendants.items():
if _process_identity_matches(pid, start_time):
groups.add(process_group)
for process_group in groups:
try:
os.killpg(process_group, sig)
except ProcessLookupError:
pass
for pid, (_, start_time) in descendants.items():
if not _process_identity_matches(pid, start_time):
continue
try:
os.kill(pid, sig)
except ProcessLookupError:
pass
def _terminate_worker_process(
process: subprocess.Popen[str],
known_descendants: dict[int, tuple[int, int]] | None = None,
) -> None:
"""Reap a worker and terminal descendants that created their own groups."""
descendants = dict(known_descendants or {})
descendants.update(_descendant_processes(process.pid))
if process.poll() is None:
_signal_worker_tree(process.pid, descendants, signal.SIGTERM)
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
pass
# Provider terminal tools create their own sessions, so killing only the
# native CLI's process group leaves test/build subprocesses orphaned. The
# identity check prevents a reused PID from targeting an unrelated worker.
_signal_worker_tree(process.pid, descendants, signal.SIGKILL)
if process.poll() is None:
process.wait(timeout=10)
def _base_env() -> dict[str, str]:
"""Build a worker environment while preserving Vault-backed CLI homes."""
env = os.environ.copy()
env.update(
{
"HOME": str(DATA_ROOT / "home"),
"CODEX_HOME": os.environ.get("CODEX_HOME", "/runtime-access/codex"),
"CLAUDE_CONFIG_DIR": os.environ.get(
"CLAUDE_CONFIG_DIR", "/runtime-access/claude"
),
"GIT_TERMINAL_PROMPT": "0",
}
)
env["PATH"] = f"{DATA_ROOT / 'tools/bin'}:/opt/coordinator:" + env.get("PATH", "")
return env
def _codex_command(route: Route, prompt: str, workspace: Path, state: dict[str, Any], result_file: Path) -> list[str]:
base = [
str(CODEX_BIN),
"--dangerously-bypass-approvals-and-sandbox",
"--dangerously-bypass-hook-trust",
"exec",
]
thread_id = str(state.get("codex_thread_id") or "")
common = [
"--json",
"-m",
route.model,
"-c",
f'model_reasoning_effort="{route.effort}"',
"--output-schema",
str(RESULT_SCHEMA_PATH),
"-o",
str(result_file),
]
if thread_id:
return [*base, "resume", *common, thread_id, prompt]
return [
*base,
*common,
"-C",
str(workspace),
prompt,
]
def _claude_command(route: Route, prompt: str, state: dict[str, Any], resume: bool) -> list[str]:
session_id = str(state["claude_session_id"])
session = ["--resume", session_id] if resume else ["--session-id", session_id]
denied = [
"Bash(git push --force *)",
"Bash(git reset --hard *)",
"Bash(git clean -f *)",
]
return [
str(CLAUDE_BIN),
"--dangerously-skip-permissions",
"--autocompact",
"auto",
"--settings",
str(CLAUDE_SETTINGS),
"--disallowedTools",
*denied,
"--model",
route.model,
"--effort",
route.effort,
"--output-format",
"stream-json",
"--verbose",
"--json-schema",
json.dumps(RESULT_SCHEMA, separators=(",", ":")),
*session,
"-p",
prompt,
]
def run_provider(
route: Route,
prompt: str,
workspace: Path,
state: dict[str, Any],
state_file: Path,
log_path: Path,
heartbeat: Callable[[str], bool],
max_runtime: int,
) -> ProcessResult:
"""Run or resume one provider session using its pinned structured CLI."""
state.setdefault("attempts", []).append({"route": asdict(route), "started_at": utc_now()})
if route.provider == "claude" and not state.get("claude_session_id"):
# The reservation is durable before Claude starts, closing the crash gap.
state["claude_session_id"] = str(uuid.uuid4())
state["current_route"] = asdict(route)
state["updated_at"] = utc_now()
env = _base_env()
if route.provider == "codex":
result_sequence = max(0, int(state.get("result_sequence", 0) or 0)) + 1
result_file = _result_path(state_file, state.get("run_id"), result_sequence)
while result_file.exists():
result_sequence += 1
result_file = _result_path(
state_file,
state.get("run_id"),
result_sequence,
)
result_file.parent.mkdir(parents=True, exist_ok=True)
result_file.touch(mode=0o600, exist_ok=False)
state["result_sequence"] = result_sequence
state["current_result_file"] = str(result_file)
atomic_json(state_file, state)
command = _codex_command(route, prompt, workspace, state, result_file)
result = stream_process(
command,
provider="codex",
cwd=workspace,
env=env,
log_path=log_path,
state=state,
state_file=state_file,
heartbeat=heartbeat,
max_runtime=max_runtime,
)
if (
state.get("codex_thread_id")
and result.returncode != 0
and NO_CODEX_THREAD in result.output.lower()
):
state.pop("codex_thread_id", None)
result_sequence += 1
result_file = _result_path(
state_file,
state.get("run_id"),
result_sequence,
)
while result_file.exists():
result_sequence += 1
result_file = _result_path(
state_file,
state.get("run_id"),
result_sequence,
)
result_file.touch(mode=0o600, exist_ok=False)
state["result_sequence"] = result_sequence
state["current_result_file"] = str(result_file)
atomic_json(state_file, state)
result = stream_process(
_codex_command(route, prompt, workspace, state, result_file),
provider="codex",
cwd=workspace,
env=env,
log_path=log_path,
state=state,
state_file=state_file,
heartbeat=heartbeat,
max_runtime=max_runtime,
)
file_result = load_json(result_file)
if file_result.get("status") in cli_lane_goal.RESULT_STATUSES:
result.structured = file_result
try:
result_file.chmod(0o600)
except OSError:
pass
return result
atomic_json(state_file, state)
resume = bool(state.get("claude_started"))
result = stream_process(
_claude_command(route, prompt, state, resume),
provider="claude",
cwd=workspace,
env=env,
log_path=log_path,
state=state,
state_file=state_file,
heartbeat=heartbeat,
max_runtime=max_runtime,
)
if resume and result.returncode == 1 and NO_CLAUDE_SESSION in result.output:
result = stream_process(
_claude_command(route, prompt, state, False),
provider="claude",
cwd=workspace,
env=env,
log_path=log_path,
state=state,
state_file=state_file,
heartbeat=heartbeat,
max_runtime=max_runtime,
)
elif not resume and result.returncode == 1 and CLAUDE_SESSION_COLLISION in result.output:
result = stream_process(
_claude_command(route, prompt, state, True),
provider="claude",
cwd=workspace,
env=env,
log_path=log_path,
state=state,
state_file=state_file,
heartbeat=heartbeat,
max_runtime=max_runtime,
)
if result.returncode == 0 or NO_CLAUDE_SESSION not in result.output:
state["claude_started"] = True
atomic_json(state_file, state)
return result
def _task_value(task: Any, name: str, default: Any = None) -> Any:
return getattr(task, name, default)
def _task_context(kanban_db: Any, conn: Any, task_id: str) -> str:
value = kanban_db.build_worker_context(conn, task_id)
if isinstance(value, str):
return value
return json.dumps(value, indent=2, default=str)
def _resolve_workspace(kanban_db: Any, conn: Any, task: Any, board: str) -> Path:
# External coding lanes always receive their own linked worktree. A task
# that does not resolve to a Git repository is blocked rather than sharing
# a mutable checkout with another unattended worker.
# Git worktree creation updates common-repository metadata. Serialize that
# short materialization step while allowing the provider workers themselves
# to run concurrently in independent worktrees.
with WORKTREE_LOCK:
value, branch_name = kanban_db._resolve_worktree_workspace(task, board=board)
kanban_db.set_branch_name(conn, str(_task_value(task, "id")), branch_name)
return Path(value).resolve()
def _board_call(
kanban_db: Any,
board: str,
operation: Callable[[Any], Any],
) -> Any:
"""Run one bounded Kanban operation on a fresh, promptly closed connection."""
last_storage_error: Exception | None = None
for attempt in range(KANBAN_STORAGE_ATTEMPTS):
conn = None
try:
with kanban_db.scoped_current_board(board):
conn = kanban_db.connect(board=board)
result = operation(conn)
BOARD_CORRUPTION_ERRORS.pop(board, None)
return result
except (OSError, sqlite3.Error) as error:
last_storage_error = error
_record_board_access_error(board, error)
if attempt + 1 < KANBAN_STORAGE_ATTEMPTS:
time.sleep(0.2 * (attempt + 1))
finally:
if conn is not None:
conn.close()
assert last_storage_error is not None
raise last_storage_error
def _rename_noreplace(
source: str,
destination: str,
*,
source_dir: int,
destination_dir: int,
) -> None:
"""Atomically rename a directory entry without replacing a collision."""
renameat2 = getattr(ctypes.CDLL(None, use_errno=True), "renameat2", None)
if renameat2 is None:
raise OSError(errno.ENOSYS, "renameat2 is unavailable")
renameat2.argtypes = (
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_uint,
)
renameat2.restype = ctypes.c_int
result = renameat2(
source_dir,
ctypes.c_char_p(os.fsencode(source)),
destination_dir,
ctypes.c_char_p(os.fsencode(destination)),
1, # RENAME_NOREPLACE
)
if result != 0:
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), source, destination)
def _retire_terminal_entry(
path: Path,
source_stat: os.stat_result,
*,
board_descriptor: int,
quarantine_descriptor: int,
authority_name: str | None = None,
) -> str:
"""Retire only the inode previously inspected; preserve any replacement."""
try:
current = os.stat(
path.name,
dir_fd=board_descriptor,
follow_symlinks=False,
)
except FileNotFoundError:
return "missing"
if current.st_dev != source_stat.st_dev or current.st_ino != source_stat.st_ino:
return "replacement"
# A replacement can itself be staged repeatedly while recovery races a
# writer. Keep every generation bound to the original canonical pending
# name instead of hashing an intermediate .retire name and hiding it from
# the next recovery pass.
path_digest = hashlib.sha256(
(authority_name or path.name).encode("utf-8")
).hexdigest()[:16]
source_digest = hashlib.sha256(
f"{source_stat.st_dev:x}\0{source_stat.st_ino:x}".encode("utf-8")
).hexdigest()[:16]
for sequence in range(32):
staging = f".retire.{path_digest}.{source_digest}.{sequence}"
try:
_rename_noreplace(
path.name,
staging,
source_dir=board_descriptor,
destination_dir=quarantine_descriptor,
)
except FileExistsError:
continue
break
else:
return "collision"
staged = os.stat(
staging,
dir_fd=quarantine_descriptor,
follow_symlinks=False,
)
if staged.st_dev == source_stat.st_dev and staged.st_ino == source_stat.st_ino:
os.unlink(staging, dir_fd=quarantine_descriptor)
os.fsync(quarantine_descriptor)
os.fsync(board_descriptor)
return "retired"
# The path changed after the pre-check but before the rename. Put the
# replacement back without overwriting an even newer entry.
try:
_rename_noreplace(
staging,
path.name,
source_dir=quarantine_descriptor,
destination_dir=board_descriptor,
)
except FileExistsError:
return "replacement-staged"
os.fsync(quarantine_descriptor)
os.fsync(board_descriptor)
return "replacement"
def _write_json_noreplace(path: Path, value: dict[str, Any]) -> bool:
"""Durably create one immutable JSON artifact without replacing a peer."""
payload = (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
if len(payload) > MAX_TERMINAL_RECORD_BYTES + 4096:
raise ValueError("terminal evidence exceeds the bounded artifact limit")
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(path.parent, 0o700, follow_symlinks=False)
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
create_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
create_flags |= getattr(os, "O_NOFOLLOW", 0)
directory = os.open(path.parent, directory_flags)
temporary = f".{path.name}.{uuid.uuid4().hex}.tmp"
temporary_stat = None
descriptor = None
try:
descriptor = os.open(temporary, create_flags, 0o600, dir_fd=directory)
temporary_stat = os.fstat(descriptor)
with os.fdopen(descriptor, "wb") as stream:
descriptor = None
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
written = os.fstat(stream.fileno())
if stat.S_IMODE(written.st_mode) != 0o600 or written.st_nlink != 1:
raise OSError("terminal evidence is not private and singly linked")
try:
_rename_noreplace(
temporary,
path.name,
source_dir=directory,
destination_dir=directory,
)
except FileExistsError:
return False
os.fsync(directory)
return True
finally:
if descriptor is not None:
os.close(descriptor)
if temporary_stat is not None:
try:
_retire_terminal_entry(
Path(temporary),
temporary_stat,
board_descriptor=directory,
quarantine_descriptor=directory,
)
except OSError:
pass
os.close(directory)
def _terminal_document_digest(document: dict[str, Any]) -> str:
"""Return the stable identity for one exact accepted result document."""
immutable = {
key: document.get(key)
for key in (
"board",
"task_id",
"expected_run_id",
"result",
"summary",
"metadata",
)
}
canonical = json.dumps(immutable, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:32]
def _terminal_evidence_path(
identity: TerminalIdentity,
state: str,
document: dict[str, Any],
) -> Path:
"""Return a deterministic immutable prepared or conflict evidence path."""
if state not in {"prepared", "conflict"}:
raise ValueError(f"invalid terminal evidence state: {state}")
base = state_path(identity.board, identity.task_id)
digest = _terminal_document_digest(document)
return base.with_name(
f"{base.stem}.run-{identity.run_id}.terminal.{state}-{digest}.json"
)
def _same_terminal_document(left: dict[str, Any], right: dict[str, Any]) -> bool:
"""Compare the immutable result-bearing fields of terminal evidence."""
keys = ("board", "task_id", "expected_run_id", "result", "summary", "metadata")
return all(left.get(key) == right.get(key) for key in keys)
def _terminal_evidence_valid(
document: dict[str, Any],
identity: TerminalIdentity,
state: str,
) -> bool:
"""Validate semantic terminal content plus its immutable evidence state."""
if document.get("kanban_state") != state:
return False
semantic = dict(document)
semantic_identity = identity
if state == "conflict":
semantic["kanban_state"] = "prepared"
semantic_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"prepared",
)
return _terminal_record_valid(semantic, semantic_identity)
def _persist_prepared_evidence(
identity: TerminalIdentity,
document: dict[str, Any],
) -> Path:
"""Fsync an immutable accepted result before entering the DB boundary."""
prepared = dict(document)
prepared["kanban_state"] = "prepared"
prepared["prepared_at"] = utc_now()
path = _terminal_evidence_path(identity, "prepared", document)
if _write_json_noreplace(path, prepared):
return path
if _terminal_evidence_identity(path) != TerminalIdentity(
identity.board, identity.task_id, identity.run_id, "prepared"
):
raise OSError("prepared terminal evidence identity is invalid")
existing = _load_small_json(path)
prepared_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"prepared",
)
if not _terminal_evidence_valid(
existing,
prepared_identity,
"prepared",
) or not _same_terminal_document(existing, prepared):
raise OSError("prepared terminal evidence path contains a conflicting result")
return path
def _persist_conflict_evidence(
identity: TerminalIdentity,
document: dict[str, Any],
reason: str,
) -> Path:
"""Preserve a losing valid result in full under deterministic retention."""
conflict = dict(document)
conflict["kanban_state"] = "conflict"
conflict["conflict_reason"] = reason
conflict["conflicted_at"] = utc_now()
path = _terminal_evidence_path(identity, "conflict", document)
if _write_json_noreplace(path, conflict):
return path
if _terminal_evidence_identity(path) != TerminalIdentity(
identity.board, identity.task_id, identity.run_id, "conflict"
):
raise OSError("terminal conflict evidence identity is invalid")
existing = _load_small_json(path)
conflict_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"conflict",
)
if not _terminal_evidence_valid(
existing,
conflict_identity,
"conflict",
) or not _same_terminal_document(existing, conflict):
raise OSError("terminal conflict evidence path contains a different result")
return path
def _quarantine_terminal(
path: Path,
identity: TerminalIdentity | None,
reason: str,
*,
snapshot: TerminalRecoverySnapshot | None = None,
) -> Path:
"""Record bounded metadata, then retire only the inspected journal inode."""
board_dir = path.parent
try:
board_stat = board_dir.stat(follow_symlinks=False)
except OSError:
board_stat = None
if board_stat is None or not stat.S_ISDIR(board_stat.st_mode):
print(
f"refused terminal journal path through unsafe board directory: {path}",
file=sys.stderr,
flush=True,
)
return path
board_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
board_flags |= getattr(os, "O_NOFOLLOW", 0)
file_flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
create_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
create_flags |= getattr(os, "O_NOFOLLOW", 0)
board_descriptor = None
quarantine_descriptor = None
source_descriptor = None
source_stat = None
destination = path
retirement = "not-attempted"
owns_source = snapshot is None
try:
if snapshot is None:
board_descriptor = os.open(board_dir, board_flags)
source_stat = os.stat(
path.name,
dir_fd=board_descriptor,
follow_symlinks=False,
)
else:
board_descriptor = snapshot.directory_descriptor
source_descriptor = snapshot.descriptor
source_stat = snapshot.file_stat
pinned_board = os.fstat(board_descriptor)
if (
pinned_board.st_dev != board_stat.st_dev
or pinned_board.st_ino != board_stat.st_ino
):
raise OSError("terminal recovery board identity changed")
safe_source = stat.S_ISREG(source_stat.st_mode) and source_stat.st_nlink == 1
digest = None
hashed_bytes = 0
hash_complete = False
if safe_source and snapshot is None:
source_descriptor = os.open(
path.name,
file_flags,
dir_fd=board_descriptor,
)
opened_stat = os.fstat(source_descriptor)
if (
opened_stat.st_dev != source_stat.st_dev
or opened_stat.st_ino != source_stat.st_ino
or opened_stat.st_nlink != 1
):
raise OSError("terminal journal identity changed while opening")
# Keep this descriptor open through retirement. The staging entry
# must match the exact fstat-verified inode, not merely a path stat
# captured before a concurrent replacement.
source_stat = opened_stat
prefix = _read_bounded(source_descriptor, QUARANTINE_HASH_BYTES)
hashed_bytes = len(prefix)
hash_complete = opened_stat.st_size <= QUARANTINE_HASH_BYTES
digest = hashlib.sha256(prefix).hexdigest()
elif safe_source and source_descriptor is not None:
prefix = snapshot.prefix
hashed_bytes = len(prefix)
hash_complete = (
source_stat.st_size <= QUARANTINE_HASH_BYTES
and hashed_bytes == source_stat.st_size
)
digest = hashlib.sha256(prefix).hexdigest()
source_kind = (
"regular"
if safe_source
else "hardlink"
if stat.S_ISREG(source_stat.st_mode) and source_stat.st_nlink != 1
else "non-regular"
)
diagnostic = {
"hash_complete": hash_complete,
"hashed_bytes": hashed_bytes,
"original_name": path.name,
"reason": reason,
"sha256": digest,
"size": source_stat.st_size,
"source_kind": source_kind,
}
payload = (json.dumps(diagnostic, sort_keys=True) + "\n").encode("utf-8")
fingerprint = (
(digest or hashlib.sha256(path.name.encode("utf-8")).hexdigest())[:16]
)
safe_reason = (
re.sub(r"[^a-zA-Z0-9_.-]+", "-", reason).strip("-") or "invalid"
)
try:
os.mkdir("quarantine", 0o700, dir_fd=board_descriptor)
except FileExistsError:
pass
quarantine_descriptor = os.open(
"quarantine",
board_flags,
dir_fd=board_descriptor,
)
for sequence in range(32):
name = f"{path.name}.{safe_reason}.{fingerprint}.{sequence}.quarantine"
try:
destination_descriptor = os.open(
name,
create_flags,
0o600,
dir_fd=quarantine_descriptor,
)
except FileExistsError:
continue
try:
with os.fdopen(destination_descriptor, "wb") as target:
target.write(payload)
target.flush()
os.fsync(target.fileno())
target_stat = os.fstat(target.fileno())
if (
stat.S_IMODE(target_stat.st_mode) != 0o600
or target_stat.st_nlink != 1
):
raise OSError("quarantine destination is not private and singly linked")
except Exception:
os.unlink(name, dir_fd=quarantine_descriptor)
raise
destination = board_dir / "quarantine" / name
break
else:
raise OSError("could not reserve a unique quarantine destination")
os.fsync(quarantine_descriptor)
retirement = _retire_terminal_entry(
path,
source_stat,
board_descriptor=board_descriptor,
quarantine_descriptor=quarantine_descriptor,
)
except OSError as error:
# A bad quarantine target must not make an attacker-controlled pending
# path eligible for replay forever. The same identity-safe staging
# protocol works in the board directory when quarantine is unusable.
if board_descriptor is not None and source_stat is not None:
try:
retirement = _retire_terminal_entry(
path,
source_stat,
board_descriptor=board_descriptor,
quarantine_descriptor=(
quarantine_descriptor
if quarantine_descriptor is not None
else board_descriptor
),
)
except OSError:
retirement = "deferred"
print(
f"terminal journal quarantine degraded safely: {type(error).__name__}: {error}",
file=sys.stderr,
flush=True,
)
finally:
if owns_source and source_descriptor is not None:
os.close(source_descriptor)
if quarantine_descriptor is not None:
os.close(quarantine_descriptor)
if owns_source and board_descriptor is not None:
os.close(board_descriptor)
print(
"quarantined terminal journal "
f"{path.name}: {reason}; identity={identity or 'unparseable'}; "
f"retirement={retirement}",
file=sys.stderr,
flush=True,
)
return destination
def _recover_exact_run(
kanban_db: Any,
identity: TerminalIdentity | None,
reason: str,
) -> bool:
"""Make an exact external run retryable after journal recovery fails."""
if identity is None:
return False
def operation(conn: Any) -> bool:
task = kanban_db.get_task(conn, identity.task_id)
if task is None:
return False
if (
str(_task_value(task, "status", "")) != "running"
or _task_value(task, "current_run_id", None) != identity.run_id
or not _external(task)
):
return False
return bool(
kanban_db.reclaim_task(
conn,
identity.task_id,
reason=f"terminal journal recovery failed ({reason}); exact run may retry",
expected_run_id=identity.run_id,
)
)
try:
recovered = bool(_board_call(kanban_db, identity.board, operation))
except Exception as error:
_record_board_access_error(identity.board, error)
return False
if recovered:
print(
f"reclaimed {identity.board}/{identity.task_id} run {identity.run_id} "
f"after terminal journal recovery: {reason}",
file=sys.stderr,
flush=True,
)
return recovered
def _retire_snapshot(
path: Path,
snapshot: TerminalSnapshot,
*,
authority_name: str | None = None,
) -> str:
"""Retire only the directory entry still naming an opened snapshot inode."""
return _retire_terminal_entry(
path,
snapshot.file_stat,
board_descriptor=snapshot.directory_descriptor,
quarantine_descriptor=snapshot.directory_descriptor,
authority_name=authority_name,
)
def _retire_snapshot_after_db(path: Path, snapshot: TerminalSnapshot) -> str:
"""Keep a committed DB result recoverable across retirement fsync errors."""
try:
return _retire_snapshot(path, snapshot)
except OSError as error:
print(
f"terminal journal retirement deferred after DB commit: "
f"{type(error).__name__}: {error}",
file=sys.stderr,
flush=True,
)
return "deferred"
def _discard_evidence(path: Path) -> str:
"""Identity-safely remove one immutable prepared evidence artifact."""
snapshot = _open_small_json_snapshot(path)
if snapshot is None:
return "missing"
try:
return _retire_snapshot(path, snapshot)
finally:
snapshot.close()
def _promote_prepared_evidence(
identity: TerminalIdentity,
document: dict[str, Any],
prepared: Path,
) -> tuple[Path, str]:
"""Publish the DB-winning result without overwriting first-writer evidence."""
committed = _terminal_path(
state_path(identity.board, identity.task_id),
identity.run_id,
"committed",
)
committed_document = dict(document)
committed_document["kanban_state"] = "committed"
committed_document["committed_at"] = utc_now()
state = "committed"
if not _write_json_noreplace(committed, committed_document):
committed_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"committed",
)
existing = _load_terminal_json(committed, committed_identity)
if (
not _terminal_record_valid(existing, committed_identity)
or existing.get("kanban_state") != "committed"
or not _same_terminal_document(existing, committed_document)
):
_persist_conflict_evidence(
identity,
document,
"canonical committed evidence already contains a different result",
)
state = "conflict"
_discard_evidence(prepared)
return committed, state
def _finalize_document_db(
kanban_db: Any,
identity: TerminalIdentity,
document: dict[str, Any],
) -> str:
"""Apply one validated exact-run result to Kanban under DB run guards."""
def operation(conn: Any) -> str:
task = kanban_db.get_task(conn, identity.task_id)
if task is None:
return "stale"
status = str(_task_value(task, "status", ""))
if status == "done":
return (
"committed"
if (
_task_value(task, "completed_run_id", None) == identity.run_id
and str(_task_value(task, "result", "") or "")
== document["result"]
)
else "stale"
)
current_run_id = _task_value(task, "current_run_id", None)
completion_guard: dict[str, int]
if status == "running" and current_run_id == identity.run_id:
completion_guard = {"expected_run_id": identity.run_id}
elif status in {"ready", "blocked"} and current_run_id is None:
# The journal may have survived an older post-persistence error
# path that ended its run. The patched DB verifies atomically that
# this is still the latest ended run before allowing completion.
completion_guard = {"replay_ended_run_id": identity.run_id}
else:
return "stale"
completed = kanban_db.complete_task(
conn,
identity.task_id,
result=document["result"],
summary=document["summary"],
metadata=document["metadata"],
**completion_guard,
)
if completed:
return "committed"
# A duplicate finalizer can lose the guarded UPDATE to an identical
# first writer. Re-read rather than reporting a false pending state.
latest = kanban_db.get_task(conn, identity.task_id)
if latest is not None and str(_task_value(latest, "status", "")) == "done":
return (
"committed"
if (
_task_value(latest, "completed_run_id", None) == identity.run_id
and str(_task_value(latest, "result", "") or "")
== document["result"]
)
else "stale"
)
return "pending"
return str(_board_call(kanban_db, identity.board, operation))
def _resolve_pending_after_winner(
path: Path,
identity: TerminalIdentity,
winning_document: dict[str, Any],
) -> bool:
"""Retire duplicates and preserve differing replacements as conflicts."""
for _attempt in range(8):
snapshot = _open_terminal_snapshot(path, identity)
if snapshot is None:
return False
document = snapshot.document
if not _terminal_record_valid(document, identity):
snapshot.close()
return False
differing = not _same_terminal_document(document, winning_document)
try:
if differing:
_persist_conflict_evidence(
identity,
document,
"different valid result lost the exact-run first-writer race",
)
retirement = _retire_snapshot_after_db(path, snapshot)
finally:
snapshot.close()
if retirement in {"retired", "missing"}:
if differing:
print(
f"preserved terminal result conflict for {identity.board}/"
f"{identity.task_id} run {identity.run_id}",
file=sys.stderr,
flush=True,
)
return differing
if retirement not in {"replacement", "replacement-staged", "collision"}:
return differing
return False
def _finalize_terminal_record(
kanban_db: Any,
path: Path,
_record: dict[str, Any] | None = None,
*,
snapshot: TerminalRecoverySnapshot | None = None,
) -> str:
"""Commit one inode-pinned exact-run journal, or leave it safely pending."""
identity = _terminal_identity(path)
if identity is None or identity.state != "pending":
if snapshot is not None:
snapshot.close()
return "invalid"
pinned: TerminalSnapshot | TerminalRecoverySnapshot | None = snapshot
if pinned is None:
pinned = _open_terminal_snapshot(path, identity)
if pinned is None or pinned.document is None:
if pinned is not None:
pinned.close()
return "invalid"
document = pinned.document
try:
if not _terminal_record_valid(document):
return "invalid"
if not _terminal_record_valid(document, identity):
return "foreign"
prepared = _persist_prepared_evidence(identity, document)
outcome = _finalize_document_db(kanban_db, identity, document)
if outcome == "committed":
_committed, evidence_state = _promote_prepared_evidence(
identity,
document,
prepared,
)
retirement = _retire_snapshot_after_db(path, pinned)
elif outcome == "stale":
_persist_conflict_evidence(
identity,
document,
"valid terminal result no longer matches the authoritative task run",
)
_discard_evidence(prepared)
retirement = _retire_snapshot_after_db(path, pinned)
outcome = "conflict"
evidence_state = "conflict"
else:
return outcome
finally:
pinned.close()
if outcome == "committed" and retirement not in {"retired", "missing"}:
_resolve_pending_after_winner(path, identity, document)
if evidence_state == "conflict":
print(
f"terminal first-writer conflict recorded for {identity.board}/"
f"{identity.task_id} run {identity.run_id}",
file=sys.stderr,
flush=True,
)
return outcome
def _terminal_entry_absent(path: Path) -> bool:
"""Confirm absence through a nofollow directory descriptor."""
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
directory = None
try:
directory = os.open(path.parent, directory_flags)
os.stat(path.name, dir_fd=directory, follow_symlinks=False)
return False
except FileNotFoundError:
return True
except OSError:
return False
finally:
if directory is not None:
os.close(directory)
def _staged_terminal_authority(
path: Path,
snapshot: TerminalRecoverySnapshot,
) -> tuple[TerminalIdentity, Path] | None:
"""Bind a staged inode to the canonical pending name it was moved from."""
try:
relative = path.relative_to(STATE_ROOT)
except ValueError:
return None
if len(relative.parts) != 2:
return None
board, filename = relative.parts
match = _RETIRE_NAME.fullmatch(filename)
legacy = _LEGACY_RETIRE_NAME.fullmatch(filename)
document = snapshot.document
if (
(match is None and legacy is None)
or document is None
or not _SAFE_BOARD.fullmatch(board)
or board in {".", ".."}
or document.get("board") != board
or not isinstance(document.get("task_id"), str)
or not isinstance(document.get("expected_run_id"), int)
):
return None
identity = TerminalIdentity(
board,
document["task_id"],
document["expected_run_id"],
"pending",
)
canonical = _terminal_path(
state_path(identity.board, identity.task_id),
identity.run_id,
"pending",
)
if (
canonical.parent != path.parent
or (
match is not None
and hashlib.sha256(canonical.name.encode("utf-8")).hexdigest()[:16]
!= match.group("path_digest")
)
or not _terminal_record_valid(document, identity)
):
return None
return identity, canonical
def _recover_retirement_staging() -> int:
"""Promote valid hidden journals to durable prepared evidence."""
recovered = 0
for path in sorted(STATE_ROOT.glob("*/.retire.*")):
snapshot = _open_terminal_recovery_snapshot(path)
if snapshot is None:
continue
authority = _staged_terminal_authority(path, snapshot)
if authority is None:
try:
_quarantine_terminal(
path,
None,
"untrusted-retirement-staging",
snapshot=snapshot,
)
finally:
snapshot.close()
continue
identity, canonical = authority
try:
_persist_prepared_evidence(identity, snapshot.document or {})
retirement = _retire_snapshot(
path,
snapshot,
authority_name=canonical.name,
)
except Exception as error:
_record_board_access_error(identity.board, error)
continue
finally:
snapshot.close()
if retirement in {"retired", "missing"}:
recovered += 1
return recovered
def _recover_prepared_finalizations(kanban_db: Any) -> int:
"""Recover an accepted result staged before an interrupted DB boundary."""
recovered = 0
prepared_paths = list(STATE_ROOT.glob("*/*.terminal.prepared-*.json"))
def durable_order(path: Path) -> tuple[int, str]:
try:
return path.stat(follow_symlinks=False).st_mtime_ns, str(path)
except OSError:
return 2**63 - 1, str(path)
# When no DB winner exists yet, the first durably prepared result owns the
# run. The DB transaction remains the final arbiter across runner processes.
for path in sorted(prepared_paths, key=durable_order):
identity = _terminal_evidence_identity(path)
snapshot = _open_small_json_snapshot(path)
if identity is None or identity.state != "prepared" or snapshot is None:
if snapshot is not None:
snapshot.close()
_quarantine_terminal(path, identity, "malformed-prepared-evidence")
continue
document = snapshot.document
try:
expected = _terminal_evidence_path(identity, "prepared", document)
if (
expected.name != path.name
or not _terminal_evidence_valid(document, identity, "prepared")
):
_quarantine_terminal(path, identity, "foreign-prepared-evidence")
continue
try:
outcome = _finalize_document_db(kanban_db, identity, document)
except Exception as error:
_record_board_access_error(identity.board, error)
continue
try:
if outcome == "committed":
_promote_prepared_evidence(identity, document, path)
pending = _terminal_path(
state_path(identity.board, identity.task_id),
identity.run_id,
"pending",
)
pending_identity = TerminalIdentity(
identity.board,
identity.task_id,
identity.run_id,
"pending",
)
_resolve_pending_after_winner(
pending,
pending_identity,
document,
)
recovered += 1
elif outcome == "stale":
_persist_conflict_evidence(
identity,
document,
"prepared result no longer matches the authoritative task run",
)
_discard_evidence(path)
except Exception as error:
_record_board_access_error(identity.board, error)
continue
finally:
snapshot.close()
return recovered
def recover_pending_finalizations() -> int:
"""Replay accepted exact-run results before scheduling more provider work."""
from hermes_cli import kanban_db
_recover_retirement_staging()
recovered = _recover_prepared_finalizations(kanban_db)
for path in sorted(STATE_ROOT.glob("*/*.terminal.pending.json")):
identity = _terminal_identity(path)
last_invalid_reason = "malformed-name" if identity is None else None
retired_invalid = False
for _replacement_attempt in range(16):
snapshot = _open_terminal_recovery_snapshot(path)
if snapshot is None:
if (
identity is not None
and retired_invalid
and _terminal_entry_absent(path)
):
_recover_exact_run(
kanban_db,
identity,
last_invalid_reason or "malformed-payload",
)
break
record = snapshot.document or {}
reason = None
if identity is None:
reason = "malformed-name"
elif not _terminal_record_valid(record):
reason = snapshot.invalid_reason or "malformed-payload"
elif not _terminal_record_valid(record, identity):
reason = "foreign-identity"
if reason is not None:
try:
_quarantine_terminal(
path,
identity,
reason,
snapshot=snapshot,
)
finally:
snapshot.close()
retired_invalid = True
last_invalid_reason = reason
continue
assert identity is not None
try:
outcome = _finalize_terminal_record(
kanban_db,
path,
record,
snapshot=snapshot,
)
except Exception as error:
_record_board_access_error(identity.board, error)
break
if outcome == "committed":
recovered += 1
break
if outcome in {"invalid", "foreign"}:
# The pathname changed after the recovery snapshot closed.
# Reclassify and pin the new inode instead of quarantining it
# using authority derived from the older entry.
last_invalid_reason = outcome
continue
break
else:
print(
f"terminal journal replacement churn deferred for {path.name}",
file=sys.stderr,
flush=True,
)
return recovered
def _has_pending_finalization(board: str, task_id: str, run_id: Any) -> bool:
"""Keep an exact run claimed while its accepted result awaits replay."""
if not isinstance(run_id, int):
return False
path = _terminal_path(state_path(board, task_id), run_id, "pending")
try:
path.stat()
except FileNotFoundError:
pass
except OSError:
return False
else:
identity = _terminal_identity(path)
if identity is not None:
record = _load_terminal_json(path, identity)
if _terminal_record_valid(record, identity):
return True
base = state_path(board, task_id)
for prepared in base.parent.glob(
f"{base.stem}.run-{run_id}.terminal.prepared-*.json"
):
identity = _terminal_evidence_identity(prepared)
if identity is None or identity.state != "prepared":
continue
record = _load_small_json(prepared)
if _terminal_evidence_valid(record, identity, "prepared"):
return True
for staged in base.parent.glob(".retire.*"):
snapshot = _open_terminal_recovery_snapshot(staged)
if snapshot is None:
continue
try:
authority = _staged_terminal_authority(staged, snapshot)
if authority is not None and authority[0] == TerminalIdentity(
board,
task_id,
run_id,
"pending",
):
return True
finally:
snapshot.close()
return False
def _artifact_gc_candidates(board_dir: Path) -> list[Path]:
"""List only bounded artifact classes; pending journals are excluded."""
candidates: list[Path] = []
patterns = (
"*.provider-*.result.json",
"*.candidate-*.json",
"*.terminal.committed.json",
"*.terminal.conflict-*.json",
".retire.*",
)
for pattern in patterns:
candidates.extend(board_dir.glob(pattern))
quarantine = board_dir / "quarantine"
if quarantine.is_dir():
candidates.extend(quarantine.glob("*.quarantine"))
return candidates
def _unlink_artifact_if_same(path: Path, observed: os.stat_result) -> bool:
"""Remove only the exact artifact inode selected by retention."""
directory_flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
descriptor = None
try:
descriptor = os.open(path.parent, directory_flags)
outcome = _retire_terminal_entry(
path,
observed,
board_descriptor=descriptor,
quarantine_descriptor=descriptor,
)
return outcome == "retired"
except OSError:
return False
finally:
if descriptor is not None:
os.close(descriptor)
def gc_lane_artifacts(
*,
now: float | None = None,
max_age_seconds: int = ARTIFACT_RETENTION_AGE_SECONDS,
max_count: int = ARTIFACT_RETENTION_COUNT,
max_bytes: int = ARTIFACT_RETENTION_BYTES,
) -> int:
"""Bound non-pending lane artifacts by age, count, and bytes per board."""
current = time.time() if now is None else now
removed = 0
for board_dir in STATE_ROOT.iterdir() if STATE_ROOT.is_dir() else ():
if not board_dir.is_dir() or board_dir.is_symlink():
continue
board_removed = 0
entries: list[tuple[Path, os.stat_result]] = []
for path in _artifact_gc_candidates(board_dir):
if path.name.startswith(".retire."):
snapshot = _open_terminal_recovery_snapshot(path)
if snapshot is not None:
try:
if _staged_terminal_authority(path, snapshot) is not None:
# Accepted authority is first promoted/replayed by
# recovery; retention never deletes it directly.
continue
finally:
snapshot.close()
try:
file_stat = path.stat(follow_symlinks=False)
except OSError:
continue
if not stat.S_ISREG(file_stat.st_mode):
if _unlink_artifact_if_same(path, file_stat):
removed += 1
board_removed += 1
continue
if max_age_seconds >= 0 and current - file_stat.st_mtime > max_age_seconds:
if _unlink_artifact_if_same(path, file_stat):
removed += 1
board_removed += 1
else:
entries.append((path, file_stat))
retained_count = 0
retained_bytes = 0
for path, file_stat in sorted(
entries, key=lambda item: item[1].st_mtime, reverse=True
):
exceeds_count = max_count >= 0 and retained_count >= max_count
exceeds_bytes = (
max_bytes >= 0 and retained_bytes + file_stat.st_size > max_bytes
)
if exceeds_count or exceeds_bytes:
if _unlink_artifact_if_same(path, file_stat):
removed += 1
board_removed += 1
continue
retained_count += 1
retained_bytes += file_stat.st_size
if board_removed:
_fsync_directory(board_dir)
return removed
def maybe_gc_lane_artifacts(*, now: float | None = None) -> int:
"""Run artifact retention on a bounded cadence, not every dispatcher tick."""
global LAST_ARTIFACT_GC
current = time.time() if now is None else now
if current - LAST_ARTIFACT_GC < ARTIFACT_GC_INTERVAL_SECONDS:
return 0
LAST_ARTIFACT_GC = current
try:
return gc_lane_artifacts(now=current)
except OSError as error:
print(
f"lane artifact retention deferred: {type(error).__name__}: {error}",
file=sys.stderr,
flush=True,
)
return 0
def execute_claim(board: str, task_id: str) -> None:
"""Execute one already-claimed task and commit its outcome to Kanban."""
from hermes_cli import kanban_db
run_id = None
with kanban_db.scoped_current_board(board):
conn = kanban_db.connect(board=board)
try:
task = kanban_db.get_task(conn, task_id)
if task is None:
return
run_id = _task_value(task, "current_run_id", None)
assignee = str(_task_value(task, "assignee", "cli-auto") or "cli-auto")
state_file = state_path(board, task_id)
state = load_json(state_file)
state.update(
{
"board": board,
"task_id": task_id,
"run_id": run_id,
"assignee": assignee,
}
)
atomic_json(state_file, state)
log_path = Path(kanban_db.worker_log_path(task_id, board=board))
workspace = _resolve_workspace(kanban_db, conn, task, board)
kanban_db.set_workspace_path(conn, task_id, str(workspace))
context = _task_context(kanban_db, conn, task_id)
except Exception as error:
failure_reason = (
f"Direct CLI lane preparation failed: {type(error).__name__}: {error}"
)
conn.close()
_board_call(
kanban_db,
board,
lambda fresh: kanban_db.block_task(
fresh,
task_id,
reason=failure_reason,
kind="capability",
expected_run_id=run_id,
),
)
return
finally:
conn.close()
def heartbeat(note: str) -> bool:
try:
return bool(
_board_call(
kanban_db,
board,
lambda fresh: kanban_db.heartbeat_worker(
fresh,
task_id,
note=note,
expected_run_id=run_id,
),
)
)
except (OSError, sqlite3.Error):
# The claim TTL is deliberately long. Keep the expensive
# provider process alive during a transient volume stall and
# retry on the next heartbeat instead of losing its work.
return True
def comment(body: str) -> None:
try:
_board_call(
kanban_db,
board,
lambda fresh: kanban_db.add_comment(
fresh,
task_id,
"cli-lane-runner",
body,
),
)
except (OSError, sqlite3.Error):
# Route state is also written to the durable lane-state file;
# a later heartbeat or terminal result remains authoritative.
pass
try:
previous_route = state.get("current_route")
excluded_provider = (
fresh_unavailable_provider() if assignee == "cli-auto" else None
)
unavailable_provider = excluded_provider
route = select_route(
context,
assignee,
exclude_provider=excluded_provider,
exclude_reason="is unavailable according to fresh native health"
if excluded_provider
else None,
)
if excluded_provider:
comment(
f"Provider health guard excluded {excluded_provider} before automatic routing.",
)
comment(
f"CLI route: {route.provider}/{route.model} at {route.effort}; classifier={route.classifier}; {route.reason}",
)
resume_handoff = ""
if (
isinstance(previous_route, dict)
and previous_route.get("provider")
and previous_route.get("provider") != route.provider
):
try:
previous_output = log_path.read_text(encoding="utf-8")[-10000:]
except OSError:
previous_output = "Previous provider log was unavailable after restart."
resume_handoff = git_handoff(workspace, previous_output)
comment(
f"Restart-time provider change: {previous_route.get('provider')} -> {route.provider}; explicit workspace handoff attached.",
)
prompt = build_prompt(context, workspace, resume_handoff)
max_runtime = int(
_task_value(task, "max_runtime_seconds", 0) or DEFAULT_MAX_RUNTIME
)
goal_mode = bool(_task_value(task, "goal_mode", False))
goal_max_turns = max(1, int(_task_value(task, "goal_max_turns", 1) or 1))
goal_turn = max(1, int(state.get("goal_turn", 0) or 0) + 1)
deadline = time.monotonic() + max_runtime
while True:
state["goal_turn"] = goal_turn
atomic_json(state_file, state)
remaining = max(1, int(deadline - time.monotonic()))
result = run_provider(
route,
prompt,
workspace,
state,
state_file,
log_path,
heartbeat,
remaining,
)
candidate_file = None
if result.structured:
candidate_file = _persist_candidate(
state,
state_file,
dict(result.structured),
route=route,
returncode=result.returncode,
goal_turn=goal_turn,
)
if result.capacity_failure:
unavailable_provider = route.provider
retry_context = (
context
+ "\n\nRouting boundary: the first provider failed from capacity/authentication. "
+ "Select the alternate hosted provider at an appropriate effort."
)
alternate = "claude" if route.provider == "codex" else "codex"
fallback = select_route(
retry_context,
f"cli-{alternate}-{route.effort}",
)
comment(
f"Provider fallback: {route.provider} -> {fallback.provider}; Jetson reclassified the retry boundary.",
)
result = run_provider(
fallback,
build_prompt(
context,
workspace,
git_handoff(workspace, result.output),
),
workspace,
state,
state_file,
log_path,
heartbeat,
max(1, int(deadline - time.monotonic())),
)
route = fallback
if result.structured:
candidate_file = _persist_candidate(
state,
state_file,
dict(result.structured),
route=route,
returncode=result.returncode,
goal_turn=goal_turn,
)
structured = result.structured
if structured:
structured = dict(structured)
structured["artifacts"] = workspace_artifacts(
workspace,
structured.get("artifacts"),
)
metadata = {
"executor": "direct-cli-lane",
"provider": route.provider,
"model": route.model,
"effort": route.effort,
"classifier": route.classifier,
"state_file": str(state_file),
"codex_thread_id": state.get("codex_thread_id"),
"claude_session_id": state.get("claude_session_id"),
"goal_mode": goal_mode,
"goal_turn": goal_turn,
}
if candidate_file is not None:
metadata["candidate_file"] = str(candidate_file)
if structured:
for key in (
"changed_files",
"tests_run",
"artifacts",
"findings",
"blockers",
):
value = structured.get(key)
metadata[key] = value if isinstance(value, list) else []
completion_problem = None
if structured and result.returncode == 0:
completion_problem = cli_lane_goal.unfinished_result_reason(structured)
if (
structured.get("status") == "completed"
and completion_problem is None
and goal_mode
):
heartbeat("local goal-completion judge active")
rejection_history = state.get("goal_rejections", [])
if not isinstance(rejection_history, list):
rejection_history = []
judge_context = context
if goal_turn > 1 or rejection_history:
judge_context += (
"\n\nAuthoritative Hermes goal-controller evidence: "
f"current turn {goal_turn}/{goal_max_turns}; prior rejected "
f"reports: {json.dumps(rejection_history[-5:])}."
)
accepted, judge_reason = cli_lane_goal.judge_goal_completion(
judge_context,
structured,
)
metadata["goal_judge_reason"] = judge_reason
if not accepted:
completion_problem = f"local goal judge requested continuation: {judge_reason}"
if (
structured
and structured.get("status") == "completed"
and result.returncode == 0
and completion_problem is None
):
terminal_file = _terminal_path(state_file, run_id, "pending")
metadata["terminal_record"] = str(
_terminal_path(state_file, run_id, "committed")
)
try:
terminal_file, terminal_record = _write_terminal_record(
state_file,
board=board,
task_id=task_id,
run_id=run_id,
structured=structured,
summary=str(structured.get("summary") or "Completed"),
metadata=metadata,
)
outcome = _finalize_terminal_record(
kanban_db,
terminal_file,
terminal_record,
)
except Exception as error:
identity = _terminal_identity(terminal_file)
replayable = bool(
identity is not None
and _has_pending_finalization(
identity.board,
identity.task_id,
identity.run_id,
)
)
if not replayable:
_recover_exact_run(
kanban_db,
identity,
f"persistence raised {type(error).__name__}",
)
raise TerminalFinalizationPending(
"accepted worker result remains outside the generic block path; "
f"terminal replayable={replayable}; persistence/finalization "
f"raised {type(error).__name__}"
) from error
if outcome != "committed":
raise TerminalFinalizationPending(
"accepted worker result is durably journaled but "
f"Kanban finalization is {outcome}"
)
break
can_continue = (
goal_mode
and completion_problem is not None
and goal_turn < goal_max_turns
and deadline - time.monotonic() > 30
)
if can_continue:
rejection_history = state.get("goal_rejections", [])
if not isinstance(rejection_history, list):
rejection_history = []
state["goal_rejections"] = [
*rejection_history[-4:],
completion_problem,
]
goal_turn += 1
comment(
f"Goal completion rejected; continuing turn {goal_turn}/{goal_max_turns}: {completion_problem}",
)
escalation_context = (
context
+ "\n\nThe previous worker turn missed its completion quality mark: "
+ completion_problem
+ "\nSelect a route that can finish and verify the remaining work."
)
excluded = unavailable_provider
if excluded is None and assignee == "cli-auto":
excluded = fresh_unavailable_provider()
next_route = select_route(
escalation_context,
assignee,
exclude_provider=excluded,
exclude_reason="is unavailable according to fresh native health"
if excluded
else None,
)
comment(
f"Goal route {goal_turn}/{goal_max_turns}: "
f"{next_route.provider}/{next_route.model} at {next_route.effort}; "
f"classifier={next_route.classifier}; {next_route.reason}",
)
handoff = (
git_handoff(workspace, result.output)
if next_route.provider != route.provider
else ""
)
route = next_route
prompt = build_prompt(
context,
workspace,
handoff
+ "\nGoal-loop continuation: the previous final report was rejected because "
+ completion_problem
+ ". Reinspect live state, finish the outstanding work, and return new final evidence.",
)
continue
reason = completion_problem
if reason is None and structured:
blockers = structured.get("blockers", [])
reason = "; ".join(str(item) for item in blockers)
reason = reason or str(structured.get("summary") or "")
if reason is None:
reason = result.output[-4000:]
_board_call(
kanban_db,
board,
lambda fresh: kanban_db.block_task(
fresh,
task_id,
reason=reason
or f"{route.provider} worker failed with exit {result.returncode}",
kind="transient" if result.capacity_failure else "capability",
expected_run_id=run_id,
),
)
break
except TerminalFinalizationPending as error:
comment(str(error))
except Exception as error:
failure_reason = f"Direct CLI lane failed: {type(error).__name__}: {error}"
_board_call(
kanban_db,
board,
lambda fresh: kanban_db.block_task(
fresh,
task_id,
reason=failure_reason,
kind="capability",
expected_run_id=run_id,
),
)
def _board_slug(board: Any) -> str:
if isinstance(board, dict):
return str(board.get("slug") or board.get("id") or "")
return str(getattr(board, "slug", None) or getattr(board, "id", None) or board)
def _external(task: Any) -> bool:
return str(_task_value(task, "assignee", "") or "").startswith(EXTERNAL_PREFIX)
def _connect_healthy_board(kanban_db: Any, board: str) -> Any | None:
"""Open one board without letting localized storage faults stop other lanes."""
try:
return kanban_db.connect(board=board)
except Exception as error:
_record_board_access_error(board, error)
return None
def _record_board_access_error(board: str, error: Exception) -> None:
"""Report one board access failure once while allowing other lanes to run."""
failure_kind = "storage" if isinstance(error, (OSError, sqlite3.Error)) else "access"
detail = f"{failure_kind} {type(error).__name__}: {error}"
if BOARD_CORRUPTION_ERRORS.get(board) == detail:
return
print(
f"temporarily skipping Kanban board {board!r}: {detail}",
file=sys.stderr,
flush=True,
)
BOARD_CORRUPTION_ERRORS[board] = detail
def recover_orphans() -> None:
"""Return external running tasks to ready after a runner/pod restart."""
from hermes_cli import kanban_db
recover_pending_finalizations()
try:
boards = kanban_db.list_boards(include_archived=False)
except Exception as error:
_record_board_access_error("board-registry", error)
return
for raw_board in boards:
board = _board_slug(raw_board)
if not board:
continue
with kanban_db.scoped_current_board(board):
conn = _connect_healthy_board(kanban_db, board)
if conn is None:
continue
try:
for task in kanban_db.list_tasks(conn):
if _external(task) and str(_task_value(task, "status", "")) == "running":
task_id = str(_task_value(task, "id"))
run_id = _task_value(task, "current_run_id", None)
if not isinstance(run_id, int):
continue
if _has_pending_finalization(board, task_id, run_id):
continue
kanban_db.reclaim_task(
conn,
task_id,
reason="direct CLI lane restarted; provider session will resume",
expected_run_id=run_id,
)
BOARD_CORRUPTION_ERRORS.pop(board, None)
except Exception as error:
_record_board_access_error(board, error)
finally:
conn.close()
def claim_ready(active: set[tuple[str, str]], limit: int) -> list[tuple[str, str]]:
"""Atomically claim external ready tasks across all non-archived boards."""
from hermes_cli import kanban_db
claimed: list[tuple[str, str]] = []
if limit <= 0:
return claimed
for raw_board in kanban_db.list_boards(include_archived=False):
board = _board_slug(raw_board)
if not board:
continue
with kanban_db.scoped_current_board(board):
conn = _connect_healthy_board(kanban_db, board)
if conn is None:
continue
try:
kanban_db.recompute_ready(conn)
tasks = kanban_db.list_tasks(conn)
BOARD_CORRUPTION_ERRORS.pop(board, None)
for task in tasks:
task_id = str(_task_value(task, "id", ""))
assignee = str(_task_value(task, "assignee", "") or "")
if task_id and not assignee and str(_task_value(task, "status", "")) == "ready":
if kanban_db.assign_task(conn, task_id, "cli-auto"):
task = kanban_db.get_task(conn, task_id)
assignee = "cli-auto"
if (
not task_id
or (board, task_id) in active
or not assignee.startswith(EXTERNAL_PREFIX)
or str(_task_value(task, "status", "")) != "ready"
):
continue
try:
result = kanban_db.claim_task(
conn,
task_id,
ttl_seconds=DEFAULT_CLAIM_TTL,
claimer="direct-cli-lane",
)
except Exception:
continue
if result is not None:
claimed.append((board, task_id))
if len(claimed) >= limit:
return claimed
except Exception as error:
_record_board_access_error(board, error)
finally:
conn.close()
return claimed
def main() -> int:
"""Continuously bridge external Kanban lanes to provider CLIs."""
RESULT_SCHEMA_PATH.parent.mkdir(parents=True, exist_ok=True)
atomic_json(RESULT_SCHEMA_PATH, RESULT_SCHEMA, 0o644)
recover_orphans()
workers = max(1, min(int(os.environ.get("HERMES_CLI_LANE_CONCURRENCY", "4")), 8))
futures: dict[concurrent.futures.Future[None], tuple[str, str]] = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
while True:
for future in list(futures):
if future.done():
try:
future.result()
except Exception as error:
print(f"worker future failed: {error}", file=sys.stderr, flush=True)
del futures[future]
recover_pending_finalizations()
maybe_gc_lane_artifacts()
active = set(futures.values())
try:
newly_claimed = claim_ready(active, workers - len(futures))
BOARD_CORRUPTION_ERRORS.pop("board-registry", None)
except Exception as error:
_record_board_access_error("board-registry", error)
newly_claimed = []
for board, task_id in newly_claimed:
future = pool.submit(execute_claim, board, task_id)
futures[future] = (board, task_id)
time.sleep(5)
return 0
if __name__ == "__main__":
raise SystemExit(main())