mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-07 21:52:20 +01:00
feat(runtime): claim effects before dispatch and gate completion on them
Dispatcher seam: mark_dispatch, which runs inside the live Wave 3 binding scope immediately before backend invocation, now durably claims a possible effect before execution_id is assigned. If the claim cannot be persisted the action stays undispatched and the dispatcher returns BLOCKED; dispatched() closes the never-awaited coroutine. record_action appends the outcome (including cancellation/interruption) and admitted-read observations before the receipt reduction drops producer facts. Adapters consume only the bound operations the dispatcher admitted: filesystem bindings give exact scope and predicates (write_file content digest after fence unwrapping, apply_patch add/delete, edit existence); bash/python launches have unknown scope with the launch generation as lineage; job kills scope the exact job and its processes; owned operations scope their exact revisioned records; external backends are claimed as external and never verified by acknowledgement; browser session_info yields session lifecycle observations only, and a page binding is never effect scope. Complete read_file re-reads the exact bound source to digest it; offset/limit, truncation, extraction and listings are partial. Background launches stay RUNNING until an admitted read of the exact job generation (via a durable launch index, across continuation runs) reports settlement. Producer seams: typed job lifecycle facts on manage_bg_jobs reads/kills, a structured timed_out flag on containment timeouts, and mutation_attempted on in-place write_file/edit_file failures after truncation. Completion: the existing EvidenceLedger consumes effect assessments through a single helper used for the decision, ask_user and prose filtering. A required artifact is unsettled by a later unresolved effect that may have touched it, a fresh contradicting readback fails the decision, and partial reads no longer count as artifact validation. Ordinary conversation and read-only turns are unchanged; no second completion policy is introduced.
This commit is contained in:
@@ -17,6 +17,7 @@ from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import threading
|
||||
import weakref
|
||||
from typing import Any
|
||||
|
||||
from src.constants import DATA_DIR
|
||||
@@ -41,11 +42,17 @@ def effects_dir() -> Path:
|
||||
|
||||
|
||||
class EffectLog:
|
||||
# Logs still owned by a live run in this process. A later turn appends to
|
||||
# the same object rather than a second copy with its own sequence counter.
|
||||
_LIVE: "weakref.WeakValueDictionary[tuple[str, str], EffectLog]" = weakref.WeakValueDictionary()
|
||||
|
||||
def __init__(self, run_id: str, *, durable: bool = True, directory: str | os.PathLike | None = None) -> None:
|
||||
if not isinstance(run_id, str) or not _RUN_ID.fullmatch(run_id):
|
||||
raise ValueError("Effect log requires a server-generated run identifier")
|
||||
self.run_id = run_id
|
||||
self.path = (Path(directory) if directory is not None else effects_dir()) / f"{run_id}.jsonl" if durable else None
|
||||
if self.path is not None:
|
||||
self._LIVE[(str(self.path.parent), run_id)] = self
|
||||
self._claims: list[EffectClaim] = []
|
||||
self._outcomes: list[EffectOutcome] = []
|
||||
self._observations: list[Observation] = []
|
||||
@@ -167,6 +174,60 @@ class EffectLog:
|
||||
default=0)
|
||||
return log
|
||||
|
||||
@classmethod
|
||||
def open(cls, run_id: str, *, directory: str | os.PathLike | None = None) -> "EffectLog":
|
||||
"""The live log for a run, or its replayed durable history.
|
||||
|
||||
A log that is not live belongs to a finished or crashed run, so its
|
||||
unsettled claims are recovered as interrupted before any append.
|
||||
"""
|
||||
base = Path(directory) if directory is not None else effects_dir()
|
||||
live = cls._LIVE.get((str(base), run_id))
|
||||
if live is not None:
|
||||
return live
|
||||
log = cls.load(run_id, directory=base)
|
||||
log.recover_interrupted()
|
||||
return log
|
||||
|
||||
# -- background launch lineage ------------------------------------------
|
||||
|
||||
def index_launch(self, generation: str, effect_id: str) -> None:
|
||||
"""Durably map an exact Wave 3 launch generation to its claim."""
|
||||
if self.path is None:
|
||||
return
|
||||
if not _RUN_ID.fullmatch(generation or ""):
|
||||
raise ValueError("Malformed launch generation")
|
||||
target = self.path.parent / f"launch-{generation}.json"
|
||||
temporary = target.with_suffix(".tmp")
|
||||
data = json.dumps({"run_id": self.run_id, "effect_id": effect_id}, sort_keys=True).encode()
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_NOFOLLOW", 0)
|
||||
descriptor = os.open(temporary, flags, 0o600)
|
||||
try:
|
||||
os.write(descriptor, data)
|
||||
os.fsync(descriptor)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
os.replace(temporary, target)
|
||||
|
||||
@staticmethod
|
||||
def launch_owner(generation: str, *, directory: str | os.PathLike | None = None) -> tuple[str, str] | None:
|
||||
if not _RUN_ID.fullmatch(generation or ""):
|
||||
return None
|
||||
base = Path(directory) if directory is not None else effects_dir()
|
||||
try:
|
||||
descriptor = os.open(base / f"launch-{generation}.json", os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
||||
with os.fdopen(descriptor, "rb") as stream:
|
||||
if os.fstat(stream.fileno()).st_nlink != 1:
|
||||
return None
|
||||
value = json.loads(stream.read(4096))
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
if (not isinstance(value, dict) or set(value) != {"run_id", "effect_id"}
|
||||
or not isinstance(value["run_id"], str) or not _RUN_ID.fullmatch(value["run_id"])
|
||||
or not isinstance(value["effect_id"], str)):
|
||||
return None
|
||||
return value["run_id"], value["effect_id"]
|
||||
|
||||
def recover_interrupted(self) -> tuple[EffectOutcome, ...]:
|
||||
"""Append INTERRUPTED outcomes for claims that never settled."""
|
||||
with self._lock:
|
||||
|
||||
Reference in New Issue
Block a user