Wl6adams's picture
Organize private Nexum release into Lite, Universal, and Expanded profiles
9a70a84
Raw
History Blame Contribute Delete
10 kB
"""Durable task lifecycle for long-running tools and agent operations."""
from __future__ import annotations
import fcntl
import hashlib
import json
import os
import re
import secrets
import signal
import subprocess
import sys
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Literal
from .events import EventLog
from .sandbox import control_root, ensure_control_root, sandbox_environment
TaskStatus = Literal["working", "input_required", "completed", "failed", "cancelled"]
@dataclass(frozen=True)
class TaskRecord:
task_id: str
kind: str
status: TaskStatus
session_id_sha256: str
request_sha256: str
created_unix_ms: int
updated_unix_ms: int
status_message: str = ""
progress: dict[str, Any] | None = None
result: dict[str, Any] | None = None
error: str = ""
process_id: int = 0
def to_dict(self) -> dict[str, Any]:
return asdict(self)
class TaskStore:
def __init__(self, workspace: str | Path) -> None:
self.workspace = ensure_control_root(workspace)
root = control_root(self.workspace) / "tasks"
root.mkdir(parents=True, exist_ok=True, mode=0o700)
os.chmod(root, 0o700)
self.root = root
def _path(self, task_id: str) -> Path:
if not re.fullmatch(r"task_[a-f0-9]{32}", task_id):
raise ValueError("task id is invalid")
return self.root / f"{task_id}.json"
@staticmethod
def _write(path: Path, record: TaskRecord) -> None:
temporary = path.with_name(f".{path.name}.{secrets.token_hex(8)}.tmp")
temporary.write_text(
json.dumps(record.to_dict(), sort_keys=True, separators=(",", ":")),
encoding="utf-8",
)
temporary.chmod(0o600)
with temporary.open("rb") as handle:
os.fsync(handle.fileno())
os.replace(temporary, path)
directory_fd = os.open(path.parent, os.O_RDONLY)
try:
os.fsync(directory_fd)
finally:
os.close(directory_fd)
@staticmethod
def _load(path: Path) -> TaskRecord:
payload = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise RuntimeError("task record is invalid")
return TaskRecord(**payload)
def create(
self,
*,
kind: str,
session_id: str,
request: dict[str, Any],
process_id: int = 0,
status_message: str = "",
) -> TaskRecord:
canonical = json.dumps(
request, sort_keys=True, default=str, separators=(",", ":")
).encode("utf-8")
now = int(time.time() * 1000)
record = TaskRecord(
task_id="task_" + secrets.token_hex(16),
kind=kind,
status="working",
session_id_sha256=hashlib.sha256(session_id.encode("utf-8")).hexdigest(),
request_sha256=hashlib.sha256(canonical).hexdigest(),
created_unix_ms=now,
updated_unix_ms=now,
process_id=process_id,
status_message=status_message,
)
self._write(self._path(record.task_id), record)
return record
def get(self, task_id: str) -> TaskRecord:
return self._load(self._path(task_id))
@staticmethod
def _session_sha256(session_id: str) -> str:
return hashlib.sha256(session_id.encode("utf-8")).hexdigest()
@classmethod
def _require_session(cls, record: TaskRecord, session_id: str) -> TaskRecord:
if session_id and not secrets.compare_digest(
record.session_id_sha256, cls._session_sha256(session_id)
):
raise PermissionError("task does not belong to this session")
return record
def status(self, task_id: str, *, session_id: str = "") -> TaskRecord:
record = self.get(task_id)
self._require_session(record, session_id)
if record.status != "working" or record.process_id <= 0:
return record
try:
os.kill(record.process_id, 0)
return record
except ProcessLookupError:
failed = self.update(
task_id,
status="failed",
status_message="execution worker exited without a final receipt",
error="execution worker unavailable",
process_id=0,
)
EventLog(self.workspace).append(
"task_failed",
session_id_sha256=failed.session_id_sha256,
task_id=failed.task_id,
status="failed",
detail={"reason": "worker_unavailable"},
)
return failed
def update(
self,
task_id: str,
*,
status: TaskStatus | None = None,
status_message: str | None = None,
progress: dict[str, Any] | None = None,
result: dict[str, Any] | None = None,
error: str | None = None,
process_id: int | None = None,
) -> TaskRecord:
path = self._path(task_id)
lock_path = path.with_suffix(".lock")
with lock_path.open("a+b") as lock_handle:
lock_path.chmod(0o600)
fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX)
try:
current = self._load(path)
if current.status in {"completed", "failed", "cancelled"}:
if status is None or status != current.status:
return current
payload = current.to_dict()
if status is not None:
payload["status"] = status
if status_message is not None:
payload["status_message"] = status_message
if progress is not None:
payload["progress"] = progress
if result is not None:
payload["result"] = result
if error is not None:
payload["error"] = error
if process_id is not None:
payload["process_id"] = process_id
payload["updated_unix_ms"] = int(time.time() * 1000)
updated = TaskRecord(**payload)
self._write(path, updated)
return updated
finally:
fcntl.flock(lock_handle.fileno(), fcntl.LOCK_UN)
def list(self, *, session_id: str = "") -> tuple[TaskRecord, ...]:
session_hash = (
hashlib.sha256(session_id.encode("utf-8")).hexdigest()
if session_id
else ""
)
records: list[TaskRecord] = []
for path in sorted(self.root.glob("task_*.json")):
try:
record = self._load(path)
except (OSError, TypeError, ValueError, RuntimeError):
continue
if session_hash and record.session_id_sha256 != session_hash:
continue
record = self.status(record.task_id)
records.append(record)
return tuple(records)
def start_terminal(
self,
*,
session_id: str,
command: str,
workspace: str | Path,
) -> TaskRecord:
"""Start a durable terminal task without imposing a host time limit."""
workspace_root = ensure_control_root(workspace)
if workspace_root != self.workspace:
raise ValueError("task workspace does not match its state store")
if not command.strip():
raise ValueError("terminal task command is required")
record = self.create(
kind="terminal",
session_id=session_id,
request={"command": command, "workspace": str(workspace_root)},
status_message="starting",
)
source_root = str(Path(__file__).resolve().parents[2])
environment = sandbox_environment(pythonpath=source_root)
environment["NEXUM_HOME"] = str(control_root(self.workspace))
process = subprocess.Popen(
[
sys.executable,
"-m",
"nexum_runtime.tooling.task_worker",
"--task-root",
str(self.root),
"--task-id",
record.task_id,
"--workspace",
str(workspace_root),
"--command",
command,
],
cwd=workspace_root,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
close_fds=True,
env=environment,
)
updated = self.update(
record.task_id,
process_id=process.pid,
status_message="running",
)
EventLog(workspace_root).append(
"task_started",
session_id=session_id,
task_id=updated.task_id,
status=updated.status,
detail={"kind": updated.kind, "process_id": updated.process_id},
)
return updated
def cancel(self, task_id: str, *, session_id: str = "") -> TaskRecord:
"""Cancel a running task and retain its durable state for inspection."""
record = self.status(task_id, session_id=session_id)
if record.status != "working":
return record
updated = self.update(
task_id,
status="cancelled",
status_message="cancelled by caller",
)
if record.process_id > 0:
try:
os.killpg(record.process_id, signal.SIGTERM)
except ProcessLookupError:
pass
EventLog(self.workspace).append(
"task_cancelled",
session_id_sha256=record.session_id_sha256,
task_id=record.task_id,
status="cancelled",
)
return updated
__all__ = ["TaskRecord", "TaskStatus", "TaskStore"]