sts2-bot/recording.py
0xrsydn 9696282110 feat(recording): link session observations proposals and action results
Capture all successful state reads with hashes and session-local IDs. Record action intent before POST, retain accepted/rejected/unknown results, and link subsequent observations. Preserve legacy feeds and finalize each invocation synchronously.
2026-09-22 15:53:14 +07:00

138 lines
6.4 KiB
Python

"""Session-local journal for observations and runner-supplied action calls."""
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import time
from datetime import datetime, timezone
import uuid
def timestamp() -> str:
return datetime.now(timezone.utc).isoformat()
class SessionRecorder:
"""A journal is authoritative; root JSONL files are compatibility feeds.
An attempt records intent before the POST. Neither acceptance nor a later
observation proves that the expected game effect occurred.
"""
def __init__(self, root: str | Path, *, dry_run: bool = False):
self.root = Path(root)
self.session_id = f"{time.strftime('%Y%m%dT%H%M%S')}-{uuid.uuid4().hex}"
self.directory = self.root / "sessions" / self.session_id
self.directory.mkdir(parents=True, exist_ok=False)
(self.directory / "observations").mkdir()
self.events = self.directory / "events.jsonl"
self.started = timestamp()
self.dry_run = dry_run
self.step = 0
self.sources = {"code": 0, "jev": 0, "fallback": 0}
self.exit_code = 1
self.stop_reason = "interrupted"
self.summary: dict = {}
self.sequence = self.observations = self.proposals = self.attempts = 0
self.observation_id: str | None = None
self.awaiting_observation: str | None = None
@staticmethod
def _append(path: Path, row: dict, *, durable: bool = False) -> None:
line = json.dumps(row, sort_keys=True, allow_nan=False) + "\n"
with path.open("a", encoding="utf-8") as stream:
stream.write(line)
if durable:
stream.flush()
os.fsync(stream.fileno())
def write(self, record: dict, *, mirror: bool = True, durable: bool = False) -> dict:
self.sequence += 1
row = {**record, "schema_version": 1, "session": self.session_id,
"sequence": self.sequence, "recorded_at": timestamp(),
"ts": time.strftime("%H:%M:%S")}
self._append(self.events, row, durable=durable)
if mirror:
self._append(self.root / "decisions.jsonl", row)
return row
def observe(self, observation: dict, *, step: int, phase: str) -> str:
self.step = step
self.observations += 1
observation_id = f"{self.session_id}:o{self.observations:06d}"
path = self.directory / "observations" / f"{self.observations:06d}.json"
data = (json.dumps(observation, sort_keys=True, allow_nan=False) + "\n").encode("utf-8")
with path.open("xb") as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
self.observation_id = observation_id
self.write({"event": "observation", "observation_id": observation_id,
"step": step, "phase": phase, "state_type": observation.get("state_type"),
"path": str(path.relative_to(self.root)), "sha256": hashlib.sha256(data).hexdigest(),
"after_attempt_id": self.awaiting_observation}, mirror=False)
self.awaiting_observation = None
return observation_id
def propose(self, record: dict) -> str:
self.proposals += 1
proposal_id = f"{self.session_id}:p{self.proposals:06d}"
self.write({**record, "event": "proposal", "proposal_id": proposal_id,
"observation_id": self.observation_id}, mirror=False)
return proposal_id
def disposition(self, proposal_id: str, status: str) -> None:
self.write({"event": "proposal_status", "proposal_id": proposal_id,
"status": status, "step": self.step}, mirror=False)
def execute(self, proposal_id: str, decision, act):
"""Flush intent before invoking the transport. Never retry here."""
if self.dry_run:
raise RuntimeError("cannot execute actions in a dry-run recording")
self.attempts += 1
attempt_id = f"{self.session_id}:a{self.attempts:06d}"
self.disposition(proposal_id, "execute")
self.write({"event": "action_attempt", "attempt_id": attempt_id,
"proposal_id": proposal_id, "observation_id": self.observation_id,
"step": self.step, "action": decision.action, "params": decision.params},
mirror=False, durable=True)
self.awaiting_observation = attempt_id
try:
result = act(decision.action, **decision.params)
except BaseException as exc:
self.write({"event": "action_result", "attempt_id": attempt_id,
"step": self.step, "outcome": "unknown",
"error": f"{type(exc).__name__}: {str(exc)[:200]}"}, mirror=False)
raise
self.write({"event": "action_result", "attempt_id": attempt_id, "step": self.step,
"outcome": "accepted" if result.ok else "rejected",
"message": result.message, "response": getattr(result, "raw", None)}, mirror=False)
return result
def finish(self, code: int, reason: str) -> int:
self.exit_code, self.stop_reason = code, reason
return code
def __enter__(self):
self.write({"event": "session_start", "dry_run": self.dry_run}, mirror=False)
return self
def __exit__(self, exc_type, exc, traceback):
if exc is not None:
self.exit_code = 130 if isinstance(exc, KeyboardInterrupt) else 1
self.stop_reason = "interrupted" if isinstance(exc, KeyboardInterrupt) else "exception"
self.summary["error"] = f"{type(exc).__name__}: {str(exc)[:200]}"
row = {**self.summary, "schema_version": 1, "session": self.session_id,
"started": self.started, "ended": timestamp(), "steps": self.step,
"sources": self.sources, "dry_run": self.dry_run,
"exit_code": self.exit_code, "stop_reason": self.stop_reason,
"recording_path": str(self.events.relative_to(self.root)),
"awaiting_observation_after_attempt": self.awaiting_observation}
self.write({**row, "event": "session_end", "step": self.step, "reason": self.stop_reason}, durable=True)
with (self.directory / "session.json").open("x", encoding="utf-8") as stream:
json.dump(row, stream, sort_keys=True, allow_nan=False)
stream.write("\n")
self._append(self.root / "sessions.jsonl", row)
return False